Skip to content

Commit

Permalink
Restore and simplify Fermat integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
aarongable committed Feb 14, 2025
1 parent 709ae64 commit ab8d918
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 7 deletions.
14 changes: 7 additions & 7 deletions goodkey/good_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,13 @@ func TestCheckPrimeFactorsTooClose(t *testing.T) {
expectRounds: 1,
},
{
// FIPS requires that |p-q| > 2^(nlen/2 - 100), i.e. that the absolute
// value of the difference between the prime factors of a 2048-bit RSA key
// be at least 2^924. These two factors have a difference of exactly 2^924
// + 4, just *barely* FIPS-compliant. Their first different digit is in
// column 52 of this file, which still makes them vastly further apart
// than the cases above. Their product cannot be factored even with one
// hundred million rounds of Fermat's Algorithm.
// FIPS requires that |p-q| > 2^(nlen/2 - 100). For example, a 2048-bit
// RSA key must have prime factors with a difference of at least 2^924.
// These two factors have a difference of exactly 2^924 + 4, just *barely*
// FIPS-compliant. Their first different digit is in column 52 of this
// file, which makes them vastly further apart than the cases above. Their
// product cannot be factored even with 100,000,000 rounds of Fermat's
// Algorithm.
name: "barely FIPS compliant (2048 bit)",
p: "151546560166767007654995655231369126386504564489055366370313539237722892921762327477057109592614214965864835328962951695621854530739049166771701397343693962526456985866167580660948398404000483264137738772983130282095332559392185543017295488346592188097443414824871619976114874896240350402349774470198190454623",
q: "151546560166767007654995655231510939369872272987323309037144546294925352276321214430320942815891873491060949332482502812040326472743233767963240491605860423063942576391584034077877871768428333113881339606298282107984376151546711223157061364850161576363709081794948857957944390170575452970542651659150041855843",
Expand Down
66 changes: 66 additions & 0 deletions test/integration/bad_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//go:build integration

package integration

import (
"crypto/x509"
"encoding/pem"
"os"
"testing"

"github.com/eggsampler/acme/v3"

"github.com/letsencrypt/boulder/test"
)

// TestFermat ensures that a certificate public key which can be factored using
// less than 100 rounds of Fermat's Algorithm is rejected.
func TestFermat(t *testing.T) {
t.Parallel()

// Create a client and complete an HTTP-01 challenge for a fake domain.
c, err := makeClient()
test.AssertNotError(t, err, "creating acme client")

domain := random_domain()

order, err := c.Client.NewOrder(
c.Account, []acme.Identifier{{Type: "dns", Value: domain}})
test.AssertNotError(t, err, "creating new order")
test.AssertEquals(t, len(order.Authorizations), 1)

authUrl := order.Authorizations[0]

auth, err := c.Client.FetchAuthorization(c.Account, authUrl)
test.AssertNotError(t, err, "fetching authorization")

chal, ok := auth.ChallengeMap[acme.ChallengeTypeHTTP01]
test.Assert(t, ok, "getting HTTP-01 challenge")

err = addHTTP01Response(chal.Token, chal.KeyAuthorization)
defer delHTTP01Response(chal.Token)
test.AssertNotError(t, err, "adding HTTP-01 response")

chal, err = c.Client.UpdateChallenge(c.Account, chal)
test.AssertNotError(t, err, "updating HTTP-01 challenge")

// Load the Fermat-weak CSR that we'll submit for finalize. This CSR was
// generated using test/integration/testdata/fermat_csr.go, has prime factors
// that differ by only 2^516 + 254, and can be factored in 42 rounds.
csrPem, err := os.ReadFile("test/integration/testdata/fermat_csr.pem")
test.AssertNotError(t, err, "reading CSR PEM from disk")

csrDer, _ := pem.Decode(csrPem)
if csrDer == nil {
t.Fatal("failed to decode CSR PEM")
}

csr, err := x509.ParseCertificateRequest(csrDer.Bytes)
test.AssertNotError(t, err, "parsing CSR")

// Finalizing the order should fail as we reject the public key.
_, err = c.Client.FinalizeOrder(c.Account, order, csr)
test.AssertError(t, err, "finalizing order")
test.AssertContains(t, err.Error(), "urn:ietf:params:acme:error:badCSR")
test.AssertContains(t, err.Error(), "key generated with factors too close together")
}
99 changes: 99 additions & 0 deletions test/integration/testdata/fermat_csr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"os"
)

