~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
160 lines · 3.6 KB Raw History
package tlsreload

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"log/slog"
	"sync"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
)

var ErrNoServerName = errors.New("tlsreload: the connection names no server to verify")

func TrustPool(ctx context.Context, trust vault.Trust, log *slog.Logger) (*Pool, func()) {
	p := &Pool{log: logOrDiscard(log)}
	return p, p.Follow(ctx, trust)
}

func EmptyPool(log *slog.Logger) *Pool {
	return &Pool{log: logOrDiscard(log)}
}

func SecretPool(read func(ctx context.Context) ([]byte, error), renew time.Duration, log *slog.Logger) *Pool {
	return NewPool(func(ctx context.Context) (*x509.CertPool, time.Time, error) {
		data, err := read(ctx)
		if err != nil {
			return nil, time.Time{}, err
		}
		pool, err := parsePool(data)
		if err != nil {
			return nil, time.Time{}, err
		}
		return pool, time.Now().Add(renew), nil
	}, log)
}

func (p *Pool) Follow(ctx context.Context, trust vault.Trust) func() {
	if trust == nil {
		return func() {}
	}
	cancel := trust.Watch(func(bundle []byte) { p.feed(bundle) })
	if bundle, err := trust.CABundle(ctx); err != nil {
		p.log.Warn("no CA bundle is published yet; the pool fills when it is", "err", err)
	} else {
		p.feed(bundle)
	}
	return cancel
}

func (p *Pool) feed(bundle []byte) {
	if len(bundle) == 0 {
		return
	}
	if err := p.Set(bundle); err != nil {
		p.log.Warn("keeping the last CA pool", "err", err)
		return
	}
	p.log.Info("loaded the published CA pool")
}

func (p *Pool) ClientConfig(base *tls.Config) *tls.Config {
	return p.clientConfig(base, false)
}

func (p *Pool) PublicClientConfig(base *tls.Config) *tls.Config {
	return p.clientConfig(base, true)
}

func (p *Pool) clientConfig(base *tls.Config, system bool) *tls.Config {
	cfg := &tls.Config{MinVersion: tls.VersionTLS12}
	if base != nil {
		cfg = base.Clone()
	}
	cfg.RootCAs = nil
	cfg.InsecureSkipVerify = true
	inner := cfg.VerifyConnection
	cfg.VerifyConnection = func(cs tls.ConnectionState) error {
		if err := p.verify(cs, system); err != nil {
			return err
		}
		if inner != nil {
			return inner(cs)
		}
		return nil
	}
	return cfg
}

func (p *Pool) verify(cs tls.ConnectionState, system bool) error {
	if len(cs.PeerCertificates) == 0 {
		return errors.New("tlsreload: the peer sent no certificate")
	}
	if cs.ServerName == "" {
		return ErrNoServerName
	}
	intermediates := x509.NewCertPool()
	for _, cert := range cs.PeerCertificates[1:] {
		intermediates.AddCert(cert)
	}
	opts := x509.VerifyOptions{DNSName: cs.ServerName, Intermediates: intermediates}
	if system {
		if _, err := cs.PeerCertificates[0].Verify(opts); err == nil {
			return nil
		}
	}
	roots, err := p.Current(context.Background())
	if err != nil {
		return err
	}
	opts.Roots = roots
	_, err = cs.PeerCertificates[0].Verify(opts)
	return err
}

type Pools struct {
	mu sync.Mutex

	entries map[string]poolEntry
}

type poolEntry struct {
	fingerprint string

	pool *Pool

	stop func()
}

func (ps *Pools) Get(name, fingerprint string, open func() (*Pool, func(), error)) (*Pool, error) {
	ps.mu.Lock()
	defer ps.mu.Unlock()
	if e, ok := ps.entries[name]; ok && e.fingerprint == fingerprint {
		return e.pool, nil
	}
	pool, stop, err := open()
	if err != nil {
		return nil, err
	}
	if ps.entries == nil {
		ps.entries = map[string]poolEntry{}
	}
	if e, ok := ps.entries[name]; ok && e.stop != nil {
		e.stop()
	}
	ps.entries[name] = poolEntry{fingerprint: fingerprint, pool: pool, stop: stop}
	return pool, nil
}

func (ps *Pools) Drop(name string) {
	ps.mu.Lock()
	e, ok := ps.entries[name]
	delete(ps.entries, name)
	ps.mu.Unlock()
	if ok && e.stop != nil {
		e.stop()
	}
}