mirror of
				https://source.quilibrium.com/quilibrium/ceremonyclient.git
				synced 2025-11-04 00:27:41 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			214 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			214 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package libp2pwebtransport
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/sha256"
 | 
						|
	"crypto/tls"
 | 
						|
	"encoding/binary"
 | 
						|
	"fmt"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/benbjohnson/clock"
 | 
						|
	ic "github.com/libp2p/go-libp2p/core/crypto"
 | 
						|
	ma "github.com/multiformats/go-multiaddr"
 | 
						|
	"github.com/multiformats/go-multihash"
 | 
						|
)
 | 
						|
 | 
						|
// Allow for a bit of clock skew.
 | 
						|
// When we generate a certificate, the NotBefore time is set to clockSkewAllowance before the current time.
 | 
						|
// Similarly, we stop using a certificate one clockSkewAllowance before its expiry time.
 | 
						|
const clockSkewAllowance = time.Hour
 | 
						|
const validityMinusTwoSkew = certValidity - (2 * clockSkewAllowance)
 | 
						|
 | 
						|
type certConfig struct {
 | 
						|
	tlsConf *tls.Config
 | 
						|
	sha256  [32]byte // cached from the tlsConf
 | 
						|
}
 | 
						|
 | 
						|
func (c *certConfig) Start() time.Time { return c.tlsConf.Certificates[0].Leaf.NotBefore }
 | 
						|
func (c *certConfig) End() time.Time   { return c.tlsConf.Certificates[0].Leaf.NotAfter }
 | 
						|
 | 
						|
