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:
Matt Holt 2020-03-13 11:06:08 -06:00 committed by GitHub
parent cfe85a9fe6
commit 5a19db5dc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1293 additions and 176 deletions

View File

@ -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?
)

View File

@ -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)
}
}

View File

@ -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,
})
}

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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

View File

@ -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)
}

View File

@ -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}

View File

@ -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
View 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
)

View 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
}

View 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
View 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)
}
}

View 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
View 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)
)

View File

@ -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)
)

View 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)
)

View File

@ -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)
)

View File

@ -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"

View File

@ -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