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