mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-30 22:11:07 +00:00 
			
		
		
		
	* use certmagic for more extensible/robust ACME cert handling * accept TOS based on config option Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv>
		
			
				
	
	
		
			404 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			404 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // 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 (
 | |
| 	"crypto/tls"
 | |
| 	"crypto/x509"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"net"
 | |
| 	"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.
 | |
| 	ocsp *ocsp.Response
 | |
| 
 | |
| 	// The hex-encoded hash of this cert's chain's bytes.
 | |
| 	hash string
 | |
| 
 | |
| 	// Whether this certificate is under our management
 | |
| 	managed bool
 | |
| }
 | |
| 
 | |
| // 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, cert.Leaf.NotAfter, 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(cert.Leaf.NotAfter)
 | |
| }
 | |
| 
 | |
| // 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
 | |
| }
 | |
| 
 | |
| // 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(domain string) (Certificate, error) {
 | |
| 	cert, err := cfg.loadManagedCertificate(domain)
 | |
| 	if err != nil {
 | |
| 		return cert, err
 | |
| 	}
 | |
| 	cfg.certCache.cacheCertificate(cert)
 | |
| 	cfg.emit("cached_managed_cert", cert.Names)
 | |
| 	return cert, nil
 | |
| }
 | |
| 
 | |
| // loadManagedCertificate loads the managed certificate for domain,
 | |
| // but it does not add it to the cache. It just loads from storage.
 | |
| func (cfg *Config) loadManagedCertificate(domain string) (Certificate, error) {
 | |
| 	certRes, err := cfg.loadCertResource(domain)
 | |
| 	if err != nil {
 | |
| 		return Certificate{}, err
 | |
| 	}
 | |
| 	cert, err := cfg.makeCertificateWithOCSP(certRes.CertificatePEM, certRes.PrivateKeyPEM)
 | |
| 	if err != nil {
 | |
| 		return cert, err
 | |
| 	}
 | |
| 	cert.managed = true
 | |
| 	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.
 | |
| //
 | |
| // This method is safe for concurrent use.
 | |
| func (cfg *Config) CacheUnmanagedCertificatePEMFile(certFile, keyFile string, tags []string) error {
 | |
| 	cert, err := cfg.makeCertificateFromDiskWithOCSP(cfg.Storage, certFile, keyFile)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	cert.Tags = tags
 | |
| 	cfg.certCache.cacheCertificate(cert)
 | |
| 	cfg.emit("cached_unmanaged_cert", cert.Names)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // CacheUnmanagedTLSCertificate adds tlsCert to the certificate cache.
 | |
| // It staples OCSP if possible.
 | |
| //
 | |
| // This method is safe for concurrent use.
 | |
| func (cfg *Config) CacheUnmanagedTLSCertificate(tlsCert tls.Certificate, tags []string) error {
 | |
| 	var cert Certificate
 | |
| 	err := fillCertFromLeaf(&cert, tlsCert)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	_, err = stapleOCSP(cfg.Storage, &cert, nil)
 | |
| 	if err != nil && cfg.Logger != nil {
 | |
| 		cfg.Logger.Warn("stapling OCSP", zap.Error(err))
 | |
| 	}
 | |
| 	cfg.emit("cached_unmanaged_cert", cert.Names)
 | |
| 	cert.Tags = tags
 | |
| 	cfg.certCache.cacheCertificate(cert)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // CacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
 | |
| // of the certificate and key, then caches it in memory.
 | |
| //
 | |
| // This method is safe for concurrent use.
 | |
| func (cfg *Config) CacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte, tags []string) error {
 | |
| 	cert, err := cfg.makeCertificateWithOCSP(certBytes, keyBytes)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	cert.Tags = tags
 | |
| 	cfg.certCache.cacheCertificate(cert)
 | |
| 	cfg.emit("cached_unmanaged_cert", cert.Names)
 | |
| 	return 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(storage Storage, certFile, keyFile string) (Certificate, error) {
 | |
| 	certPEMBlock, err := ioutil.ReadFile(certFile)
 | |
| 	if err != nil {
 | |
| 		return Certificate{}, err
 | |
| 	}
 | |
| 	keyPEMBlock, err := ioutil.ReadFile(keyFile)
 | |
| 	if err != nil {
 | |
| 		return Certificate{}, err
 | |
| 	}
 | |
| 	return cfg.makeCertificateWithOCSP(certPEMBlock, keyPEMBlock)
 | |
| }
 | |
| 
 | |
| // makeCertificateWithOCSP is the same as makeCertificate except that it also
 | |
| // staples OCSP to the certificate.
 | |
| func (cfg Config) makeCertificateWithOCSP(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
 | |
| 	cert, err := makeCertificate(certPEMBlock, keyPEMBlock)
 | |
| 	if err != nil {
 | |
| 		return cert, err
 | |
| 	}
 | |
| 	_, err = stapleOCSP(cfg.Storage, &cert, certPEMBlock)
 | |
| 	if err != nil && cfg.Logger != nil {
 | |
| 		cfg.Logger.Warn("stapling OCSP", zap.Error(err))
 | |
| 	}
 | |
| 	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, 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")
 | |
| 	}
 | |
| 
 | |
| 	// save the hash of this certificate (chain) and
 | |
| 	// expiration date, for necessity and efficiency
 | |
| 	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(cert Certificate) (bool, error) {
 | |
| 	certRes, err := cfg.loadCertResource(cert.Names[0])
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	tlsCert, err := tls.X509KeyPair(certRes.CertificatePEM, certRes.PrivateKeyPEM)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	return currentlyInRenewalWindow(leaf.NotBefore, leaf.NotAfter, cfg.RenewalWindowRatio), 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.
 | |
| func (cfg *Config) reloadManagedCertificate(oldCert Certificate) error {
 | |
| 	if cfg.Logger != nil {
 | |
| 		cfg.Logger.Info("reloading managed certificate", zap.Strings("identifiers", oldCert.Names))
 | |
| 	}
 | |
| 	newCert, err := cfg.loadManagedCertificate(oldCert.Names[0])
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("loading managed certificate for %v from storage: %v", oldCert.Names, err)
 | |
| 	}
 | |
| 	cfg.certCache.replaceCertificate(oldCert, newCert)
 | |
| 	return 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)
 | |
| // - 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
 | |
| 		(!strings.Contains(subj, "*") || strings.HasPrefix(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 certificate
 | |
| 	return SubjectQualifiesForCert(subj) &&
 | |
| 
 | |
| 		// localhost is ineligible
 | |
| 		subj != "localhost" &&
 | |
| 
 | |
| 		// .localhost TLD is ineligible
 | |
| 		!strings.HasSuffix(subj, ".localhost") &&
 | |
| 
 | |
| 		// .local TLD is ineligible
 | |
| 		!strings.HasSuffix(subj, ".local") &&
 | |
| 
 | |
| 		// only one wildcard label allowed, and it must be left-most
 | |
| 		(!strings.Contains(subj, "*") ||
 | |
| 			(strings.Count(subj, "*") == 1 &&
 | |
| 				len(subj) > 2 &&
 | |
| 				strings.HasPrefix(subj, "*."))) &&
 | |
| 
 | |
| 		// cannot be an IP address (as of yet), see
 | |
| 		// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
 | |
| 		net.ParseIP(subj) == nil
 | |
| }
 | |
| 
 | |
| // MatchWildcard returns true if subject (a candidate DNS name)
 | |
| // matches wildcard (a reference DNS name), mostly according to
 | |
| // RFC6125-compliant wildcard rules.
 | |
| func MatchWildcard(subject, wildcard string) bool {
 | |
| 	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
 | |
| }
 |