From 3366be50f11def7687863cb5d5c3101021a00535 Mon Sep 17 00:00:00 2001 From: Phil Porada Date: Wed, 6 Dec 2023 13:44:17 -0500 Subject: [PATCH] Use RFC 7093 truncated SHA256 hash for Subject Key Identifier (#7179) - Adds a feature flag to gate rollout for SHA256 Subject Key Identifiers for end-entity certificates. - The ceremony tool will now use the RFC 7093 section 2 option 1 method for generating Subject Key Identifiers for future root CA, intermediate CA, and cross-sign ceremonies. - - - - [RFC 7093 section 2 option 1](https://datatracker.ietf.org/doc/html/rfc7093#section-2) provides a method for generating a truncated SHA256 hash for the Subject Key Identifier field in accordance with Baseline Requirement [section 7.1.2.11.4 Subject Key Identifier](https://github.com/cabforum/servercert/blob/90a98dc7c1131eaab01af411968aa7330d315b9b/docs/BR.md#712114-subject-key-identifier). > [RFC5280] specifies two examples for generating key identifiers from > public keys. Four additional mechanisms are as follows: > > 1) The keyIdentifier is composed of the leftmost 160-bits of the > SHA-256 hash of the value of the BIT STRING subjectPublicKey > (excluding the tag, length, and number of unused bits). The related [RFC 5280 section 4.2.1.2](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2) states: > For CA certificates, subject key identifiers SHOULD be derived from > the public key or a method that generates unique values. Two common > methods for generating key identifiers from the public key are: > ... > Other methods of generating unique numbers are also acceptable. --- cmd/ceremony/cert.go | 7 ++++++- cmd/ceremony/cert_test.go | 7 +++++++ features/featureflag_string.go | 5 +++-- features/features.go | 6 ++++++ issuance/cert.go | 16 ++++++++++++++-- issuance/cert_test.go | 23 +++++++++++++++++++++++ test/config-next/ca-a.json | 3 ++- test/config-next/ca-b.json | 3 ++- 8 files changed, 63 insertions(+), 7 deletions(-) diff --git a/cmd/ceremony/cert.go b/cmd/ceremony/cert.go index 720dff502..6c8a5c4f5 100644 --- a/cmd/ceremony/cert.go +++ b/cmd/ceremony/cert.go @@ -207,8 +207,13 @@ func generateSKID(pk []byte) ([]byte, error) { if _, err := asn1.Unmarshal(pk, &pkixPublicKey); err != nil { return nil, err } + + // RFC 7093 Section 2 Additional Methods for Generating Key Identifiers: The + // keyIdentifier [may be] composed of the leftmost 160-bits of the SHA-256 + // hash of the value of the BIT STRING subjectPublicKey (excluding the tag, + // length, and number of unused bits). skid := sha256.Sum256(pkixPublicKey.BitString.Bytes) - return skid[:], nil + return skid[0:20:20], nil } // makeTemplate generates the certificate template for use in x509.CreateCertificate diff --git a/cmd/ceremony/cert_test.go b/cmd/ceremony/cert_test.go index 10e6a9696..c31313ed2 100644 --- a/cmd/ceremony/cert_test.go +++ b/cmd/ceremony/cert_test.go @@ -579,3 +579,10 @@ func TestLoadCert(t *testing.T) { _, err = loadCert("../../test/test-root.pubkey.pem") test.AssertError(t, err, "should have failed when trying to parse a public key") } + +func TestGenerateSKID(t *testing.T) { + sha256skid, err := generateSKID(samplePubkey()) + test.AssertNotError(t, err, "Error generating SKID") + test.AssertEquals(t, len(sha256skid), 20) + test.AssertEquals(t, cap(sha256skid), 20) +} diff --git a/features/featureflag_string.go b/features/featureflag_string.go index bf947cfc4..022e6bcad 100644 --- a/features/featureflag_string.go +++ b/features/featureflag_string.go @@ -28,11 +28,12 @@ func _() { _ = x[AsyncFinalize-17] _ = x[AllowNoCommonName-18] _ = x[CAAAfterValidation-19] + _ = x[SHA256SubjectKeyIdentifier-20] } -const _FeatureFlag_name = "unusedStoreRevokerInfoROCSPStage6ROCSPStage7StoreLintingCertificateInsteadOfPrecertificateCAAValidationMethodsCAAAccountURILeaseCRLShardsEnforceMultiVAMultiVAFullResultsECDSAForAllServeRenewalInfoAllowUnrecognizedFeaturesExpirationMailerUsesJoinCertCheckerChecksValidationsCertCheckerRequiresValidationsCertCheckerRequiresCorrespondenceAsyncFinalizeAllowNoCommonNameCAAAfterValidation" +const _FeatureFlag_name = "unusedStoreRevokerInfoROCSPStage6ROCSPStage7StoreLintingCertificateInsteadOfPrecertificateCAAValidationMethodsCAAAccountURILeaseCRLShardsEnforceMultiVAMultiVAFullResultsECDSAForAllServeRenewalInfoAllowUnrecognizedFeaturesExpirationMailerUsesJoinCertCheckerChecksValidationsCertCheckerRequiresValidationsCertCheckerRequiresCorrespondenceAsyncFinalizeAllowNoCommonNameCAAAfterValidationSHA256SubjectKeyIdentifier" -var _FeatureFlag_index = [...]uint16{0, 6, 22, 33, 44, 90, 110, 123, 137, 151, 169, 180, 196, 221, 245, 273, 303, 336, 349, 366, 384} +var _FeatureFlag_index = [...]uint16{0, 6, 22, 33, 44, 90, 110, 123, 137, 151, 169, 180, 196, 221, 245, 273, 303, 336, 349, 366, 384, 410} func (i FeatureFlag) String() string { if i < 0 || i >= FeatureFlag(len(_FeatureFlag_index)-1) { diff --git a/features/features.go b/features/features.go index ff65253f2..8839c2149 100644 --- a/features/features.go +++ b/features/features.go @@ -81,6 +81,11 @@ const ( // successful validations slower by serializing the DCV and CAA work, but // makes unsuccessful validations easier by not doing CAA work at all. CAAAfterValidation + + // SHA256SubjectKeyIdentifier enables the generation and use of an RFC 7093 + // compliant truncated SHA256 Subject Key Identifier in end-entity + // certificates. + SHA256SubjectKeyIdentifier ) // List of features and their default value, protected by fMu @@ -104,6 +109,7 @@ var features = map[FeatureFlag]bool{ AllowNoCommonName: false, LeaseCRLShards: false, CAAAfterValidation: false, + SHA256SubjectKeyIdentifier: false, StoreLintingCertificateInsteadOfPrecertificate: false, } diff --git a/issuance/cert.go b/issuance/cert.go index 43d3fb663..e21affdc4 100644 --- a/issuance/cert.go +++ b/issuance/cert.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha1" + "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -21,6 +22,7 @@ import ( ctx509 "github.com/google/certificate-transparency-go/x509" "github.com/jmhodges/clock" + "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/precert" ) @@ -211,8 +213,18 @@ func generateSKID(pk crypto.PublicKey) ([]byte, error) { if _, err := asn1.Unmarshal(pkBytes, &pkixPublicKey); err != nil { return nil, err } - skid := sha1.Sum(pkixPublicKey.BitString.Bytes) - return skid[:], nil + + if features.Enabled(features.SHA256SubjectKeyIdentifier) { + // RFC 7093 Section 2 Additional Methods for Generating Key Identifiers: + // The keyIdentifier [may be] composed of the leftmost 160-bits of the + // SHA-256 hash of the value of the BIT STRING subjectPublicKey + // (excluding the tag, length, and number of unused bits). + skid := sha256.Sum256(pkixPublicKey.BitString.Bytes) + return skid[0:20:20], nil + } else { + skid := sha1.Sum(pkixPublicKey.BitString.Bytes) + return skid[:], nil + } } // IssuanceRequest describes a certificate issuance request diff --git a/issuance/cert_test.go b/issuance/cert_test.go index d51277b68..8ae297f45 100644 --- a/issuance/cert_test.go +++ b/issuance/cert_test.go @@ -18,6 +18,7 @@ import ( "github.com/jmhodges/clock" "github.com/letsencrypt/boulder/ctpolicy/loglist" + "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/linter" "github.com/letsencrypt/boulder/test" ) @@ -764,3 +765,25 @@ func TestMismatchedProfiles(t *testing.T) { test.AssertError(t, err, "preparing final cert issuance") test.AssertContains(t, err.Error(), "precert does not correspond to linted final cert") } + +func TestGenerateSKID(t *testing.T) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + test.AssertNotError(t, err, "Error generating key") + + _ = features.Set(map[string]bool{"SHA256SubjectKeyIdentifier": true}) + defer features.Reset() + // RFC 7093 section 2 method 1 allows us to use 160 of the leftmost bits for + // the Subject Key Identifier. This is the same amount of bits as the + // related SHA1 hash. + sha256skid, err := generateSKID(key.Public()) + test.AssertNotError(t, err, "Error generating SKID") + test.AssertEquals(t, len(sha256skid), 20) + test.AssertEquals(t, cap(sha256skid), 20) + features.Reset() + + _ = features.Set(map[string]bool{"SHA256SubjectKeyIdentifier": false}) + sha1skid, err := generateSKID(key.Public()) + test.AssertNotError(t, err, "Error generating SKID") + test.AssertEquals(t, len(sha1skid), 20) + test.AssertEquals(t, cap(sha1skid), 20) +} diff --git a/test/config-next/ca-a.json b/test/config-next/ca-a.json index 2feac67ed..c1a28198c 100644 --- a/test/config-next/ca-a.json +++ b/test/config-next/ca-a.json @@ -113,7 +113,8 @@ "ctLogListFile": "test/ct-test-srv/log_list.json", "features": { "ECDSAForAll": true, - "AllowNoCommonName": true + "AllowNoCommonName": true, + "SHA256SubjectKeyIdentifier": true } }, "pa": { diff --git a/test/config-next/ca-b.json b/test/config-next/ca-b.json index 2feac67ed..c1a28198c 100644 --- a/test/config-next/ca-b.json +++ b/test/config-next/ca-b.json @@ -113,7 +113,8 @@ "ctLogListFile": "test/ct-test-srv/log_list.json", "features": { "ECDSAForAll": true, - "AllowNoCommonName": true + "AllowNoCommonName": true, + "SHA256SubjectKeyIdentifier": true } }, "pa": {