Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
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
Matt Holt committed
Mar 13, 2020 at 11:06 UTC
5a19db5dc2db7c02d0f99630a07a64cacb7f7b44
21 files changed
+1292
-175
caddyconfig/httpcaddyfile/addresses.go
+4
-21
@@ -172,20 +172,14 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
172
httpsPort = strconv.Itoa(hsport.(int))
173
}
174
175
- lnPort := DefaultPort
175
+ // default port is the HTTPS port
176
+ lnPort := httpsPort
177
if addr.Port != "" {
178
// port explicitly defined
179
lnPort = addr.Port
179
- } else if addr.Scheme != "" {
180
+ } else if addr.Scheme == "http" {
181
// port inferred from scheme
181
- if addr.Scheme == "http" {
182
- lnPort = httpPort
183
- } else if addr.Scheme == "https" {
184
- lnPort = httpsPort
185
- }
186
- } else if certmagic.HostQualifies(addr.Host) {
187
- // automatic HTTPS
188
- lnPort = httpsPort
182
+ lnPort = httpPort
183
}
184
185
// error if scheme and port combination violate convention
@@ -213,7 +207,6 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
207
for lnStr := range listeners {
208
listenersList = append(listenersList, lnStr)
209
}
216
- // sort.Strings(listenersList) // TODO: is sorting necessary?
210
211
return listenersList, nil
212
}
@@ -317,9 +310,6 @@ func (a Address) String() string {
310
// Normalize returns a normalized version of a.
311
func (a Address) Normalize() Address {
312
path := a.Path
320
- if !caseSensitivePath {
321
- path = strings.ToLower(path)
322
- }
313
314
// ensure host is normalized if it's an IP address
315
host := a.Host
@@ -357,10 +347,3 @@ func (a Address) Key() string {
347
}
348
return res
349
}
360
-
361
-const (
362
- // DefaultPort is the default port to use.
363
- DefaultPort = "2015"
364
-
365
- caseSensitivePath = false // TODO: Used?
366
-)
caddyconfig/httpcaddyfile/addresses_test.go
+2
-10
@@ -1,7 +1,6 @@
1
package httpcaddyfile
2
3
import (
4
- "strings"
4
"testing"
5
)
6
@@ -156,15 +155,8 @@ func TestKeyNormalization(t *testing.T) {
155
t.Errorf("Test %d: Parsing address '%s': %v", i, tc.input, err)
156
continue
157
}
159
- expect := tc.expect
160
- if !caseSensitivePath {
161
- // every other part of the address should be lowercased when normalized,
162
- // so simply lower-case the whole thing to do case-insensitive comparison
163
- // of the path as well
164
- expect = strings.ToLower(expect)
165
- }
166
- if actual := addr.Normalize().Key(); actual != expect {
167
- t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, expect)
158
+ if actual := addr.Normalize().Key(); actual != tc.expect {
159
+ t.Errorf("Test %d: Normalized key for address '%s' was '%s' but expected '%s'", i, tc.input, actual, tc.expect)
160
}
161
162
}
caddyconfig/httpcaddyfile/builtins.go
+54
-27
@@ -95,7 +95,7 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
95
96
// parseTLS parses the tls directive. Syntax:
97
//
98
-// tls [<email>]|[<cert_file> <key_file>] {
98
+// tls [<email>|internal]|[<cert_file> <key_file>] {
99
// protocols <min> [<max>]
100
// ciphers <cipher_suites...>
101
// curves <curves...>
@@ -106,23 +106,11 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
106
// }
107
//
108
func parseTLS(h Helper) ([]ConfigValue, error) {
109
- var configVals []ConfigValue
110
-
109
var cp *caddytls.ConnectionPolicy
110
var fileLoader caddytls.FileLoader
111
var folderLoader caddytls.FolderLoader
114
- var mgr caddytls.ACMEIssuer
115
-
116
- // fill in global defaults, if configured
117
- if email := h.Option("email"); email != nil {
118
- mgr.Email = email.(string)
119
- }
120
- if acmeCA := h.Option("acme_ca"); acmeCA != nil {
121
- mgr.CA = acmeCA.(string)
122
- }
123
- if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
124
- mgr.TrustedRootsPEMFiles = append(mgr.TrustedRootsPEMFiles, caPemFile.(string))
125
- }
112
+ var acmeIssuer *caddytls.ACMEIssuer
113
+ var internalIssuer *caddytls.InternalIssuer
114
115
for h.Next() {
116
// file certificate loader
@@ -130,10 +118,17 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
118
switch len(firstLine) {
119
case 0:
120
case 1:
133
- if !strings.Contains(firstLine[0], "@") {
134
- return nil, h.Err("single argument must be an email address")
121
+ if firstLine[0] == "internal" {
122
+ internalIssuer = new(caddytls.InternalIssuer)
123
+ } else if !strings.Contains(firstLine[0], "@") {
124
+ return nil, h.Err("single argument must either be 'internal' or an email address")
125
+ } else {
126
+ if acmeIssuer == nil {
127
+ acmeIssuer = new(caddytls.ACMEIssuer)
128
+ }
129
+ acmeIssuer.Email = firstLine[0]
130
}
136
- mgr.Email = firstLine[0]
131
+
132
case 2:
133
certFilename := firstLine[0]
134
keyFilename := firstLine[1]
@@ -143,7 +138,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
138
// https://github.com/caddyserver/caddy/issues/2588 ... but we
139
// must be careful about how we do this; being careless will
140
// lead to failed handshakes
146
-
141
+ //
142
// we need to remember which cert files we've seen, since we
143
// must load each cert only once; otherwise, they each get a
144
// different tag... since a cert loaded twice has the same
@@ -152,7 +147,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
147
// policy that is looking for any tag but the last one to be
148
// loaded won't find it, and TLS handshakes will fail (see end)
149
// of issue #3004)
155
-
150
+ //
151
// tlsCertTags maps certificate filenames to their tag.
152
// This is used to remember which tag is used for each
153
// certificate files, since we need to avoid loading
@@ -256,29 +251,38 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
251
if len(arg) != 1 {
252
return nil, h.ArgErr()
253
}
259
- mgr.CA = arg[0]
254
+ if acmeIssuer == nil {
255
+ acmeIssuer = new(caddytls.ACMEIssuer)
256
+ }
257
+ acmeIssuer.CA = arg[0]
258
259
// DNS provider for ACME DNS challenge
260
case "dns":
261
if !h.Next() {
262
return nil, h.ArgErr()
263
}
264
+ if acmeIssuer == nil {
265
+ acmeIssuer = new(caddytls.ACMEIssuer)
266
+ }
267
provName := h.Val()
267
- if mgr.Challenges == nil {
268
- mgr.Challenges = new(caddytls.ChallengesConfig)
268
+ if acmeIssuer.Challenges == nil {
269
+ acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
270
}
271
dnsProvModule, err := caddy.GetModule("tls.dns." + provName)
272
if err != nil {
273
return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
274
}
274
- mgr.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, h.warnings)
275
+ acmeIssuer.Challenges.DNSRaw = caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, h.warnings)
276
277
case "ca_root":
278
arg := h.RemainingArgs()
279
if len(arg) != 1 {
280
return nil, h.ArgErr()
281
}
281
- mgr.TrustedRootsPEMFiles = append(mgr.TrustedRootsPEMFiles, arg[0])
282
+ if acmeIssuer == nil {
283
+ acmeIssuer = new(caddytls.ACMEIssuer)
284
+ }
285
+ acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0])
286
287
default:
288
return nil, h.Errf("unknown subdirective: %s", h.Val())
@@ -291,6 +295,9 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
295
}
296
}
297
298
+ // begin building the final config values
299
+ var configVals []ConfigValue
300
+
301
// certificate loaders
302
if len(fileLoader) > 0 {
303
configVals = append(configVals, ConfigValue{
@@ -322,10 +329,30 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
329
}
330
331
// automation policy
325
- if !reflect.DeepEqual(mgr, caddytls.ACMEIssuer{}) {
332
+ if acmeIssuer != nil && internalIssuer != nil {
333
+ // the logic to support this would be complex
334
+ return nil, h.Err("cannot use both ACME and internal issuers in same server block")
335
+ }
336
+ if acmeIssuer != nil {
337
+ // fill in global defaults, if configured
338
+ if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
339
+ acmeIssuer.Email = email.(string)
340
+ }
341
+ if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
342
+ acmeIssuer.CA = acmeCA.(string)
343
+ }
344
+ if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
345
+ acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
346
+ }
347
+
348
+ configVals = append(configVals, ConfigValue{
349
+ Class: "tls.cert_issuer",
350
+ Value: acmeIssuer,
351
+ })
352
+ } else if internalIssuer != nil {
353
configVals = append(configVals, ConfigValue{
354
Class: "tls.cert_issuer",
328
- Value: mgr,
355
+ Value: internalIssuer,
356
})
357
}
358
caddyconfig/httpcaddyfile/httptype.go
+9
-27
@@ -185,10 +185,10 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
185
for _, p := range pairings {
186
for i, sblock := range p.serverBlocks {
187
// tls automation policies
188
- if mmVals, ok := sblock.pile["tls.cert_issuer"]; ok {
189
- for _, mmVal := range mmVals {
190
- mm := mmVal.Value.(certmagic.Issuer)
191
- sblockHosts, err := st.autoHTTPSHosts(sblock)
188
+ if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
189
+ for _, issuerVal := range issuerVals {
190
+ issuer := issuerVal.Value.(certmagic.Issuer)
191
+ sblockHosts, err := st.hostsFromServerBlockKeys(sblock.block)
192
if err != nil {
193
return nil, warnings, err
194
}
@@ -198,7 +198,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
198
}
199
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{
200
Hosts: sblockHosts,
201
- IssuerRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID.Name(), &warnings),
201
+ IssuerRaw: caddyconfig.JSONModuleObject(issuer, "module", issuer.(caddy.Module).CaddyModule().ID.Name(), &warnings),
202
})
203
} else {
204
warnings = append(warnings, caddyconfig.Warning{
@@ -500,16 +500,13 @@ func (st *ServerType) serversFromPairings(
500
501
// tls: connection policies and toggle auto HTTPS
502
defaultSNI := tryString(options["default_sni"], warnings)
503
- autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock)
504
- if err != nil {
505
- return nil, err
506
- }
507
- if _, ok := sblock.pile["tls.off"]; ok && len(autoHTTPSQualifiedHosts) > 0 {
503
+ if _, ok := sblock.pile["tls.off"]; ok {
504
+ // TODO: right now, no directives yield any tls.off value...
505
// tls off: disable TLS (and automatic HTTPS) for server block's names
506
if srv.AutoHTTPS == nil {
507
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
508
}
512
- srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...)
509
+ srv.AutoHTTPS.Disabled = true
510
} else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
511
// tls connection policies
512
@@ -741,25 +738,10 @@ func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subro
738
return subroute, nil
739
}
740
744
-func (st ServerType) autoHTTPSHosts(sb serverBlock) ([]string, error) {
745
- // get the hosts for this server block...
746
- hosts, err := st.hostsFromServerBlockKeys(sb.block)
747
- if err != nil {
748
- return nil, err
749
- }
750
- // ...and of those, which ones qualify for auto HTTPS
751
- var autoHTTPSQualifiedHosts []string
752
- for _, h := range hosts {
753
- if certmagic.HostQualifies(h) {
754
- autoHTTPSQualifiedHosts = append(autoHTTPSQualifiedHosts, h)
755
- }
756
- }
757
- return autoHTTPSQualifiedHosts, nil
758
-}
759
-
741
// consolidateRoutes combines routes with the same properties
742
// (same matchers, same Terminal and Group settings) for a
743
// cleaner overall output.
744
+// FIXME: See caddyserver/caddy#3108
745
func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList {
746
for i := 0; i < len(routes)-1; i++ {
747
if reflect.DeepEqual(routes[i].MatcherSetsRaw, routes[i+1].MatcherSetsRaw) &&
go.mod
+4
-4
@@ -4,9 +4,9 @@ go 1.14
4
5
require (
6
github.com/Masterminds/sprig/v3 v3.0.2
7
- github.com/alecthomas/chroma v0.7.1
7
+ github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a
8
github.com/andybalholm/brotli v1.0.0
9
- github.com/caddyserver/certmagic v0.10.0
9
+ github.com/caddyserver/certmagic v0.10.1
10
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
11
github.com/go-acme/lego/v3 v3.4.0
12
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
@@ -24,8 +24,8 @@ require (
24
github.com/smallstep/cli v0.14.0-rc.3
25
github.com/smallstep/truststore v0.9.4
26
github.com/vulcand/oxy v1.0.0
27
- github.com/yuin/goldmark v1.1.24
28
- github.com/yuin/goldmark-highlighting v0.0.0-20200218065240-d1af22c1126f
27
+ github.com/yuin/goldmark v1.1.25
28
+ github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
29
go.uber.org/zap v1.14.0
30
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
31
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
go.sum
+8
-8
@@ -72,8 +72,8 @@ github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75 h1:3ILj
72
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
73
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
74
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
75
-github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ=
76
-github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc=
75
+github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a h1:3v1NrYWWqp2S72e4HLgxKt83B3l0lnORDholH/ihoMM=
76
+github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
77
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
78
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
79
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
108
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
109
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
110
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
111
-github.com/caddyserver/certmagic v0.10.0 h1:kbQsqN5RmdUMClVUNd8svTzemCo8W6NNc8UJOXnUIu0=
112
-github.com/caddyserver/certmagic v0.10.0/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ=
111
+github.com/caddyserver/certmagic v0.10.1 h1:k9E+C4b8WM3sTs3PSfmWIAwxtO9cXtr0bDHX2Bc0RIM=
112
+github.com/caddyserver/certmagic v0.10.1/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ=
113
github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU=
114
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
115
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
730
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
731
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
732
github.com/yuin/goldmark v1.1.22/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
733
-github.com/yuin/goldmark v1.1.24 h1:K4FemPDr4x/ZcqldoXWnexTLfdMIy2eEfXxsLnotTRI=
734
-github.com/yuin/goldmark v1.1.24/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
735
-github.com/yuin/goldmark-highlighting v0.0.0-20200218065240-d1af22c1126f h1:5295skDVJn90SXIYI22jOMeR9XbnuN76y/V1m9N8ITQ=
736
-github.com/yuin/goldmark-highlighting v0.0.0-20200218065240-d1af22c1126f/go.mod h1:9yW2CHuRSORvHgw7YfybB09PqUZTbzERyW3QFvd8+0Q=
733
+github.com/yuin/goldmark v1.1.25 h1:isv+Q6HQAmmL2Ofcmg8QauBmDPlUUnSoNhEcC940Rds=
734
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
735
+github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 h1:VWSxtAiQNh3zgHJpdpkpVYjTPqRE3P6UZCOPa1nRDio=
736
+github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691/go.mod h1:YLF3kDffRfUH/bTxOxHhV6lxwIB3Vfj91rEwNMS9MXo=
737
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
738
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
739
go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
modules/caddyhttp/autohttps.go
+104
-56
@@ -4,6 +4,7 @@ import (
4
"fmt"
5
"net/http"
6
"strconv"
7
+ "strings"
8
9
"github.com/caddyserver/caddy/v2"
10
"github.com/caddyserver/caddy/v2/modules/caddytls"
@@ -130,8 +131,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
131
return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
132
srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
133
}
133
- if certmagic.HostQualifies(d) &&
134
- !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
134
+ if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
135
serverDomainSet[d] = struct{}{}
136
}
137
}
@@ -161,6 +161,15 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
161
)
162
continue
163
}
164
+
165
+ // most clients don't accept wildcards like *.tld... we
166
+ // can handle that, but as a courtesy, warn the user
167
+ if strings.Contains(d, "*") &&
168
+ strings.Count(strings.Trim(d, "."), ".") == 1 {
169
+ app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)",
170
+ zap.String("domain", d))
171
+ }
172
+
173
uniqueDomainsForCerts[d] = struct{}{}
174
}
175
}
@@ -202,12 +211,18 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
211
// we now have a list of all the unique names for which we need certs;
212
// turn the set into a slice so that phase 2 can use it
213
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
214
+ var internal, external []string
215
for d := range uniqueDomainsForCerts {
216
+ if certmagic.SubjectQualifiesForPublicCert(d) {
217
+ external = append(external, d)
218
+ } else {
219
+ internal = append(internal, d)
220
+ }
221
app.allCertDomains = append(app.allCertDomains, d)
222
}
223
224
// ensure there is an automation policy to handle these certs
210
- err := app.createAutomationPolicy(ctx)
225
+ err := app.createAutomationPolicies(ctx, external, internal)
226
if err != nil {
227
return err
228
}
@@ -354,23 +369,29 @@ redirServersLoop:
369
return nil
370
}
371
357
-// createAutomationPolicy ensures that certificates for this app are
358
-// managed properly; for example, it's implied that the HTTPPort
359
-// should also be the port the HTTP challenge is solved on; the same
360
-// for HTTPS port and TLS-ALPN challenge also. We need to tell the
361
-// TLS app to manage these certs by honoring those port configurations,
362
-// so we either find an existing matching automation policy with an
363
-// ACME issuer, or make a new one and append it.
364
-func (app *App) createAutomationPolicy(ctx caddy.Context) error {
372
+// createAutomationPolicy ensures that automated certificates for this
373
+// app are managed properly. This adds up to two automation policies:
374
+// one for the public names, and one for the internal names. If a catch-all
375
+// automation policy exists, it will be shallow-copied and used as the
376
+// base for the new ones (this is important for preserving behavior the
377
+// user intends to be "defaults").
378
+func (app *App) createAutomationPolicies(ctx caddy.Context, publicNames, internalNames []string) error {
379
+ // nothing to do if no names to manage certs for
380
+ if len(publicNames) == 0 && len(internalNames) == 0 {
381
+ return nil
382
+ }
383
+
384
+ // start by finding a base policy that the user may have defined
385
+ // which should, in theory, apply to any policies derived from it;
386
+ // typically this would be a "catch-all" policy with no host filter
387
var matchingPolicy *caddytls.AutomationPolicy
366
- var acmeIssuer *caddytls.ACMEIssuer
388
if app.tlsApp.Automation != nil {
368
- // maybe we can find an exisitng one that matches; this is
369
- // useful if the user made a single automation policy to
370
- // set the CA endpoint to a test/staging endpoint (very
371
- // common), but forgot to customize the ports here, while
372
- // setting them in the HTTP app instead (I did this too
373
- // many times)
389
+ // if an existing policy matches (specifically, a catch-all policy),
390
+ // we should inherit from it, because that is what the user expects;
391
+ // this is very common for user setting a default issuer, with a
392
+ // custom CA endpoint, for example - whichever one we choose must
393
+ // have a host list that is a superset of the policy we make...
394
+ // the policy with no host filter is guaranteed to qualify
395
for _, ap := range app.tlsApp.Automation.Policies {
396
if len(ap.Hosts) == 0 {
397
matchingPolicy = ap
@@ -378,51 +399,78 @@ func (app *App) createAutomationPolicy(ctx caddy.Context) error {
399
}
400
}
401
}
381
- if matchingPolicy != nil {
382
- // if it has an ACME issuer, maybe we can just use that
383
- acmeIssuer, _ = matchingPolicy.Issuer.(*caddytls.ACMEIssuer)
384
- }
385
- if acmeIssuer == nil {
386
- acmeIssuer = new(caddytls.ACMEIssuer)
387
- }
388
- if acmeIssuer.Challenges == nil {
389
- acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
390
- }
391
- if acmeIssuer.Challenges.HTTP == nil {
392
- acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
393
- }
394
- if acmeIssuer.Challenges.HTTP.AlternatePort == 0 {
395
- // don't overwrite existing explicit config
396
- acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort
397
- }
398
- if acmeIssuer.Challenges.TLSALPN == nil {
399
- acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
402
+ if matchingPolicy == nil {
403
+ matchingPolicy = new(caddytls.AutomationPolicy)
404
}
401
- if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 {
402
- // don't overwrite existing explicit config
403
- acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort
405
+
406
+ // addPolicy adds an automation policy that uses issuer for hosts.
407
+ addPolicy := func(issuer certmagic.Issuer, hosts []string) error {
408
+ // shallow-copy the matching policy; we want to inherit
409
+ // from it, not replace it... this takes two lines to
410
+ // overrule compiler optimizations
411
+ policyCopy := *matchingPolicy
412
+ newPolicy := &policyCopy
413
+
414
+ // very important to provision it, since we are
415
+ // bypassing the JSON-unmarshaling step
416
+ if prov, ok := issuer.(caddy.Provisioner); ok {
417
+ err := prov.Provision(ctx)
418
+ if err != nil {
419
+ return err
420
+ }
421
+ }
422
+ newPolicy.Issuer = issuer
423
+ newPolicy.Hosts = hosts
424
+
425
+ return app.tlsApp.AddAutomationPolicy(newPolicy)
426
}
427
406
- if matchingPolicy == nil {
407
- // if there was no matching policy, we'll have to append our own
408
- err := app.tlsApp.AddAutomationPolicy(&caddytls.AutomationPolicy{
409
- Hosts: app.allCertDomains,
410
- Issuer: acmeIssuer,
411
- })
412
- if err != nil {
428
+ if len(publicNames) > 0 {
429
+ var acmeIssuer *caddytls.ACMEIssuer
430
+ // if it has an ACME issuer, maybe we can just use that
431
+ // TODO: we might need a deep copy here, like a Clone() method on ACMEIssuer...
432
+ acmeIssuer, _ = matchingPolicy.Issuer.(*caddytls.ACMEIssuer)
433
+ if acmeIssuer == nil {
434
+ acmeIssuer = new(caddytls.ACMEIssuer)
435
+ }
436
+ if app.HTTPPort > 0 || app.HTTPSPort > 0 {
437
+ if acmeIssuer.Challenges == nil {
438
+ acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
439
+ }
440
+ }
441
+ if app.HTTPPort > 0 {
442
+ if acmeIssuer.Challenges.HTTP == nil {
443
+ acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
444
+ }
445
+ // don't overwrite existing explicit config
446
+ if acmeIssuer.Challenges.HTTP.AlternatePort == 0 {
447
+ acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort
448
+ }
449
+ }
450
+ if app.HTTPSPort > 0 {
451
+ if acmeIssuer.Challenges.TLSALPN == nil {
452
+ acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
453
+ }
454
+ // don't overwrite existing explicit config
455
+ if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 {
456
+ acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort
457
+ }
458
+ }
459
+ if err := addPolicy(acmeIssuer, publicNames); err != nil {
460
return err
461
}
415
- } else {
416
- // if there was an existing matching policy, we need to reprovision
417
- // its issuer (because we just changed its port settings and it has
418
- // to re-build its stored certmagic config template with the new
419
- // values), then re-assign the Issuer pointer on the policy struct
420
- // because our type assertion changed the address
421
- err := acmeIssuer.Provision(ctx)
422
- if err != nil {
462
+ }
463
+
464
+ if len(internalNames) > 0 {
465
+ internalIssuer := new(caddytls.InternalIssuer)
466
+ if err := addPolicy(internalIssuer, internalNames); err != nil {
467
return err
468
}
425
- matchingPolicy.Issuer = acmeIssuer
469
+ }
470
+
471
+ err := app.tlsApp.Validate()
472
+ if err != nil {
473
+ return err
474
}
475
476
return nil
modules/caddyhttp/caddyhttp.go
+8
-2
@@ -286,8 +286,8 @@ func (app *App) Start() error {
286
}
287
288
// enable TLS if there is a policy and if this is not the HTTP port
289
- if len(srv.TLSConnPolicies) > 0 &&
290
- int(listenAddr.StartPort+portOffset) != app.httpPort() {
289
+ useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
290
+ if useTLS {
291
// create TLS listener
292
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
293
ln = tls.NewListener(ln, tlsCfg)
@@ -317,6 +317,12 @@ func (app *App) Start() error {
317
/////////
318
}
319
320
+ app.logger.Debug("starting server loop",
321
+ zap.String("address", lnAddr),
322
+ zap.Bool("http3", srv.ExperimentalHTTP3),
323
+ zap.Bool("tls", useTLS),
324
+ )
325
+
326
go s.Serve(ln)
327
app.servers = append(app.servers, s)
328
}
modules/caddyhttp/fileserver/command.go
+1
-6
@@ -23,7 +23,6 @@ import (
23
24
"github.com/caddyserver/caddy/v2"
25
"github.com/caddyserver/caddy/v2/caddyconfig"
26
- "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
26
caddycmd "github.com/caddyserver/caddy/v2/cmd"
27
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
28
"github.com/caddyserver/certmagic"
@@ -90,11 +89,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
89
Routes: caddyhttp.RouteList{route},
90
}
91
if listen == "" {
93
- if certmagic.HostQualifies(domain) {
94
- listen = ":" + strconv.Itoa(certmagic.HTTPSPort)
95
- } else {
96
- listen = ":" + httpcaddyfile.DefaultPort
97
- }
92
+ listen = ":" + strconv.Itoa(certmagic.HTTPSPort)
93
}
94
server.Listen = []string{listen}
95
modules/caddyhttp/reverseproxy/command.go
+2
-6
@@ -25,11 +25,9 @@ import (
25
26
"github.com/caddyserver/caddy/v2"
27
"github.com/caddyserver/caddy/v2/caddyconfig"
28
- "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
28
caddycmd "github.com/caddyserver/caddy/v2/cmd"
29
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
30
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
32
- "github.com/caddyserver/certmagic"
31
)
32
33
func init() {
@@ -67,7 +65,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
65
changeHost := fs.Bool("change-host-header")
66
67
if from == "" {
70
- from = "localhost:" + httpcaddyfile.DefaultPort
68
+ from = "localhost:443"
69
}
70
71
// URLs need a scheme in order to parse successfully
@@ -129,11 +127,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
127
}
128
}
129
132
- listen := ":80"
130
+ listen := ":443"
131
if urlPort := fromURL.Port(); urlPort != "" {
132
listen = ":" + urlPort
135
- } else if certmagic.HostQualifies(urlHost) {
136
- listen = ":443"
133
}
134
135
server := &caddyhttp.Server{
modules/caddypki/ca.go
new
+334
@@ -0,0 +1,334 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddypki
16
+
17
+import (
18
+ "crypto/x509"
19
+ "encoding/json"
20
+ "fmt"
21
+ "path"
22
+ "sync"
23
+ "time"
24
+
25
+ "github.com/caddyserver/caddy/v2"
26
+ "github.com/caddyserver/certmagic"
27
+ "go.uber.org/zap"
28
+)
29
+
30
+// CA describes a certificate authority, which consists of
31
+// root/signing certificates and various settings pertaining
32
+// to the issuance of certificates and trusting them.
33
+type CA struct {
34
+ // The user-facing name of the certificate authority.
35
+ Name string `json:"name,omitempty"`
36
+
37
+ // The name to put in the CommonName field of the
38
+ // root certificate.
39
+ RootCommonName string `json:"root_common_name,omitempty"`
40
+
41
+ // The name to put in the CommonName field of the
42
+ // intermediate certificates.
43
+ IntermediateCommonName string `json:"intermediate_common_name,omitempty"`
44
+
45
+ // Whether Caddy will attempt to install the CA's root
46
+ // into the system trust store, as well as into Java
47
+ // and Mozilla Firefox trust stores. Default: true.
48
+ InstallTrust *bool `json:"install_trust,omitempty"`
49
+
50
+ Root *KeyPair `json:"root,omitempty"`
51
+ Intermediate *KeyPair `json:"intermediate,omitempty"`
52
+
53
+ // Optionally configure a separate storage module associated with this
54
+ // issuer, instead of using Caddy's global/default-configured storage.
55
+ // This can be useful if you want to keep your signing keys in a
56
+ // separate location from your leaf certificates.
57
+ StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
58
+
59
+ id string
60
+ storage certmagic.Storage
61
+ root, inter *x509.Certificate
62
+ interKey interface{} // TODO: should we just store these as crypto.Signer?
63
+ mu *sync.RWMutex
64
+
65
+ rootCertPath string // mainly used for logging purposes if trusting
66
+ log *zap.Logger
67
+}
68
+
69
+// Provision sets up the CA.
70
+func (ca *CA) Provision(ctx caddy.Context, id string, log *zap.Logger) error {
71
+ ca.mu = new(sync.RWMutex)
72
+ ca.log = log.Named("ca." + id)
73
+
74
+ if id == "" {
75
+ return fmt.Errorf("CA ID is required (use 'local' for the default CA)")
76
+ }
77
+ ca.mu.Lock()
78
+ ca.id = id
79
+ ca.mu.Unlock()
80
+
81
+ if ca.StorageRaw != nil {
82
+ val, err := ctx.LoadModule(ca, "StorageRaw")
83
+ if err != nil {
84
+ return fmt.Errorf("loading storage module: %v", err)
85
+ }
86
+ cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
87
+ if err != nil {
88
+ return fmt.Errorf("creating storage configuration: %v", err)
89
+ }
90
+ ca.storage = cmStorage
91
+ }
92
+ if ca.storage == nil {
93
+ ca.storage = ctx.Storage()
94
+ }
95
+
96
+ if ca.Name == "" {
97
+ ca.Name = defaultCAName
98
+ }
99
+ if ca.RootCommonName == "" {
100
+ ca.RootCommonName = defaultRootCommonName
101
+ }
102
+ if ca.IntermediateCommonName == "" {
103
+ ca.IntermediateCommonName = defaultIntermediateCommonName
104
+ }
105
+
106
+ // load the certs and key that will be used for signing
107
+ var rootCert, interCert *x509.Certificate
108
+ var rootKey, interKey interface{}
109
+ var err error
110
+ if ca.Root != nil {
111
+ if ca.Root.Format == "" || ca.Root.Format == "pem_file" {
112
+ ca.rootCertPath = ca.Root.Certificate
113
+ }
114
+ rootCert, rootKey, err = ca.Root.Load()
115
+ } else {
116
+ ca.rootCertPath = "storage:" + ca.storageKeyRootCert()
117
+ rootCert, rootKey, err = ca.loadOrGenRoot()
118
+ }
119
+ if err != nil {
120
+ return err
121
+ }
122
+ if ca.Intermediate != nil {
123
+ interCert, interKey, err = ca.Intermediate.Load()
124
+ } else {
125
+ interCert, interKey, err = ca.loadOrGenIntermediate(rootCert, rootKey)
126
+ }
127
+ if err != nil {
128
+ return err
129
+ }
130
+
131
+ ca.mu.Lock()
132
+ ca.root, ca.inter, ca.interKey = rootCert, interCert, interKey
133
+ ca.mu.Unlock()
134
+
135
+ return nil
136
+}
137
+
138
+// ID returns the CA's ID, as given by the user in the config.
139
+func (ca CA) ID() string {
140
+ return ca.id
141
+}
142
+
143
+// RootCertificate returns the CA's root certificate (public key).
144
+func (ca CA) RootCertificate() *x509.Certificate {
145
+ ca.mu.RLock()
146
+ defer ca.mu.RUnlock()
147
+ return ca.root
148
+}
149
+
150
+// RootKey returns the CA's root private key. Since the root key is
151
+// not cached in memory long-term, it needs to be loaded from storage,
152
+// which could yield an error.
153
+func (ca CA) RootKey() (interface{}, error) {
154
+ _, rootKey, err := ca.loadOrGenRoot()
155
+ return rootKey, err
156
+}
157
+
158
+// IntermediateCertificate returns the CA's intermediate
159
+// certificate (public key).
160
+func (ca CA) IntermediateCertificate() *x509.Certificate {
161
+ ca.mu.RLock()
162
+ defer ca.mu.RUnlock()
163
+ return ca.inter
164
+}
165
+
166
+// IntermediateKey returns the CA's intermediate private key.
167
+func (ca CA) IntermediateKey() interface{} {
168
+ ca.mu.RLock()
169
+ defer ca.mu.RUnlock()
170
+ return ca.interKey
171
+}
172
+
173
+func (ca CA) loadOrGenRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) {
174
+ rootCertPEM, err := ca.storage.Load(ca.storageKeyRootCert())
175
+ if err != nil {
176
+ if _, ok := err.(certmagic.ErrNotExist); !ok {
177
+ return nil, nil, fmt.Errorf("loading root cert: %v", err)
178
+ }
179
+
180
+ // TODO: should we require that all or none of the assets are required before overwriting anything?
181
+ rootCert, rootKey, err = ca.genRoot()
182
+ if err != nil {
183
+ return nil, nil, fmt.Errorf("generating root: %v", err)
184
+ }
185
+ }
186
+
187
+ if rootCert == nil {
188
+ rootCert, err = pemDecodeSingleCert(rootCertPEM)
189
+ if err != nil {
190
+ return nil, nil, fmt.Errorf("parsing root certificate PEM: %v", err)
191
+ }
192
+ }
193
+ if rootKey == nil {
194
+ rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey())
195
+ if err != nil {
196
+ return nil, nil, fmt.Errorf("loading root key: %v", err)
197
+ }
198
+ rootKey, err = pemDecodePrivateKey(rootKeyPEM)
199
+ if err != nil {
200
+ return nil, nil, fmt.Errorf("decoding root key: %v", err)
201
+ }
202
+ }
203
+
204
+ return rootCert, rootKey, nil
205
+}
206
+
207
+func (ca CA) genRoot() (rootCert *x509.Certificate, rootKey interface{}, err error) {
208
+ repl := ca.newReplacer()
209
+
210
+ rootCert, rootKey, err = generateRoot(repl.ReplaceAll(ca.RootCommonName, ""))
211
+ if err != nil {
212
+ return nil, nil, fmt.Errorf("generating CA root: %v", err)
213
+ }
214
+ rootCertPEM, err := pemEncodeCert(rootCert.Raw)
215
+ if err != nil {
216
+ return nil, nil, fmt.Errorf("encoding root certificate: %v", err)
217
+ }
218
+ err = ca.storage.Store(ca.storageKeyRootCert(), rootCertPEM)
219
+ if err != nil {
220
+ return nil, nil, fmt.Errorf("saving root certificate: %v", err)
221
+ }
222
+ rootKeyPEM, err := pemEncodePrivateKey(rootKey)
223
+ if err != nil {
224
+ return nil, nil, fmt.Errorf("encoding root key: %v", err)
225
+ }
226
+ err = ca.storage.Store(ca.storageKeyRootKey(), rootKeyPEM)
227
+ if err != nil {
228
+ return nil, nil, fmt.Errorf("saving root key: %v", err)
229
+ }
230
+
231
+ return rootCert, rootKey, nil
232
+}
233
+
234
+func (ca CA) loadOrGenIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) {
235
+ interCertPEM, err := ca.storage.Load(ca.storageKeyIntermediateCert())
236
+ if err != nil {
237
+ if _, ok := err.(certmagic.ErrNotExist); !ok {
238
+ return nil, nil, fmt.Errorf("loading intermediate cert: %v", err)
239
+ }
240
+
241
+ // TODO: should we require that all or none of the assets are required before overwriting anything?
242
+ interCert, interKey, err = ca.genIntermediate(rootCert, rootKey)
243
+ if err != nil {
244
+ return nil, nil, fmt.Errorf("generating new intermediate cert: %v", err)
245
+ }
246
+ }
247
+
248
+ if interCert == nil {
249
+ interCert, err = pemDecodeSingleCert(interCertPEM)
250
+ if err != nil {
251
+ return nil, nil, fmt.Errorf("decoding intermediate certificate PEM: %v", err)
252
+ }
253
+ }
254
+
255
+ if interKey == nil {
256
+ interKeyPEM, err := ca.storage.Load(ca.storageKeyIntermediateKey())
257
+ if err != nil {
258
+ return nil, nil, fmt.Errorf("loading intermediate key: %v", err)
259
+ }
260
+ interKey, err = pemDecodePrivateKey(interKeyPEM)
261
+ if err != nil {
262
+ return nil, nil, fmt.Errorf("decoding intermediate key: %v", err)
263
+ }
264
+ }
265
+
266
+ return interCert, interKey, nil
267
+}
268
+
269
+func (ca CA) genIntermediate(rootCert *x509.Certificate, rootKey interface{}) (interCert *x509.Certificate, interKey interface{}, err error) {
270
+ repl := ca.newReplacer()
271
+
272
+ rootKeyPEM, err := ca.storage.Load(ca.storageKeyRootKey())
273
+ if err != nil {
274
+ return nil, nil, fmt.Errorf("loading root key to sign new intermediate: %v", err)
275
+ }
276
+ rootKey, err = pemDecodePrivateKey(rootKeyPEM)
277
+ if err != nil {
278
+ return nil, nil, fmt.Errorf("decoding root key: %v", err)
279
+ }
280
+ interCert, interKey, err = generateIntermediate(repl.ReplaceAll(ca.IntermediateCommonName, ""), rootCert, rootKey)
281
+ if err != nil {
282
+ return nil, nil, fmt.Errorf("generating CA intermediate: %v", err)
283
+ }
284
+ interCertPEM, err := pemEncodeCert(interCert.Raw)
285
+ if err != nil {
286
+ return nil, nil, fmt.Errorf("encoding intermediate certificate: %v", err)
287
+ }
288
+ err = ca.storage.Store(ca.storageKeyIntermediateCert(), interCertPEM)
289
+ if err != nil {
290
+ return nil, nil, fmt.Errorf("saving intermediate certificate: %v", err)
291
+ }
292
+ interKeyPEM, err := pemEncodePrivateKey(interKey)
293
+ if err != nil {
294
+ return nil, nil, fmt.Errorf("encoding intermediate key: %v", err)
295
+ }
296
+ err = ca.storage.Store(ca.storageKeyIntermediateKey(), interKeyPEM)
297
+ if err != nil {
298
+ return nil, nil, fmt.Errorf("saving intermediate key: %v", err)
299
+ }
300
+
301
+ return interCert, interKey, nil
302
+}
303
+
304
+func (ca CA) storageKeyCAPrefix() string {
305
+ return path.Join("pki", "authorities", certmagic.StorageKeys.Safe(ca.id))
306
+}
307
+func (ca CA) storageKeyRootCert() string {
308
+ return path.Join(ca.storageKeyCAPrefix(), "root.crt")
309
+}
310
+func (ca CA) storageKeyRootKey() string {
311
+ return path.Join(ca.storageKeyCAPrefix(), "root.key")
312
+}
313
+func (ca CA) storageKeyIntermediateCert() string {
314
+ return path.Join(ca.storageKeyCAPrefix(), "intermediate.crt")
315
+}
316
+func (ca CA) storageKeyIntermediateKey() string {
317
+ return path.Join(ca.storageKeyCAPrefix(), "intermediate.key")
318
+}
319
+
320
+func (ca CA) newReplacer() *caddy.Replacer {
321
+ repl := caddy.NewReplacer()
322
+ repl.Set("pki.ca.name", ca.Name)
323
+ return repl
324
+}
325
+
326
+const (
327
+ defaultCAID = "local"
328
+ defaultCAName = "Caddy Local Authority"
329
+ defaultRootCommonName = "{pki.ca.name} - {time.now.year} ECC Root"
330
+ defaultIntermediateCommonName = "{pki.ca.name} - ECC Intermediate"
331
+
332
+ defaultRootLifetime = 24 * time.Hour * 30 * 12 * 10
333
+ defaultIntermediateLifetime = 24 * time.Hour * 7
334
+)
modules/caddypki/certificates.go
new
+50
@@ -0,0 +1,50 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddypki
16
+
17
+import (
18
+ "crypto/x509"
19
+ "time"
20
+
21
+ "github.com/smallstep/cli/crypto/x509util"
22
+)
23
+
24
+func generateRoot(commonName string) (rootCrt *x509.Certificate, privateKey interface{}, err error) {
25
+ rootProfile, err := x509util.NewRootProfile(commonName)
26
+ if err != nil {
27
+ return
28
+ }
29
+ rootProfile.Subject().NotAfter = time.Now().Add(defaultRootLifetime) // TODO: make configurable
30
+ return newCert(rootProfile)
31
+}
32
+
33
+func generateIntermediate(commonName string, rootCrt *x509.Certificate, rootKey interface{}) (cert *x509.Certificate, privateKey interface{}, err error) {
34
+ interProfile, err := x509util.NewIntermediateProfile(commonName, rootCrt, rootKey)
35
+ if err != nil {
36
+ return
37
+ }
38
+ interProfile.Subject().NotAfter = time.Now().Add(defaultIntermediateLifetime) // TODO: make configurable
39
+ return newCert(interProfile)
40
+}
41
+
42
+func newCert(profile x509util.Profile) (cert *x509.Certificate, privateKey interface{}, err error) {
43
+ certBytes, err := profile.CreateCertificate()
44
+ if err != nil {
45
+ return
46
+ }
47
+ privateKey = profile.SubjectPrivateKey()
48
+ cert, err = x509.ParseCertificate(certBytes)
49
+ return
50
+}
modules/caddypki/command.go
new
+89
@@ -0,0 +1,89 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddypki
16
+
17
+import (
18
+ "flag"
19
+ "fmt"
20
+ "os"
21
+ "path/filepath"
22
+
23
+ "github.com/caddyserver/caddy/v2"
24
+ caddycmd "github.com/caddyserver/caddy/v2/cmd"
25
+ "github.com/smallstep/truststore"
26
+)
27
+
28
+func init() {
29
+ caddycmd.RegisterCommand(caddycmd.Command{
30
+ Name: "untrust",
31
+ Func: cmdUntrust,
32
+ Usage: "[--ca <id> | --cert <path>]",
33
+ Short: "Untrusts a locally-trusted CA certificate",
34
+ Long: `
35
+Untrusts a root certificate from the local trust store(s). Intended
36
+for development environments only.
37
+
38
+This command uninstalls trust; it does not necessarily delete the
39
+root certificate from trust stores entirely. Thus, repeatedly
40
+trusting and untrusting new certificates can fill up trust databases.
41
+
42
+This command does not delete or modify certificate files.
43
+
44
+Specify which certificate to untrust either by the ID of its CA with
45
+the --ca flag, or the direct path to the certificate file with the
46
+--cert flag. If the --ca flag is used, only the default storage paths
47
+are assumed (i.e. using --ca flag with custom storage backends or file
48
+paths will not work).
49
+
50
+If no flags are specified, --ca=local is assumed.`,
51
+ Flags: func() *flag.FlagSet {
52
+ fs := flag.NewFlagSet("untrust", flag.ExitOnError)
53
+ fs.String("ca", "", "The ID of the CA to untrust")
54
+ fs.String("cert", "", "The path to the CA certificate to untrust")
55
+ return fs
56
+ }(),
57
+ })
58
+}
59
+
60
+func cmdUntrust(fs caddycmd.Flags) (int, error) {
61
+ ca := fs.String("ca")
62
+ cert := fs.String("cert")
63
+
64
+ if ca != "" && cert != "" {
65
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("conflicting command line arguments")
66
+ }
67
+ if ca == "" && cert == "" {
68
+ ca = defaultCAID
69
+ }
70
+ if ca != "" {
71
+ cert = filepath.Join(caddy.AppDataDir(), "pki", "authorities", ca, "root.crt")
72
+ }
73
+
74
+ // sanity check, make sure cert file exists first
75
+ _, err := os.Stat(cert)
76
+ if err != nil {
77
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing certificate file: %v", err)
78
+ }
79
+
80
+ err = truststore.UninstallFile(cert,
81
+ truststore.WithDebug(),
82
+ truststore.WithFirefox(),
83
+ truststore.WithJava())
84
+ if err != nil {
85
+ return caddy.ExitCodeFailedStartup, err
86
+ }
87
+
88
+ return caddy.ExitCodeSuccess, nil
89
+}
modules/caddypki/crypto.go
new
+155
@@ -0,0 +1,155 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddypki
16
+
17
+import (
18
+ "bytes"
19
+ "crypto"
20
+ "crypto/ecdsa"
21
+ "crypto/ed25519"
22
+ "crypto/rsa"
23
+ "crypto/x509"
24
+ "encoding/pem"
25
+ "fmt"
26
+ "io/ioutil"
27
+ "strings"
28
+)
29
+
30
+func pemDecodeSingleCert(pemDER []byte) (*x509.Certificate, error) {
31
+ pemBlock, remaining := pem.Decode(pemDER)
32
+ if pemBlock == nil {
33
+ return nil, fmt.Errorf("no PEM block found")
34
+ }
35
+ if len(remaining) > 0 {
36
+ return nil, fmt.Errorf("input contained more than a single PEM block")
37
+ }
38
+ if pemBlock.Type != "CERTIFICATE" {
39
+ return nil, fmt.Errorf("expected PEM block type to be CERTIFICATE, but got '%s'", pemBlock.Type)
40
+ }
41
+ return x509.ParseCertificate(pemBlock.Bytes)
42
+}
43
+
44
+func pemEncodeCert(der []byte) ([]byte, error) {
45
+ return pemEncode("CERTIFICATE", der)
46
+}
47
+
48
+// pemEncodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes.
49
+// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported.
50
+func pemEncodePrivateKey(key crypto.PrivateKey) ([]byte, error) {
51
+ var pemType string
52
+ var keyBytes []byte
53
+ switch key := key.(type) {
54
+ case *ecdsa.PrivateKey:
55
+ var err error
56
+ pemType = "EC"
57
+ keyBytes, err = x509.MarshalECPrivateKey(key)
58
+ if err != nil {
59
+ return nil, err
60
+ }
61
+ case *rsa.PrivateKey:
62
+ pemType = "RSA"
63
+ keyBytes = x509.MarshalPKCS1PrivateKey(key)
64
+ case *ed25519.PrivateKey:
65
+ var err error
66
+ pemType = "ED25519"
67
+ keyBytes, err = x509.MarshalPKCS8PrivateKey(key)
68
+ if err != nil {
69
+ return nil, err
70
+ }
71
+ default:
72
+ return nil, fmt.Errorf("unsupported key type: %T", key)
73
+ }
74
+ return pemEncode(pemType+" PRIVATE KEY", keyBytes)
75
+}
76
+
77
+// pemDecodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
78
+// Borrowed from Go standard library, to handle various private key and PEM block types.
79
+// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L291-L308
80
+// https://github.com/golang/go/blob/693748e9fa385f1e2c3b91ca9acbb6c0ad2d133d/src/crypto/tls/tls.go#L238)
81
+// TODO: this is the same thing as in certmagic. Should we reuse that code somehow? It's unexported.
82
+func pemDecodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) {
83
+ keyBlockDER, _ := pem.Decode(keyPEMBytes)
84
+
85
+ if keyBlockDER.Type != "PRIVATE KEY" && !strings.HasSuffix(keyBlockDER.Type, " PRIVATE KEY") {
86
+ return nil, fmt.Errorf("unknown PEM header %q", keyBlockDER.Type)
87
+ }
88
+
89
+ if key, err := x509.ParsePKCS1PrivateKey(keyBlockDER.Bytes); err == nil {
90
+ return key, nil
91
+ }
92
+
93
+ if key, err := x509.ParsePKCS8PrivateKey(keyBlockDER.Bytes); err == nil {
94
+ switch key := key.(type) {
95
+ case *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey:
96
+ return key, nil
97
+ default:
98
+ return nil, fmt.Errorf("found unknown private key type in PKCS#8 wrapping: %T", key)
99
+ }
100
+ }
101
+
102
+ if key, err := x509.ParseECPrivateKey(keyBlockDER.Bytes); err == nil {
103
+ return key, nil
104
+ }
105
+
106
+ return nil, fmt.Errorf("unknown private key type")
107
+}
108
+
109
+func pemEncode(blockType string, b []byte) ([]byte, error) {
110
+ var buf bytes.Buffer
111
+ err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: b})
112
+ return buf.Bytes(), err
113
+}
114
+
115
+func trusted(cert *x509.Certificate) bool {
116
+ chains, err := cert.Verify(x509.VerifyOptions{})
117
+ return len(chains) > 0 && err == nil
118
+}
119
+
120
+// KeyPair represents a public-private key pair, where the
121
+// public key is also called a certificate.
122
+type KeyPair struct {
123
+ Certificate string `json:"certificate,omitempty"`
124
+ PrivateKey string `json:"private_key,omitempty"`
125
+ Format string `json:"format,omitempty"`
126
+}
127
+
128
+// Load loads the certificate and key.
129
+func (kp KeyPair) Load() (*x509.Certificate, interface{}, error) {
130
+ switch kp.Format {
131
+ case "", "pem_file":
132
+ certData, err := ioutil.ReadFile(kp.Certificate)
133
+ if err != nil {
134
+ return nil, nil, err
135
+ }
136
+ keyData, err := ioutil.ReadFile(kp.PrivateKey)
137
+ if err != nil {
138
+ return nil, nil, err
139
+ }
140
+
141
+ cert, err := pemDecodeSingleCert(certData)
142
+ if err != nil {
143
+ return nil, nil, err
144
+ }
145
+ key, err := pemDecodePrivateKey(keyData)
146
+ if err != nil {
147
+ return nil, nil, err
148
+ }
149
+
150
+ return cert, key, nil
151
+
152
+ default:
153
+ return nil, nil, fmt.Errorf("unsupported format: %s", kp.Format)
154
+ }
155
+}
modules/caddypki/maintain.go
new
+99
@@ -0,0 +1,99 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddypki
16
+
17
+import (
18
+ "crypto/x509"
19
+ "fmt"
20
+ "time"
21
+
22
+ "go.uber.org/zap"
23
+)
24
+
25
+func (p *PKI) maintenance() {
26
+ ticker := time.NewTicker(10 * time.Minute) // TODO: make configurable
27
+ defer ticker.Stop()
28
+
29
+ for {
30
+ select {
31
+ case <-ticker.C:
32
+ p.renewCerts()
33
+ case <-p.ctx.Done():
34
+ return
35
+ }
36
+ }
37
+}
38
+
39
+func (p *PKI) renewCerts() {
40
+ for _, ca := range p.CAs {
41
+ err := p.renewCertsForCA(ca)
42
+ if err != nil {
43
+ p.log.Error("renewing intermediate certificates",
44
+ zap.Error(err),
45
+ zap.String("ca", ca.id))
46
+ }
47
+ }
48
+}
49
+
50
+func (p *PKI) renewCertsForCA(ca *CA) error {
51
+ ca.mu.Lock()
52
+ defer ca.mu.Unlock()
53
+
54
+ log := p.log.With(zap.String("ca", ca.id))
55
+
56
+ // only maintain the root if it's not manually provided in the config
57
+ if ca.Root == nil {
58
+ if needsRenewal(ca.root) {
59
+ // TODO: implement root renewal (use same key)
60
+ log.Warn("root certificate expiring soon (FIXME: ROOT RENEWAL NOT YET IMPLEMENTED)",
61
+ zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)),
62
+ )
63
+ }
64
+ }
65
+
66
+ // only maintain the intermediate if it's not manually provided in the config
67
+ if ca.Intermediate == nil {
68
+ if needsRenewal(ca.inter) {
69
+ log.Info("intermediate expires soon; renewing",
70
+ zap.Duration("time_remaining", time.Until(ca.inter.NotAfter)),
71
+ )
72
+
73
+ rootCert, rootKey, err := ca.loadOrGenRoot()
74
+ if err != nil {
75
+ return fmt.Errorf("loading root key: %v", err)
76
+ }
77
+ interCert, interKey, err := ca.genIntermediate(rootCert, rootKey)
78
+ if err != nil {
79
+ return fmt.Errorf("generating new certificate: %v", err)
80
+ }
81
+ ca.inter, ca.interKey = interCert, interKey
82
+
83
+ log.Info("renewed intermediate",
84
+ zap.Time("new_expiration", ca.inter.NotAfter),
85
+ )
86
+ }
87
+ }
88
+
89
+ return nil
90
+}
91
+
92
+func needsRenewal(cert *x509.Certificate) bool {
93
+ lifetime := cert.NotAfter.Sub(cert.NotBefore)
94
+ renewalWindow := time.Duration(float64(lifetime) * renewalWindowRatio)
95
+ renewalWindowStart := cert.NotAfter.Add(-renewalWindow)
96
+ return time.Now().After(renewalWindowStart)
97
+}
98
+
99
+const renewalWindowRatio = 0.2 // TODO: make configurable
modules/caddypki/pki.go
new
+117
@@ -0,0 +1,117 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddypki
16
+
17
+import (
18
+ "fmt"
19
+
20
+ "github.com/caddyserver/caddy/v2"
21
+ "github.com/smallstep/truststore"
22
+ "go.uber.org/zap"
23
+)
24
+
25
+func init() {
26
+ caddy.RegisterModule(PKI{})
27
+}
28
+
29
+// PKI provides Public Key Infrastructure facilities for Caddy.
30
+type PKI struct {
31
+ // The CAs to manage. Each CA is keyed by an ID that is used
32
+ // to uniquely identify it from other CAs. The default CA ID
33
+ // is "local".
34
+ CAs map[string]*CA `json:"certificate_authorities,omitempty"`
35
+
36
+ ctx caddy.Context
37
+ log *zap.Logger
38
+}
39
+
40
+// CaddyModule returns the Caddy module information.
41
+func (PKI) CaddyModule() caddy.ModuleInfo {
42
+ return caddy.ModuleInfo{
43
+ ID: "pki",
44
+ New: func() caddy.Module { return new(PKI) },
45
+ }
46
+}
47
+
48
+// Provision sets up the configuration for the PKI app.
49
+func (p *PKI) Provision(ctx caddy.Context) error {
50
+ p.ctx = ctx
51
+ p.log = ctx.Logger(p)
52
+
53
+ // if this app is initialized at all, ensure there's
54
+ // at least a default CA that can be used
55
+ if len(p.CAs) == 0 {
56
+ p.CAs = map[string]*CA{defaultCAID: new(CA)}
57
+ }
58
+
59
+ for caID, ca := range p.CAs {
60
+ err := ca.Provision(ctx, caID, p.log)
61
+ if err != nil {
62
+ return fmt.Errorf("provisioning CA '%s': %v", caID, err)
63
+ }
64
+ }
65
+
66
+ return nil
67
+}
68
+
69
+// Start starts the PKI app.
70
+func (p *PKI) Start() error {
71
+ // install roots to trust store, if not disabled
72
+ for _, ca := range p.CAs {
73
+ if ca.InstallTrust != nil && !*ca.InstallTrust {
74
+ ca.log.Warn("root certificate trust store installation disabled; clients will show warnings without intervention",
75
+ zap.String("path", ca.rootCertPath))
76
+ continue
77
+ }
78
+
79
+ // avoid password prompt if already trusted
80
+ if trusted(ca.root) {
81
+ ca.log.Info("root certificate is already trusted by system",
82
+ zap.String("path", ca.rootCertPath))
83
+ continue
84
+ }
85
+
86
+ ca.log.Warn("trusting root certificate (you might be prompted for password)",
87
+ zap.String("path", ca.rootCertPath))
88
+
89
+ err := truststore.Install(ca.root,
90
+ truststore.WithDebug(),
91
+ truststore.WithFirefox(),
92
+ truststore.WithJava(),
93
+ )
94
+ if err != nil {
95
+ return fmt.Errorf("adding root certificate to trust store: %v", err)
96
+ }
97
+ }
98
+
99
+ // see if root/intermediates need renewal...
100
+ p.renewCerts()
101
+
102
+ // ...and keep them renewed
103
+ go p.maintenance()
104
+
105
+ return nil
106
+}
107
+
108
+// Stop stops the PKI app.
109
+func (p *PKI) Stop() error {
110
+ return nil
111
+}
112
+
113
+// Interface guards
114
+var (
115
+ _ caddy.Provisioner = (*PKI)(nil)
116
+ _ caddy.App = (*PKI)(nil)
117
+)
modules/caddytls/acmeissuer.go
+3
-2
@@ -145,7 +145,7 @@ func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) {
145
}
146
147
// PreCheck implements the certmagic.PreChecker interface.
148
-func (m *ACMEIssuer) PreCheck(names []string, interactive bool) (skip bool, err error) {
148
+func (m *ACMEIssuer) PreCheck(names []string, interactive bool) error {
149
return certmagic.NewACMEManager(m.magic, m.template).PreCheck(names, interactive)
150
}
151
@@ -200,8 +200,9 @@ type DNSProviderMaker interface {
200
201
// Interface guards
202
var (
203
+ _ certmagic.PreChecker = (*ACMEIssuer)(nil)
204
_ certmagic.Issuer = (*ACMEIssuer)(nil)
205
_ certmagic.Revoker = (*ACMEIssuer)(nil)
205
- _ certmagic.PreChecker = (*ACMEIssuer)(nil)
206
+ _ caddy.Provisioner = (*ACMEIssuer)(nil)
207
_ ConfigSetter = (*ACMEIssuer)(nil)
208
)
modules/caddytls/internalissuer.go
new
+199
@@ -0,0 +1,199 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+package caddytls
16
+
17
+import (
18
+ "bytes"
19
+ "context"
20
+ "crypto"
21
+ "crypto/x509"
22
+ "encoding/pem"
23
+ "fmt"
24
+ "time"
25
+
26
+ "github.com/caddyserver/caddy/v2"
27
+ "github.com/caddyserver/caddy/v2/modules/caddypki"
28
+ "github.com/caddyserver/certmagic"
29
+ "github.com/smallstep/certificates/authority"
30
+ "github.com/smallstep/certificates/authority/provisioner"
31
+ "github.com/smallstep/cli/crypto/x509util"
32
+)
33
+
34
+func init() {
35
+ caddy.RegisterModule(InternalIssuer{})
36
+}
37
+
38
+// InternalIssuer is a certificate issuer that generates
39
+// certificates internally using a locally-configured
40
+// CA which can be customized using the `pki` app.
41
+type InternalIssuer struct {
42
+ // The ID of the CA to use for signing. The default
43
+ // CA ID is "local". The CA can be configured with the
44
+ // `pki` app.
45
+ CA string `json:"ca,omitempty"`
46
+
47
+ // The validity period of certificates.
48
+ Lifetime caddy.Duration `json:"lifetime,omitempty"`
49
+
50
+ // If true, the root will be the issuer instead of
51
+ // the intermediate. This is NOT recommended and should
52
+ // only be used when devices/clients do not properly
53
+ // validate certificate chains.
54
+ SignWithRoot bool `json:"sign_with_root,omitempty"`
55
+
56
+ ca *caddypki.CA
57
+}
58
+
59
+// CaddyModule returns the Caddy module information.
60
+func (InternalIssuer) CaddyModule() caddy.ModuleInfo {
61
+ return caddy.ModuleInfo{
62
+ ID: "tls.issuance.internal",
63
+ New: func() caddy.Module { return new(InternalIssuer) },
64
+ }
65
+}
66
+
67
+// Provision sets up the issuer.
68
+func (li *InternalIssuer) Provision(ctx caddy.Context) error {
69
+ // get a reference to the configured CA
70
+ appModule, err := ctx.App("pki")
71
+ if err != nil {
72
+ return err
73
+ }
74
+ pkiApp := appModule.(*caddypki.PKI)
75
+ if li.CA == "" {
76
+ li.CA = defaultInternalCAName
77
+ }
78
+ ca, ok := pkiApp.CAs[li.CA]
79
+ if !ok {
80
+ return fmt.Errorf("no certificate authority configured with id: %s", li.CA)
81
+ }
82
+ li.ca = ca
83
+
84
+ // set any other default values
85
+ if li.Lifetime == 0 {
86
+ li.Lifetime = caddy.Duration(defaultInternalCertLifetime)
87
+ }
88
+
89
+ return nil
90
+}
91
+
92
+// IssuerKey returns the unique issuer key for the
93
+// confgured CA endpoint.
94
+func (li InternalIssuer) IssuerKey() string {
95
+ return li.ca.ID()
96
+}
97
+
98
+// Issue issues a certificate to satisfy the CSR.
99
+func (li InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
100
+ // prepare the signing authority
101
+ // TODO: eliminate placeholders / needless values
102
+ cfg := &authority.Config{
103
+ Address: "placeholder_Address:1",
104
+ Root: []string{"placeholder_Root"},
105
+ IntermediateCert: "placeholder_IntermediateCert",
106
+ IntermediateKey: "placeholder_IntermediateKey",
107
+ DNSNames: []string{"placeholder_DNSNames"},
108
+ AuthorityConfig: &authority.AuthConfig{
109
+ Provisioners: provisioner.List{},
110
+ },
111
+ }
112
+
113
+ // get the root certificate and the issuer cert+key
114
+ rootCert := li.ca.RootCertificate()
115
+ var issuerCert *x509.Certificate
116
+ var issuerKey interface{}
117
+ if li.SignWithRoot {
118
+ issuerCert = rootCert
119
+ var err error
120
+ issuerKey, err = li.ca.RootKey()
121
+ if err != nil {
122
+ return nil, fmt.Errorf("loading signing key: %v", err)
123
+ }
124
+ } else {
125
+ issuerCert = li.ca.IntermediateCertificate()
126
+ issuerKey = li.ca.IntermediateKey()
127
+ }
128
+
129
+ auth, err := authority.New(cfg,
130
+ authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)),
131
+ authority.WithX509RootCerts(rootCert),
132
+ )
133
+ if err != nil {
134
+ return nil, fmt.Errorf("initializing certificate authority: %v", err)
135
+ }
136
+
137
+ // ensure issued certificate does not expire later than its issuer
138
+ lifetime := time.Duration(li.Lifetime)
139
+ if time.Now().Add(lifetime).After(issuerCert.NotAfter) {
140
+ // TODO: log this
141
+ lifetime = issuerCert.NotAfter.Sub(time.Now())
142
+ }
143
+
144
+ certChain, err := auth.Sign(csr, provisioner.Options{},
145
+ profileDefaultDuration(li.Lifetime),
146
+ )
147
+ if err != nil {
148
+ return nil, err
149
+ }
150
+
151
+ var buf bytes.Buffer
152
+ for _, cert := range certChain {
153
+ err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
154
+ if err != nil {
155
+ return nil, err
156
+ }
157
+ }
158
+
159
+ return &certmagic.IssuedCertificate{
160
+ Certificate: buf.Bytes(),
161
+ }, nil
162
+}
163
+
164
+// TODO: borrowing from https://github.com/smallstep/certificates/blob/806abb6232a5691198b891d76b9898ea7f269da0/authority/provisioner/sign_options.go#L191-L211
165
+// as per https://github.com/smallstep/certificates/issues/198.
166
+// profileDefaultDuration is a wrapper against x509util.WithOption to conform
167
+// the SignOption interface.
168
+type profileDefaultDuration time.Duration
169
+
170
+// TODO: is there a better way to set cert lifetimes than copying from the smallstep libs?
171
+func (d profileDefaultDuration) Option(so provisioner.Options) x509util.WithOption {
172
+ var backdate time.Duration
173
+ notBefore := so.NotBefore.Time()
174
+ if notBefore.IsZero() {
175
+ notBefore = time.Now().Truncate(time.Second)
176
+ backdate = -1 * so.Backdate
177
+ }
178
+ notAfter := so.NotAfter.RelativeTime(notBefore)
179
+ return func(p x509util.Profile) error {
180
+ fn := x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(d))
181
+ if err := fn(p); err != nil {
182
+ return err
183
+ }
184
+ crt := p.Subject()
185
+ crt.NotBefore = crt.NotBefore.Add(backdate)
186
+ return nil
187
+ }
188
+}
189
+
190
+const (
191
+ defaultInternalCAName = "local"
192
+ defaultInternalCertLifetime = 12 * time.Hour
193
+)
194
+
195
+// Interface guards
196
+var (
197
+ _ caddy.Provisioner = (*InternalIssuer)(nil)
198
+ _ certmagic.Issuer = (*InternalIssuer)(nil)
199
+)
modules/caddytls/tls.go
+46
-6
@@ -175,6 +175,26 @@ func (t *TLS) Provision(ctx caddy.Context) error {
175
return nil
176
}
177
178
+// Validate validates t's configuration.
179
+func (t *TLS) Validate() error {
180
+ if t.Automation != nil {
181
+ // ensure that host aren't repeated; since only the first
182
+ // automation policy is used, repeating a host in the lists
183
+ // isn't useful and is probably a mistake
184
+ // TODO: test this
185
+ hostSet := make(map[string]int)
186
+ for i, ap := range t.Automation.Policies {
187
+ for _, h := range ap.Hosts {
188
+ if first, ok := hostSet[h]; ok {
189
+ return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first)
190
+ }
191
+ hostSet[h] = i
192
+ }
193
+ }
194
+ }
195
+ return nil
196
+}
197
+
198
// Start activates the TLS module.
199
func (t *TLS) Start() error {
200
// 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 {
286
}
287
288
// AddAutomationPolicy provisions and adds ap to the list of the app's
269
-// automation policies.
289
+// automation policies. If an existing automation policy exists that has
290
+// fewer hosts in its list than ap does, ap will be inserted before that
291
+// other policy (this helps ensure that ap will be prioritized/chosen
292
+// over, say, a catch-all policy).
293
func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error {
294
if t.Automation == nil {
295
t.Automation = new(AutomationConfig)
@@ -275,6 +298,16 @@ func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error {
298
if err != nil {
299
return err
300
}
301
+ for i, other := range t.Automation.Policies {
302
+ // if a catch-all policy (or really, any policy with
303
+ // fewer names) exists, prioritize this new policy
304
+ if len(other.Hosts) < len(ap.Hosts) {
305
+ t.Automation.Policies = append(t.Automation.Policies[:i],
306
+ append([]*AutomationPolicy{ap}, t.Automation.Policies[i+1:]...)...)
307
+ return nil
308
+ }
309
+ }
310
+ // otherwise just append the new one
311
t.Automation.Policies = append(t.Automation.Policies, ap)
312
return nil
313
}
@@ -444,6 +477,7 @@ type AutomationPolicy struct {
477
// obtaining or renewing certificates. This is often
478
// not desirable, especially when serving sites out
479
// of your control. Default: false
480
+ // TODO: is this really necessary per-policy? why not a global setting...
481
ManageSync bool `json:"manage_sync,omitempty"`
482
483
Issuer certmagic.Issuer `json:"-"`
@@ -510,8 +544,7 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error {
544
OnDemand: ond,
545
Storage: storage,
546
}
513
- cfg := certmagic.New(tlsApp.certCache, template)
514
- ap.magic = cfg
547
+ ap.magic = certmagic.New(tlsApp.certCache, template)
548
549
if ap.IssuerRaw != nil {
550
val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw")
@@ -527,12 +560,12 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error {
560
// ACME challenges -- it's an annoying, inelegant circular
561
// dependency that I don't know how to resolve nicely!)
562
if configger, ok := ap.Issuer.(ConfigSetter); ok {
530
- configger.SetConfig(cfg)
563
+ configger.SetConfig(ap.magic)
564
}
565
533
- cfg.Issuer = ap.Issuer
566
+ ap.magic.Issuer = ap.Issuer
567
if rev, ok := ap.Issuer.(certmagic.Revoker); ok {
535
- cfg.Revoker = rev
568
+ ap.magic.Revoker = rev
569
}
570
571
return nil
@@ -789,3 +822,10 @@ func (t *TLS) moveCertificates() error {
822
823
return nil
824
}
825
+
826
+// Interface guards
827
+var (
828
+ _ caddy.Provisioner = (*TLS)(nil)
829
+ _ caddy.Validator = (*TLS)(nil)
830
+ _ caddy.App = (*TLS)(nil)
831
+)
modules/standard/import.go
+1
@@ -6,6 +6,7 @@ import (
6
_ "github.com/caddyserver/caddy/v2/caddyconfig/json5"
7
_ "github.com/caddyserver/caddy/v2/caddyconfig/jsonc"
8
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard"
9
+ _ "github.com/caddyserver/caddy/v2/modules/caddypki"
10
_ "github.com/caddyserver/caddy/v2/modules/caddytls"
11
_ "github.com/caddyserver/caddy/v2/modules/caddytls/distributedstek"
12
_ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek"
replacer.go
+3
@@ -19,6 +19,7 @@ import (
19
"os"
20
"path/filepath"
21
"runtime"
22
+ "strconv"
23
"strings"
24
"time"
25
)
@@ -236,6 +237,8 @@ func globalDefaultReplacements(key string) (string, bool) {
237
return runtime.GOARCH, true
238
case "time.now.common_log":
239
return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true
240
+ case "time.now.year":
241
+ return strconv.Itoa(nowFunc().Year()), true
242
}
243
244
return "", false