Skip to content

Commit

Permalink
caddytls: Add support for ZeroSSL; add Caddyfile support for issuers (#…
Browse files Browse the repository at this point in the history
…3633)

* caddytls: Add support for ZeroSSL; add Caddyfile support for issuers

Configuring issuers explicitly in a Caddyfile is not easily compatible
with existing ACME-specific parameters such as email or acme_ca which
infer the kind of issuer it creates (this is complicated now because
the ZeroSSL issuer wraps the ACME issuer)... oh well, we can revisit
that later if we need to.

New Caddyfile global option:

    {
        cert_issuer <name> ...
    }

Or, alternatively, as a tls subdirective:

    tls {
        issuer <name> ...
    }

For example, to use ZeroSSL with an API key:

    {
        cert_issuser zerossl API_KEY
    }

For now, that still uses ZeroSSL's ACME endpoint; it fetches EAB
credentials for you. You can also provide the EAB credentials directly
just like any other ACME endpoint:

    {
        cert_issuer acme {
            eab KEY_ID MAC_KEY
        }
    }

All these examples use the new global option (or tls subdirective). You
can still use traditional/existing options with ZeroSSL, since it's
just another ACME endpoint:

    {
        acme_ca  https://acme.zerossl.com/v2/DV90
        acme_eab KEY_ID MAC_KEY
    }

That's all there is to it. You just can't mix-and-match acme_* options
with cert_issuer, because it becomes confusing/ambiguous/complicated to
merge the settings.

* Fix broken test

This test was asserting buggy behavior, oops - glad this branch both
discovers and fixes the bug at the same time!

* Fix broken test (post-merge)

* Update modules/caddytls/acmeissuer.go

Fix godoc comment

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Add support for ZeroSSL's EAB-by-email endpoint

Also transform the ACMEIssuer into ZeroSSLIssuer implicitly if set to
the ZeroSSL endpoint without EAB (the ZeroSSLIssuer is needed to
generate EAB if not already provided); this is now possible with either
an API key or an email address.

* go.mod: Use latest certmagic, acmez, and x/net

* Wrap underlying logic rather than repeating it

Oops, duh

* Form-encode email info into request body for EAB endpoint

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
  • Loading branch information
mholt and francislavoie authored Aug 11, 2020
1 parent c42bfaf commit 66863aa
Show file tree
Hide file tree
Showing 12 changed files with 579 additions and 96 deletions.
52 changes: 44 additions & 8 deletions caddyconfig/httpcaddyfile/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap/zapcore"
)
Expand Down Expand Up @@ -76,6 +77,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// ca_root <pem_file>
// dns <provider_name>
// on_demand
// issuer <module_name> ...
// }
//
func parseTLS(h Helper) ([]ConfigValue, error) {
Expand All @@ -85,6 +87,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var certSelector caddytls.CustomCertSelectionPolicy
var acmeIssuer *caddytls.ACMEIssuer
var internalIssuer *caddytls.InternalIssuer
var issuer certmagic.Issuer
var onDemand bool

for h.Next() {
Expand Down Expand Up @@ -276,6 +279,28 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
MACKey: arg[1],
}

case "issuer":
if !h.NextArg() {
return nil, h.ArgErr()
}
modName := h.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, h.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
if err != nil {
return nil, err
}
issuer, ok = unm.(certmagic.Issuer)
if !ok {
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
}

case "dns":
if !h.NextArg() {
return nil, h.ArgErr()
Expand Down Expand Up @@ -350,7 +375,24 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
// the logic to support this would be complex
return nil, h.Err("cannot use both ACME and internal issuers in same server block")
}
if acmeIssuer != nil {
if issuer != nil && (acmeIssuer != nil || internalIssuer != nil) {
// similarly, the logic to support this would be complex
return nil, h.Err("when defining an issuer, all its config must be in its block, rather than from separate tls subdirectives")
}
switch {
case issuer != nil:
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: issuer,
})

case internalIssuer != nil:
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: internalIssuer,
})

