Remove CFSSL issuance path (#5347)
Make the `NonCFSSLSigner` code path the only code path through the CA. Remove all code related to the old, CFSSL-based code path. Update tests to supply (or mock) issuers of the new kind. Remove or simplify a few tests that were testing for behavior only exhibited by the old code path, such as incrementing certain metrics. Remove code from `//cmd/` for initializing the CFSSL library. Finally, mark the `NonCFSSLSigner` feature flag itself as deprecated. Delete the portions of the vendored CFSSL code which were only used by these deleted code paths. This does not remove the CFSSL library entirely, the rest of the cleanup will follow shortly. Part of #5115
This commit is contained in:
parent
26c65e7f1f
commit
bfd3f83717
376
ca/ca.go
376
ca/ca.go
|
|
@ -1,17 +1,11 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
|
@ -19,10 +13,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/beeker1121/goque"
|
||||
cfsslConfig "github.com/cloudflare/cfssl/config"
|
||||
cferr "github.com/cloudflare/cfssl/errors"
|
||||
"github.com/cloudflare/cfssl/signer"
|
||||
"github.com/cloudflare/cfssl/signer/local"
|
||||
ct "github.com/google/certificate-transparency-go"
|
||||
cttls "github.com/google/certificate-transparency-go/tls"
|
||||
"github.com/jmhodges/clock"
|
||||
|
|
@ -42,50 +32,6 @@ import (
|
|||
sapb "github.com/letsencrypt/boulder/sa/proto"
|
||||
)
|
||||
|
||||
// Miscellaneous PKIX OIDs that we need to refer to
|
||||
var (
|
||||
// X.509 Extensions
|
||||
oidAuthorityInfoAccess = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 1}
|
||||
oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
|
||||
oidBasicConstraints = asn1.ObjectIdentifier{2, 5, 29, 19}
|
||||
oidCertificatePolicies = asn1.ObjectIdentifier{2, 5, 29, 32}
|
||||
oidCrlDistributionPoints = asn1.ObjectIdentifier{2, 5, 29, 31}
|
||||
oidExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
|
||||
oidKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15}
|
||||
oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
|
||||
oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
|
||||
oidTLSFeature = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
||||
|
||||
// CSR attribute requesting extensions
|
||||
oidExtensionRequest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 14}
|
||||
)
|
||||
|
||||
// OID and fixed value for the "must staple" variant of the TLS Feature
|
||||
// extension:
|
||||
//
|
||||
// Features ::= SEQUENCE OF INTEGER [RFC7633]
|
||||
// enum { ... status_request(5) ...} ExtensionType; [RFC6066]
|
||||
//
|
||||
// DER Encoding:
|
||||
// 30 03 - SEQUENCE (3 octets)
|
||||
// |-- 02 01 - INTEGER (1 octet)
|
||||
// | |-- 05 - 5
|
||||
var (
|
||||
mustStapleFeatureValue = []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
||||
mustStapleExtension = signer.Extension{
|
||||
ID: cfsslConfig.OID(oidTLSFeature),
|
||||
Critical: false,
|
||||
Value: hex.EncodeToString(mustStapleFeatureValue),
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc6962#section-3.1
|
||||
ctPoisonExtension = signer.Extension{
|
||||
ID: cfsslConfig.OID(signer.CTPoisonOID),
|
||||
Critical: true,
|
||||
Value: "0500", // ASN.1 DER NULL, Hex encoded.
|
||||
}
|
||||
)
|
||||
|
||||
// Metrics for CA statistics
|
||||
const (
|
||||
csrExtensionCategory = "category"
|
||||
|
|
@ -131,8 +77,6 @@ type CertificateAuthorityImpl struct {
|
|||
pa core.PolicyAuthority
|
||||
issuers issuerMaps
|
||||
ecdsaAllowedRegIDs map[int64]bool
|
||||
cfsslRSAProfile string
|
||||
cfsslECDSAProfile string
|
||||
prefix int // Prepended to the serial number
|
||||
validityPeriod time.Duration
|
||||
backdate time.Duration
|
||||
|
|
@ -156,22 +100,12 @@ type Issuer struct {
|
|||
Cert *issuance.Certificate
|
||||
}
|
||||
|
||||
// localSigner is an interface describing the functions of a cfssl.local.Signer
|
||||
// that the Boulder CA uses. It allows mocking the local.Signer in unit tests.
|
||||
type localSigner interface {
|
||||
Sign(signer.SignRequest) ([]byte, error)
|
||||
SignFromPrecert(*x509.Certificate, []ct.SignedCertificateTimestamp) ([]byte, error)
|
||||
}
|
||||
|
||||
// internalIssuer represents the fully initialized internal state for a single
|
||||
// issuer, including the cfssl signer and OCSP signer objects.
|
||||
// issuer, including the OCSP signer object.
|
||||
// TODO(#5086): Remove the ocsp-specific pieces of this as we factor OCSP out.
|
||||
type internalIssuer struct {
|
||||
cert *issuance.Certificate
|
||||
ocspSigner crypto.Signer
|
||||
|
||||
// Only one of cfsslSigner and boulderIssuer will be non-nill
|
||||
cfsslSigner localSigner
|
||||
cert *issuance.Certificate
|
||||
ocspSigner crypto.Signer
|
||||
boulderIssuer *issuance.Issuer
|
||||
}
|
||||
|
||||
|
|
@ -203,61 +137,12 @@ func makeInternalIssuers(issuers []*issuance.Issuer, lifespanOCSP time.Duration)
|
|||
return issuerMaps{issuersByAlg, issuersByName, issuersByID, issuersByNameID}, nil
|
||||
}
|
||||
|
||||
func makeCFSSLInternalIssuers(issuers []Issuer, policy *cfsslConfig.Signing, lifespanOCSP time.Duration) (issuerMaps, error) {
|
||||
if len(issuers) == 0 {
|
||||
return issuerMaps{}, errors.New("No issuers specified.")
|
||||
}
|
||||
issuersByAlg := make(map[x509.PublicKeyAlgorithm]*internalIssuer, len(issuers))
|
||||
issuersByName := make(map[string]*internalIssuer, len(issuers))
|
||||
issuersByID := make(map[issuance.IssuerID]*internalIssuer, len(issuers))
|
||||
issuersByNameID := make(map[issuance.IssuerNameID]*internalIssuer, len(issuers))
|
||||
for idx, iss := range issuers {
|
||||
if iss.Cert == nil || iss.Signer == nil {
|
||||
return issuerMaps{}, errors.New("Issuer with nil cert or signer specified.")
|
||||
}
|
||||
cfsslSigner, err := local.NewSigner(iss.Signer, iss.Cert.Certificate, x509.SHA256WithRSA, policy)
|
||||
if err != nil {
|
||||
return issuerMaps{}, err
|
||||
}
|
||||
cn := iss.Cert.Subject.CommonName
|
||||
if issuersByName[cn] != nil {
|
||||
return issuerMaps{}, errors.New("Multiple issuer certs with the same CommonName are not supported")
|
||||
}
|
||||
|
||||
ii := &internalIssuer{
|
||||
cert: iss.Cert,
|
||||
cfsslSigner: cfsslSigner,
|
||||
ocspSigner: iss.Signer,
|
||||
}
|
||||
|
||||
// Rather than reading a config to pick which issuer to use for each alg,
|
||||
// just fall back to our old behavior of "the first issuer is used by default
|
||||
// for everything". Ensure that the first issuer is an RSA key so that signing
|
||||
// with x509.SHA256WithRSA doesn't break.
|
||||
if idx == 0 {
|
||||
if iss.Cert.PublicKeyAlgorithm != x509.RSA {
|
||||
return issuerMaps{}, errors.New("Default (first) issuer must be RSA when using CFSSL")
|
||||
}
|
||||
issuersByAlg[x509.RSA] = ii
|
||||
issuersByAlg[x509.ECDSA] = ii
|
||||
}
|
||||
issuersByName[cn] = ii
|
||||
issuersByID[iss.Cert.ID()] = ii
|
||||
issuersByNameID[iss.Cert.NameID()] = ii
|
||||
}
|
||||
return issuerMaps{issuersByAlg, issuersByName, issuersByID, issuersByNameID}, nil
|
||||
}
|
||||
|
||||
// NewCertificateAuthorityImpl creates a CA instance that can sign certificates
|
||||
// from a single issuer (the first first in the issuers slice), and can sign OCSP
|
||||
// for any of the issuer certificates provided.
|
||||
func NewCertificateAuthorityImpl(
|
||||
sa certificateStorage,
|
||||
pa core.PolicyAuthority,
|
||||
cfsslProfiles cfsslConfig.Config,
|
||||
cfsslRSAProfile string,
|
||||
cfsslECDSAProfile string,
|
||||
cfsslIssuers []Issuer,
|
||||
boulderIssuers []*issuance.Issuer,
|
||||
ecdsaAllowedRegIDs []int64,
|
||||
certExpiry time.Duration,
|
||||
|
|
@ -287,42 +172,9 @@ func NewCertificateAuthorityImpl(
|
|||
err = errors.New("Must have a positive non-zero serial prefix less than 256 for CA.")
|
||||
return nil, err
|
||||
}
|
||||
var issuers issuerMaps
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
issuers, err = makeInternalIssuers(boulderIssuers, ocspLifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// CFSSL requires processing JSON configs through its own LoadConfig, so we
|
||||
// serialize and then deserialize.
|
||||
cfsslJSON, err := json.Marshal(cfsslProfiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfsslConfigObj, err := cfsslConfig.LoadConfig(cfsslJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ocspLifetime == 0 {
|
||||
return nil, errors.New("Config must specify an OCSP lifespan period.")
|
||||
}
|
||||
|
||||
for _, profile := range cfsslConfigObj.Signing.Profiles {
|
||||
if len(profile.IssuerURL) > 1 {
|
||||
return nil, errors.New("only one issuer_url supported")
|
||||
}
|
||||
}
|
||||
|
||||
issuers, err = makeCFSSLInternalIssuers(cfsslIssuers, cfsslConfigObj.Signing, ocspLifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfsslRSAProfile == "" || cfsslECDSAProfile == "" {
|
||||
return nil, errors.New("must specify rsaProfile and ecdsaProfile")
|
||||
}
|
||||
issuers, err := makeInternalIssuers(boulderIssuers, ocspLifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ecdsaAllowedRegIDsMap := make(map[int64]bool, len(ecdsaAllowedRegIDs))
|
||||
|
|
@ -377,8 +229,6 @@ func NewCertificateAuthorityImpl(
|
|||
sa: sa,
|
||||
pa: pa,
|
||||
issuers: issuers,
|
||||
cfsslRSAProfile: cfsslRSAProfile,
|
||||
cfsslECDSAProfile: cfsslECDSAProfile,
|
||||
ecdsaAllowedRegIDs: ecdsaAllowedRegIDsMap,
|
||||
validityPeriod: certExpiry,
|
||||
backdate: certBackdate,
|
||||
|
|
@ -400,85 +250,14 @@ func NewCertificateAuthorityImpl(
|
|||
return ca, nil
|
||||
}
|
||||
|
||||
// noteSignError is called after operations that may cause a CFSSL
|
||||
// or PKCS11 signing error.
|
||||
// noteSignError is called after operations that may cause a PKCS11 signing error.
|
||||
func (ca *CertificateAuthorityImpl) noteSignError(err error) {
|
||||
var pkcs11Error *pkcs11.Error
|
||||
var cfErr *cferr.Error
|
||||
if errors.As(err, &pkcs11Error) {
|
||||
ca.signErrorCounter.WithLabelValues("HSM").Inc()
|
||||
} else if errors.As(err, &cfErr) {
|
||||
ca.signErrorCounter.WithLabelValues(fmt.Sprintf("CFSSL %d", cfErr.ErrorCode)).Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// Extract supported extensions from a CSR. The following extensions are
|
||||
// currently supported:
|
||||
//
|
||||
// * 1.3.6.1.5.5.7.1.24 - TLS Feature [RFC7633], with the "must staple" value.
|
||||
// Any other value will result in an error.
|
||||
//
|
||||
// Other requested extensions are silently ignored.
|
||||
func (ca *CertificateAuthorityImpl) extensionsFromCSR(csr *x509.CertificateRequest) ([]signer.Extension, error) {
|
||||
extensions := []signer.Extension{}
|
||||
|
||||
extensionSeen := map[string]bool{}
|
||||
hasBasic := false
|
||||
hasOther := false
|
||||
|
||||
for _, attr := range csr.Attributes {
|
||||
if !attr.Type.Equal(oidExtensionRequest) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, extList := range attr.Value {
|
||||
for _, ext := range extList {
|
||||
if extensionSeen[ext.Type.String()] {
|
||||
// Ignore duplicate certificate extensions
|
||||
continue
|
||||
}
|
||||
extensionSeen[ext.Type.String()] = true
|
||||
|
||||
switch {
|
||||
case ext.Type.Equal(oidTLSFeature):
|
||||
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionTLSFeature}).Inc()
|
||||
value, ok := ext.Value.([]byte)
|
||||
if !ok {
|
||||
return nil, berrors.MalformedError("malformed extension with OID %v", ext.Type)
|
||||
} else if !bytes.Equal(value, mustStapleFeatureValue) {
|
||||
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionTLSFeatureInvalid}).Inc()
|
||||
return nil, berrors.MalformedError("unsupported value for extension with OID %v", ext.Type)
|
||||
}
|
||||
|
||||
extensions = append(extensions, mustStapleExtension)
|
||||
case ext.Type.Equal(oidAuthorityInfoAccess),
|
||||
ext.Type.Equal(oidAuthorityKeyIdentifier),
|
||||
ext.Type.Equal(oidBasicConstraints),
|
||||
ext.Type.Equal(oidCertificatePolicies),
|
||||
ext.Type.Equal(oidCrlDistributionPoints),
|
||||
ext.Type.Equal(oidExtKeyUsage),
|
||||
ext.Type.Equal(oidKeyUsage),
|
||||
ext.Type.Equal(oidSubjectAltName),
|
||||
ext.Type.Equal(oidSubjectKeyIdentifier):
|
||||
hasBasic = true
|
||||
default:
|
||||
hasOther = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasBasic {
|
||||
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionBasic}).Inc()
|
||||
}
|
||||
|
||||
if hasOther {
|
||||
ca.csrExtensionCount.With(prometheus.Labels{csrExtensionCategory: csrExtensionOther}).Inc()
|
||||
}
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
var ocspStatusToCode = map[string]int{
|
||||
"good": ocsp.Good,
|
||||
"revoked": ocsp.Revoked,
|
||||
|
|
@ -688,28 +467,13 @@ func (ca *CertificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
|
|||
return nil, berrors.InternalServerError("no issuer found for Issuer Name %s", precert.Issuer)
|
||||
}
|
||||
|
||||
var certDER []byte
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
issuanceReq, err := issuance.RequestFromPrecert(precert, scts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certDER, err = issuer.boulderIssuer.Issue(issuanceReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
certPEM, err := issuer.cfsslSigner.SignFromPrecert(precert, scts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
err = berrors.InternalServerError("invalid certificate value returned")
|
||||
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
||||
return nil, err
|
||||
}
|
||||
certDER = block.Bytes
|
||||
issuanceReq, err := issuance.RequestFromPrecert(precert, scts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certDER, err := issuer.boulderIssuer.Issue(issuanceReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ca.signatureCount.WithLabelValues(string(certType)).Inc()
|
||||
ca.log.AuditInfof("Signing success: serial=[%s] names=[%s] csr=[%s] certificate=[%s]",
|
||||
|
|
@ -777,11 +541,6 @@ func (ca *CertificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
extensions, err := ca.extensionsFromCSR(csr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var issuer *internalIssuer
|
||||
var ok bool
|
||||
if issueReq.IssuerNameID == 0 {
|
||||
|
|
@ -811,96 +570,23 @@ func (ca *CertificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
|
|||
|
||||
serialHex := core.SerialToString(serialBigInt)
|
||||
|
||||
var certDER []byte
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
||||
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
||||
certDER, err = issuer.boulderIssuer.Issue(&issuance.IssuanceRequest{
|
||||
PublicKey: csr.PublicKey,
|
||||
Serial: serialBigInt.Bytes(),
|
||||
CommonName: csr.Subject.CommonName,
|
||||
DNSNames: csr.DNSNames,
|
||||
IncludeCTPoison: true,
|
||||
IncludeMustStaple: issuance.ContainsMustStaple(csr.Extensions),
|
||||
NotBefore: validity.NotBefore,
|
||||
NotAfter: validity.NotAfter,
|
||||
})
|
||||
ca.noteSignError(err)
|
||||
if err != nil {
|
||||
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
} else {
|
||||
// Convert the CSR to PEM
|
||||
csrPEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr.Raw,
|
||||
}))
|
||||
|
||||
var profile string
|
||||
switch csr.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
profile = ca.cfsslRSAProfile
|
||||
case *ecdsa.PublicKey:
|
||||
profile = ca.cfsslECDSAProfile
|
||||
default:
|
||||
err = berrors.InternalServerError("unsupported key type %T", csr.PublicKey)
|
||||
ca.log.AuditErr(err.Error())
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Send the cert off for signing
|
||||
req := signer.SignRequest{
|
||||
Request: csrPEM,
|
||||
Profile: profile,
|
||||
Hosts: csr.DNSNames,
|
||||
Subject: &signer.Subject{
|
||||
CN: csr.Subject.CommonName,
|
||||
},
|
||||
Serial: serialBigInt,
|
||||
Extensions: extensions,
|
||||
NotBefore: validity.NotBefore,
|
||||
NotAfter: validity.NotAfter,
|
||||
ReturnPrecert: true,
|
||||
}
|
||||
|
||||
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
||||
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
||||
|
||||
certPEM, err := issuer.cfsslSigner.Sign(req)
|
||||
ca.noteSignError(err)
|
||||
if err != nil {
|
||||
// If the Signing error was a pre-issuance lint error then marshal the
|
||||
// linting errors to include in the audit err msg.
|
||||
if lErr, ok := err.(*local.LintError); ok {
|
||||
// NOTE(@cpu): We throw away the JSON marshal error here. If marshaling
|
||||
// fails for some reason it's acceptable to log an empty string for the
|
||||
// JSON component.
|
||||
lintErrsJSON, _ := json.Marshal(lErr.ErrorResults)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v] lintErrors=%s",
|
||||
serialHex, err, string(lintErrsJSON))
|
||||
return nil, nil, berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
}
|
||||
|
||||
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(certPEM) == 0 {
|
||||
err = berrors.InternalServerError("no certificate returned by server")
|
||||
ca.log.AuditErrf("PEM empty from Signer: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
err = berrors.InternalServerError("invalid certificate value returned")
|
||||
ca.log.AuditErrf("PEM decode error, aborting: serial=[%s] pem=[%s] err=[%v]", serialHex, certPEM, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
certDER = block.Bytes
|
||||
ca.log.AuditInfof("Signing: serial=[%s] names=[%s] csr=[%s]",
|
||||
serialHex, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw))
|
||||
certDER, err := issuer.boulderIssuer.Issue(&issuance.IssuanceRequest{
|
||||
PublicKey: csr.PublicKey,
|
||||
Serial: serialBigInt.Bytes(),
|
||||
CommonName: csr.Subject.CommonName,
|
||||
DNSNames: csr.DNSNames,
|
||||
IncludeCTPoison: true,
|
||||
IncludeMustStaple: issuance.ContainsMustStaple(csr.Extensions),
|
||||
NotBefore: validity.NotBefore,
|
||||
NotAfter: validity.NotAfter,
|
||||
})
|
||||
ca.noteSignError(err)
|
||||
if err != nil {
|
||||
err = berrors.InternalServerError("failed to sign certificate: %s", err)
|
||||
ca.log.AuditErrf("Signing failed: serial=[%s] err=[%v]", serialHex, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
ca.signatureCount.WithLabelValues(string(precertType)).Inc()
|
||||
|
||||
|
|
|
|||
719
ca/ca_test.go
719
ca/ca_test.go
|
|
@ -19,15 +19,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/beeker1121/goque"
|
||||
cfsslConfig "github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/signer"
|
||||
"github.com/cloudflare/cfssl/signer/local"
|
||||
ct "github.com/google/certificate-transparency-go"
|
||||
cttls "github.com/google/certificate-transparency-go/tls"
|
||||
"github.com/jmhodges/clock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
zlint "github.com/zmap/zlint/v2/lint"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
capb "github.com/letsencrypt/boulder/ca/proto"
|
||||
|
|
@ -98,6 +94,9 @@ var (
|
|||
// OIDExtensionCTPoison is defined in RFC 6962 s3.1.
|
||||
OIDExtensionCTPoison = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
|
||||
|
||||
// OIDExtensionSCTList is defined in RFC 6962 s3.3.
|
||||
OIDExtensionSCTList = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
|
||||
|
||||
// The "certificate-for-precertificate" tests use the precertificate from a
|
||||
// previous "precertificate" test, in order to verify that the CA is
|
||||
// stateless with respect to these two operations, since a separate CA
|
||||
|
|
@ -117,9 +116,7 @@ var (
|
|||
|
||||
const arbitraryRegID int64 = 1001
|
||||
|
||||
// CFSSL config
|
||||
const rsaProfileName = "rsaEE"
|
||||
const ecdsaProfileName = "ecdsaEE"
|
||||
// Useful key and certificate files.
|
||||
const caKeyFile = "../test/test-ca.key"
|
||||
const caCertFile = "../test/test-ca.pem"
|
||||
const caCertFile2 = "../test/test-ca2.pem"
|
||||
|
|
@ -133,21 +130,17 @@ func mustRead(path string) []byte {
|
|||
}
|
||||
|
||||
type testCtx struct {
|
||||
pa core.PolicyAuthority
|
||||
certExpiry time.Duration
|
||||
certBackdate time.Duration
|
||||
serialPrefix int
|
||||
maxNames int
|
||||
ocspLifetime time.Duration
|
||||
cfsslProfiles cfsslConfig.Config
|
||||
cfsslRSAProfile string
|
||||
cfsslECDSAProfile string
|
||||
cfsslIssuers []Issuer
|
||||
boulderIssuers []*issuance.Issuer
|
||||
keyPolicy goodkey.KeyPolicy
|
||||
fc clock.FakeClock
|
||||
stats prometheus.Registerer
|
||||
logger *blog.Mock
|
||||
pa core.PolicyAuthority
|
||||
certExpiry time.Duration
|
||||
certBackdate time.Duration
|
||||
serialPrefix int
|
||||
maxNames int
|
||||
ocspLifetime time.Duration
|
||||
boulderIssuers []*issuance.Issuer
|
||||
keyPolicy goodkey.KeyPolicy
|
||||
fc clock.FakeClock
|
||||
stats prometheus.Registerer
|
||||
logger *blog.Mock
|
||||
}
|
||||
|
||||
type mockSA struct {
|
||||
|
|
@ -202,64 +195,6 @@ func setup(t *testing.T) *testCtx {
|
|||
err = pa.SetHostnamePolicyFile("../test/hostname-policy.yaml")
|
||||
test.AssertNotError(t, err, "Couldn't set hostname policy")
|
||||
|
||||
allowedExtensions := []cfsslConfig.OID{
|
||||
cfsslConfig.OID(oidTLSFeature),
|
||||
cfsslConfig.OID(OIDExtensionCTPoison),
|
||||
}
|
||||
|
||||
cfsslProfiles := cfsslConfig.Config{
|
||||
Signing: &cfsslConfig.Signing{
|
||||
Profiles: map[string]*cfsslConfig.SigningProfile{
|
||||
rsaProfileName: {
|
||||
Usage: []string{"digital signature", "key encipherment", "server auth"},
|
||||
IssuerURL: []string{"http://not-example.com/issuer-url"},
|
||||
OCSP: "http://not-example.com/ocsp",
|
||||
CRL: "http://not-example.com/crl",
|
||||
|
||||
Policies: []cfsslConfig.CertificatePolicy{
|
||||
{
|
||||
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
|
||||
},
|
||||
},
|
||||
ExpiryString: "8760h",
|
||||
Backdate: time.Hour,
|
||||
CSRWhitelist: &cfsslConfig.CSRWhitelist{
|
||||
PublicKeyAlgorithm: true,
|
||||
PublicKey: true,
|
||||
SignatureAlgorithm: true,
|
||||
},
|
||||
ClientProvidesSerialNumbers: true,
|
||||
AllowedExtensions: allowedExtensions,
|
||||
},
|
||||
ecdsaProfileName: {
|
||||
Usage: []string{"digital signature", "server auth"},
|
||||
IssuerURL: []string{"http://not-example.com/issuer-url"},
|
||||
OCSP: "http://not-example.com/ocsp",
|
||||
CRL: "http://not-example.com/crl",
|
||||
|
||||
Policies: []cfsslConfig.CertificatePolicy{
|
||||
{
|
||||
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
|
||||
},
|
||||
},
|
||||
ExpiryString: "8760h",
|
||||
Backdate: time.Hour,
|
||||
CSRWhitelist: &cfsslConfig.CSRWhitelist{
|
||||
PublicKeyAlgorithm: true,
|
||||
PublicKey: true,
|
||||
SignatureAlgorithm: true,
|
||||
},
|
||||
ClientProvidesSerialNumbers: true,
|
||||
AllowedExtensions: allowedExtensions,
|
||||
},
|
||||
},
|
||||
Default: &cfsslConfig.SigningProfile{
|
||||
ExpiryString: "8760h",
|
||||
},
|
||||
},
|
||||
}
|
||||
cfsslIssuers := []Issuer{{caKey, caCert}}
|
||||
|
||||
boulderProfile := func(rsa, ecdsa bool) *issuance.Profile {
|
||||
res, _ := issuance.NewProfile(
|
||||
issuance.ProfileConfig{
|
||||
|
|
@ -283,7 +218,7 @@ func setup(t *testing.T) *testCtx {
|
|||
)
|
||||
return res
|
||||
}
|
||||
boulderLinter, _ := lint.NewLinter(caKey, nil)
|
||||
boulderLinter, _ := lint.NewLinter(caKey, []string{"n_subject_common_name_included"})
|
||||
boulderIssuers := []*issuance.Issuer{
|
||||
// Must list ECDSA-only issuer first, so it is the default for ECDSA.
|
||||
{
|
||||
|
|
@ -309,21 +244,17 @@ func setup(t *testing.T) *testCtx {
|
|||
}
|
||||
|
||||
return &testCtx{
|
||||
pa: pa,
|
||||
certExpiry: 8760 * time.Hour,
|
||||
certBackdate: time.Hour,
|
||||
serialPrefix: 17,
|
||||
maxNames: 2,
|
||||
ocspLifetime: time.Hour,
|
||||
cfsslProfiles: cfsslProfiles,
|
||||
cfsslRSAProfile: rsaProfileName,
|
||||
cfsslECDSAProfile: ecdsaProfileName,
|
||||
cfsslIssuers: cfsslIssuers,
|
||||
boulderIssuers: boulderIssuers,
|
||||
keyPolicy: keyPolicy,
|
||||
fc: fc,
|
||||
stats: metrics.NoopRegisterer,
|
||||
logger: blog.NewMock(),
|
||||
pa: pa,
|
||||
certExpiry: 8760 * time.Hour,
|
||||
certBackdate: time.Hour,
|
||||
serialPrefix: 17,
|
||||
maxNames: 2,
|
||||
ocspLifetime: time.Hour,
|
||||
boulderIssuers: boulderIssuers,
|
||||
keyPolicy: keyPolicy,
|
||||
fc: fc,
|
||||
stats: metrics.NoopRegisterer,
|
||||
logger: blog.NewMock(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -333,10 +264,6 @@ func TestFailNoSerialPrefix(t *testing.T) {
|
|||
_, err := NewCertificateAuthorityImpl(
|
||||
nil,
|
||||
nil,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
|
|
@ -369,88 +296,74 @@ type IssuanceMode struct {
|
|||
}
|
||||
|
||||
func TestIssuePrecertificate(t *testing.T) {
|
||||
for _, nonCFSSL := range []bool{true, false} {
|
||||
testCases := []struct {
|
||||
name string
|
||||
csr []byte
|
||||
subTest func(t *testing.T, i *TestCertificateIssuance)
|
||||
}{
|
||||
{"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate},
|
||||
{"ValidityUsesCAClock", CNandSANCSR, issueCertificateSubTestValidityUsesCAClock},
|
||||
{"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA},
|
||||
{"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA},
|
||||
{"MustStaple", MustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"MustStapleDuplicate", DuplicateMustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
|
||||
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
csr []byte
|
||||
subTest func(t *testing.T, i *TestCertificateIssuance)
|
||||
}{
|
||||
{"IssuePrecertificate", CNandSANCSR, issueCertificateSubTestIssuePrecertificate},
|
||||
{"ValidityUsesCAClock", CNandSANCSR, issueCertificateSubTestValidityUsesCAClock},
|
||||
{"ProfileSelectionRSA", CNandSANCSR, issueCertificateSubTestProfileSelectionRSA},
|
||||
{"ProfileSelectionECDSA", ECDSACSR, issueCertificateSubTestProfileSelectionECDSA},
|
||||
{"MustStaple", MustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"MustStapleDuplicate", DuplicateMustStapleCSR, issueCertificateSubTestMustStaple},
|
||||
{"UnknownExtension", UnsupportedExtensionCSR, issueCertificateSubTestUnknownExtension},
|
||||
{"CTPoisonExtension", CTPoisonExtensionCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
{"CTPoisonExtensionEmpty", CTPoisonExtensionEmptyCSR, issueCertificateSubTestCTPoisonExtension},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
// The loop through |issuanceModes| must be inside the loop through
|
||||
// |testCases| because the "certificate-for-precertificate" tests use
|
||||
// the precertificates previously generated from the preceding
|
||||
// "precertificate" test. See also the comment above |issuanceModes|.
|
||||
for _, mode := range issuanceModes {
|
||||
ca, sa := issueCertificateSubTestSetup(t, nonCFSSL)
|
||||
for _, testCase := range testCases {
|
||||
// The loop through |issuanceModes| must be inside the loop through
|
||||
// |testCases| because the "certificate-for-precertificate" tests use
|
||||
// the precertificates previously generated from the preceding
|
||||
// "precertificate" test. See also the comment above |issuanceModes|.
|
||||
for _, mode := range issuanceModes {
|
||||
ca, sa := issueCertificateSubTestSetup(t)
|
||||
|
||||
t.Run(fmt.Sprintf("%s - %s (using boulder signer: %t)", mode.name, testCase.name, nonCFSSL), func(t *testing.T) {
|
||||
req, err := x509.ParseCertificateRequest(testCase.csr)
|
||||
test.AssertNotError(t, err, "Certificate request failed to parse")
|
||||
t.Run(fmt.Sprintf("%s - %s", mode.name, testCase.name), func(t *testing.T) {
|
||||
req, err := x509.ParseCertificateRequest(testCase.csr)
|
||||
test.AssertNotError(t, err, "Certificate request failed to parse")
|
||||
|
||||
issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: arbitraryRegID}
|
||||
issueReq := &capb.IssueCertificateRequest{Csr: testCase.csr, RegistrationID: arbitraryRegID}
|
||||
|
||||
var certDER []byte
|
||||
response, err := ca.IssuePrecertificate(ctx, issueReq)
|
||||
var certDER []byte
|
||||
response, err := ca.IssuePrecertificate(ctx, issueReq)
|
||||
|
||||
test.AssertNotError(t, err, "Failed to issue precertificate")
|
||||
certDER = response.DER
|
||||
test.AssertNotError(t, err, "Failed to issue precertificate")
|
||||
certDER = response.DER
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
|
||||
poisonExtension := findExtension(cert.Extensions, OIDExtensionCTPoison)
|
||||
test.AssertEquals(t, true, poisonExtension != nil)
|
||||
if poisonExtension != nil {
|
||||
test.AssertEquals(t, poisonExtension.Critical, true)
|
||||
test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL
|
||||
}
|
||||
poisonExtension := findExtension(cert.Extensions, OIDExtensionCTPoison)
|
||||
test.AssertEquals(t, true, poisonExtension != nil)
|
||||
if poisonExtension != nil {
|
||||
test.AssertEquals(t, poisonExtension.Critical, true)
|
||||
test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL
|
||||
}
|
||||
|
||||
i := TestCertificateIssuance{
|
||||
ca: ca,
|
||||
sa: sa,
|
||||
req: req,
|
||||
mode: mode,
|
||||
certDER: certDER,
|
||||
cert: cert,
|
||||
}
|
||||
i := TestCertificateIssuance{
|
||||
ca: ca,
|
||||
sa: sa,
|
||||
req: req,
|
||||
mode: mode,
|
||||
certDER: certDER,
|
||||
cert: cert,
|
||||
}
|
||||
|
||||
testCase.subTest(t, &i)
|
||||
})
|
||||
}
|
||||
testCase.subTest(t, &i)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func issueCertificateSubTestSetup(t *testing.T, boulderIssuer bool) (*CertificateAuthorityImpl, *mockSA) {
|
||||
func issueCertificateSubTestSetup(t *testing.T) (*CertificateAuthorityImpl, *mockSA) {
|
||||
testCtx := setup(t)
|
||||
sa := &mockSA{}
|
||||
var cfsslIssuers []Issuer
|
||||
var boulderIssuers []*issuance.Issuer
|
||||
if boulderIssuer {
|
||||
boulderIssuers = testCtx.boulderIssuers
|
||||
_ = features.Set(map[string]bool{"NonCFSSLSigner": true})
|
||||
} else {
|
||||
cfsslIssuers = testCtx.cfsslIssuers
|
||||
}
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
cfsslIssuers,
|
||||
boulderIssuers,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -494,28 +407,11 @@ func issueCertificateSubTestValidityUsesCAClock(t *testing.T, i *TestCertificate
|
|||
// Test issuing when multiple issuers are present.
|
||||
func TestMultipleIssuers(t *testing.T) {
|
||||
testCtx := setup(t)
|
||||
// Load multiple issuers, and ensure the first one in the list is used.
|
||||
newIssuerCert, err := issuance.LoadCertificate("../test/test-ca2.pem")
|
||||
test.AssertNotError(t, err, "Failed to load new cert")
|
||||
newIssuers := []Issuer{
|
||||
{
|
||||
Signer: caKey,
|
||||
// newIssuerCert is first, so it will be the default.
|
||||
Cert: newIssuerCert,
|
||||
}, {
|
||||
Signer: caKey,
|
||||
Cert: caCert,
|
||||
},
|
||||
}
|
||||
sa := &mockSA{}
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
newIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -531,13 +427,20 @@ func TestMultipleIssuers(t *testing.T) {
|
|||
testCtx.fc)
|
||||
test.AssertNotError(t, err, "Failed to remake CA")
|
||||
|
||||
// Test that an RSA CSR gets issuance from the RSA issuer, caCert.
|
||||
issuedCert, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
|
||||
cert, err := x509.ParseCertificate(issuedCert.DER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
// Verify cert was signed by newIssuerCert, not caCert.
|
||||
err = cert.CheckSignatureFrom(newIssuerCert.Certificate)
|
||||
err = cert.CheckSignatureFrom(caCert2.Certificate)
|
||||
test.AssertNotError(t, err, "Certificate failed signature validation")
|
||||
|
||||
// Test that an ECDSA CSR gets issuance from the ECDSA issuer, caCert2.
|
||||
issuedCert, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID})
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
cert, err = x509.ParseCertificate(issuedCert.DER)
|
||||
test.AssertNotError(t, err, "Certificate failed to parse")
|
||||
err = cert.CheckSignatureFrom(caCert2.Certificate)
|
||||
test.AssertNotError(t, err, "Certificate failed signature validation")
|
||||
}
|
||||
|
||||
|
|
@ -545,7 +448,7 @@ func TestECDSAAllowList(t *testing.T) {
|
|||
req := &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID}
|
||||
|
||||
// With allowlist containing arbitraryRegID, issuance should come from ECDSA issuer.
|
||||
ca, _ := issueCertificateSubTestSetup(t, true)
|
||||
ca, _ := issueCertificateSubTestSetup(t)
|
||||
ca.ecdsaAllowedRegIDs[arbitraryRegID] = true
|
||||
result, err := ca.IssuePrecertificate(ctx, req)
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
|
|
@ -554,7 +457,7 @@ func TestECDSAAllowList(t *testing.T) {
|
|||
test.AssertByteEquals(t, cert.RawIssuer, caCert2.RawSubject)
|
||||
|
||||
// With allowlist not containing arbitraryRegID, issuance should fall back to RSA issuer.
|
||||
ca, _ = issueCertificateSubTestSetup(t, true)
|
||||
ca, _ = issueCertificateSubTestSetup(t)
|
||||
ca.ecdsaAllowedRegIDs[2002] = true
|
||||
result, err = ca.IssuePrecertificate(ctx, req)
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
|
|
@ -563,7 +466,7 @@ func TestECDSAAllowList(t *testing.T) {
|
|||
test.AssertByteEquals(t, cert.RawIssuer, caCert.RawSubject)
|
||||
|
||||
// With empty allowlist but ECDSAForAll enabled, issuance should come from ECDSA issuer.
|
||||
ca, _ = issueCertificateSubTestSetup(t, true)
|
||||
ca, _ = issueCertificateSubTestSetup(t)
|
||||
_ = features.Set(map[string]bool{"ECDSAForAll": true})
|
||||
defer features.Reset()
|
||||
result, err = ca.IssuePrecertificate(ctx, req)
|
||||
|
|
@ -579,11 +482,7 @@ func TestOCSP(t *testing.T) {
|
|||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -599,99 +498,44 @@ func TestOCSP(t *testing.T) {
|
|||
testCtx.fc)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
|
||||
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID}
|
||||
|
||||
cert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
||||
test.AssertNotError(t, err, "Failed to issue")
|
||||
parsedCert, err := x509.ParseCertificate(cert.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse cert")
|
||||
status := string(core.OCSPStatusGood)
|
||||
ocspResp, err := ca.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
CertDER: cert.DER,
|
||||
Status: status,
|
||||
// Issue a certificate from the RSA issuer caCert, then check OCSP comes from the same issuer.
|
||||
rsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID})
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
rsaCert, err := x509.ParseCertificate(rsaCertPB.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse rsaCert")
|
||||
rsaOCSPPB, err := ca.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
CertDER: rsaCertPB.DER,
|
||||
Status: string(core.OCSPStatusGood),
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to generate OCSP")
|
||||
parsed, err := ocsp.ParseResponse(ocspResp.Response, caCert.Certificate)
|
||||
test.AssertNotError(t, err, "Failed to parse validate OCSP")
|
||||
test.AssertEquals(t, parsed.Status, 0)
|
||||
test.AssertEquals(t, parsed.RevocationReason, 0)
|
||||
test.AssertEquals(t, parsed.SerialNumber.Cmp(parsedCert.SerialNumber), 0)
|
||||
rsaOCSP, err := ocsp.ParseResponse(rsaOCSPPB.Response, caCert.Certificate)
|
||||
test.AssertNotError(t, err, "Failed to parse / validate OCSP for rsaCert")
|
||||
test.AssertEquals(t, rsaOCSP.Status, 0)
|
||||
test.AssertEquals(t, rsaOCSP.RevocationReason, 0)
|
||||
test.AssertEquals(t, rsaOCSP.SerialNumber.Cmp(rsaCert.SerialNumber), 0)
|
||||
|
||||
// Issue a certificate from the ECDSA issuer caCert2, then check OCSP comes from the same issuer.
|
||||
ecdsaCertPB, err := ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{Csr: ECDSACSR, RegistrationID: arbitraryRegID})
|
||||
test.AssertNotError(t, err, "Failed to issue certificate")
|
||||
ecdsaCert, err := x509.ParseCertificate(ecdsaCertPB.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse ecdsaCert")
|
||||
ecdsaOCSPPB, err := ca.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
CertDER: ecdsaCertPB.DER,
|
||||
Status: string(core.OCSPStatusGood),
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to generate OCSP")
|
||||
ecdsaOCSP, err := ocsp.ParseResponse(ecdsaOCSPPB.Response, caCert2.Certificate)
|
||||
test.AssertNotError(t, err, "Failed to parse / validate OCSP for ecdsaCert")
|
||||
test.AssertEquals(t, ecdsaOCSP.Status, 0)
|
||||
test.AssertEquals(t, ecdsaOCSP.RevocationReason, 0)
|
||||
test.AssertEquals(t, ecdsaOCSP.SerialNumber.Cmp(ecdsaCert.SerialNumber), 0)
|
||||
|
||||
// Test that signatures are checked.
|
||||
_, err = ca.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
CertDER: append(cert.DER, byte(0)),
|
||||
Status: status,
|
||||
CertDER: append(rsaCertPB.DER, byte(0)),
|
||||
Status: string(core.OCSPStatusGood),
|
||||
})
|
||||
test.AssertError(t, err, "Generated OCSP for cert with bad signature")
|
||||
|
||||
// Load multiple issuers, including the old issuer, and ensure OCSP is still
|
||||
// signed correctly.
|
||||
newIssuerCert, err := issuance.LoadCertificate("../test/test-ca2.pem")
|
||||
test.AssertNotError(t, err, "Failed to load new cert")
|
||||
newIssuers := []Issuer{
|
||||
{
|
||||
Signer: caKey,
|
||||
// newIssuerCert is first, so it will be the default.
|
||||
Cert: newIssuerCert,
|
||||
}, {
|
||||
Signer: caKey,
|
||||
Cert: caCert,
|
||||
},
|
||||
}
|
||||
ca, err = NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
newIssuers,
|
||||
nil,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
testCtx.serialPrefix,
|
||||
testCtx.maxNames,
|
||||
testCtx.ocspLifetime,
|
||||
testCtx.keyPolicy,
|
||||
nil,
|
||||
0,
|
||||
time.Second,
|
||||
testCtx.logger,
|
||||
testCtx.stats,
|
||||
testCtx.fc)
|
||||
test.AssertNotError(t, err, "Failed to remake CA")
|
||||
|
||||
// Now issue a new precert, signed by newIssuerCert
|
||||
newCert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
||||
test.AssertNotError(t, err, "Failed to issue newCert")
|
||||
parsedNewCert, err := x509.ParseCertificate(newCert.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse newCert")
|
||||
|
||||
err = parsedNewCert.CheckSignatureFrom(newIssuerCert.Certificate)
|
||||
t.Logf("check sig: %s", err)
|
||||
|
||||
// ocspResp2 is a second OCSP response for `cert` (issued by caCert), and
|
||||
// should be signed by caCert.
|
||||
ocspResp2, err := ca.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
CertDER: append([]byte(nil), cert.DER...),
|
||||
Status: status,
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to sign second OCSP response")
|
||||
_, err = ocsp.ParseResponse(ocspResp2.Response, caCert.Certificate)
|
||||
test.AssertNotError(t, err, "Failed to parse / validate second OCSP response")
|
||||
|
||||
// newCertOcspResp is an OCSP response for `newCert` (issued by newIssuer),
|
||||
// and should be signed by newIssuer.
|
||||
newCertOcspResp, err := ca.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{
|
||||
CertDER: newCert.DER,
|
||||
Status: status,
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to generate OCSP")
|
||||
parsedNewCertOcspResp, err := ocsp.ParseResponse(newCertOcspResp.Response, newIssuerCert.Certificate)
|
||||
test.AssertNotError(t, err, "Failed to parse / validate OCSP for newCert")
|
||||
test.AssertEquals(t, parsedNewCertOcspResp.Status, 0)
|
||||
test.AssertEquals(t, parsedNewCertOcspResp.RevocationReason, 0)
|
||||
test.AssertEquals(t, parsedNewCertOcspResp.SerialNumber.Cmp(parsedNewCert.SerialNumber), 0)
|
||||
}
|
||||
|
||||
func TestInvalidCSRs(t *testing.T) {
|
||||
|
|
@ -735,12 +579,6 @@ func TestInvalidCSRs(t *testing.T) {
|
|||
// CSR generated by OpenSSL:
|
||||
// Edited signature to become invalid.
|
||||
{"RejectWrongSignature", "./testdata/invalid_signature.der.csr", nil, "Issued a certificate based on a CSR with an invalid signature.", berrors.BadCSR},
|
||||
|
||||
// CSR generated by Go:
|
||||
// * Random public key
|
||||
// * CN = not-example.com
|
||||
// * Includes an extensionRequest attribute for an empty TLS Feature extension
|
||||
{"TLSFeatureUnknown", "./testdata/tls_feature_unknown.der.csr", issueCertificateSubTestTLSFeatureUnknown, "Issued a certificate based on a CSR with an empty TLS feature extension.", berrors.Malformed},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
|
@ -749,11 +587,7 @@ func TestInvalidCSRs(t *testing.T) {
|
|||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -791,11 +625,7 @@ func TestRejectValidityTooLong(t *testing.T) {
|
|||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -822,60 +652,6 @@ func TestRejectValidityTooLong(t *testing.T) {
|
|||
test.AssertErrorIs(t, err, berrors.InternalServer)
|
||||
}
|
||||
|
||||
func TestSingleAIAEnforcement(t *testing.T) {
|
||||
pa, err := policy.New(nil)
|
||||
test.AssertNotError(t, err, "Couldn't create PA")
|
||||
|
||||
_, err = NewCertificateAuthorityImpl(
|
||||
&mockSA{},
|
||||
pa,
|
||||
cfsslConfig.Config{
|
||||
Signing: &cfsslConfig.Signing{
|
||||
Profiles: map[string]*cfsslConfig.SigningProfile{
|
||||
rsaProfileName: {
|
||||
IssuerURL: []string{"http://not-example.com/issuer-url", "bad"},
|
||||
Usage: []string{"digital signature", "key encipherment", "server auth"},
|
||||
OCSP: "http://not-example.com/ocsp",
|
||||
CRL: "http://not-example.com/crl",
|
||||
Policies: []cfsslConfig.CertificatePolicy{
|
||||
{
|
||||
ID: cfsslConfig.OID(asn1.ObjectIdentifier{2, 23, 140, 1, 2, 1}),
|
||||
},
|
||||
},
|
||||
ExpiryString: "8760h",
|
||||
Backdate: time.Hour,
|
||||
CSRWhitelist: &cfsslConfig.CSRWhitelist{
|
||||
PublicKeyAlgorithm: true,
|
||||
PublicKey: true,
|
||||
SignatureAlgorithm: true,
|
||||
},
|
||||
ClientProvidesSerialNumbers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
rsaProfileName,
|
||||
ecdsaProfileName,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
8760*time.Hour,
|
||||
time.Hour,
|
||||
1,
|
||||
1,
|
||||
time.Second,
|
||||
goodkey.KeyPolicy{},
|
||||
nil,
|
||||
0,
|
||||
time.Second,
|
||||
&blog.Mock{},
|
||||
metrics.NoopRegisterer,
|
||||
clock.New(),
|
||||
)
|
||||
test.AssertError(t, err, "NewCertificateAuthorityImpl allowed a profile with multiple issuer_urls")
|
||||
test.AssertEquals(t, err.Error(), "only one issuer_url supported")
|
||||
}
|
||||
|
||||
func issueCertificateSubTestProfileSelectionRSA(t *testing.T, i *TestCertificateIssuance) {
|
||||
// Certificates for RSA keys should be marked as usable for signatures and encryption.
|
||||
expectedKeyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
|
||||
|
|
@ -892,6 +668,7 @@ func issueCertificateSubTestProfileSelectionECDSA(t *testing.T, i *TestCertifica
|
|||
|
||||
func countMustStaple(t *testing.T, cert *x509.Certificate) (count int) {
|
||||
oidTLSFeature := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
|
||||
mustStapleFeatureValue := []byte{0x30, 0x03, 0x02, 0x01, 0x05}
|
||||
for _, ext := range cert.Extensions {
|
||||
if ext.Id.Equal(oidTLSFeature) {
|
||||
test.Assert(t, !ext.Critical, "Extension was marked critical")
|
||||
|
|
@ -903,22 +680,11 @@ func countMustStaple(t *testing.T, cert *x509.Certificate) (count int) {
|
|||
}
|
||||
|
||||
func issueCertificateSubTestMustStaple(t *testing.T, i *TestCertificateIssuance) {
|
||||
// a TLS feature extension should put a must-staple extension into the cert. Even
|
||||
// if there are multiple TLS Feature extensions, only one extension should be included.
|
||||
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeature, i.ca.csrExtensionCount), 1)
|
||||
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeatureInvalid, i.ca.csrExtensionCount), 0)
|
||||
test.AssertEquals(t, signatureCountByPurpose("precertificate", i.ca.signatureCount), 1)
|
||||
test.AssertEquals(t, countMustStaple(t, i.cert), 1)
|
||||
}
|
||||
|
||||
func issueCertificateSubTestTLSFeatureUnknown(t *testing.T, ca *CertificateAuthorityImpl, _ *mockSA) {
|
||||
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeature, ca.csrExtensionCount), 1)
|
||||
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionTLSFeatureInvalid, ca.csrExtensionCount), 1)
|
||||
}
|
||||
|
||||
func issueCertificateSubTestUnknownExtension(t *testing.T, i *TestCertificateIssuance) {
|
||||
// Unsupported extensions in the CSR should be silently ignored.
|
||||
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionOther, i.ca.csrExtensionCount), 1)
|
||||
test.AssertEquals(t, signatureCountByPurpose("precertificate", i.ca.signatureCount), 1)
|
||||
|
||||
// NOTE: The hard-coded value here will have to change over time as Boulder
|
||||
|
|
@ -928,11 +694,6 @@ func issueCertificateSubTestUnknownExtension(t *testing.T, i *TestCertificateIss
|
|||
}
|
||||
|
||||
func issueCertificateSubTestCTPoisonExtension(t *testing.T, i *TestCertificateIssuance) {
|
||||
// The CT poison extension in the CSR should be silently ignored like an
|
||||
// unknown extension, whether it has a valid or invalid value. The check
|
||||
// for whether or not the poison extension is present in the issued
|
||||
// certificate/precertificate is done in the caller.
|
||||
test.AssertEquals(t, test.CountCounterVec(csrExtensionCategory, csrExtensionOther, i.ca.csrExtensionCount), 1)
|
||||
test.AssertEquals(t, signatureCountByPurpose("precertificate", i.ca.signatureCount), 1)
|
||||
}
|
||||
|
||||
|
|
@ -967,77 +728,63 @@ func makeSCTs() ([][]byte, error) {
|
|||
func TestIssueCertificateForPrecertificate(t *testing.T) {
|
||||
testCtx := setup(t)
|
||||
sa := &mockSA{}
|
||||
for _, nonCFSSL := range []bool{true, false} {
|
||||
_ = features.Set(map[string]bool{"NonCFSSLSigner": nonCFSSL})
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
testCtx.serialPrefix,
|
||||
testCtx.maxNames,
|
||||
testCtx.ocspLifetime,
|
||||
testCtx.keyPolicy,
|
||||
nil,
|
||||
0,
|
||||
time.Second,
|
||||
testCtx.logger,
|
||||
testCtx.stats,
|
||||
testCtx.fc)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
testCtx.serialPrefix,
|
||||
testCtx.maxNames,
|
||||
testCtx.ocspLifetime,
|
||||
testCtx.keyPolicy,
|
||||
nil,
|
||||
0,
|
||||
time.Second,
|
||||
testCtx.logger,
|
||||
testCtx.stats,
|
||||
testCtx.fc)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
|
||||
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
|
||||
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
||||
test.AssertNotError(t, err, "Failed to issue precert")
|
||||
parsedPrecert, err := x509.ParseCertificate(precert.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse precert")
|
||||
issueReq := capb.IssueCertificateRequest{Csr: CNandSANCSR, RegistrationID: arbitraryRegID, OrderID: 0}
|
||||
precert, err := ca.IssuePrecertificate(ctx, &issueReq)
|
||||
test.AssertNotError(t, err, "Failed to issue precert")
|
||||
parsedPrecert, err := x509.ParseCertificate(precert.DER)
|
||||
test.AssertNotError(t, err, "Failed to parse precert")
|
||||
|
||||
// Check for poison extension
|
||||
poisoned := false
|
||||
for _, ext := range parsedPrecert.Extensions {
|
||||
if ext.Id.Equal(signer.CTPoisonOID) && ext.Critical {
|
||||
poisoned = true
|
||||
}
|
||||
}
|
||||
test.Assert(t, poisoned, "returned precert not poisoned")
|
||||
// Check for poison extension
|
||||
poisonExtension := findExtension(parsedPrecert.Extensions, OIDExtensionCTPoison)
|
||||
test.AssertNotNil(t, poisonExtension, "Couldn't find CTPoison extension")
|
||||
test.AssertEquals(t, poisonExtension.Critical, true)
|
||||
test.AssertDeepEquals(t, poisonExtension.Value, []byte{0x05, 0x00}) // ASN.1 DER NULL
|
||||
|
||||
sctBytes, err := makeSCTs()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
test.AssertNotError(t, err, "Failed to marshal SCT")
|
||||
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
|
||||
DER: precert.DER,
|
||||
SCTs: sctBytes,
|
||||
RegistrationID: arbitraryRegID,
|
||||
OrderID: 0,
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to issue cert from precert")
|
||||
parsedCert, err := x509.ParseCertificate(cert.Der)
|
||||
test.AssertNotError(t, err, "Failed to parse cert")
|
||||
|
||||
// Check for SCT list extension
|
||||
list := false
|
||||
for _, ext := range parsedCert.Extensions {
|
||||
if ext.Id.Equal(signer.SCTListOID) && !ext.Critical {
|
||||
list = true
|
||||
var rawValue []byte
|
||||
_, err = asn1.Unmarshal(ext.Value, &rawValue)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal extension value")
|
||||
sctList, err := helpers.DeserializeSCTList(rawValue)
|
||||
test.AssertNotError(t, err, "Failed to deserialize SCT list")
|
||||
test.Assert(t, len(sctList) == 1, fmt.Sprintf("Wrong number of SCTs, wanted: 1, got: %d", len(sctList)))
|
||||
}
|
||||
}
|
||||
test.Assert(t, list, "returned cert doesn't contain SCT list")
|
||||
sctBytes, err := makeSCTs()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
test.AssertNotError(t, err, "Failed to marshal SCT")
|
||||
cert, err := ca.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{
|
||||
DER: precert.DER,
|
||||
SCTs: sctBytes,
|
||||
RegistrationID: arbitraryRegID,
|
||||
OrderID: 0,
|
||||
})
|
||||
test.AssertNotError(t, err, "Failed to issue cert from precert")
|
||||
parsedCert, err := x509.ParseCertificate(cert.Der)
|
||||
test.AssertNotError(t, err, "Failed to parse cert")
|
||||
|
||||
// Check for SCT list extension
|
||||
sctListExtension := findExtension(parsedCert.Extensions, OIDExtensionSCTList)
|
||||
test.AssertNotNil(t, sctListExtension, "Couldn't find SCTList extension")
|
||||
test.AssertEquals(t, sctListExtension.Critical, false)
|
||||
var rawValue []byte
|
||||
_, err = asn1.Unmarshal(sctListExtension.Value, &rawValue)
|
||||
test.AssertNotError(t, err, "Failed to unmarshal extension value")
|
||||
sctList, err := helpers.DeserializeSCTList(rawValue)
|
||||
test.AssertNotError(t, err, "Failed to deserialize SCT list")
|
||||
test.Assert(t, len(sctList) == 1, fmt.Sprintf("Wrong number of SCTs, wanted: 1, got: %d", len(sctList)))
|
||||
}
|
||||
|
||||
// dupeSA returns a non-error to GetCertificate in order to simulate a request
|
||||
|
|
@ -1065,11 +812,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
|
|||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -1112,11 +855,7 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
|
|||
errorca, err := NewCertificateAuthorityImpl(
|
||||
errorsa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -1196,11 +935,7 @@ func TestPrecertOrphanQueue(t *testing.T) {
|
|||
ca, err := NewCertificateAuthorityImpl(
|
||||
qsa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -1269,11 +1004,7 @@ func TestOrphanQueue(t *testing.T) {
|
|||
ca, err := NewCertificateAuthorityImpl(
|
||||
qsa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
@ -1374,84 +1105,6 @@ func TestOrphanQueue(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
type linttrapSigner struct {
|
||||
lintErr error
|
||||
}
|
||||
|
||||
func (s *linttrapSigner) Sign(signer.SignRequest) ([]byte, error) {
|
||||
return nil, s.lintErr
|
||||
}
|
||||
|
||||
func (s *linttrapSigner) SignFromPrecert(*x509.Certificate, []ct.SignedCertificateTimestamp) ([]byte, error) {
|
||||
return nil, errors.New("SignFromPrecert not implemented for linttrapSigner")
|
||||
}
|
||||
|
||||
func TestIssuePrecertificateLinting(t *testing.T) {
|
||||
testCtx := setup(t)
|
||||
sa := &mockSA{}
|
||||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
testCtx.serialPrefix,
|
||||
testCtx.maxNames,
|
||||
testCtx.ocspLifetime,
|
||||
testCtx.keyPolicy,
|
||||
nil,
|
||||
0,
|
||||
time.Second,
|
||||
testCtx.logger,
|
||||
testCtx.stats,
|
||||
testCtx.fc)
|
||||
test.AssertNotError(t, err, "Failed to create CA")
|
||||
|
||||
// Reconfigure the CA's cfsslSigner to be a linttrapSigner that always returns
|
||||
// two LintResults.
|
||||
rsaIssuer := ca.issuers.byAlg[x509.RSA]
|
||||
rsaIssuer.cfsslSigner = &linttrapSigner{
|
||||
lintErr: &local.LintError{
|
||||
ErrorResults: map[string]zlint.LintResult{
|
||||
"foobar": {
|
||||
Status: zlint.Error,
|
||||
Details: "foobar is error",
|
||||
},
|
||||
"foobar2": {
|
||||
Status: zlint.Warn,
|
||||
Details: "foobar2 is warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Clear the mock logger
|
||||
testCtx.logger.Clear()
|
||||
|
||||
// Attempt to issue a pre-certificate
|
||||
_, err = ca.IssuePrecertificate(ctx, &capb.IssueCertificateRequest{
|
||||
Csr: CNandSANCSR,
|
||||
RegistrationID: arbitraryRegID,
|
||||
})
|
||||
// It should error
|
||||
test.AssertError(t, err, "expected err from IssuePrecertificate with linttrapSigner")
|
||||
// The local.LintError should have been converted to an internal server error
|
||||
// berror with the correct message.
|
||||
test.AssertErrorIs(t, err, berrors.InternalServer)
|
||||
test.AssertEquals(t, err.Error(), "failed to sign certificate: pre-issuance linting found 2 error results")
|
||||
|
||||
// We also expect that an AUDIT level error is logged that includes the expect
|
||||
// serialized JSON lintErrors
|
||||
regex := `ERR: \[AUDIT\] Signing failed: serial=\[.*\] err=\[pre-issuance linting found 2 error results\] lintErrors=\{"foobar":\{"result":"error","details":"foobar is error"\},"foobar2":\{"result":"warn","details":"foobar2 is warning"\}\}`
|
||||
matches := testCtx.logger.GetAllMatching(regex)
|
||||
test.AssertEquals(t, len(matches), 1)
|
||||
}
|
||||
|
||||
func TestGenerateOCSPWithIssuerID(t *testing.T) {
|
||||
testCtx := setup(t)
|
||||
sa := &mockSA{}
|
||||
|
|
@ -1459,11 +1112,7 @@ func TestGenerateOCSPWithIssuerID(t *testing.T) {
|
|||
ca, err := NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
testCtx.pa,
|
||||
testCtx.cfsslProfiles,
|
||||
testCtx.cfsslRSAProfile,
|
||||
testCtx.cfsslECDSAProfile,
|
||||
testCtx.cfsslIssuers,
|
||||
nil,
|
||||
testCtx.boulderIssuers,
|
||||
nil,
|
||||
testCtx.certExpiry,
|
||||
testCtx.certBackdate,
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,26 +1,18 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/beeker1121/goque"
|
||||
cfsslConfig "github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
pkcs11key "github.com/letsencrypt/pkcs11key/v4"
|
||||
"google.golang.org/grpc/health"
|
||||
healthpb "google.golang.org/grpc/health/grpc_health_v1"
|
||||
|
||||
"github.com/letsencrypt/boulder/ca"
|
||||
capb "github.com/letsencrypt/boulder/ca/proto"
|
||||
"github.com/letsencrypt/boulder/cmd"
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/features"
|
||||
"github.com/letsencrypt/boulder/goodkey"
|
||||
bgrpc "github.com/letsencrypt/boulder/grpc"
|
||||
|
|
@ -45,30 +37,17 @@ type config struct {
|
|||
|
||||
SAService *cmd.GRPCClientConfig
|
||||
|
||||
// CFSSL contains CFSSL-specific configs as specified by that library.
|
||||
CFSSL cfsslConfig.Config
|
||||
// RSAProfile and ECDSAProfile name which of the profiles specified in the
|
||||
// CFSSL config should be used when issuing RSA and ECDSA certs, respectively.
|
||||
RSAProfile string
|
||||
ECDSAProfile string
|
||||
// Issuers contains configuration information for each issuer cert and key
|
||||
// this CA knows about. The first in the list is used as the default.
|
||||
// Only used by CFSSL.
|
||||
Issuers []IssuerConfig
|
||||
|
||||
// Issuance contains all information necessary to load and initialize non-CFSSL issuers.
|
||||
// Issuance contains all information necessary to load and initialize issuers.
|
||||
Issuance struct {
|
||||
Profile issuance.ProfileConfig
|
||||
Issuers []issuance.IssuerConfig
|
||||
IgnoredLints []string
|
||||
}
|
||||
|
||||
// How long issued certificates are valid for, should match expiry field
|
||||
// in cfssl config.
|
||||
// How long issued certificates are valid for.
|
||||
Expiry cmd.ConfigDuration
|
||||
|
||||
// How far back certificates should be backdated, should match backdate
|
||||
// field in cfssl config.
|
||||
// How far back certificates should be backdated.
|
||||
Backdate cmd.ConfigDuration
|
||||
|
||||
// What digits we should prepend to serials after randomly generating them.
|
||||
|
|
@ -125,91 +104,6 @@ type config struct {
|
|||
Syslog cmd.SyslogConfig
|
||||
}
|
||||
|
||||
// IssuerConfig contains info about an issuer: private key and issuer cert.
|
||||
// It should contain either a File path to a PEM-format private key,
|
||||
// or a PKCS11Config defining how to load a module for an HSM. Used by CFSSL.
|
||||
type IssuerConfig struct {
|
||||
// A file from which a pkcs11key.Config will be read and parsed, if present
|
||||
ConfigFile string
|
||||
File string
|
||||
PKCS11 *pkcs11key.Config
|
||||
CertFile string
|
||||
// Number of sessions to open with the HSM. For maximum performance,
|
||||
// this should be equal to the number of cores in the HSM. Defaults to 1.
|
||||
NumSessions int
|
||||
}
|
||||
|
||||
func loadCFSSLIssuers(configs []IssuerConfig) ([]ca.Issuer, error) {
|
||||
var issuers []ca.Issuer
|
||||
for _, issuerConfig := range configs {
|
||||
signer, cert, err := loadCFSSLIssuer(issuerConfig)
|
||||
cmd.FailOnError(err, "Couldn't load private key")
|
||||
issuers = append(issuers, ca.Issuer{
|
||||
Signer: signer,
|
||||
Cert: cert,
|
||||
})
|
||||
}
|
||||
return issuers, nil
|
||||
}
|
||||
|
||||
func loadCFSSLIssuer(issuerConfig IssuerConfig) (crypto.Signer, *issuance.Certificate, error) {
|
||||
cert, err := issuance.LoadCertificate(issuerConfig.CertFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
signer, err := loadCFSSLSigner(issuerConfig, cert.Certificate)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !core.KeyDigestEquals(signer.Public(), cert.PublicKey) {
|
||||
return nil, nil, fmt.Errorf("Issuer key did not match issuer cert %s", issuerConfig.CertFile)
|
||||
}
|
||||
return signer, cert, err
|
||||
}
|
||||
|
||||
func loadCFSSLSigner(issuerConfig IssuerConfig, cert *x509.Certificate) (crypto.Signer, error) {
|
||||
if issuerConfig.File != "" {
|
||||
keyBytes, err := ioutil.ReadFile(issuerConfig.File)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read key file %s", issuerConfig.File)
|
||||
}
|
||||
|
||||
signer, err := helpers.ParsePrivateKeyPEM(keyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
var pkcs11Config *pkcs11key.Config
|
||||
if issuerConfig.ConfigFile != "" {
|
||||
contents, err := ioutil.ReadFile(issuerConfig.ConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkcs11Config = new(pkcs11key.Config)
|
||||
err = json.Unmarshal(contents, pkcs11Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
pkcs11Config = issuerConfig.PKCS11
|
||||
}
|
||||
if pkcs11Config.Module == "" ||
|
||||
pkcs11Config.TokenLabel == "" ||
|
||||
pkcs11Config.PIN == "" {
|
||||
return nil, fmt.Errorf("Missing a field in pkcs11Config %#v", pkcs11Config)
|
||||
}
|
||||
numSessions := issuerConfig.NumSessions
|
||||
if numSessions <= 0 {
|
||||
numSessions = 1
|
||||
}
|
||||
return pkcs11key.NewPool(numSessions, pkcs11Config.Module,
|
||||
pkcs11Config.TokenLabel, pkcs11Config.PIN, cert.PublicKey)
|
||||
}
|
||||
|
||||
func loadBoulderIssuers(profileConfig issuance.ProfileConfig, issuerConfigs []issuance.IssuerConfig, ignoredLints []string) ([]*issuance.Issuer, error) {
|
||||
issuers := make([]*issuance.Issuer, 0, len(issuerConfigs))
|
||||
for _, issuerConfig := range issuerConfigs {
|
||||
|
|
@ -285,15 +179,9 @@ func main() {
|
|||
err = pa.SetHostnamePolicyFile(c.CA.HostnamePolicyFile)
|
||||
cmd.FailOnError(err, "Couldn't load hostname policy file")
|
||||
|
||||
var cfsslIssuers []ca.Issuer
|
||||
var boulderIssuers []*issuance.Issuer
|
||||
if features.Enabled(features.NonCFSSLSigner) {
|
||||
boulderIssuers, err = loadBoulderIssuers(c.CA.Issuance.Profile, c.CA.Issuance.Issuers, c.CA.Issuance.IgnoredLints)
|
||||
cmd.FailOnError(err, "Couldn't load issuers")
|
||||
} else {
|
||||
cfsslIssuers, err = loadCFSSLIssuers(c.CA.Issuers)
|
||||
cmd.FailOnError(err, "Couldn't load issuers")
|
||||
}
|
||||
boulderIssuers, err = loadBoulderIssuers(c.CA.Issuance.Profile, c.CA.Issuance.Issuers, c.CA.Issuance.IgnoredLints)
|
||||
cmd.FailOnError(err, "Couldn't load issuers")
|
||||
|
||||
tlsConfig, err := c.CA.TLS.Load()
|
||||
cmd.FailOnError(err, "TLS config")
|
||||
|
|
@ -318,10 +206,6 @@ func main() {
|
|||
cai, err := ca.NewCertificateAuthorityImpl(
|
||||
sa,
|
||||
pa,
|
||||
c.CA.CFSSL,
|
||||
c.CA.RSAProfile,
|
||||
c.CA.ECDSAProfile,
|
||||
cfsslIssuers,
|
||||
boulderIssuers,
|
||||
c.CA.ECDSAAllowedAccounts,
|
||||
c.CA.Expiry.Duration,
|
||||
|
|
|
|||
|
|
@ -1,41 +1 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadIssuerSuccess(t *testing.T) {
|
||||
signer, cert, err := loadCFSSLIssuer(IssuerConfig{
|
||||
File: "../../test/test-ca.key",
|
||||
CertFile: "../../test/test-ca2.pem",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if signer == nil {
|
||||
t.Fatal("loadIssuer returned nil signer")
|
||||
}
|
||||
if cert == nil {
|
||||
t.Fatal("loadIssuer returned nil cert")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadIssuerBadKey(t *testing.T) {
|
||||
_, _, err := loadCFSSLIssuer(IssuerConfig{
|
||||
File: "/dev/null",
|
||||
CertFile: "../../test/test-ca2.pem",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("loadIssuer succeeded when loading key from /dev/null")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadIssuerBadCert(t *testing.T) {
|
||||
_, _, err := loadCFSSLIssuer(IssuerConfig{
|
||||
File: "../../test/test-ca.key",
|
||||
CertFile: "/dev/null",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("loadIssuer succeeded when loading key from /dev/null")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
24
cmd/shell.go
24
cmd/shell.go
|
|
@ -20,7 +20,6 @@ import (
|
|||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
|
||||
cfsslLog "github.com/cloudflare/cfssl/log"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
|
@ -50,20 +49,6 @@ func (m mysqlLogger) Print(v ...interface{}) {
|
|||
m.AuditErrf("[mysql] %s", fmt.Sprint(v...))
|
||||
}
|
||||
|
||||
// cfsslLogger provides two additional methods that are expected by CFSSL's
|
||||
// logger but not supported by Boulder's Logger.
|
||||
type cfsslLogger struct {
|
||||
blog.Logger
|
||||
}
|
||||
|
||||
func (cl cfsslLogger) Crit(msg string) {
|
||||
cl.AuditErr(msg)
|
||||
}
|
||||
|
||||
func (cl cfsslLogger) Emerg(msg string) {
|
||||
cl.AuditErr(msg)
|
||||
}
|
||||
|
||||
type grpcLogger struct {
|
||||
blog.Logger
|
||||
}
|
||||
|
|
@ -146,7 +131,7 @@ func (lw logWriter) Write(p []byte) (n int, err error) {
|
|||
// server on the provided port to report the stats and provide pprof profiling
|
||||
// handlers. NewLogger and newStatsRegistry will call os.Exit on errors.
|
||||
// Also sets the constructed AuditLogger as the default logger, and configures
|
||||
// the cfssl, mysql, and grpc packages to use our logger.
|
||||
// the mysql and grpc packages to use our logger.
|
||||
// This must be called before any gRPC code is called, because gRPC's SetLogger
|
||||
// doesn't use any locking.
|
||||
func StatsAndLogging(logConf SyslogConfig, addr string) (prometheus.Registerer, blog.Logger) {
|
||||
|
|
@ -170,13 +155,6 @@ func NewLogger(logConf SyslogConfig) blog.Logger {
|
|||
FailOnError(err, "Could not connect to Syslog")
|
||||
|
||||
_ = blog.Set(logger)
|
||||
// We set the cfssl logging level to Debug as it
|
||||
// won't actually call logging methods for any
|
||||
// level less than what is set. We will ignore
|
||||
// any logging we don't care about at the syslog
|
||||
// level, so this doesn't cause extraneous logging.
|
||||
cfsslLog.Level = cfsslLog.LevelDebug
|
||||
cfsslLog.SetLogger(cfsslLogger{logger})
|
||||
_ = mysql.SetLogger(mysqlLogger{logger})
|
||||
grpclog.SetLoggerV2(grpcLogger{logger})
|
||||
log.SetOutput(logWriter{logger})
|
||||
|
|
|
|||
|
|
@ -91,38 +91,6 @@ func TestMysqlLogger(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCfsslLogger(t *testing.T) {
|
||||
log := blog.UseMock()
|
||||
cLog := cfsslLogger{log}
|
||||
|
||||
testCases := []struct {
|
||||
msg, expected string
|
||||
}{
|
||||
{
|
||||
"",
|
||||
"ERR: [AUDIT] ",
|
||||
},
|
||||
{
|
||||
"Test",
|
||||
"ERR: [AUDIT] Test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
// cfsslLogger proxies blog.AuditLogger to provide Crit() and Emerg()
|
||||
// methods that are expected by CFSSL's logger
|
||||
cLog.Crit(tc.msg)
|
||||
cLog.Emerg(tc.msg)
|
||||
logged := log.GetAll()
|
||||
// Calling Crit and Emerg should produce two AuditErr outputs matching the
|
||||
// testCase expected output
|
||||
test.AssertEquals(t, len(logged), 2)
|
||||
test.AssertEquals(t, logged[0], tc.expected)
|
||||
test.AssertEquals(t, logged[1], tc.expected)
|
||||
log.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureStdlibLog(t *testing.T) {
|
||||
logger := blog.UseMock()
|
||||
oldDest := log.Writer()
|
||||
|
|
|
|||
|
|
@ -11,24 +11,24 @@ func _() {
|
|||
_ = x[unused-0]
|
||||
_ = x[PrecertificateRevocation-1]
|
||||
_ = x[StripDefaultSchemePort-2]
|
||||
_ = x[CAAValidationMethods-3]
|
||||
_ = x[CAAAccountURI-4]
|
||||
_ = x[EnforceMultiVA-5]
|
||||
_ = x[MultiVAFullResults-6]
|
||||
_ = x[MandatoryPOSTAsGET-7]
|
||||
_ = x[AllowV1Registration-8]
|
||||
_ = x[V1DisableNewValidations-9]
|
||||
_ = x[StoreIssuerInfo-10]
|
||||
_ = x[StoreRevokerInfo-11]
|
||||
_ = x[RestrictRSAKeySizes-12]
|
||||
_ = x[FasterNewOrdersRateLimit-13]
|
||||
_ = x[NonCFSSLSigner-14]
|
||||
_ = x[NonCFSSLSigner-3]
|
||||
_ = x[CAAValidationMethods-4]
|
||||
_ = x[CAAAccountURI-5]
|
||||
_ = x[EnforceMultiVA-6]
|
||||
_ = x[MultiVAFullResults-7]
|
||||
_ = x[MandatoryPOSTAsGET-8]
|
||||
_ = x[AllowV1Registration-9]
|
||||
_ = x[V1DisableNewValidations-10]
|
||||
_ = x[StoreIssuerInfo-11]
|
||||
_ = x[StoreRevokerInfo-12]
|
||||
_ = x[RestrictRSAKeySizes-13]
|
||||
_ = x[FasterNewOrdersRateLimit-14]
|
||||
_ = x[ECDSAForAll-15]
|
||||
}
|
||||
|
||||
const _FeatureFlag_name = "unusedPrecertificateRevocationStripDefaultSchemePortCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsStoreIssuerInfoStoreRevokerInfoRestrictRSAKeySizesFasterNewOrdersRateLimitNonCFSSLSignerECDSAForAll"
|
||||
const _FeatureFlag_name = "unusedPrecertificateRevocationStripDefaultSchemePortNonCFSSLSignerCAAValidationMethodsCAAAccountURIEnforceMultiVAMultiVAFullResultsMandatoryPOSTAsGETAllowV1RegistrationV1DisableNewValidationsStoreIssuerInfoStoreRevokerInfoRestrictRSAKeySizesFasterNewOrdersRateLimitECDSAForAll"
|
||||
|
||||
var _FeatureFlag_index = [...]uint16{0, 6, 30, 52, 72, 85, 99, 117, 135, 154, 177, 192, 208, 227, 251, 265, 276}
|
||||
var _FeatureFlag_index = [...]uint16{0, 6, 30, 52, 66, 86, 99, 113, 131, 149, 168, 191, 206, 222, 241, 265, 276}
|
||||
|
||||
func (i FeatureFlag) String() string {
|
||||
if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const (
|
|||
// Deprecated features, these can be removed once stripped from production configs
|
||||
PrecertificateRevocation
|
||||
StripDefaultSchemePort
|
||||
NonCFSSLSigner
|
||||
|
||||
// Currently in-use features
|
||||
// Check CAA and respect validationmethods parameter.
|
||||
|
|
@ -46,9 +47,6 @@ const (
|
|||
// FasterNewOrdersRateLimit enables use of a separate table for counting the
|
||||
// new orders rate limit.
|
||||
FasterNewOrdersRateLimit
|
||||
// NonCFSSLSigner enables usage of our own certificate signer instead of the
|
||||
// CFSSL signer.
|
||||
NonCFSSLSigner
|
||||
// ECDSAForAll enables all accounts, regardless of their presence in the CA's
|
||||
// ecdsaAllowedAccounts config value, to get issuance from ECDSA issuers.
|
||||
ECDSAForAll
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
// Package auth implements an interface for providing CFSSL
|
||||
// authentication. This is meant to authenticate a client CFSSL to a
|
||||
// remote CFSSL in order to prevent unauthorised use of the signature
|
||||
// capabilities. This package provides both the interface and a
|
||||
// standard HMAC-based implementation.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An AuthenticatedRequest contains a request and authentication
|
||||
// token. The Provider may determine whether to validate the timestamp
|
||||
// and remote address.
|
||||
type AuthenticatedRequest struct {
|
||||
// An Authenticator decides whether to use this field.
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
RemoteAddress []byte `json:"remote_address,omitempty"`
|
||||
Token []byte `json:"token"`
|
||||
Request []byte `json:"request"`
|
||||
}
|
||||
|
||||
// A Provider can generate tokens from a request and verify a
|
||||
// request. The handling of additional authentication data (such as
|
||||
// the IP address) is handled by the concrete type, as is any
|
||||
// serialisation and state-keeping.
|
||||
type Provider interface {
|
||||
Token(req []byte) (token []byte, err error)
|
||||
Verify(aReq *AuthenticatedRequest) bool
|
||||
}
|
||||
|
||||
// Standard implements an HMAC-SHA-256 authentication provider. It may
|
||||
// be supplied additional data at creation time that will be used as
|
||||
// request || additional-data with the HMAC.
|
||||
type Standard struct {
|
||||
key []byte
|
||||
ad []byte
|
||||
}
|
||||
|
||||
// New generates a new standard authentication provider from the key
|
||||
// and additional data. The additional data will be used when
|
||||
// generating a new token.
|
||||
func New(key string, ad []byte) (*Standard, error) {
|
||||
if splitKey := strings.SplitN(key, ":", 2); len(splitKey) == 2 {
|
||||
switch splitKey[0] {
|
||||
case "env":
|
||||
key = os.Getenv(splitKey[1])
|
||||
case "file":
|
||||
data, err := ioutil.ReadFile(splitKey[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = strings.TrimSpace(string(data))
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown key prefix: %s", splitKey[0])
|
||||
}
|
||||
}
|
||||
|
||||
keyBytes, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Standard{keyBytes, ad}, nil
|
||||
}
|
||||
|
||||
// Token generates a new authentication token from the request.
|
||||
func (p Standard) Token(req []byte) (token []byte, err error) {
|
||||
h := hmac.New(sha256.New, p.key)
|
||||
h.Write(req)
|
||||
h.Write(p.ad)
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
// Verify determines whether an authenticated request is valid.
|
||||
func (p Standard) Verify(ad *AuthenticatedRequest) bool {
|
||||
if ad == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Standard token generation returns no error.
|
||||
token, _ := p.Token(ad.Request)
|
||||
if len(ad.Token) != len(token) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hmac.Equal(token, ad.Token)
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# certdb usage
|
||||
|
||||
Using a database enables additional functionality for existing commands when a
|
||||
db config is provided:
|
||||
|
||||
- `sign` and `gencert` add a certificate to the certdb after signing it
|
||||
- `serve` enables database functionality for the sign and revoke endpoints
|
||||
|
||||
A database is required for the following:
|
||||
|
||||
- `revoke` marks certificates revoked in the database with an optional reason
|
||||
- `ocsprefresh` refreshes the table of cached OCSP responses
|
||||
- `ocspdump` outputs cached OCSP responses in a concatenated base64-encoded format
|
||||
|
||||
## Setup/Migration
|
||||
|
||||
This directory stores [goose](https://bitbucket.org/liamstask/goose/) db migration scripts for various DB backends.
|
||||
Currently supported:
|
||||
- MySQL in mysql
|
||||
- PostgreSQL in pg
|
||||
- SQLite in sqlite
|
||||
|
||||
### Get goose
|
||||
|
||||
go get bitbucket.org/liamstask/goose/cmd/goose
|
||||
|
||||
### Use goose to start and terminate a MySQL DB
|
||||
To start a MySQL using goose:
|
||||
|
||||
goose -path certdb/mysql up
|
||||
|
||||
To tear down a MySQL DB using goose
|
||||
|
||||
goose -path certdb/mysql down
|
||||
|
||||
Note: the administration of MySQL DB is not included. We assume
|
||||
the databases being connected to are already created and access control
|
||||
is properly handled.
|
||||
|
||||
### Use goose to start and terminate a PostgreSQL DB
|
||||
To start a PostgreSQL using goose:
|
||||
|
||||
goose -path certdb/pg up
|
||||
|
||||
To tear down a PostgreSQL DB using goose
|
||||
|
||||
goose -path certdb/pg down
|
||||
|
||||
Note: the administration of PostgreSQL DB is not included. We assume
|
||||
the databases being connected to are already created and access control
|
||||
is properly handled.
|
||||
|
||||
### Use goose to start and terminate a SQLite DB
|
||||
To start a SQLite DB using goose:
|
||||
|
||||
goose -path certdb/sqlite up
|
||||
|
||||
To tear down a SQLite DB using goose
|
||||
|
||||
goose -path certdb/sqlite down
|
||||
|
||||
## CFSSL Configuration
|
||||
|
||||
Several cfssl commands take a -db-config flag. Create a file with a
|
||||
JSON dictionary:
|
||||
|
||||
{"driver":"sqlite3","data_source":"certs.db"}
|
||||
|
||||
or
|
||||
|
||||
{"driver":"postgres","data_source":"postgres://user:password@host/db"}
|
||||
|
||||
or
|
||||
|
||||
{"driver":"mysql","data_source":"user:password@tcp(hostname:3306)/db?parseTime=true"}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package certdb
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CertificateRecord encodes a certificate and its metadata
|
||||
// that will be recorded in a database.
|
||||
type CertificateRecord struct {
|
||||
Serial string `db:"serial_number"`
|
||||
AKI string `db:"authority_key_identifier"`
|
||||
CALabel string `db:"ca_label"`
|
||||
Status string `db:"status"`
|
||||
Reason int `db:"reason"`
|
||||
Expiry time.Time `db:"expiry"`
|
||||
RevokedAt time.Time `db:"revoked_at"`
|
||||
PEM string `db:"pem"`
|
||||
}
|
||||
|
||||
// OCSPRecord encodes a OCSP response body and its metadata
|
||||
// that will be recorded in a database.
|
||||
type OCSPRecord struct {
|
||||
Serial string `db:"serial_number"`
|
||||
AKI string `db:"authority_key_identifier"`
|
||||
Body string `db:"body"`
|
||||
Expiry time.Time `db:"expiry"`
|
||||
}
|
||||
|
||||
// Accessor abstracts the CRUD of certdb objects from a DB.
|
||||
type Accessor interface {
|
||||
InsertCertificate(cr CertificateRecord) error
|
||||
GetCertificate(serial, aki string) ([]CertificateRecord, error)
|
||||
GetUnexpiredCertificates() ([]CertificateRecord, error)
|
||||
GetRevokedAndUnexpiredCertificates() ([]CertificateRecord, error)
|
||||
GetRevokedAndUnexpiredCertificatesByLabel(label string) ([]CertificateRecord, error)
|
||||
RevokeCertificate(serial, aki string, reasonCode int) error
|
||||
InsertOCSP(rr OCSPRecord) error
|
||||
GetOCSP(serial, aki string) ([]OCSPRecord, error)
|
||||
GetUnexpiredOCSPs() ([]OCSPRecord, error)
|
||||
UpdateOCSP(serial, aki, body string, expiry time.Time) error
|
||||
UpsertOCSP(serial, aki, body string, expiry time.Time) error
|
||||
}
|
||||
|
|
@ -1,744 +0,0 @@
|
|||
// Package config contains the configuration logic for CFSSL.
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cfssl/auth"
|
||||
cferr "github.com/cloudflare/cfssl/errors"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
ocspConfig "github.com/cloudflare/cfssl/ocsp/config"
|
||||
// empty import of zlint/v2 required to have lints registered.
|
||||
_ "github.com/zmap/zlint/v2"
|
||||
"github.com/zmap/zlint/v2/lint"
|
||||
)
|
||||
|
||||
// A CSRWhitelist stores booleans for fields in the CSR. If a CSRWhitelist is
|
||||
// not present in a SigningProfile, all of these fields may be copied from the
|
||||
// CSR into the signed certificate. If a CSRWhitelist *is* present in a
|
||||
// SigningProfile, only those fields with a `true` value in the CSRWhitelist may
|
||||
// be copied from the CSR to the signed certificate. Note that some of these
|
||||
// fields, like Subject, can be provided or partially provided through the API.
|
||||
// Since API clients are expected to be trusted, but CSRs are not, fields
|
||||
// provided through the API are not subject to whitelisting through this
|
||||
// mechanism.
|
||||
type CSRWhitelist struct {
|
||||
Subject, PublicKeyAlgorithm, PublicKey, SignatureAlgorithm bool
|
||||
DNSNames, IPAddresses, EmailAddresses, URIs bool
|
||||
}
|
||||
|
||||
// OID is our own version of asn1's ObjectIdentifier, so we can define a custom
|
||||
// JSON marshal / unmarshal.
|
||||
type OID asn1.ObjectIdentifier
|
||||
|
||||
// CertificatePolicy represents the ASN.1 PolicyInformation structure from
|
||||
// https://tools.ietf.org/html/rfc3280.html#page-106.
|
||||
// Valid values of Type are "id-qt-unotice" and "id-qt-cps"
|
||||
type CertificatePolicy struct {
|
||||
ID OID
|
||||
Qualifiers []CertificatePolicyQualifier
|
||||
}
|
||||
|
||||
// CertificatePolicyQualifier represents a single qualifier from an ASN.1
|
||||
// PolicyInformation structure.
|
||||
type CertificatePolicyQualifier struct {
|
||||
Type string
|
||||
Value string
|
||||
}
|
||||
|
||||
// AuthRemote is an authenticated remote signer.
|
||||
type AuthRemote struct {
|
||||
RemoteName string `json:"remote"`
|
||||
AuthKeyName string `json:"auth_key"`
|
||||
}
|
||||
|
||||
// CAConstraint specifies various CA constraints on the signed certificate.
|
||||
// CAConstraint would verify against (and override) the CA
|
||||
// extensions in the given CSR.
|
||||
type CAConstraint struct {
|
||||
IsCA bool `json:"is_ca"`
|
||||
MaxPathLen int `json:"max_path_len"`
|
||||
MaxPathLenZero bool `json:"max_path_len_zero"`
|
||||
}
|
||||
|
||||
// A SigningProfile stores information that the CA needs to store
|
||||
// signature policy.
|
||||
type SigningProfile struct {
|
||||
Usage []string `json:"usages"`
|
||||
IssuerURL []string `json:"issuer_urls"`
|
||||
OCSP string `json:"ocsp_url"`
|
||||
CRL string `json:"crl_url"`
|
||||
CAConstraint CAConstraint `json:"ca_constraint"`
|
||||
OCSPNoCheck bool `json:"ocsp_no_check"`
|
||||
ExpiryString string `json:"expiry"`
|
||||
BackdateString string `json:"backdate"`
|
||||
AuthKeyName string `json:"auth_key"`
|
||||
PrevAuthKeyName string `json:"prev_auth_key"` // to suppport key rotation
|
||||
RemoteName string `json:"remote"`
|
||||
NotBefore time.Time `json:"not_before"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
NameWhitelistString string `json:"name_whitelist"`
|
||||
AuthRemote AuthRemote `json:"auth_remote"`
|
||||
CTLogServers []string `json:"ct_log_servers"`
|
||||
AllowedExtensions []OID `json:"allowed_extensions"`
|
||||
CertStore string `json:"cert_store"`
|
||||
// LintErrLevel controls preissuance linting for the signing profile.
|
||||
// 0 = no linting is performed [default]
|
||||
// 2..3 = reserved
|
||||
// 3 = all lint results except pass are considered errors
|
||||
// 4 = all lint results except pass and notice are considered errors
|
||||
// 5 = all lint results except pass, notice and warn are considered errors
|
||||
// 6 = all lint results except pass, notice, warn and error are considered errors.
|
||||
// 7 = lint is performed, no lint results are treated as errors.
|
||||
LintErrLevel lint.LintStatus `json:"lint_error_level"`
|
||||
// ExcludeLints lists ZLint lint names to exclude from preissuance linting.
|
||||
ExcludeLints []string `json:"ignored_lints"`
|
||||
// ExcludeLintSources lists ZLint lint sources to exclude from preissuance
|
||||
// linting.
|
||||
ExcludeLintSources []string `json:"ignored_lint_sources"`
|
||||
|
||||
Policies []CertificatePolicy
|
||||
Expiry time.Duration
|
||||
Backdate time.Duration
|
||||
Provider auth.Provider
|
||||
PrevProvider auth.Provider // to suppport key rotation
|
||||
RemoteProvider auth.Provider
|
||||
RemoteServer string
|
||||
RemoteCAs *x509.CertPool
|
||||
ClientCert *tls.Certificate
|
||||
CSRWhitelist *CSRWhitelist
|
||||
NameWhitelist *regexp.Regexp
|
||||
ExtensionWhitelist map[string]bool
|
||||
ClientProvidesSerialNumbers bool
|
||||
// LintRegistry is the collection of lints that should be used if
|
||||
// LintErrLevel is configured. By default all ZLint lints are used. If
|
||||
// ExcludeLints or ExcludeLintSources are set then this registry will be
|
||||
// filtered in populate() to exclude the named lints and lint sources.
|
||||
LintRegistry lint.Registry
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals a JSON string into an OID.
|
||||
func (oid *OID) UnmarshalJSON(data []byte) (err error) {
|
||||
if data[0] != '"' || data[len(data)-1] != '"' {
|
||||
return errors.New("OID JSON string not wrapped in quotes." + string(data))
|
||||
}
|
||||
data = data[1 : len(data)-1]
|
||||
parsedOid, err := parseObjectIdentifier(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*oid = OID(parsedOid)
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalJSON marshals an oid into a JSON string.
|
||||
func (oid OID) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf(`"%v"`, asn1.ObjectIdentifier(oid))), nil
|
||||
}
|
||||
|
||||
func parseObjectIdentifier(oidString string) (oid asn1.ObjectIdentifier, err error) {
|
||||
validOID, err := regexp.MatchString("\\d(\\.\\d+)*", oidString)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !validOID {
|
||||
err = errors.New("Invalid OID")
|
||||
return
|
||||
}
|
||||
|
||||
segments := strings.Split(oidString, ".")
|
||||
oid = make(asn1.ObjectIdentifier, len(segments))
|
||||
for i, intString := range segments {
|
||||
oid[i], err = strconv.Atoi(intString)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const timeFormat = "2006-01-02T15:04:05"
|
||||
|
||||
// populate is used to fill in the fields that are not in JSON
|
||||
//
|
||||
// First, the ExpiryString parameter is needed to parse
|
||||
// expiration timestamps from JSON. The JSON decoder is not able to
|
||||
// decode a string time duration to a time.Duration, so this is called
|
||||
// when loading the configuration to properly parse and fill out the
|
||||
// Expiry parameter.
|
||||
// This function is also used to create references to the auth key
|
||||
// and default remote for the profile.
|
||||
// It returns true if ExpiryString is a valid representation of a
|
||||
// time.Duration, and the AuthKeyString and RemoteName point to
|
||||
// valid objects. It returns false otherwise.
|
||||
func (p *SigningProfile) populate(cfg *Config) error {
|
||||
if p == nil {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("can't parse nil profile"))
|
||||
}
|
||||
|
||||
var err error
|
||||
if p.RemoteName == "" && p.AuthRemote.RemoteName == "" {
|
||||
log.Debugf("parse expiry in profile")
|
||||
if p.ExpiryString == "" {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("empty expiry string"))
|
||||
}
|
||||
|
||||
dur, err := time.ParseDuration(p.ExpiryString)
|
||||
if err != nil {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err)
|
||||
}
|
||||
|
||||
log.Debugf("expiry is valid")
|
||||
p.Expiry = dur
|
||||
|
||||
if p.BackdateString != "" {
|
||||
dur, err = time.ParseDuration(p.BackdateString)
|
||||
if err != nil {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err)
|
||||
}
|
||||
|
||||
p.Backdate = dur
|
||||
}
|
||||
|
||||
if !p.NotBefore.IsZero() && !p.NotAfter.IsZero() && p.NotAfter.Before(p.NotBefore) {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err)
|
||||
}
|
||||
|
||||
if len(p.Policies) > 0 {
|
||||
for _, policy := range p.Policies {
|
||||
for _, qualifier := range policy.Qualifiers {
|
||||
if qualifier.Type != "" && qualifier.Type != "id-qt-unotice" && qualifier.Type != "id-qt-cps" {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("invalid policy qualifier type"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if p.RemoteName != "" {
|
||||
log.Debug("match remote in profile to remotes section")
|
||||
if p.AuthRemote.RemoteName != "" {
|
||||
log.Error("profile has both a remote and an auth remote specified")
|
||||
return cferr.New(cferr.PolicyError, cferr.InvalidPolicy)
|
||||
}
|
||||
if remote := cfg.Remotes[p.RemoteName]; remote != "" {
|
||||
if err := p.updateRemote(remote); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to find remote in remotes section"))
|
||||
}
|
||||
} else {
|
||||
log.Debug("match auth remote in profile to remotes section")
|
||||
if remote := cfg.Remotes[p.AuthRemote.RemoteName]; remote != "" {
|
||||
if err := p.updateRemote(remote); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to find remote in remotes section"))
|
||||
}
|
||||
}
|
||||
|
||||
if p.AuthKeyName != "" {
|
||||
log.Debug("match auth key in profile to auth_keys section")
|
||||
if key, ok := cfg.AuthKeys[p.AuthKeyName]; ok {
|
||||
if key.Type == "standard" {
|
||||
p.Provider, err = auth.New(key.Key, nil)
|
||||
if err != nil {
|
||||
log.Debugf("failed to create new standard auth provider: %v", err)
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to create new standard auth provider"))
|
||||
}
|
||||
} else {
|
||||
log.Debugf("unknown authentication type %v", key.Type)
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("unknown authentication type"))
|
||||
}
|
||||
} else {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to find auth_key in auth_keys section"))
|
||||
}
|
||||
}
|
||||
|
||||
if p.PrevAuthKeyName != "" {
|
||||
log.Debug("match previous auth key in profile to auth_keys section")
|
||||
if key, ok := cfg.AuthKeys[p.PrevAuthKeyName]; ok {
|
||||
if key.Type == "standard" {
|
||||
p.PrevProvider, err = auth.New(key.Key, nil)
|
||||
if err != nil {
|
||||
log.Debugf("failed to create new standard auth provider: %v", err)
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to create new standard auth provider"))
|
||||
}
|
||||
} else {
|
||||
log.Debugf("unknown authentication type %v", key.Type)
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("unknown authentication type"))
|
||||
}
|
||||
} else {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to find prev_auth_key in auth_keys section"))
|
||||
}
|
||||
}
|
||||
|
||||
if p.AuthRemote.AuthKeyName != "" {
|
||||
log.Debug("match auth remote key in profile to auth_keys section")
|
||||
if key, ok := cfg.AuthKeys[p.AuthRemote.AuthKeyName]; ok == true {
|
||||
if key.Type == "standard" {
|
||||
p.RemoteProvider, err = auth.New(key.Key, nil)
|
||||
if err != nil {
|
||||
log.Debugf("failed to create new standard auth provider: %v", err)
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to create new standard auth provider"))
|
||||
}
|
||||
} else {
|
||||
log.Debugf("unknown authentication type %v", key.Type)
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("unknown authentication type"))
|
||||
}
|
||||
} else {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to find auth_remote's auth_key in auth_keys section"))
|
||||
}
|
||||
}
|
||||
|
||||
if p.NameWhitelistString != "" {
|
||||
log.Debug("compiling whitelist regular expression")
|
||||
rule, err := regexp.Compile(p.NameWhitelistString)
|
||||
if err != nil {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to compile name whitelist section"))
|
||||
}
|
||||
p.NameWhitelist = rule
|
||||
}
|
||||
|
||||
p.ExtensionWhitelist = map[string]bool{}
|
||||
for _, oid := range p.AllowedExtensions {
|
||||
p.ExtensionWhitelist[asn1.ObjectIdentifier(oid).String()] = true
|
||||
}
|
||||
|
||||
// By default perform any required preissuance linting with all ZLint lints.
|
||||
p.LintRegistry = lint.GlobalRegistry()
|
||||
|
||||
// If ExcludeLintSources are present in config build a lint.SourceList while
|
||||
// validating that no unknown sources were specified.
|
||||
var excludedSources lint.SourceList
|
||||
if len(p.ExcludeLintSources) > 0 {
|
||||
for _, sourceName := range p.ExcludeLintSources {
|
||||
var lintSource lint.LintSource
|
||||
lintSource.FromString(sourceName)
|
||||
if lintSource == lint.UnknownLintSource {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
fmt.Errorf("failed to build excluded lint source list: unknown source %q",
|
||||
sourceName))
|
||||
}
|
||||
excludedSources = append(excludedSources, lintSource)
|
||||
}
|
||||
}
|
||||
|
||||
opts := lint.FilterOptions{
|
||||
ExcludeNames: p.ExcludeLints,
|
||||
ExcludeSources: excludedSources,
|
||||
}
|
||||
if !opts.Empty() {
|
||||
// If ExcludeLints or ExcludeLintSources were not empty then filter out the
|
||||
// lints we don't want to use for preissuance linting with this profile.
|
||||
filteredRegistry, err := p.LintRegistry.Filter(opts)
|
||||
if err != nil {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
fmt.Errorf("failed to build filtered lint registry: %v", err))
|
||||
}
|
||||
p.LintRegistry = filteredRegistry
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateRemote takes a signing profile and initializes the remote server object
|
||||
// to the hostname:port combination sent by remote.
|
||||
func (p *SigningProfile) updateRemote(remote string) error {
|
||||
if remote != "" {
|
||||
p.RemoteServer = remote
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OverrideRemotes takes a signing configuration and updates the remote server object
|
||||
// to the hostname:port combination sent by remote
|
||||
func (p *Signing) OverrideRemotes(remote string) error {
|
||||
if remote != "" {
|
||||
var err error
|
||||
for _, profile := range p.Profiles {
|
||||
err = profile.updateRemote(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = p.Default.updateRemote(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetClientCertKeyPairFromFile updates the properties to set client certificates for mutual
|
||||
// authenticated TLS remote requests
|
||||
func (p *Signing) SetClientCertKeyPairFromFile(certFile string, keyFile string) error {
|
||||
if certFile != "" && keyFile != "" {
|
||||
cert, err := helpers.LoadClientCertificate(certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, profile := range p.Profiles {
|
||||
profile.ClientCert = cert
|
||||
}
|
||||
p.Default.ClientCert = cert
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRemoteCAsFromFile reads root CAs from file and updates the properties to set remote CAs for TLS
|
||||
// remote requests
|
||||
func (p *Signing) SetRemoteCAsFromFile(caFile string) error {
|
||||
if caFile != "" {
|
||||
remoteCAs, err := helpers.LoadPEMCertPool(caFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.SetRemoteCAs(remoteCAs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRemoteCAs updates the properties to set remote CAs for TLS
|
||||
// remote requests
|
||||
func (p *Signing) SetRemoteCAs(remoteCAs *x509.CertPool) {
|
||||
for _, profile := range p.Profiles {
|
||||
profile.RemoteCAs = remoteCAs
|
||||
}
|
||||
p.Default.RemoteCAs = remoteCAs
|
||||
}
|
||||
|
||||
// NeedsRemoteSigner returns true if one of the profiles has a remote set
|
||||
func (p *Signing) NeedsRemoteSigner() bool {
|
||||
for _, profile := range p.Profiles {
|
||||
if profile.RemoteServer != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if p.Default.RemoteServer != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NeedsLocalSigner returns true if one of the profiles doe not have a remote set
|
||||
func (p *Signing) NeedsLocalSigner() bool {
|
||||
for _, profile := range p.Profiles {
|
||||
if profile.RemoteServer == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if p.Default.RemoteServer == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Usages parses the list of key uses in the profile, translating them
|
||||
// to a list of X.509 key usages and extended key usages. The unknown
|
||||
// uses are collected into a slice that is also returned.
|
||||
func (p *SigningProfile) Usages() (ku x509.KeyUsage, eku []x509.ExtKeyUsage, unk []string) {
|
||||
for _, keyUse := range p.Usage {
|
||||
if kuse, ok := KeyUsage[keyUse]; ok {
|
||||
ku |= kuse
|
||||
} else if ekuse, ok := ExtKeyUsage[keyUse]; ok {
|
||||
eku = append(eku, ekuse)
|
||||
} else {
|
||||
unk = append(unk, keyUse)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// A valid profile must be a valid local profile or a valid remote profile.
|
||||
// A valid local profile has defined at least key usages to be used, and a
|
||||
// valid local default profile has defined at least a default expiration.
|
||||
// A valid remote profile (default or not) has remote signer initialized.
|
||||
// In addition, a remote profile must has a valid auth provider if auth
|
||||
// key defined. A valid profile must not include a lint_error_level outside of
|
||||
// [0,8).
|
||||
func (p *SigningProfile) validProfile(isDefault bool) bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if p.AuthRemote.RemoteName == "" && p.AuthRemote.AuthKeyName != "" {
|
||||
log.Debugf("invalid auth remote profile: no remote signer specified")
|
||||
return false
|
||||
}
|
||||
|
||||
if p.RemoteName != "" {
|
||||
log.Debugf("validate remote profile")
|
||||
|
||||
if p.RemoteServer == "" {
|
||||
log.Debugf("invalid remote profile: no remote signer specified")
|
||||
return false
|
||||
}
|
||||
|
||||
if p.AuthKeyName != "" && p.Provider == nil {
|
||||
log.Debugf("invalid remote profile: auth key name is defined but no auth provider is set")
|
||||
return false
|
||||
}
|
||||
|
||||
if p.AuthRemote.RemoteName != "" {
|
||||
log.Debugf("invalid remote profile: auth remote is also specified")
|
||||
return false
|
||||
}
|
||||
} else if p.AuthRemote.RemoteName != "" {
|
||||
log.Debugf("validate auth remote profile")
|
||||
if p.RemoteServer == "" {
|
||||
log.Debugf("invalid auth remote profile: no remote signer specified")
|
||||
return false
|
||||
}
|
||||
|
||||
if p.AuthRemote.AuthKeyName == "" || p.RemoteProvider == nil {
|
||||
log.Debugf("invalid auth remote profile: no auth key is defined")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
log.Debugf("validate local profile")
|
||||
if !isDefault {
|
||||
if len(p.Usage) == 0 {
|
||||
log.Debugf("invalid local profile: no usages specified")
|
||||
return false
|
||||
} else if _, _, unk := p.Usages(); len(unk) == len(p.Usage) {
|
||||
log.Debugf("invalid local profile: no valid usages")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if p.Expiry == 0 {
|
||||
log.Debugf("invalid local profile: no expiry set")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.LintErrLevel < 0 || p.LintErrLevel >= 8 {
|
||||
log.Debugf("invalid profile: lint_error_level outside of range [0,8)")
|
||||
return false
|
||||
}
|
||||
|
||||
log.Debugf("profile is valid")
|
||||
return true
|
||||
}
|
||||
|
||||
// This checks if the SigningProfile object contains configurations that are only effective with a local signer
|
||||
// which has access to CA private key.
|
||||
func (p *SigningProfile) hasLocalConfig() bool {
|
||||
if p.Usage != nil ||
|
||||
p.IssuerURL != nil ||
|
||||
p.OCSP != "" ||
|
||||
p.ExpiryString != "" ||
|
||||
p.BackdateString != "" ||
|
||||
p.CAConstraint.IsCA != false ||
|
||||
!p.NotBefore.IsZero() ||
|
||||
!p.NotAfter.IsZero() ||
|
||||
p.NameWhitelistString != "" ||
|
||||
len(p.CTLogServers) != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// warnSkippedSettings prints a log warning message about skipped settings
|
||||
// in a SigningProfile, usually due to remote signer.
|
||||
func (p *Signing) warnSkippedSettings() {
|
||||
const warningMessage = `The configuration value by "usages", "issuer_urls", "ocsp_url", "crl_url", "ca_constraint", "expiry", "backdate", "not_before", "not_after", "cert_store" and "ct_log_servers" are skipped`
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if (p.Default.RemoteName != "" || p.Default.AuthRemote.RemoteName != "") && p.Default.hasLocalConfig() {
|
||||
log.Warning("default profile points to a remote signer: ", warningMessage)
|
||||
}
|
||||
|
||||
for name, profile := range p.Profiles {
|
||||
if (profile.RemoteName != "" || profile.AuthRemote.RemoteName != "") && profile.hasLocalConfig() {
|
||||
log.Warningf("Profiles[%s] points to a remote signer: %s", name, warningMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Signing codifies the signature configuration policy for a CA.
|
||||
type Signing struct {
|
||||
Profiles map[string]*SigningProfile `json:"profiles"`
|
||||
Default *SigningProfile `json:"default"`
|
||||
}
|
||||
|
||||
// Config stores configuration information for the CA.
|
||||
type Config struct {
|
||||
Signing *Signing `json:"signing"`
|
||||
OCSP *ocspConfig.Config `json:"ocsp"`
|
||||
AuthKeys map[string]AuthKey `json:"auth_keys,omitempty"`
|
||||
Remotes map[string]string `json:"remotes,omitempty"`
|
||||
}
|
||||
|
||||
// Valid ensures that Config is a valid configuration. It should be
|
||||
// called immediately after parsing a configuration file.
|
||||
func (c *Config) Valid() bool {
|
||||
return c.Signing.Valid()
|
||||
}
|
||||
|
||||
// Valid checks the signature policies, ensuring they are valid
|
||||
// policies. A policy is valid if it has defined at least key usages
|
||||
// to be used, and a valid default profile has defined at least a
|
||||
// default expiration.
|
||||
func (p *Signing) Valid() bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
log.Debugf("validating configuration")
|
||||
if !p.Default.validProfile(true) {
|
||||
log.Debugf("default profile is invalid")
|
||||
return false
|
||||
}
|
||||
|
||||
for _, sp := range p.Profiles {
|
||||
if !sp.validProfile(false) {
|
||||
log.Debugf("invalid profile")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
p.warnSkippedSettings()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// KeyUsage contains a mapping of string names to key usages.
|
||||
var KeyUsage = map[string]x509.KeyUsage{
|
||||
"signing": x509.KeyUsageDigitalSignature,
|
||||
"digital signature": x509.KeyUsageDigitalSignature,
|
||||
"content commitment": x509.KeyUsageContentCommitment,
|
||||
"key encipherment": x509.KeyUsageKeyEncipherment,
|
||||
"key agreement": x509.KeyUsageKeyAgreement,
|
||||
"data encipherment": x509.KeyUsageDataEncipherment,
|
||||
"cert sign": x509.KeyUsageCertSign,
|
||||
"crl sign": x509.KeyUsageCRLSign,
|
||||
"encipher only": x509.KeyUsageEncipherOnly,
|
||||
"decipher only": x509.KeyUsageDecipherOnly,
|
||||
}
|
||||
|
||||
// ExtKeyUsage contains a mapping of string names to extended key
|
||||
// usages.
|
||||
var ExtKeyUsage = map[string]x509.ExtKeyUsage{
|
||||
"any": x509.ExtKeyUsageAny,
|
||||
"server auth": x509.ExtKeyUsageServerAuth,
|
||||
"client auth": x509.ExtKeyUsageClientAuth,
|
||||
"code signing": x509.ExtKeyUsageCodeSigning,
|
||||
"email protection": x509.ExtKeyUsageEmailProtection,
|
||||
"s/mime": x509.ExtKeyUsageEmailProtection,
|
||||
"ipsec end system": x509.ExtKeyUsageIPSECEndSystem,
|
||||
"ipsec tunnel": x509.ExtKeyUsageIPSECTunnel,
|
||||
"ipsec user": x509.ExtKeyUsageIPSECUser,
|
||||
"timestamping": x509.ExtKeyUsageTimeStamping,
|
||||
"ocsp signing": x509.ExtKeyUsageOCSPSigning,
|
||||
"microsoft sgc": x509.ExtKeyUsageMicrosoftServerGatedCrypto,
|
||||
"netscape sgc": x509.ExtKeyUsageNetscapeServerGatedCrypto,
|
||||
}
|
||||
|
||||
// An AuthKey contains an entry for a key used for authentication.
|
||||
type AuthKey struct {
|
||||
// Type contains information needed to select the appropriate
|
||||
// constructor. For example, "standard" for HMAC-SHA-256,
|
||||
// "standard-ip" for HMAC-SHA-256 incorporating the client's
|
||||
// IP.
|
||||
Type string `json:"type"`
|
||||
// Key contains the key information, such as a hex-encoded
|
||||
// HMAC key.
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// DefaultConfig returns a default configuration specifying basic key
|
||||
// usage and a 1 year expiration time. The key usages chosen are
|
||||
// signing, key encipherment, client auth and server auth.
|
||||
func DefaultConfig() *SigningProfile {
|
||||
d := helpers.OneYear
|
||||
return &SigningProfile{
|
||||
Usage: []string{"signing", "key encipherment", "server auth", "client auth"},
|
||||
Expiry: d,
|
||||
ExpiryString: "8760h",
|
||||
}
|
||||
}
|
||||
|
||||
// LoadFile attempts to load the configuration file stored at the path
|
||||
// and returns the configuration. On error, it returns nil.
|
||||
func LoadFile(path string) (*Config, error) {
|
||||
log.Debugf("loading configuration file from %s", path)
|
||||
if path == "" {
|
||||
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("invalid path"))
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("could not read configuration file"))
|
||||
}
|
||||
|
||||
return LoadConfig(body)
|
||||
}
|
||||
|
||||
// LoadConfig attempts to load the configuration from a byte slice.
|
||||
// On error, it returns nil.
|
||||
func LoadConfig(config []byte) (*Config, error) {
|
||||
var cfg = &Config{}
|
||||
err := json.Unmarshal(config, &cfg)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy,
|
||||
errors.New("failed to unmarshal configuration: "+err.Error()))
|
||||
}
|
||||
|
||||
if cfg.Signing == nil {
|
||||
return nil, errors.New("No \"signing\" field present")
|
||||
}
|
||||
|
||||
if cfg.Signing.Default == nil {
|
||||
log.Debugf("no default given: using default config")
|
||||
cfg.Signing.Default = DefaultConfig()
|
||||
} else {
|
||||
if err := cfg.Signing.Default.populate(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for k := range cfg.Signing.Profiles {
|
||||
if err := cfg.Signing.Profiles[k].populate(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.Valid() {
|
||||
return nil, cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, errors.New("invalid configuration"))
|
||||
}
|
||||
|
||||
log.Debugf("configuration ok")
|
||||
return cfg, nil
|
||||
}
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
// Package csr implements certificate requests for CFSSL.
|
||||
package csr
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
cferr "github.com/cloudflare/cfssl/errors"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
)
|
||||
|
||||
const (
|
||||
curveP256 = 256
|
||||
curveP384 = 384
|
||||
curveP521 = 521
|
||||
)
|
||||
|
||||
// A Name contains the SubjectInfo fields.
|
||||
type Name struct {
|
||||
C string `json:"C,omitempty" yaml:"C,omitempty"` // Country
|
||||
ST string `json:"ST,omitempty" yaml:"ST,omitempty"` // State
|
||||
L string `json:"L,omitempty" yaml:"L,omitempty"` // Locality
|
||||
O string `json:"O,omitempty" yaml:"O,omitempty"` // OrganisationName
|
||||
OU string `json:"OU,omitempty" yaml:"OU,omitempty"` // OrganisationalUnitName
|
||||
SerialNumber string `json:"SerialNumber,omitempty" yaml:"SerialNumber,omitempty"`
|
||||
}
|
||||
|
||||
// A KeyRequest contains the algorithm and key size for a new private key.
|
||||
type KeyRequest struct {
|
||||
A string `json:"algo" yaml:"algo"`
|
||||
S int `json:"size" yaml:"size"`
|
||||
}
|
||||
|
||||
// NewKeyRequest returns a default KeyRequest.
|
||||
func NewKeyRequest() *KeyRequest {
|
||||
return &KeyRequest{"ecdsa", curveP256}
|
||||
}
|
||||
|
||||
// Algo returns the requested key algorithm represented as a string.
|
||||
func (kr *KeyRequest) Algo() string {
|
||||
return kr.A
|
||||
}
|
||||
|
||||
// Size returns the requested key size.
|
||||
func (kr *KeyRequest) Size() int {
|
||||
return kr.S
|
||||
}
|
||||
|
||||
// Generate generates a key as specified in the request. Currently,
|
||||
// only ECDSA and RSA are supported.
|
||||
func (kr *KeyRequest) Generate() (crypto.PrivateKey, error) {
|
||||
log.Debugf("generate key from request: algo=%s, size=%d", kr.Algo(), kr.Size())
|
||||
switch kr.Algo() {
|
||||
case "rsa":
|
||||
if kr.Size() < 2048 {
|
||||
return nil, errors.New("RSA key is too weak")
|
||||
}
|
||||
if kr.Size() > 8192 {
|
||||
return nil, errors.New("RSA key size too large")
|
||||
}
|
||||
return rsa.GenerateKey(rand.Reader, kr.Size())
|
||||
case "ecdsa":
|
||||
var curve elliptic.Curve
|
||||
switch kr.Size() {
|
||||
case curveP256:
|
||||
curve = elliptic.P256()
|
||||
case curveP384:
|
||||
curve = elliptic.P384()
|
||||
case curveP521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return nil, errors.New("invalid curve")
|
||||
}
|
||||
return ecdsa.GenerateKey(curve, rand.Reader)
|
||||
default:
|
||||
return nil, errors.New("invalid algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
// SigAlgo returns an appropriate X.509 signature algorithm given the
|
||||
// key request's type and size.
|
||||
func (kr *KeyRequest) SigAlgo() x509.SignatureAlgorithm {
|
||||
switch kr.Algo() {
|
||||
case "rsa":
|
||||
switch {
|
||||
case kr.Size() >= 4096:
|
||||
return x509.SHA512WithRSA
|
||||
case kr.Size() >= 3072:
|
||||
return x509.SHA384WithRSA
|
||||
case kr.Size() >= 2048:
|
||||
return x509.SHA256WithRSA
|
||||
default:
|
||||
return x509.SHA1WithRSA
|
||||
}
|
||||
case "ecdsa":
|
||||
switch kr.Size() {
|
||||
case curveP521:
|
||||
return x509.ECDSAWithSHA512
|
||||
case curveP384:
|
||||
return x509.ECDSAWithSHA384
|
||||
case curveP256:
|
||||
return x509.ECDSAWithSHA256
|
||||
default:
|
||||
return x509.ECDSAWithSHA1
|
||||
}
|
||||
default:
|
||||
return x509.UnknownSignatureAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
// CAConfig is a section used in the requests initialising a new CA.
|
||||
type CAConfig struct {
|
||||
PathLength int `json:"pathlen" yaml:"pathlen"`
|
||||
PathLenZero bool `json:"pathlenzero" yaml:"pathlenzero"`
|
||||
Expiry string `json:"expiry" yaml:"expiry"`
|
||||
Backdate string `json:"backdate" yaml:"backdate"`
|
||||
}
|
||||
|
||||
// A CertificateRequest encapsulates the API interface to the
|
||||
// certificate request functionality.
|
||||
type CertificateRequest struct {
|
||||
CN string `json:"CN" yaml:"CN"`
|
||||
Names []Name `json:"names" yaml:"names"`
|
||||
Hosts []string `json:"hosts" yaml:"hosts"`
|
||||
KeyRequest *KeyRequest `json:"key,omitempty" yaml:"key,omitempty"`
|
||||
CA *CAConfig `json:"ca,omitempty" yaml:"ca,omitempty"`
|
||||
SerialNumber string `json:"serialnumber,omitempty" yaml:"serialnumber,omitempty"`
|
||||
}
|
||||
|
||||
// New returns a new, empty CertificateRequest with a
|
||||
// KeyRequest.
|
||||
func New() *CertificateRequest {
|
||||
return &CertificateRequest{
|
||||
KeyRequest: NewKeyRequest(),
|
||||
}
|
||||
}
|
||||
|
||||
// appendIf appends to a if s is not an empty string.
|
||||
func appendIf(s string, a *[]string) {
|
||||
if s != "" {
|
||||
*a = append(*a, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the PKIX name for the request.
|
||||
func (cr *CertificateRequest) Name() pkix.Name {
|
||||
var name pkix.Name
|
||||
name.CommonName = cr.CN
|
||||
|
||||
for _, n := range cr.Names {
|
||||
appendIf(n.C, &name.Country)
|
||||
appendIf(n.ST, &name.Province)
|
||||
appendIf(n.L, &name.Locality)
|
||||
appendIf(n.O, &name.Organization)
|
||||
appendIf(n.OU, &name.OrganizationalUnit)
|
||||
}
|
||||
name.SerialNumber = cr.SerialNumber
|
||||
return name
|
||||
}
|
||||
|
||||
// BasicConstraints CSR information RFC 5280, 4.2.1.9
|
||||
type BasicConstraints struct {
|
||||
IsCA bool `asn1:"optional"`
|
||||
MaxPathLen int `asn1:"optional,default:-1"`
|
||||
}
|
||||
|
||||
// ParseRequest takes a certificate request and generates a key and
|
||||
// CSR from it. It does no validation -- caveat emptor. It will,
|
||||
// however, fail if the key request is not valid (i.e., an unsupported
|
||||
// curve or RSA key size). The lack of validation was specifically
|
||||
// chosen to allow the end user to define a policy and validate the
|
||||
// request appropriately before calling this function.
|
||||
func ParseRequest(req *CertificateRequest) (csr, key []byte, err error) {
|
||||
log.Info("received CSR")
|
||||
if req.KeyRequest == nil {
|
||||
req.KeyRequest = NewKeyRequest()
|
||||
}
|
||||
|
||||
log.Infof("generating key: %s-%d", req.KeyRequest.Algo(), req.KeyRequest.Size())
|
||||
priv, err := req.KeyRequest.Generate()
|
||||
if err != nil {
|
||||
err = cferr.Wrap(cferr.PrivateKeyError, cferr.GenerationFailed, err)
|
||||
return
|
||||
}
|
||||
|
||||
switch priv := priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
key = x509.MarshalPKCS1PrivateKey(priv)
|
||||
block := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: key,
|
||||
}
|
||||
key = pem.EncodeToMemory(&block)
|
||||
case *ecdsa.PrivateKey:
|
||||
key, err = x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
err = cferr.Wrap(cferr.PrivateKeyError, cferr.Unknown, err)
|
||||
return
|
||||
}
|
||||
block := pem.Block{
|
||||
Type: "EC PRIVATE KEY",
|
||||
Bytes: key,
|
||||
}
|
||||
key = pem.EncodeToMemory(&block)
|
||||
default:
|
||||
panic("Generate should have failed to produce a valid key.")
|
||||
}
|
||||
|
||||
csr, err = Generate(priv.(crypto.Signer), req)
|
||||
if err != nil {
|
||||
log.Errorf("failed to generate a CSR: %v", err)
|
||||
err = cferr.Wrap(cferr.CSRError, cferr.BadRequest, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ExtractCertificateRequest extracts a CertificateRequest from
|
||||
// x509.Certificate. It is aimed to used for generating a new certificate
|
||||
// from an existing certificate. For a root certificate, the CA expiry
|
||||
// length is calculated as the duration between cert.NotAfter and cert.NotBefore.
|
||||
func ExtractCertificateRequest(cert *x509.Certificate) *CertificateRequest {
|
||||
req := New()
|
||||
req.CN = cert.Subject.CommonName
|
||||
req.Names = getNames(cert.Subject)
|
||||
req.Hosts = getHosts(cert)
|
||||
req.SerialNumber = cert.Subject.SerialNumber
|
||||
|
||||
if cert.IsCA {
|
||||
req.CA = new(CAConfig)
|
||||
// CA expiry length is calculated based on the input cert
|
||||
// issue date and expiry date.
|
||||
req.CA.Expiry = cert.NotAfter.Sub(cert.NotBefore).String()
|
||||
req.CA.PathLength = cert.MaxPathLen
|
||||
req.CA.PathLenZero = cert.MaxPathLenZero
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func getHosts(cert *x509.Certificate) []string {
|
||||
var hosts []string
|
||||
for _, ip := range cert.IPAddresses {
|
||||
hosts = append(hosts, ip.String())
|
||||
}
|
||||
for _, dns := range cert.DNSNames {
|
||||
hosts = append(hosts, dns)
|
||||
}
|
||||
for _, email := range cert.EmailAddresses {
|
||||
hosts = append(hosts, email)
|
||||
}
|
||||
for _, uri := range cert.URIs {
|
||||
hosts = append(hosts, uri.String())
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
// getNames returns an array of Names from the certificate
|
||||
// It onnly cares about Country, Organization, OrganizationalUnit, Locality, Province
|
||||
func getNames(sub pkix.Name) []Name {
|
||||
// anonymous func for finding the max of a list of interger
|
||||
max := func(v1 int, vn ...int) (max int) {
|
||||
max = v1
|
||||
for i := 0; i < len(vn); i++ {
|
||||
if vn[i] > max {
|
||||
max = vn[i]
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
nc := len(sub.Country)
|
||||
norg := len(sub.Organization)
|
||||
nou := len(sub.OrganizationalUnit)
|
||||
nl := len(sub.Locality)
|
||||
np := len(sub.Province)
|
||||
|
||||
n := max(nc, norg, nou, nl, np)
|
||||
|
||||
names := make([]Name, n)
|
||||
for i := range names {
|
||||
if i < nc {
|
||||
names[i].C = sub.Country[i]
|
||||
}
|
||||
if i < norg {
|
||||
names[i].O = sub.Organization[i]
|
||||
}
|
||||
if i < nou {
|
||||
names[i].OU = sub.OrganizationalUnit[i]
|
||||
}
|
||||
if i < nl {
|
||||
names[i].L = sub.Locality[i]
|
||||
}
|
||||
if i < np {
|
||||
names[i].ST = sub.Province[i]
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// A Generator is responsible for validating certificate requests.
|
||||
type Generator struct {
|
||||
Validator func(*CertificateRequest) error
|
||||
}
|
||||
|
||||
// ProcessRequest validates and processes the incoming request. It is
|
||||
// a wrapper around a validator and the ParseRequest function.
|
||||
func (g *Generator) ProcessRequest(req *CertificateRequest) (csr, key []byte, err error) {
|
||||
|
||||
log.Info("generate received request")
|
||||
err = g.Validator(req)
|
||||
if err != nil {
|
||||
log.Warningf("invalid request: %v", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
csr, key, err = ParseRequest(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IsNameEmpty returns true if the name has no identifying information in it.
|
||||
func IsNameEmpty(n Name) bool {
|
||||
empty := func(s string) bool { return strings.TrimSpace(s) == "" }
|
||||
|
||||
if empty(n.C) && empty(n.ST) && empty(n.L) && empty(n.O) && empty(n.OU) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Regenerate uses the provided CSR as a template for signing a new
|
||||
// CSR using priv.
|
||||
func Regenerate(priv crypto.Signer, csr []byte) ([]byte, error) {
|
||||
req, extra, err := helpers.ParseCSR(csr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(extra) > 0 {
|
||||
return nil, errors.New("csr: trailing data in certificate request")
|
||||
}
|
||||
|
||||
return x509.CreateCertificateRequest(rand.Reader, req, priv)
|
||||
}
|
||||
|
||||
// Generate creates a new CSR from a CertificateRequest structure and
|
||||
// an existing key. The KeyRequest field is ignored.
|
||||
func Generate(priv crypto.Signer, req *CertificateRequest) (csr []byte, err error) {
|
||||
sigAlgo := helpers.SignerAlgo(priv)
|
||||
if sigAlgo == x509.UnknownSignatureAlgorithm {
|
||||
return nil, cferr.New(cferr.PrivateKeyError, cferr.Unavailable)
|
||||
}
|
||||
|
||||
var tpl = x509.CertificateRequest{
|
||||
Subject: req.Name(),
|
||||
SignatureAlgorithm: sigAlgo,
|
||||
}
|
||||
|
||||
for i := range req.Hosts {
|
||||
if ip := net.ParseIP(req.Hosts[i]); ip != nil {
|
||||
tpl.IPAddresses = append(tpl.IPAddresses, ip)
|
||||
} else if email, err := mail.ParseAddress(req.Hosts[i]); err == nil && email != nil {
|
||||
tpl.EmailAddresses = append(tpl.EmailAddresses, email.Address)
|
||||
} else if uri, err := url.ParseRequestURI(req.Hosts[i]); err == nil && uri != nil {
|
||||
tpl.URIs = append(tpl.URIs, uri)
|
||||
} else {
|
||||
tpl.DNSNames = append(tpl.DNSNames, req.Hosts[i])
|
||||
}
|
||||
}
|
||||
|
||||
if req.CA != nil {
|
||||
err = appendCAInfoToCSR(req.CA, &tpl)
|
||||
if err != nil {
|
||||
err = cferr.Wrap(cferr.CSRError, cferr.GenerationFailed, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
csr, err = x509.CreateCertificateRequest(rand.Reader, &tpl, priv)
|
||||
if err != nil {
|
||||
log.Errorf("failed to generate a CSR: %v", err)
|
||||
err = cferr.Wrap(cferr.CSRError, cferr.BadRequest, err)
|
||||
return
|
||||
}
|
||||
block := pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr,
|
||||
}
|
||||
|
||||
log.Info("encoded CSR")
|
||||
csr = pem.EncodeToMemory(&block)
|
||||
return
|
||||
}
|
||||
|
||||
// appendCAInfoToCSR appends CAConfig BasicConstraint extension to a CSR
|
||||
func appendCAInfoToCSR(reqConf *CAConfig, csr *x509.CertificateRequest) error {
|
||||
pathlen := reqConf.PathLength
|
||||
if pathlen == 0 && !reqConf.PathLenZero {
|
||||
pathlen = -1
|
||||
}
|
||||
val, err := asn1.Marshal(BasicConstraints{true, pathlen})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
csr.ExtraExtensions = []pkix.Extension{
|
||||
{
|
||||
Id: asn1.ObjectIdentifier{2, 5, 29, 19},
|
||||
Value: val,
|
||||
Critical: true,
|
||||
},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
// Package info contains the definitions for the info endpoint
|
||||
package info
|
||||
|
||||
// Req is the request struct for an info API request.
|
||||
type Req struct {
|
||||
Label string `json:"label"`
|
||||
Profile string `json:"profile"`
|
||||
}
|
||||
|
||||
// Resp is the response for an Info API request.
|
||||
type Resp struct {
|
||||
Certificate string `json:"certificate"`
|
||||
Usage []string `json:"usages"`
|
||||
ExpiryString string `json:"expiry"`
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
// Package config in the ocsp directory provides configuration data for an OCSP
|
||||
// signer.
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// Config contains configuration information required to set up an OCSP signer.
|
||||
type Config struct {
|
||||
CACertFile string
|
||||
ResponderCertFile string
|
||||
KeyFile string
|
||||
Interval time.Duration
|
||||
}
|
||||
|
|
@ -1,668 +0,0 @@
|
|||
// Package local implements certificate signature functionality for CFSSL.
|
||||
package local
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/cloudflare/cfssl/certdb"
|
||||
"github.com/cloudflare/cfssl/config"
|
||||
cferr "github.com/cloudflare/cfssl/errors"
|
||||
"github.com/cloudflare/cfssl/helpers"
|
||||
"github.com/cloudflare/cfssl/info"
|
||||
"github.com/cloudflare/cfssl/log"
|
||||
"github.com/cloudflare/cfssl/signer"
|
||||
"github.com/google/certificate-transparency-go"
|
||||
"github.com/google/certificate-transparency-go/client"
|
||||
"github.com/google/certificate-transparency-go/jsonclient"
|
||||
|
||||
zx509 "github.com/zmap/zcrypto/x509"
|
||||
"github.com/zmap/zlint/v2"
|
||||
"github.com/zmap/zlint/v2/lint"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Signer contains a signer that uses the standard library to
|
||||
// support both ECDSA and RSA CA keys.
|
||||
type Signer struct {
|
||||
ca *x509.Certificate
|
||||
priv crypto.Signer
|
||||
// lintPriv is generated randomly when pre-issuance linting is configured and
|
||||
// used to sign TBSCertificates for linting.
|
||||
lintPriv crypto.Signer
|
||||
policy *config.Signing
|
||||
sigAlgo x509.SignatureAlgorithm
|
||||
dbAccessor certdb.Accessor
|
||||
}
|
||||
|
||||
// NewSigner creates a new Signer directly from a
|
||||
// private key and certificate, with optional policy.
|
||||
func NewSigner(priv crypto.Signer, cert *x509.Certificate, sigAlgo x509.SignatureAlgorithm, policy *config.Signing) (*Signer, error) {
|
||||
if policy == nil {
|
||||
policy = &config.Signing{
|
||||
Profiles: map[string]*config.SigningProfile{},
|
||||
Default: config.DefaultConfig()}
|
||||
}
|
||||
|
||||
if !policy.Valid() {
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.InvalidPolicy)
|
||||
}
|
||||
|
||||
var lintPriv crypto.Signer
|
||||
// If there is at least one profile (including the default) that configures
|
||||
// pre-issuance linting then generate the one-off lintPriv key.
|
||||
for _, profile := range policy.Profiles {
|
||||
if profile.LintErrLevel > 0 || policy.Default.LintErrLevel > 0 {
|
||||
// In the future there may be demand for specifying the type of signer used
|
||||
// for pre-issuance linting in configuration. For now we assume that signing
|
||||
// with a randomly generated P-256 ECDSA private key is acceptable for all cases
|
||||
// where linting is requested.
|
||||
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, cferr.New(cferr.PrivateKeyError, cferr.GenerationFailed)
|
||||
}
|
||||
lintPriv = k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &Signer{
|
||||
ca: cert,
|
||||
priv: priv,
|
||||
lintPriv: lintPriv,
|
||||
sigAlgo: sigAlgo,
|
||||
policy: policy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewSignerFromFile generates a new local signer from a caFile
|
||||
// and a caKey file, both PEM encoded.
|
||||
func NewSignerFromFile(caFile, caKeyFile string, policy *config.Signing) (*Signer, error) {
|
||||
log.Debug("Loading CA: ", caFile)
|
||||
ca, err := helpers.ReadBytes(caFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug("Loading CA key: ", caKeyFile)
|
||||
cakey, err := helpers.ReadBytes(caKeyFile)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CertificateError, cferr.ReadFailed, err)
|
||||
}
|
||||
|
||||
parsedCa, err := helpers.ParseCertificatePEM(ca)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
strPassword := os.Getenv("CFSSL_CA_PK_PASSWORD")
|
||||
password := []byte(strPassword)
|
||||
if strPassword == "" {
|
||||
password = nil
|
||||
}
|
||||
|
||||
priv, err := helpers.ParsePrivateKeyPEMWithPassword(cakey, password)
|
||||
if err != nil {
|
||||
log.Debugf("Malformed private key %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewSigner(priv, parsedCa, signer.DefaultSigAlgo(priv), policy)
|
||||
}
|
||||
|
||||
// LintError is an error type returned when pre-issuance linting is configured
|
||||
// in a signing profile and a TBS Certificate fails linting. It wraps the
|
||||
// concrete zlint LintResults so that callers can further inspect the cause of
|
||||
// the failing lints.
|
||||
type LintError struct {
|
||||
ErrorResults map[string]lint.LintResult
|
||||
}
|
||||
|
||||
func (e *LintError) Error() string {
|
||||
return fmt.Sprintf("pre-issuance linting found %d error results",
|
||||
len(e.ErrorResults))
|
||||
}
|
||||
|
||||
// lint performs pre-issuance linting of a given TBS certificate template when
|
||||
// the provided errLevel is > 0. Note that the template is provided by-value and
|
||||
// not by-reference. This is important as the lint function needs to mutate the
|
||||
// template's signature algorithm to match the lintPriv.
|
||||
func (s *Signer) lint(template x509.Certificate, errLevel lint.LintStatus, lintRegistry lint.Registry) error {
|
||||
// Always return nil when linting is disabled (lint.Reserved == 0).
|
||||
if errLevel == lint.Reserved {
|
||||
return nil
|
||||
}
|
||||
// without a lintPriv key to use to sign the tbsCertificate we can't lint it.
|
||||
if s.lintPriv == nil {
|
||||
return cferr.New(cferr.PrivateKeyError, cferr.Unavailable)
|
||||
}
|
||||
|
||||
// The template's SignatureAlgorithm must be mutated to match the lintPriv or
|
||||
// x509.CreateCertificate will error because of the mismatch. At the time of
|
||||
// writing s.lintPriv is always an ECDSA private key. This switch will need to
|
||||
// be expanded if the lint key type is made configurable.
|
||||
switch s.lintPriv.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
template.SignatureAlgorithm = x509.ECDSAWithSHA256
|
||||
default:
|
||||
return cferr.New(cferr.PrivateKeyError, cferr.KeyMismatch)
|
||||
}
|
||||
|
||||
prelintBytes, err := x509.CreateCertificate(rand.Reader, &template, s.ca, template.PublicKey, s.lintPriv)
|
||||
if err != nil {
|
||||
return cferr.Wrap(cferr.CertificateError, cferr.Unknown, err)
|
||||
}
|
||||
prelintCert, err := zx509.ParseCertificate(prelintBytes)
|
||||
if err != nil {
|
||||
return cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
|
||||
}
|
||||
errorResults := map[string]lint.LintResult{}
|
||||
results := zlint.LintCertificateEx(prelintCert, lintRegistry)
|
||||
for name, res := range results.Results {
|
||||
if res.Status > errLevel {
|
||||
errorResults[name] = *res
|
||||
}
|
||||
}
|
||||
if len(errorResults) > 0 {
|
||||
return &LintError{
|
||||
ErrorResults: errorResults,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Signer) sign(template *x509.Certificate, lintErrLevel lint.LintStatus, lintRegistry lint.Registry) (cert []byte, err error) {
|
||||
var initRoot bool
|
||||
if s.ca == nil {
|
||||
if !template.IsCA {
|
||||
err = cferr.New(cferr.PolicyError, cferr.InvalidRequest)
|
||||
return
|
||||
}
|
||||
template.DNSNames = nil
|
||||
template.EmailAddresses = nil
|
||||
template.URIs = nil
|
||||
s.ca = template
|
||||
initRoot = true
|
||||
}
|
||||
|
||||
if err := s.lint(*template, lintErrLevel, lintRegistry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, template, s.ca, template.PublicKey, s.priv)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CertificateError, cferr.Unknown, err)
|
||||
}
|
||||
if initRoot {
|
||||
s.ca, err = x509.ParseCertificate(derBytes)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CertificateError, cferr.ParseFailed, err)
|
||||
}
|
||||
}
|
||||
|
||||
cert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
log.Infof("signed certificate with serial number %d", template.SerialNumber)
|
||||
return
|
||||
}
|
||||
|
||||
// replaceSliceIfEmpty replaces the contents of replaced with newContents if
|
||||
// the slice referenced by replaced is empty
|
||||
func replaceSliceIfEmpty(replaced, newContents *[]string) {
|
||||
if len(*replaced) == 0 {
|
||||
*replaced = *newContents
|
||||
}
|
||||
}
|
||||
|
||||
// PopulateSubjectFromCSR has functionality similar to Name, except
|
||||
// it fills the fields of the resulting pkix.Name with req's if the
|
||||
// subject's corresponding fields are empty
|
||||
func PopulateSubjectFromCSR(s *signer.Subject, req pkix.Name) pkix.Name {
|
||||
// if no subject, use req
|
||||
if s == nil {
|
||||
return req
|
||||
}
|
||||
|
||||
name := s.Name()
|
||||
|
||||
if name.CommonName == "" {
|
||||
name.CommonName = req.CommonName
|
||||
}
|
||||
|
||||
replaceSliceIfEmpty(&name.Country, &req.Country)
|
||||
replaceSliceIfEmpty(&name.Province, &req.Province)
|
||||
replaceSliceIfEmpty(&name.Locality, &req.Locality)
|
||||
replaceSliceIfEmpty(&name.Organization, &req.Organization)
|
||||
replaceSliceIfEmpty(&name.OrganizationalUnit, &req.OrganizationalUnit)
|
||||
if name.SerialNumber == "" {
|
||||
name.SerialNumber = req.SerialNumber
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// OverrideHosts fills template's IPAddresses, EmailAddresses, DNSNames, and URIs with the
|
||||
// content of hosts, if it is not nil.
|
||||
func OverrideHosts(template *x509.Certificate, hosts []string) {
|
||||
if hosts != nil {
|
||||
template.IPAddresses = []net.IP{}
|
||||
template.EmailAddresses = []string{}
|
||||
template.DNSNames = []string{}
|
||||
template.URIs = []*url.URL{}
|
||||
}
|
||||
|
||||
for i := range hosts {
|
||||
if ip := net.ParseIP(hosts[i]); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else if email, err := mail.ParseAddress(hosts[i]); err == nil && email != nil {
|
||||
template.EmailAddresses = append(template.EmailAddresses, email.Address)
|
||||
} else if uri, err := url.ParseRequestURI(hosts[i]); err == nil && uri != nil {
|
||||
template.URIs = append(template.URIs, uri)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, hosts[i])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Sign signs a new certificate based on the PEM-encoded client
|
||||
// certificate or certificate request with the signing profile,
|
||||
// specified by profileName.
|
||||
func (s *Signer) Sign(req signer.SignRequest) (cert []byte, err error) {
|
||||
profile, err := signer.Profile(s, req.Profile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
block, _ := pem.Decode([]byte(req.Request))
|
||||
if block == nil {
|
||||
return nil, cferr.New(cferr.CSRError, cferr.DecodeFailed)
|
||||
}
|
||||
|
||||
if block.Type != "NEW CERTIFICATE REQUEST" && block.Type != "CERTIFICATE REQUEST" {
|
||||
return nil, cferr.Wrap(cferr.CSRError,
|
||||
cferr.BadRequest, errors.New("not a csr"))
|
||||
}
|
||||
|
||||
csrTemplate, err := signer.ParseCertificateRequest(s, block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Copy out only the fields from the CSR authorized by policy.
|
||||
safeTemplate := x509.Certificate{}
|
||||
// If the profile contains no explicit whitelist, assume that all fields
|
||||
// should be copied from the CSR.
|
||||
if profile.CSRWhitelist == nil {
|
||||
safeTemplate = *csrTemplate
|
||||
} else {
|
||||
if profile.CSRWhitelist.Subject {
|
||||
safeTemplate.Subject = csrTemplate.Subject
|
||||
}
|
||||
if profile.CSRWhitelist.PublicKeyAlgorithm {
|
||||
safeTemplate.PublicKeyAlgorithm = csrTemplate.PublicKeyAlgorithm
|
||||
}
|
||||
if profile.CSRWhitelist.PublicKey {
|
||||
safeTemplate.PublicKey = csrTemplate.PublicKey
|
||||
}
|
||||
if profile.CSRWhitelist.SignatureAlgorithm {
|
||||
safeTemplate.SignatureAlgorithm = csrTemplate.SignatureAlgorithm
|
||||
}
|
||||
if profile.CSRWhitelist.DNSNames {
|
||||
safeTemplate.DNSNames = csrTemplate.DNSNames
|
||||
}
|
||||
if profile.CSRWhitelist.IPAddresses {
|
||||
safeTemplate.IPAddresses = csrTemplate.IPAddresses
|
||||
}
|
||||
if profile.CSRWhitelist.EmailAddresses {
|
||||
safeTemplate.EmailAddresses = csrTemplate.EmailAddresses
|
||||
}
|
||||
if profile.CSRWhitelist.URIs {
|
||||
safeTemplate.URIs = csrTemplate.URIs
|
||||
}
|
||||
}
|
||||
|
||||
if req.CRLOverride != "" {
|
||||
safeTemplate.CRLDistributionPoints = []string{req.CRLOverride}
|
||||
}
|
||||
|
||||
if safeTemplate.IsCA {
|
||||
if !profile.CAConstraint.IsCA {
|
||||
log.Error("local signer policy disallows issuing CA certificate")
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.InvalidRequest)
|
||||
}
|
||||
|
||||
if s.ca != nil && s.ca.MaxPathLen > 0 {
|
||||
if safeTemplate.MaxPathLen >= s.ca.MaxPathLen {
|
||||
log.Error("local signer certificate disallows CA MaxPathLen extending")
|
||||
// do not sign a cert with pathlen > current
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.InvalidRequest)
|
||||
}
|
||||
} else if s.ca != nil && s.ca.MaxPathLen == 0 && s.ca.MaxPathLenZero {
|
||||
log.Error("local signer certificate disallows issuing CA certificate")
|
||||
// signer has pathlen of 0, do not sign more intermediate CAs
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.InvalidRequest)
|
||||
}
|
||||
}
|
||||
|
||||
OverrideHosts(&safeTemplate, req.Hosts)
|
||||
safeTemplate.Subject = PopulateSubjectFromCSR(req.Subject, safeTemplate.Subject)
|
||||
|
||||
// If there is a whitelist, ensure that both the Common Name and SAN DNSNames match
|
||||
if profile.NameWhitelist != nil {
|
||||
if safeTemplate.Subject.CommonName != "" {
|
||||
if profile.NameWhitelist.Find([]byte(safeTemplate.Subject.CommonName)) == nil {
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.UnmatchedWhitelist)
|
||||
}
|
||||
}
|
||||
for _, name := range safeTemplate.DNSNames {
|
||||
if profile.NameWhitelist.Find([]byte(name)) == nil {
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.UnmatchedWhitelist)
|
||||
}
|
||||
}
|
||||
for _, name := range safeTemplate.EmailAddresses {
|
||||
if profile.NameWhitelist.Find([]byte(name)) == nil {
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.UnmatchedWhitelist)
|
||||
}
|
||||
}
|
||||
for _, name := range safeTemplate.URIs {
|
||||
if profile.NameWhitelist.Find([]byte(name.String())) == nil {
|
||||
return nil, cferr.New(cferr.PolicyError, cferr.UnmatchedWhitelist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if profile.ClientProvidesSerialNumbers {
|
||||
if req.Serial == nil {
|
||||
return nil, cferr.New(cferr.CertificateError, cferr.MissingSerial)
|
||||
}
|
||||
safeTemplate.SerialNumber = req.Serial
|
||||
} else {
|
||||
// RFC 5280 4.1.2.2:
|
||||
// Certificate users MUST be able to handle serialNumber
|
||||
// values up to 20 octets. Conforming CAs MUST NOT use
|
||||
// serialNumber values longer than 20 octets.
|
||||
//
|
||||
// If CFSSL is providing the serial numbers, it makes
|
||||
// sense to use the max supported size.
|
||||
serialNumber := make([]byte, 20)
|
||||
_, err = io.ReadFull(rand.Reader, serialNumber)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CertificateError, cferr.Unknown, err)
|
||||
}
|
||||
|
||||
// SetBytes interprets buf as the bytes of a big-endian
|
||||
// unsigned integer. The leading byte should be masked
|
||||
// off to ensure it isn't negative.
|
||||
serialNumber[0] &= 0x7F
|
||||
|
||||
safeTemplate.SerialNumber = new(big.Int).SetBytes(serialNumber)
|
||||
}
|
||||
|
||||
if len(req.Extensions) > 0 {
|
||||
for _, ext := range req.Extensions {
|
||||
oid := asn1.ObjectIdentifier(ext.ID)
|
||||
if !profile.ExtensionWhitelist[oid.String()] {
|
||||
return nil, cferr.New(cferr.CertificateError, cferr.InvalidRequest)
|
||||
}
|
||||
|
||||
rawValue, err := hex.DecodeString(ext.Value)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CertificateError, cferr.InvalidRequest, err)
|
||||
}
|
||||
|
||||
safeTemplate.ExtraExtensions = append(safeTemplate.ExtraExtensions, pkix.Extension{
|
||||
Id: oid,
|
||||
Critical: ext.Critical,
|
||||
Value: rawValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var distPoints = safeTemplate.CRLDistributionPoints
|
||||
err = signer.FillTemplate(&safeTemplate, s.policy.Default, profile, req.NotBefore, req.NotAfter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if distPoints != nil && len(distPoints) > 0 {
|
||||
safeTemplate.CRLDistributionPoints = distPoints
|
||||
}
|
||||
|
||||
var certTBS = safeTemplate
|
||||
|
||||
if len(profile.CTLogServers) > 0 || req.ReturnPrecert {
|
||||
// Add a poison extension which prevents validation
|
||||
var poisonExtension = pkix.Extension{Id: signer.CTPoisonOID, Critical: true, Value: []byte{0x05, 0x00}}
|
||||
var poisonedPreCert = certTBS
|
||||
poisonedPreCert.ExtraExtensions = append(safeTemplate.ExtraExtensions, poisonExtension)
|
||||
cert, err = s.sign(&poisonedPreCert, profile.LintErrLevel, profile.LintRegistry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.ReturnPrecert {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
derCert, _ := pem.Decode(cert)
|
||||
prechain := []ct.ASN1Cert{{Data: derCert.Bytes}, {Data: s.ca.Raw}}
|
||||
var sctList []ct.SignedCertificateTimestamp
|
||||
|
||||
for _, server := range profile.CTLogServers {
|
||||
log.Infof("submitting poisoned precertificate to %s", server)
|
||||
ctclient, err := client.New(server, nil, jsonclient.Options{})
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CTError, cferr.PrecertSubmissionFailed, err)
|
||||
}
|
||||
var resp *ct.SignedCertificateTimestamp
|
||||
ctx := context.Background()
|
||||
resp, err = ctclient.AddPreChain(ctx, prechain)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CTError, cferr.PrecertSubmissionFailed, err)
|
||||
}
|
||||
sctList = append(sctList, *resp)
|
||||
}
|
||||
|
||||
var serializedSCTList []byte
|
||||
serializedSCTList, err = helpers.SerializeSCTList(sctList)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, err)
|
||||
}
|
||||
|
||||
// Serialize again as an octet string before embedding
|
||||
serializedSCTList, err = asn1.Marshal(serializedSCTList)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, err)
|
||||
}
|
||||
|
||||
var SCTListExtension = pkix.Extension{Id: signer.SCTListOID, Critical: false, Value: serializedSCTList}
|
||||
certTBS.ExtraExtensions = append(certTBS.ExtraExtensions, SCTListExtension)
|
||||
}
|
||||
|
||||
var signedCert []byte
|
||||
signedCert, err = s.sign(&certTBS, profile.LintErrLevel, profile.LintRegistry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the AKI from signedCert. This is required to support Go 1.9+.
|
||||
// In prior versions of Go, x509.CreateCertificate updated the
|
||||
// AuthorityKeyId of certTBS.
|
||||
parsedCert, _ := helpers.ParseCertificatePEM(signedCert)
|
||||
|
||||
if s.dbAccessor != nil {
|
||||
var certRecord = certdb.CertificateRecord{
|
||||
Serial: certTBS.SerialNumber.String(),
|
||||
// this relies on the specific behavior of x509.CreateCertificate
|
||||
// which sets the AuthorityKeyId from the signer's SubjectKeyId
|
||||
AKI: hex.EncodeToString(parsedCert.AuthorityKeyId),
|
||||
CALabel: req.Label,
|
||||
Status: "good",
|
||||
Expiry: certTBS.NotAfter,
|
||||
PEM: string(signedCert),
|
||||
}
|
||||
|
||||
err = s.dbAccessor.InsertCertificate(certRecord)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug("saved certificate with serial number ", certTBS.SerialNumber)
|
||||
}
|
||||
|
||||
return signedCert, nil
|
||||
}
|
||||
|
||||
// SignFromPrecert creates and signs a certificate from an existing precertificate
|
||||
// that was previously signed by Signer.ca and inserts the provided SCTs into the
|
||||
// new certificate. The resulting certificate will be a exact copy of the precert
|
||||
// except for the removal of the poison extension and the addition of the SCT list
|
||||
// extension. SignFromPrecert does not verify that the contents of the certificate
|
||||
// still match the signing profile of the signer, it only requires that the precert
|
||||
// was previously signed by the Signers CA. Similarly, any linting configured
|
||||
// by the profile used to sign the precert will not be re-applied to the final
|
||||
// cert and must be done separately by the caller.
|
||||
func (s *Signer) SignFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTimestamp) ([]byte, error) {
|
||||
// Verify certificate was signed by s.ca
|
||||
if err := precert.CheckSignatureFrom(s.ca); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify certificate is a precert
|
||||
isPrecert := false
|
||||
poisonIndex := 0
|
||||
for i, ext := range precert.Extensions {
|
||||
if ext.Id.Equal(signer.CTPoisonOID) {
|
||||
if !ext.Critical {
|
||||
return nil, cferr.New(cferr.CTError, cferr.PrecertInvalidPoison)
|
||||
}
|
||||
// Check extension contains ASN.1 NULL
|
||||
if bytes.Compare(ext.Value, []byte{0x05, 0x00}) != 0 {
|
||||
return nil, cferr.New(cferr.CTError, cferr.PrecertInvalidPoison)
|
||||
}
|
||||
isPrecert = true
|
||||
poisonIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isPrecert {
|
||||
return nil, cferr.New(cferr.CTError, cferr.PrecertMissingPoison)
|
||||
}
|
||||
|
||||
// Serialize SCTs into list format and create extension
|
||||
serializedList, err := helpers.SerializeSCTList(scts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Serialize again as an octet string before embedding
|
||||
serializedList, err = asn1.Marshal(serializedList)
|
||||
if err != nil {
|
||||
return nil, cferr.Wrap(cferr.CTError, cferr.Unknown, err)
|
||||
}
|
||||
sctExt := pkix.Extension{Id: signer.SCTListOID, Critical: false, Value: serializedList}
|
||||
|
||||
// Create the new tbsCert from precert. Do explicit copies of any slices so that we don't
|
||||
// use memory that may be altered by us or the caller at a later stage.
|
||||
tbsCert := x509.Certificate{
|
||||
SignatureAlgorithm: precert.SignatureAlgorithm,
|
||||
PublicKeyAlgorithm: precert.PublicKeyAlgorithm,
|
||||
PublicKey: precert.PublicKey,
|
||||
Version: precert.Version,
|
||||
SerialNumber: precert.SerialNumber,
|
||||
Issuer: precert.Issuer,
|
||||
Subject: precert.Subject,
|
||||
NotBefore: precert.NotBefore,
|
||||
NotAfter: precert.NotAfter,
|
||||
KeyUsage: precert.KeyUsage,
|
||||
BasicConstraintsValid: precert.BasicConstraintsValid,
|
||||
IsCA: precert.IsCA,
|
||||
MaxPathLen: precert.MaxPathLen,
|
||||
MaxPathLenZero: precert.MaxPathLenZero,
|
||||
PermittedDNSDomainsCritical: precert.PermittedDNSDomainsCritical,
|
||||
}
|
||||
if len(precert.Extensions) > 0 {
|
||||
tbsCert.ExtraExtensions = make([]pkix.Extension, len(precert.Extensions))
|
||||
copy(tbsCert.ExtraExtensions, precert.Extensions)
|
||||
}
|
||||
|
||||
// Remove the poison extension from ExtraExtensions
|
||||
tbsCert.ExtraExtensions = append(tbsCert.ExtraExtensions[:poisonIndex], tbsCert.ExtraExtensions[poisonIndex+1:]...)
|
||||
// Insert the SCT list extension
|
||||
tbsCert.ExtraExtensions = append(tbsCert.ExtraExtensions, sctExt)
|
||||
|
||||
// Sign the tbsCert. Linting is always disabled because there is no way for
|
||||
// this API to know the correct lint settings to use because there is no
|
||||
// reference to the signing profile of the precert available.
|
||||
return s.sign(&tbsCert, 0, nil)
|
||||
}
|
||||
|
||||
// Info return a populated info.Resp struct or an error.
|
||||
func (s *Signer) Info(req info.Req) (resp *info.Resp, err error) {
|
||||
cert, err := s.Certificate(req.Label, req.Profile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := signer.Profile(s, req.Profile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp = new(info.Resp)
|
||||
if cert.Raw != nil {
|
||||
resp.Certificate = string(bytes.TrimSpace(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})))
|
||||
}
|
||||
resp.Usage = profile.Usage
|
||||
resp.ExpiryString = profile.ExpiryString
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SigAlgo returns the RSA signer's signature algorithm.
|
||||
func (s *Signer) SigAlgo() x509.SignatureAlgorithm {
|
||||
return s.sigAlgo
|
||||
}
|
||||
|
||||
// Certificate returns the signer's certificate.
|
||||
func (s *Signer) Certificate(label, profile string) (*x509.Certificate, error) {
|
||||
cert := *s.ca
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// SetPolicy sets the signer's signature policy.
|
||||
func (s *Signer) SetPolicy(policy *config.Signing) {
|
||||
s.policy = policy
|
||||
}
|
||||
|
||||
// SetDBAccessor sets the signers' cert db accessor
|
||||
func (s *Signer) SetDBAccessor(dba certdb.Accessor) {
|
||||
s.dbAccessor = dba
|
||||
}
|
||||
|
||||
// GetDBAccessor returns the signers' cert db accessor
|
||||
func (s *Signer) GetDBAccessor() certdb.Accessor {
|
||||
return s.dbAccessor
|
||||
}
|
||||
|
||||
// SetReqModifier does nothing for local
|
||||
func (s *Signer) SetReqModifier(func(*http.Request, []byte)) {
|
||||
// noop
|
||||
}
|
||||
|
||||
// Policy returns the signer's policy.
|
||||
func (s *Signer) Policy() *config.Signing {
|
||||
return s.policy
|
||||
}
|
||||
|
|
@ -1,438 +0,0 @@
|
|||
// Package signer implements certificate signature functionality for CFSSL.
|
||||
package signer
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cfssl/certdb"
|
||||
"github.com/cloudflare/cfssl/config"
|
||||
"github.com/cloudflare/cfssl/csr"
|
||||
cferr "github.com/cloudflare/cfssl/errors"
|
||||
"github.com/cloudflare/cfssl/info"
|
||||
)
|
||||
|
||||
// Subject contains the information that should be used to override the
|
||||
// subject information when signing a certificate.
|
||||
type Subject struct {
|
||||
CN string
|
||||
Names []csr.Name `json:"names"`
|
||||
SerialNumber string
|
||||
}
|
||||
|
||||
// Extension represents a raw extension to be included in the certificate. The
|
||||
// "value" field must be hex encoded.
|
||||
type Extension struct {
|
||||
ID config.OID `json:"id"`
|
||||
Critical bool `json:"critical"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// SignRequest stores a signature request, which contains the hostname,
|
||||
// the CSR, optional subject information, and the signature profile.
|
||||
//
|
||||
// Extensions provided in the signRequest are copied into the certificate, as
|
||||
// long as they are in the ExtensionWhitelist for the signer's policy.
|
||||
// Extensions requested in the CSR are ignored, except for those processed by
|
||||
// ParseCertificateRequest (mainly subjectAltName).
|
||||
type SignRequest struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
Request string `json:"certificate_request"`
|
||||
Subject *Subject `json:"subject,omitempty"`
|
||||
Profile string `json:"profile"`
|
||||
CRLOverride string `json:"crl_override"`
|
||||
Label string `json:"label"`
|
||||
Serial *big.Int `json:"serial,omitempty"`
|
||||
Extensions []Extension `json:"extensions,omitempty"`
|
||||
// If provided, NotBefore will be used without modification (except
|
||||
// for canonicalization) as the value of the notBefore field of the
|
||||
// certificate. In particular no backdating adjustment will be made
|
||||
// when NotBefore is provided.
|
||||
NotBefore time.Time
|
||||
// If provided, NotAfter will be used without modification (except
|
||||
// for canonicalization) as the value of the notAfter field of the
|
||||
// certificate.
|
||||
NotAfter time.Time
|
||||
// If ReturnPrecert is true a certificate with the CT poison extension
|
||||
// will be returned from the Signer instead of attempting to retrieve
|
||||
// SCTs and populate the tbsCert with them itself. This precert can then
|
||||
// be passed to SignFromPrecert with the SCTs in order to create a
|
||||
// valid certificate.
|
||||
ReturnPrecert bool
|
||||
}
|
||||
|
||||
// appendIf appends to a if s is not an empty string.
|
||||
func appendIf(s string, a *[]string) {
|
||||
if s != "" {
|
||||
*a = append(*a, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the PKIX name for the subject.
|
||||
func (s *Subject) Name() pkix.Name {
|
||||
var name pkix.Name
|
||||
name.CommonName = s.CN
|
||||
|
||||
for _, n := range s.Names {
|
||||
appendIf(n.C, &name.Country)
|
||||
appendIf(n.ST, &name.Province)
|
||||
appendIf(n.L, &name.Locality)
|
||||
appendIf(n.O, &name.Organization)
|
||||
appendIf(n.OU, &name.OrganizationalUnit)
|
||||
}
|
||||
name.SerialNumber = s.SerialNumber
|
||||
return name
|
||||
}
|
||||
|
||||
// SplitHosts takes a comma-spearated list of hosts and returns a slice
|
||||
// with the hosts split
|
||||
func SplitHosts(hostList string) []string {
|
||||
if hostList == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(hostList, ",")
|
||||
}
|
||||
|
||||
// A Signer contains a CA's certificate and private key for signing
|
||||
// certificates, a Signing policy to refer to and a SignatureAlgorithm.
|
||||
type Signer interface {
|
||||
Info(info.Req) (*info.Resp, error)
|
||||
Policy() *config.Signing
|
||||
SetDBAccessor(certdb.Accessor)
|
||||
GetDBAccessor() certdb.Accessor
|
||||
SetPolicy(*config.Signing)
|
||||
SigAlgo() x509.SignatureAlgorithm
|
||||
Sign(req SignRequest) (cert []byte, err error)
|
||||
SetReqModifier(func(*http.Request, []byte))
|
||||
}
|
||||
|
||||
// Profile gets the specific profile from the signer
|
||||
func Profile(s Signer, profile string) (*config.SigningProfile, error) {
|
||||
var p *config.SigningProfile
|
||||
policy := s.Policy()
|
||||
if policy != nil && policy.Profiles != nil && profile != "" {
|
||||
p = policy.Profiles[profile]
|
||||
}
|
||||
|
||||
if p == nil && policy != nil {
|
||||
p = policy.Default
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
return nil, cferr.Wrap(cferr.APIClientError, cferr.ClientHTTPError, errors.New("profile must not be nil"))
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// DefaultSigAlgo returns an appropriate X.509 signature algorithm given
|
||||
// the CA's private key.
|
||||
func DefaultSigAlgo(priv crypto.Signer) x509.SignatureAlgorithm {
|
||||
pub := priv.Public()
|
||||
switch pub := pub.(type) {
|
||||
case *rsa.PublicKey:
|
||||
keySize := pub.N.BitLen()
|
||||
switch {
|
||||
case keySize >= 4096:
|
||||
return x509.SHA512WithRSA
|
||||
case keySize >= 3072:
|
||||
return x509.SHA384WithRSA
|
||||
case keySize >= 2048:
|
||||
return x509.SHA256WithRSA
|
||||
default:
|
||||
return x509.SHA1WithRSA
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
switch pub.Curve {
|
||||
case elliptic.P256():
|
||||
return x509.ECDSAWithSHA256
|
||||
case elliptic.P384():
|
||||
return x509.ECDSAWithSHA384
|
||||
case elliptic.P521():
|
||||
return x509.ECDSAWithSHA512
|
||||
default:
|
||||
return x509.ECDSAWithSHA1
|
||||
}
|
||||
default:
|
||||
return x509.UnknownSignatureAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCertificateRequest takes an incoming certificate request and
|
||||
// builds a certificate template from it.
|
||||
func ParseCertificateRequest(s Signer, csrBytes []byte) (template *x509.Certificate, err error) {
|
||||
csrv, err := x509.ParseCertificateRequest(csrBytes)
|
||||
if err != nil {
|
||||
err = cferr.Wrap(cferr.CSRError, cferr.ParseFailed, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = csrv.CheckSignature()
|
||||
if err != nil {
|
||||
err = cferr.Wrap(cferr.CSRError, cferr.KeyMismatch, err)
|
||||
return
|
||||
}
|
||||
|
||||
template = &x509.Certificate{
|
||||
Subject: csrv.Subject,
|
||||
PublicKeyAlgorithm: csrv.PublicKeyAlgorithm,
|
||||
PublicKey: csrv.PublicKey,
|
||||
SignatureAlgorithm: s.SigAlgo(),
|
||||
DNSNames: csrv.DNSNames,
|
||||
IPAddresses: csrv.IPAddresses,
|
||||
EmailAddresses: csrv.EmailAddresses,
|
||||
URIs: csrv.URIs,
|
||||
}
|
||||
|
||||
for _, val := range csrv.Extensions {
|
||||
// Check the CSR for the X.509 BasicConstraints (RFC 5280, 4.2.1.9)
|
||||
// extension and append to template if necessary
|
||||
if val.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 19}) {
|
||||
var constraints csr.BasicConstraints
|
||||
var rest []byte
|
||||
|
||||
if rest, err = asn1.Unmarshal(val.Value, &constraints); err != nil {
|
||||
return nil, cferr.Wrap(cferr.CSRError, cferr.ParseFailed, err)
|
||||
} else if len(rest) != 0 {
|
||||
return nil, cferr.Wrap(cferr.CSRError, cferr.ParseFailed, errors.New("x509: trailing data after X.509 BasicConstraints"))
|
||||
}
|
||||
|
||||
template.BasicConstraintsValid = true
|
||||
template.IsCA = constraints.IsCA
|
||||
template.MaxPathLen = constraints.MaxPathLen
|
||||
template.MaxPathLenZero = template.MaxPathLen == 0
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type subjectPublicKeyInfo struct {
|
||||
Algorithm pkix.AlgorithmIdentifier
|
||||
SubjectPublicKey asn1.BitString
|
||||
}
|
||||
|
||||
// ComputeSKI derives an SKI from the certificate's public key in a
|
||||
// standard manner. This is done by computing the SHA-1 digest of the
|
||||
// SubjectPublicKeyInfo component of the certificate.
|
||||
func ComputeSKI(template *x509.Certificate) ([]byte, error) {
|
||||
pub := template.PublicKey
|
||||
encodedPub, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var subPKI subjectPublicKeyInfo
|
||||
_, err = asn1.Unmarshal(encodedPub, &subPKI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubHash := sha1.Sum(subPKI.SubjectPublicKey.Bytes)
|
||||
return pubHash[:], nil
|
||||
}
|
||||
|
||||
// FillTemplate is a utility function that tries to load as much of
|
||||
// the certificate template as possible from the profiles and current
|
||||
// template. It fills in the key uses, expiration, revocation URLs
|
||||
// and SKI.
|
||||
func FillTemplate(template *x509.Certificate, defaultProfile, profile *config.SigningProfile, notBefore time.Time, notAfter time.Time) error {
|
||||
ski, err := ComputeSKI(template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
eku []x509.ExtKeyUsage
|
||||
ku x509.KeyUsage
|
||||
backdate time.Duration
|
||||
expiry time.Duration
|
||||
crlURL, ocspURL string
|
||||
issuerURL = profile.IssuerURL
|
||||
)
|
||||
|
||||
// The third value returned from Usages is a list of unknown key usages.
|
||||
// This should be used when validating the profile at load, and isn't used
|
||||
// here.
|
||||
ku, eku, _ = profile.Usages()
|
||||
if profile.IssuerURL == nil {
|
||||
issuerURL = defaultProfile.IssuerURL
|
||||
}
|
||||
|
||||
if ku == 0 && len(eku) == 0 {
|
||||
return cferr.New(cferr.PolicyError, cferr.NoKeyUsages)
|
||||
}
|
||||
|
||||
if expiry = profile.Expiry; expiry == 0 {
|
||||
expiry = defaultProfile.Expiry
|
||||
}
|
||||
|
||||
if crlURL = profile.CRL; crlURL == "" {
|
||||
crlURL = defaultProfile.CRL
|
||||
}
|
||||
if ocspURL = profile.OCSP; ocspURL == "" {
|
||||
ocspURL = defaultProfile.OCSP
|
||||
}
|
||||
|
||||
if notBefore.IsZero() {
|
||||
if !profile.NotBefore.IsZero() {
|
||||
notBefore = profile.NotBefore
|
||||
} else {
|
||||
if backdate = profile.Backdate; backdate == 0 {
|
||||
backdate = -5 * time.Minute
|
||||
} else {
|
||||
backdate = -1 * profile.Backdate
|
||||
}
|
||||
notBefore = time.Now().Round(time.Minute).Add(backdate)
|
||||
}
|
||||
}
|
||||
notBefore = notBefore.UTC()
|
||||
|
||||
if notAfter.IsZero() {
|
||||
if !profile.NotAfter.IsZero() {
|
||||
notAfter = profile.NotAfter
|
||||
} else {
|
||||
notAfter = notBefore.Add(expiry)
|
||||
}
|
||||
}
|
||||
notAfter = notAfter.UTC()
|
||||
|
||||
template.NotBefore = notBefore
|
||||
template.NotAfter = notAfter
|
||||
template.KeyUsage = ku
|
||||
template.ExtKeyUsage = eku
|
||||
template.BasicConstraintsValid = true
|
||||
template.IsCA = profile.CAConstraint.IsCA
|
||||
if template.IsCA {
|
||||
template.MaxPathLen = profile.CAConstraint.MaxPathLen
|
||||
if template.MaxPathLen == 0 {
|
||||
template.MaxPathLenZero = profile.CAConstraint.MaxPathLenZero
|
||||
}
|
||||
template.DNSNames = nil
|
||||
template.EmailAddresses = nil
|
||||
template.URIs = nil
|
||||
}
|
||||
template.SubjectKeyId = ski
|
||||
|
||||
if ocspURL != "" {
|
||||
template.OCSPServer = []string{ocspURL}
|
||||
}
|
||||
if crlURL != "" {
|
||||
template.CRLDistributionPoints = []string{crlURL}
|
||||
}
|
||||
|
||||
if len(issuerURL) != 0 {
|
||||
template.IssuingCertificateURL = issuerURL
|
||||
}
|
||||
if len(profile.Policies) != 0 {
|
||||
err = addPolicies(template, profile.Policies)
|
||||
if err != nil {
|
||||
return cferr.Wrap(cferr.PolicyError, cferr.InvalidPolicy, err)
|
||||
}
|
||||
}
|
||||
if profile.OCSPNoCheck {
|
||||
ocspNoCheckExtension := pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 5},
|
||||
Critical: false,
|
||||
Value: []byte{0x05, 0x00},
|
||||
}
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, ocspNoCheckExtension)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type policyInformation struct {
|
||||
PolicyIdentifier asn1.ObjectIdentifier
|
||||
Qualifiers []interface{} `asn1:"tag:optional,omitempty"`
|
||||
}
|
||||
|
||||
type cpsPolicyQualifier struct {
|
||||
PolicyQualifierID asn1.ObjectIdentifier
|
||||
Qualifier string `asn1:"tag:optional,ia5"`
|
||||
}
|
||||
|
||||
type userNotice struct {
|
||||
ExplicitText string `asn1:"tag:optional,utf8"`
|
||||
}
|
||||
type userNoticePolicyQualifier struct {
|
||||
PolicyQualifierID asn1.ObjectIdentifier
|
||||
Qualifier userNotice
|
||||
}
|
||||
|
||||
var (
|
||||
// Per https://tools.ietf.org/html/rfc3280.html#page-106, this represents:
|
||||
// iso(1) identified-organization(3) dod(6) internet(1) security(5)
|
||||
// mechanisms(5) pkix(7) id-qt(2) id-qt-cps(1)
|
||||
iDQTCertificationPracticeStatement = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 1}
|
||||
// iso(1) identified-organization(3) dod(6) internet(1) security(5)
|
||||
// mechanisms(5) pkix(7) id-qt(2) id-qt-unotice(2)
|
||||
iDQTUserNotice = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 2, 2}
|
||||
|
||||
// CTPoisonOID is the object ID of the critical poison extension for precertificates
|
||||
// https://tools.ietf.org/html/rfc6962#page-9
|
||||
CTPoisonOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
|
||||
|
||||
// SCTListOID is the object ID for the Signed Certificate Timestamp certificate extension
|
||||
// https://tools.ietf.org/html/rfc6962#page-14
|
||||
SCTListOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
|
||||
)
|
||||
|
||||
// addPolicies adds Certificate Policies and optional Policy Qualifiers to a
|
||||
// certificate, based on the input config. Go's x509 library allows setting
|
||||
// Certificate Policies easily, but does not support nested Policy Qualifiers
|
||||
// under those policies. So we need to construct the ASN.1 structure ourselves.
|
||||
func addPolicies(template *x509.Certificate, policies []config.CertificatePolicy) error {
|
||||
asn1PolicyList := []policyInformation{}
|
||||
|
||||
for _, policy := range policies {
|
||||
pi := policyInformation{
|
||||
// The PolicyIdentifier is an OID assigned to a given issuer.
|
||||
PolicyIdentifier: asn1.ObjectIdentifier(policy.ID),
|
||||
}
|
||||
for _, qualifier := range policy.Qualifiers {
|
||||
switch qualifier.Type {
|
||||
case "id-qt-unotice":
|
||||
pi.Qualifiers = append(pi.Qualifiers,
|
||||
userNoticePolicyQualifier{
|
||||
PolicyQualifierID: iDQTUserNotice,
|
||||
Qualifier: userNotice{
|
||||
ExplicitText: qualifier.Value,
|
||||
},
|
||||
})
|
||||
case "id-qt-cps":
|
||||
pi.Qualifiers = append(pi.Qualifiers,
|
||||
cpsPolicyQualifier{
|
||||
PolicyQualifierID: iDQTCertificationPracticeStatement,
|
||||
Qualifier: qualifier.Value,
|
||||
})
|
||||
default:
|
||||
return errors.New("Invalid qualifier type in Policies " + qualifier.Type)
|
||||
}
|
||||
}
|
||||
asn1PolicyList = append(asn1PolicyList, pi)
|
||||
}
|
||||
|
||||
asn1Bytes, err := asn1.Marshal(asn1PolicyList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{
|
||||
Id: asn1.ObjectIdentifier{2, 5, 29, 32},
|
||||
Critical: false,
|
||||
Value: asn1Bytes,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
|
@ -5,19 +5,11 @@ github.com/beorn7/perks/quantile
|
|||
# github.com/cespare/xxhash/v2 v2.1.1
|
||||
github.com/cespare/xxhash/v2
|
||||
# github.com/cloudflare/cfssl v1.4.2-0.20200324225241-abef926615f4
|
||||
github.com/cloudflare/cfssl/auth
|
||||
github.com/cloudflare/cfssl/certdb
|
||||
github.com/cloudflare/cfssl/config
|
||||
github.com/cloudflare/cfssl/crypto/pkcs7
|
||||
github.com/cloudflare/cfssl/csr
|
||||
github.com/cloudflare/cfssl/errors
|
||||
github.com/cloudflare/cfssl/helpers
|
||||
github.com/cloudflare/cfssl/helpers/derhelpers
|
||||
github.com/cloudflare/cfssl/info
|
||||
github.com/cloudflare/cfssl/log
|
||||
github.com/cloudflare/cfssl/ocsp/config
|
||||
github.com/cloudflare/cfssl/signer
|
||||
github.com/cloudflare/cfssl/signer/local
|
||||
# github.com/eggsampler/acme/v3 v3.0.0
|
||||
github.com/eggsampler/acme/v3
|
||||
# github.com/go-gorp/gorp/v3 v3.0.2
|
||||
|
|
|
|||
Loading…
Reference in New Issue