certificates/authority/tls.go
Mariano Cano 9e5762fe06 Allow the reuse of azure token if DisableTrustOnFirstUse is true
Azure caches tokens for 24h and we cannot issue a new certificate
for the same instance in that period of time.

The meaning of this parameter is to allow the signing of multiple
certificate in one instance. This is possible in GCP, because we
get a new token, and is possible in AWS because we can generate
a new one. On Azure there was no other way to do it unless you
wait for 24h.

Fixes #656
2021-08-11 11:50:54 -07:00

493 lines
17 KiB
Go

package authority
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"net/http"
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/config"
"github.com/smallstep/certificates/authority/provisioner"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/errs"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"
)
// GetTLSOptions returns the tls options configured.
func (a *Authority) GetTLSOptions() *config.TLSOptions {
return a.config.TLS
}
var oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
var oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {
return func(crt *x509.Certificate, opts provisioner.SignOptions) error {
if def == nil {
return errors.New("default ASN1DN template cannot be nil")
}
if len(crt.Subject.Country) == 0 && def.Country != "" {
crt.Subject.Country = append(crt.Subject.Country, def.Country)
}
if len(crt.Subject.Organization) == 0 && def.Organization != "" {
crt.Subject.Organization = append(crt.Subject.Organization, def.Organization)
}
if len(crt.Subject.OrganizationalUnit) == 0 && def.OrganizationalUnit != "" {
crt.Subject.OrganizationalUnit = append(crt.Subject.OrganizationalUnit, def.OrganizationalUnit)
}
if len(crt.Subject.Locality) == 0 && def.Locality != "" {
crt.Subject.Locality = append(crt.Subject.Locality, def.Locality)
}
if len(crt.Subject.Province) == 0 && def.Province != "" {
crt.Subject.Province = append(crt.Subject.Province, def.Province)
}
if len(crt.Subject.StreetAddress) == 0 && def.StreetAddress != "" {
crt.Subject.StreetAddress = append(crt.Subject.StreetAddress, def.StreetAddress)
}
return nil
}
}
// Sign creates a signed certificate from a certificate signing request.
func (a *Authority) Sign(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
var (
certOptions []x509util.Option
certValidators []provisioner.CertificateValidator
certModifiers []provisioner.CertificateModifier
certEnforcers []provisioner.CertificateEnforcer
)
opts := []interface{}{errs.WithKeyVal("csr", csr), errs.WithKeyVal("signOptions", signOpts)}
if err := csr.CheckSignature(); err != nil {
return nil, errs.Wrap(http.StatusBadRequest, err, "authority.Sign; invalid certificate request", opts...)
}
// Set backdate with the configured value
signOpts.Backdate = a.config.AuthorityConfig.Backdate.Duration
for _, op := range extraOpts {
switch k := op.(type) {
// Adds new options to NewCertificate
case provisioner.CertificateOptions:
certOptions = append(certOptions, k.Options(signOpts)...)
// Validate the given certificate request.
case provisioner.CertificateRequestValidator:
if err := k.Valid(csr); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
// Validates the unsigned certificate template.
case provisioner.CertificateValidator:
certValidators = append(certValidators, k)
// Modifies a certificate before validating it.
case provisioner.CertificateModifier:
certModifiers = append(certModifiers, k)
// Modifies a certificate after validating it.
case provisioner.CertificateEnforcer:
certEnforcers = append(certEnforcers, k)
default:
return nil, errs.InternalServer("authority.Sign; invalid extra option type %T", append([]interface{}{k}, opts...)...)
}
}
cert, err := x509util.NewCertificate(csr, certOptions...)
if err != nil {
if _, ok := err.(*x509util.TemplateError); ok {
return nil, errs.NewErr(http.StatusBadRequest, err,
errs.WithMessage(err.Error()),
errs.WithKeyVal("csr", csr),
errs.WithKeyVal("signOptions", signOpts),
)
}
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign", opts...)
}
// Certificate modifiers before validation
leaf := cert.GetCertificate()
// Set default subject
if err := withDefaultASN1DN(a.config.AuthorityConfig.Template).Modify(leaf, signOpts); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
for _, m := range certModifiers {
if err := m.Modify(leaf, signOpts); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
}
// Certificate validation.
for _, v := range certValidators {
if err := v.Valid(leaf, signOpts); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
}
// Certificate modifiers after validation
for _, m := range certEnforcers {
if err := m.Enforce(leaf); err != nil {
return nil, errs.Wrap(http.StatusUnauthorized, err, "authority.Sign", opts...)
}
}
lifetime := leaf.NotAfter.Sub(leaf.NotBefore.Add(signOpts.Backdate))
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
Template: leaf,
CSR: csr,
Lifetime: lifetime,
Backdate: signOpts.Backdate,
})
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Sign; error creating certificate", opts...)
}
fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)
if err = a.storeCertificate(fullchain); err != nil {
if err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err,
"authority.Sign; error storing certificate in db", opts...)
}
}
return fullchain, nil
}
// Renew creates a new Certificate identical to the old certificate, except
// with a validity window that begins 'now'.
func (a *Authority) Renew(oldCert *x509.Certificate) ([]*x509.Certificate, error) {
return a.Rekey(oldCert, nil)
}
// Rekey is used for rekeying and renewing based on the public key.
// If the public key is 'nil' then it's assumed that the cert should be renewed
// using the existing public key. If the public key is not 'nil' then it's
// assumed that the cert should be rekeyed.
// For both Rekey and Renew all other attributes of the new certificate should
// match the old certificate. The exceptions are 'AuthorityKeyId' (which may
// have changed), 'SubjectKeyId' (different in case of rekey), and
// 'NotBefore/NotAfter' (the validity duration of the new certificate should be
// equal to the old one, but starting 'now').
func (a *Authority) Rekey(oldCert *x509.Certificate, pk crypto.PublicKey) ([]*x509.Certificate, error) {
isRekey := (pk != nil)
opts := []interface{}{errs.WithKeyVal("serialNumber", oldCert.SerialNumber.String())}
// Check step provisioner extensions
if err := a.authorizeRenew(oldCert); err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...)
}
// Durations
backdate := a.config.AuthorityConfig.Backdate.Duration
duration := oldCert.NotAfter.Sub(oldCert.NotBefore)
lifetime := duration - backdate
// Create new certificate from previous values.
// Issuer, NotBefore, NotAfter and SubjectKeyId will be set by the CAS.
newCert := &x509.Certificate{
Subject: oldCert.Subject,
KeyUsage: oldCert.KeyUsage,
UnhandledCriticalExtensions: oldCert.UnhandledCriticalExtensions,
ExtKeyUsage: oldCert.ExtKeyUsage,
UnknownExtKeyUsage: oldCert.UnknownExtKeyUsage,
BasicConstraintsValid: oldCert.BasicConstraintsValid,
IsCA: oldCert.IsCA,
MaxPathLen: oldCert.MaxPathLen,
MaxPathLenZero: oldCert.MaxPathLenZero,
OCSPServer: oldCert.OCSPServer,
IssuingCertificateURL: oldCert.IssuingCertificateURL,
PermittedDNSDomainsCritical: oldCert.PermittedDNSDomainsCritical,
PermittedEmailAddresses: oldCert.PermittedEmailAddresses,
DNSNames: oldCert.DNSNames,
EmailAddresses: oldCert.EmailAddresses,
IPAddresses: oldCert.IPAddresses,
URIs: oldCert.URIs,
PermittedDNSDomains: oldCert.PermittedDNSDomains,
ExcludedDNSDomains: oldCert.ExcludedDNSDomains,
PermittedIPRanges: oldCert.PermittedIPRanges,
ExcludedIPRanges: oldCert.ExcludedIPRanges,
ExcludedEmailAddresses: oldCert.ExcludedEmailAddresses,
PermittedURIDomains: oldCert.PermittedURIDomains,
ExcludedURIDomains: oldCert.ExcludedURIDomains,
CRLDistributionPoints: oldCert.CRLDistributionPoints,
PolicyIdentifiers: oldCert.PolicyIdentifiers,
}
if isRekey {
newCert.PublicKey = pk
} else {
newCert.PublicKey = oldCert.PublicKey
}
// Copy all extensions except:
//
// 1. Authority Key Identifier - This one might be different if we rotate
// the intermediate certificate and it will cause a TLS bad certificate
// error.
//
// 2. Subject Key Identifier, if rekey - For rekey, SubjectKeyIdentifier
// extension will be calculated for the new public key by
// x509util.CreateCertificate()
for _, ext := range oldCert.Extensions {
if ext.Id.Equal(oidAuthorityKeyIdentifier) {
continue
}
if ext.Id.Equal(oidSubjectKeyIdentifier) && isRekey {
newCert.SubjectKeyId = nil
continue
}
newCert.ExtraExtensions = append(newCert.ExtraExtensions, ext)
}
resp, err := a.x509CAService.RenewCertificate(&casapi.RenewCertificateRequest{
Template: newCert,
Lifetime: lifetime,
Backdate: backdate,
})
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey", opts...)
}
fullchain := append([]*x509.Certificate{resp.Certificate}, resp.CertificateChain...)
if err = a.storeRenewedCertificate(oldCert, fullchain); err != nil {
if err != db.ErrNotImplemented {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.Rekey; error storing certificate in db", opts...)
}
}
return fullchain, nil
}
// storeCertificate allows to use an extension of the db.AuthDB interface that
// can log the full chain of certificates.
//
// TODO: at some point we should replace the db.AuthDB interface to implement
// `StoreCertificate(...*x509.Certificate) error` instead of just
// `StoreCertificate(*x509.Certificate) error`.
func (a *Authority) storeCertificate(fullchain []*x509.Certificate) error {
if s, ok := a.db.(interface {
StoreCertificateChain(...*x509.Certificate) error
}); ok {
return s.StoreCertificateChain(fullchain...)
}
return a.db.StoreCertificate(fullchain[0])
}
// storeRenewedCertificate allows to use an extension of the db.AuthDB interface
// that can log if a certificate has been renewed or rekeyed.
//
// TODO: at some point we should implement this in the standard implementation.
func (a *Authority) storeRenewedCertificate(oldCert *x509.Certificate, fullchain []*x509.Certificate) error {
if s, ok := a.db.(interface {
StoreRenewedCertificate(*x509.Certificate, ...*x509.Certificate) error
}); ok {
return s.StoreRenewedCertificate(oldCert, fullchain...)
}
return a.db.StoreCertificate(fullchain[0])
}
// RevokeOptions are the options for the Revoke API.
type RevokeOptions struct {
Serial string
Reason string
ReasonCode int
PassiveOnly bool
MTLS bool
Crt *x509.Certificate
OTT string
}
// Revoke revokes a certificate.
//
// NOTE: Only supports passive revocation - prevent existing certificates from
// being renewed.
//
// TODO: Add OCSP and CRL support.
func (a *Authority) Revoke(ctx context.Context, revokeOpts *RevokeOptions) error {
opts := []interface{}{
errs.WithKeyVal("serialNumber", revokeOpts.Serial),
errs.WithKeyVal("reasonCode", revokeOpts.ReasonCode),
errs.WithKeyVal("reason", revokeOpts.Reason),
errs.WithKeyVal("passiveOnly", revokeOpts.PassiveOnly),
errs.WithKeyVal("MTLS", revokeOpts.MTLS),
errs.WithKeyVal("context", provisioner.MethodFromContext(ctx).String()),
}
if revokeOpts.MTLS {
opts = append(opts, errs.WithKeyVal("certificate", base64.StdEncoding.EncodeToString(revokeOpts.Crt.Raw)))
} else {
opts = append(opts, errs.WithKeyVal("token", revokeOpts.OTT))
}
rci := &db.RevokedCertificateInfo{
Serial: revokeOpts.Serial,
ReasonCode: revokeOpts.ReasonCode,
Reason: revokeOpts.Reason,
MTLS: revokeOpts.MTLS,
RevokedAt: time.Now().UTC(),
}
var (
p provisioner.Interface
err error
)
// If not mTLS then get the TokenID of the token.
if !revokeOpts.MTLS {
token, err := jose.ParseSigned(revokeOpts.OTT)
if err != nil {
return errs.Wrap(http.StatusUnauthorized, err,
"authority.Revoke; error parsing token", opts...)
}
// Get claims w/out verification.
var claims Claims
if err = token.UnsafeClaimsWithoutVerification(&claims); err != nil {
return errs.Wrap(http.StatusUnauthorized, err, "authority.Revoke", opts...)
}
// This method will also validate the audiences for JWK provisioners.
p, err = a.LoadProvisionerByToken(token, &claims.Claims)
if err != nil {
return err
}
rci.ProvisionerID = p.GetID()
rci.TokenID, err = p.GetTokenID(revokeOpts.OTT)
if err != nil && !errors.Is(err, provisioner.ErrAllowTokenReuse) {
return errs.Wrap(http.StatusInternalServerError, err,
"authority.Revoke; could not get ID for token")
}
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
opts = append(opts, errs.WithKeyVal("tokenID", rci.TokenID))
} else {
// Load the Certificate provisioner if one exists.
if p, err = a.LoadProvisionerByCertificate(revokeOpts.Crt); err == nil {
rci.ProvisionerID = p.GetID()
opts = append(opts, errs.WithKeyVal("provisionerID", rci.ProvisionerID))
}
}
if provisioner.MethodFromContext(ctx) == provisioner.SSHRevokeMethod {
err = a.db.RevokeSSH(rci)
} else {
// Revoke an X.509 certificate using CAS. If the certificate is not
// provided we will try to read it from the db. If the read fails we
// won't throw an error as it will be responsibility of the CAS
// implementation to require a certificate.
var revokedCert *x509.Certificate
if revokeOpts.Crt != nil {
revokedCert = revokeOpts.Crt
} else if rci.Serial != "" {
revokedCert, _ = a.db.GetCertificate(rci.Serial)
}
// CAS operation, note that SoftCAS (default) is a noop.
// The revoke happens when this is stored in the db.
_, err = a.x509CAService.RevokeCertificate(&casapi.RevokeCertificateRequest{
Certificate: revokedCert,
SerialNumber: rci.Serial,
Reason: rci.Reason,
ReasonCode: rci.ReasonCode,
PassiveOnly: revokeOpts.PassiveOnly,
})
if err != nil {
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
}
// Save as revoked in the Db.
err = a.db.Revoke(rci)
}
switch err {
case nil:
return nil
case db.ErrNotImplemented:
return errs.NotImplemented("authority.Revoke; no persistence layer configured", opts...)
case db.ErrAlreadyExists:
return errs.BadRequest("authority.Revoke; certificate with serial "+
"number %s has already been revoked", append([]interface{}{rci.Serial}, opts...)...)
default:
return errs.Wrap(http.StatusInternalServerError, err, "authority.Revoke", opts...)
}
}
// GetTLSCertificate creates a new leaf certificate to be used by the CA HTTPS server.
func (a *Authority) GetTLSCertificate() (*tls.Certificate, error) {
fatal := func(err error) (*tls.Certificate, error) {
return nil, errs.Wrap(http.StatusInternalServerError, err, "authority.GetTLSCertificate")
}
// Generate default key.
priv, err := keyutil.GenerateDefaultKey()
if err != nil {
return fatal(err)
}
signer, ok := priv.(crypto.Signer)
if !ok {
return fatal(errors.New("private key is not a crypto.Signer"))
}
// Create initial certificate request.
cr, err := x509util.CreateCertificateRequest("Step Online CA", a.config.DNSNames, signer)
if err != nil {
return fatal(err)
}
// Generate certificate template directly from the certificate request.
template, err := x509util.NewCertificate(cr)
if err != nil {
return fatal(err)
}
// Get x509 certificate template, set validity and sign it.
now := time.Now()
certTpl := template.GetCertificate()
certTpl.NotBefore = now.Add(-1 * time.Minute)
certTpl.NotAfter = now.Add(24 * time.Hour)
resp, err := a.x509CAService.CreateCertificate(&casapi.CreateCertificateRequest{
Template: certTpl,
CSR: cr,
Lifetime: 24 * time.Hour,
Backdate: 1 * time.Minute,
})
if err != nil {
return fatal(err)
}
// Generate PEM blocks to create tls.Certificate
pemBlocks := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: resp.Certificate.Raw,
})
for _, crt := range resp.CertificateChain {
pemBlocks = append(pemBlocks, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: crt.Raw,
})...)
}
keyPEM, err := pemutil.Serialize(priv)
if err != nil {
return fatal(err)
}
tlsCrt, err := tls.X509KeyPair(pemBlocks, pem.EncodeToMemory(keyPEM))
if err != nil {
return fatal(err)
}
// Set leaf certificate
tlsCrt.Leaf = resp.Certificate
return &tlsCrt, nil
}