aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/caddyserver/certmagic/acmemanager.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/caddyserver/certmagic/acmemanager.go')
-rw-r--r--vendor/github.com/caddyserver/certmagic/acmemanager.go154
1 files changed, 135 insertions, 19 deletions
diff --git a/vendor/github.com/caddyserver/certmagic/acmemanager.go b/vendor/github.com/caddyserver/certmagic/acmemanager.go
index 6820b4f918..82b6cc12ef 100644
--- a/vendor/github.com/caddyserver/certmagic/acmemanager.go
+++ b/vendor/github.com/caddyserver/certmagic/acmemanager.go
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "sort"
"strings"
"time"
@@ -19,7 +20,7 @@ import (
// Issuer, and Revoker interfaces.
//
// It is NOT VALID to use an ACMEManager without calling NewACMEManager().
-// It fills in default values from DefaultACME as well as setting up
+// It fills in any default values from DefaultACME as well as setting up
// internal state that is necessary for valid use. Always call
// NewACMEManager() to get a valid ACMEManager value.
type ACMEManager struct {
@@ -37,6 +38,12 @@ type ACMEManager struct {
// selecting an existing ACME server account
Email string
+ // The PEM-encoded private key of the ACME
+ // account to use; only needed if the account
+ // is already created on the server and
+ // can be looked up with the ACME protocol
+ AccountKeyPEM string
+
// Set to true if agreed to the CA's
// subscriber agreement
Agreed bool
@@ -92,9 +99,13 @@ type ACMEManager struct {
// Callback function that is called before a
// new ACME account is registered with the CA;
// it allows for last-second config changes
- // of the ACMEManager (TODO: this feature is
- // still EXPERIMENTAL and subject to change)
- NewAccountFunc func(context.Context, *ACMEManager, acme.Account) error
+ // of the ACMEManager and the Account.
+ // (TODO: this feature is still EXPERIMENTAL and subject to change)
+ NewAccountFunc func(context.Context, *ACMEManager, acme.Account) (acme.Account, error)
+
+ // Preferences for selecting alternate
+ // certificate chains
+ PreferredChains ChainPreference
// Set a logger to enable logging
Logger *zap.Logger
@@ -105,10 +116,12 @@ type ACMEManager struct {
// NewACMEManager constructs a valid ACMEManager based on a template
// configuration; any empty values will be filled in by defaults in
-// DefaultACME. The associated config is also required.
+// DefaultACME, and if any required values are still empty, sensible
+// defaults will be used.
//
-// Typically, you'll create the Config first, then call NewACMEManager(),
-// then assign the return value to the Issuer/Revoker fields of the Config.
+// Typically, you'll create the Config first with New() or NewDefault(),
+// then call NewACMEManager(), then assign the return value to the Issuers
+// field of the Config.
func NewACMEManager(cfg *Config, template ACMEManager) *ACMEManager {
if cfg == nil {
panic("cannot make valid ACMEManager without an associated CertMagic config")
@@ -126,6 +139,9 @@ func NewACMEManager(cfg *Config, template ACMEManager) *ACMEManager {
if template.Email == "" {
template.Email = DefaultACME.Email
}
+ if template.AccountKeyPEM == "" {
+ template.AccountKeyPEM = DefaultACME.AccountKeyPEM
+ }
if !template.Agreed {
template.Agreed = DefaultACME.Agreed
}
@@ -175,7 +191,7 @@ func (am *ACMEManager) IssuerKey() string {
return am.issuerKey(am.CA)
}
-func (am *ACMEManager) issuerKey(ca string) string {
+func (*ACMEManager) issuerKey(ca string) string {
key := ca
if caURL, err := url.Parse(key); err == nil {
key = caURL.Host
@@ -202,11 +218,11 @@ func (am *ACMEManager) issuerKey(ca string) string {
// batch is eligible for certificates if using Let's Encrypt.
// It also ensures that an email address is available.
func (am *ACMEManager) PreCheck(_ context.Context, names []string, interactive bool) error {
- letsEncrypt := strings.Contains(am.CA, "api.letsencrypt.org")
- if letsEncrypt {
+ publicCA := strings.Contains(am.CA, "api.letsencrypt.org") || strings.Contains(am.CA, "acme.zerossl.com")
+ if publicCA {
for _, name := range names {
if !SubjectQualifiesForPublicCert(name) {
- return fmt.Errorf("subject does not qualify for a Let's Encrypt certificate: %s", name)
+ return fmt.Errorf("subject does not qualify for a public certificate: %s", name)
}
}
}
@@ -282,7 +298,7 @@ func (am *ACMEManager) Issue(ctx context.Context, csr *x509.CertificateRequest)
}
func (am *ACMEManager) doIssue(ctx context.Context, csr *x509.CertificateRequest, useTestCA bool) (*IssuedCertificate, bool, error) {
- client, err := am.newACMEClient(ctx, useTestCA, false)
+ client, err := am.newACMEClientWithAccount(ctx, useTestCA, false)
if err != nil {
return nil, false, err
}
@@ -300,20 +316,103 @@ func (am *ACMEManager) doIssue(ctx context.Context, csr *x509.CertificateRequest
if err != nil {
return nil, usingTestCA, fmt.Errorf("%v %w (ca=%s)", nameSet, err, client.acmeClient.Directory)
}
+ if len(certChains) == 0 {
+ return nil, usingTestCA, fmt.Errorf("no certificate chains")
+ }
+
+ preferredChain := am.selectPreferredChain(certChains)
- // TODO: ACME server could in theory issue a cert with multiple chains,
- // but we don't (yet) have a way to choose one, so just use first one
ic := &IssuedCertificate{
- Certificate: certChains[0].ChainPEM,
- Metadata: certChains[0],
+ Certificate: preferredChain.ChainPEM,
+ Metadata: preferredChain,
}
return ic, usingTestCA, nil
}
+// selectPreferredChain sorts and then filters the certificate chains to find the optimal
+// chain preferred by the client. If there's only one chain, that is returned without any
+// processing. If there are no matches, the first chain is returned.
+func (am *ACMEManager) selectPreferredChain(certChains []acme.Certificate) acme.Certificate {
+ if len(certChains) == 1 {
+ if am.Logger != nil && (len(am.PreferredChains.AnyCommonName) > 0 || len(am.PreferredChains.RootCommonName) > 0) {
+ am.Logger.Debug("there is only one chain offered; selecting it regardless of preferences",
+ zap.String("chain_url", certChains[0].URL))
+ }
+ return certChains[0]
+ }
+
+ if am.PreferredChains.Smallest != nil {
+ if *am.PreferredChains.Smallest {
+ sort.Slice(certChains, func(i, j int) bool {
+ return len(certChains[i].ChainPEM) < len(certChains[j].ChainPEM)
+ })
+ } else {
+ sort.Slice(certChains, func(i, j int) bool {
+ return len(certChains[i].ChainPEM) > len(certChains[j].ChainPEM)
+ })
+ }
+ }
+
+ if len(am.PreferredChains.AnyCommonName) > 0 || len(am.PreferredChains.RootCommonName) > 0 {
+ // in order to inspect, we need to decode their PEM contents
+ decodedChains := make([][]*x509.Certificate, len(certChains))
+ for i, chain := range certChains {
+ certs, err := parseCertsFromPEMBundle(chain.ChainPEM)
+ if err != nil {
+ if am.Logger != nil {
+ am.Logger.Error("unable to parse PEM certificate chain",
+ zap.Int("chain", i),
+ zap.Error(err))
+ }
+ continue
+ }
+ decodedChains[i] = certs
+ }
+
+ if len(am.PreferredChains.AnyCommonName) > 0 {
+ for _, prefAnyCN := range am.PreferredChains.AnyCommonName {
+ for i, chain := range decodedChains {
+ for _, cert := range chain {
+ if cert.Issuer.CommonName == prefAnyCN {
+ if am.Logger != nil {
+ am.Logger.Debug("found preferred certificate chain by issuer common name",
+ zap.String("preference", prefAnyCN),
+ zap.Int("chain", i))
+ }
+ return certChains[i]
+ }
+ }
+ }
+ }
+ }
+
+ if len(am.PreferredChains.RootCommonName) > 0 {
+ for _, prefRootCN := range am.PreferredChains.RootCommonName {
+ for i, chain := range decodedChains {
+ if chain[len(chain)-1].Issuer.CommonName == prefRootCN {
+ if am.Logger != nil {
+ am.Logger.Debug("found preferred certificate chain by root common name",
+ zap.String("preference", prefRootCN),
+ zap.Int("chain", i))
+ }
+ return certChains[i]
+ }
+ }
+ }
+ }
+
+ if am.Logger != nil {
+ am.Logger.Warn("did not find chain matching preferences; using first")
+ }
+ }
+
+ return certChains[0]
+}
+
// Revoke implements the Revoker interface. It revokes the given certificate.
func (am *ACMEManager) Revoke(ctx context.Context, cert CertificateResource, reason int) error {
- client, err := am.newACMEClient(ctx, false, false)
+ client, err := am.newACMEClientWithAccount(ctx, false, false)
if err != nil {
return err
}
@@ -326,8 +425,24 @@ func (am *ACMEManager) Revoke(ctx context.Context, cert CertificateResource, rea
return client.revoke(ctx, certs[0], reason)
}
-// DefaultACME specifies the default settings
-// to use for ACMEManagers.
+// ChainPreference describes the client's preferred certificate chain,
+// useful if the CA offers alternate chains. The first matching chain
+// will be selected.
+type ChainPreference struct {
+ // Prefer chains with the fewest number of bytes.
+ Smallest *bool
+
+ // Select first chain having a root with one of
+ // these common names.
+ RootCommonName []string
+
+ // Select first chain that has any issuer with one
+ // of these common names.
+ AnyCommonName []string
+}
+
+// DefaultACME specifies default settings to use for ACMEManagers.
+// Using this value is optional but can be convenient.
var DefaultACME = ACMEManager{
CA: LetsEncryptProductionCA,
TestCA: LetsEncryptStagingCA,
@@ -337,6 +452,7 @@ var DefaultACME = ACMEManager{
const (
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
+ ZeroSSLProductionCA = "https://acme.zerossl.com/v2/DV90"
)
// prefixACME is the storage key prefix used for ACME-specific assets.