summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2020-03-13 11:06:08 -0600
committerGitHub <noreply@github.com>2020-03-13 11:06:08 -0600
commit5a19db5dc2db7c02d0f99630a07a64cacb7f7b44 (patch)
treed820ee2920d97d7cf2faf0fd9541156e20c88d60
parentcfe85a9fe625fea55dc4f809fd91b5c061064508 (diff)
v2: Implement 'pki' app powered by Smallstep for localhost certificates (#3125)
* pki: Initial commit of PKI app (WIP) (see #2502 and #3021) * pki: Ability to use root/intermediates, and sign with root * pki: Fix benign misnamings left over from copy+paste * pki: Only install root if not already trusted * Make HTTPS port the default; all names use auto-HTTPS; bug fixes * Fix build - what happened to our CI tests?? * Fix go.mod
-rw-r--r--caddyconfig/httpcaddyfile/addresses.go25
-rw-r--r--caddyconfig/httpcaddyfile/addresses_test.go12
-rw-r--r--caddyconfig/httpcaddyfile/builtins.go81
-rw-r--r--caddyconfig/httpcaddyfile/httptype.go36
-rw-r--r--go.mod8
-rw-r--r--go.sum16
-rw-r--r--modules/caddyhttp/autohttps.go160
-rw-r--r--modules/caddyhttp/caddyhttp.go10
-rw-r--r--modules/caddyhttp/fileserver/command.go7
-rw-r--r--modules/caddyhttp/reverseproxy/command.go8
-rw-r--r--modules/caddypki/ca.go334
-rw-r--r--modules/caddypki/certificates.go50
-rw-r--r--modules/caddypki/command.go89
-rw-r--r--modules/caddypki/crypto.go155
-rw-r--r--modules/caddypki/maintain.go99
-rw-r--r--modules/caddypki/pki.go117
-rw-r--r--modules/caddytls/acmeissuer.go5
-rw-r--r--modules/caddytls/internalissuer.go199
-rw-r--r--modules/caddytls/tls.go52
-rw-r--r--modules/standard/import.go1
-rw-r--r--replacer.go3
21 files changed, 1292 insertions, 175 deletions
diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go
index 64c5d4f..2d17833 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 e22535c..8de1f09 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 3b5a4f5..91c1c0a 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 [<email>]|[<cert_file> <key_file>] {
+// tls [<email>|internal]|[<cert_file> <key_file>] {
// protocols <min> [<max>]
// ciphers <cipher_suites...>
// curves <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 d880d97..96f2bb0 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 1edcd2b..8e00de9 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 f19bdb1..0a685cd 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 7dab359..6a23ca0 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 6ad70f5..06719b5 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 fa6560b..18e9be3 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 6f70d14..6110ca8 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 0000000..f15883e
--- /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 0000000..a55c165
--- /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 0000000..9276fcb
--- /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 <id> | --cert <path>]",
+ 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 0000000..e701c40
--- /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 0000000..2fce0d9
--- /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 0000000..1b10a8e
--- /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 36fd76c..f108d72 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 0000000..53a1d00
--- /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 4fa126e..f91229f 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 5ecfb4a..a88200f 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 d1c58e8..4ff578c 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