Issuance: carry SKID forward from precert (#7253)

Rather than regenerating the Subject Key ID during both precertificate
and final certificate issuance, carry the SKID forward from the precert
to the final cert. This ensures that the SKID remains stable between the
precert and final cert, even when the method for computing the SKID is
updated in the middle of certificate finalization.

Additionally, to ensure that the IssuanceRequest -> Certificate
conversion process is nearly identical for both precerts and final
certs, move SKID computation out of the issuance package and into the
CA, so that the SKID is always supplied as part of the issuance request
and the issuance package itself doesn't have conditionals or feature
flags regarding this behavior.
This commit is contained in:
Aaron Gable 2024-01-10 14:42:25 -08:00 committed by GitHub
parent 7b347dd6c3
commit 19bd4a7a87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 95 deletions

View File

@ -2,8 +2,13 @@ package ca
import (
"context"
"crypto"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"errors"
"fmt"
@ -344,6 +349,32 @@ func (ca *certificateAuthorityImpl) generateSerialNumberAndValidity() (*big.Int,
return serialBigInt, validity, nil
}
func generateSKID(pk crypto.PublicKey) ([]byte, error) {
pkBytes, err := x509.MarshalPKIXPublicKey(pk)
if err != nil {
return nil, err
}
var pkixPublicKey struct {
Algo pkix.AlgorithmIdentifier
BitString asn1.BitString
}
if _, err := asn1.Unmarshal(pkBytes, &pkixPublicKey); err != nil {
return nil, err
}
if features.Get().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
}
}
func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *capb.IssueCertificateRequest, serialBigInt *big.Int, validity validity) ([]byte, *issuance.Issuer, error) {
csr, err := x509.ParseCertificateRequest(issueReq.Csr)
if err != nil {
@ -385,6 +416,11 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
return nil, nil, err
}
subjectKeyId, err := generateSKID(csr.PublicKey)
if err != nil {
return nil, nil, fmt.Errorf("computing subject key ID: %w", err)
}
serialHex := core.SerialToString(serialBigInt)
ca.log.AuditInfof("Signing precert: serial=[%s] regID=[%d] names=[%s] csr=[%s]",
@ -393,6 +429,7 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
names := csrlib.NamesFromCSR(csr)
req := &issuance.IssuanceRequest{
PublicKey: csr.PublicKey,
SubjectKeyId: subjectKeyId,
Serial: serialBigInt.Bytes(),
DNSNames: names.SANs,
CommonName: names.CN,

View File

@ -3,6 +3,9 @@ package ca
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
@ -890,3 +893,25 @@ func TestIssueCertificateForPrecertificateDuplicateSerial(t *testing.T) {
t.Fatalf("Wrong type of error issuing duplicate serial. Expected 'error checking for duplicate', got '%s'", err)
}
}
func TestGenerateSKID(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "Error generating key")
features.Set(features.Config{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(features.Config{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)
}

View File

@ -6,8 +6,6 @@ import (
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
@ -22,7 +20,6 @@ import (
ctx509 "github.com/google/certificate-transparency-go/x509"
"github.com/jmhodges/clock"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/precert"
)
@ -88,6 +85,10 @@ func (p *Profile) requestValid(clk clock.Clock, req *IssuanceRequest) error {
return errors.New("unsupported public key type")
}
if len(req.SubjectKeyId) != 20 {
return errors.New("unexpected subject key ID length")
}
if !p.allowMustStaple && req.IncludeMustStaple {
return errors.New("must-staple extension cannot be included")
}
@ -201,35 +202,10 @@ var mustStapleExt = pkix.Extension{
Value: []byte{0x30, 0x03, 0x02, 0x01, 0x05},
}
func generateSKID(pk crypto.PublicKey) ([]byte, error) {
pkBytes, err := x509.MarshalPKIXPublicKey(pk)
if err != nil {
return nil, err
}
var pkixPublicKey struct {
Algo pkix.AlgorithmIdentifier
BitString asn1.BitString
}
if _, err := asn1.Unmarshal(pkBytes, &pkixPublicKey); err != nil {
return nil, err
}
if features.Get().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
type IssuanceRequest struct {
PublicKey crypto.PublicKey
PublicKey crypto.PublicKey
SubjectKeyId []byte
Serial []byte
@ -288,12 +264,6 @@ func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
}
template.DNSNames = req.DNSNames
skid, err := generateSKID(req.PublicKey)
if err != nil {
return nil, nil, err
}
template.SubjectKeyId = skid
switch req.PublicKey.(type) {
case *rsa.PublicKey:
template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
@ -301,6 +271,8 @@ func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
template.KeyUsage = x509.KeyUsageDigitalSignature
}
template.SubjectKeyId = req.SubjectKeyId
if req.IncludeCTPoison {
template.ExtraExtensions = append(template.ExtraExtensions, ctPoisonExt)
} else if len(req.sctList) > 0 {
@ -387,6 +359,7 @@ func RequestFromPrecert(precert *x509.Certificate, scts []ct.SignedCertificateTi
}
return &IssuanceRequest{
PublicKey: precert.PublicKey,
SubjectKeyId: precert.SubjectKeyId,
Serial: precert.SerialNumber.Bytes(),
NotBefore: precert.NotBefore,
NotAfter: precert.NotAfter,

View File

@ -18,11 +18,14 @@ 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"
)
var (
goodSKID = []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
)
func defaultProfile() *Profile {
p, _ := NewProfile(defaultProfileConfig(), defaultIssuerConfig())
return p
@ -43,6 +46,7 @@ func TestNewProfileNoOCSPURL(t *testing.T) {
func TestRequestValid(t *testing.T) {
fc := clock.NewFake()
fc.Add(time.Hour * 24)
tests := []struct {
name string
profile *Profile
@ -67,6 +71,21 @@ func TestRequestValid(t *testing.T) {
request: &IssuanceRequest{PublicKey: &ecdsa.PublicKey{}},
expectedError: "cannot sign ECDSA public keys",
},
{
name: "skid too short",
profile: &Profile{
useForECDSALeaves: true,
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: []byte{0, 1, 2, 3, 4},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
},
expectedError: "unexpected subject key ID length",
},
{
name: "must staple not allowed",
profile: &Profile{
@ -74,6 +93,7 @@ func TestRequestValid(t *testing.T) {
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
IncludeMustStaple: true,
},
expectedError: "must-staple extension cannot be included",
@ -85,6 +105,7 @@ func TestRequestValid(t *testing.T) {
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
IncludeCTPoison: true,
},
expectedError: "ct poison extension cannot be included",
@ -95,8 +116,9 @@ func TestRequestValid(t *testing.T) {
useForECDSALeaves: true,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
sctList: []ct.SignedCertificateTimestamp{},
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
sctList: []ct.SignedCertificateTimestamp{},
},
expectedError: "sct list extension cannot be included",
},
@ -109,6 +131,7 @@ func TestRequestValid(t *testing.T) {
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
IncludeCTPoison: true,
sctList: []ct.SignedCertificateTimestamp{},
},
@ -120,8 +143,9 @@ func TestRequestValid(t *testing.T) {
useForECDSALeaves: true,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
CommonName: "cn",
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
CommonName: "cn",
},
expectedError: "common name cannot be included",
},
@ -131,9 +155,10 @@ func TestRequestValid(t *testing.T) {
useForECDSALeaves: true,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now().Add(time.Hour),
NotAfter: fc.Now(),
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now().Add(time.Hour),
NotAfter: fc.Now(),
},
expectedError: "NotAfter must be after NotBefore",
},
@ -144,9 +169,10 @@ func TestRequestValid(t *testing.T) {
maxValidity: time.Minute,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
},
expectedError: "validity period is more than the maximum allowed period (1h0m0s>1m0s)",
},
@ -157,9 +183,10 @@ func TestRequestValid(t *testing.T) {
maxValidity: time.Hour,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
},
expectedError: "validity period is more than the maximum allowed period (1h0m1s>1h0m0s)",
},
@ -171,9 +198,10 @@ func TestRequestValid(t *testing.T) {
maxBackdate: time.Hour,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now().Add(-time.Hour * 2),
NotAfter: fc.Now().Add(-time.Hour),
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now().Add(-time.Hour * 2),
NotAfter: fc.Now().Add(-time.Hour),
},
expectedError: "NotBefore is backdated more than the maximum allowed period (2h0m0s>1h0m0s)",
},
@ -185,9 +213,10 @@ func TestRequestValid(t *testing.T) {
maxBackdate: time.Hour,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now().Add(time.Hour),
NotAfter: fc.Now().Add(time.Hour * 2),
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now().Add(time.Hour),
NotAfter: fc.Now().Add(time.Hour * 2),
},
expectedError: "NotBefore is in the future",
},
@ -198,10 +227,11 @@ func TestRequestValid(t *testing.T) {
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7},
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7},
},
expectedError: "serial must be between 9 and 19 bytes",
},
@ -212,10 +242,11 @@ func TestRequestValid(t *testing.T) {
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
},
expectedError: "serial must be between 9 and 19 bytes",
},
@ -226,10 +257,11 @@ func TestRequestValid(t *testing.T) {
maxValidity: time.Hour * 2,
},
request: &IssuanceRequest{
PublicKey: &ecdsa.PublicKey{},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
PublicKey: &ecdsa.PublicKey{},
SubjectKeyId: goodSKID,
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
},
},
}
@ -322,6 +354,7 @@ func TestIssue(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
lintCertBytes, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
CommonName: "example.com",
DNSNames: []string{"example.com"},
@ -366,6 +399,7 @@ func TestIssueRSA(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
@ -404,6 +438,7 @@ func TestIssueCommonName(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
ir := &IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
CommonName: "example.com",
DNSNames: []string{"example.com", "www.example.com"},
@ -453,6 +488,7 @@ func TestIssueCTPoison(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
IncludeCTPoison: true,
@ -497,6 +533,7 @@ func TestIssueSCTList(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
@ -568,6 +605,7 @@ func TestIssueMustStaple(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
IncludeMustStaple: true,
@ -599,6 +637,7 @@ func TestIssueBadLint(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, _, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example-com"},
NotBefore: fc.Now(),
@ -627,6 +666,7 @@ func TestIssuanceToken(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
@ -643,6 +683,7 @@ func TestIssuanceToken(t *testing.T) {
_, issuanceToken, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
@ -676,6 +717,7 @@ func TestInvalidProfile(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, _, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
@ -686,11 +728,12 @@ func TestInvalidProfile(t *testing.T) {
test.AssertError(t, err, "Invalid IssuanceRequest")
_, _, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
sctList: []ct.SignedCertificateTimestamp{
{
SCTVersion: ct.V1,
@ -723,6 +766,7 @@ func TestMismatchedProfiles(t *testing.T) {
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := issuer1.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
SubjectKeyId: goodSKID,
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
CommonName: "example.com",
DNSNames: []string{"example.com"},
@ -765,25 +809,3 @@ 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(features.Config{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(features.Config{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)
}