OCSP stapling
This commit is contained in:
parent
2949995abc
commit
b39ee8ede5
30 changed files with 1576 additions and 178 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue