diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/caddyhttp/autohttps.go | 16 | ||||
-rw-r--r-- | modules/caddytls/acmeissuer.go | 221 | ||||
-rw-r--r-- | modules/caddytls/internalissuer.go | 57 | ||||
-rw-r--r-- | modules/caddytls/tls.go | 6 | ||||
-rw-r--r-- | modules/caddytls/zerosslissuer.go | 236 |
5 files changed, 464 insertions, 72 deletions
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 97cbed3..0780981 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -451,8 +451,8 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna if ap.Issuer == nil { ap.Issuer = new(caddytls.ACMEIssuer) } - if acmeIssuer, ok := ap.Issuer.(*caddytls.ACMEIssuer); ok { - err := app.fillInACMEIssuer(acmeIssuer) + if acmeIssuer, ok := ap.Issuer.(acmeCapable); ok { + err := app.fillInACMEIssuer(acmeIssuer.GetACMEIssuer()) if err != nil { return err } @@ -470,9 +470,13 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, interna basePolicy = new(caddytls.AutomationPolicy) } - // if the basePolicy has an existing ACMEIssuer, let's - // use it, otherwise we'll make one - baseACMEIssuer, _ := basePolicy.Issuer.(*caddytls.ACMEIssuer) + // if the basePolicy has an existing ACMEIssuer (particularly to + // include any type that embeds/wraps an ACMEIssuer), let's use it, + // otherwise we'll make one + var baseACMEIssuer *caddytls.ACMEIssuer + if acmeWrapper, ok := basePolicy.Issuer.(acmeCapable); ok { + baseACMEIssuer = acmeWrapper.GetACMEIssuer() + } if baseACMEIssuer == nil { // note that this happens if basePolicy.Issuer is nil // OR if it is not nil but is not an ACMEIssuer @@ -630,3 +634,5 @@ func (app *App) automaticHTTPSPhase2() error { app.allCertDomains = nil // no longer needed; allow GC to deallocate return nil } + +type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer } diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 8fe308d..963143b 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -20,9 +20,11 @@ import ( "fmt" "io/ioutil" "net/url" + "strconv" "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/certmagic" "github.com/mholt/acmez" "github.com/mholt/acmez/acme" @@ -85,13 +87,13 @@ func (ACMEIssuer) CaddyModule() caddy.ModuleInfo { } } -// Provision sets up m. -func (m *ACMEIssuer) Provision(ctx caddy.Context) error { - m.logger = ctx.Logger(m) +// Provision sets up iss. +func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { + iss.logger = ctx.Logger(iss) // DNS providers - if m.Challenges != nil && m.Challenges.DNS != nil && m.Challenges.DNS.ProviderRaw != nil { - val, err := ctx.LoadModule(m.Challenges.DNS, "ProviderRaw") + if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.ProviderRaw != nil { + val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw") if err != nil { return fmt.Errorf("loading DNS provider module: %v", err) } @@ -104,32 +106,32 @@ func (m *ACMEIssuer) Provision(ctx caddy.Context) error { // acmez.Solver type, so we use it directly. The user must set environment // variables to configure it. Remove this shim once a sufficient number of // DNS providers are implemented for the libdns APIs instead. - m.Challenges.DNS.solver = deprecatedProvider + iss.Challenges.DNS.solver = deprecatedProvider } else { - m.Challenges.DNS.solver = &certmagic.DNS01Solver{ + iss.Challenges.DNS.solver = &certmagic.DNS01Solver{ DNSProvider: val.(certmagic.ACMEDNSProvider), - TTL: time.Duration(m.Challenges.DNS.TTL), - PropagationTimeout: time.Duration(m.Challenges.DNS.PropagationTimeout), + TTL: time.Duration(iss.Challenges.DNS.TTL), + PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout), } } } // add any custom CAs to trust store - if len(m.TrustedRootsPEMFiles) > 0 { - m.rootPool = x509.NewCertPool() - for _, pemFile := range m.TrustedRootsPEMFiles { + if len(iss.TrustedRootsPEMFiles) > 0 { + iss.rootPool = x509.NewCertPool() + for _, pemFile := range iss.TrustedRootsPEMFiles { pemData, err := ioutil.ReadFile(pemFile) if err != nil { return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err) } - if !m.rootPool.AppendCertsFromPEM(pemData) { + if !iss.rootPool.AppendCertsFromPEM(pemData) { return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err) } } } var err error - m.template, err = m.makeIssuerTemplate() + iss.template, err = iss.makeIssuerTemplate() if err != nil { return err } @@ -137,30 +139,30 @@ func (m *ACMEIssuer) Provision(ctx caddy.Context) error { return nil } -func (m *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEManager, error) { +func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEManager, error) { template := certmagic.ACMEManager{ - CA: m.CA, - TestCA: m.TestCA, - Email: m.Email, - CertObtainTimeout: time.Duration(m.ACMETimeout), - TrustedRoots: m.rootPool, - ExternalAccount: m.ExternalAccount, - Logger: m.logger, + CA: iss.CA, + TestCA: iss.TestCA, + Email: iss.Email, + CertObtainTimeout: time.Duration(iss.ACMETimeout), + TrustedRoots: iss.rootPool, + ExternalAccount: iss.ExternalAccount, + Logger: iss.logger, } - if m.Challenges != nil { - if m.Challenges.HTTP != nil { - template.DisableHTTPChallenge = m.Challenges.HTTP.Disabled - template.AltHTTPPort = m.Challenges.HTTP.AlternatePort + if iss.Challenges != nil { + if iss.Challenges.HTTP != nil { + template.DisableHTTPChallenge = iss.Challenges.HTTP.Disabled + template.AltHTTPPort = iss.Challenges.HTTP.AlternatePort } - if m.Challenges.TLSALPN != nil { - template.DisableTLSALPNChallenge = m.Challenges.TLSALPN.Disabled - template.AltTLSALPNPort = m.Challenges.TLSALPN.AlternatePort + if iss.Challenges.TLSALPN != nil { + template.DisableTLSALPNChallenge = iss.Challenges.TLSALPN.Disabled + template.AltTLSALPNPort = iss.Challenges.TLSALPN.AlternatePort } - if m.Challenges.DNS != nil { - template.DNS01Solver = m.Challenges.DNS.solver + if iss.Challenges.DNS != nil { + template.DNS01Solver = iss.Challenges.DNS.solver } - template.ListenHost = m.Challenges.BindHost + template.ListenHost = iss.Challenges.BindHost } return template, nil @@ -170,8 +172,8 @@ func (m *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEManager, error) { // This is required because ACME needs values from the config in // order to solve the challenges during issuance. This implements // the ConfigSetter interface. -func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) { - m.magic = cfg +func (iss *ACMEIssuer) SetConfig(cfg *certmagic.Config) { + iss.magic = cfg } // TODO: I kind of hate how each call to these methods needs to @@ -179,23 +181,147 @@ func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) { // we find the right place to do that just once and then re-use? // PreCheck implements the certmagic.PreChecker interface. -func (m *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error { - return certmagic.NewACMEManager(m.magic, m.template).PreCheck(ctx, names, interactive) +func (iss *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error { + return certmagic.NewACMEManager(iss.magic, iss.template).PreCheck(ctx, names, interactive) } // Issue obtains a certificate for the given csr. -func (m *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { - return certmagic.NewACMEManager(m.magic, m.template).Issue(ctx, csr) +func (iss *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + return certmagic.NewACMEManager(iss.magic, iss.template).Issue(ctx, csr) } // IssuerKey returns the unique issuer key for the configured CA endpoint. -func (m *ACMEIssuer) IssuerKey() string { - return certmagic.NewACMEManager(m.magic, m.template).IssuerKey() +func (iss *ACMEIssuer) IssuerKey() string { + return certmagic.NewACMEManager(iss.magic, iss.template).IssuerKey() } // Revoke revokes the given certificate. -func (m *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error { - return certmagic.NewACMEManager(m.magic, m.template).Revoke(ctx, cert, reason) +func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error { + return certmagic.NewACMEManager(iss.magic, iss.template).Revoke(ctx, cert, reason) +} + +// GetACMEIssuer returns iss. This is useful when other types embed ACMEIssuer, because +// type-asserting them to *ACMEIssuer will fail, but type-asserting them to an interface +// with only this method will succeed, and will still allow the embedded ACMEIssuer +// to be accessed and manipulated. +func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss } + +// UnmarshalCaddyfile deserializes Caddyfile tokens into iss. +// +// ... acme { +// dir <directory_url> +// test_dir <test_directory_url> +// email <email> +// timeout <duration> +// disable_http_challenge +// disable_tlsalpn_challenge +// alt_http_port <port> +// alt_tlsalpn_port <port> +// eab <key_id> <mac_key> +// trusted_roots <pem_files...> +// } +// +func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "dir": + if !d.AllArgs(&iss.CA) { + return d.ArgErr() + } + + case "test_dir": + if !d.AllArgs(&iss.TestCA) { + return d.ArgErr() + } + + case "email": + if !d.AllArgs(&iss.Email) { + return d.ArgErr() + } + + case "timeout": + var timeoutStr string + if !d.AllArgs(&timeoutStr) { + return d.ArgErr() + } + timeout, err := caddy.ParseDuration(timeoutStr) + if err != nil { + return d.Errf("invalid timeout duration %s: %v", timeoutStr, err) + } + iss.ACMETimeout = caddy.Duration(timeout) + + case "disable_http_challenge": + if d.NextArg() { + return d.ArgErr() + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.HTTP == nil { + iss.Challenges.HTTP = new(HTTPChallengeConfig) + } + iss.Challenges.HTTP.Disabled = true + + case "disable_tlsalpn_challenge": + if d.NextArg() { + return d.ArgErr() + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.TLSALPN == nil { + iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) + } + iss.Challenges.TLSALPN.Disabled = true + + case "alt_http_port": + if !d.NextArg() { + return d.ArgErr() + } + port, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("invalid port %s: %v", d.Val(), err) + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.HTTP == nil { + iss.Challenges.HTTP = new(HTTPChallengeConfig) + } + iss.Challenges.HTTP.AlternatePort = port + + case "alt_tlsalpn_port": + if !d.NextArg() { + return d.ArgErr() + } + port, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("invalid port %s: %v", d.Val(), err) + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.TLSALPN == nil { + iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) + } + iss.Challenges.TLSALPN.AlternatePort = port + + case "eab": + iss.ExternalAccount = new(acme.EAB) + if !d.AllArgs(&iss.ExternalAccount.KeyID, &iss.ExternalAccount.MACKey) { + return d.ArgErr() + } + + case "trusted_roots": + iss.TrustedRootsPEMFiles = d.RemainingArgs() + + default: + return d.Errf("unrecognized ACME issuer property: %s", d.Val()) + } + } + } + return nil } // onDemandAskRequest makes a request to the ask URL @@ -228,9 +354,10 @@ func onDemandAskRequest(ask string, name string) error { // Interface guards var ( - _ certmagic.PreChecker = (*ACMEIssuer)(nil) - _ certmagic.Issuer = (*ACMEIssuer)(nil) - _ certmagic.Revoker = (*ACMEIssuer)(nil) - _ caddy.Provisioner = (*ACMEIssuer)(nil) - _ ConfigSetter = (*ACMEIssuer)(nil) + _ certmagic.PreChecker = (*ACMEIssuer)(nil) + _ certmagic.Issuer = (*ACMEIssuer)(nil) + _ certmagic.Revoker = (*ACMEIssuer)(nil) + _ caddy.Provisioner = (*ACMEIssuer)(nil) + _ ConfigSetter = (*ACMEIssuer)(nil) + _ caddyfile.Unmarshaler = (*ACMEIssuer)(nil) ) diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go index ca43bf8..d70b8ca 100644 --- a/modules/caddytls/internalissuer.go +++ b/modules/caddytls/internalissuer.go @@ -23,6 +23,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/certmagic" "github.com/smallstep/certificates/authority/provisioner" @@ -63,25 +64,25 @@ func (InternalIssuer) CaddyModule() caddy.ModuleInfo { } // Provision sets up the issuer. -func (li *InternalIssuer) Provision(ctx caddy.Context) error { +func (iss *InternalIssuer) Provision(ctx caddy.Context) error { // get a reference to the configured CA appModule, err := ctx.App("pki") if err != nil { return err } pkiApp := appModule.(*caddypki.PKI) - if li.CA == "" { - li.CA = caddypki.DefaultCAID + if iss.CA == "" { + iss.CA = caddypki.DefaultCAID } - ca, ok := pkiApp.CAs[li.CA] + ca, ok := pkiApp.CAs[iss.CA] if !ok { - return fmt.Errorf("no certificate authority configured with id: %s", li.CA) + return fmt.Errorf("no certificate authority configured with id: %s", iss.CA) } - li.ca = ca + iss.ca = ca // set any other default values - if li.Lifetime == 0 { - li.Lifetime = caddy.Duration(defaultInternalCertLifetime) + if iss.Lifetime == 0 { + iss.Lifetime = caddy.Duration(defaultInternalCertLifetime) } return nil @@ -89,38 +90,38 @@ func (li *InternalIssuer) Provision(ctx caddy.Context) error { // IssuerKey returns the unique issuer key for the // confgured CA endpoint. -func (li InternalIssuer) IssuerKey() string { - return li.ca.ID() +func (iss InternalIssuer) IssuerKey() string { + return iss.ca.ID() } // Issue issues a certificate to satisfy the CSR. -func (li InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { +func (iss InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { // prepare the signing authority authCfg := caddypki.AuthorityConfig{ - SignWithRoot: li.SignWithRoot, + SignWithRoot: iss.SignWithRoot, } - auth, err := li.ca.NewAuthority(authCfg) + auth, err := iss.ca.NewAuthority(authCfg) if err != nil { return nil, err } // get the cert (public key) that will be used for signing var issuerCert *x509.Certificate - if li.SignWithRoot { - issuerCert = li.ca.RootCertificate() + if iss.SignWithRoot { + issuerCert = iss.ca.RootCertificate() } else { - issuerCert = li.ca.IntermediateCertificate() + issuerCert = iss.ca.IntermediateCertificate() } // ensure issued certificate does not expire later than its issuer - lifetime := time.Duration(li.Lifetime) + lifetime := time.Duration(iss.Lifetime) if time.Now().Add(lifetime).After(issuerCert.NotAfter) { // TODO: log this lifetime = issuerCert.NotAfter.Sub(time.Now()) } certChain, err := auth.Sign(csr, provisioner.Options{}, - profileDefaultDuration(li.Lifetime), + profileDefaultDuration(iss.Lifetime), ) if err != nil { return nil, err @@ -139,6 +140,26 @@ func (li InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest }, nil } +// UnmarshalCaddyfile deserializes Caddyfile tokens into iss. +// +// ... internal { +// ca <name> +// } +// +func (iss *InternalIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for d.NextBlock(0) { + switch d.Val() { + case "ca": + if !d.AllArgs(&iss.CA) { + return d.ArgErr() + } + } + } + } + return nil +} + // profileDefaultDuration is a wrapper against x509util.WithOption to conform // the SignOption interface. // diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 8178026..6a635d4 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -313,8 +313,10 @@ func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { if ap.magic.Issuer == nil { return false } - if am, ok := ap.magic.Issuer.(*ACMEIssuer); ok { - return certmagic.NewACMEManager(am.magic, am.template).HandleHTTPChallenge(w, r) + type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer } + if am, ok := ap.magic.Issuer.(acmeCapable); ok { + iss := am.GetACMEIssuer() + return certmagic.NewACMEManager(iss.magic, iss.template).HandleHTTPChallenge(w, r) } return false } diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go new file mode 100644 index 0000000..d0f4950 --- /dev/null +++ b/modules/caddytls/zerosslissuer.go @@ -0,0 +1,236 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// 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 caddytls + +import ( + "context" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/certmagic" + "github.com/mholt/acmez/acme" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(new(ZeroSSLIssuer)) +} + +// ZeroSSLIssuer makes an ACME manager +// for managing certificates using ACME. +type ZeroSSLIssuer struct { + *ACMEIssuer + + // The API key (or "access key") for using the ZeroSSL API. + APIKey string `json:"api_key,omitempty"` + + mu sync.Mutex + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.issuance.zerossl", + New: func() caddy.Module { return new(ZeroSSLIssuer) }, + } +} + +// Provision sets up iss. +func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error { + iss.logger = ctx.Logger(iss) + + if iss.ACMEIssuer == nil { + iss.ACMEIssuer = new(ACMEIssuer) + } + err := iss.ACMEIssuer.Provision(ctx) + if err != nil { + return err + } + + return nil +} + +func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, am *certmagic.ACMEManager, _ acme.Account) error { + if am.ExternalAccount != nil { + return nil + } + var err error + am.ExternalAccount, err = iss.generateEABCredentials(ctx) + return err +} + +func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context) (*acme.EAB, error) { + var endpoint string + var body io.Reader + + // there are two ways to generate EAB credentials: authenticated with + // their API key, or unauthenticated with their email address + switch { + case iss.APIKey != "": + apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "") + if apiKey == "" { + return nil, fmt.Errorf("missing API key: '%v'", iss.APIKey) + } + qs := url.Values{"access_key": []string{apiKey}} + endpoint = fmt.Sprintf("%s/eab-credentials?%s", zerosslAPIBase, qs.Encode()) + + case iss.Email != "": + email := caddy.NewReplacer().ReplaceAll(iss.Email, "") + if email == "" { + return nil, fmt.Errorf("missing email: '%v'", iss.Email) + } + endpoint = zerosslAPIBase + "/eab-credentials-email" + form := url.Values{"email": []string{email}} + body = strings.NewReader(form.Encode()) + + default: + return nil, fmt.Errorf("must configure either an API key or email address to use ZeroSSL without explicit EAB") + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) + if err != nil { + return nil, fmt.Errorf("forming request: %v", err) + } + if body != nil { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + req.Header.Set("User-Agent", certmagic.UserAgent) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("performing EAB credentials request: %v", err) + } + defer resp.Body.Close() + + var result struct { + Success bool `json:"success"` + Error struct { + Code int `json:"code"` + Type string `json:"type"` + } `json:"error"` + EABKID string `json:"eab_kid"` + EABHMACKey string `json:"eab_hmac_key"` + } + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, fmt.Errorf("decoding API response: %v", err) + } + if result.Error.Code != 0 { + return nil, fmt.Errorf("failed getting EAB credentials: HTTP %d: %s (code %d)", + resp.StatusCode, result.Error.Type, result.Error.Code) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode) + } + + iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID)) + + return &acme.EAB{ + KeyID: result.EABKID, + MACKey: result.EABHMACKey, + }, nil +} + +// initialize modifies the template for the underlying ACMEManager +// values by setting the CA endpoint to the ZeroSSL directory and +// setting the NewAccountFunc callback to one which allows us to +// generate EAB credentials only if a new account is being made. +// Since it modifies the stored template, its effect should only +// be needed once, but it is fine to call it repeatedly. +func (iss *ZeroSSLIssuer) initialize() { + iss.mu.Lock() + defer iss.mu.Unlock() + if iss.template.CA == "" { + iss.template.CA = zerosslACMEDirectory + } + if iss.template.NewAccountFunc == nil { + iss.template.NewAccountFunc = iss.newAccountCallback + } +} + +// PreCheck implements the certmagic.PreChecker interface. +func (iss *ZeroSSLIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error { + iss.initialize() + return iss.ACMEIssuer.PreCheck(ctx, names, interactive) +} + +// Issue obtains a certificate for the given csr. +func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + iss.initialize() + return iss.ACMEIssuer.Issue(ctx, csr) +} + +// IssuerKey returns the unique issuer key for the configured CA endpoint. +func (iss *ZeroSSLIssuer) IssuerKey() string { + iss.initialize() + return iss.ACMEIssuer.IssuerKey() +} + +// Revoke revokes the given certificate. +func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error { + iss.initialize() + return iss.ACMEIssuer.Revoke(ctx, cert, reason) +} + +// UnmarshalCaddyfile deserializes Caddyfile tokens into iss. +// +// ... zerossl <api_key> { +// ... +// } +// +// Any of the subdirectives for the ACME issuer can be used in the block. +func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.AllArgs(&iss.APIKey) { + return d.ArgErr() + } + + if iss.ACMEIssuer == nil { + iss.ACMEIssuer = new(ACMEIssuer) + } + err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment()) + if err != nil { + return err + } + } + return nil +} + +const ( + zerosslACMEDirectory = "https://acme.zerossl.com/v2/DV90" + zerosslAPIBase = "https://api.zerossl.com/acme" +) + +// Interface guards +var ( + _ certmagic.PreChecker = (*ZeroSSLIssuer)(nil) + _ certmagic.Issuer = (*ZeroSSLIssuer)(nil) + _ certmagic.Revoker = (*ZeroSSLIssuer)(nil) + _ caddy.Provisioner = (*ZeroSSLIssuer)(nil) + _ ConfigSetter = (*ZeroSSLIssuer)(nil) + + // a type which properly embeds an ACMEIssuer should implement + // this interface so it can be treated as an ACMEIssuer + _ interface{ GetACMEIssuer() *ACMEIssuer } = (*ZeroSSLIssuer)(nil) +) |