Skip to content

Commit

Permalink
crypto/x509: add RFC 5280/X509v2 compliant CRL generation function
Browse files Browse the repository at this point in the history
The existing Certificate.CreateCRL method generates non-conformant CRLs and as
such cannot be used for implementations that require standards compliance. This
change implements a new top level method, CreateCRL, which generates compliant
CRLs, and offers an extensible API if any extensions/fields need to be
supported in the future.

Fixes golang#35428

Change-Id: I06ef833cb860077b2d42c1bb262a72c3e918aa0d
  • Loading branch information
Roland Shoemaker committed Jan 31, 2020
1 parent 9bb40ed commit 55804f0
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 0 deletions.
107 changes: 107 additions & 0 deletions src/crypto/x509/x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -1644,6 +1644,7 @@ var (
oidExtensionNameConstraints = []int{2, 5, 29, 30}
oidExtensionCRLDistributionPoints = []int{2, 5, 29, 31}
oidExtensionAuthorityInfoAccess = []int{1, 3, 6, 1, 5, 5, 7, 1, 1}
oidExtensionCRLNumber = []int{2, 5, 29, 20}
)

var (
Expand Down Expand Up @@ -2213,6 +2214,10 @@ func ParseDERCRL(derBytes []byte) (*pkix.CertificateList, error) {

// CreateCRL returns a DER encoded CRL, signed by this Certificate, that
// contains the given list of revoked certificates.
//
// Note: this method does not generate RFC 5280 X509 v2 conformant CRLs as
// it omits the CRL number extension and will also omit the authority key
// identifier extension if the certificate has an empty SubjectKeyId.
func (c *Certificate) CreateCRL(rand io.Reader, priv interface{}, revokedCerts []pkix.RevokedCertificate, now, expiry time.Time) (crlBytes []byte, err error) {
key, ok := priv.(crypto.Signer)
if !ok {
Expand Down Expand Up @@ -2660,3 +2665,105 @@ func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error
func (c *CertificateRequest) CheckSignature() error {
return checkSignature(c.SignatureAlgorithm, c.RawTBSCertificateRequest, c.Signature, c.PublicKey)
}

// CRLTemplate contains the fields used to create an X.509 v2 Certificate
// Revocation list.
type CRLTemplate struct {
RevokedCertificates []pkix.RevokedCertificate
Number int
ThisUpdate time.Time
NextUpdate time.Time
Extensions []pkix.Extension
}

// CreateCRL creates a new x509 v2 Certificate Revocation List.
//
// The CRL is signed by priv.
//
// revokedCerts may be nil, in which case an empty CRL will be created.
//
// The issuer distinguished name CRL field and authority key identifier extension
// are populated using the issuer certificate. issuer must have SubjectKeyId set.
//
// The CRL number extension is populated using the Number field of template.
//
// The template fields NextUpdate must be greater than ThisUpdate.
//
// Any extensions in the Extensions field of template will be copied directly into
// the CRL.
//
// This method is differentiated from the Certificate.CreateCRL method as
// it creates X509 v2 conformant CRLs as defined by the RFC 5280 CRL profile.
// This method should be used if created CRLs need to be standards compliant.
func CreateCRL(rand io.Reader, issuer *Certificate, priv crypto.Signer, template CRLTemplate) ([]byte, error) {
if len(issuer.SubjectKeyId) == 0 {
return nil, errors.New("x509: issuer certificate doesn't contain a subject key identifier")
}
if template.NextUpdate.Before(template.ThisUpdate) {
return nil, errors.New("x509: template.ThisUpdate is after template.NextUpdate")
}

hashFunc, signatureAlgorithm, err := signingParamsForPublicKey(priv.Public(), 0)
if err != nil {
return nil, err
}

// Force revocation times to UTC per RFC 5280.
revokedCertsUTC := make([]pkix.RevokedCertificate, len(template.RevokedCertificates))
for i, rc := range template.RevokedCertificates {
rc.RevocationTime = rc.RevocationTime.UTC()
revokedCertsUTC[i] = rc
}

aki, err := asn1.Marshal(authKeyId{Id: issuer.SubjectKeyId})
if err != nil {
return nil, err
}
crlNum, err := asn1.Marshal(template.Number)
if err != nil {
return nil, err
}

tbsCertList := pkix.TBSCertificateList{
Version: 1,
Signature: signatureAlgorithm,
Issuer: issuer.Subject.ToRDNSequence(),
ThisUpdate: template.ThisUpdate.UTC(),
NextUpdate: template.NextUpdate.UTC(),
RevokedCertificates: revokedCertsUTC,
Extensions: []pkix.Extension{
{
Id: oidExtensionAuthorityKeyId,
Value: aki,
},
{
Id: oidExtensionCRLNumber,
Value: crlNum,
},
},
}

if len(template.Extensions) > 0 {
tbsCertList.Extensions = append(tbsCertList.Extensions, template.Extensions...)
}

tbsCertListContents, err := asn1.Marshal(tbsCertList)
if err != nil {
return nil, err
}

h := hashFunc.New()
h.Write(tbsCertListContents)
digest := h.Sum(nil)

signature, err := priv.Sign(rand, digest, hashFunc)
if err != nil {
return nil, err
}

return asn1.Marshal(pkix.CertificateList{
TBSCertList: tbsCertList,
SignatureAlgorithm: signatureAlgorithm,
SignatureValue: asn1.BitString{Bytes: signature, BitLength: len(signature) * 8},
})
}
118 changes: 118 additions & 0 deletions src/crypto/x509/x509_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2293,3 +2293,121 @@ func TestPKCS1MismatchKeyFormat(t *testing.T) {
}
}
}

func TestCreateCRLExt(t *testing.T) {
ecdsaPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("Failed to generate ECDSA key: %s", err)
}
tests := []struct {
name string
issuer Certificate
template CRLTemplate
expectedError string
}{
{
name: "issuer missing SubjectKeyId",
issuer: Certificate{},
template: CRLTemplate{},
expectedError: "x509: issuer certificate doesn't contain a subject key identifier",
},
{
name: "nextUpdate before thisUpdate",
issuer: Certificate{
Subject: pkix.Name{
CommonName: "testing",
},
SubjectKeyId: []byte{1, 2, 3},
},
template: CRLTemplate{
ThisUpdate: time.Time{}.Add(time.Hour),
NextUpdate: time.Time{},
},
expectedError: "x509: template.ThisUpdate is after template.NextUpdate",
},
{
name: "valid, extra extension",
issuer: Certificate{
Subject: pkix.Name{
CommonName: "testing",
},
SubjectKeyId: []byte{1, 2, 3},
},
template: CRLTemplate{
RevokedCertificates: []pkix.RevokedCertificate{
{
SerialNumber: big.NewInt(2),
RevocationTime: time.Time{}.Add(time.Hour),
},
},
Number: 5,
ThisUpdate: time.Time{}.Add(time.Hour * 24),
NextUpdate: time.Time{}.Add(time.Hour * 48),
Extensions: []pkix.Extension{
{
Id: []int{2, 5, 29, 99},
Value: []byte{5, 0},
},
},
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
crl, err := CreateCRL(rand.Reader, &tc.issuer, ecdsaPriv, tc.template)
if err != nil && tc.expectedError == "" {
t.Fatalf("CreateCRL failed unexpectedly: %s", err)
} else if err != nil && tc.expectedError != err.Error() {
t.Fatalf("CreateCRL failed unexpectedly, wanted: %s, got: %s", tc.expectedError, err)
} else if err == nil && tc.expectedError != "" {
t.Fatalf("CreateCRL didn't fail, expected: %s", tc.expectedError)
}
if tc.expectedError != "" {
return
}

parsedCRL, err := ParseDERCRL(crl)
if err != nil {
t.Fatalf("Failed to parse generated CRL: %s", err)
}

if !reflect.DeepEqual(parsedCRL.TBSCertList.RevokedCertificates, tc.template.RevokedCertificates) {
t.Fatalf("RevokedCertificates mismatch: got %v; want %v.",
parsedCRL.TBSCertList.RevokedCertificates, tc.template.RevokedCertificates)
}

if len(parsedCRL.TBSCertList.Extensions) != 2+len(tc.template.Extensions) {
t.Fatalf("Generated CRL has wrong number of extensions, wanted: %d, got: %d", 2+len(tc.template.Extensions), len(parsedCRL.TBSCertList.Extensions))
}
expectedAKI, err := asn1.Marshal(authKeyId{Id: tc.issuer.SubjectKeyId})
if err != nil {
t.Fatalf("asn1.Marshal failed: %s", err)
}
akiExt := pkix.Extension{
Id: oidExtensionAuthorityKeyId,
Value: expectedAKI,
}
if !reflect.DeepEqual(parsedCRL.TBSCertList.Extensions[0], akiExt) {
t.Fatalf("Unexpected first extension: got %v, want %v",
parsedCRL.TBSCertList.Extensions[0], akiExt)
}
expectedNum, err := asn1.Marshal(tc.template.Number)
if err != nil {
t.Fatalf("asn1.Marshal failed: %s", err)
}
crlExt := pkix.Extension{
Id: oidExtensionCRLNumber,
Value: expectedNum,
}
if !reflect.DeepEqual(parsedCRL.TBSCertList.Extensions[1], crlExt) {
t.Fatalf("Unexpected second extension: got %v, want %v",
parsedCRL.TBSCertList.Extensions[1], crlExt)
}
if !reflect.DeepEqual(parsedCRL.TBSCertList.Extensions[2:], tc.template.Extensions) {
t.Fatalf("Extensions mismatch: got %v; want %v.",
parsedCRL.TBSCertList.Extensions[2:], tc.template.Extensions)
}
})
}
}

0 comments on commit 55804f0

Please sign in to comment.