summaryrefslogtreecommitdiff
path: root/modules/caddytls
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2020-03-13 11:06:08 -0600
committerGitHub <noreply@github.com>2020-03-13 11:06:08 -0600
commit5a19db5dc2db7c02d0f99630a07a64cacb7f7b44 (patch)
treed820ee2920d97d7cf2faf0fd9541156e20c88d60 /modules/caddytls
parentcfe85a9fe625fea55dc4f809fd91b5c061064508 (diff)
v2: Implement 'pki' app powered by Smallstep for localhost certificates (#3125)
* pki: Initial commit of PKI app (WIP) (see #2502 and #3021) * pki: Ability to use root/intermediates, and sign with root * pki: Fix benign misnamings left over from copy+paste * pki: Only install root if not already trusted * Make HTTPS port the default; all names use auto-HTTPS; bug fixes * Fix build - what happened to our CI tests?? * Fix go.mod
Diffstat (limited to 'modules/caddytls')
-rw-r--r--modules/caddytls/acmeissuer.go5
-rw-r--r--modules/caddytls/internalissuer.go199
-rw-r--r--modules/caddytls/tls.go52
3 files changed, 248 insertions, 8 deletions
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)
+)