case acmeIssuer != nil:
// fill in global defaults, if configured
if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = email.(string)
Expand All @@ -361,15 +403,9 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
}

configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: acmeIssuer,
})
} else if internalIssuer != nil {
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
Value: internalIssuer,
Value: disambiguateACMEIssuer(acmeIssuer),
})
}

Expand Down
29 changes: 29 additions & 0 deletions caddyconfig/httpcaddyfile/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)

Expand All @@ -35,6 +36,7 @@ func init() {
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
RegisterGlobalOption("acme_dns", parseOptSingleString)
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
RegisterGlobalOption("email", parseOptSingleString)
RegisterGlobalOption("admin", parseOptAdmin)
RegisterGlobalOption("on_demand_tls", parseOptOnDemand)
Expand Down Expand Up @@ -210,6 +212,33 @@ func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
return eab, nil
}

func parseOptCertIssuer(d *caddyfile.Dispenser) (interface{}, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
}
if !d.Next() { // get issuer module name
return nil, d.ArgErr()
}
modName := d.Val()
mod, err := caddy.GetModule("tls.issuance." + modName)
if err != nil {
return nil, d.Errf("getting issuer module '%s': %v", modName, err)
}
unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return nil, d.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
}
err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s is not a certmagic.Issuer", mod.ID)
}
return iss, nil
}