const (
// bits is the size of the resulting RSA key, also known as "nlen" or "Length
// of the modulus N". Usually 1024, 2048, or 4096.
bits = 2048
// gap is the exponent of the different between the prime factors of the RSA
// key, i.e. |p-q| ~= 2^gap. For FIPS compliance, set this to (bits/2 - 100).
gap = 516
)

func main() {
// Generate q, which will be the smaller of the two factors. We set its length
// so that the product of two similarly-sized factors will be the desired
// bit length.
q, err := rand.Prime(rand.Reader, (bits+1)/2)
if err != nil {
log.Fatalln(err)
}

// Our starting point for p is q + 2^gap.
p := new(big.Int).Add(q, new(big.Int).Exp(big.NewInt(2), big.NewInt(gap), nil))

// Now we just keep incrementing P until we find a prime. You might think
// this would take a while, but it won't: there are a lot of primes.
attempts := 0
for {
// Using 34 rounds of Miller-Rabin primality testing is enough for the go
// stdlib, so it's enough for us.
if p.ProbablyPrime(34) {
break
}

// We know P is odd because it started as a prime (odd) plus a power of two
// (even), so we can increment by 2 to remain odd.
p.Add(p, big.NewInt(2))
attempts++
}

fmt.Println("p:", p.String())
fmt.Println("q:", q.String())
fmt.Println("Differ by", fmt.Sprintf("2^%d + %d", gap, 2*attempts))

// Construct the public modulus N from the prime factors.
n := new(big.Int).Mul(p, q)

// Construct the public key from the modulus and (fixed) public exponent.
pubkey := rsa.PublicKey{
N: n,
E: 65537,
}

// Construct the private exponent D from the prime factors.
p_1 := new(big.Int).Sub(p, big.NewInt(1))
q_1 := new(big.Int).Sub(q, big.NewInt(1))
field := new(big.Int).Mul(p_1, q_1)
d := new(big.Int).ModInverse(big.NewInt(65537), field)

// Construct the private key from the factors and private exponent.
privkey := rsa.PrivateKey{
PublicKey: pubkey,
D: d,
Primes: []*big.Int{p, q},
}
privkey.Precompute()

// Sign a CSR using this key, so we can use it in integration tests.
// Note that this step *only works on go1.23 and earlier*. Later versions of
// go detect that the prime factors are too close together and refuse to
// produce a signature.
csrDER, err := x509.CreateCertificateRequest(
rand.Reader,
&x509.CertificateRequest{
Subject: pkix.Name{CommonName: "example.com"},
PublicKey: &pubkey,
},
&privkey)
if err != nil {
log.Fatalln(err)
}

csrPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrDER,
})
fmt.Fprint(os.Stdout, string(csrPEM))
}
15 changes: 15 additions & 0 deletions test/integration/testdata/fermat_csr.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICWzCCAUMCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCVIY5cKFJU+qqXCtls7VA+oAwcDnsIk3W8+4ZO
y5vKEk3Ye9rWglKPqHSDvr4UdEv5cP6RaByWaL7PUswIPwQD8HFywR84V82+3pl+
sEVo88M3HK1ZwI19FcmsaZn3Zh0gVymEYi4VJof2toYUK8M2DRjJGvVrnpG2P6y0
VKpq7jBTR6G8PXr4q2JjGJaBci1Bzw2sWMUcyfOdIpdKpe185e7WSl9N0YT4pg7t
lHMoGHWYPQ6Pd7TR6EmGzKs+MThsWhREx91ViA9UmYe4n607lGevm2nHV2PJ09PR
tn+136BIE30E4uVgPVuHp5y36PKylfA5NHA9M0TMgpn0AK0/AgMBAAGgADANBgkq
hkiG9w0BAQsFAAOCAQEAk3xNRIahAtVzlygRwh57gRBqEi6uJXh651rNSSdvk1YA
MR4bhkA9IXSwrOlb8euRWGdRMnxSqx+16OqZ0MDGrTMg3RaQoSkmFo28zbMNtHgd
4243lzDF0KrZCSyQHh9bSmcMuPjbCRPZJObg70ALw1K2pdrUamTh7EjKWPbGA3hg
lrfl9RsMzC/6UDUoMUyCHRJx6pT6t6PwDl8g+tesQemnVxKNEY8WZOyf/1uEEhNb
1PmpgfnV+NQp3sOXSLsxlDpl0zRlbWq6QGnvW2O6FalxoVSZ3WIXX/FyT2rxePWg
LDaCwR0qj4byFL2On7FsbU4Wfx6bD70cplaxfv8uQQ==
-----END CERTIFICATE REQUEST-----

0 comments on commit ab8d918

Please sign in to comment.