ceremonyclient/go-libp2p/p2p/transport/webtransport/cert_manager.go
2023-08-20 23:07:43 -05:00

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
}