aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/caddyhttp/autohttps.go10
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/client_test.go2
-rw-r--r--modules/caddytls/acmeissuer.go94
-rw-r--r--modules/caddytls/automation.go47
-rw-r--r--modules/caddytls/capools.go3
-rw-r--r--modules/caddytls/certmanagers.go9
-rw-r--r--modules/caddytls/connpolicy.go2
-rw-r--r--modules/caddytls/tls.go55
-rw-r--r--modules/caddytls/zerosslissuer.go330
9 files changed, 352 insertions, 200 deletions
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go
index e28062f05..54a2d9ccd 100644
--- a/modules/caddyhttp/autohttps.go
+++ b/modules/caddyhttp/autohttps.go
@@ -287,6 +287,16 @@ uniqueDomainsLoop:
for _, ap := range app.tlsApp.Automation.Policies {
for _, apHost := range ap.Subjects() {
if apHost == d {
+ // if the automation policy has all internal subjects but no issuers,
+ // it will default to CertMagic's issuers which are public CAs; use
+ // our internal issuer instead
+ if len(ap.Issuers) == 0 && ap.AllInternalSubjects() {
+ iss := new(caddytls.InternalIssuer)
+ if err := iss.Provision(ctx); err != nil {
+ return err
+ }
+ ap.Issuers = append(ap.Issuers, iss)
+ }
continue uniqueDomainsLoop
}
}
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go
index a2227a653..14a1cf684 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go
@@ -213,8 +213,6 @@ func DisabledTest(t *testing.T) {
// TODO: test chunked reader
globalt = t
- rand.Seed(time.Now().UTC().UnixNano())
-
// server
go func() {
listener, err := net.Listen("tcp", ipPort)
diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go
index 8a7f8b499..a14dc61a8 100644
--- a/modules/caddytls/acmeissuer.go
+++ b/modules/caddytls/acmeissuer.go
@@ -17,14 +17,19 @@ package caddytls
import (
"context"
"crypto/x509"
+ "encoding/json"
"fmt"
+ "net/http"
+ "net/url"
"os"
"strconv"
+ "strings"
"time"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez"
- "github.com/mholt/acmez/acme"
+ "github.com/caddyserver/zerossl"
+ "github.com/mholt/acmez/v2"
+ "github.com/mholt/acmez/v2/acme"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
@@ -142,12 +147,14 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
iss.Challenges.DNS.solver = deprecatedProvider
} else {
iss.Challenges.DNS.solver = &certmagic.DNS01Solver{
- DNSProvider: val.(certmagic.ACMEDNSProvider),
- TTL: time.Duration(iss.Challenges.DNS.TTL),
- PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
- PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
- Resolvers: iss.Challenges.DNS.Resolvers,
- OverrideDomain: iss.Challenges.DNS.OverrideDomain,
+ DNSManager: certmagic.DNSManager{
+ DNSProvider: val.(certmagic.DNSProvider),
+ TTL: time.Duration(iss.Challenges.DNS.TTL),
+ PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay),
+ PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout),
+ Resolvers: iss.Challenges.DNS.Resolvers,
+ OverrideDomain: iss.Challenges.DNS.OverrideDomain,
+ },
}
}
}
@@ -210,6 +217,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) {
}
}
+ // ZeroSSL requires EAB, but we can generate that automatically (requires an email address be configured)
+ if strings.HasPrefix(iss.CA, "https://acme.zerossl.com/") {
+ template.NewAccountFunc = func(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
+ if acmeIss.ExternalAccount != nil {
+ return acct, nil
+ }
+ var err error
+ acmeIss.ExternalAccount, acct, err = iss.generateZeroSSLEABCredentials(ctx, acct)
+ return acct, err
+ }
+ }
+
return template, nil
}
@@ -248,6 +267,65 @@ func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateRes
// to be accessed and manipulated.
func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss }
+// generateZeroSSLEABCredentials generates ZeroSSL EAB credentials for the primary contact email
+// on the issuer. It should only be usedif the CA endpoint is ZeroSSL. An email address is required.
+func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, error) {
+ if strings.TrimSpace(iss.Email) == "" {
+ return nil, acme.Account{}, fmt.Errorf("your email address is required to use ZeroSSL's ACME endpoint")
+ }
+
+ if len(acct.Contact) == 0 {
+ // we borrow the email from config or the default email, so ensure it's saved with the account
+ acct.Contact = []string{"mailto:" + iss.Email}
+ }
+
+ endpoint := zerossl.BaseURL + "/acme/eab-credentials-email"
+ form := url.Values{"email": []string{iss.Email}}
+ body := strings.NewReader(form.Encode())
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
+ if err != nil {
+ return nil, acct, fmt.Errorf("forming request: %v", err)
+ }
+ 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, acct, 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, acct, fmt.Errorf("decoding API response: %v", err)
+ }
+ if result.Error.Code != 0 {
+ // do this check first because ZeroSSL's API returns 200 on errors
+ return nil, acct, 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, acct, 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,
+ }, acct, nil
+}
+
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
//
// ... acme [<directory_url>] {
diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go
index a90e5ded8..803cb174e 100644
--- a/modules/caddytls/automation.go
+++ b/modules/caddytls/automation.go
@@ -24,7 +24,7 @@ import (
"strings"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez"
+ "github.com/mholt/acmez/v2"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
@@ -201,6 +201,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
// store them on the policy before putting it on the config
// load and provision any cert manager modules
+ hadExplicitManagers := len(ap.ManagersRaw) > 0
if ap.ManagersRaw != nil {
vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw")
if err != nil {
@@ -256,12 +257,25 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
if ap.OnDemand || len(ap.Managers) > 0 {
// permission module is now required after a number of negligence cases that allowed abuse;
// but it may still be optional for explicit subjects (bounded, non-wildcard), for the
- // internal issuer since it doesn't cause public PKI pressure on ACME servers
- if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) {
- return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
+ // internal issuer since it doesn't cause public PKI pressure on ACME servers; subtly, it
+ // is useful to allow on-demand TLS to be enabled so Managers can be used, but to still
+ // prevent issuance from Issuers (when Managers don't provide a certificate) if there's no
+ // permission module configured
+ noProtections := ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil)
+ failClosed := noProtections && hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured
+ if noProtections {
+ if !hadExplicitManagers {
+ // no managers, no explicitly-configured permission module, this is a config error
+ return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
+ }
+ // allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers
+ tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified")
}
ond = &certmagic.OnDemandConfig{
DecisionFunc: func(ctx context.Context, name string) error {
+ if failClosed {
+ return fmt.Errorf("no permission module configured; certificates not allowed except from external Managers")
+ }
if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil {
return nil
}
@@ -344,6 +358,16 @@ func (ap *AutomationPolicy) Subjects() []string {
return ap.subjects
}
+// AllInternalSubjects returns true if all the subjects on this policy are internal.
+func (ap *AutomationPolicy) AllInternalSubjects() bool {
+ for _, subj := range ap.subjects {
+ if !certmagic.SubjectIsInternal(subj) {
+ return false
+ }
+ }
+ return true
+}
+
func (ap *AutomationPolicy) onlyInternalIssuer() bool {
if len(ap.Issuers) != 1 {
return false
@@ -370,17 +394,21 @@ func (ap *AutomationPolicy) isWildcardOrDefault() bool {
// DefaultIssuers returns empty Issuers (not provisioned) to be used as defaults.
// This function is experimental and has no compatibility promises.
-func DefaultIssuers() []certmagic.Issuer {
- return []certmagic.Issuer{
- new(ACMEIssuer),
- &ZeroSSLIssuer{ACMEIssuer: new(ACMEIssuer)},
+func DefaultIssuers(userEmail string) []certmagic.Issuer {
+ issuers := []certmagic.Issuer{new(ACMEIssuer)}
+ if strings.TrimSpace(userEmail) != "" {
+ issuers = append(issuers, &ACMEIssuer{
+ CA: certmagic.ZeroSSLProductionCA,
+ Email: userEmail,
+ })
}
+ return issuers
}
// DefaultIssuersProvisioned returns empty but provisioned default Issuers from
// DefaultIssuers(). This function is experimental and has no compatibility promises.
func DefaultIssuersProvisioned(ctx caddy.Context) ([]certmagic.Issuer, error) {
- issuers := DefaultIssuers()
+ issuers := DefaultIssuers("")
for i, iss := range issuers {
if prov, ok := iss.(caddy.Provisioner); ok {
err := prov.Provision(ctx)
@@ -453,6 +481,7 @@ type TLSALPNChallengeConfig struct {
type DNSChallengeConfig struct {
// The DNS provider module to use which will manage
// the DNS records relevant to the ACME challenge.
+ // Required.
ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
// The TTL of the TXT record used for the DNS challenge.
diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go
index 44a2fa2c2..7e378aac4 100644
--- a/modules/caddytls/capools.go
+++ b/modules/caddytls/capools.go
@@ -579,8 +579,7 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
customTransport.TLSClientConfig = tlsConfig
}
- var httpClient *http.Client
- *httpClient = *http.DefaultClient
+ httpClient := *http.DefaultClient
httpClient.Transport = customTransport
for _, uri := range hcp.Endpoints {
diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go
index 9bb436a37..b2e2eb073 100644
--- a/modules/caddytls/certmanagers.go
+++ b/modules/caddytls/certmanagers.go
@@ -96,6 +96,11 @@ type HTTPCertGetter struct {
// To be valid, the response must be HTTP 200 with a PEM body
// consisting of blocks for the certificate chain and the private
// key.
+ //
+ // To indicate that this manager is not managing a certificate for
+ // the described handshake, the endpoint should return HTTP 204
+ // (No Content). Error statuses will indicate that the manager is
+ // capable of providing a certificate but was unable to.
URL string `json:"url,omitempty"`
ctx context.Context
@@ -147,6 +152,10 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH
return nil, err
}
defer resp.Body.Close()
+ if resp.StatusCode == http.StatusNoContent {
+ // endpoint is not managing certs for this handshake
+ return nil, nil
+ }
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("got HTTP %d", resp.StatusCode)
}
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index 49c7add49..1e1455446 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -26,7 +26,7 @@ import (
"path/filepath"
"strings"
- "github.com/mholt/acmez"
+ "github.com/mholt/acmez/v2"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go
index bb4cd9715..2a05d5235 100644
--- a/modules/caddytls/tls.go
+++ b/modules/caddytls/tls.go
@@ -176,8 +176,9 @@ func (t *TLS) Provision(ctx caddy.Context) error {
t.Automation.OnDemand.permission = val.(OnDemandPermission)
}
- // on-demand rate limiting
+ // on-demand rate limiting (TODO: deprecated, and should be removed later; rate limiting is ineffective now that permission modules are required)
if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil {
+ t.logger.Warn("DEPRECATED: on_demand.rate_limit will be removed in a future release; use permission modules or external certificate managers instead")
onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst)
onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval))
} else {
@@ -413,35 +414,49 @@ func (t *TLS) Manage(names []string) error {
return nil
}
-// HandleHTTPChallenge ensures that the HTTP challenge is handled for the
-// certificate named by r.Host, if it is an HTTP challenge request. It
-// requires that the automation policy for r.Host has an issuer of type
-// *certmagic.ACMEManager, or one that is ACME-enabled (GetACMEIssuer()).
+// HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP
+// validation request is handled for the certificate named by r.Host, if it
+// is an HTTP challenge request. It requires that the automation policy for
+// r.Host has an issuer that implements GetACMEIssuer() or is a *ZeroSSLIssuer.
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
+ acmeChallenge := certmagic.LooksLikeHTTPChallenge(r)
+ zerosslValidation := certmagic.LooksLikeZeroSSLHTTPValidation(r)
+
// no-op if it's not an ACME challenge request
- if !certmagic.LooksLikeHTTPChallenge(r) {
+ if !acmeChallenge && !zerosslValidation {
return false
}
// try all the issuers until we find the one that initiated the challenge
ap := t.getAutomationPolicyForName(r.Host)
- type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
- for _, iss := range ap.magic.Issuers {
- if am, ok := iss.(acmeCapable); ok {
- iss := am.GetACMEIssuer()
- if iss.issuer.HandleHTTPChallenge(w, r) {
- return true
+
+ if acmeChallenge {
+ type acmeCapable interface{ GetACMEIssuer() *ACMEIssuer }
+
+ for _, iss := range ap.magic.Issuers {
+ if acmeIssuer, ok := iss.(acmeCapable); ok {
+ if acmeIssuer.GetACMEIssuer().issuer.HandleHTTPChallenge(w, r) {
+ return true
+ }
}
}
- }
- // it's possible another server in this process initiated the challenge;
- // users have requested that Caddy only handle HTTP challenges it initiated,
- // so that users can proxy the others through to their backends; but we
- // might not have an automation policy for all identifiers that are trying
- // to get certificates (e.g. the admin endpoint), so we do this manual check
- if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok {
- return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge)
+ // it's possible another server in this process initiated the challenge;
+ // users have requested that Caddy only handle HTTP challenges it initiated,
+ // so that users can proxy the others through to their backends; but we
+ // might not have an automation policy for all identifiers that are trying
+ // to get certificates (e.g. the admin endpoint), so we do this manual check
+ if challenge, ok := certmagic.GetACMEChallenge(r.Host); ok {
+ return certmagic.SolveHTTPChallenge(t.logger, w, r, challenge.Challenge)
+ }
+ } else if zerosslValidation {
+ for _, iss := range ap.magic.Issuers {
+ if ziss, ok := iss.(*ZeroSSLIssuer); ok {
+ if ziss.issuer.HandleZeroSSLHTTPValidation(w, r) {
+ return true
+ }
+ }
+ }
}
return false
diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go
index 1c091a076..b8727ab66 100644
--- a/modules/caddytls/zerosslissuer.go
+++ b/modules/caddytls/zerosslissuer.go
@@ -17,19 +17,15 @@ package caddytls
import (
"context"
"crypto/x509"
- "encoding/json"
"fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "sync"
+ "strconv"
+ "time"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez/acme"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
@@ -37,24 +33,36 @@ func init() {
caddy.RegisterModule(new(ZeroSSLIssuer))
}
-// ZeroSSLIssuer makes an ACME issuer for getting certificates
-// from ZeroSSL by automatically generating EAB credentials.
-// Please be sure to set a valid email address in your config
-// so you can access/manage your domains in your ZeroSSL account.
-//
-// This issuer is only needed for automatic generation of EAB
-// credentials. If manually configuring/reusing EAB credentials,
-// the standard ACMEIssuer may be used if desired.
+// ZeroSSLIssuer uses the ZeroSSL API to get certificates.
+// Note that this is distinct from ZeroSSL's ACME endpoint.
+// To use ZeroSSL's ACME endpoint, use the ACMEIssuer
+// configured with ZeroSSL's ACME directory endpoint.
type ZeroSSLIssuer struct {
- *ACMEIssuer
-
// The API key (or "access key") for using the ZeroSSL API.
- // This is optional, but can be used if you have an API key
- // already and don't want to supply your email address.
+ // REQUIRED.
APIKey string `json:"api_key,omitempty"`
- mu sync.Mutex
- logger *zap.Logger
+ // How many days the certificate should be valid for.
+ // Only certain values are accepted; see ZeroSSL docs.
+ ValidityDays int `json:"validity_days,omitempty"`
+
+ // The host to bind to when opening a listener for
+ // verifying domain names (or IPs).
+ ListenHost string `json:"listen_host,omitempty"`
+
+ // If HTTP is forwarded from port 80, specify the
+ // forwarded port here.
+ AlternateHTTPPort int `json:"alternate_http_port,omitempty"`
+
+ // Use CNAME validation instead of HTTP. ZeroSSL's
+ // API uses CNAME records for DNS validation, similar
+ // to how Let's Encrypt uses TXT records for the
+ // DNS challenge.
+ CNAMEValidation *DNSChallengeConfig `json:"cname_validation,omitempty"`
+
+ logger *zap.Logger
+ storage certmagic.Storage
+ issuer *certmagic.ZeroSSLIssuer
}
// CaddyModule returns the Caddy module information.
@@ -65,178 +73,184 @@ func (*ZeroSSLIssuer) CaddyModule() caddy.ModuleInfo {
}
}
-// Provision sets up iss.
+// Provision sets up the issuer.
func (iss *ZeroSSLIssuer) Provision(ctx caddy.Context) error {
iss.logger = ctx.Logger()
- if iss.ACMEIssuer == nil {
- iss.ACMEIssuer = new(ACMEIssuer)
- }
- if iss.ACMEIssuer.CA == "" {
- iss.ACMEIssuer.CA = certmagic.ZeroSSLProductionCA
- }
- return iss.ACMEIssuer.Provision(ctx)
-}
+ iss.storage = ctx.Storage()
+ repl := caddy.NewReplacer()
-// newAccountCallback generates EAB if not already provided. It also sets a valid default contact on the account if not set.
-func (iss *ZeroSSLIssuer) newAccountCallback(ctx context.Context, acmeIss *certmagic.ACMEIssuer, acct acme.Account) (acme.Account, error) {
- if acmeIss.ExternalAccount != nil {
- return acct, nil
- }
- var err error
- acmeIss.ExternalAccount, acct, err = iss.generateEABCredentials(ctx, acct)
- return acct, err
-}
-
-// generateEABCredentials generates EAB credentials using the API key if provided,
-// otherwise using the primary contact email on the issuer. If an email is not set
-// on the issuer, a default generic email is used.
-func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context, acct acme.Account) (*acme.EAB, acme.Account, 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
- if iss.APIKey != "" {
- apiKey := caddy.NewReplacer().ReplaceAll(iss.APIKey, "")
- if apiKey == "" {
- return nil, acct, 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())
- } else {
- email := iss.Email
- if email == "" {
- iss.logger.Warn("missing email address for ZeroSSL; it is strongly recommended to set one for next time")
- email = "[email protected]" // special email address that preserves backwards-compat, but which black-holes dashboard features, oh well
+ var dnsManager *certmagic.DNSManager
+ if iss.CNAMEValidation != nil && len(iss.CNAMEValidation.ProviderRaw) > 0 {
+ val, err := ctx.LoadModule(iss.CNAMEValidation, "ProviderRaw")
+ if err != nil {
+ return fmt.Errorf("loading DNS provider module: %v", err)
}
- if len(acct.Contact) == 0 {
- // we borrow the email from config or the default email, so ensure it's saved with the account
- acct.Contact = []string{"mailto:" + email}
+ dnsManager = &certmagic.DNSManager{
+ DNSProvider: val.(certmagic.DNSProvider),
+ TTL: time.Duration(iss.CNAMEValidation.TTL),
+ PropagationDelay: time.Duration(iss.CNAMEValidation.PropagationDelay),
+ PropagationTimeout: time.Duration(iss.CNAMEValidation.PropagationTimeout),
+ Resolvers: iss.CNAMEValidation.Resolvers,
+ OverrideDomain: iss.CNAMEValidation.OverrideDomain,
+ Logger: iss.logger.Named("cname"),
}
- endpoint = zerosslAPIBase + "/eab-credentials-email"
- form := url.Values{"email": []string{email}}
- body = strings.NewReader(form.Encode())
- }
-
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
- if err != nil {
- return nil, acct, 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, acct, 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, acct, fmt.Errorf("decoding API response: %v", err)
- }
- if result.Error.Code != 0 {
- return nil, acct, 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, acct, 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,
- }, acct, nil
-}
-// initialize modifies the template for the underlying ACMEIssuer
-// 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.ACMEIssuer.issuer.NewAccountFunc == nil {
- iss.ACMEIssuer.issuer.NewAccountFunc = iss.newAccountCallback
+ iss.issuer = &certmagic.ZeroSSLIssuer{
+ APIKey: repl.ReplaceAll(iss.APIKey, ""),
+ ValidityDays: iss.ValidityDays,
+ ListenHost: iss.ListenHost,
+ AltHTTPPort: iss.AlternateHTTPPort,
+ Storage: iss.storage,
+ CNAMEValidation: dnsManager,
+ Logger: iss.logger,
}
-}
-// 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)
+ return nil
}
// 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)
+ return iss.issuer.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()
+ return iss.issuer.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)
+ return iss.issuer.Revoke(ctx, cert, reason)
}
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
//
-// ... zerossl [<api_key>] {
-// ...
+// ... zerossl <api_key> {
+// validity_days <days>
+// alt_http_port <port>
+// dns <provider_name> ...
+// propagation_delay <duration>
+// propagation_timeout <duration>
+// resolvers <list...>
+// dns_ttl <duration>
// }
-//
-// Any of the subdirectives for the ACME issuer can be used in the block.
func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume issuer name
+
+ // API key is required
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ iss.APIKey = d.Val()
if d.NextArg() {
- iss.APIKey = d.Val()
- if d.NextArg() {
- return d.ArgErr()
- }
+ return d.ArgErr()
}
- if iss.ACMEIssuer == nil {
- iss.ACMEIssuer = new(ACMEIssuer)
- }
- err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment())
- if err != nil {
- return err
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ switch d.Val() {
+ case "validity_days":
+ if iss.ValidityDays != 0 {
+ return d.Errf("validity days is already specified: %d", iss.ValidityDays)
+ }
+ days, err := strconv.Atoi(d.Val())
+ if err != nil {
+ return d.Errf("invalid number of days %s: %v", d.Val(), err)
+ }
+ iss.ValidityDays = days
+
+ 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)
+ }
+ iss.AlternateHTTPPort = port
+
+ case "dns":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ provName := d.Val()
+ if iss.CNAMEValidation == nil {
+ iss.CNAMEValidation = new(DNSChallengeConfig)
+ }
+ unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName)
+ if err != nil {
+ return err
+ }
+ iss.CNAMEValidation.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil)
+
+ case "propagation_delay":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ delayStr := d.Val()
+ delay, err := caddy.ParseDuration(delayStr)
+ if err != nil {
+ return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err)
+ }
+ if iss.CNAMEValidation == nil {
+ iss.CNAMEValidation = new(DNSChallengeConfig)
+ }
+ iss.CNAMEValidation.PropagationDelay = caddy.Duration(delay)
+
+ case "propagation_timeout":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ timeoutStr := d.Val()
+ var timeout time.Duration
+ if timeoutStr == "-1" {
+ timeout = time.Duration(-1)
+ } else {
+ var err error
+ timeout, err = caddy.ParseDuration(timeoutStr)
+ if err != nil {
+ return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err)
+ }
+ }
+ if iss.CNAMEValidation == nil {
+ iss.CNAMEValidation = new(DNSChallengeConfig)
+ }
+ iss.CNAMEValidation.PropagationTimeout = caddy.Duration(timeout)
+
+ case "resolvers":
+ if iss.CNAMEValidation == nil {
+ iss.CNAMEValidation = new(DNSChallengeConfig)
+ }
+ iss.CNAMEValidation.Resolvers = d.RemainingArgs()
+ if len(iss.CNAMEValidation.Resolvers) == 0 {
+ return d.ArgErr()
+ }
+
+ case "dns_ttl":
+ if !d.NextArg() {
+ return d.ArgErr()
+ }
+ ttlStr := d.Val()
+ ttl, err := caddy.ParseDuration(ttlStr)
+ if err != nil {
+ return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err)
+ }
+ if iss.CNAMEValidation == nil {
+ iss.CNAMEValidation = new(DNSChallengeConfig)
+ }
+ iss.CNAMEValidation.TTL = caddy.Duration(ttl)
+
+ default:
+ return d.Errf("unrecognized zerossl issuer property: %s", d.Val())
+ }
}
+
return nil
}
-const 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)
+ _ certmagic.Issuer = (*ZeroSSLIssuer)(nil)
+ _ certmagic.Revoker = (*ZeroSSLIssuer)(nil)
+ _ caddy.Provisioner = (*ZeroSSLIssuer)(nil)
)