Skip to content

Commit 5a4924f

Browse files
committed
Make key identifier hashing selectable
Preserve legacy RFC 5280 SHA-1 subject key identifiers by default and allow callers and binary configuration to explicitly select RFC 7093 SHA-256. Made-with: Codex
1 parent 200b05c commit 5a4924f

4 files changed

Lines changed: 177 additions & 20 deletions

File tree

‎README.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,29 @@ correctly.
7575
5. cd pebble
7676
6. go install ./cmd/pebble
7777

78+
### FIPS builds
79+
80+
Ordinary builds retain Pebble's legacy RFC 5280 SHA-1 subject key identifiers.
81+
To use RFC 7093 SHA-256 subject key identifiers, set
82+
`subjectKeyIdentifierHash` to `sha256` in the `pebble` section of the
83+
configuration:
84+
85+
```json
86+
{
87+
"pebble": {
88+
"subjectKeyIdentifierHash": "sha256"
89+
}
90+
}
91+
```
92+
93+
The SHA-256 setting is required when running with `GODEBUG=fips140=only`. To
94+
build against the Go Cryptographic Module v1.0.0 with FIPS mode enabled by
95+
default:
96+
97+
```bash
98+
GOFIPS140=v1.0.0 go install ./cmd/pebble
99+
```
100+
78101
## Usage
79102

80103
### Binary

