1
0
Fork 0

OCSP stapling

This commit is contained in:
Alessandro Chitolina 2025-06-06 17:44:04 +02:00 committed by GitHub
parent 2949995abc
commit b39ee8ede5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1576 additions and 178 deletions

View file

@ -6,7 +6,9 @@ import (
"crypto/x509"
"errors"
"fmt"
"hash/fnv"
"slices"
"strconv"
"strings"
"sync"
@ -43,6 +45,11 @@ func getCipherSuites() []string {
return ciphers
}
// OCSPConfig contains the OCSP configuration.
type OCSPConfig struct {
ResponderOverrides map[string]string `description:"Defines a map of OCSP responders to replace for querying OCSP servers." json:"responderOverrides,omitempty" toml:"responderOverrides,omitempty" yaml:"responderOverrides,omitempty"`
}
// Manager is the TLS option/store/configuration factory.
type Manager struct {
lock sync.RWMutex
@ -50,16 +57,33 @@ type Manager struct {
stores map[string]*CertificateStore
configs map[string]Options
certs []*CertAndStores
// As of today, the TLS manager contains and is responsible for creating/starting the OCSP ocspStapler.
// It would likely have been a Configuration listener but this implies that certs are re-parsed.
// But this would probably have impact on resource consumption.
ocspStapler *ocspStapler
}
// NewManager creates a new Manager.
func NewManager() *Manager {
return &Manager{
func NewManager(ocspConfig *OCSPConfig) *Manager {
manager := &Manager{
stores: map[string]*CertificateStore{},
configs: map[string]Options{
"default": DefaultTLSOptions,
},
}
if ocspConfig != nil {
manager.ocspStapler = newOCSPStapler(ocspConfig.ResponderOverrides)
}
return manager
}
func (m *Manager) Run(ctx context.Context) {
if m.ocspStapler != nil {
m.ocspStapler.Run(ctx)
}
}
// UpdateConfigs updates the TLS* configuration options.
@ -91,7 +115,14 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
m.storesConfig[tlsalpn01.ACMETLS1Protocol] = Store{}
}
storesCertificates := make(map[string]map[string]*tls.Certificate)
storesCertificates := make(map[string]map[string]*CertificateData)
// Define the TTL for all the cache entries with no TTL.
// This will discard entries that are not used anymore.
if m.ocspStapler != nil {
m.ocspStapler.ResetTTL()
}
for _, conf := range certs {
if len(conf.Stores) == 0 {
log.Ctx(ctx).Debug().MsgFunc(func() string {
@ -101,24 +132,49 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
conf.Stores = []string{DefaultTLSStoreName}
}
for _, store := range conf.Stores {
logger := log.Ctx(ctx).With().Str(logs.TLSStoreName, store).Logger()
cert, SANs, err := parseCertificate(&conf.Certificate)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msgf("Unable to parse certificate %s", conf.Certificate.GetTruncatedCertificateName())
continue
}
var certHash string
if m.ocspStapler != nil && len(cert.Leaf.OCSPServer) > 0 {
certHash = hashRawCert(cert.Leaf.Raw)
issuer := cert.Leaf
if len(cert.Certificate) > 1 {
issuer, err = x509.ParseCertificate(cert.Certificate[1])
if err != nil {
log.Ctx(ctx).Error().Err(err).Msgf("Unable to parse issuer certificate %s", conf.Certificate.GetTruncatedCertificateName())
continue
}
}
if err := m.ocspStapler.Upsert(certHash, cert.Leaf, issuer); err != nil {
log.Ctx(ctx).Error().Err(err).Msgf("Unable to upsert OCSP certificate %s", conf.Certificate.GetTruncatedCertificateName())
continue
}
}
certData := &CertificateData{
Certificate: &cert,
Hash: certHash,
}
for _, store := range conf.Stores {
if _, ok := m.storesConfig[store]; !ok {
m.storesConfig[store] = Store{}
}
err := conf.Certificate.AppendCertificate(storesCertificates, store)
if err != nil {
logger.Error().Err(err).Msgf("Unable to append certificate %s to store", conf.Certificate.GetTruncatedCertificateName())
}
appendCertificate(storesCertificates, SANs, store, certData)
}
}
m.stores = make(map[string]*CertificateStore)
for storeName, storeConfig := range m.storesConfig {
st := NewCertificateStore()
st := NewCertificateStore(m.ocspStapler)
m.stores[storeName] = st
if certs, ok := storesCertificates[storeName]; ok {
@ -133,13 +189,17 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co
logger := log.Ctx(ctx).With().Str(logs.TLSStoreName, storeName).Logger()
ctxStore := logger.WithContext(ctx)
certificate, err := getDefaultCertificate(ctxStore, storeConfig, st)
certificate, err := m.getDefaultCertificate(ctxStore, storeConfig, st)
if err != nil {
logger.Error().Err(err).Msg("Error while creating certificate store")
}
st.DefaultCertificate = certificate
}
if m.ocspStapler != nil {
m.ocspStapler.ForceStapleUpdates()
}
}
// sanitizeDomains sanitizes the domain definition Main and SANS,
@ -226,7 +286,8 @@ func (m *Manager) Get(storeName, configName string) (*tls.Config, error) {
}
log.Debug().Msgf("Serving default certificate for request: %q", domainToCheck)
return store.DefaultCertificate, nil
return store.GetDefaultCertificate(), nil
}
return tlsConfig, err
@ -245,8 +306,8 @@ func (m *Manager) GetServerCertificates() []*x509.Certificate {
// We iterate over all the certificates.
if defaultStore.DynamicCerts != nil && defaultStore.DynamicCerts.Get() != nil {
for _, cert := range defaultStore.DynamicCerts.Get().(map[string]*tls.Certificate) {
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
for _, cert := range defaultStore.DynamicCerts.Get().(map[string]*CertificateData) {
x509Cert, err := x509.ParseCertificate(cert.Certificate.Certificate[0])
if err != nil {
continue
}
@ -256,7 +317,7 @@ func (m *Manager) GetServerCertificates() []*x509.Certificate {
}
if defaultStore.DefaultCertificate != nil {
x509Cert, err := x509.ParseCertificate(defaultStore.DefaultCertificate.Certificate[0])
x509Cert, err := x509.ParseCertificate(defaultStore.DefaultCertificate.Certificate.Certificate[0])
if err != nil {
return certificates
}
@ -289,9 +350,9 @@ func (m *Manager) GetStore(storeName string) *CertificateStore {
return m.getStore(storeName)
}
func getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*tls.Certificate, error) {
func (m *Manager) getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateStore) (*CertificateData, error) {
if tlsStore.DefaultCertificate != nil {
cert, err := buildDefaultCertificate(tlsStore.DefaultCertificate)
cert, err := m.buildDefaultCertificate(tlsStore.DefaultCertificate)
if err != nil {
return nil, err
}
@ -304,22 +365,65 @@ func getDefaultCertificate(ctx context.Context, tlsStore Store, st *CertificateS
return nil, err
}
defaultCertificate := &CertificateData{
Certificate: defaultCert,
}
if tlsStore.DefaultGeneratedCert != nil && tlsStore.DefaultGeneratedCert.Domain != nil && tlsStore.DefaultGeneratedCert.Resolver != "" {
domains, err := sanitizeDomains(*tlsStore.DefaultGeneratedCert.Domain)
if err != nil {
return defaultCert, fmt.Errorf("falling back to the internal generated certificate because invalid domains: %w", err)
return defaultCertificate, fmt.Errorf("falling back to the internal generated certificate because invalid domains: %w", err)
}
defaultACMECert := st.GetCertificate(domains)
if defaultACMECert == nil {
return defaultCert, fmt.Errorf("unable to find certificate for domains %q: falling back to the internal generated certificate", strings.Join(domains, ","))
return defaultCertificate, fmt.Errorf("unable to find certificate for domains %q: falling back to the internal generated certificate", strings.Join(domains, ","))
}
return defaultACMECert, nil
}
log.Ctx(ctx).Debug().Msg("No default certificate, fallback to the internal generated certificate")
return defaultCert, nil
return defaultCertificate, nil
}
func (m *Manager) buildDefaultCertificate(defaultCertificate *Certificate) (*CertificateData, error) {
certFile, err := defaultCertificate.CertFile.Read()
if err != nil {
return nil, fmt.Errorf("failed to get cert file content: %w", err)
}
keyFile, err := defaultCertificate.KeyFile.Read()
if err != nil {
return nil, fmt.Errorf("failed to get key file content: %w", err)
}
cert, err := tls.X509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("failed to load X509 key pair: %w", err)
}
var certHash string
if m.ocspStapler != nil && len(cert.Leaf.OCSPServer) > 0 {
certHash = hashRawCert(cert.Leaf.Raw)
issuer := cert.Leaf
if len(cert.Certificate) > 1 {
issuer, err = x509.ParseCertificate(cert.Certificate[1])
if err != nil {
return nil, fmt.Errorf("parsing issuer certificate %s: %w", defaultCertificate.GetTruncatedCertificateName(), err)
}
}
if err := m.ocspStapler.Upsert(certHash, cert.Leaf, issuer); err != nil {
return nil, fmt.Errorf("upserting OCSP certificate %s: %w", defaultCertificate.GetTruncatedCertificateName(), err)
}
}
return &CertificateData{
Certificate: &cert,
Hash: certHash,
}, nil
}
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI.
@ -412,20 +516,10 @@ func buildTLSConfig(tlsOption Options) (*tls.Config, error) {
return conf, nil
}
func buildDefaultCertificate(defaultCertificate *Certificate) (*tls.Certificate, error) {
certFile, err := defaultCertificate.CertFile.Read()
if err != nil {
return nil, fmt.Errorf("failed to get cert file content: %w", err)
}
func hashRawCert(rawCert []byte) string {
hasher := fnv.New64()
keyFile, err := defaultCertificate.KeyFile.Read()
if err != nil {
return nil, fmt.Errorf("failed to get key file content: %w", err)
}
cert, err := tls.X509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("failed to load X509 key pair: %w", err)
}
return &cert, nil
// purposely ignoring the error, as no error can be returned from the implementation.
_, _ = hasher.Write(rawCert)
return strconv.FormatUint(hasher.Sum64(), 16)
}