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

@ -28,6 +28,7 @@ import (
"github.com/traefik/traefik/v3/pkg/provider/kv/zk"
"github.com/traefik/traefik/v3/pkg/provider/nomad"
"github.com/traefik/traefik/v3/pkg/provider/rest"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
@ -80,6 +81,8 @@ type Configuration struct {
Core *Core `description:"Core controls." json:"core,omitempty" toml:"core,omitempty" yaml:"core,omitempty" export:"true"`
Spiffe *SpiffeClientConfig `description:"SPIFFE integration configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" export:"true"`
OCSP *tls.OCSPConfig `description:"OCSP configuration." json:"ocsp,omitempty" toml:"ocsp,omitempty" yaml:"ocsp,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
}
// Core configures Traefik core behavior.
@ -424,6 +427,14 @@ func (c *Configuration) ValidateConfiguration() error {
return errors.New("API basePath must be a valid absolute path")
}
if c.OCSP != nil {
for responderURL, url := range c.OCSP.ResponderOverrides {
if url == "" {
return fmt.Errorf("OCSP responder override value for %s cannot be empty", responderURL)
}
}
}
return nil
}

View file

@ -325,7 +325,7 @@ func TestRouterManager_Get(t *testing.T) {
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := traefiktls.NewManager()
tlsManager := traefiktls.NewManager(nil)
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
@ -712,7 +712,7 @@ func TestRuntimeConfiguration(t *testing.T) {
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, proxyBuilderMock{})
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := traefiktls.NewManager()
tlsManager := traefiktls.NewManager(nil)
tlsManager.UpdateConfigs(t.Context(), nil, test.tlsOptions, nil)
parser, err := httpmuxer.NewSyntaxParser()
@ -794,7 +794,7 @@ func TestProviderOnMiddlewares(t *testing.T) {
serviceManager := service.NewManager(rtConf.Services, nil, nil, transportManager, nil)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := traefiktls.NewManager()
tlsManager := traefiktls.NewManager(nil)
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(t, err)
@ -873,7 +873,7 @@ func BenchmarkRouterServe(b *testing.B) {
serviceManager := service.NewManager(rtConf.Services, nil, nil, staticTransportManager{res}, nil)
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
tlsManager := traefiktls.NewManager()
tlsManager := traefiktls.NewManager(nil)
parser, err := httpmuxer.NewSyntaxParser()
require.NoError(b, err)

View file

@ -347,7 +347,7 @@ func TestRuntimeConfiguration(t *testing.T) {
dialerManager := tcp2.NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
serviceManager := tcp.NewManager(conf, dialerManager)
tlsManager := traefiktls.NewManager()
tlsManager := traefiktls.NewManager(nil)
tlsManager.UpdateConfigs(
t.Context(),
map[string]traefiktls.Store{},
@ -659,7 +659,7 @@ func TestDomainFronting(t *testing.T) {
serviceManager := tcp.NewManager(conf, tcp2.NewDialerManager(nil))
tlsManager := traefiktls.NewManager()
tlsManager := traefiktls.NewManager(nil)
tlsManager.UpdateConfigs(t.Context(), map[string]traefiktls.Store{}, test.tlsOptions, []*traefiktls.CertAndStores{})
httpsHandler := map[string]http.Handler{

View file

@ -172,7 +172,7 @@ func Test_Routing(t *testing.T) {
require.NoError(t, err)
// Creates the tlsManager and defines the TLS 1.0 and 1.2 TLSOptions.
tlsManager := traefiktls.NewManager()
tlsManager := traefiktls.NewManager(nil)
tlsManager.UpdateConfigs(
t.Context(),
map[string]traefiktls.Store{

View file

@ -55,7 +55,7 @@ func TestReuseService(t *testing.T) {
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
tlsManager := tls.NewManager()
tlsManager := tls.NewManager(nil)
dialerManager := tcp.NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
@ -193,7 +193,7 @@ func TestServerResponseEmptyBackend(t *testing.T) {
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, proxyBuilderMock{}, nil)
tlsManager := tls.NewManager()
tlsManager := tls.NewManager(nil)
dialerManager := tcp.NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})
@ -239,7 +239,7 @@ func TestInternalServices(t *testing.T) {
transportManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
managerFactory := service.NewManagerFactory(staticConfig, nil, nil, transportManager, nil, nil)
tlsManager := tls.NewManager()
tlsManager := tls.NewManager(nil)
dialerManager := tcp.NewDialerManager(nil)
dialerManager.Update(map[string]*dynamic.TCPServersTransport{"default@internal": {}})

View file

@ -7,7 +7,6 @@ import (
"fmt"
"net/url"
"os"
"sort"
"strings"
"github.com/rs/zerolog/log"
@ -76,68 +75,6 @@ type Certificate struct {
KeyFile types.FileOrContent `json:"keyFile,omitempty" toml:"keyFile,omitempty" yaml:"keyFile,omitempty" loggable:"false"`
}
// AppendCertificate appends a Certificate to a certificates map keyed by store name.
func (c *Certificate) AppendCertificate(certs map[string]map[string]*tls.Certificate, storeName string) error {
certContent, err := c.CertFile.Read()
if err != nil {
return fmt.Errorf("unable to read CertFile : %w", err)
}
keyContent, err := c.KeyFile.Read()
if err != nil {
return fmt.Errorf("unable to read KeyFile : %w", err)
}
tlsCert, err := tls.X509KeyPair(certContent, keyContent)
if err != nil {
return fmt.Errorf("unable to generate TLS certificate : %w", err)
}
parsedCert, _ := x509.ParseCertificate(tlsCert.Certificate[0])
var SANs []string
if parsedCert.Subject.CommonName != "" {
SANs = append(SANs, strings.ToLower(parsedCert.Subject.CommonName))
}
if parsedCert.DNSNames != nil {
for _, dnsName := range parsedCert.DNSNames {
if dnsName != parsedCert.Subject.CommonName {
SANs = append(SANs, strings.ToLower(dnsName))
}
}
}
if parsedCert.IPAddresses != nil {
for _, ip := range parsedCert.IPAddresses {
if ip.String() != parsedCert.Subject.CommonName {
SANs = append(SANs, strings.ToLower(ip.String()))
}
}
}
// Guarantees the order to produce a unique cert key.
sort.Strings(SANs)
certKey := strings.Join(SANs, ",")
certExists := false
if certs[storeName] == nil {
certs[storeName] = make(map[string]*tls.Certificate)
} else {
for domains := range certs[storeName] {
if domains == certKey {
certExists = true
break
}
}
}
if certExists {
log.Debug().Msgf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName)
} else {
log.Debug().Msgf("Adding certificate for domain(s) %s", certKey)
certs[storeName][certKey] = &tlsCert
}
return err
}
// GetCertificate returns a tls.Certificate matching the configured CertFile and KeyFile.
func (c *Certificate) GetCertificate() (tls.Certificate, error) {
certContent, err := c.CertFile.Read()
@ -169,24 +106,6 @@ func (c *Certificate) GetCertificateFromBytes() (tls.Certificate, error) {
return cert, nil
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (c *Certificates) Set(value string) error {
certificates := strings.Split(value, ";")
for _, certificate := range certificates {
files := strings.Split(certificate, ",")
if len(files) != 2 {
return fmt.Errorf("bad certificates format: %s", value)
}
*c = append(*c, Certificate{
CertFile: types.FileOrContent(files[0]),
KeyFile: types.FileOrContent(files[1]),
})
}
return nil
}
// GetTruncatedCertificateName truncates the certificate name.
func (c *Certificate) GetTruncatedCertificateName() string {
certName := c.CertFile.String()

View file

@ -2,7 +2,7 @@ package tls
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"sort"
"strings"
@ -13,57 +13,40 @@ import (
"github.com/traefik/traefik/v3/pkg/safe"
)
// CertificateData holds runtime data for runtime TLS certificate handling.
type CertificateData struct {
Hash string
Certificate *tls.Certificate
}
// CertificateStore store for dynamic certificates.
type CertificateStore struct {
DynamicCerts *safe.Safe
DefaultCertificate *tls.Certificate
DefaultCertificate *CertificateData
CertCache *cache.Cache
ocspStapler *ocspStapler
}
// NewCertificateStore create a store for dynamic certificates.
func NewCertificateStore() *CertificateStore {
s := &safe.Safe{}
s.Set(make(map[string]*tls.Certificate))
func NewCertificateStore(ocspStapler *ocspStapler) *CertificateStore {
var dynamicCerts safe.Safe
dynamicCerts.Set(make(map[string]*CertificateData))
return &CertificateStore{
DynamicCerts: s,
DynamicCerts: &dynamicCerts,
CertCache: cache.New(1*time.Hour, 10*time.Minute),
ocspStapler: ocspStapler,
}
}
func (c *CertificateStore) getDefaultCertificateDomains() []string {
var allCerts []string
if c.DefaultCertificate == nil {
return allCerts
}
x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0])
if err != nil {
log.Error().Err(err).Msg("Could not parse default certificate")
return allCerts
}
if len(x509Cert.Subject.CommonName) > 0 {
allCerts = append(allCerts, x509Cert.Subject.CommonName)
}
allCerts = append(allCerts, x509Cert.DNSNames...)
for _, ipSan := range x509Cert.IPAddresses {
allCerts = append(allCerts, ipSan.String())
}
return allCerts
}
// GetAllDomains return a slice with all the certificate domain.
func (c *CertificateStore) GetAllDomains() []string {
allDomains := c.getDefaultCertificateDomains()
// Get dynamic certificates
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
for domain := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
for domain := range c.DynamicCerts.Get().(map[string]*CertificateData) {
allDomains = append(allDomains, domain)
}
}
@ -71,6 +54,23 @@ func (c *CertificateStore) GetAllDomains() []string {
return allDomains
}
// GetDefaultCertificate returns the default certificate.
func (c *CertificateStore) GetDefaultCertificate() *tls.Certificate {
if c == nil {
return nil
}
if c.ocspStapler != nil && c.DefaultCertificate.Hash != "" {
if staple, ok := c.ocspStapler.GetStaple(c.DefaultCertificate.Hash); ok {
// We are updating the OCSPStaple of the certificate without any synchronization
// as this should not cause any issue.
c.DefaultCertificate.Certificate.OCSPStaple = staple
}
}
return c.DefaultCertificate.Certificate
}
// GetBestCertificate returns the best match certificate, and caches the response.
func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *tls.Certificate {
if c == nil {
@ -87,12 +87,21 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo)
}
if cert, ok := c.CertCache.Get(serverName); ok {
return cert.(*tls.Certificate)
certificateData := cert.(*CertificateData)
if c.ocspStapler != nil && certificateData.Hash != "" {
if staple, ok := c.ocspStapler.GetStaple(certificateData.Hash); ok {
// We are updating the OCSPStaple of the certificate without any synchronization
// as this should not cause any issue.
certificateData.Certificate.OCSPStaple = staple
}
}
return certificateData.Certificate
}
matchedCerts := map[string]*tls.Certificate{}
matchedCerts := map[string]*CertificateData{}
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
for domains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
for domains, cert := range c.DynamicCerts.Get().(map[string]*CertificateData) {
for _, certDomain := range strings.Split(domains, ",") {
if matchDomain(serverName, certDomain) {
matchedCerts[certDomain] = cert
@ -110,15 +119,25 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo)
sort.Strings(keys)
// cache best match
c.CertCache.SetDefault(serverName, matchedCerts[keys[len(keys)-1]])
return matchedCerts[keys[len(keys)-1]]
certificateData := matchedCerts[keys[len(keys)-1]]
c.CertCache.SetDefault(serverName, certificateData)
if c.ocspStapler != nil && certificateData.Hash != "" {
if staple, ok := c.ocspStapler.GetStaple(certificateData.Hash); ok {
// We are updating the OCSPStaple of the certificate without any synchronization
// as this should not cause any issue.
certificateData.Certificate.OCSPStaple = staple
}
}
return certificateData.Certificate
}
return nil
}
// GetCertificate returns the first certificate matching all the given domains.
func (c *CertificateStore) GetCertificate(domains []string) *tls.Certificate {
func (c *CertificateStore) GetCertificate(domains []string) *CertificateData {
if c == nil {
return nil
}
@ -127,11 +146,11 @@ func (c *CertificateStore) GetCertificate(domains []string) *tls.Certificate {
domainsKey := strings.Join(domains, ",")
if cert, ok := c.CertCache.Get(domainsKey); ok {
return cert.(*tls.Certificate)
return cert.(*CertificateData)
}
if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil {
for certDomains, cert := range c.DynamicCerts.Get().(map[string]*tls.Certificate) {
for certDomains, cert := range c.DynamicCerts.Get().(map[string]*CertificateData) {
if domainsKey == certDomains {
c.CertCache.SetDefault(domainsKey, cert)
return cert
@ -163,6 +182,91 @@ func (c *CertificateStore) ResetCache() {
}
}
func (c *CertificateStore) getDefaultCertificateDomains() []string {
if c.DefaultCertificate == nil {
return nil
}
defaultCert := c.DefaultCertificate.Certificate.Leaf
var allCerts []string
if len(defaultCert.Subject.CommonName) > 0 {
allCerts = append(allCerts, defaultCert.Subject.CommonName)
}
allCerts = append(allCerts, defaultCert.DNSNames...)
for _, ipSan := range defaultCert.IPAddresses {
allCerts = append(allCerts, ipSan.String())
}
return allCerts
}
// appendCertificate appends a Certificate to a certificates map keyed by store name.
func appendCertificate(certs map[string]map[string]*CertificateData, subjectAltNames []string, storeName string, cert *CertificateData) {
// Guarantees the order to produce a unique cert key.
sort.Strings(subjectAltNames)
certKey := strings.Join(subjectAltNames, ",")
certExists := false
if certs[storeName] == nil {
certs[storeName] = make(map[string]*CertificateData)
} else {
for domains := range certs[storeName] {
if domains == certKey {
certExists = true
break
}
}
}
if certExists {
log.Debug().Msgf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName)
} else {
log.Debug().Msgf("Adding certificate for domain(s) %s", certKey)
certs[storeName][certKey] = cert
}
}
func parseCertificate(cert *Certificate) (tls.Certificate, []string, error) {
certContent, err := cert.CertFile.Read()
if err != nil {
return tls.Certificate{}, nil, fmt.Errorf("unable to read CertFile: %w", err)
}
keyContent, err := cert.KeyFile.Read()
if err != nil {
return tls.Certificate{}, nil, fmt.Errorf("unable to read KeyFile: %w", err)
}
tlsCert, err := tls.X509KeyPair(certContent, keyContent)
if err != nil {
return tls.Certificate{}, nil, fmt.Errorf("unable to generate TLS certificate: %w", err)
}
var SANs []string
if tlsCert.Leaf.Subject.CommonName != "" {
SANs = append(SANs, strings.ToLower(tlsCert.Leaf.Subject.CommonName))
}
if tlsCert.Leaf.DNSNames != nil {
for _, dnsName := range tlsCert.Leaf.DNSNames {
if dnsName != tlsCert.Leaf.Subject.CommonName {
SANs = append(SANs, strings.ToLower(dnsName))
}
}
}
if tlsCert.Leaf.IPAddresses != nil {
for _, ip := range tlsCert.Leaf.IPAddresses {
if ip.String() != tlsCert.Leaf.Subject.CommonName {
SANs = append(SANs, strings.ToLower(ip.String()))
}
}
}
return tlsCert, SANs, err
}
// matchDomain returns whether the server name matches the cert domain.
// The server name, from TLS SNI, must not have trailing dots (https://datatracker.ietf.org/doc/html/rfc6066#section-3).
// This is enforced by https://github.com/golang/go/blob/d3d7998756c33f69706488cade1cd2b9b10a4c7f/src/crypto/tls/handshake_messages.go#L423-L427.

View file

@ -58,12 +58,12 @@ func TestGetBestCertificate(t *testing.T) {
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
dynamicMap := map[string]*tls.Certificate{}
dynamicMap := map[string]*CertificateData{}
if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert, test.uppercase)
require.NoError(t, err)
dynamicMap[strings.ToLower(test.dynamicCert)] = cert
dynamicMap[strings.ToLower(test.dynamicCert)] = &CertificateData{Certificate: cert}
}
store := &CertificateStore{

206
pkg/tls/ocsp.go Normal file
View file

@ -0,0 +1,206 @@
package tls
import (
"bytes"
"context"
"crypto/x509"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/patrickmn/go-cache"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/ocsp"
)
const defaultCacheDuration = 24 * time.Hour
type ocspEntry struct {
leaf *x509.Certificate
issuer *x509.Certificate
responders []string
nextUpdate time.Time
staple []byte
}
// ocspStapler retrieves staples from OCSP responders and store them in an in-memory cache.
// It also updates the staples on a regular basis and before they expire.
type ocspStapler struct {
client *http.Client
cache cache.Cache
forceStapleUpdates chan struct{}
responderOverrides map[string]string
}
// newOCSPStapler creates a new ocspStapler cache.
func newOCSPStapler(responderOverrides map[string]string) *ocspStapler {
return &ocspStapler{
client: &http.Client{Timeout: 10 * time.Second},
cache: *cache.New(defaultCacheDuration, 5*time.Minute),
forceStapleUpdates: make(chan struct{}, 1),
responderOverrides: responderOverrides,
}
}
// Run updates the OCSP staples every hours.
func (o *ocspStapler) Run(ctx context.Context) {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
select {
case <-ctx.Done():
return
case <-o.forceStapleUpdates:
o.updateStaples(ctx)
case <-ticker.C:
o.updateStaples(ctx)
}
}
// ForceStapleUpdates triggers staple updates in the background instead of waiting for the Run routine to update them.
func (o *ocspStapler) ForceStapleUpdates() {
select {
case o.forceStapleUpdates <- struct{}{}:
default:
}
}
// GetStaple retrieves the OCSP staple for the corresponding to the given key (public certificate hash).
func (o *ocspStapler) GetStaple(key string) ([]byte, bool) {
if item, ok := o.cache.Get(key); ok && item != nil {
if entry, ok := item.(*ocspEntry); ok {
return entry.staple, true
}
}
return nil, false
}
// Upsert creates a new entry for the given certificate.
// The ocspStapler will then be responsible from retrieving and updating the corresponding OCSP obtainStaple.
func (o *ocspStapler) Upsert(key string, leaf, issuer *x509.Certificate) error {
if len(leaf.OCSPServer) == 0 {
return errors.New("leaf certificate does not contain an OCSP server")
}
if item, ok := o.cache.Get(key); ok {
o.cache.Set(key, item, cache.NoExpiration)
return nil
}
var responders []string
for _, url := range leaf.OCSPServer {
if len(o.responderOverrides) > 0 {
if newURL, ok := o.responderOverrides[url]; ok {
url = newURL
}
}
responders = append(responders, url)
}
o.cache.Set(key, &ocspEntry{
leaf: leaf,
issuer: issuer,
responders: responders,
}, cache.NoExpiration)
return nil
}
// ResetTTL resets the expiration time for all items having no expiration.
// This allows setting a TTL for certificates that do not exist anymore in the dynamic configuration.
// For certificates that are still provided by the dynamic configuration,
// their expiration time will be unset when calling the Upsert method.
func (o *ocspStapler) ResetTTL() {
for key, item := range o.cache.Items() {
if item.Expiration > 0 {
continue
}
o.cache.Set(key, item.Object, defaultCacheDuration)
}
}
func (o *ocspStapler) updateStaples(ctx context.Context) {
for _, item := range o.cache.Items() {
select {
case <-ctx.Done():
return
default:
}
entry := item.Object.(*ocspEntry)
if entry.staple != nil && time.Now().Before(entry.nextUpdate) {
continue
}
if err := o.updateStaple(ctx, entry); err != nil {
log.Error().Err(err).Msgf("Unable to retieve OCSP staple for: %s", entry.leaf.Subject.CommonName)
continue
}
}
}
// obtainStaple obtains the OCSP stable for the given leaf certificate.
func (o *ocspStapler) updateStaple(ctx context.Context, entry *ocspEntry) error {
ocspReq, err := ocsp.CreateRequest(entry.leaf, entry.issuer, nil)
if err != nil {
return fmt.Errorf("creating OCSP request: %w", err)
}
for _, responder := range entry.responders {
logger := log.With().Str("responder", responder).Logger()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, responder, bytes.NewReader(ocspReq))
if err != nil {
return fmt.Errorf("creating OCSP request: %w", err)
}
req.Header.Set("Content-Type", "application/ocsp-request")
res, err := o.client.Do(req)
if err != nil && ctx.Err() != nil {
return ctx.Err()
}
if err != nil {
logger.Debug().Err(err).Msg("Unable to obtain OCSP response")
continue
}
defer res.Body.Close()
if res.StatusCode/100 != 2 {
logger.Debug().Msgf("Unable to obtain OCSP response due to status code: %d", res.StatusCode)
continue
}
ocspResBytes, err := io.ReadAll(res.Body)
if err != nil {
logger.Debug().Err(err).Msg("Unable to read OCSP response bytes")
continue
}
ocspRes, err := ocsp.ParseResponseForCert(ocspResBytes, entry.leaf, entry.issuer)
if err != nil {
logger.Debug().Err(err).Msg("Unable to parse OCSP response")
continue
}
entry.staple = ocspResBytes
// As per RFC 6960, the nextUpdate field is optional.
if ocspRes.NextUpdate.IsZero() {
// NextUpdate is not set, the staple should be updated on the next update.
entry.nextUpdate = time.Now()
} else {
entry.nextUpdate = ocspRes.ThisUpdate.Add(ocspRes.NextUpdate.Sub(ocspRes.ThisUpdate) / 2)
}
return nil
}
return errors.New("no OCSP staple obtained from any responders")
}

485
pkg/tls/ocsp_test.go Normal file
View file

@ -0,0 +1,485 @@
package tls
import (
"crypto"
"crypto/tls"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/patrickmn/go-cache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ocsp"
)
const certWithOCSPServer = `-----BEGIN CERTIFICATE-----
MIIBgjCCASegAwIBAgICIAAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHVGVzdCBD
QTAeFw0yMzAxMDExMjAwMDBaFw0yMzAyMDExMjAwMDBaMCAxHjAcBgNVBAMTFU9D
U1AgVGVzdCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIoe
I/bjo34qony8LdRJD+Jhuk8/S8YHXRHl6rH9t5VFCFtX8lIPN/Ll1zCrQ2KB3Wlb
fxSgiQyLrCpZyrdhVPSjXzBdMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU+Eo3
5sST4LRrwS4dueIdGBZ5d7IwLAYIKwYBBQUHAQEEIDAeMBwGCCsGAQUFBzABhhBv
Y3NwLmV4YW1wbGUuY29tMAoGCCqGSM49BAMCA0kAMEYCIQDg94xY/+/VepESdvTT
ykCwiWOS2aCpjyryrKpwMKkR0AIhAPc/+ZEz4W10OENxC1t+NUTvS8JbEGOwulkZ
z9yfaLuD
-----END CERTIFICATE-----`
const certWithoutOCSPServer = `-----BEGIN CERTIFICATE-----
MIIBUzCB+aADAgECAgIgADAKBggqhkjOPQQDAjASMRAwDgYDVQQDEwdUZXN0IENB
MB4XDTIzMDEwMTEyMDAwMFoXDTIzMDIwMTEyMDAwMFowIDEeMBwGA1UEAxMVT0NT
UCBUZXN0IENlcnRpZmljYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEih4j
9uOjfiqifLwt1EkP4mG6Tz9LxgddEeXqsf23lUUIW1fyUg838uXXMKtDYoHdaVt/
FKCJDIusKlnKt2FU9KMxMC8wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBT4Sjfm
xJPgtGvBLh254h0YFnl3sjAKBggqhkjOPQQDAgNJADBGAiEA3rWetLGblfSuNZKf
5CpZxhj3A0BjEocEh+2P+nAgIdUCIQDIgptabR1qTLQaF2u0hJsEX2IKuIUvYWH3
6Lb92+zIHg==
-----END CERTIFICATE-----`
// certKey is the private key for both certWithOCSPServer and certWithoutOCSPServer.
const certKey = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINnVcgrSNh4HlThWlZpegq14M8G/p9NVDtdVjZrseUGLoAoGCCqGSM49
AwEHoUQDQgAEih4j9uOjfiqifLwt1EkP4mG6Tz9LxgddEeXqsf23lUUIW1fyUg83
8uXXMKtDYoHdaVt/FKCJDIusKlnKt2FU9A==
-----END EC PRIVATE KEY-----`
// caCert is the issuing certificate for certWithOCSPServer and certWithoutOCSPServer.
const caCert = `-----BEGIN CERTIFICATE-----
MIIBazCCARGgAwIBAgICEAAwCgYIKoZIzj0EAwIwEjEQMA4GA1UEAxMHVGVzdCBD
QTAeFw0yMzAxMDExMjAwMDBaFw0yMzAyMDExMjAwMDBaMBIxEDAOBgNVBAMTB1Rl
c3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASdKexSor/aeazDM57UHhAX
rCkJxUeF2BWf0lZYCRxc3f0GdrEsVvjJW8+/E06eAzDCGSdM/08Nvun1nb6AmAlt
o1cwVTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwkwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQU+Eo35sST4LRrwS4dueIdGBZ5d7IwCgYIKoZI
zj0EAwIDSAAwRQIgGbA39+kETTB/YMLBFoC2fpZe1cDWfFB7TUdfINUqdH4CIQCR
ByUFC8A+hRNkK5YNH78bgjnKk/88zUQF5ONy4oPGdQ==
-----END CERTIFICATE-----`
const caKey = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDJ59ptjq3MzILH4zn5IKoH1sYn+zrUeq2kD8+DD2x+OoAoGCCqGSM49
AwEHoUQDQgAEnSnsUqK/2nmswzOe1B4QF6wpCcVHhdgVn9JWWAkcXN39BnaxLFb4
yVvPvxNOngMwwhknTP9PDb7p9Z2+gJgJbQ==
-----END EC PRIVATE KEY-----`
func TestOCSPStapler_Upsert(t *testing.T) {
ocspStapler := newOCSPStapler(nil)
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
require.NoError(t, err)
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
require.NoError(t, err)
// Upsert a certificate without an OCSP server should raise an error.
leafCertWithoutOCSPServer, err := tls.X509KeyPair([]byte(certWithoutOCSPServer), []byte(certKey))
require.NoError(t, err)
err = ocspStapler.Upsert("foo", leafCertWithoutOCSPServer.Leaf, issuerCert.Leaf)
require.Error(t, err)
// Upsert a certificate with an OCSP server.
err = ocspStapler.Upsert("foo", leafCert.Leaf, issuerCert.Leaf)
require.NoError(t, err)
i, ok := ocspStapler.cache.Get("foo")
require.True(t, ok)
e, ok := i.(*ocspEntry)
require.True(t, ok)
assert.Equal(t, leafCert.Leaf, e.leaf)
assert.Equal(t, issuerCert.Leaf, e.issuer)
assert.Nil(t, e.staple)
assert.Equal(t, []string{"ocsp.example.com"}, e.responders)
assert.Equal(t, int64(0), ocspStapler.cache.Items()["foo"].Expiration)
// Upsert an existing entry to make sure that the existing staple is preserved.
e.staple = []byte("foo")
e.nextUpdate = time.Now()
e.responders = []string{"foo.com"}
err = ocspStapler.Upsert("foo", leafCert.Leaf, issuerCert.Leaf)
require.NoError(t, err)
i, ok = ocspStapler.cache.Get("foo")
require.True(t, ok)
e, ok = i.(*ocspEntry)
require.True(t, ok)
assert.Equal(t, leafCert.Leaf, e.leaf)
assert.Equal(t, issuerCert.Leaf, e.issuer)
assert.Equal(t, []byte("foo"), e.staple)
assert.NotZero(t, e.nextUpdate)
assert.Equal(t, []string{"foo.com"}, e.responders)
assert.Equal(t, int64(0), ocspStapler.cache.Items()["foo"].Expiration)
}
func TestOCSPStapler_Upsert_withResponderOverrides(t *testing.T) {
ocspStapler := newOCSPStapler(map[string]string{
"ocsp.example.com": "foo.com",
})
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
require.NoError(t, err)
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
require.NoError(t, err)
err = ocspStapler.Upsert("foo", leafCert.Leaf, issuerCert.Leaf)
require.NoError(t, err)
i, ok := ocspStapler.cache.Get("foo")
require.True(t, ok)
e, ok := i.(*ocspEntry)
require.True(t, ok)
assert.Equal(t, leafCert.Leaf, e.leaf)
assert.Equal(t, issuerCert.Leaf, e.issuer)
assert.Nil(t, e.staple)
assert.Equal(t, []string{"foo.com"}, e.responders)
}
func TestOCSPStapler_ResetTTL(t *testing.T) {
ocspStapler := newOCSPStapler(nil)
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
require.NoError(t, err)
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
require.NoError(t, err)
ocspStapler.cache.Set("foo", &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{"foo.com"},
nextUpdate: time.Now(),
staple: []byte("foo"),
}, cache.NoExpiration)
ocspStapler.cache.Set("bar", &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{"bar.com"},
nextUpdate: time.Now(),
staple: []byte("bar"),
}, time.Hour)
wantBarExpiration := ocspStapler.cache.Items()["bar"].Expiration
ocspStapler.ResetTTL()
item, ok := ocspStapler.cache.Items()["foo"]
require.True(t, ok)
e, ok := item.Object.(*ocspEntry)
require.True(t, ok)
assert.Positive(t, item.Expiration)
assert.Equal(t, leafCert.Leaf, e.leaf)
assert.Equal(t, issuerCert.Leaf, e.issuer)
assert.Equal(t, []byte("foo"), e.staple)
assert.NotZero(t, e.nextUpdate)
assert.Equal(t, []string{"foo.com"}, e.responders)
item, ok = ocspStapler.cache.Items()["bar"]
require.True(t, ok)
e, ok = item.Object.(*ocspEntry)
require.True(t, ok)
assert.Equal(t, wantBarExpiration, item.Expiration)
assert.Equal(t, leafCert.Leaf, e.leaf)
assert.Equal(t, issuerCert.Leaf, e.issuer)
assert.Equal(t, []byte("bar"), e.staple)
assert.NotZero(t, e.nextUpdate)
assert.Equal(t, []string{"bar.com"}, e.responders)
}
func TestOCSPStapler_GetStaple(t *testing.T) {
ocspStapler := newOCSPStapler(nil)
// Get an un-existing staple.
staple, exists := ocspStapler.GetStaple("foo")
assert.False(t, exists)
assert.Nil(t, staple)
// Get an existing staple.
ocspStapler.cache.Set("foo", &ocspEntry{staple: []byte("foo")}, cache.NoExpiration)
staple, exists = ocspStapler.GetStaple("foo")
assert.True(t, exists)
assert.Equal(t, []byte("foo"), staple)
}
func TestOCSPStapler_updateStaple(t *testing.T) {
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
require.NoError(t, err)
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
require.NoError(t, err)
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
require.NoError(t, err)
nextUpdate, err := time.Parse("2006-01-02", "2025-01-02")
require.NoError(t, err)
stapleUpdate := thisUpdate.Add(nextUpdate.Sub(thisUpdate) / 2)
ocspResponseTmpl := ocsp.Response{
SerialNumber: leafCert.Leaf.SerialNumber,
TBSResponseData: []byte("foo"),
ThisUpdate: thisUpdate,
NextUpdate: nextUpdate,
}
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
require.NoError(t, err)
handler := func(rw http.ResponseWriter, req *http.Request) {
ct := req.Header.Get("Content-Type")
assert.Equal(t, "application/ocsp-request", ct)
reqBytes, err := io.ReadAll(req.Body)
require.NoError(t, err)
_, err = ocsp.ParseRequest(reqBytes)
require.NoError(t, err)
rw.Header().Set("Content-Type", "application/ocsp-response")
_, err = rw.Write(ocspResponse)
require.NoError(t, err)
}
responder := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(responder.Close)
responderStatusNotOK := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(responderStatusNotOK.Close)
testCases := []struct {
desc string
entry *ocspEntry
expectError bool
}{
{
desc: "no responder",
entry: &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
},
expectError: true,
},
{
desc: "wrong responder",
entry: &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{"http://foo.bar"},
},
expectError: true,
},
{
desc: "not ok status responder",
entry: &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{responderStatusNotOK.URL},
},
expectError: true,
},
{
desc: "one wrong responder, one ok",
entry: &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{"http://foo.bar", responder.URL},
},
},
{
desc: "ok responder",
entry: &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{responder.URL},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
ocspStapler := newOCSPStapler(nil)
ocspStapler.client = &http.Client{Timeout: time.Second}
err = ocspStapler.updateStaple(t.Context(), test.entry)
if test.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, ocspResponse, test.entry.staple)
assert.Equal(t, stapleUpdate.UTC(), test.entry.nextUpdate)
})
}
}
func TestOCSPStapler_updateStaple_withoutNextUpdate(t *testing.T) {
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
require.NoError(t, err)
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
require.NoError(t, err)
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
require.NoError(t, err)
ocspResponseTmpl := ocsp.Response{
SerialNumber: leafCert.Leaf.SerialNumber,
TBSResponseData: []byte("foo"),
ThisUpdate: thisUpdate,
}
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
require.NoError(t, err)
handler := func(rw http.ResponseWriter, req *http.Request) {
ct := req.Header.Get("Content-Type")
assert.Equal(t, "application/ocsp-request", ct)
reqBytes, err := io.ReadAll(req.Body)
require.NoError(t, err)
_, err = ocsp.ParseRequest(reqBytes)
require.NoError(t, err)
rw.Header().Set("Content-Type", "application/ocsp-response")
_, err = rw.Write(ocspResponse)
require.NoError(t, err)
}
responder := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(responder.Close)
responderStatusNotOK := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(responderStatusNotOK.Close)
ocspStapler := newOCSPStapler(nil)
ocspStapler.client = &http.Client{Timeout: time.Second}
entry := &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{responder.URL},
}
err = ocspStapler.updateStaple(t.Context(), entry)
require.NoError(t, err)
assert.Equal(t, ocspResponse, entry.staple)
assert.NotZero(t, entry.nextUpdate)
assert.Greater(t, time.Now(), entry.nextUpdate)
}
func TestOCSPStapler_updateStaples(t *testing.T) {
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
require.NoError(t, err)
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
require.NoError(t, err)
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
require.NoError(t, err)
nextUpdate, err := time.Parse("2006-01-02", "2025-01-02")
require.NoError(t, err)
stapleUpdate := thisUpdate.Add(nextUpdate.Sub(thisUpdate) / 2)
ocspResponseTmpl := ocsp.Response{
SerialNumber: leafCert.Leaf.SerialNumber,
TBSResponseData: []byte("foo"),
ThisUpdate: thisUpdate,
NextUpdate: nextUpdate,
}
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
require.NoError(t, err)
handler := func(rw http.ResponseWriter, req *http.Request) {
ct := req.Header.Get("Content-Type")
assert.Equal(t, "application/ocsp-request", ct)
reqBytes, err := io.ReadAll(req.Body)
require.NoError(t, err)
_, err = ocsp.ParseRequest(reqBytes)
require.NoError(t, err)
rw.Header().Set("Content-Type", "application/ocsp-response")
_, err = rw.Write(ocspResponse)
require.NoError(t, err)
}
responder := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(responder.Close)
ocspStapler := newOCSPStapler(nil)
ocspStapler.client = &http.Client{Timeout: time.Second}
// nil staple entry
ocspStapler.cache.Set("nilStaple", &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{responder.URL},
nextUpdate: time.Now().Add(-time.Hour),
}, cache.NoExpiration)
// staple entry with nextUpdate in the past
ocspStapler.cache.Set("toUpdate", &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{responder.URL},
staple: []byte("foo"),
nextUpdate: time.Now().Add(-time.Hour),
}, cache.NoExpiration)
// staple entry with nextUpdate in the future
inOneHour := time.Now().Add(time.Hour)
ocspStapler.cache.Set("noUpdate", &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
responders: []string{responder.URL},
staple: []byte("foo"),
nextUpdate: inOneHour,
}, cache.NoExpiration)
ocspStapler.updateStaples(t.Context())
nilStaple, ok := ocspStapler.cache.Get("nilStaple")
require.True(t, ok)
assert.Equal(t, ocspResponse, nilStaple.(*ocspEntry).staple)
assert.Equal(t, stapleUpdate.UTC(), nilStaple.(*ocspEntry).nextUpdate)
toUpdate, ok := ocspStapler.cache.Get("toUpdate")
require.True(t, ok)
assert.Equal(t, ocspResponse, toUpdate.(*ocspEntry).staple)
assert.Equal(t, stapleUpdate.UTC(), nilStaple.(*ocspEntry).nextUpdate)
noUpdate, ok := ocspStapler.cache.Get("noUpdate")
require.True(t, ok)
assert.Equal(t, []byte("foo"), noUpdate.(*ocspEntry).staple)
assert.Equal(t, inOneHour, noUpdate.(*ocspEntry).nextUpdate)
}

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)
}

View file

@ -1,14 +1,22 @@
package tls
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/patrickmn/go-cache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/types"
"golang.org/x/crypto/ocsp"
)
// LocalhostCert is a PEM-encoded TLS cert with SAN IPs
@ -76,10 +84,10 @@ func TestTLSInStore(t *testing.T) {
},
}}
tlsManager := NewManager()
tlsManager := NewManager(nil)
tlsManager.UpdateConfigs(t.Context(), nil, nil, dynamicConfigs)
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*tls.Certificate)
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*CertificateData)
if len(certs) == 0 {
t.Fatal("got error: default store must have TLS certificates.")
}
@ -93,7 +101,7 @@ func TestTLSInvalidStore(t *testing.T) {
},
}}
tlsManager := NewManager()
tlsManager := NewManager(nil)
tlsManager.UpdateConfigs(t.Context(),
map[string]Store{
"default": {
@ -104,7 +112,7 @@ func TestTLSInvalidStore(t *testing.T) {
},
}, nil, dynamicConfigs)
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*tls.Certificate)
certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*CertificateData)
if len(certs) == 0 {
t.Fatal("got error: default store must have TLS certificates.")
}
@ -157,7 +165,7 @@ func TestManager_Get(t *testing.T) {
},
}
tlsManager := NewManager()
tlsManager := NewManager(nil)
tlsManager.UpdateConfigs(t.Context(), nil, tlsConfigs, dynamicConfigs)
for _, test := range testCases {
@ -296,7 +304,7 @@ func TestClientAuth(t *testing.T) {
},
}
tlsManager := NewManager()
tlsManager := NewManager(nil)
tlsManager.UpdateConfigs(t.Context(), nil, tlsConfigs, nil)
for _, test := range testCases {
@ -323,8 +331,108 @@ func TestClientAuth(t *testing.T) {
}
}
func TestManager_UpdateConfigs_OCSPConfig(t *testing.T) {
leafCert, err := tls.X509KeyPair([]byte(certWithOCSPServer), []byte(certKey))
require.NoError(t, err)
issuerCert, err := tls.X509KeyPair([]byte(caCert), []byte(caKey))
require.NoError(t, err)
thisUpdate, err := time.Parse("2006-01-02", "2025-01-01")
require.NoError(t, err)
nextUpdate, err := time.Parse("2006-01-02", "2025-01-02")
require.NoError(t, err)
ocspResponseTmpl := ocsp.Response{
SerialNumber: leafCert.Leaf.SerialNumber,
TBSResponseData: []byte("foo"),
ThisUpdate: thisUpdate,
NextUpdate: nextUpdate,
}
ocspResponse, err := ocsp.CreateResponse(leafCert.Leaf, leafCert.Leaf, ocspResponseTmpl, issuerCert.PrivateKey.(crypto.Signer))
require.NoError(t, err)
responderCall := make(chan struct{})
handler := func(rw http.ResponseWriter, req *http.Request) {
ct := req.Header.Get("Content-Type")
assert.Equal(t, "application/ocsp-request", ct)
reqBytes, err := io.ReadAll(req.Body)
require.NoError(t, err)
_, err = ocsp.ParseRequest(reqBytes)
require.NoError(t, err)
rw.Header().Set("Content-Type", "application/ocsp-response")
_, err = rw.Write(ocspResponse)
require.NoError(t, err)
responderCall <- struct{}{}
}
responder := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(responder.Close)
testContext, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
tlsManager := NewManager(&OCSPConfig{
ResponderOverrides: map[string]string{
"ocsp.example.com": responder.URL,
},
})
go tlsManager.Run(testContext)
tlsManager.ocspStapler.cache.Set("existing", &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
staple: []byte("foo"),
nextUpdate: time.Now().Add(time.Hour),
}, cache.NoExpiration)
tlsManager.ocspStapler.cache.Set("existingWithTTL", &ocspEntry{
leaf: leafCert.Leaf,
issuer: issuerCert.Leaf,
staple: []byte("foo"),
nextUpdate: time.Now().Add(time.Hour),
}, 2*defaultCacheDuration)
tlsManager.UpdateConfigs(testContext, nil, nil, []*CertAndStores{
{
Certificate: Certificate{
CertFile: certWithOCSPServer,
KeyFile: certKey,
},
},
})
// Asserting that UpdateConfigs resets the expiration for existing entries.
_, expiration, ok := tlsManager.ocspStapler.cache.GetWithExpiration("existing")
require.True(t, ok)
assert.Greater(t, expiration, time.Now())
// But not for entries with TTL already set.
_, expiration, ok = tlsManager.ocspStapler.cache.GetWithExpiration("existingWithTTL")
require.True(t, ok)
assert.Greater(t, expiration, time.Now().Add(defaultCacheDuration))
select {
case <-responderCall:
case <-time.After(3 * time.Second):
t.Fatal("Timeout waiting for OCSP responder call")
}
assert.Len(t, tlsManager.ocspStapler.cache.Items(), 3)
certHash := hashRawCert(leafCert.Leaf.Raw)
_, ok = tlsManager.ocspStapler.cache.Get(certHash)
require.True(t, ok)
}
func TestManager_Get_DefaultValues(t *testing.T) {
tlsManager := NewManager()
tlsManager := NewManager(nil)
// Ensures we won't break things for Traefik users when updating Go
config, _ := tlsManager.Get("default", "default")