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:
parent
a8586d05cd
commit
f02e9da38f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIH1MIGboAMCAQICAQEwCgYIKoZIzj0EAwIwADAiGA8wMDAxMDEwMTAwMDAwMFoY
|
||||
DzAwMDEwMTAxMDAwMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4LqG
|
||||
kzIYWSgmyTS+B9Eet1xx1wpCKiSklMPnHfFp8eSHr1uNk6ilWv/s4AoKHSvMNAb/
|
||||
1uPfxjlijEIjK2bOQKMCMAAwCgYIKoZIzj0EAwIDSQAwRgIhAJBK1/C1BYDnzSCu
|
||||
cR2pE40d8dyrRuHKj8htO/fzRgCgAiEA0UG0Vda8w0Tp84AMlJpZHOx9QUbwExSl
|
||||
oFEDADJ9WQM=
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kty":"EC","crv":"P-256","alg":"ECDSA","x":"4LqGkzIYWSgmyTS-B9Eet1xx1wpCKiSklMPnHfFp8eQ","y":"h69bjZOopVr_7OAKCh0rzDQG_9bj38Y5YoxCIytmzkA"}
|
||||
|
|
@ -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-----
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"kty":"RSA","alg":"RS256","n":"vnqSJovhnTOMqZkOy3lkmG219AB3mSb0UnKefib5FUDUyA40Bp2McO6JjltXZJ9KhpEHJBZr1Jgh17iuqBdBmDr0cxHTsG-ink2_jjWEBxi4qylNm7frpicYgf_TzGlecj8KcjVYtF1eUa_gaR3VdYvZZueNvuM8H1omESk9frY6b4nJsitGr0_-tFNJH3nknQFNE3EhUeEQxMNX7nVyEZOxT4Hdhh43iXgHjJiy-FWm6_rknnBqaUx3ZG5zw1edVlTx3MqDleq3YnEhKKhPWqHjI0F2ekp7Vxo2OKapcrsI6qHC2UrRCs7cadw-3snh7m_Z6woKflLMWlp7NQ7T9w","e":"AQAB"}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue