diff --git a/cmd/ceremony/cert.go b/cmd/ceremony/cert.go index 720dff50242..6c8a5c4f52d 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 10e6a969658..c31313ed290 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 bf947cfc442..022e6bcadb1 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 ff65253f26f..8839c214917 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 43d3fb663b3..e21affdc495 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 d51277b6845..8ae297f45cb 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 2feac67ed1f..c1a28198cb9 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 2feac67ed1f..c1a28198cb9 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": {