func newCertConfig(key ic.PrivKey, start, end time.Time) (*certConfig, error) {
 | 
						|
	conf, err := getTLSConf(key, start, end)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return &certConfig{
 | 
						|
		tlsConf: conf,
 | 
						|
		sha256:  sha256.Sum256(conf.Certificates[0].Leaf.Raw),
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// Certificate renewal logic:
 | 
						|
//  1. On startup, we generate one cert that is valid from now (-1h, to allow for clock skew), and another
 | 
						|
//     cert that is valid from the expiry date of the first certificate (again, with allowance for clock skew).
 | 
						|
//  2. Once we reach 1h before expiry of the first certificate, we switch over to the second certificate.
 | 
						|
//     At the same time, we stop advertising the certhash of the first cert and generate the next cert.
 | 
						|
type certManager struct {
 | 
						|
	clock     clock.Clock
 | 
						|
	ctx       context.Context
 | 
						|
	ctxCancel context.CancelFunc
 | 
						|
	refCount  sync.WaitGroup
 | 
						|
 | 
						|
	mx            sync.RWMutex
 | 
						|
	lastConfig    *certConfig // initially nil
 | 
						|
	currentConfig *certConfig
 | 
						|
	nextConfig    *certConfig // nil until we have passed half the certValidity of the current config
 | 
						|
	addrComp      ma.Multiaddr
 | 
						|
 | 
						|
	serializedCertHashes [][]byte
 | 
						|
}
 | 
						|
 | 
						|
func newCertManager(hostKey ic.PrivKey, clock clock.Clock) (*certManager, error) {
 | 
						|
	m := &certManager{clock: clock}
 | 
						|
	m.ctx, m.ctxCancel = context.WithCancel(context.Background())
 | 
						|
	if err := m.init(hostKey); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	m.background(hostKey)
 | 
						|
	return m, nil
 | 
						|
}
 | 
						|
 | 
						|
// getCurrentTimeBucket returns the canonical start time of the given time as
 | 
						|
// bucketed by ranges of certValidity since unix epoch (plus an offset). This
 | 
						|
// lets you get the same time ranges across reboots without having to persist
 | 
						|
// state.
 | 
						|
// ```
 | 
						|
// ... v--- epoch + offset
 | 
						|
// ... |--------|    |--------|        ...
 | 
						|
// ...        |--------|    |--------| ...
 | 
						|
// ```
 | 
						|
func getCurrentBucketStartTime(now time.Time, offset time.Duration) time.Time {
 | 
						|
	currentBucket := (now.UnixMilli() - offset.Milliseconds()) / validityMinusTwoSkew.Milliseconds()
 | 
						|
	return time.UnixMilli(offset.Milliseconds() + currentBucket*validityMinusTwoSkew.Milliseconds())
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) init(hostKey ic.PrivKey) error {
 | 
						|
	start := m.clock.Now()
 | 
						|
	pubkeyBytes, err := hostKey.GetPublic().Raw()
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// We want to add a random offset to each start time so that not all certs
 | 
						|
	// rotate at the same time across the network. The offset represents moving
 | 
						|
	// the bucket start time some `offset` earlier.
 | 
						|
	offset := (time.Duration(binary.LittleEndian.Uint16(pubkeyBytes)) * time.Minute) % certValidity
 | 
						|
 | 
						|
	// We want the certificate have been valid for at least one clockSkewAllowance
 | 
						|
	start = start.Add(-clockSkewAllowance)
 | 
						|
	startTime := getCurrentBucketStartTime(start, offset)
 | 
						|
	m.nextConfig, err = newCertConfig(hostKey, startTime, startTime.Add(certValidity))
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return m.rollConfig(hostKey)
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) rollConfig(hostKey ic.PrivKey) error {
 | 
						|
	// We stop using the current certificate clockSkewAllowance before its expiry time.
 | 
						|
	// At this point, the next certificate needs to be valid for one clockSkewAllowance.
 | 
						|
	nextStart := m.nextConfig.End().Add(-2 * clockSkewAllowance)
 | 
						|
	c, err := newCertConfig(hostKey, nextStart, nextStart.Add(certValidity))
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	m.lastConfig = m.currentConfig
 | 
						|
	m.currentConfig = m.nextConfig
 | 
						|
	m.nextConfig = c
 | 
						|
	if err := m.cacheSerializedCertHashes(); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return m.cacheAddrComponent()
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) background(hostKey ic.PrivKey) {
 | 
						|
	d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(m.clock.Now())
 | 
						|
	log.Debugw("setting timer", "duration", d.String())
 | 
						|
	t := m.clock.Timer(d)
 | 
						|
	m.refCount.Add(1)
 | 
						|
 | 
						|
	go func() {
 | 
						|
		defer m.refCount.Done()
 | 
						|
		defer t.Stop()
 | 
						|
 | 
						|
		for {
 | 
						|
			select {
 | 
						|
			case <-m.ctx.Done():
 | 
						|
				return
 | 
						|
			case <-t.C:
 | 
						|
				now := m.clock.Now()
 | 
						|
				m.mx.Lock()
 | 
						|
				if err := m.rollConfig(hostKey); err != nil {
 | 
						|
					log.Errorw("rolling config failed", "error", err)
 | 
						|
				}
 | 
						|
				d := m.currentConfig.End().Add(-clockSkewAllowance).Sub(now)
 | 
						|
				log.Debugw("rolling certificates", "next", d.String())
 | 
						|
				t.Reset(d)
 | 
						|
				m.mx.Unlock()
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}()
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) GetConfig() *tls.Config {
 | 
						|
	m.mx.RLock()
 | 
						|
	defer m.mx.RUnlock()
 | 
						|
	return m.currentConfig.tlsConf
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) AddrComponent() ma.Multiaddr {
 | 
						|
	m.mx.RLock()
 | 
						|
	defer m.mx.RUnlock()
 | 
						|
	return m.addrComp
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) SerializedCertHashes() [][]byte {
 | 
						|
	return m.serializedCertHashes
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) cacheSerializedCertHashes() error {
 | 
						|
	hashes := make([][32]byte, 0, 3)
 | 
						|
	if m.lastConfig != nil {
 | 
						|
		hashes = append(hashes, m.lastConfig.sha256)
 | 
						|
	}
 | 
						|
	hashes = append(hashes, m.currentConfig.sha256)
 | 
						|
	if m.nextConfig != nil {
 | 
						|
		hashes = append(hashes, m.nextConfig.sha256)
 | 
						|
	}
 | 
						|
 | 
						|
	m.serializedCertHashes = m.serializedCertHashes[:0]
 | 
						|
	for _, certHash := range hashes {
 | 
						|
		h, err := multihash.Encode(certHash[:], multihash.SHA2_256)
 | 
						|
		if err != nil {
 | 
						|
			return fmt.Errorf("failed to encode certificate hash: %w", err)
 | 
						|
		}
 | 
						|
		m.serializedCertHashes = append(m.serializedCertHashes, h)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) cacheAddrComponent() error {
 | 
						|
	addr, err := addrComponentForCert(m.currentConfig.sha256[:])
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if m.nextConfig != nil {
 | 
						|
		comp, err := addrComponentForCert(m.nextConfig.sha256[:])
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		addr = addr.Encapsulate(comp)
 | 
						|
	}
 | 
						|
	m.addrComp = addr
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (m *certManager) Close() error {
 | 
						|
	m.ctxCancel()
 | 
						|
	m.refCount.Wait()
 | 
						|
	return nil
 | 
						|
}
 |