goodkey: default to 110 rounds of Fermat factorization (#7579)

This change guarantees compliance with CA/BF Ballot SC-073 "Compromised
and Weak Keys", which requires that at least 100 rounds of Fermat
Factorization be attempted:

> Section 6.1.1.3 Subscriber Key Pair Generation
> The CA SHALL reject a certificate request if... The Public Key
corresponds to an industry-demonstrated weak Private Key. For requests
submitted on or after November 15, 2024,... In the case of Close Primes
vulnerability (https://fermatattack.secvuln.info/), the CA SHALL reject
weak keys which can be factored within 100 rounds using Fermat’s
factorization method.

We choose 110 rounds to ensure a margin above and beyond the requirements.

Fixes https://github.com/letsencrypt/boulder/issues/7558
This commit is contained in:
Aaron Gable 2024-07-17 16:05:30 -07:00 committed by GitHub
parent 2c157251ac
commit a3e99432bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 18 additions and 20 deletions

View File

@ -34,7 +34,7 @@ var kp goodkey.KeyPolicy
func init() { func init() {
var err error var err error
kp, err = goodkey.NewPolicy(&goodkey.Config{FermatRounds: 100}, nil) kp, err = goodkey.NewPolicy(nil, nil)
if err != nil { if err != nil {
log.Fatal("Could not create goodkey.KeyPolicy") log.Fatal("Could not create goodkey.KeyPolicy")
} }

View File

@ -59,7 +59,7 @@ func init() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
kp, err = sagoodkey.NewPolicy(&goodkey.Config{FermatRounds: 100}, nil) kp, err = sagoodkey.NewPolicy(nil, nil)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -53,7 +53,7 @@ type Config struct {
// FermatRounds is an integer number of rounds of Fermat's factorization // FermatRounds is an integer number of rounds of Fermat's factorization
// method that should be performed to attempt to detect keys whose modulus can // method that should be performed to attempt to detect keys whose modulus can
// be trivially factored because the two factors are very close to each other. // be trivially factored because the two factors are very close to each other.
// If this config value is empty (0), no factorization will be attempted. // If this config value is empty or 0, it will default to 110 rounds.
FermatRounds int FermatRounds int
} }
@ -122,7 +122,7 @@ type KeyPolicy struct {
// defaults. If the config's AllowedKeys is nil, the LetsEncryptCPS AllowedKeys // defaults. If the config's AllowedKeys is nil, the LetsEncryptCPS AllowedKeys
// is used. If the config's WeakKeyFile or BlockedKeyFile paths are empty, those // is used. If the config's WeakKeyFile or BlockedKeyFile paths are empty, those
// checks are disabled. If the config's FermatRounds is 0, Fermat Factorization // checks are disabled. If the config's FermatRounds is 0, Fermat Factorization
// is disabled. // defaults to attempting 110 rounds.
func NewPolicy(config *Config, bkc BlockedKeyCheckFunc) (KeyPolicy, error) { func NewPolicy(config *Config, bkc BlockedKeyCheckFunc) (KeyPolicy, error) {
if config == nil { if config == nil {
config = &Config{} config = &Config{}
@ -149,10 +149,14 @@ func NewPolicy(config *Config, bkc BlockedKeyCheckFunc) (KeyPolicy, error) {
} }
kp.blockedList = blocked kp.blockedList = blocked
} }
if config.FermatRounds < 0 { if config.FermatRounds == 0 {
return KeyPolicy{}, fmt.Errorf("Fermat factorization rounds cannot be negative: %d", config.FermatRounds) // The BRs require 100 rounds, so give ourselves a margin above that.
kp.fermatRounds = 110
} else if config.FermatRounds < 100 {
return KeyPolicy{}, fmt.Errorf("Fermat factorization rounds must be at least 100: %d", config.FermatRounds)
} else {
kp.fermatRounds = config.FermatRounds
} }
kp.fermatRounds = config.FermatRounds
return kp, nil return kp, nil
} }
@ -354,12 +358,11 @@ func (policy *KeyPolicy) goodKeyRSA(key *rsa.PublicKey) error {
if rocacheck.IsWeak(key) { if rocacheck.IsWeak(key) {
return badKey("key generated by vulnerable Infineon-based hardware") return badKey("key generated by vulnerable Infineon-based hardware")
} }
// Check if the key can be easily factored via Fermat's factorization method. // Check if the key can be easily factored via Fermat's factorization method.
if policy.fermatRounds > 0 { err = checkPrimeFactorsTooClose(modulus, policy.fermatRounds)
err := checkPrimeFactorsTooClose(modulus, policy.fermatRounds) if err != nil {
if err != nil { return badKey("key generated with factors too close together: %w", err)
return badKey("key generated with factors too close together: %w", err)
}
} }
return nil return nil

View File

@ -300,7 +300,7 @@ func TestDefaultAllowedKeys(t *testing.T) {
test.Assert(t, policy.allowedKeys.ECDSAP384, "NIST P384 should be allowed") test.Assert(t, policy.allowedKeys.ECDSAP384, "NIST P384 should be allowed")
test.Assert(t, !policy.allowedKeys.ECDSAP521, "NIST P521 should not be allowed") test.Assert(t, !policy.allowedKeys.ECDSAP521, "NIST P521 should not be allowed")
policy, err = NewPolicy(&Config{FermatRounds: 100}, nil) policy, err = NewPolicy(&Config{}, nil)
test.AssertNotError(t, err, "NewPolicy with nil config.AllowedKeys failed") test.AssertNotError(t, err, "NewPolicy with nil config.AllowedKeys failed")
test.Assert(t, policy.allowedKeys.RSA2048, "RSA 2048 should be allowed") test.Assert(t, policy.allowedKeys.RSA2048, "RSA 2048 should be allowed")
test.Assert(t, policy.allowedKeys.RSA3072, "RSA 3072 should be allowed") test.Assert(t, policy.allowedKeys.RSA3072, "RSA 3072 should be allowed")

View File

@ -143,8 +143,7 @@
"lifespanOCSP": "96h", "lifespanOCSP": "96h",
"goodkey": { "goodkey": {
"weakKeyFile": "test/example-weak-keys.json", "weakKeyFile": "test/example-weak-keys.json",
"blockedKeyFile": "test/example-blocked-keys.yaml", "blockedKeyFile": "test/example-blocked-keys.yaml"
"fermatRounds": 100
}, },
"ocspLogMaxLength": 4000, "ocspLogMaxLength": 4000,
"ocspLogPeriod": "500ms", "ocspLogPeriod": "500ms",

View File

@ -5,9 +5,6 @@
"maxOpenConns": 10 "maxOpenConns": 10
}, },
"hostnamePolicyFile": "test/hostname-policy.yaml", "hostnamePolicyFile": "test/hostname-policy.yaml",
"goodkey": {
"fermatRounds": 100
},
"workers": 16, "workers": 16,
"unexpiredOnly": true, "unexpiredOnly": true,
"badResultsOnly": true, "badResultsOnly": true,

View File

@ -8,8 +8,7 @@
"pendingAuthorizationLifetimeDays": 7, "pendingAuthorizationLifetimeDays": 7,
"goodkey": { "goodkey": {
"weakKeyFile": "test/example-weak-keys.json", "weakKeyFile": "test/example-weak-keys.json",
"blockedKeyFile": "test/example-blocked-keys.yaml", "blockedKeyFile": "test/example-blocked-keys.yaml"
"fermatRounds": 100
}, },
"orderLifetime": "168h", "orderLifetime": "168h",
"finalizeTimeout": "30s", "finalizeTimeout": "30s",