summaryrefslogtreecommitdiff
path: root/modules/caddytls/internalissuer.go
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/internalissuer.go
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/internalissuer.go')
-rw-r--r--modules/caddytls/internalissuer.go199
1 files changed, 199 insertions, 0 deletions
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)
+)