diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go index 64c5d4f43bb..2d178336511 100644 --- a/caddyconfig/httpcaddyfile/addresses.go +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -172,20 +172,14 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str httpsPort = strconv.Itoa(hsport.(int)) } - lnPort := DefaultPort + // default port is the HTTPS port + lnPort := httpsPort if addr.Port != "" { // port explicitly defined lnPort = addr.Port - } else if addr.Scheme != "" { + } else if addr.Scheme == "http" { // port inferred from scheme - if addr.Scheme == "http" { - lnPort = httpPort - } else if addr.Scheme == "https" { - lnPort = httpsPort - } - } else if certmagic.HostQualifies(addr.Host) { - // automatic HTTPS - lnPort = httpsPort + lnPort = httpPort } // error if scheme and port combination violate convention @@ -213,7 +207,6 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str for lnStr := range listeners { listenersList = append(listenersList, lnStr) } - // sort.Strings(listenersList) // TODO: is sorting necessary? return listenersList, nil } @@ -317,9 +310,6 @@ func (a Address) String() string { // Normalize returns a normalized version of a. func (a Address) Normalize() Address { path := a.Path - if !caseSensitivePath { - path = strings.ToLower(path) - } // ensure host is normalized if it's an IP address host := a.Host @@ -357,10 +347,3 @@ func (a Address) Key() string { } return res } - -const ( - // DefaultPort is the default port to use. - DefaultPort = "2015" - - caseSensitivePath = false // TODO: Used? -) diff --git a/caddyconfig/httpcaddyfile/addresses_test.go b/caddyconfig/httpcaddyfile/addresses_test.go index e22535c5dc6..8de1f099d02 100644 --- a/caddyconfig/httpcaddyfile/addresses_test.go +++ b/caddyconfig/httpcaddyfile/addresses_test.go @@ -1,7 +1,6 @@ package httpcaddyfile import ( - "strings" "testing" ) @@ -156,15 +155,8 @@ func TestKeyNormalization(t *testing.T) { t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err) continue } - expect := tc.expect - if !caseSensitivePath { - // every other part of the address should be lowercased when normalized, - // so simply lower-case the whole thing to do case-insensitive comparison - // of the path as well - expect = strings.ToLower(expect) - } - if actual := addr.Normalize().Key(); actual != expect { - t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, expect) + if actual := addr.Normalize().Key(); actual != tc.expect { + t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, tc.expect) } } diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 3b5a4f5459e..91c1c0a91b1 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -95,7 +95,7 @@ func parseRoot(h Helper) ([]ConfigValue, error) { // parseTLS parses the tls directive. Syntax: // -// tls []|[ ] { +// tls [|internal]|[ ] { // protocols [] // ciphers // curves @@ -106,23 +106,11 @@ func parseRoot(h Helper) ([]ConfigValue, error) { // } // func parseTLS(h Helper) ([]ConfigValue, error) { - var configVals []ConfigValue - var cp *caddytls.ConnectionPolicy var fileLoader caddytls.FileLoader var folderLoader caddytls.FolderLoader - var mgr caddytls.ACMEIssuer - - // fill in global defaults, if configured - if email := h.Option("email"); email != nil { - mgr.Email = email.(string) - } - if acmeCA := h.Option("acme_ca"); acmeCA != nil { - mgr.CA = acmeCA.(string) - } - if caPemFile := h.Option("acme_ca_root"); caPemFile != nil { - mgr.TrustedRootsPEMFiles = append(mgr.TrustedRootsPEMFiles, caPemFile.(string)) - } + var acmeIssuer *caddytls.ACMEIssuer + var internalIssuer *caddytls.InternalIssuer for h.Next() { // file certificate loader @@ -130,10 +118,17 @@ func parseTLS(h Helper) ([]ConfigValue, error) { switch len(firstLine) { case 0: case 1: - if !strings.Contains(firstLine[0], "@") { - return nil, h.Err("single argument must be an email address") + if firstLine[0] == "internal" { + internalIssuer = new(caddytls.InternalIssuer) + } else if !strings.Contains(firstLine[0], "@") { + return nil, h.Err("single argument must either be 'internal' or an email address") + } else { + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + acmeIssuer.Email = firstLine[0] } - mgr.Email = firstLine[0] + case 2: certFilename := firstLine[0] keyFilename := firstLine[1] @@ -143,7 +138,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { // https://github.com/caddyserver/caddy/issues/2588 ... but we // must be careful about how we do this; being careless will // lead to failed handshakes - + // // we need to remember which cert files we've seen, since we // must load each cert only once; otherwise, they each get a // different tag... since a cert loaded twice has the same @@ -152,7 +147,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) { // policy that is looking for any tag but the last one to be // loaded won't find it, and TLS handshakes will fail (see end) // of issue #3004) - + // // tlsCertTags maps certificate filenames to their tag. // This is used to remember which tag is used for each // certificate files, since we need to avoid loading @@ -256,29 +251,38 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if len(arg) != 1 { return nil, h.ArgErr() } - mgr.CA = arg[0] + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + acmeIssuer.CA = arg[0] // DNS provider for ACME DNS challenge case "dns": if !h.Next() { return nil, h.ArgErr() } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } provName := h.Val() - if mgr.Challenges == nil { - mgr.Challenges = new(caddytls.ChallengesConfig) + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } dnsProvModule, err := caddy.GetModule("tls.dns." + provName) if err != nil { return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err) } - mgr.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, h.warnings) + acmeIssuer.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, h.warnings) case "ca_root": arg := h.RemainingArgs() if len(arg) != 1 { return nil, h.ArgErr() } - mgr.TrustedRootsPEMFiles = append(mgr.TrustedRootsPEMFiles, arg[0]) + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0]) default: return nil, h.Errf("unknown subdirective: %s", h.Val()) @@ -291,6 +295,9 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } } + // begin building the final config values + var configVals []ConfigValue + // certificate loaders if len(fileLoader) > 0 { configVals = append(configVals, ConfigValue{ @@ -322,10 +329,30 @@ func parseTLS(h Helper) ([]ConfigValue, error) { } // automation policy - if !reflect.DeepEqual(mgr, caddytls.ACMEIssuer{}) { + if acmeIssuer != nil && internalIssuer != nil { + // 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 { + // fill in global defaults, if configured + if email := h.Option("email"); email != nil && acmeIssuer.Email == "" { + acmeIssuer.Email = email.(string) + } + if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" { + acmeIssuer.CA = acmeCA.(string) + } + 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: mgr, + Value: internalIssuer, }) } diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index d880d9758e3..96f2bb04d3c 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -185,10 +185,10 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, for _, p := range pairings { for i, sblock := range p.serverBlocks { // tls automation policies - if mmVals, ok := sblock.pile["tls.cert_issuer"]; ok { - for _, mmVal := range mmVals { - mm := mmVal.Value.(certmagic.Issuer) - sblockHosts, err := st.autoHTTPSHosts(sblock) + if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok { + for _, issuerVal := range issuerVals { + issuer := issuerVal.Value.(certmagic.Issuer) + sblockHosts, err := st.hostsFromServerBlockKeys(sblock.block) if err != nil { return nil, warnings, err } @@ -198,7 +198,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, } tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{ Hosts: sblockHosts, - IssuerRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID.Name(), &warnings), + IssuerRaw: caddyconfig.JSONModuleObject(issuer, "module", issuer.(caddy.Module).CaddyModule().ID.Name(), &warnings), }) } else { warnings = append(warnings, caddyconfig.Warning{ @@ -500,16 +500,13 @@ func (st *ServerType) serversFromPairings( // tls: connection policies and toggle auto HTTPS defaultSNI := tryString(options["default_sni"], warnings) - autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock) - if err != nil { - return nil, err - } - if _, ok := sblock.pile["tls.off"]; ok && len(autoHTTPSQualifiedHosts) > 0 { + if _, ok := sblock.pile["tls.off"]; ok { + // TODO: right now, no directives yield any tls.off value... // tls off: disable TLS (and automatic HTTPS) for server block's names if srv.AutoHTTPS == nil { srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) } - srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...) + srv.AutoHTTPS.Disabled = true } else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok { // tls connection policies @@ -741,25 +738,10 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro return subroute, nil } -func (st ServerType) autoHTTPSHosts(sb serverBlock) ([]string, error) { - // get the hosts for this server block... - hosts, err := st.hostsFromServerBlockKeys(sb.block) - if err != nil { - return nil, err - } - // ...and of those, which ones qualify for auto HTTPS - var autoHTTPSQualifiedHosts []string - for _, h := range hosts { - if certmagic.HostQualifies(h) { - autoHTTPSQualifiedHosts = append(autoHTTPSQualifiedHosts, h) - } - } - return autoHTTPSQualifiedHosts, nil -} - // consolidateRoutes combines routes with the same properties // (same matchers, same Terminal and Group settings) for a // cleaner overall output. +// FIXME: See caddyserver/caddy#3108 func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList { for i := 0; i < len(routes)-1; i++ { if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) && diff --git a/go.mod b/go.mod index 1edcd2b4472..8e00de95f4a 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.14 require ( github.com/Masterminds/sprig/v3 v3.0.2 - github.com/alecthomas/chroma v0.7.1 + github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a github.com/andybalholm/brotli v1.0.0 - github.com/caddyserver/certmagic v0.10.0 + github.com/caddyserver/certmagic v0.10.1 github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/go-acme/lego/v3 v3.4.0 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e @@ -24,8 +24,8 @@ require ( github.com/smallstep/cli v0.14.0-rc.3 github.com/smallstep/truststore v0.9.4 github.com/vulcand/oxy v1.0.0 - github.com/yuin/goldmark v1.1.24 - github.com/yuin/goldmark-highlighting v0.0.0-20200218065240-d1af22c1126f + github.com/yuin/goldmark v1.1.25 + github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 go.uber.org/zap v1.14.0 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/net v0.0.0-20200301022130-244492dfa37a diff --git a/go.sum b/go.sum index f19bdb1930f..0a685cd890a 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75 h1:3ILj github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ= -github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc= +github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a h1:3v1NrYWWqp2S72e4HLgxKt83B3l0lnORDholH/ihoMM= +github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= @@ -108,8 +108,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.10.0 h1:kbQsqN5RmdUMClVUNd8svTzemCo8W6NNc8UJOXnUIu0= -github.com/caddyserver/certmagic v0.10.0/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ= +github.com/caddyserver/certmagic v0.10.1 h1:k9E+C4b8WM3sTs3PSfmWIAwxtO9cXtr0bDHX2Bc0RIM= +github.com/caddyserver/certmagic v0.10.1/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ= github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU= github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -730,10 +730,10 @@ github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4m github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.22/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.24 h1:K4FemPDr4x/ZcqldoXWnexTLfdMIy2eEfXxsLnotTRI= -github.com/yuin/goldmark v1.1.24/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark-highlighting v0.0.0-20200218065240-d1af22c1126f h1:5295skDVJn90SXIYI22jOMeR9XbnuN76y/V1m9N8ITQ= -github.com/yuin/goldmark-highlighting v0.0.0-20200218065240-d1af22c1126f/go.mod h1:9yW2CHuRSORvHgw7YfybB09PqUZTbzERyW3QFvd8+0Q= +github.com/yuin/goldmark v1.1.25 h1:isv+Q6HQAmmL2Ofcmg8QauBmDPlUUnSoNhEcC940Rds= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 h1:VWSxtAiQNh3zgHJpdpkpVYjTPqRE3P6UZCOPa1nRDio= +github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691/go.mod h1:YLF3kDffRfUH/bTxOxHhV6lxwIB3Vfj91rEwNMS9MXo= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 7dab3597a1a..6a23ca0a869 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" @@ -130,8 +131,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v", srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err) } - if certmagic.HostQualifies(d) && - !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) { + if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) { serverDomainSet[d] = struct{}{} } } @@ -161,6 +161,15 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er ) continue } + + // most clients don't accept wildcards like *.tld... we + // can handle that, but as a courtesy, warn the user + if strings.Contains(d, "*") && + strings.Count(strings.Trim(d, "."), ".") == 1 { + app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)", + zap.String("domain", d)) + } + uniqueDomainsForCerts[d] = struct{}{} } } @@ -202,12 +211,18 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // we now have a list of all the unique names for which we need certs; // turn the set into a slice so that phase 2 can use it app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) + var internal, external []string for d := range uniqueDomainsForCerts { + if certmagic.SubjectQualifiesForPublicCert(d) { + external = append(external, d) + } else { + internal = append(internal, d) + } app.allCertDomains = append(app.allCertDomains, d) } // ensure there is an automation policy to handle these certs - err := app.createAutomationPolicy(ctx) + err := app.createAutomationPolicies(ctx, external, internal) if err != nil { return err } @@ -354,23 +369,29 @@ redirServersLoop: return nil } -// createAutomationPolicy ensures that certificates for this app are -// managed properly; for example, it's implied that the HTTPPort -// should also be the port the HTTP challenge is solved on; the same -// for HTTPS port and TLS-ALPN challenge also. We need to tell the -// TLS app to manage these certs by honoring those port configurations, -// so we either find an existing matching automation policy with an -// ACME issuer, or make a new one and append it. -func (app *App) createAutomationPolicy(ctx caddy.Context) error { +// createAutomationPolicy ensures that automated certificates for this +// app are managed properly. This adds up to two automation policies: +// one for the public names, and one for the internal names. If a catch-all +// automation policy exists, it will be shallow-copied and used as the +// base for the new ones (this is important for preserving behavior the +// user intends to be "defaults"). +func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, internalNames []string) error { + // nothing to do if no names to manage certs for + if len(publicNames) == 0 && len(internalNames) == 0 { + return nil + } + + // start by finding a base policy that the user may have defined + // which should, in theory, apply to any policies derived from it; + // typically this would be a "catch-all" policy with no host filter var matchingPolicy *caddytls.AutomationPolicy - var acmeIssuer *caddytls.ACMEIssuer if app.tlsApp.Automation != nil { - // maybe we can find an exisitng one that matches; this is - // useful if the user made a single automation policy to - // set the CA endpoint to a test/staging endpoint (very - // common), but forgot to customize the ports here, while - // setting them in the HTTP app instead (I did this too - // many times) + // if an existing policy matches (specifically, a catch-all policy), + // we should inherit from it, because that is what the user expects; + // this is very common for user setting a default issuer, with a + // custom CA endpoint, for example - whichever one we choose must + // have a host list that is a superset of the policy we make... + // the policy with no host filter is guaranteed to qualify for _, ap := range app.tlsApp.Automation.Policies { if len(ap.Hosts) == 0 { matchingPolicy = ap @@ -378,51 +399,78 @@ func (app *App) createAutomationPolicy(ctx caddy.Context) error { } } } - if matchingPolicy != nil { - // if it has an ACME issuer, maybe we can just use that - acmeIssuer, _ = matchingPolicy.Issuer.(*caddytls.ACMEIssuer) - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.HTTP == nil { - acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig) - } - if acmeIssuer.Challenges.HTTP.AlternatePort == 0 { - // don't overwrite existing explicit config - acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort - } - if acmeIssuer.Challenges.TLSALPN == nil { - acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig) + if matchingPolicy == nil { + matchingPolicy = new(caddytls.AutomationPolicy) } - if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 { - // don't overwrite existing explicit config - acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort + + // addPolicy adds an automation policy that uses issuer for hosts. + addPolicy := func(issuer certmagic.Issuer, hosts []string) error { + // shallow-copy the matching policy; we want to inherit + // from it, not replace it... this takes two lines to + // overrule compiler optimizations + policyCopy := *matchingPolicy + newPolicy := &policyCopy + + // very important to provision it, since we are + // bypassing the JSON-unmarshaling step + if prov, ok := issuer.(caddy.Provisioner); ok { + err := prov.Provision(ctx) + if err != nil { + return err + } + } + newPolicy.Issuer = issuer + newPolicy.Hosts = hosts + + return app.tlsApp.AddAutomationPolicy(newPolicy) } - if matchingPolicy == nil { - // if there was no matching policy, we'll have to append our own - err := app.tlsApp.AddAutomationPolicy(&caddytls.AutomationPolicy{ - Hosts: app.allCertDomains, - Issuer: acmeIssuer, - }) - if err != nil { + if len(publicNames) > 0 { + var acmeIssuer *caddytls.ACMEIssuer + // if it has an ACME issuer, maybe we can just use that + // TODO: we might need a deep copy here, like a Clone() method on ACMEIssuer... + acmeIssuer, _ = matchingPolicy.Issuer.(*caddytls.ACMEIssuer) + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if app.HTTPPort > 0 || app.HTTPSPort > 0 { + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + } + if app.HTTPPort > 0 { + if acmeIssuer.Challenges.HTTP == nil { + acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig) + } + // don't overwrite existing explicit config + if acmeIssuer.Challenges.HTTP.AlternatePort == 0 { + acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort + } + } + if app.HTTPSPort > 0 { + if acmeIssuer.Challenges.TLSALPN == nil { + acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig) + } + // don't overwrite existing explicit config + if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 { + acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort + } + } + if err := addPolicy(acmeIssuer, publicNames); err != nil { return err } - } else { - // if there was an existing matching policy, we need to reprovision - // its issuer (because we just changed its port settings and it has - // to re-build its stored certmagic config template with the new - // values), then re-assign the Issuer pointer on the policy struct - // because our type assertion changed the address - err := acmeIssuer.Provision(ctx) - if err != nil { + } + + if len(internalNames) > 0 { + internalIssuer := new(caddytls.InternalIssuer) + if err := addPolicy(internalIssuer, internalNames); err != nil { return err } - matchingPolicy.Issuer = acmeIssuer + } + + err := app.tlsApp.Validate() + if err != nil { + return err } return nil diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 6ad70f560d1..06719b51e16 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -286,8 +286,8 @@ func (app *App) Start() error { } // enable TLS if there is a policy and if this is not the HTTP port - if len(srv.TLSConnPolicies) > 0 && - int(listenAddr.StartPort+portOffset) != app.httpPort() { + useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() + if useTLS { // create TLS listener tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx) ln = tls.NewListener(ln, tlsCfg) @@ -317,6 +317,12 @@ func (app *App) Start() error { ///////// } + app.logger.Debug("starting server loop", + zap.String("address", lnAddr), + zap.Bool("http3", srv.ExperimentalHTTP3), + zap.Bool("tls", useTLS), + ) + go s.Serve(ln) app.servers = append(app.servers, s) } diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go index fa6560b8193..18e9be3bc50 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -23,7 +23,6 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/certmagic" @@ -90,11 +89,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { Routes: caddyhttp.RouteList{route}, } if listen == "" { - if certmagic.HostQualifies(domain) { - listen = ":" + strconv.Itoa(certmagic.HTTPSPort) - } else { - listen = ":" + httpcaddyfile.DefaultPort - } + listen = ":" + strconv.Itoa(certmagic.HTTPSPort) } server.Listen = []string{listen} diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 6f70d144ff2..6110ca80b19 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -25,11 +25,9 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" - "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" - "github.com/caddyserver/certmagic" ) func init() { @@ -67,7 +65,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { changeHost := fs.Bool("change-host-header") if from == "" { - from = "localhost:" + httpcaddyfile.DefaultPort + from = "localhost:443" } // URLs need a scheme in order to parse successfully @@ -129,11 +127,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { } } - listen := ":80" + listen := ":443" if urlPort := fromURL.Port(); urlPort != "" { listen = ":" + urlPort - } else if certmagic.HostQualifies(urlHost) { - listen = ":443" } server := &caddyhttp.Server{ diff --git a/modules/caddypki/ca.go b/modules/caddypki/ca.go new file mode 100644 index 00000000000..f15883e1df6 --- /dev/null +++ b/modules/caddypki/ca.go @@ -0,0 +1,334 @@ +// 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 caddypki + +import ( + "crypto/x509" + "encoding/json" + "fmt" + "path" + "sync" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" + "go.uber.org/zap" +) + +// CA describes a certificate authority, which consists of +// root/signing certificates and various settings pertaining +// to the issuance of certificates and trusting them. +type CA struct { + // The user-facing name of the certificate authority. + Name string `json:"name,omitempty"` + + // The name to put in the CommonName field of the + // root certificate. + RootCommonName string `json:"root_common_name,omitempty"` + + // The name to put in the CommonName field of the + // intermediate certificates. + IntermediateCommonName string `json:"intermediate_common_name,omitempty"` + + // Whether Caddy will attempt to install the CA's root + // into the system trust store, as well as into Java + // and Mozilla Firefox trust stores. Default: true. + InstallTrust *bool `json:"install_trust,omitempty"` + + Root *KeyPair `json:"root,omitempty"` + Intermediate *KeyPair `json:"intermediate,omitempty"` + + // Optionally configure a separate storage module associated with this + // issuer, instead of using Caddy's global/default-configured storage. + // This can be useful if you want to keep your signing keys in a + // separate location from your leaf certificates. + StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` + + id string + storage certmagic.Storage + root, inter *x509.Certificate + interKey interface{} // TODO: should we just store these as crypto.Signer? + mu *sync.RWMutex + + rootCertPath string // mainly used for logging purposes if trusting + log *zap.Logger +} + +// Provision sets up the CA. +func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error { + ca.mu = new(sync.RWMutex) + ca.log = log.Named("ca." + id) + + if id == "" { + return fmt.Errorf("CA ID is required (use 'local' for the default CA)") + } + ca.mu.Lock() + ca.id = id + ca.mu.Unlock() + + if ca.StorageRaw != nil { + val, err := ctx.LoadModule(ca, "StorageRaw") + if err != nil { + return fmt.Errorf("loading storage module: %v", err) + } + cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() + if err != nil { + return fmt.Errorf("creating storage configuration: %v", err) + } + ca.storage = cmStorage + } + if ca.storage == nil { + ca.storage = ctx.Storage() + } + + if ca.Name == "" { + ca.Name = defaultCAName + } + if ca.RootCommonName == "" { + ca.RootCommonName = defaultRootCommonName + } + if ca.IntermediateCommonName == "" { + ca.IntermediateCommonName = defaultIntermediateCommonName + } + + // load the certs and key that will be used for signing + var rootCert, interCert *x509.Certificate + var rootKey, interKey interface{} + var err error + if ca.Root != nil { + if ca.Root.Format == "" || ca.Root.Format == "pem_file" { + ca.rootCertPath = ca.Root.Certificate + } + rootCert, rootKey, err = ca.Root.Load() + } else { + ca.rootCertPath = "storage:" + ca.storageKeyRootCert() + rootCert, rootKey, err = ca.loadOrGenRoot() + } + if err != nil { + return err + } + if ca.Intermediate != nil { + interCert, interKey, err = ca.Intermediate.Load() + } else { + interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey) + } + if err != nil { + return err + } + + ca.mu.Lock() + ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey + ca.mu.Unlock() + + return nil +} + +// ID returns the CA's ID, as given by the user in the config. +func (ca CA) ID() string { + return ca.id +} + +// RootCertificate returns the CA's root certificate (public key). +func (ca CA) RootCertificate() *x509.Certificate { + ca.mu.RLock() + defer ca.mu.RUnlock() + return ca.root +} + +// RootKey returns the CA's root private key. Since the root key is +// not cached in memory long-term, it needs to be loaded from storage, +// which could yield an error. +func (ca CA) RootKey() (interface{}, error) { + _, rootKey, err := ca.loadOrGenRoot() + return rootKey, err +} + +// IntermediateCertificate returns the CA's intermediate +// certificate (public key). +func (ca CA) IntermediateCertificate() *x509.Certificate { + ca.mu.RLock() + defer ca.mu.RUnlock() + return ca.inter +} + +// IntermediateKey returns the CA's intermediate private key. +func (ca CA) IntermediateKey() interface{} { + ca.mu.RLock() + defer ca.mu.RUnlock() + return ca.interKey +} + +func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { + rootCertPEM, err := ca.storage.Load(ca.storageKeyRootCert()) + if err != nil { + if _, ok := err.(certmagic.ErrNotExist); !ok { + return nil, nil, fmt.Errorf("loading root cert: %v", err) + } + + // TODO: should we require that all or none of the assets are required before overwriting anything? + rootCert, rootKey, err = ca.genRoot() + if err != nil { + return nil, nil, fmt.Errorf("generating root: %v", err) + } + } + + if rootCert == nil { + rootCert, err = pemDecodeSingleCert(rootCertPEM) + if err != nil { + return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err) + } + } + if rootKey == nil { + rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey()) + if err != nil { + return nil, nil, fmt.Errorf("loading root key: %v", err) + } + rootKey, err = pemDecodePrivateKey(rootKeyPEM) + if err != nil { + return nil, nil, fmt.Errorf("decoding root key: %v", err) + } + } + + return rootCert, rootKey, nil +} + +func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) { + repl := ca.newReplacer() + + rootCert, rootKey, err = generateRoot(repl.ReplaceAll(ca.RootCommonName, "")) + if err != nil { + return nil, nil, fmt.Errorf("generating CA root: %v", err) + } + rootCertPEM, err := pemEncodeCert(rootCert.Raw) + if err != nil { + return nil, nil, fmt.Errorf("encoding root certificate: %v", err) + } + err = ca.storage.Store(ca.storageKeyRootCert(), rootCertPEM) + if err != nil { + return nil, nil, fmt.Errorf("saving root certificate: %v", err) + } + rootKeyPEM, err := pemEncodePrivateKey(rootKey) + if err != nil { + return nil, nil, fmt.Errorf("encoding root key: %v", err) + } + err = ca.storage.Store(ca.storageKeyRootKey(), rootKeyPEM) + if err != nil { + return nil, nil, fmt.Errorf("saving root key: %v", err) + } + + return rootCert, rootKey, nil +} + +func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { + interCertPEM, err := ca.storage.Load(ca.storageKeyIntermediateCert()) + if err != nil { + if _, ok := err.(certmagic.ErrNotExist); !ok { + return nil, nil, fmt.Errorf("loading intermediate cert: %v", err) + } + + // TODO: should we require that all or none of the assets are required before overwriting anything? + interCert, interKey, err = ca.genIntermediate(rootCert, rootKey) + if err != nil { + return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err) + } + } + + if interCert == nil { + interCert, err = pemDecodeSingleCert(interCertPEM) + if err != nil { + return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err) + } + } + + if interKey == nil { + interKeyPEM, err := ca.storage.Load(ca.storageKeyIntermediateKey()) + if err != nil { + return nil, nil, fmt.Errorf("loading intermediate key: %v", err) + } + interKey, err = pemDecodePrivateKey(interKeyPEM) + if err != nil { + return nil, nil, fmt.Errorf("decoding intermediate key: %v", err) + } + } + + return interCert, interKey, nil +} + +func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) { + repl := ca.newReplacer() + + rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey()) + if err != nil { + return nil, nil, fmt.Errorf("loading root key to sign new intermediate: %v", err) + } + rootKey, err = pemDecodePrivateKey(rootKeyPEM) + if err != nil { + return nil, nil, fmt.Errorf("decoding root key: %v", err) + } + interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey) + if err != nil { + return nil, nil, fmt.Errorf("generating CA intermediate: %v", err) + } + interCertPEM, err := pemEncodeCert(interCert.Raw) + if err != nil { + return nil, nil, fmt.Errorf("encoding intermediate certificate: %v", err) + } + err = ca.storage.Store(ca.storageKeyIntermediateCert(), interCertPEM) + if err != nil { + return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err) + } + interKeyPEM, err := pemEncodePrivateKey(interKey) + if err != nil { + return nil, nil, fmt.Errorf("encoding intermediate key: %v", err) + } + err = ca.storage.Store(ca.storageKeyIntermediateKey(), interKeyPEM) + if err != nil { + return nil, nil, fmt.Errorf("saving intermediate key: %v", err) + } + + return interCert, interKey, nil +} + +func (ca CA) storageKeyCAPrefix() string { + return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.id)) +} +func (ca CA) storageKeyRootCert() string { + return path.Join(ca.storageKeyCAPrefix(), "root.crt") +} +func (ca CA) storageKeyRootKey() string { + return path.Join(ca.storageKeyCAPrefix(), "root.key") +} +func (ca CA) storageKeyIntermediateCert() string { + return path.Join(ca.storageKeyCAPrefix(), "intermediate.crt") +} +func (ca CA) storageKeyIntermediateKey() string { + return path.Join(ca.storageKeyCAPrefix(), "intermediate.key") +} + +func (ca CA) newReplacer() *caddy.Replacer { + repl := caddy.NewReplacer() + repl.Set("pki.ca.name", ca.Name) + return repl +} + +const ( + defaultCAID = "local" + defaultCAName = "Caddy Local Authority" + defaultRootCommonName = "{pki.ca.name} - {time.now.year} ECC Root" + defaultIntermediateCommonName = "{pki.ca.name} - ECC Intermediate" + + defaultRootLifetime = 24 * time.Hour * 30 * 12 * 10 + defaultIntermediateLifetime = 24 * time.Hour * 7 +) diff --git a/modules/caddypki/certificates.go b/modules/caddypki/certificates.go new file mode 100644 index 00000000000..a55c16581be --- /dev/null +++ b/modules/caddypki/certificates.go @@ -0,0 +1,50 @@ +// 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 caddypki + +import ( + "crypto/x509" + "time" + + "github.com/smallstep/cli/crypto/x509util" +) + +func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey interface{}, err error) { + rootProfile, err := x509util.NewRootProfile(commonName) + if err != nil { + return + } + rootProfile.Subject().NotAfter = time.Now().Add(defaultRootLifetime) // TODO: make configurable + return newCert(rootProfile) +} + +func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey interface{}) (cert *x509.Certificate, privateKey interface{}, err error) { + interProfile, err := x509util.NewIntermediateProfile(commonName, rootCrt, rootKey) + if err != nil { + return + } + interProfile.Subject().NotAfter = time.Now().Add(defaultIntermediateLifetime) // TODO: make configurable + return newCert(interProfile) +} + +func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey interface{}, err error) { + certBytes, err := profile.CreateCertificate() + if err != nil { + return + } + privateKey = profile.SubjectPrivateKey() + cert, err = x509.ParseCertificate(certBytes) + return +} diff --git a/modules/caddypki/command.go b/modules/caddypki/command.go new file mode 100644 index 00000000000..9276fcb5453 --- /dev/null +++ b/modules/caddypki/command.go @@ -0,0 +1,89 @@ +// 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 caddypki + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/caddyserver/caddy/v2" + caddycmd "github.com/caddyserver/caddy/v2/cmd" + "github.com/smallstep/truststore" +) + +func init() { + caddycmd.RegisterCommand(caddycmd.Command{ + Name: "untrust", + Func: cmdUntrust, + Usage: "[--ca | --cert ]", + Short: "Untrusts a locally-trusted CA certificate", + Long: ` +Untrusts a root certificate from the local trust store(s). Intended +for development environments only. + +This command uninstalls trust; it does not necessarily delete the +root certificate from trust stores entirely. Thus, repeatedly +trusting and untrusting new certificates can fill up trust databases. + +This command does not delete or modify certificate files. + +Specify which certificate to untrust either by the ID of its CA with +the --ca flag, or the direct path to the certificate file with the +--cert flag. If the --ca flag is used, only the default storage paths +are assumed (i.e. using --ca flag with custom storage backends or file +paths will not work). + +If no flags are specified, --ca=local is assumed.`, + Flags: func() *flag.FlagSet { + fs := flag.NewFlagSet("untrust", flag.ExitOnError) + fs.String("ca", "", "The ID of the CA to untrust") + fs.String("cert", "", "The path to the CA certificate to untrust") + return fs + }(), + }) +} + +func cmdUntrust(fs caddycmd.Flags) (int, error) { + ca := fs.String("ca") + cert := fs.String("cert") + + if ca != "" && cert != "" { + return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments") + } + if ca == "" && cert == "" { + ca = defaultCAID + } + if ca != "" { + cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt") + } + + // sanity check, make sure cert file exists first + _, err := os.Stat(cert) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err) + } + + err = truststore.UninstallFile(cert, + truststore.WithDebug(), + truststore.WithFirefox(), + truststore.WithJava()) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + return caddy.ExitCodeSuccess, nil +} diff --git a/modules/caddypki/crypto.go b/modules/caddypki/crypto.go new file mode 100644 index 00000000000..e701c40d712 --- /dev/null +++ b/modules/caddypki/crypto.go @@ -0,0 +1,155 @@ +// 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 caddypki + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "strings" +) + +func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) { + pemBlock, remaining := pem.Decode(pemDER) + if pemBlock == nil { + return nil, fmt.Errorf("no PEM block found") + } + if len(remaining) > 0 { + return nil, fmt.Errorf("input contained more than a single PEM block") + } + if pemBlock.Type != "CERTIFICATE" { + return nil, fmt.Errorf("expected PEM block type to be CERTIFICATE, but got '%s'", pemBlock.Type) + } + return x509.ParseCertificate(pemBlock.Bytes) +} + +func pemEncodeCert(der []byte) ([]byte, error) { + return pemEncode("CERTIFICATE", der) +} + +// pemEncodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes. +// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported. +func pemEncodePrivateKey(key crypto.PrivateKey) ([]byte, error) { + var pemType string + var keyBytes []byte + switch key := key.(type) { + case *ecdsa.PrivateKey: + var err error + pemType = "EC" + keyBytes, err = x509.MarshalECPrivateKey(key) + if err != nil { + return nil, err + } + case *rsa.PrivateKey: + pemType = "RSA" + keyBytes = x509.MarshalPKCS1PrivateKey(key) + case *ed25519.PrivateKey: + var err error + pemType = "ED25519" + keyBytes, err = x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported key type: %T", key) + } + return pemEncode(pemType+" PRIVATE KEY", keyBytes) +} + +// pemDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes. +// Borrowed from Go standard library, to handle various private key and PEM block types. +// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308 +// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238) +// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported. +func pemDecodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) { + keyBlockDER, _ := pem.Decode(keyPEMBytes) + + if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") { + return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type) + } + + if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil { + return key, nil + } + + if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey: + return key, nil + default: + return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key) + } + } + + if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil { + return key, nil + } + + return nil, fmt.Errorf("unknown private key type") +} + +func pemEncode(blockType string, b []byte) ([]byte, error) { + var buf bytes.Buffer + err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: b}) + return buf.Bytes(), err +} + +func trusted(cert *x509.Certificate) bool { + chains, err := cert.Verify(x509.VerifyOptions{}) + return len(chains) > 0 && err == nil +} + +// KeyPair represents a public-private key pair, where the +// public key is also called a certificate. +type KeyPair struct { + Certificate string `json:"certificate,omitempty"` + PrivateKey string `json:"private_key,omitempty"` + Format string `json:"format,omitempty"` +} + +// Load loads the certificate and key. +func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) { + switch kp.Format { + case "", "pem_file": + certData, err := ioutil.ReadFile(kp.Certificate) + if err != nil { + return nil, nil, err + } + keyData, err := ioutil.ReadFile(kp.PrivateKey) + if err != nil { + return nil, nil, err + } + + cert, err := pemDecodeSingleCert(certData) + if err != nil { + return nil, nil, err + } + key, err := pemDecodePrivateKey(keyData) + if err != nil { + return nil, nil, err + } + + return cert, key, nil + + default: + return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format) + } +} diff --git a/modules/caddypki/maintain.go b/modules/caddypki/maintain.go new file mode 100644 index 00000000000..2fce0d920a4 --- /dev/null +++ b/modules/caddypki/maintain.go @@ -0,0 +1,99 @@ +// 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 caddypki + +import ( + "crypto/x509" + "fmt" + "time" + + "go.uber.org/zap" +) + +func (p *PKI) maintenance() { + ticker := time.NewTicker(10 * time.Minute) // TODO: make configurable + defer ticker.Stop() + + for { + select { + case <-ticker.C: + p.renewCerts() + case <-p.ctx.Done(): + return + } + } +} + +func (p *PKI) renewCerts() { + for _, ca := range p.CAs { + err := p.renewCertsForCA(ca) + if err != nil { + p.log.Error("renewing intermediate certificates", + zap.Error(err), + zap.String("ca", ca.id)) + } + } +} + +func (p *PKI) renewCertsForCA(ca *CA) error { + ca.mu.Lock() + defer ca.mu.Unlock() + + log := p.log.With(zap.String("ca", ca.id)) + + // only maintain the root if it's not manually provided in the config + if ca.Root == nil { + if needsRenewal(ca.root) { + // TODO: implement root renewal (use same key) + log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)", + zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), + ) + } + } + + // only maintain the intermediate if it's not manually provided in the config + if ca.Intermediate == nil { + if needsRenewal(ca.inter) { + log.Info("intermediate expires soon; renewing", + zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)), + ) + + rootCert, rootKey, err := ca.loadOrGenRoot() + if err != nil { + return fmt.Errorf("loading root key: %v", err) + } + interCert, interKey, err := ca.genIntermediate(rootCert, rootKey) + if err != nil { + return fmt.Errorf("generating new certificate: %v", err) + } + ca.inter, ca.interKey = interCert, interKey + + log.Info("renewed intermediate", + zap.Time("new_expiration", ca.inter.NotAfter), + ) + } + } + + return nil +} + +func needsRenewal(cert *x509.Certificate) bool { + lifetime := cert.NotAfter.Sub(cert.NotBefore) + renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio) + renewalWindowStart := cert.NotAfter.Add(-renewalWindow) + return time.Now().After(renewalWindowStart) +} + +const renewalWindowRatio = 0.2 // TODO: make configurable diff --git a/modules/caddypki/pki.go b/modules/caddypki/pki.go new file mode 100644 index 00000000000..1b10a8e4ecb --- /dev/null +++ b/modules/caddypki/pki.go @@ -0,0 +1,117 @@ +// 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 caddypki + +import ( + "fmt" + + "github.com/caddyserver/caddy/v2" + "github.com/smallstep/truststore" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(PKI{}) +} + +// PKI provides Public Key Infrastructure facilities for Caddy. +type PKI struct { + // The CAs to manage. Each CA is keyed by an ID that is used + // to uniquely identify it from other CAs. The default CA ID + // is "local". + CAs map[string]*CA `json:"certificate_authorities,omitempty"` + + ctx caddy.Context + log *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (PKI) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "pki", + New: func() caddy.Module { return new(PKI) }, + } +} + +// Provision sets up the configuration for the PKI app. +func (p *PKI) Provision(ctx caddy.Context) error { + p.ctx = ctx + p.log = ctx.Logger(p) + + // if this app is initialized at all, ensure there's + // at least a default CA that can be used + if len(p.CAs) == 0 { + p.CAs = map[string]*CA{defaultCAID: new(CA)} + } + + for caID, ca := range p.CAs { + err := ca.Provision(ctx, caID, p.log) + if err != nil { + return fmt.Errorf("provisioning CA '%s': %v", caID, err) + } + } + + return nil +} + +// Start starts the PKI app. +func (p *PKI) Start() error { + // install roots to trust store, if not disabled + for _, ca := range p.CAs { + if ca.InstallTrust != nil && !*ca.InstallTrust { + ca.log.Warn("root certificate trust store installation disabled; clients will show warnings without intervention", + zap.String("path", ca.rootCertPath)) + continue + } + + // avoid password prompt if already trusted + if trusted(ca.root) { + ca.log.Info("root certificate is already trusted by system", + zap.String("path", ca.rootCertPath)) + continue + } + + ca.log.Warn("trusting root certificate (you might be prompted for password)", + zap.String("path", ca.rootCertPath)) + + err := truststore.Install(ca.root, + truststore.WithDebug(), + truststore.WithFirefox(), + truststore.WithJava(), + ) + if err != nil { + return fmt.Errorf("adding root certificate to trust store: %v", err) + } + } + + // see if root/intermediates need renewal... + p.renewCerts() + + // ...and keep them renewed + go p.maintenance() + + return nil +} + +// Stop stops the PKI app. +func (p *PKI) Stop() error { + return nil +} + +// Interface guards +var ( + _ caddy.Provisioner = (*PKI)(nil) + _ caddy.App = (*PKI)(nil) +) diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 36fd76c006b..f108d721e3d 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -145,7 +145,7 @@ func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) { } // PreCheck implements the certmagic.PreChecker interface. -func (m *ACMEIssuer) PreCheck(names []string, interactive bool) (skip bool, err error) { +func (m *ACMEIssuer) PreCheck(names []string, interactive bool) error { return certmagic.NewACMEManager(m.magic, m.template).PreCheck(names, interactive) } @@ -200,8 +200,9 @@ type DNSProviderMaker interface { // Interface guards var ( + _ certmagic.PreChecker = (*ACMEIssuer)(nil) _ certmagic.Issuer = (*ACMEIssuer)(nil) _ certmagic.Revoker = (*ACMEIssuer)(nil) - _ certmagic.PreChecker = (*ACMEIssuer)(nil) + _ caddy.Provisioner = (*ACMEIssuer)(nil) _ ConfigSetter = (*ACMEIssuer)(nil) ) diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go new file mode 100644 index 00000000000..53a1d004366 --- /dev/null +++ b/modules/caddytls/internalissuer.go @@ -0,0 +1,199 @@ +// 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 ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddypki" + "github.com/caddyserver/certmagic" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/cli/crypto/x509util" +) + +func init() { + caddy.RegisterModule(InternalIssuer{}) +} + +// InternalIssuer is a certificate issuer that generates +// certificates internally using a locally-configured +// CA which can be customized using the `pki` app. +type InternalIssuer struct { + // The ID of the CA to use for signing. The default + // CA ID is "local". The CA can be configured with the + // `pki` app. + CA string `json:"ca,omitempty"` + + // The validity period of certificates. + Lifetime caddy.Duration `json:"lifetime,omitempty"` + + // If true, the root will be the issuer instead of + // the intermediate. This is NOT recommended and should + // only be used when devices/clients do not properly + // validate certificate chains. + SignWithRoot bool `json:"sign_with_root,omitempty"` + + ca *caddypki.CA +} + +// CaddyModule returns the Caddy module information. +func (InternalIssuer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.issuance.internal", + New: func() caddy.Module { return new(InternalIssuer) }, + } +} + +// Provision sets up the issuer. +func (li *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 = defaultInternalCAName + } + ca, ok := pkiApp.CAs[li.CA] + if !ok { + return fmt.Errorf("no certificate authority configured with id: %s", li.CA) + } + li.ca = ca + + // set any other default values + if li.Lifetime == 0 { + li.Lifetime = caddy.Duration(defaultInternalCertLifetime) + } + + return nil +} + +// IssuerKey returns the unique issuer key for the +// confgured CA endpoint. +func (li InternalIssuer) IssuerKey() string { + return li.ca.ID() +} + +// Issue issues a certificate to satisfy the CSR. +func (li InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + // prepare the signing authority + // TODO: eliminate placeholders / needless values + cfg := &authority.Config{ + Address: "placeholder_Address:1", + Root: []string{"placeholder_Root"}, + IntermediateCert: "placeholder_IntermediateCert", + IntermediateKey: "placeholder_IntermediateKey", + DNSNames: []string{"placeholder_DNSNames"}, + AuthorityConfig: &authority.AuthConfig{ + Provisioners: provisioner.List{}, + }, + } + + // get the root certificate and the issuer cert+key + rootCert := li.ca.RootCertificate() + var issuerCert *x509.Certificate + var issuerKey interface{} + if li.SignWithRoot { + issuerCert = rootCert + var err error + issuerKey, err = li.ca.RootKey() + if err != nil { + return nil, fmt.Errorf("loading signing key: %v", err) + } + } else { + issuerCert = li.ca.IntermediateCertificate() + issuerKey = li.ca.IntermediateKey() + } + + auth, err := authority.New(cfg, + authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)), + authority.WithX509RootCerts(rootCert), + ) + if err != nil { + return nil, fmt.Errorf("initializing certificate authority: %v", err) + } + + // ensure issued certificate does not expire later than its issuer + lifetime := time.Duration(li.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), + ) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + for _, cert := range certChain { + err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return &certmagic.IssuedCertificate{ + Certificate: buf.Bytes(), + }, nil +} + +// TODO: borrowing from https://github.com/smallstep/certificates/blob/806abb6232a5691198b891d76b9898ea7f269da0/authority/provisioner/sign_options.go#L191-L211 +// as per https://github.com/smallstep/certificates/issues/198. +// profileDefaultDuration is a wrapper against x509util.WithOption to conform +// the SignOption interface. +type profileDefaultDuration time.Duration + +// TODO: is there a better way to set cert lifetimes than copying from the smallstep libs? +func (d profileDefaultDuration) Option(so provisioner.Options) x509util.WithOption { + var backdate time.Duration + notBefore := so.NotBefore.Time() + if notBefore.IsZero() { + notBefore = time.Now().Truncate(time.Second) + backdate = -1 * so.Backdate + } + notAfter := so.NotAfter.RelativeTime(notBefore) + return func(p x509util.Profile) error { + fn := x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(d)) + if err := fn(p); err != nil { + return err + } + crt := p.Subject() + crt.NotBefore = crt.NotBefore.Add(backdate) + return nil + } +} + +const ( + defaultInternalCAName = "local" + defaultInternalCertLifetime = 12 * time.Hour +) + +// Interface guards +var ( + _ caddy.Provisioner = (*InternalIssuer)(nil) + _ certmagic.Issuer = (*InternalIssuer)(nil) +) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 4fa126e3432..f91229f4c88 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -175,6 +175,26 @@ func (t *TLS) Provision(ctx caddy.Context) error { return nil } +// Validate validates t's configuration. +func (t *TLS) Validate() error { + if t.Automation != nil { + // ensure that host aren't repeated; since only the first + // automation policy is used, repeating a host in the lists + // isn't useful and is probably a mistake + // TODO: test this + hostSet := make(map[string]int) + for i, ap := range t.Automation.Policies { + for _, h := range ap.Hosts { + if first, ok := hostSet[h]; ok { + return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first) + } + hostSet[h] = i + } + } + } + return nil +} + // Start activates the TLS module. func (t *TLS) Start() error { // now that we are running, and all manual certificates have @@ -266,7 +286,10 @@ func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { } // AddAutomationPolicy provisions and adds ap to the list of the app's -// automation policies. +// automation policies. If an existing automation policy exists that has +// fewer hosts in its list than ap does, ap will be inserted before that +// other policy (this helps ensure that ap will be prioritized/chosen +// over, say, a catch-all policy). func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { if t.Automation == nil { t.Automation = new(AutomationConfig) @@ -275,6 +298,16 @@ func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { if err != nil { return err } + for i, other := range t.Automation.Policies { + // if a catch-all policy (or really, any policy with + // fewer names) exists, prioritize this new policy + if len(other.Hosts) < len(ap.Hosts) { + t.Automation.Policies = append(t.Automation.Policies[:i], + append([]*AutomationPolicy{ap}, t.Automation.Policies[i+1:]...)...) + return nil + } + } + // otherwise just append the new one t.Automation.Policies = append(t.Automation.Policies, ap) return nil } @@ -444,6 +477,7 @@ type AutomationPolicy struct { // obtaining or renewing certificates. This is often // not desirable, especially when serving sites out // of your control. Default: false + // TODO: is this really necessary per-policy? why not a global setting... ManageSync bool `json:"manage_sync,omitempty"` Issuer certmagic.Issuer `json:"-"` @@ -510,8 +544,7 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error { OnDemand: ond, Storage: storage, } - cfg := certmagic.New(tlsApp.certCache, template) - ap.magic = cfg + ap.magic = certmagic.New(tlsApp.certCache, template) if ap.IssuerRaw != nil { val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw") @@ -527,12 +560,12 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error { // ACME challenges -- it's an annoying, inelegant circular // dependency that I don't know how to resolve nicely!) if configger, ok := ap.Issuer.(ConfigSetter); ok { - configger.SetConfig(cfg) + configger.SetConfig(ap.magic) } - cfg.Issuer = ap.Issuer + ap.magic.Issuer = ap.Issuer if rev, ok := ap.Issuer.(certmagic.Revoker); ok { - cfg.Revoker = rev + ap.magic.Revoker = rev } return nil @@ -789,3 +822,10 @@ func (t *TLS) moveCertificates() error { return nil } + +// Interface guards +var ( + _ caddy.Provisioner = (*TLS)(nil) + _ caddy.Validator = (*TLS)(nil) + _ caddy.App = (*TLS)(nil) +) diff --git a/modules/standard/import.go b/modules/standard/import.go index 5ecfb4ac401..a88200fb20b 100644 --- a/modules/standard/import.go +++ b/modules/standard/import.go @@ -6,6 +6,7 @@ import ( _ "github.com/caddyserver/caddy/v2/caddyconfig/json5" _ "github.com/caddyserver/caddy/v2/caddyconfig/jsonc" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" + _ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddytls" _ "github.com/caddyserver/caddy/v2/modules/caddytls/distributedstek" _ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek" diff --git a/replacer.go b/replacer.go index d1c58e8decc..4ff578cc69a 100644 --- a/replacer.go +++ b/replacer.go @@ -19,6 +19,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "time" ) @@ -236,6 +237,8 @@ func globalDefaultReplacements(key string) (string, bool) { return runtime.GOARCH, true case "time.now.common_log": return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true + case "time.now.year": + return strconv.Itoa(nowFunc().Year()), true } return "", false