mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
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
This commit is contained in:
parent
cfe85a9fe6
commit
5a19db5dc2
21 changed files with 1293 additions and 176 deletions
|
@ -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?
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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: mgr,
|
||||
Value: acmeIssuer,
|
||||
})
|
||||
} else if internalIssuer != nil {
|
||||
configVals = append(configVals, ConfigValue{
|
||||
Class: "tls.cert_issuer",
|
||||
Value: internalIssuer,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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) &&
|
||||
|
|
8
go.mod
8
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
|
||||
|
|
16
go.sum
16
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=
|
||||
|
|
|
@ -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 acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 {
|
||||
// don't overwrite existing explicit config
|
||||
acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort
|
||||
if matchingPolicy == nil {
|
||||
matchingPolicy = new(caddytls.AutomationPolicy)
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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 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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
334
modules/caddypki/ca.go
Normal file
334
modules/caddypki/ca.go
Normal file
|
@ -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
|
||||
)
|
50
modules/caddypki/certificates.go
Normal file
50
modules/caddypki/certificates.go
Normal file
|
@ -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
|
||||
}
|
89
modules/caddypki/command.go
Normal file
89
modules/caddypki/command.go
Normal file
|
@ -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
|
||||
}
|
155
modules/caddypki/crypto.go
Normal file
155
modules/caddypki/crypto.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
99
modules/caddypki/maintain.go
Normal file
99
modules/caddypki/maintain.go
Normal file
|
@ -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
|
117
modules/caddypki/pki.go
Normal file
117
modules/caddypki/pki.go
Normal file
|
@ -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)
|
||||
)
|
|
@ -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)
|
||||
)
|
||||
|
|
199
modules/caddytls/internalissuer.go
Normal file
199
modules/caddytls/internalissuer.go
Normal file
|
@ -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)
|
||||
)
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue