From 5a19db5dc2db7c02d0f99630a07a64cacb7f7b44 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 13 Mar 2020 11:06:08 -0600 Subject: 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 --- modules/caddytls/acmeissuer.go | 5 +- modules/caddytls/internalissuer.go | 199 +++++++++++++++++++++++++++++++++++++ modules/caddytls/tls.go | 52 ++++++++-- 3 files changed, 248 insertions(+), 8 deletions(-) create mode 100644 modules/caddytls/internalissuer.go (limited to 'modules/caddytls') diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 36fd76c..f108d72 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -145,7 +145,7 @@ func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) { } // PreCheck implements the certmagic.PreChecker interface. -func (m *ACMEIssuer) PreCheck(names []string, interactive bool) (skip bool, err error) { +func (m *ACMEIssuer) PreCheck(names []string, interactive bool) error { return certmagic.NewACMEManager(m.magic, m.template).PreCheck(names, interactive) } @@ -200,8 +200,9 @@ type DNSProviderMaker interface { // Interface guards var ( + _ certmagic.PreChecker = (*ACMEIssuer)(nil) _ certmagic.Issuer = (*ACMEIssuer)(nil) _ certmagic.Revoker = (*ACMEIssuer)(nil) - _ certmagic.PreChecker = (*ACMEIssuer)(nil) + _ caddy.Provisioner = (*ACMEIssuer)(nil) _ ConfigSetter = (*ACMEIssuer)(nil) ) diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go new file mode 100644 index 0000000..53a1d00 --- /dev/null +++ b/modules/caddytls/internalissuer.go @@ -0,0 +1,199 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddytls + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddypki" + "github.com/caddyserver/certmagic" + "github.com/smallstep/certificates/authority" + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/cli/crypto/x509util" +) + +func init() { + caddy.RegisterModule(InternalIssuer{}) +} + +// InternalIssuer is a certificate issuer that generates +// certificates internally using a locally-configured +// CA which can be customized using the `pki` app. +type InternalIssuer struct { + // The ID of the CA to use for signing. The default + // CA ID is "local". The CA can be configured with the + // `pki` app. + CA string `json:"ca,omitempty"` + + // The validity period of certificates. + Lifetime caddy.Duration `json:"lifetime,omitempty"` + + // If true, the root will be the issuer instead of + // the intermediate. This is NOT recommended and should + // only be used when devices/clients do not properly + // validate certificate chains. + SignWithRoot bool `json:"sign_with_root,omitempty"` + + ca *caddypki.CA +} + +// CaddyModule returns the Caddy module information. +func (InternalIssuer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.issuance.internal", + New: func() caddy.Module { return new(InternalIssuer) }, + } +} + +// Provision sets up the issuer. +func (li *InternalIssuer) Provision(ctx caddy.Context) error { + // get a reference to the configured CA + appModule, err := ctx.App("pki") + if err != nil { + return err + } + pkiApp := appModule.(*caddypki.PKI) + if li.CA == "" { + li.CA = defaultInternalCAName + } + ca, ok := pkiApp.CAs[li.CA] + if !ok { + return fmt.Errorf("no certificate authority configured with id: %s", li.CA) + } + li.ca = ca + + // set any other default values + if li.Lifetime == 0 { + li.Lifetime = caddy.Duration(defaultInternalCertLifetime) + } + + return nil +} + +// IssuerKey returns the unique issuer key for the +// confgured CA endpoint. +func (li InternalIssuer) IssuerKey() string { + return li.ca.ID() +} + +// Issue issues a certificate to satisfy the CSR. +func (li InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + // prepare the signing authority + // TODO: eliminate placeholders / needless values + cfg := &authority.Config{ + Address: "placeholder_Address:1", + Root: []string{"placeholder_Root"}, + IntermediateCert: "placeholder_IntermediateCert", + IntermediateKey: "placeholder_IntermediateKey", + DNSNames: []string{"placeholder_DNSNames"}, + AuthorityConfig: &authority.AuthConfig{ + Provisioners: provisioner.List{}, + }, + } + + // get the root certificate and the issuer cert+key + rootCert := li.ca.RootCertificate() + var issuerCert *x509.Certificate + var issuerKey interface{} + if li.SignWithRoot { + issuerCert = rootCert + var err error + issuerKey, err = li.ca.RootKey() + if err != nil { + return nil, fmt.Errorf("loading signing key: %v", err) + } + } else { + issuerCert = li.ca.IntermediateCertificate() + issuerKey = li.ca.IntermediateKey() + } + + auth, err := authority.New(cfg, + authority.WithX509Signer(issuerCert, issuerKey.(crypto.Signer)), + authority.WithX509RootCerts(rootCert), + ) + if err != nil { + return nil, fmt.Errorf("initializing certificate authority: %v", err) + } + + // ensure issued certificate does not expire later than its issuer + lifetime := time.Duration(li.Lifetime) + if time.Now().Add(lifetime).After(issuerCert.NotAfter) { + // TODO: log this + lifetime = issuerCert.NotAfter.Sub(time.Now()) + } + + certChain, err := auth.Sign(csr, provisioner.Options{}, + profileDefaultDuration(li.Lifetime), + ) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + for _, cert := range certChain { + err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + if err != nil { + return nil, err + } + } + + return &certmagic.IssuedCertificate{ + Certificate: buf.Bytes(), + }, nil +} + +// TODO: borrowing from https://github.com/smallstep/certificates/blob/806abb6232a5691198b891d76b9898ea7f269da0/authority/provisioner/sign_options.go#L191-L211 +// as per https://github.com/smallstep/certificates/issues/198. +// profileDefaultDuration is a wrapper against x509util.WithOption to conform +// the SignOption interface. +type profileDefaultDuration time.Duration + +// TODO: is there a better way to set cert lifetimes than copying from the smallstep libs? +func (d profileDefaultDuration) Option(so provisioner.Options) x509util.WithOption { + var backdate time.Duration + notBefore := so.NotBefore.Time() + if notBefore.IsZero() { + notBefore = time.Now().Truncate(time.Second) + backdate = -1 * so.Backdate + } + notAfter := so.NotAfter.RelativeTime(notBefore) + return func(p x509util.Profile) error { + fn := x509util.WithNotBeforeAfterDuration(notBefore, notAfter, time.Duration(d)) + if err := fn(p); err != nil { + return err + } + crt := p.Subject() + crt.NotBefore = crt.NotBefore.Add(backdate) + return nil + } +} + +const ( + defaultInternalCAName = "local" + defaultInternalCertLifetime = 12 * time.Hour +) + +// Interface guards +var ( + _ caddy.Provisioner = (*InternalIssuer)(nil) + _ certmagic.Issuer = (*InternalIssuer)(nil) +) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 4fa126e..f91229f 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -175,6 +175,26 @@ func (t *TLS) Provision(ctx caddy.Context) error { return nil } +// Validate validates t's configuration. +func (t *TLS) Validate() error { + if t.Automation != nil { + // ensure that host aren't repeated; since only the first + // automation policy is used, repeating a host in the lists + // isn't useful and is probably a mistake + // TODO: test this + hostSet := make(map[string]int) + for i, ap := range t.Automation.Policies { + for _, h := range ap.Hosts { + if first, ok := hostSet[h]; ok { + return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first) + } + hostSet[h] = i + } + } + } + return nil +} + // Start activates the TLS module. func (t *TLS) Start() error { // now that we are running, and all manual certificates have @@ -266,7 +286,10 @@ func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool { } // AddAutomationPolicy provisions and adds ap to the list of the app's -// automation policies. +// automation policies. If an existing automation policy exists that has +// fewer hosts in its list than ap does, ap will be inserted before that +// other policy (this helps ensure that ap will be prioritized/chosen +// over, say, a catch-all policy). func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { if t.Automation == nil { t.Automation = new(AutomationConfig) @@ -275,6 +298,16 @@ func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { if err != nil { return err } + for i, other := range t.Automation.Policies { + // if a catch-all policy (or really, any policy with + // fewer names) exists, prioritize this new policy + if len(other.Hosts) < len(ap.Hosts) { + t.Automation.Policies = append(t.Automation.Policies[:i], + append([]*AutomationPolicy{ap}, t.Automation.Policies[i+1:]...)...) + return nil + } + } + // otherwise just append the new one t.Automation.Policies = append(t.Automation.Policies, ap) return nil } @@ -444,6 +477,7 @@ type AutomationPolicy struct { // obtaining or renewing certificates. This is often // not desirable, especially when serving sites out // of your control. Default: false + // TODO: is this really necessary per-policy? why not a global setting... ManageSync bool `json:"manage_sync,omitempty"` Issuer certmagic.Issuer `json:"-"` @@ -510,8 +544,7 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error { OnDemand: ond, Storage: storage, } - cfg := certmagic.New(tlsApp.certCache, template) - ap.magic = cfg + ap.magic = certmagic.New(tlsApp.certCache, template) if ap.IssuerRaw != nil { val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw") @@ -527,12 +560,12 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error { // ACME challenges -- it's an annoying, inelegant circular // dependency that I don't know how to resolve nicely!) if configger, ok := ap.Issuer.(ConfigSetter); ok { - configger.SetConfig(cfg) + configger.SetConfig(ap.magic) } - cfg.Issuer = ap.Issuer + ap.magic.Issuer = ap.Issuer if rev, ok := ap.Issuer.(certmagic.Revoker); ok { - cfg.Revoker = rev + ap.magic.Revoker = rev } return nil @@ -789,3 +822,10 @@ func (t *TLS) moveCertificates() error { return nil } + +// Interface guards +var ( + _ caddy.Provisioner = (*TLS)(nil) + _ caddy.Validator = (*TLS)(nil) + _ caddy.App = (*TLS)(nil) +) -- cgit v1.2.3