From 3ae0b9342b891f704dec9924e650bbbe75533027 Mon Sep 17 00:00:00 2001 From: Alessandro Chitolina Date: Wed, 29 Sep 2021 17:46:03 +0200 Subject: [PATCH] split certificate config from runtime structures baalajimaestro: Forward port for v2.9+ Signed-off-by: baalajimaestro --- pkg/tls/certificate.go | 204 +--------------------------- pkg/tls/certificate_store.go | 10 +- pkg/tls/certificate_store_test.go | 9 +- pkg/tls/tls_certificate.go | 217 ++++++++++++++++++++++++++++++ pkg/tls/tlsmanager.go | 7 +- pkg/tls/tlsmanager_test.go | 4 +- pkg/tls/zz_generated.deepcopy.go | 36 +++++ 7 files changed, 275 insertions(+), 212 deletions(-) create mode 100644 pkg/tls/tls_certificate.go diff --git a/pkg/tls/certificate.go b/pkg/tls/certificate.go index ea0036dfa..c026d53dd 100644 --- a/pkg/tls/certificate.go +++ b/pkg/tls/certificate.go @@ -1,22 +1,15 @@ package tls import ( - "bytes" "crypto/tls" "crypto/x509" "errors" "fmt" - "io" - "io/ioutil" - "net/http" "net/url" "os" - "sort" "strings" - "time" "github.com/traefik/traefik/v2/pkg/log" - "golang.org/x/crypto/ocsp" ) var ( @@ -51,29 +44,29 @@ var ( } ) +// +k8s:deepcopy-gen=true + // OCSPConfig configures how OCSP is handled. type OCSPConfig struct { DisableStapling bool `json:"disableStapling,omitempty" toml:"disableStapling,omitempty" yaml:"disableStapling,omitempty"` } +// +k8s:deepcopy-gen=true + // Certificate holds a SSL cert/key pair // Certs and Key could be either a file path, or the file content itself. type Certificate struct { CertFile FileOrContent `json:"certFile,omitempty" toml:"certFile,omitempty" yaml:"certFile,omitempty"` KeyFile FileOrContent `json:"keyFile,omitempty" toml:"keyFile,omitempty" yaml:"keyFile,omitempty" loggable:"false"` OCSP OCSPConfig `json:"ocsp,omitempty" toml:"ocsp,omitempty" yaml:"ocsp,omitempty" label:"allowEmpty" file:"allowEmpty"` - - Certificate *tls.Certificate `json:"-" toml:"-" yaml:"-"` - SANs []string `json:"-" toml:"-" yaml:"-"` - OCSPServer []string `json:"-" toml:"-" yaml:"-"` - OCSPResponse *ocsp.Response `json:"-" toml:"-" yaml:"-"` + SANs []string `json:"-" toml:"-" yaml:"-"` } -// Certificates defines traefik certificates type +// Certificates defines traefik Certificates type // Certs and Keys could be either a file path, or the file content itself. type Certificates []Certificate -// GetCertificates retrieves the certificates as slice of tls.Certificate. +// GetCertificates retrieves the Certs as slice of tls.Certificate. func (c Certificates) GetCertificates() []tls.Certificate { var certs []tls.Certificate @@ -117,150 +110,6 @@ func (f FileOrContent) Read() ([]byte, error) { return content, nil } -// AppendCertificate appends a Certificate to a certificates map keyed by store name. -func (c *Certificate) AppendCertificate(certs map[string]map[string]*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]*Certificate) - } else { - for domains := range certs[storeName] { - if domains == certKey { - certExists = true - break - } - } - } - - if certExists { - log.Debugf("Skipping addition of certificate for domain(s) %q, to TLS Store %s, as it already exists for this store.", certKey, storeName) - } else { - log.Debugf("Adding certificate for domain(s) %s", certKey) - - certs[storeName][certKey] = &Certificate{ - Certificate: &tlsCert, - SANs: SANs, - OCSPServer: parsedCert.OCSPServer, - OCSP: OCSPConfig{ - DisableStapling: false, - }, - } - } - - return err -} - -func getOCSPForCert(certificate *Certificate, issuedCertificate *x509.Certificate, issuerCertificate *x509.Certificate) ([]byte, *ocsp.Response, error) { - if len(certificate.OCSPServer) == 0 { - return nil, nil, fmt.Errorf("no OCSP server specified in certificate") - } - - respURL := certificate.OCSPServer[0] - ocspReq, err := ocsp.CreateRequest(issuedCertificate, issuerCertificate, nil) - if err != nil { - return nil, nil, fmt.Errorf("creating OCSP request: %w", err) - } - - reader := bytes.NewReader(ocspReq) - req, err := http.Post(respURL, "application/ocsp-request", reader) - if err != nil { - return nil, nil, fmt.Errorf("making OCSP request: %w", err) - } - defer req.Body.Close() - - ocspResBytes, err := ioutil.ReadAll(io.LimitReader(req.Body, 1024*1024)) - if err != nil { - return nil, nil, fmt.Errorf("reading OCSP response: %w", err) - } - - ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCertificate) - if err != nil { - return nil, nil, fmt.Errorf("parsing OCSP response: %w", err) - } - - return ocspResBytes, ocspRes, nil -} - -// StapleOCSP populates the ocsp response of the certificate if needed and not disabled by configuration. -func (c *Certificate) StapleOCSP() error { - if c.OCSP.DisableStapling { - return nil - } - - ocspResponse := c.OCSPResponse - if ocspResponse != nil && time.Now().Before(ocspResponse.ThisUpdate.Add(ocspResponse.NextUpdate.Sub(ocspResponse.ThisUpdate)/2)) { - return nil - } - - leaf, _ := x509.ParseCertificate(c.Certificate.Certificate[0]) - var issuerCertificate *x509.Certificate - if len(c.Certificate.Certificate) == 1 { - issuerCertificate = leaf - } else { - ic, err := x509.ParseCertificate(c.Certificate.Certificate[1]) - if err != nil { - return fmt.Errorf("cannot parse issuer certificate for %v: %w", c.SANs, err) - } - - issuerCertificate = ic - } - - ocspBytes, ocspResp, ocspErr := getOCSPForCert(c, leaf, issuerCertificate) - if ocspErr != nil { - return fmt.Errorf("no OCSP stapling for %v: %w", c.SANs, ocspErr) - } - - log.WithoutContext().Debugf("ocsp response: %v", ocspResp) - if ocspResp.Status == ocsp.Good { - if ocspResp.NextUpdate.After(leaf.NotAfter) { - return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)", c.SANs, leaf.NotAfter.Sub(ocspResp.NextUpdate)) - } - - c.Certificate.OCSPStaple = ocspBytes - c.OCSPResponse = ocspResp - } - - return nil -} - // GetCertificate returns a tls.Certificate matching the configured CertFile and KeyFile. func (c *Certificate) GetCertificate() (tls.Certificate, error) { certContent, err := c.CertFile.Read() @@ -304,45 +153,6 @@ func (c *Certificate) GetTruncatedCertificateName() string { return certName } -// String is the method to format the flag's value, part of the flag.Value interface. -// The String method's output will be used in diagnostics. -func (c *Certificates) String() string { - if len(*c) == 0 { - return "" - } - var result []string - for _, certificate := range *c { - result = append(result, certificate.CertFile.String()+","+certificate.KeyFile.String()) - } - return strings.Join(result, ";") -} - -// 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: FileOrContent(files[0]), - KeyFile: FileOrContent(files[1]), - OCSP: OCSPConfig{ - DisableStapling: false, - }, - }) - } - return nil -} - -// Type is type of the struct. -func (c *Certificates) Type() string { - return "certificates" -} - // VerifyPeerCertificate verifies the chain certificates and their URI. func VerifyPeerCertificate(uri string, cfg *tls.Config, rawCerts [][]byte) error { // TODO: Refactor to avoid useless verifyChain (ex: when insecureskipverify is false) diff --git a/pkg/tls/certificate_store.go b/pkg/tls/certificate_store.go index 65fae2a9d..e3bed5002 100644 --- a/pkg/tls/certificate_store.go +++ b/pkg/tls/certificate_store.go @@ -63,7 +63,7 @@ func (c CertificateStore) GetAllDomains() []string { // Get dynamic certificates if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil { - for domain := range c.DynamicCerts.Get().(map[string]*Certificate) { + for domain := range c.DynamicCerts.Get().(map[string]*Cert) { allDomains = append(allDomains, domain) } } @@ -72,7 +72,7 @@ func (c CertificateStore) GetAllDomains() []string { } // GetBestCertificate returns the best match certificate, and caches the response. -func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *Certificate { +func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) *Cert { if c == nil { return nil } @@ -87,12 +87,12 @@ func (c *CertificateStore) GetBestCertificate(clientHello *tls.ClientHelloInfo) } if cert, ok := c.CertCache.Get(serverName); ok { - return cert.(*Certificate) + return cert.(*Cert) } - matchedCerts := map[string]*Certificate{} + matchedCerts := map[string]*Cert{} if c.DynamicCerts != nil && c.DynamicCerts.Get() != nil { - for domains, cert := range c.DynamicCerts.Get().(map[string]*Certificate) { + for domains, cert := range c.DynamicCerts.Get().(map[string]*Cert) { for _, certDomain := range strings.Split(domains, ",") { if matchDomain(serverName, certDomain) { matchedCerts[certDomain] = cert diff --git a/pkg/tls/certificate_store_test.go b/pkg/tls/certificate_store_test.go index 8fc414db8..ce935cf32 100644 --- a/pkg/tls/certificate_store_test.go +++ b/pkg/tls/certificate_store_test.go @@ -59,7 +59,7 @@ func TestGetBestCertificate(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - dynamicMap := map[string]*Certificate{} + dynamicMap := map[string]*Cert{} if test.dynamicCert != "" { cert, err := loadTestCert(test.dynamicCert, test.uppercase) @@ -72,7 +72,7 @@ func TestGetBestCertificate(t *testing.T) { CertCache: cache.New(1*time.Hour, 10*time.Minute), } - var expected *Certificate + var expected *Cert if test.expectedCert != "" { cert, err := loadTestCert(test.expectedCert, test.uppercase) require.NoError(t, err) @@ -89,7 +89,7 @@ func TestGetBestCertificate(t *testing.T) { } } -func loadTestCert(certName string, uppercase bool) (*Certificate, error) { +func loadTestCert(certName string, uppercase bool) (*Cert, error) { replacement := "wildcard" if uppercase { replacement = "uppercase_wildcard" @@ -103,8 +103,7 @@ func loadTestCert(certName string, uppercase bool) (*Certificate, error) { return nil, err } - return &Certificate{ + return &Cert{ Certificate: &staticCert, - SANs: []string{}, }, nil } diff --git a/pkg/tls/tls_certificate.go b/pkg/tls/tls_certificate.go new file mode 100644 index 000000000..bf0a855d5 --- /dev/null +++ b/pkg/tls/tls_certificate.go @@ -0,0 +1,217 @@ +package tls + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "io/ioutil" + "net/http" + "sort" + "strings" + "time" + + "github.com/traefik/traefik/v2/pkg/log" + "github.com/traefik/traefik/v2/pkg/tls/generate" + "golang.org/x/crypto/ocsp" +) + +// Cert holds runtime data for runtime TLS certificate handling. +type Cert struct { + config *Certificate + Certificate *tls.Certificate + OCSPServer []string + OCSPResponse *ocsp.Response +} + +// Certs defines traefik Certs type +// Certs and Keys could be either a file path, or the file content itself. +type Certs []Cert + +// CreateTLSConfig creates a TLS config from Certificate structures. + +// AppendCertificate appends a Cert to a certificates map keyed by entrypoint. +func (c *Cert) AppendCertificate(certs map[string]map[string]*Cert, storeName string) error { + certContent, err := c.config.CertFile.Read() + if err != nil { + return fmt.Errorf("unable to read CertFile : %w", err) + } + + keyContent, err := c.config.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 { + sort.Strings(parsedCert.DNSNames) + 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())) + } + } + } + certKey := strings.Join(SANs, ",") + + certExists := false + if certs[storeName] == nil { + certs[storeName] = make(map[string]*Cert) + } else { + for domains := range certs[storeName] { + if domains == certKey { + certExists = true + break + } + } + } + + if certExists { + log.Debugf("Skipping addition of certificate for domain(s) %q, to EntryPoint %s, as it already exists for this Entrypoint.", certKey, storeName) + } else { + log.Debugf("Adding certificate for domain(s) %s", certKey) + + certs[storeName][certKey] = &Cert{ + Certificate: &tlsCert, + OCSPServer: parsedCert.OCSPServer, + config: &Certificate{ + SANs: SANs, + OCSP: OCSPConfig{ + DisableStapling: c.config.OCSP.DisableStapling, + }, + }, + } + } + + return err +} + +func getOCSPForCert(certificate *Cert, issuedCertificate *x509.Certificate, issuerCertificate *x509.Certificate) ([]byte, *ocsp.Response, error) { + if len(certificate.OCSPServer) == 0 { + return nil, nil, fmt.Errorf("no OCSP server specified in certificate") + } + + respURL := certificate.OCSPServer[0] + ocspReq, err := ocsp.CreateRequest(issuedCertificate, issuerCertificate, nil) + if err != nil { + return nil, nil, fmt.Errorf("creating OCSP request: %w", err) + } + + reader := bytes.NewReader(ocspReq) + req, err := http.Post(respURL, "application/ocsp-request", reader) + if err != nil { + return nil, nil, fmt.Errorf("making OCSP request: %w", err) + } + defer req.Body.Close() + + ocspResBytes, err := ioutil.ReadAll(io.LimitReader(req.Body, 1024*1024)) + if err != nil { + return nil, nil, fmt.Errorf("reading OCSP response: %w", err) + } + + ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCertificate) + if err != nil { + return nil, nil, fmt.Errorf("parsing OCSP response: %w", err) + } + + return ocspResBytes, ocspRes, nil +} + +// StapleOCSP populates the ocsp response of the certificate if needed and not disabled by configuration. +func (c *Cert) StapleOCSP() error { + if c.config.OCSP.DisableStapling { + return nil + } + + ocspResponse := c.OCSPResponse + if ocspResponse != nil && time.Now().Before(ocspResponse.ThisUpdate.Add(ocspResponse.NextUpdate.Sub(ocspResponse.ThisUpdate)/2)) { + return nil + } + + leaf, _ := x509.ParseCertificate(c.Certificate.Certificate[0]) + var issuerCertificate *x509.Certificate + if len(c.Certificate.Certificate) == 1 { + issuerCertificate = leaf + } else { + ic, err := x509.ParseCertificate(c.Certificate.Certificate[1]) + if err != nil { + return fmt.Errorf("cannot parse issuer certificate for %v: %w", c.config.SANs, err) + } + + issuerCertificate = ic + } + + ocspBytes, ocspResp, ocspErr := getOCSPForCert(c, leaf, issuerCertificate) + if ocspErr != nil { + return fmt.Errorf("no OCSP stapling for %v: %w", c.config.SANs, ocspErr) + } + + log.WithoutContext().Debugf("ocsp response: %v", ocspResp) + if ocspResp.Status == ocsp.Good { + if ocspResp.NextUpdate.After(leaf.NotAfter) { + return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)", c.config.SANs, leaf.NotAfter.Sub(ocspResp.NextUpdate)) + } + + c.Certificate.OCSPStaple = ocspBytes + c.OCSPResponse = ocspResp + } + + return nil +} + +// String is the method to format the flag's value, part of the flag.Value interface. +// The String method's output will be used in diagnostics. +func (c *Certs) String() string { + if len(*c) == 0 { + return "" + } + var result []string + for _, certificate := range *c { + result = append(result, certificate.config.CertFile.String()+","+certificate.config.KeyFile.String()) + } + return strings.Join(result, ";") +} + +// 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 *Certs) Set(value string) error { + TLSCertificates := strings.Split(value, ";") + for _, certificate := range TLSCertificates { + files := strings.Split(certificate, ",") + if len(files) != 2 { + return fmt.Errorf("bad Certs format: %s", value) + } + *c = append(*c, Cert{ + config: &Certificate{ + CertFile: FileOrContent(files[0]), + KeyFile: FileOrContent(files[1]), + OCSP: OCSPConfig{ + DisableStapling: false, + }, + }, + }) + } + return nil +} + +// Type is type of the struct. +func (c *Certs) Type() string { + return "Certs" +} diff --git a/pkg/tls/tlsmanager.go b/pkg/tls/tlsmanager.go index 6102fa4be..131adea24 100644 --- a/pkg/tls/tlsmanager.go +++ b/pkg/tls/tlsmanager.go @@ -83,7 +83,7 @@ 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]*Cert) for _, conf := range certs { if len(conf.Stores) == 0 { if log.GetLevel() >= logrus.DebugLevel { @@ -100,7 +100,8 @@ func (m *Manager) UpdateConfigs(ctx context.Context, stores map[string]Store, co m.storesConfig[store] = Store{} } - err := conf.Certificate.AppendCertificate(storesCertificates, store) + cert := Cert{config: &conf.Certificate} + err := cert.AppendCertificate(storesCertificates, store) if err != nil { log.FromContext(ctxStore).Errorf("Unable to append certificate %s to store: %v", conf.Certificate.GetTruncatedCertificateName(), err) } @@ -241,7 +242,7 @@ func (m *Manager) GetCertificates() []*x509.Certificate { // We iterate over all the certificates. for _, store := range m.stores { if store.DynamicCerts != nil && store.DynamicCerts.Get() != nil { - for _, cert := range store.DynamicCerts.Get().(map[string]*Certificate) { + for _, cert := range store.DynamicCerts.Get().(map[string]*Cert) { x509Cert, err := x509.ParseCertificate(cert.Certificate.Certificate[0]) if err != nil { continue diff --git a/pkg/tls/tlsmanager_test.go b/pkg/tls/tlsmanager_test.go index 5035108a7..99d558f02 100644 --- a/pkg/tls/tlsmanager_test.go +++ b/pkg/tls/tlsmanager_test.go @@ -79,7 +79,7 @@ func TestTLSInStore(t *testing.T) { tlsManager := NewManager() tlsManager.UpdateConfigs(context.Background(), nil, nil, dynamicConfigs) - certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*Certificate) + certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*Cert) if len(certs) == 0 { t.Fatal("got error: default store must have TLS certificates.") } @@ -104,7 +104,7 @@ func TestTLSInvalidStore(t *testing.T) { }, }, nil, dynamicConfigs) - certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*Certificate) + certs := tlsManager.GetStore("default").DynamicCerts.Get().(map[string]*Cert) if len(certs) == 0 { t.Fatal("got error: default store must have TLS certificates.") } diff --git a/pkg/tls/zz_generated.deepcopy.go b/pkg/tls/zz_generated.deepcopy.go index d22feadbe..98a15f00d 100644 --- a/pkg/tls/zz_generated.deepcopy.go +++ b/pkg/tls/zz_generated.deepcopy.go @@ -55,6 +55,28 @@ func (in *CertAndStores) DeepCopy() *CertAndStores { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Certificate) DeepCopyInto(out *Certificate) { + *out = *in + out.OCSP = in.OCSP + if in.SANs != nil { + in, out := &in.SANs, &out.SANs + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate. +func (in *Certificate) DeepCopy() *Certificate { + if in == nil { + return nil + } + out := new(Certificate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientAuth) DeepCopyInto(out *ClientAuth) { *out = *in @@ -96,6 +118,20 @@ func (in *GeneratedCert) DeepCopy() *GeneratedCert { in.DeepCopyInto(out) return out } +func (in *OCSPConfig) DeepCopyInto(out *OCSPConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCSPConfig. +func (in *OCSPConfig) DeepCopy() *OCSPConfig { + if in == nil { + return nil + } + out := new(OCSPConfig) + in.DeepCopyInto(out) + return out +} // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Options) DeepCopyInto(out *Options) {