forked from forgejo/forgejo
Use caddy's certmagic library for extensible/robust ACME handling (#14177)
* 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>
This commit is contained in:
parent
bc05ddc0eb
commit
d2ea21d0d8
437 changed files with 56286 additions and 4270 deletions
404
vendor/github.com/caddyserver/certmagic/certificates.go
generated
vendored
Normal file
404
vendor/github.com/caddyserver/certmagic/certificates.go
generated
vendored
Normal file
|
@ -0,0 +1,404 @@
|
|||
// 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue