mirror of https://github.com/caddyserver/caddy
204 lines
5.4 KiB
Go
204 lines
5.4 KiB
Go
// 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/x509"
|
|
"encoding/pem"
|
|
"time"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
"github.com/smallstep/certificates/authority/provisioner"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
|
)
|
|
|
|
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
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// 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 (iss *InternalIssuer) Provision(ctx caddy.Context) error {
|
|
iss.logger = ctx.Logger()
|
|
|
|
// set some defaults
|
|
if iss.CA == "" {
|
|
iss.CA = caddypki.DefaultCAID
|
|
}
|
|
|
|
// get a reference to the configured CA
|
|
appModule, err := ctx.App("pki")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pkiApp := appModule.(*caddypki.PKI)
|
|
ca, err := pkiApp.GetCA(ctx, iss.CA)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
iss.ca = ca
|
|
|
|
// set any other default values
|
|
if iss.Lifetime == 0 {
|
|
iss.Lifetime = caddy.Duration(defaultInternalCertLifetime)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IssuerKey returns the unique issuer key for the
|
|
// configured CA endpoint.
|
|
func (iss InternalIssuer) IssuerKey() string {
|
|
return iss.ca.ID
|
|
}
|
|
|
|
// Issue issues a certificate to satisfy the CSR.
|
|
func (iss InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
|
|
// prepare the signing authority
|
|
authCfg := caddypki.AuthorityConfig{
|
|
SignWithRoot: iss.SignWithRoot,
|
|
}
|
|
auth, err := iss.ca.NewAuthority(authCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get the cert (public key) that will be used for signing
|
|
var issuerCert *x509.Certificate
|
|
if iss.SignWithRoot {
|
|
issuerCert = iss.ca.RootCertificate()
|
|
} else {
|
|
issuerCert = iss.ca.IntermediateCertificate()
|
|
}
|
|
|
|
// ensure issued certificate does not expire later than its issuer
|
|
lifetime := time.Duration(iss.Lifetime)
|
|
if time.Now().Add(lifetime).After(issuerCert.NotAfter) {
|
|
lifetime = time.Until(issuerCert.NotAfter)
|
|
iss.logger.Warn("cert lifetime would exceed issuer NotAfter, clamping lifetime",
|
|
zap.Duration("orig_lifetime", time.Duration(iss.Lifetime)),
|
|
zap.Duration("lifetime", lifetime),
|
|
zap.Time("not_after", issuerCert.NotAfter),
|
|
)
|
|
}
|
|
|
|
certChain, err := auth.SignWithContext(ctx, csr, provisioner.SignOptions{}, customCertLifetime(caddy.Duration(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
|
|
}
|
|
|
|
// UnmarshalCaddyfile deserializes Caddyfile tokens into iss.
|
|
//
|
|
// ... internal {
|
|
// ca <name>
|
|
// lifetime <duration>
|
|
// sign_with_root
|
|
// }
|
|
func (iss *InternalIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
d.Next() // consume issuer name
|
|
for d.NextBlock(0) {
|
|
switch d.Val() {
|
|
case "ca":
|
|
if !d.AllArgs(&iss.CA) {
|
|
return d.ArgErr()
|
|
}
|
|
|
|
case "lifetime":
|
|
if !d.NextArg() {
|
|
return d.ArgErr()
|
|
}
|
|
dur, err := caddy.ParseDuration(d.Val())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
iss.Lifetime = caddy.Duration(dur)
|
|
|
|
case "sign_with_root":
|
|
if d.NextArg() {
|
|
return d.ArgErr()
|
|
}
|
|
iss.SignWithRoot = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// customCertLifetime allows us to customize certificates that are issued
|
|
// by Smallstep libs, particularly the NotBefore & NotAfter dates.
|
|
type customCertLifetime time.Duration
|
|
|
|
func (d customCertLifetime) Modify(cert *x509.Certificate, _ provisioner.SignOptions) error {
|
|
cert.NotBefore = time.Now()
|
|
cert.NotAfter = cert.NotBefore.Add(time.Duration(d))
|
|
return nil
|
|
}
|
|
|
|
const defaultInternalCertLifetime = 12 * time.Hour
|
|
|
|
// Interface guards
|
|
var (
|
|
_ caddy.Provisioner = (*InternalIssuer)(nil)
|
|
_ certmagic.Issuer = (*InternalIssuer)(nil)
|
|
_ provisioner.CertificateModifier = (*customCertLifetime)(nil)
|
|
)
|