‎ca/ca.go‎

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import (
77
"crypto/elliptic"
88
"crypto/rand"
99
"crypto/rsa"
10-
"crypto/sha1"
10+
"crypto/sha1" //nolint:gosec // Required for legacy RFC 5280 SKI compatibility.
11+
"crypto/sha256"
1112
"crypto/x509"
1213
"crypto/x509/pkix"
1314
"encoding/asn1"
@@ -32,10 +33,37 @@ const (
3233
defaultValidityPeriod = 7776000
3334
)
3435

36+
// SubjectKeyIdentifierHash identifies the hash used to generate certificate
37+
// subject key identifiers.
38+
type SubjectKeyIdentifierHash string
39+
40+
const (
41+
SubjectKeyIdentifierHashSHA1 SubjectKeyIdentifierHash = "sha1"
42+
SubjectKeyIdentifierHashSHA256 SubjectKeyIdentifierHash = "sha256"
43+
)
44+
45+
// Option configures a CA.
46+
type Option struct {
47+
apply func(*options)
48+
}
49+
50+
// WithSubjectKeyIdentifierHash configures the hash used to generate
51+
// certificate subject key identifiers.
52+
func WithSubjectKeyIdentifierHash(hash SubjectKeyIdentifierHash) Option {
53+
return Option{apply: func(options *options) {
54+
options.subjectKeyIdentifierHash = hash
55+
}}
56+
}
57+
58+
type options struct {
59+
subjectKeyIdentifierHash SubjectKeyIdentifierHash
60+
}
61+
3562
type CAImpl struct {
36-
log *log.Logger
37-
db *db.MemoryStore
38-
ocspResponderURL string
63+
log *log.Logger
64+
db *db.MemoryStore
65+
ocspResponderURL string
66+
subjectKeyIdentifierHash SubjectKeyIdentifierHash
3967

4068
chains []*chain
4169
profiles map[string]*Profile
@@ -77,7 +105,7 @@ func makeSerial() *big.Int {
77105
}
78106

79107
// Taken from https://github.com/cloudflare/cfssl/blob/b94e044bb51ec8f5a7232c71b1ed05dbe4da96ce/signer/signer.go#L221-L244
80-
func makeSubjectKeyID(key crypto.PublicKey) ([]byte, error) {
108+
func makeSubjectKeyID(key crypto.PublicKey, hash SubjectKeyIdentifierHash) ([]byte, error) {
81109
// Marshal the public key as ASN.1
82110
pubAsDER, err := x509.MarshalPKIXPublicKey(key)
83111
if err != nil {
@@ -94,17 +122,28 @@ func makeSubjectKeyID(key crypto.PublicKey) ([]byte, error) {
94122
return nil, err
95123
}
96124

97-
// Hash it according to https://tools.ietf.org/html/rfc5280#section-4.2.1.2 Method #1:
98-
ski := sha1.Sum(pubInfo.SubjectPublicKey.Bytes)
99-
return ski[:], nil
125+
switch hash {
126+
case SubjectKeyIdentifierHashSHA256:
127+
// RFC 7093, section 2, method 1 uses the leftmost 160 bits of the
128+
// SHA-256 hash of the subjectPublicKey.
129+
ski := sha256.Sum256(pubInfo.SubjectPublicKey.Bytes)
130+
return ski[:20], nil
131+
case SubjectKeyIdentifierHashSHA1:
132+
// RFC 5280, section 4.2.1.2, method 1. SHA-1 is retained for legacy
133+
// compatibility when explicitly selected.
134+
ski := sha1.Sum(pubInfo.SubjectPublicKey.Bytes)
135+
return ski[:], nil
136+
default:
137+
return nil, fmt.Errorf("unsupported subject key identifier hash %q", hash)
138+
}
100139
}
101140

102141
// makeKey and makeRootCert are adapted from MiniCA:
103142
// https://github.com/jsha/minica/blob/3a621c05b61fa1c24bcb42fbde4b261db504a74f/main.go
104143

105144
// makeKey creates a new private key of the requested key algorithm, and
106145
// returns it and its corresponding Subject Key Identifier.
107-
func makeKey(keyAlg string) (crypto.Signer, []byte, error) {
146+
func (ca *CAImpl) makeKey(keyAlg string) (crypto.Signer, []byte, error) {
108147
var key crypto.Signer
109148
var err error
110149
switch keyAlg {
@@ -116,7 +155,7 @@ func makeKey(keyAlg string) (crypto.Signer, []byte, error) {
116155
if err != nil {
117156
return nil, nil, err
118157
}
119-
ski, err := makeSubjectKeyID(key.Public())
158+
ski, err := makeSubjectKeyID(key.Public(), ca.subjectKeyIdentifierHash)
120159
if err != nil {
121160
return nil, nil, err
122161
}
@@ -182,7 +221,7 @@ func (ca *CAImpl) makeCACert(
182221

183222
func (ca *CAImpl) newRootIssuer(name string, keyAlg string) (*issuer, error) {
184223
// Make a root private key
185-
rk, subjectKeyID, err := makeKey(keyAlg)
224+
rk, subjectKeyID, err := ca.makeKey(keyAlg)
186225
if err != nil {
187226
return nil, err
188227
}
@@ -238,7 +277,7 @@ func (ca *CAImpl) newChain(intermediateKey crypto.Signer, intermediateSubject pk
238277
prev := root
239278
intermediates := make([]*issuer, numIntermediates)
240279
for i := numIntermediates - 1; i > 0; i-- {
241-
k, ski, err := makeKey(keyAlg)
280+
k, ski, err := ca.makeKey(keyAlg)
242281
if err != nil {
243282
panic(fmt.Sprintf("Error creating new intermediate issuer: %v", err))
244283
}
@@ -373,11 +412,19 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
373412
return newCert, nil
374413
}
375414

376-
func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, keyAlg string, alternateRoots int, chainLength int, profiles map[string]Profile) *CAImpl {
415+
func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, keyAlg string, alternateRoots int, chainLength int, profiles map[string]Profile, opts ...Option) *CAImpl {
416+
options := options{subjectKeyIdentifierHash: SubjectKeyIdentifierHashSHA1}
417+
for _, option := range opts {
418+
if option.apply != nil {
419+
option.apply(&options)
420+
}
421+
}
422+
377423
ca := &CAImpl{
378-
log: log,
379-
db: db,
380-
profiles: make(map[string]*Profile, len(profiles)),
424+
log: log,
425+
db: db,
426+
subjectKeyIdentifierHash: options.subjectKeyIdentifierHash,
427+
profiles: make(map[string]*Profile, len(profiles)),
381428
}
382429

383430
if ocspResponderURL != "" {
@@ -388,7 +435,7 @@ func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, keyAlg st
388435
intermediateSubject := pkix.Name{
389436
CommonName: intermediateCAPrefix + hex.EncodeToString(makeSerial().Bytes()[:3]),
390437
}
391-
intermediateKey, subjectKeyID, err := makeKey(keyAlg)
438+
intermediateKey, subjectKeyID, err := ca.makeKey(keyAlg)
392439
if err != nil {
393440
panic(fmt.Sprintf("Error creating new intermediate private key: %s", err.Error()))
394441
}

‎ca/ca_test.go‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"bytes"
55
"crypto/ecdsa"
66
"crypto/elliptic"
7+
"crypto/fips140"
78
"crypto/rand"
89
"crypto/x509"
910
"crypto/x509/pkix"
1011
"encoding/asn1"
12+
"encoding/hex"
1113
"log"
1214
"net"
1315
"os"
@@ -27,7 +29,71 @@ var (
2729
func makeCa() *CAImpl {
2830
logger := log.New(os.Stdout, "Pebble ", log.LstdFlags)
2931
db := db.NewMemoryStore()
30-
return New(logger, db, "", "ecdsa", 0, 1, map[string]Profile{"default": {}})
32+
return New(
33+
logger,
34+
db,
35+
"",
36+
"ecdsa",
37+
0,
38+
1,
39+
map[string]Profile{"default": {}},
40+
WithSubjectKeyIdentifierHash(SubjectKeyIdentifierHashSHA256),
41+
)
42+
}
43+
44+
func testMakeSubjectKeyID(t *testing.T, hash SubjectKeyIdentifierHash, expected string) {
45+
t.Helper()
46+
publicKeyBytes, err := hex.DecodeString(
47+
"047f7f35a79794c950060b8029fc8f363a28f11159692d9d34e6ac948190434735" +
48+
"f833b1a66652dc514337aff7f5c9c75d670c019d95a5d639b72744c64a9128bb",
49+
)
50+
if err != nil {
51+
t.Fatal(err)
52+
}
53+
x, y := elliptic.Unmarshal(elliptic.P256(), publicKeyBytes)
54+
if x == nil || y == nil {
55+
t.Fatal("failed to parse RFC 7093 public key")
56+
}
57+
58+
got, err := makeSubjectKeyID(&ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, hash)
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
want, err := hex.DecodeString(expected)
63+
if err != nil {
64+
t.Fatal(err)
65+
}
66+
if !bytes.Equal(got, want) {
67+
t.Fatalf("unexpected subject key identifier: got %x, want %x", got, want)
68+
}
69+
}
70+
71+
func TestMakeSubjectKeyIDSHA1(t *testing.T) {
72+
if fips140.Enforced() {
73+
t.Skip("SHA-1 is unavailable when FIPS enforcement is enabled")
74+
}
75+
testMakeSubjectKeyID(t, SubjectKeyIdentifierHashSHA1, "6fef9162c0a3f2e7608956d41c37da0c8e87f0ae")
76+
}
77+
78+
func TestMakeSubjectKeyIDSHA256(t *testing.T) {
79+
testMakeSubjectKeyID(t, SubjectKeyIdentifierHashSHA256, "bf37b3e5808fd46d54b28e846311bcce1cad2e1a")
80+
}
81+
82+
func TestMakeSubjectKeyIDRejectsUnknownHash(t *testing.T) {
83+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
if _, err := makeSubjectKeyID(key.Public(), SubjectKeyIdentifierHash("unknown")); err == nil {
88+
t.Fatal("expected an error")
89+
}
90+
}
91+
92+
func TestWithSubjectKeyIdentifierHash(t *testing.T) {
93+
ca := makeCa()
94+
if ca.subjectKeyIdentifierHash != SubjectKeyIdentifierHashSHA256 {
95+
t.Fatalf("unexpected hash: got %q, want %q", ca.subjectKeyIdentifierHash, SubjectKeyIdentifierHashSHA256)
96+
}
3197
}
3298

3399
func makeCertOrderWithExtensions(extensions []pkix.Extension) core.Order {

‎cmd/pebble/main.go‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"errors"
45
"flag"
56
"fmt"
67
"log"
@@ -35,7 +36,9 @@ type config struct {
3536
// Configure policies to deny certain domains
3637
DomainBlocklist []string
3738
KeyAlgorithm string
38-
Profiles map[string]ca.Profile
39+
// Select the hash used for certificate subject key identifiers.
40+
SubjectKeyIdentifierHash string
41+
Profiles map[string]ca.Profile
3942

4043
RetryAfter struct {
4144
Authz int
@@ -115,6 +118,15 @@ func main() {
115118
cmd.FailOnError(fmt.Errorf("%q is not one of %#v", keyAlg, acceptableKeyAlgs), "invalid key algorithm")
116119
}
117120

121+
subjectKeyIdentifierHash := ca.SubjectKeyIdentifierHash(c.Pebble.SubjectKeyIdentifierHash)
122+
if subjectKeyIdentifierHash == "" {
123+
subjectKeyIdentifierHash = ca.SubjectKeyIdentifierHashSHA1
124+
}
125+
if subjectKeyIdentifierHash != ca.SubjectKeyIdentifierHashSHA1 &&
126+
subjectKeyIdentifierHash != ca.SubjectKeyIdentifierHashSHA256 {
127+
cmd.FailOnError(errors.New("must be sha1 or sha256"), "invalid subject key identifier hash")
128+
}
129+
118130
profiles := c.Pebble.Profiles
119131
if len(profiles) == 0 {
120132
profiles = map[string]ca.Profile{
@@ -126,7 +138,16 @@ func main() {
126138
}
127139

128140
db := db.NewMemoryStore()
129-
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, keyAlg, alternateRoots, chainLength, profiles)
141+
ca := ca.New(
142+
logger,
143+
db,
144+
c.Pebble.OCSPResponderURL,
145+
keyAlg,
146+
alternateRoots,
147+
chainLength,
148+
profiles,
149+
ca.WithSubjectKeyIdentifierHash(subjectKeyIdentifierHash),
150+
)
130151
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress, db)
131152

132153
for keyID, key := range c.Pebble.ExternalAccountMACKeys {

0 commit comments

Comments
 (0)