func parseOptSingleString(d *caddyfile.Dispenser) (interface{}, error) {
d.Next() // consume parameter name
if !d.Next() {
Expand Down
40 changes: 32 additions & 8 deletions caddyconfig/httpcaddyfile/tlsapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"reflect"
"sort"
"strconv"
"strings"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
Expand Down Expand Up @@ -135,8 +136,11 @@ func (st ServerType) buildTLSApp(
// issuer, skip, since we intend to adjust only ACME issuers
var acmeIssuer *caddytls.ACMEIssuer
if ap.Issuer != nil {
var ok bool
if acmeIssuer, ok = ap.Issuer.(*caddytls.ACMEIssuer); !ok {
// ensure we include any issuer that embeds/wraps an underlying ACME issuer
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
if acmeWrapper, ok := ap.Issuer.(acmeCapable); ok {
acmeIssuer = acmeWrapper.GetACMEIssuer()
} else {
break
}
}
Expand Down Expand Up @@ -348,6 +352,8 @@ func (st ServerType) buildTLSApp(
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
issuer, hasIssuer := options["cert_issuer"]

acmeCA, hasACMECA := options["acme_ca"]
acmeCARoot, hasACMECARoot := options["acme_ca_root"]
acmeDNS, hasACMEDNS := options["acme_dns"]
Expand All @@ -357,7 +363,7 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
localCerts, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]

hasGlobalAutomationOpts := hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType
hasGlobalAutomationOpts := hasIssuer || hasACMECA || hasACMECARoot || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts || hasKeyType

// if there are no global options related to automation policies
// set, then we can just return right away
Expand All @@ -369,8 +375,16 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
}

ap := new(caddytls.AutomationPolicy)
if keyType != nil {
ap.KeyType = keyType.(string)
}

if localCerts != nil {
if hasIssuer {
if hasACMECA || hasACMEDNS || hasACMEEAB || hasEmail || hasLocalCerts {
return nil, fmt.Errorf("global options are ambiguous: cert_issuer is confusing when combined with acme_*, email, or local_certs options")
}
ap.Issuer = issuer.(certmagic.Issuer)
} else if localCerts != nil {
// internal issuer enabled trumps any ACME configurations; useful in testing
ap.Issuer = new(caddytls.InternalIssuer) // we'll encode it later
} else {
Expand Down Expand Up @@ -402,15 +416,25 @@ func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddycon
if acmeEAB != nil {
mgr.ExternalAccount = acmeEAB.(*acme.EAB)
}
if keyType != nil {
ap.KeyType = keyType.(string)
}
ap.Issuer = mgr // we'll encode it later
ap.Issuer = disambiguateACMEIssuer(mgr) // we'll encode it later
}

return ap, nil
}

// disambiguateACMEIssuer returns an issuer based on the properties of acmeIssuer.
// If acmeIssuer implicitly configures a certain kind of ACMEIssuer (for example,
// ZeroSSL), the proper wrapper over acmeIssuer will be returned instead.
func disambiguateACMEIssuer(acmeIssuer *caddytls.ACMEIssuer) certmagic.Issuer {
// as a special case, we integrate with ZeroSSL's ACME endpoint if it looks like an
// implicit ZeroSSL configuration (this requires a wrapper type over ACMEIssuer
// because of the EAB generation; if EAB is provided, we can use plain ACMEIssuer)
if strings.Contains(acmeIssuer.CA, "acme.zerossl.com") && acmeIssuer.ExternalAccount == nil {
return &caddytls.ZeroSSLIssuer{ACMEIssuer: acmeIssuer}
}
return acmeIssuer
}

// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
Expand Down
3 changes: 2 additions & 1 deletion caddytest/integration/caddyfile_adapt/global_options.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
{
"issuer": {
"module": "internal"
}
},
"key_type": "ed25519"
}
],
"on_demand": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
{
"issuer": {
"module": "internal"
}
},
"key_type": "ed25519"
}
],
"on_demand": {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ require (
github.com/Masterminds/sprig/v3 v3.1.0
github.com/alecthomas/chroma v0.8.0
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
github.com/caddyserver/certmagic v0.11.3-0.20200808143826-2e100d6d0bcf
github.com/caddyserver/certmagic v0.11.3-0.20200810220624-10a8b5c72339
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.5.1
github.com/jsternberg/zap-logfmt v1.2.0
github.com/klauspost/compress v1.10.10
github.com/klauspost/cpuid v1.2.5
github.com/lucas-clemente/quic-go v0.17.3
github.com/mholt/acmez v0.1.0
github.com/mholt/acmez v0.1.1-0.20200810215816-dbe88fc6cf09
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/naoina/toml v0.1.1
github.com/smallstep/certificates v0.14.6
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTK
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/caddyserver/certmagic v0.11.3-0.20200808143826-2e100d6d0bcf h1:Srcj7CdiavYImKMLDj8sSJKS3htwwwJB3bjPRTXh+P0=
github.com/caddyserver/certmagic v0.11.3-0.20200808143826-2e100d6d0bcf/go.mod h1:z0loWzjr6Sr8rOG2pdsiwajDpAcck2wfF8E7hTV0qT4=
github.com/caddyserver/certmagic v0.11.3-0.20200810220624-10a8b5c72339 h1:wTD+Y63XoBtiTJhe/Xn7WLrwKenmjkt2WxH3FP+Y0DM=
github.com/caddyserver/certmagic v0.11.3-0.20200810220624-10a8b5c72339/go.mod h1:mqOzOvKa7UcC+TWbBLcP0ZLRut/xaaQBw0hRGWHBIkY=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
Expand Down Expand Up @@ -384,8 +384,8 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/acmez v0.1.0 h1:LL7WQVCjZvwbo3pjAUdsqEao5kI8nyHubAGtCzrTS5g=
github.com/mholt/acmez v0.1.0/go.mod h1:rNLSzYu5VR6ggyarXDKDCNefo06cOkJn+VObTDJCk0U=
github.com/mholt/acmez v0.1.1-0.20200810215816-dbe88fc6cf09 h1:J7NVJ46iBFeWsUc5aeVv8QNO2mLhI6rJKIbpAsH7d7g=
github.com/mholt/acmez v0.1.1-0.20200810215816-dbe88fc6cf09/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
Expand Down
16 changes: 11 additions & 5 deletions modules/caddyhttp/autohttps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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 }
Loading

0 comments on commit 66863aa

Please sign in to comment.