// Copyright 2015 Matthew Holt // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package certmagic import ( "context" "crypto/tls" "crypto/x509" "fmt" "net" "os" "strings" "time" "go.uber.org/zap" "golang.org/x/crypto/ocsp" ) // Certificate is a tls.Certificate with associated metadata tacked on. // Even if the metadata can be obtained by parsing the certificate, // we are more efficient by extracting the metadata onto this struct, // but at the cost of slightly higher memory use. type Certificate struct { tls.Certificate // Names is the list of subject names this // certificate is signed for. Names []string // Optional; user-provided, and arbitrary. Tags []string // OCSP contains the certificate's parsed OCSP response. // It is not necessarily the response that is stapled // (e.g. if the status is not Good), it is simply the // most recent OCSP response we have for this certificate. ocsp *ocsp.Response // The hex-encoded hash of this cert's chain's DER bytes. hash string // Whether this certificate is under our management. managed bool // The unique string identifying the issuer of this certificate. issuerKey string } // Empty returns true if the certificate struct is not filled out; at // least the tls.Certificate.Certificate field is expected to be set. func (cert Certificate) Empty() bool { return len(cert.Certificate.Certificate) == 0 } // Hash returns a checksum of the certificate chain's DER-encoded bytes. func (cert Certificate) Hash() string { return cert.hash } // NeedsRenewal returns true if the certificate is // expiring soon (according to cfg) or has expired. func (cert Certificate) NeedsRenewal(cfg *Config) bool { return currentlyInRenewalWindow(cert.Leaf.NotBefore, expiresAt(cert.Leaf), cfg.RenewalWindowRatio) } // Expired returns true if the certificate has expired. func (cert Certificate) Expired() bool { if cert.Leaf == nil { // ideally cert.Leaf would never be nil, but this can happen for // "synthetic" certs like those made to solve the TLS-ALPN challenge // which adds a special cert directly to the cache, since // tls.X509KeyPair() discards the leaf; oh well return false } return time.Now().After(expiresAt(cert.Leaf)) } // currentlyInRenewalWindow returns true if the current time is // within the renewal window, according to the given start/end // dates and the ratio of the renewal window. If true is returned, // the certificate being considered is due for renewal. func currentlyInRenewalWindow(notBefore, notAfter time.Time, renewalWindowRatio float64) bool { if notAfter.IsZero() { return false } lifetime := notAfter.Sub(notBefore) if renewalWindowRatio == 0 { renewalWindowRatio = DefaultRenewalWindowRatio } renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio) renewalWindowStart := notAfter.Add(-renewalWindow) return time.Now().After(renewalWindowStart) } // HasTag returns true if cert.Tags has tag. func (cert Certificate) HasTag(tag string) bool { for _, t := range cert.Tags { if t == tag { return true } } return false } // expiresAt return the time that a certificate expires. Account for the 1s // resolution of ASN.1 UTCTime/GeneralizedTime by including the extra fraction // of a second of certificate validity beyond the NotAfter value. func expiresAt(cert *x509.Certificate) time.Time { if cert == nil { return time.Time{} } return cert.NotAfter.Truncate(time.Second).Add(1 * time.Second) } // CacheManagedCertificate loads the certificate for domain into the // cache, from the TLS storage for managed certificates. It returns a // copy of the Certificate that was put into the cache. // // This is a lower-level method; normally you'll call Manage() instead. // // This method is safe for concurrent use. func (cfg *Config) CacheManagedCertificate(ctx context.Context, domain string) (Certificate, error) { cert, err := cfg.loadManagedCertificate(ctx, domain) if err != nil { return cert, err } cfg.certCache.cacheCertificate(cert) cfg.emit(ctx, "cached_managed_cert", map[string]any{"sans": cert.Names}) return cert, nil } // loadManagedCertificate loads the managed certificate for domain from any // of the configured issuers' storage locations, but it does not add it to // the cache. It just loads from storage and returns it. func (cfg *Config) loadManagedCertificate(ctx context.Context, domain string) (Certificate, error) { certRes, err := cfg.loadCertResourceAnyIssuer(ctx, domain) if err != nil { return Certificate{}, err } cert, err := cfg.makeCertificateWithOCSP(ctx, certRes.CertificatePEM, certRes.PrivateKeyPEM) if err != nil { return cert, err } cert.managed = true cert.issuerKey = certRes.issuerKey return cert, nil } // CacheUnmanagedCertificatePEMFile loads a certificate for host using certFile // and keyFile, which must be in PEM format. It stores the certificate in // the in-memory cache and returns the hash, useful for removing from the cache. // // This method is safe for concurrent use. func (cfg *Config) CacheUnmanagedCertificatePEMFile(ctx context.Context, certFile, keyFile string, tags []string) (string, error) { cert, err := cfg.makeCertificateFromDiskWithOCSP(ctx, cfg.Storage, certFile, keyFile) if err != nil { return "", err } cert.Tags = tags cfg.certCache.cacheCertificate(cert) cfg.emit(ctx, "cached_unmanaged_cert", map[string]any{"sans": cert.Names}) return cert.hash, nil } // CacheUnmanagedTLSCertificate adds tlsCert to the certificate cache // // and returns the hash, useful for removing from the cache. // // It staples OCSP if possible. // // This method is safe for concurrent use. func (cfg *Config) CacheUnmanagedTLSCertificate(ctx context.Context, tlsCert tls.Certificate, tags []string) (string, error) { var cert Certificate err := fillCertFromLeaf(&cert, tlsCert) if err != nil { return "", err } err = stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, nil) if err != nil { cfg.Logger.Warn("stapling OCSP", zap.Error(err)) } cfg.emit(ctx, "cached_unmanaged_cert", map[string]any{"sans": cert.Names}) cert.Tags = tags cfg.certCache.cacheCertificate(cert) return cert.hash, nil } // CacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes // of the certificate and key, then caches it in memory, and returns the hash, // which is useful for removing from the cache. // // This method is safe for concurrent use. func (cfg *Config) CacheUnmanagedCertificatePEMBytes(ctx context.Context, certBytes, keyBytes []byte, tags []string) (string, error) { cert, err := cfg.makeCertificateWithOCSP(ctx, certBytes, keyBytes) if err != nil { return "", err } cert.Tags = tags cfg.certCache.cacheCertificate(cert) cfg.emit(ctx, "cached_unmanaged_cert", map[string]any{"sans": cert.Names}) return cert.hash, nil } // makeCertificateFromDiskWithOCSP makes a Certificate by loading the // certificate and key files. It fills out all the fields in // the certificate except for the Managed and OnDemand flags. // (It is up to the caller to set those.) It staples OCSP. func (cfg Config) makeCertificateFromDiskWithOCSP(ctx context.Context, storage Storage, certFile, keyFile string) (Certificate, error) { certPEMBlock, err := os.ReadFile(certFile) if err != nil { return Certificate{}, err } keyPEMBlock, err := os.ReadFile(keyFile) if err != nil { return Certificate{}, err } return cfg.makeCertificateWithOCSP(ctx, certPEMBlock, keyPEMBlock) } // makeCertificateWithOCSP is the same as makeCertificate except that it also // staples OCSP to the certificate. func (cfg Config) makeCertificateWithOCSP(ctx context.Context, certPEMBlock, keyPEMBlock []byte) (Certificate, error) { cert, err := makeCertificate(certPEMBlock, keyPEMBlock) if err != nil { return cert, err } err = stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, certPEMBlock) if err != nil { cfg.Logger.Warn("stapling OCSP", zap.Error(err), zap.Strings("identifiers", cert.Names)) } return cert, nil } // makeCertificate turns a certificate PEM bundle and a key PEM block into // a Certificate with necessary metadata from parsing its bytes filled into // its struct fields for convenience (except for the OnDemand and Managed // flags; it is up to the caller to set those properties!). This function // does NOT staple OCSP. func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { var cert Certificate // Convert to a tls.Certificate tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) if err != nil { return cert, err } // Extract necessary metadata err = fillCertFromLeaf(&cert, tlsCert) if err != nil { return cert, err } return cert, nil } // fillCertFromLeaf populates cert from tlsCert. If it succeeds, it // guarantees that cert.Leaf is non-nil. func fillCertFromLeaf(cert *Certificate, tlsCert tls.Certificate) error { if len(tlsCert.Certificate) == 0 { return fmt.Errorf("certificate is empty") } cert.Certificate = tlsCert // the leaf cert should be the one for the site; we must set // the tls.Certificate.Leaf field so that TLS handshakes are // more efficient leaf := cert.Certificate.Leaf if leaf == nil { var err error leaf, err = x509.ParseCertificate(tlsCert.Certificate[0]) if err != nil { return err } cert.Certificate.Leaf = leaf } // for convenience, we do want to assemble all the // subjects on the certificate into one list if leaf.Subject.CommonName != "" { // TODO: CommonName is deprecated cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)} } for _, name := range leaf.DNSNames { if name != leaf.Subject.CommonName { // TODO: CommonName is deprecated cert.Names = append(cert.Names, strings.ToLower(name)) } } for _, ip := range leaf.IPAddresses { if ipStr := ip.String(); ipStr != leaf.Subject.CommonName { // TODO: CommonName is deprecated cert.Names = append(cert.Names, strings.ToLower(ipStr)) } } for _, email := range leaf.EmailAddresses { if email != leaf.Subject.CommonName { // TODO: CommonName is deprecated cert.Names = append(cert.Names, strings.ToLower(email)) } } for _, u := range leaf.URIs { if u.String() != leaf.Subject.CommonName { // TODO: CommonName is deprecated cert.Names = append(cert.Names, u.String()) } } if len(cert.Names) == 0 { return fmt.Errorf("certificate has no names") } cert.hash = hashCertificateChain(cert.Certificate.Certificate) return nil } // managedCertInStorageExpiresSoon returns true if cert (being a // managed certificate) is expiring within RenewDurationBefore. // It returns false if there was an error checking the expiration // of the certificate as found in storage, or if the certificate // in storage is NOT expiring soon. A certificate that is expiring // soon in our cache but is not expiring soon in storage probably // means that another instance renewed the certificate in the // meantime, and it would be a good idea to simply load the cert // into our cache rather than repeating the renewal process again. func (cfg *Config) managedCertInStorageExpiresSoon(ctx context.Context, cert Certificate) (bool, error) { certRes, err := cfg.loadCertResourceAnyIssuer(ctx, cert.Names[0]) if err != nil { return false, err } _, needsRenew := cfg.managedCertNeedsRenewal(certRes) return needsRenew, nil } // reloadManagedCertificate reloads the certificate corresponding to the name(s) // on oldCert into the cache, from storage. This also replaces the old certificate // with the new one, so that all configurations that used the old cert now point // to the new cert. It assumes that the new certificate for oldCert.Names[0] is // already in storage. It returns the newly-loaded certificate if successful. func (cfg *Config) reloadManagedCertificate(ctx context.Context, oldCert Certificate) (Certificate, error) { cfg.Logger.Info("reloading managed certificate", zap.Strings("identifiers", oldCert.Names)) newCert, err := cfg.loadManagedCertificate(ctx, oldCert.Names[0]) if err != nil { return Certificate{}, fmt.Errorf("loading managed certificate for %v from storage: %v", oldCert.Names, err) } cfg.certCache.replaceCertificate(oldCert, newCert) return newCert, nil } // SubjectQualifiesForCert returns true if subj is a name which, // as a quick sanity check, looks like it could be the subject // of a certificate. Requirements are: // - must not be empty // - must not start or end with a dot (RFC 1034; RFC 6066 section 3) // - must not contain common accidental special characters func SubjectQualifiesForCert(subj string) bool { // must not be empty return strings.TrimSpace(subj) != "" && // must not start or end with a dot !strings.HasPrefix(subj, ".") && !strings.HasSuffix(subj, ".") && // if it has a wildcard, must be a left-most label (or exactly "*" // which won't be trusted by browsers but still technically works) (!strings.Contains(subj, "*") || strings.HasPrefix(subj, "*.") || subj == "*") && // must not contain other common special characters !strings.ContainsAny(subj, "()[]{}<> \t\n\"\\!@#$%^&|;'+=") } // SubjectQualifiesForPublicCert returns true if the subject // name appears eligible for automagic TLS with a public // CA such as Let's Encrypt. For example: localhost and IP // addresses are not eligible because we cannot obtain certs // for those names with a public CA. Wildcard names are // allowed, as long as they conform to CABF requirements (only // one wildcard label, and it must be the left-most label). func SubjectQualifiesForPublicCert(subj string) bool { // must at least qualify for a certificate return SubjectQualifiesForCert(subj) && // localhost, .localhost TLD, and .local TLD are ineligible !SubjectIsInternal(subj) && // cannot be an IP address (as of yet), see // https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt !SubjectIsIP(subj) && // only one wildcard label allowed, and it must be left-most, with 3+ labels (!strings.Contains(subj, "*") || (strings.Count(subj, "*") == 1 && strings.Count(subj, ".") > 1 && len(subj) > 2 && strings.HasPrefix(subj, "*."))) } // SubjectIsIP returns true if subj is an IP address. func SubjectIsIP(subj string) bool { return net.ParseIP(subj) != nil } // SubjectIsInternal returns true if subj is an internal-facing // hostname or address. func SubjectIsInternal(subj string) bool { return subj == "localhost" || strings.HasSuffix(subj, ".localhost") || strings.HasSuffix(subj, ".local") } // MatchWildcard returns true if subject (a candidate DNS name) // matches wildcard (a reference DNS name), mostly according to // RFC 6125-compliant wildcard rules. See also RFC 2818 which // states that IP addresses must match exactly, but this function // does not attempt to distinguish IP addresses from internal or // external DNS names that happen to look like IP addresses. // It uses DNS wildcard matching logic and is case-insensitive. // https://tools.ietf.org/html/rfc2818#section-3.1 func MatchWildcard(subject, wildcard string) bool { subject, wildcard = strings.ToLower(subject), strings.ToLower(wildcard) if subject == wildcard { return true } if !strings.Contains(wildcard, "*") { return false } labels := strings.Split(subject, ".") for i := range labels { if labels[i] == "" { continue // invalid label } labels[i] = "*" candidate := strings.Join(labels, ".") if candidate == wildcard { return true } } return false }