X Tutup
Skip to content

Commit 83a7220

Browse files
admin-revoker: Block and revoke by private key (letsencrypt#5878)
Incidents of key compromise where proof is supplied in the form of a private key have historically been labor intensive for SRE. This PR seeks to automate the process of embedded public key validation , query for issuance, revocation, and blocking by SPKI hash. For an example of private keys embedding a mismatched public key, see: https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html. Adds two new sub-commands (private-key-block and private-key-revoke) and one new flag (-dry-run) to admin-revoker. Both new sub-commands validate that the provided private key and provide the operator with an issuance count. Any blocking and revocation actions are gated by the new '-dry-run' flag, which is 'true' by default. private-key-block: if -dry-run=false, will immediately block issuance for the provided key. The operator is informed that bad-key-revoker will eventually revoke any certificates using the provided key. private-key-revoke: if -dry-run=false, will revoke all certificates using the provided key and then blocks future issuance. This avoids a race with the bad-key-revoker. This command will execute successfully even if issuance for the provided key is already blocked. - Add support for blocking issuance by private key to admin-revoker - Add support for revoking certificates by private key to admin-revoker - Create new package called 'privatekey' - Move private key loading logic from 'issuance' to 'privatekey' - Add embedded public key verification to 'privatekey' - Add new field `skipBlockKey` to `AdministrativelyRevokeCertificate` protobuf - Add check in RA to ensure that only KeyCompromise revocations use `skipBlockKey` Fixes letsencrypt#5785
1 parent ab79f96 commit 83a7220

File tree

9 files changed

+970
-128
lines changed

9 files changed

+970
-128
lines changed

cmd/admin-revoker/main.go

Lines changed: 284 additions & 28 deletions
Large diffs are not rendered by default.

cmd/admin-revoker/main_test.go

Lines changed: 413 additions & 0 deletions
Large diffs are not rendered by default.

issuance/issuance.go

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"crypto/x509/pkix"
1414
"encoding/asn1"
1515
"encoding/json"
16-
"encoding/pem"
1716
"errors"
1817
"fmt"
1918
"io/ioutil"
@@ -30,6 +29,7 @@ import (
3029
"github.com/letsencrypt/boulder/core"
3130
"github.com/letsencrypt/boulder/linter"
3231
"github.com/letsencrypt/boulder/policyasn1"
32+
"github.com/letsencrypt/boulder/privatekey"
3333
"github.com/letsencrypt/pkcs11key/v4"
3434
)
3535

@@ -114,44 +114,11 @@ func LoadCertificate(path string) (*Certificate, error) {
114114

115115
func loadSigner(location IssuerLoc, cert *Certificate) (crypto.Signer, error) {
116116
if location.File != "" {
117-
keyBytes, err := ioutil.ReadFile(location.File)
117+
signer, _, err := privatekey.Load(location.File)
118118
if err != nil {
119-
return nil, fmt.Errorf("Could not read key file %q", location.File)
120-
}
121-
122-
var keyDER *pem.Block
123-
for {
124-
keyDER, keyBytes = pem.Decode(keyBytes)
125-
if keyDER == nil || keyDER.Type != "EC PARAMETERS" {
126-
break
127-
}
128-
}
129-
if keyDER == nil {
130-
return nil, fmt.Errorf("No key block found in %q", location.File)
131-
}
132-
133-
// Try to interpret the bytes first as a generic PKCS8 key, then fall back
134-
// to a PKCS1 RSA key, then fall back to an EC key.
135-
// These blocks use the opposite of normal error checking patterns, to let
136-
// us early-return once we successfully parse once.
137-
signer, err := x509.ParsePKCS8PrivateKey(keyDER.Bytes)
138-
if err == nil {
139-
switch signer := signer.(type) {
140-
case *rsa.PrivateKey:
141-
return signer, nil
142-
case *ecdsa.PrivateKey:
143-
return signer, nil
144-
}
145-
}
146-
rsaSigner, err := x509.ParsePKCS1PrivateKey(keyDER.Bytes)
147-
if err == nil {
148-
return rsaSigner, nil
149-
}
150-
ecdsaSigner, err := x509.ParseECPrivateKey(keyDER.Bytes)
151-
if err == nil {
152-
return ecdsaSigner, nil
119+
return nil, err
153120
}
154-
return nil, fmt.Errorf("Unable to parse %q", location.File)
121+
return signer, nil
155122
}
156123

157124
var pkcs11Config *pkcs11key.Config

privatekey/privatekey.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package privatekey
2+
3+
import (
4+
"crypto"
5+
"crypto/ecdsa"
6+
"crypto/rand"
7+
"crypto/rsa"
8+
"crypto/sha256"
9+
"crypto/x509"
10+
"encoding/pem"
11+
"errors"
12+
"fmt"
13+
"hash"
14+
"io/ioutil"
15+
)
16+
17+
func makeVerifyHash() (hash.Hash, error) {
18+
randBytes := make([]byte, 32)
19+
_, err := rand.Read(randBytes)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
hash := sha256.New()
25+
_, err = hash.Write(randBytes)
26+
if err != nil {
27+
return nil, err
28+
}
29+
return hash, nil
30+
}
31+
32+
// verifyRSA is broken out of Verify for testing purposes.
33+
func verifyRSA(privKey *rsa.PrivateKey, pubKey *rsa.PublicKey, msgHash hash.Hash) (crypto.Signer, crypto.PublicKey, error) {
34+
signatureRSA, err := rsa.SignPSS(rand.Reader, privKey, crypto.SHA256, msgHash.Sum(nil), nil)
35+
if err != nil {
36+
return nil, nil, fmt.Errorf("failed to sign using the provided RSA private key: %s", err)
37+
}
38+
39+
err = rsa.VerifyPSS(pubKey, crypto.SHA256, msgHash.Sum(nil), signatureRSA, nil)
40+
if err != nil {
41+
return nil, nil, fmt.Errorf("the provided RSA private key failed signature verification: %s", err)
42+
}
43+
return privKey, privKey.Public(), nil
44+
}
45+
46+
// verifyECDSA is broken out of Verify for testing purposes.
47+
func verifyECDSA(privKey *ecdsa.PrivateKey, pubKey *ecdsa.PublicKey, msgHash hash.Hash) (crypto.Signer, crypto.PublicKey, error) {
48+
r, s, err := ecdsa.Sign(rand.Reader, privKey, msgHash.Sum(nil))
49+
if err != nil {
50+
return nil, nil, fmt.Errorf("failed to sign using the provided ECDSA private key: %s", err)
51+
}
52+
53+
verify := ecdsa.Verify(pubKey, msgHash.Sum(nil), r, s)
54+
if !verify {
55+
return nil, nil, errors.New("the provided ECDSA private key failed signature verification")
56+
}
57+
return privKey, privKey.Public(), nil
58+
}
59+
60+
// verify ensures that the embedded PublicKey of the provided privateKey is
61+
// actually a match for the private key. For an example of private keys
62+
// embedding a mismatched public key, see:
63+
// https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html.
64+
func verify(privateKey crypto.Signer) (crypto.Signer, crypto.PublicKey, error) {
65+
verifyHash, err := makeVerifyHash()
66+
if err != nil {
67+
return nil, nil, err
68+
}
69+
70+
switch k := privateKey.(type) {
71+
case *rsa.PrivateKey:
72+
return verifyRSA(k, &k.PublicKey, verifyHash)
73+
74+
case *ecdsa.PrivateKey:
75+
return verifyECDSA(k, &k.PublicKey, verifyHash)
76+
77+
default:
78+
// This should never happen.
79+
return nil, nil, errors.New("the provided private key could not be asserted to ECDSA or RSA")
80+
}
81+
}
82+
83+
// Load decodes and parses a private key from the provided file path and returns
84+
// the private key as crypto.Signer. keyPath is expected to be a PEM formatted
85+
// RSA or ECDSA private key in a PKCS #1, PKCS# 8, or SEC 1 container. The
86+
// embedded PublicKey of the provided private key will be verified as an actual
87+
// match for the private key and returned as a crypto.PublicKey. This function
88+
// is only intended for use in administrative tooling and tests.
89+
func Load(keyPath string) (crypto.Signer, crypto.PublicKey, error) {
90+
keyBytes, err := ioutil.ReadFile(keyPath)
91+
if err != nil {
92+
return nil, nil, fmt.Errorf("could not read key file %q", keyPath)
93+
}
94+
95+
var keyDER *pem.Block
96+
for {
97+
keyDER, keyBytes = pem.Decode(keyBytes)
98+
if keyDER == nil || keyDER.Type != "EC PARAMETERS" {
99+
break
100+
}
101+
}
102+
if keyDER == nil {
103+
return nil, nil, fmt.Errorf("no PEM formatted block found in %q", keyPath)
104+
}
105+
106+
// Attempt to parse the PEM block as a private key in a PKCS #8 container.
107+
signer, err := x509.ParsePKCS8PrivateKey(keyDER.Bytes)
108+
if err == nil {
109+
crytoSigner, ok := signer.(crypto.Signer)
110+
if ok {
111+
return verify(crytoSigner)
112+
}
113+
}
114+
115+
// Attempt to parse the PEM block as a private key in a PKCS #1 container.
116+
rsaSigner, err := x509.ParsePKCS1PrivateKey(keyDER.Bytes)
117+
if err == nil {
118+
return verify(rsaSigner)
119+
}
120+
121+
// Attempt to parse the PEM block as a private key in a SEC 1 container.
122+
ecdsaSigner, err := x509.ParseECPrivateKey(keyDER.Bytes)
123+
if err == nil {
124+
return verify(ecdsaSigner)
125+
}
126+
return nil, nil, fmt.Errorf("unable to parse %q as a private key", keyPath)
127+
}

privatekey/privatekey_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package privatekey
2+
3+
import (
4+
"crypto/ecdsa"
5+
"crypto/elliptic"
6+
"crypto/rand"
7+
"crypto/rsa"
8+
"testing"
9+
10+
"github.com/letsencrypt/boulder/test"
11+
)
12+
13+
func TestVerifyRSAKeyPair(t *testing.T) {
14+
privKey1, err := rsa.GenerateKey(rand.Reader, 2048)
15+
test.AssertNotError(t, err, "Failed while generating test key 1")
16+
17+
_, _, err = verify(privKey1)
18+
test.AssertNotError(t, err, "Failed to verify valid key")
19+
20+
privKey2, err := rsa.GenerateKey(rand.Reader, 2048)
21+
test.AssertNotError(t, err, "Failed while generating test key 2")
22+
23+
verifyHash, err := makeVerifyHash()
24+
test.AssertNotError(t, err, "Failed to make verify hash: %s")
25+
26+
_, _, err = verifyRSA(privKey1, &privKey2.PublicKey, verifyHash)
27+
test.AssertError(t, err, "Failed to detect invalid key pair")
28+
}
29+
30+
func TestVerifyECDSAKeyPair(t *testing.T) {
31+
privKey1, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
32+
test.AssertNotError(t, err, "Failed while generating test key 1")
33+
34+
_, _, err = verify(privKey1)
35+
test.AssertNotError(t, err, "Failed to verify valid key")
36+
37+
privKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
38+
test.AssertNotError(t, err, "Failed while generating test key 2")
39+
40+
verifyHash, err := makeVerifyHash()
41+
test.AssertNotError(t, err, "Failed to make verify hash: %s")
42+
43+
_, _, err = verifyECDSA(privKey1, &privKey2.PublicKey, verifyHash)
44+
test.AssertError(t, err, "Failed to detect invalid key pair")
45+
}
46+
47+
func TestLoad(t *testing.T) {
48+
signer, public, err := Load("../test/hierarchy/ee-e1.key.pem")
49+
test.AssertNotError(t, err, "Failed to load a valid ECDSA key file")
50+
test.AssertNotNil(t, signer, "Signer should not be Nil")
51+
test.AssertNotNil(t, public, "Public should not be Nil")
52+
53+
signer, public, err = Load("../test/hierarchy/ee-r3.key.pem")
54+
test.AssertNotError(t, err, "Failed to load a valid RSA key file")
55+
test.AssertNotNil(t, signer, "Signer should not be Nil")
56+
test.AssertNotNil(t, public, "Public should not be Nil")
57+
58+
signer, public, err = Load("../test/hierarchy/ee-e1.cert.pem")
59+
test.AssertError(t, err, "Should have failed, file is a certificate")
60+
test.AssertEquals(t, signer, nil)
61+
test.AssertEquals(t, public, nil)
62+
}

0 commit comments

Comments
 (0)
X Tutup