Support admin. blocking public keys. (#4419)

We occasionally have reason to block public keys from being used in CSRs
or for JWKs. This work adds support for loading a YAML blocked keys list
to the WFE, the RA and the CA (all the components already using the
`goodekey` package).

The list is loaded in-memory and is intended to be used sparingly and
not for more complicated mass blocking scenarios. This augments the
existing debian weak key checking which is specific to RSA keys and
operates on a truncated hash of the key modulus. In comparison the
admin. blocked keys are identified by the Base64 encoding of a SHA256
hash over the DER encoding of the public key expressed as a PKIX subject
public key. For ECDSA keys in particular we believe a more thorough
solution would have to consider inverted curve points but to start we're
calling this approach "Good Enough".

A utility program (`block-a-key`) is provided that can read a PEM
formatted x509 certificate or a JSON formatted JWK and emit lines to be
added to the blocked keys YAML to block the related public key.

A test blocked keys YAML file is included
(`test/example-blocked-keys.yml`), initially populated with a few of the
keys from the `test/` directory. We may want to do a more through pass
through Boulder's source code and add a block entry for every test
private key.

Resolves https://github.com/letsencrypt/boulder/issues/4404
This commit is contained in:
Daniel McCarney 2019-09-06 16:54:26 -04:00 committed by GitHub
parent a8586d05cd
commit f02e9da38f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 536 additions and 6 deletions

View File

@ -46,6 +46,11 @@ type CAConfig struct {
// hashes of known easily enumerable keys.
WeakKeyFile string
// BlockedKeyFile is the path to a YAML file containing Base64 encoded
// SHA256 hashes of DER encoded PKIX public keys that should be considered
// administratively blocked.
BlockedKeyFile string
SAService *cmd.GRPCClientConfig
// Path to directory holding orphan queue files, if not provided an orphan queue

View File

@ -156,7 +156,7 @@ func main() {
issuers, err := loadIssuers(c)
cmd.FailOnError(err, "Couldn't load issuers")
kp, err := goodkey.NewKeyPolicy(c.CA.WeakKeyFile)
kp, err := goodkey.NewKeyPolicy(c.CA.WeakKeyFile, c.CA.BlockedKeyFile)
cmd.FailOnError(err, "Unable to create key policy")
tlsConfig, err := c.CA.TLS.Load()

View File

@ -64,6 +64,11 @@ type config struct {
// hashes of known easily enumerable keys.
WeakKeyFile string
// BlockedKeyFile is the path to a YAML file containing Base64 encoded
// SHA256 hashes of DER encoded PKIX public keys that should be considered
// administratively blocked.
BlockedKeyFile string
OrderLifetime cmd.ConfigDuration
// CTLogGroups contains groupings of CT logs which we want SCTs from.
@ -199,7 +204,7 @@ func main() {
pendingAuthorizationLifetime = time.Duration(c.RA.PendingAuthorizationLifetimeDays) * 24 * time.Hour
}
kp, err := goodkey.NewKeyPolicy(c.RA.WeakKeyFile)
kp, err := goodkey.NewKeyPolicy(c.RA.WeakKeyFile, c.RA.BlockedKeyFile)
cmd.FailOnError(err, "Unable to create key policy")
if c.RA.MaxNames == 0 {

View File

@ -59,6 +59,11 @@ type config struct {
// DirectoryWebsite is used for the /directory response's "meta" element's
// "website" field.
DirectoryWebsite string
// BlockedKeyFile is the path to a YAML file containing Base64 encoded
// SHA256 hashes of DER encoded PKIX public keys that should be considered
// administratively blocked.
BlockedKeyFile string
}
Syslog cmd.SyslogConfig
@ -118,7 +123,8 @@ func main() {
clk := cmd.Clock()
kp, err := goodkey.NewKeyPolicy("") // don't load any weak keys
// don't load any weak keys, but do load blocked keys
kp, err := goodkey.NewKeyPolicy("", c.WFE.BlockedKeyFile)
cmd.FailOnError(err, "Unable to create key policy")
rac, sac, rns, npm := setupWFE(c, logger, scope, clk)
wfe, err := wfe.NewWebFrontEndImpl(scope, clk, kp, rns, npm, logger)

View File

@ -77,6 +77,11 @@ type config struct {
// header of the WFE1 instance and the legacy 'reg' path component. This
// will differ in configuration for production and staging.
LegacyKeyIDPrefix string
// BlockedKeyFile is the path to a YAML file containing Base64 encoded
// SHA256 hashes of DER encoded PKIX public keys that should be considered
// administratively blocked.
BlockedKeyFile string
}
Syslog cmd.SyslogConfig
@ -239,7 +244,8 @@ func main() {
clk := cmd.Clock()
kp, err := goodkey.NewKeyPolicy("") // don't load any weak keys
// don't load any weak keys, but do load blocked keys
kp, err := goodkey.NewKeyPolicy("", c.WFE.BlockedKeyFile)
cmd.FailOnError(err, "Unable to create key policy")
rac, sac, rns, npm := setupWFE(c, logger, scope, clk)
wfe, err := wfe2.NewWebFrontEndImpl(scope, clk, kp, certChains, rns, npm, logger)

View File

@ -25,6 +25,7 @@ const (
Duplicate
OrderNotReady
DNS
BadPublicKey
)
// BoulderError represents internal Boulder errors
@ -130,3 +131,7 @@ func OrderNotReadyError(msg string, args ...interface{}) error {
func DNSError(msg string, args ...interface{}) error {
return New(DNS, msg, args...)
}
func BadPublicKeyError(msg string, args ...interface{}) error {
return New(BadPublicKey, msg, args...)
}

70
goodkey/blocked.go Normal file
View File

@ -0,0 +1,70 @@
package goodkey
import (
"crypto"
"errors"
"io/ioutil"
"github.com/letsencrypt/boulder/core"
yaml "gopkg.in/yaml.v2"
)
// blockedKeys is a type for maintaining a map of Base64 encoded SHA256 hashes
// of DER encoded PKIX public keys that should be considered blocked.
// blockedKeys are created by using loadBlockedKeysList.
type blockedKeys map[string]bool
// blocked checks if the given public key is considered administratively
// blocked based on a Base64 encoded SHA256 hash of the DER encoded PKIX public
// key. Important: blocked should not be called except on a blockedKeys instance
// returned from loadBlockedKeysList.
// function should not be used until after `loadBlockedKeysList` has returned.
func (b blockedKeys) blocked(key crypto.PublicKey) (bool, error) {
hash, err := core.KeyDigest(key)
if err != nil {
// the bool result should be ignored when err is != nil but to be on the
// paranoid side return true anyway so that a key we can't compute the
// digest for will always be blocked even if a caller foolishly discards the
// err result.
return true, err
}
return b[hash], nil
}
// loadBlockedKeysList creates a blockedKeys object that can be used to check if
// a key is blocked. It creates a lookup map from a list of Base64 encoded
// SHA256 digest of a DER encoded PKIX public key hashes in the input YAML file
// with the expected format:
//
// ```
// blocked:
// - cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=
// <snipped>
// - Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=
// ```
//
// If no hashes are found in the input YAML an error is returned.
func loadBlockedKeysList(filename string) (*blockedKeys, error) {
yamlBytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var list struct {
BlockedHashes []string `yaml:"blocked"`
}
if err := yaml.Unmarshal(yamlBytes, &list); err != nil {
return nil, err
}
if len(list.BlockedHashes) == 0 {
return nil, errors.New("no blocked hashes in YAML")
}
blockedKeys := make(blockedKeys, len(list.BlockedHashes))
for _, hash := range list.BlockedHashes {
blockedKeys[hash] = true
}
return &blockedKeys, nil
}

99
goodkey/blocked_test.go Normal file
View File

@ -0,0 +1,99 @@
package goodkey
import (
"crypto"
"io/ioutil"
"os"
"testing"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/test"
"github.com/letsencrypt/boulder/web"
yaml "gopkg.in/yaml.v2"
)
func TestBlockedKeys(t *testing.T) {
// Start with an empty list
var inList struct {
BlockedHashes []string `yaml:"blocked"`
}
yamlList, err := yaml.Marshal(&inList)
test.AssertNotError(t, err, "error marshaling test blockedKeys list")
yamlListFile, err := ioutil.TempFile("", "test-blocked-keys-list.*.yaml")
test.AssertNotError(t, err, "error creating test blockedKeys yaml file")
defer os.Remove(yamlListFile.Name())
err = ioutil.WriteFile(yamlListFile.Name(), yamlList, 0640)
test.AssertNotError(t, err, "error writing test blockedKeys yaml file")
// Trying to load it should error
_, err = loadBlockedKeysList(yamlListFile.Name())
test.AssertError(t, err, "expected error loading empty blockedKeys yaml file")
// Load some test certs/keys - see ../test/block-a-key/test/README.txt
// for more information.
testCertA, err := core.LoadCert("../test/block-a-key/test/test.rsa.cert.pem")
test.AssertNotError(t, err, "error loading test.rsa.cert.pem")
testCertB, err := core.LoadCert("../test/block-a-key/test/test.ecdsa.cert.pem")
test.AssertNotError(t, err, "error loading test.ecdsa.cert.pem")
testJWKA, err := web.LoadJWK("../test/block-a-key/test/test.rsa.jwk.json")
test.AssertNotError(t, err, "error loading test.rsa.jwk.pem")
testJWKB, err := web.LoadJWK("../test/block-a-key/test/test.ecdsa.jwk.json")
test.AssertNotError(t, err, "error loading test.ecdsa.jwk.pem")
// All of the above should be blocked
blockedKeys := []crypto.PublicKey{
testCertA.PublicKey,
testCertB.PublicKey,
testJWKA.Key,
testJWKB.Key,
}
// Now use a populated list - these values match the base64 digest of the
// public keys in the test certs/JWKs
inList.BlockedHashes = []string{
"cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=",
"Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=",
}
yamlList, err = yaml.Marshal(&inList)
test.AssertNotError(t, err, "error marshaling test blockedKeys list")
yamlListFile, err = ioutil.TempFile("", "test-blocked-keys-list.*.yaml")
test.AssertNotError(t, err, "error creating test blockedKeys yaml file")
defer os.Remove(yamlListFile.Name())
err = ioutil.WriteFile(yamlListFile.Name(), yamlList, 0640)
test.AssertNotError(t, err, "error writing test blockedKeys yaml file")
// Trying to load it should not error
outList, err := loadBlockedKeysList(yamlListFile.Name())
test.AssertNotError(t, err, "unexpected error loading empty blockedKeys yaml file")
// Create a test policy that doesn't reference the blocked list
testingPolicy := &KeyPolicy{
AllowRSA: true,
AllowECDSANISTP256: true,
AllowECDSANISTP384: true,
}
// All of the test keys should not be considered blocked
for _, k := range blockedKeys {
err := testingPolicy.GoodKey(k)
test.AssertNotError(t, err, "test key was blocked by key policy without block list")
}
// Now update the key policy with the blocked list
testingPolicy.blockedList = outList
// Now all of the test keys should be considered blocked, and with the correct
// type of error.
for _, k := range blockedKeys {
err := testingPolicy.GoodKey(k)
test.AssertError(t, err, "test key was not blocked by key policy with block list")
test.Assert(t, berrors.Is(err, berrors.BadPublicKey), "err was not BadPublicKey error")
}
}

View File

@ -41,13 +41,17 @@ type KeyPolicy struct {
AllowECDSANISTP256 bool // Whether ECDSA NISTP256 keys should be allowed.
AllowECDSANISTP384 bool // Whether ECDSA NISTP384 keys should be allowed.
weakRSAList *WeakRSAKeys
blockedList *blockedKeys
}
// NewKeyPolicy returns a KeyPolicy that allows RSA, ECDSA256 and ECDSA384.
// weakKeyFile contains the path to a JSON file containing truncated modulus
// hashes of known weak RSA keys. If this argument is empty RSA modulus hash
// checking will be disabled.
func NewKeyPolicy(weakKeyFile string) (KeyPolicy, error) {
// checking will be disabled. blockedKeyFile contains the path to a YAML file
// containing Base64 encoded SHA256 hashes of pkix subject public keys that
// should be blocked. If this argument is empty then no blocked key checking is
// performed.
func NewKeyPolicy(weakKeyFile, blockedKeyFile string) (KeyPolicy, error) {
kp := KeyPolicy{
AllowRSA: true,
AllowECDSANISTP256: true,
@ -60,6 +64,13 @@ func NewKeyPolicy(weakKeyFile string) (KeyPolicy, error) {
}
kp.weakRSAList = keyList
}
if blockedKeyFile != "" {
blocked, err := loadBlockedKeysList(blockedKeyFile)
if err != nil {
return KeyPolicy{}, err
}
kp.blockedList = blocked
}
return kp, nil
}
@ -68,6 +79,15 @@ func NewKeyPolicy(weakKeyFile string) (KeyPolicy, error) {
// strength and algorithm checking.
// TODO: Support JSONWebKeys once go-jose migration is done.
func (policy *KeyPolicy) GoodKey(key crypto.PublicKey) error {
// If there is a blocked list configured then check if the public key is one
// that has been administratively blocked.
if policy.blockedList != nil {
if blocked, err := policy.blockedList.blocked(key); err != nil {
return berrors.InternalServerError("error checking blocklist for key: %v", key)
} else if blocked {
return berrors.BadPublicKeyError("public key is forbidden")
}
}
switch t := key.(type) {
case rsa.PublicKey:
return policy.goodKeyRSA(t)

104
test/block-a-key/main.go Normal file
View File

@ -0,0 +1,104 @@
// block-a-key is a small utility for creating key blocklist entries.
package main
import (
"crypto"
"errors"
"flag"
"fmt"
"log"
"os"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/web"
)
const usageHelp = `
block-a-key is utility tool for generating a Base64 encoded SHA256 digest of
a certificate or JWK public key in DER encoded PKIX subject public key form.
The produced encoded digest can be used with Boulder's key blocklist to block
any ACME account creation or certificate requests that use the same public
key.
installation:
go install github.com/letsencrypt/boulder/test/block-a-key/...
usage:
block-a-key -cert <PEM encoded x509 certificate file>
block-a-key -jwk <JSON encoded JWK file>
output format:
# <filepath>
- "<base64 encoded SHA256 subject public key digest>"
examples:
$> block-a-key -jwk ./test/block-a-key/test/test.ecdsa.jwk.json
./test/block-a-key/test/test.ecdsa.jwk.json cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=
$> block-a-key -cert ./test/block-a-key/test/test.rsa.cert.pem
./test/block-a-key/test/test.rsa.cert.pem Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=
`
// keyFromCert returns the public key from a PEM encoded certificate located in
// pemFile or returns an error.
func keyFromCert(pemFile string) (crypto.PublicKey, error) {
c, err := core.LoadCert(pemFile)
if err != nil {
return nil, err
}
return c.PublicKey, nil
}
// keyFromJWK returns the public key from a JSON encoded JOSE JWK located in
// jsonFile or returns an error.
func keyFromJWK(jsonFile string) (crypto.PublicKey, error) {
jwk, err := web.LoadJWK(jsonFile)
if err != nil {
return nil, err
}
return jwk.Key, nil
}
func main() {
certFileArg := flag.String("cert", "", "path to a PEM encoded X509 certificate file")
jwkFileArg := flag.String("jwk", "", "path to a JSON encoded JWK file")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "%s\n\n", usageHelp)
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if *certFileArg == "" && *jwkFileArg == "" {
log.Fatalf("error: a -cert or -jwk argument must be provided")
}
if *certFileArg != "" && *jwkFileArg != "" {
log.Fatalf("error: -cert and -jwk arguments are mutually exclusive")
}
var file string
var key crypto.PublicKey
var err error
if *certFileArg != "" {
file = *certFileArg
key, err = keyFromCert(file)
} else if *jwkFileArg != "" {
file = *jwkFileArg
key, err = keyFromJWK(file)
} else {
err = errors.New("unexpected command line state")
}
if err != nil {
log.Fatalf("error loading public key: %v", err)
}
spkiHash, err := core.KeyDigest(key)
if err != nil {
log.Fatalf("error computing spki hash: %v", err)
}
fmt.Printf(" # %s\n - %s\n", file, spkiHash)
}

View File

@ -0,0 +1,59 @@
package main
import (
"crypto"
"testing"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/test"
)
func TestKeyBlocking(t *testing.T) {
testCases := []struct {
name string
certPath string
jwkPath string
expected string
}{
// NOTE(@cpu): The JWKs and certificates were generated with the same
// keypair within an algorithm/parameter family. E.g. the RSA JWK public key
// matches the RSA certificate public key. The ECDSA JWK public key matches
// the ECDSA certificate public key.
{
name: "P-256 ECDSA JWK",
jwkPath: "test/test.ecdsa.jwk.json",
expected: "cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=",
},
{
name: "2048 RSA JWK",
jwkPath: "test/test.rsa.jwk.json",
expected: "Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=",
},
{
name: "P-256 ECDSA Certificate",
certPath: "test/test.ecdsa.cert.pem",
expected: "cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=",
},
{
name: "2048 RSA Certificate",
certPath: "test/test.rsa.cert.pem",
expected: "Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var key crypto.PublicKey
var err error
if tc.jwkPath != "" {
key, err = keyFromJWK(tc.jwkPath)
} else {
key, err = keyFromCert(tc.certPath)
}
test.AssertNotError(t, err, "error getting key from input file")
spkiHash, err := core.KeyDigest(key)
test.AssertNotError(t, err, "error computing spki hash")
test.AssertEquals(t, spkiHash, tc.expected)
})
}
}

View File

@ -0,0 +1,7 @@
The test files in this directory can be recreated with the following small program:
https://gist.github.com/cpu/df50564a473b3e8556917eb80d99ea56
Crucially the public keys in the generated JWKs/Certs are shared within
algorithm/parameters. E.g. the ECDSA JWK has the same public key as the ECDSA
Cert.

View File

@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE-----
MIH1MIGboAMCAQICAQEwCgYIKoZIzj0EAwIwADAiGA8wMDAxMDEwMTAwMDAwMFoY
DzAwMDEwMTAxMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4LqG
kzIYWSgmyTS+B9Eet1xx1wpCKiSklMPnHfFp8eSHr1uNk6ilWv/s4AoKHSvMNAb/
1uPfxjlijEIjK2bOQKMCMAAwCgYIKoZIzj0EAwIDSQAwRgIhAJBK1/C1BYDnzSCu
cR2pE40d8dyrRuHKj8htO/fzRgCgAiEA0UG0Vda8w0Tp84AMlJpZHOx9QUbwExSl
oFEDADJ9WQM=
-----END CERTIFICATE-----

View File

@ -0,0 +1 @@
{"kty":"EC","crv":"P-256","alg":"ECDSA","x":"4LqGkzIYWSgmyTS-B9Eet1xx1wpCKiSklMPnHfFp8eQ","y":"h69bjZOopVr_7OAKCh0rzDQG_9bj38Y5YoxCIytmzkA"}

View File

@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICgTCCAWmgAwIBAgIBATANBgkqhkiG9w0BAQsFADAAMCIYDzAwMDEwMTAxMDAw
MDAwWhgPMDAwMTAxMDEwMDAwMDBaMAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
ggEKAoIBAQC+epImi+GdM4ypmQ7LeWSYbbX0AHeZJvRScp5+JvkVQNTIDjQGnYxw
7omOW1dkn0qGkQckFmvUmCHXuK6oF0GYOvRzEdOwb6KeTb+ONYQHGLirKU2bt+um
JxiB/9PMaV5yPwpyNVi0XV5Rr+BpHdV1i9lm542+4zwfWiYRKT1+tjpvicmyK0av
T/60U0kfeeSdAU0TcSFR4RDEw1fudXIRk7FPgd2GHjeJeAeMmLL4Vabr+uSecGpp
THdkbnPDV51WVPHcyoOV6rdicSEoqE9aoeMjQXZ6SntXGjY4pqlyuwjqocLZStEK
ztxp3D7eyeHub9nrCgp+UsxaWns1DtP3AgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUA
A4IBAQA9sazSAm6umbleFWDrh3oyGaFBzYvRfeOAEquJky36qREjBWvrS2Yi66eX
L9Uoavr/CIk+U9qRPl81cHi5qsFBuDi+OKZzG32Uq7Rw8h+7f/9HVEUyVVy1p7v8
iqZvygU70NeT0cT91eSl6LV88BdjhbjI6Hk1+AVF6UPAmzkgJIFAwwUWa2HUT+Ni
nMxzRThuLyPbYt4clz6bGzk26LIdoByJH4pYabXh05OwalBJjMVR/4ek9blrVMAg
b4a7Eq/WXq+CVwWnb3oholDOJo3l/KwNuG6HD90JU0Vu4fipFqmsXhBHYVNVu94y
wJWm+dAtEeAcp8KfOv/IBMCjDkyt
-----END CERTIFICATE-----

View File

@ -0,0 +1 @@
{"kty":"RSA","alg":"RS256","n":"vnqSJovhnTOMqZkOy3lkmG219AB3mSb0UnKefib5FUDUyA40Bp2McO6JjltXZJ9KhpEHJBZr1Jgh17iuqBdBmDr0cxHTsG-ink2_jjWEBxi4qylNm7frpicYgf_TzGlecj8KcjVYtF1eUa_gaR3VdYvZZueNvuM8H1omESk9frY6b4nJsitGr0_-tFNJH3nknQFNE3EhUeEQxMNX7nVyEZOxT4Hdhh43iXgHjJiy-FWm6_rknnBqaUx3ZG5zw1edVlTx3MqDleq3YnEhKKhPWqHjI0F2ekp7Vxo2OKapcrsI6qHC2UrRCs7cadw-3snh7m_Z6woKflLMWlp7NQ7T9w","e":"AQAB"}

View File

@ -5,6 +5,7 @@
"ecdsaProfile": "ecdsaEE",
"debugAddr": ":8001",
"weakKeyFile": "test/example-weak-keys.json",
"blockedKeyFile": "test/example-blocked-keys.yaml",
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/ca.boulder/cert.pem",

View File

@ -5,6 +5,7 @@
"ecdsaProfile": "ecdsaEE",
"debugAddr": ":8001",
"weakKeyFile": "test/example-weak-keys.json",
"blockedKeyFile": "test/example-blocked-keys.yaml",
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/ca.boulder/cert.pem",

View File

@ -10,6 +10,7 @@
"authorizationLifetimeDays": 30,
"pendingAuthorizationLifetimeDays": 7,
"weakKeyFile": "test/example-weak-keys.json",
"blockedKeyFile": "test/example-blocked-keys.yaml",
"orderLifetime": "168h",
"issuerCertPath": "test/test-ca2.pem",
"tls": {

View File

@ -17,6 +17,7 @@
"debugAddr": ":8000",
"directoryCAAIdentity": "happy-hacker-ca.invalid",
"directoryWebsite": "https://github.com/letsencrypt/boulder",
"blockedKeyFile": "test/example-blocked-keys.yaml",
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",

View File

@ -18,6 +18,7 @@
"directoryCAAIdentity": "happy-hacker-ca.invalid",
"directoryWebsite": "https://github.com/letsencrypt/boulder",
"legacyKeyIDPrefix": "http://boulder:4000/reg/",
"blockedKeyFile": "test/example-blocked-keys.yaml",
"tls": {
"caCertFile": "test/grpc-creds/minica.pem",
"certFile": "test/grpc-creds/wfe.boulder/cert.pem",

View File

@ -0,0 +1,29 @@
#
# List of blocked keys
#
# Each blocked entry is a Base64 encoded SHA256 digest of a public key encoded
# in DER form as a PKIX public key.
#
# Use the test/block-a-key utility to generate new additions.
#
# NOTE: This list is loaded all-at-once in-memory by Boulder and is intended
# to be used infrequently. Alternative mechanisms should be explored if
# large scale blocks are required.
#
blocked:
# test/test-ca2.pem
- F4j7m0doxdWXdKOzeYjL6onsVYLLU2jb7xr994zlFFg=
# test/test-ca.pem
- F4j7m0doxdWXdKOzeYjL6onsVYLLU2jb7xr994zlFFg=
# test/test-example.pem
- 6E/Drp3Lzo85pYykpzx/tZpQZXeovto8/ezq1DBiSCc=
# test/test-root.pem
- Jy5HDlBtUvKkLtEsGbdp0o9LvVJx1lYG3R+n5G/KgIo=
# test/block-a-key/test/test.ecdsa.cert.pem
- cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=
# test/block-a-key/test/test.rsa.cert.pem
- Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=
# test/block-a-key/test/test.ecdsa.jwk.json
- cuwGhNNI6nfob5aqY90e7BleU6l7rfxku4X3UTJ3Z7M=
# test/block-a-key/test/test.rsa.jwk.json
- Qebc1V3SkX3izkYRGNJilm9Bcuvf0oox4U2Rn+b4JOE=

View File

@ -16,6 +16,7 @@ import OpenSSL
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import chisel2
from helpers import *
@ -1086,5 +1087,67 @@ def test_ocsp_exp_unauth():
tries += 1
time.sleep(0.25)
def test_blocked_key_account():
# Only config-next has a blocked keys file configured.
if not CONFIG_NEXT:
return
with open("test/test-ca.key", "rb") as key_file:
key = serialization.load_pem_private_key(key_file.read(), password=None, backend=default_backend())
# Create a client with the JWK set to a blocked private key
jwk = josepy.JWKRSA(key=key)
client = chisel2.uninitialized_client(jwk)
email = "test@not-example.com"
# Try to create an account
testPass = False
try:
client.new_account(messages.NewRegistration.from_data(email=email,
terms_of_service_agreed=True))
except acme_errors.Error as e:
if e.typ != "urn:ietf:params:acme:error:badPublicKey":
raise Exception("problem did not have correct error type, had {0}".format(e.typ))
if e.detail != "public key is forbidden":
raise Exception("problem did not have correct error detail, had {0}".format(e.detail))
testPass = True
if testPass is False:
raise Exception("expected account creation to fail with Error when using blocked key")
def test_blocked_key_cert():
# Only config-next has a blocked keys file configured.
if not CONFIG_NEXT:
return
with open("test/test-ca.key", "r") as f:
pemBytes = f.read()
domains = [random_domain(), random_domain()]
csr = acme_crypto_util.make_csr(pemBytes, domains, False)
client = chisel2.make_client(None)
order = client.new_order(csr)
authzs = order.authorizations
testPass = False
cleanup = chisel2.do_http_challenges(client, authzs)
try:
order = client.poll_and_finalize(order)
except acme_errors.Error as e:
# TODO(@cpu): this _should_ be type
# urn:ietf:params:acme:error:badPublicKey but this will require
# refactoring the `csr` package error handling.
# See https://github.com/letsencrypt/boulder/issues/4418
if e.typ != "urn:ietf:params:acme:error:malformed":
raise Exception("problem did not have correct error type, had {0}".format(e.typ))
if e.detail != "Error finalizing order :: invalid public key in CSR: public key is forbidden":
raise Exception("problem did not have correct error detail, had {0}".format(e.detail))
testPass = True
if testPass is False:
raise Exception("expected cert creation to fail with Error when using blocked key")
def run(cmd, **kwargs):
return subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, **kwargs)

19
web/jwk.go Normal file
View File

@ -0,0 +1,19 @@
package web
import (
"encoding/json"
"io/ioutil"
jose "gopkg.in/square/go-jose.v2"
)
// LoadJWK loads a JSON encoded JWK specified by filename or returns an error
func LoadJWK(filename string) (*jose.JSONWebKey, error) {
var jwk jose.JSONWebKey
if jsonBytes, err := ioutil.ReadFile(filename); err != nil {
return nil, err
} else if err = json.Unmarshal(jsonBytes, &jwk); err != nil {
return nil, err
}
return &jwk, nil
}

View File

@ -35,6 +35,8 @@ func problemDetailsForBoulderError(err *berrors.BoulderError, msg string) *probs
outProb = probs.ServerInternal("%s :: %s", msg, "Unable to meet CA SCT embedding requirements")
case berrors.OrderNotReady:
outProb = probs.OrderNotReady("%s :: %s", msg, err)
case berrors.BadPublicKey:
outProb = probs.BadPublicKey("%s :: %s", msg, err)
default:
// Internal server error messages may include sensitive data, so we do
// not include it.