5ee48a3108
This may be temporary until ARI is more mature
662 lines
22 KiB
Go
662 lines
22 KiB
Go
// 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"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mholt/acmez/v2"
|
|
"github.com/mholt/acmez/v2/acme"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ACMEIssuer gets certificates using ACME. It implements the PreChecker,
|
|
// Issuer, and Revoker interfaces.
|
|
//
|
|
// It is NOT VALID to use an ACMEIssuer without calling NewACMEIssuer().
|
|
// It fills in any default values from DefaultACME as well as setting up
|
|
// internal state that is necessary for valid use. Always call
|
|
// NewACMEIssuer() to get a valid ACMEIssuer value.
|
|
type ACMEIssuer struct {
|
|
// The endpoint of the directory for the ACME
|
|
// CA we are to use
|
|
CA string
|
|
|
|
// TestCA is the endpoint of the directory for
|
|
// an ACME CA to use to test domain validation,
|
|
// but any certs obtained from this CA are
|
|
// discarded
|
|
TestCA string
|
|
|
|
// The email address to use when creating or
|
|
// 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
|
|
|
|
// An optional external account to associate
|
|
// with this ACME account
|
|
ExternalAccount *acme.EAB
|
|
|
|
// Optionally specify the validity period of
|
|
// the certificate(s) here as offsets from the
|
|
// approximate time of certificate issuance,
|
|
// but note that not all CAs support this
|
|
// (EXPERIMENTAL: Subject to change)
|
|
NotBefore, NotAfter time.Duration
|
|
|
|
// Disable all HTTP challenges
|
|
DisableHTTPChallenge bool
|
|
|
|
// Disable all TLS-ALPN challenges
|
|
DisableTLSALPNChallenge bool
|
|
|
|
// The host (ONLY the host, not port) to listen
|
|
// on if necessary to start a listener to solve
|
|
// an ACME challenge
|
|
ListenHost string
|
|
|
|
// The alternate port to use for the ACME HTTP
|
|
// challenge; if non-empty, this port will be
|
|
// used instead of HTTPChallengePort to spin up
|
|
// a listener for the HTTP challenge
|
|
AltHTTPPort int
|
|
|
|
// The alternate port to use for the ACME
|
|
// TLS-ALPN challenge; the system must forward
|
|
// TLSALPNChallengePort to this port for
|
|
// challenge to succeed
|
|
AltTLSALPNPort int
|
|
|
|
// The solver for the dns-01 challenge;
|
|
// usually this is a DNS01Solver value
|
|
// from this package
|
|
DNS01Solver acmez.Solver
|
|
|
|
// TrustedRoots specifies a pool of root CA
|
|
// certificates to trust when communicating
|
|
// over a network to a peer.
|
|
TrustedRoots *x509.CertPool
|
|
|
|
// The maximum amount of time to allow for
|
|
// obtaining a certificate. If empty, the
|
|
// default from the underlying ACME lib is
|
|
// used. If set, it must not be too low so
|
|
// as to cancel challenges too early.
|
|
CertObtainTimeout time.Duration
|
|
|
|
// Address of custom DNS resolver to be used
|
|
// when communicating with ACME server
|
|
Resolver string
|
|
|
|
// Callback function that is called before a
|
|
// new ACME account is registered with the CA;
|
|
// it allows for last-second config changes
|
|
// of the ACMEIssuer and the Account.
|
|
// (TODO: this feature is still EXPERIMENTAL and subject to change)
|
|
NewAccountFunc func(context.Context, *ACMEIssuer, acme.Account) (acme.Account, error)
|
|
|
|
// Preferences for selecting alternate
|
|
// certificate chains
|
|
PreferredChains ChainPreference
|
|
|
|
// Set a logger to configure logging; a default
|
|
// logger must always be set; if no logging is
|
|
// desired, set this to zap.NewNop().
|
|
Logger *zap.Logger
|
|
|
|
// Set a http proxy to use when issuing a certificate.
|
|
// Default is http.ProxyFromEnvironment
|
|
HTTPProxy func(*http.Request) (*url.URL, error)
|
|
|
|
config *Config
|
|
httpClient *http.Client
|
|
|
|
// Some fields are changed on-the-fly during
|
|
// certificate management. For example, the
|
|
// email might be implicitly discovered if not
|
|
// explicitly configured, and agreement might
|
|
// happen during the flow. Changing the exported
|
|
// fields field is racey (issue #195) so we
|
|
// control unexported fields that we can
|
|
// synchronize properly.
|
|
email string
|
|
agreed bool
|
|
mu *sync.Mutex // protects the above grouped fields, as well as entire struct during NewAccountFunc calls
|
|
}
|
|
|
|
// NewACMEIssuer constructs a valid ACMEIssuer based on a template
|
|
// configuration; any empty values will be filled in by defaults in
|
|
// DefaultACME, and if any required values are still empty, sensible
|
|
// defaults will be used.
|
|
//
|
|
// Typically, you'll create the Config first with New() or NewDefault(),
|
|
// then call NewACMEIssuer(), then assign the return value to the Issuers
|
|
// field of the Config.
|
|
func NewACMEIssuer(cfg *Config, template ACMEIssuer) *ACMEIssuer {
|
|
if cfg == nil {
|
|
panic("cannot make valid ACMEIssuer without an associated CertMagic config")
|
|
}
|
|
if template.CA == "" {
|
|
template.CA = DefaultACME.CA
|
|
}
|
|
if template.TestCA == "" && template.CA == DefaultACME.CA {
|
|
// only use the default test CA if the CA is also
|
|
// the default CA; no point in testing against
|
|
// Let's Encrypt's staging server if we are not
|
|
// using their production server too
|
|
template.TestCA = DefaultACME.TestCA
|
|
}
|
|
if template.Email == "" {
|
|
template.Email = DefaultACME.Email
|
|
}
|
|
if template.AccountKeyPEM == "" {
|
|
template.AccountKeyPEM = DefaultACME.AccountKeyPEM
|
|
}
|
|
if !template.Agreed {
|
|
template.Agreed = DefaultACME.Agreed
|
|
}
|
|
if template.ExternalAccount == nil {
|
|
template.ExternalAccount = DefaultACME.ExternalAccount
|
|
}
|
|
if template.NotBefore == 0 {
|
|
template.NotBefore = DefaultACME.NotBefore
|
|
}
|
|
if template.NotAfter == 0 {
|
|
template.NotAfter = DefaultACME.NotAfter
|
|
}
|
|
if !template.DisableHTTPChallenge {
|
|
template.DisableHTTPChallenge = DefaultACME.DisableHTTPChallenge
|
|
}
|
|
if !template.DisableTLSALPNChallenge {
|
|
template.DisableTLSALPNChallenge = DefaultACME.DisableTLSALPNChallenge
|
|
}
|
|
if template.ListenHost == "" {
|
|
template.ListenHost = DefaultACME.ListenHost
|
|
}
|
|
if template.AltHTTPPort == 0 {
|
|
template.AltHTTPPort = DefaultACME.AltHTTPPort
|
|
}
|
|
if template.AltTLSALPNPort == 0 {
|
|
template.AltTLSALPNPort = DefaultACME.AltTLSALPNPort
|
|
}
|
|
if template.DNS01Solver == nil {
|
|
template.DNS01Solver = DefaultACME.DNS01Solver
|
|
}
|
|
if template.TrustedRoots == nil {
|
|
template.TrustedRoots = DefaultACME.TrustedRoots
|
|
}
|
|
if template.CertObtainTimeout == 0 {
|
|
template.CertObtainTimeout = DefaultACME.CertObtainTimeout
|
|
}
|
|
if template.Resolver == "" {
|
|
template.Resolver = DefaultACME.Resolver
|
|
}
|
|
if template.NewAccountFunc == nil {
|
|
template.NewAccountFunc = DefaultACME.NewAccountFunc
|
|
}
|
|
if template.Logger == nil {
|
|
template.Logger = DefaultACME.Logger
|
|
}
|
|
|
|
// absolutely do not allow a nil logger; that would panic
|
|
if template.Logger == nil {
|
|
template.Logger = defaultLogger
|
|
}
|
|
|
|
if template.HTTPProxy == nil {
|
|
template.HTTPProxy = DefaultACME.HTTPProxy
|
|
}
|
|
if template.HTTPProxy == nil {
|
|
template.HTTPProxy = http.ProxyFromEnvironment
|
|
}
|
|
|
|
template.config = cfg
|
|
template.mu = new(sync.Mutex)
|
|
|
|
// set up the dialer and transport / HTTP client
|
|
dialer := &net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 2 * time.Minute,
|
|
}
|
|
if template.Resolver != "" {
|
|
dialer.Resolver = &net.Resolver{
|
|
PreferGo: true,
|
|
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
|
return (&net.Dialer{
|
|
Timeout: 15 * time.Second,
|
|
}).DialContext(ctx, network, template.Resolver)
|
|
},
|
|
}
|
|
}
|
|
transport := &http.Transport{
|
|
Proxy: template.HTTPProxy,
|
|
DialContext: dialer.DialContext,
|
|
TLSHandshakeTimeout: 30 * time.Second, // increase to 30s requested in #175
|
|
ResponseHeaderTimeout: 30 * time.Second, // increase to 30s requested in #175
|
|
ExpectContinueTimeout: 2 * time.Second,
|
|
ForceAttemptHTTP2: true,
|
|
}
|
|
if template.TrustedRoots != nil {
|
|
transport.TLSClientConfig = &tls.Config{
|
|
RootCAs: template.TrustedRoots,
|
|
}
|
|
}
|
|
template.httpClient = &http.Client{
|
|
Transport: transport,
|
|
Timeout: HTTPTimeout,
|
|
}
|
|
|
|
return &template
|
|
}
|
|
|
|
// IssuerKey returns the unique issuer key for the
|
|
// confgured CA endpoint.
|
|
func (am *ACMEIssuer) IssuerKey() string {
|
|
return am.issuerKey(am.CA)
|
|
}
|
|
|
|
func (*ACMEIssuer) issuerKey(ca string) string {
|
|
key := ca
|
|
if caURL, err := url.Parse(key); err == nil {
|
|
key = caURL.Host
|
|
if caURL.Path != "" {
|
|
// keep the path, but make sure it's a single
|
|
// component (i.e. no forward slashes, and for
|
|
// good measure, no backward slashes either)
|
|
const hyphen = "-"
|
|
repl := strings.NewReplacer(
|
|
"/", hyphen,
|
|
"\\", hyphen,
|
|
)
|
|
path := strings.Trim(repl.Replace(caURL.Path), hyphen)
|
|
if path != "" {
|
|
key += hyphen + path
|
|
}
|
|
}
|
|
}
|
|
return key
|
|
}
|
|
|
|
func (iss *ACMEIssuer) getEmail() string {
|
|
iss.mu.Lock()
|
|
defer iss.mu.Unlock()
|
|
return iss.email
|
|
}
|
|
|
|
func (iss *ACMEIssuer) isAgreed() bool {
|
|
iss.mu.Lock()
|
|
defer iss.mu.Unlock()
|
|
return iss.agreed
|
|
}
|
|
|
|
// PreCheck performs a few simple checks before obtaining or
|
|
// renewing a certificate with ACME, and returns whether this
|
|
// batch is eligible for certificates. It also ensures that an
|
|
// email address is available if possible.
|
|
//
|
|
// IP certificates via ACME are defined in RFC 8738.
|
|
func (am *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
|
|
publicCAsAndIPCerts := map[string]bool{ // map of public CAs to whether they support IP certificates (last updated: Q1 2024)
|
|
"api.letsencrypt.org": false, // https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
|
|
"acme.zerossl.com": false, // only supported via their API, not ACME endpoint
|
|
"api.pki.goog": true, // https://pki.goog/faq/#faq-IPCerts
|
|
"api.buypass.com": false, // https://community.buypass.com/t/h7hm76w/buypass-support-for-rfc-8738
|
|
"acme.ssl.com": false,
|
|
}
|
|
var publicCA, ipCertAllowed bool
|
|
for caSubstr, ipCert := range publicCAsAndIPCerts {
|
|
if strings.Contains(am.CA, caSubstr) {
|
|
publicCA, ipCertAllowed = true, ipCert
|
|
break
|
|
}
|
|
}
|
|
if publicCA {
|
|
for _, name := range names {
|
|
if !SubjectQualifiesForPublicCert(name) {
|
|
return fmt.Errorf("subject '%s' does not qualify for a public certificate", name)
|
|
}
|
|
if !ipCertAllowed && SubjectIsIP(name) {
|
|
return fmt.Errorf("subject '%s' cannot have public IP certificate from %s (if CA's policy has changed, please notify the developers in an issue)", name, am.CA)
|
|
}
|
|
}
|
|
}
|
|
return am.setEmail(ctx, interactive)
|
|
}
|
|
|
|
// Issue implements the Issuer interface. It obtains a certificate for the given csr using
|
|
// the ACME configuration am.
|
|
func (am *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*IssuedCertificate, error) {
|
|
if am.config == nil {
|
|
panic("missing config pointer (must use NewACMEIssuer)")
|
|
}
|
|
|
|
var attempts int
|
|
if attemptsPtr, ok := ctx.Value(AttemptsCtxKey).(*int); ok {
|
|
attempts = *attemptsPtr
|
|
}
|
|
isRetry := attempts > 0
|
|
|
|
cert, usedTestCA, err := am.doIssue(ctx, csr, attempts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// important to note that usedTestCA is not necessarily the same as isRetry
|
|
// (usedTestCA can be true if the main CA and the test CA happen to be the same)
|
|
if isRetry && usedTestCA && am.CA != am.TestCA {
|
|
// succeeded with testing endpoint, so try again with production endpoint
|
|
// (only if the production endpoint is different from the testing endpoint)
|
|
// TODO: This logic is imperfect and could benefit from some refinement.
|
|
// The two CA endpoints likely have different states, which could cause one
|
|
// to succeed and the other to fail, even if it's not a validation error.
|
|
// Two common cases would be:
|
|
// 1) Rate limiter state. This is more likely to cause prod to fail while
|
|
// staging succeeds, since prod usually has tighter rate limits. Thus, if
|
|
// initial attempt failed in prod due to rate limit, first retry (on staging)
|
|
// might succeed, and then trying prod again right way would probably still
|
|
// fail; normally this would terminate retries but the right thing to do in
|
|
// this case is to back off and retry again later. We could refine this logic
|
|
// to stick with the production endpoint on retries unless the error changes.
|
|
// 2) Cached authorizations state. If a domain validates successfully with
|
|
// one endpoint, but then the other endpoint is used, it might fail, e.g. if
|
|
// DNS was just changed or is still propagating. In this case, the second CA
|
|
// should continue to be retried with backoff, without switching back to the
|
|
// other endpoint. This is more likely to happen if a user is testing with
|
|
// the staging CA as the main CA, then changes their configuration once they
|
|
// think they are ready for the production endpoint.
|
|
cert, _, err = am.doIssue(ctx, csr, 0)
|
|
if err != nil {
|
|
// succeeded with test CA but failed just now with the production CA;
|
|
// either we are observing differing internal states of each CA that will
|
|
// work out with time, or there is a bug/misconfiguration somewhere
|
|
// externally; it is hard to tell which! one easy cue is whether the
|
|
// error is specifically a 429 (Too Many Requests); if so, we should
|
|
// probably keep retrying
|
|
var problem acme.Problem
|
|
if errors.As(err, &problem) {
|
|
if problem.Status == http.StatusTooManyRequests {
|
|
// DON'T abort retries; the test CA succeeded (even
|
|
// if it's cached, it recently succeeded!) so we just
|
|
// need to keep trying (with backoff) until this CA's
|
|
// rate limits expire...
|
|
// TODO: as mentioned in comment above, we would benefit
|
|
// by pinning the main CA at this point instead of
|
|
// needlessly retrying with the test CA first each time
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, ErrNoRetry{err}
|
|
}
|
|
}
|
|
|
|
return cert, err
|
|
}
|
|
|
|
func (am *ACMEIssuer) doIssue(ctx context.Context, csr *x509.CertificateRequest, attempts int) (*IssuedCertificate, bool, error) {
|
|
useTestCA := attempts > 0
|
|
client, err := am.newACMEClientWithAccount(ctx, useTestCA, false)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
usingTestCA := client.usingTestCA()
|
|
|
|
nameSet := namesFromCSR(csr)
|
|
|
|
if !useTestCA {
|
|
if err := client.throttle(ctx, nameSet); err != nil {
|
|
return nil, usingTestCA, err
|
|
}
|
|
}
|
|
|
|
params, err := acmez.OrderParametersFromCSR(client.account, csr)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("generating order parameters from CSR: %v", err)
|
|
}
|
|
if am.NotBefore != 0 {
|
|
params.NotBefore = time.Now().Add(am.NotBefore)
|
|
}
|
|
if am.NotAfter != 0 {
|
|
params.NotAfter = time.Now().Add(am.NotAfter)
|
|
}
|
|
|
|
// Notify the ACME server we are replacing a certificate (if the caller says we are),
|
|
// only if the following conditions are met:
|
|
// - The caller has set a Replaces value in the context, indicating this is a renewal.
|
|
// - Not using test CA. This should be obvious, but a test CA should be in a separate
|
|
// environment from production, and thus not have knowledge of the cert being replaced.
|
|
// - Not a certain attempt number. We skip setting Replaces once early on in the retries
|
|
// in case the reason the order is failing is only because there is a state inconsistency
|
|
// between client and server or some sort of bookkeeping error with regards to the certID
|
|
// and the server is rejecting the ARI certID. In any case, an invalid certID may cause
|
|
// orders to fail. So try once without setting it.
|
|
if !am.config.DisableARI && !usingTestCA && attempts != 2 {
|
|
if replacing, ok := ctx.Value(ctxKeyARIReplaces).(*x509.Certificate); ok {
|
|
params.Replaces = replacing
|
|
}
|
|
}
|
|
|
|
// do this in a loop because there's an error case that may necessitate a retry, but not more than once
|
|
var certChains []acme.Certificate
|
|
for i := 0; i < 2; i++ {
|
|
am.Logger.Info("using ACME account",
|
|
zap.String("account_id", params.Account.Location),
|
|
zap.Strings("account_contact", params.Account.Contact))
|
|
|
|
certChains, err = client.acmeClient.ObtainCertificate(ctx, params)
|
|
if err != nil {
|
|
var prob acme.Problem
|
|
if errors.As(err, &prob) && prob.Type == acme.ProblemTypeAccountDoesNotExist {
|
|
am.Logger.Warn("ACME account does not exist on server; attempting to recreate",
|
|
zap.String("account_id", client.account.Location),
|
|
zap.Strings("account_contact", client.account.Contact),
|
|
zap.String("key_location", am.storageKeyUserPrivateKey(client.acmeClient.Directory, am.getEmail())),
|
|
zap.Object("problem", prob))
|
|
|
|
// the account we have no longer exists on the CA, so we need to create a new one;
|
|
// we could use the same key pair, but this is a good opportunity to rotate keys
|
|
// (see https://caddy.community/t/acme-account-is-not-regenerated-when-acme-server-gets-reinstalled/22627)
|
|
// (basically this happens if the CA gets reset or reinstalled; usually just internal PKI)
|
|
err := am.deleteAccountLocally(ctx, client.iss.CA, client.account)
|
|
if err != nil {
|
|
return nil, usingTestCA, fmt.Errorf("%v ACME account no longer exists on CA, but resetting our local copy of the account info failed: %v", nameSet, err)
|
|
}
|
|
|
|
// recreate account and try again
|
|
client, err = am.newACMEClientWithAccount(ctx, useTestCA, false)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
continue
|
|
}
|
|
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")
|
|
}
|
|
break
|
|
}
|
|
|
|
preferredChain := am.selectPreferredChain(certChains)
|
|
|
|
ic := &IssuedCertificate{
|
|
Certificate: preferredChain.ChainPEM,
|
|
Metadata: preferredChain,
|
|
}
|
|
|
|
am.Logger.Debug("selected certificate chain", zap.String("url", preferredChain.URL))
|
|
|
|
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 *ACMEIssuer) selectPreferredChain(certChains []acme.Certificate) acme.Certificate {
|
|
if len(certChains) == 1 {
|
|
if 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 {
|
|
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 {
|
|
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 {
|
|
am.Logger.Debug("found preferred certificate chain by root common name",
|
|
zap.String("preference", prefRootCN),
|
|
zap.Int("chain", i))
|
|
return certChains[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 *ACMEIssuer) Revoke(ctx context.Context, cert CertificateResource, reason int) error {
|
|
client, err := am.newACMEClientWithAccount(ctx, false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
certs, err := parseCertsFromPEMBundle(cert.CertificatePEM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return client.revoke(ctx, certs[0], reason)
|
|
}
|
|
|
|
// 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 ACMEIssuers.
|
|
// Using this value is optional but can be convenient.
|
|
var DefaultACME = ACMEIssuer{
|
|
CA: LetsEncryptProductionCA,
|
|
TestCA: LetsEncryptStagingCA,
|
|
Logger: defaultLogger,
|
|
HTTPProxy: http.ProxyFromEnvironment,
|
|
}
|
|
|
|
// Some well-known CA endpoints available to use. See
|
|
// the documentation for each service; some may require
|
|
// External Account Binding (EAB) and possibly payment.
|
|
// COMPATIBILITY NOTICE: These constants refer to external
|
|
// resources and are thus subject to change or removal
|
|
// without a major version bump.
|
|
const (
|
|
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory" // https://letsencrypt.org/docs/staging-environment/
|
|
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory" // https://letsencrypt.org/getting-started/
|
|
ZeroSSLProductionCA = "https://acme.zerossl.com/v2/DV90" // https://zerossl.com/documentation/acme/
|
|
GoogleTrustStagingCA = "https://dv.acme-v02.test-api.pki.goog/directory" // https://cloud.google.com/certificate-manager/docs/public-ca-tutorial
|
|
GoogleTrustProductionCA = "https://dv.acme-v02.api.pki.goog/directory" // https://cloud.google.com/certificate-manager/docs/public-ca-tutorial
|
|
)
|
|
|
|
// prefixACME is the storage key prefix used for ACME-specific assets.
|
|
const prefixACME = "acme"
|
|
|
|
type ctxKey string
|
|
|
|
const ctxKeyARIReplaces = ctxKey("ari_replaces")
|
|
|
|
// Interface guards
|
|
var (
|
|
_ PreChecker = (*ACMEIssuer)(nil)
|
|
_ Issuer = (*ACMEIssuer)(nil)
|
|
_ Revoker = (*ACMEIssuer)(nil)
|
|
)
|