R53: enforce 2048-bit minimum RSA key size on verify

This commit is contained in:
2026-08-13 15:27:10 +01:00
parent da9c21d2a6
commit 08f474b551
2 changed files with 84 additions and 0 deletions
+7
View File
@@ -568,6 +568,13 @@ func verifySignatureWithKey(tokenString string, pubKey crypto.PublicKey, alg str
hashed := h.Sum(nil)
switch pubKey := pubKey.(type) {
case *rsa.PublicKey:
// Enforce a minimum RSA modulus size (NIST SP 800-57: ≥2048 bits).
// Without this, a token signed with a weak (<2048-bit) RSA key whose
// modulus the trusted JWKS publishes is accepted as valid. Modern
// providers all use ≥2048; rejecting smaller keys is safe hardening.
if pubKey.N.BitLen() < 2048 {
return fmt.Errorf("RSA key size %d bits is below the minimum 2048 bits", pubKey.N.BitLen())
}
if strings.HasPrefix(alg, "RS") {
return rsa.VerifyPKCS1v15(pubKey, hashFunc, hashed, signature)
} else if strings.HasPrefix(alg, "PS") {
+77
View File
@@ -1595,3 +1595,80 @@ func TestInvalidRedirectURI(t *testing.T) {
t.Errorf("Expected redirect to /legitimate-redirect, but got: %s", location)
}
}
// Review regression: RSA signature verification must reject tokens signed
// with keys below the NIST 2048-bit minimum modulus size.
func TestJWTRSAKeySizeMinimum(t *testing.T) {
tc := newTestCleanup(t)
logger := NewLogger("debug")
mkToidc := func(pub *rsa.PublicKey) *TraefikOidc {
jwk := JWK{
Kty: "RSA",
Kid: "test-key-id",
Alg: "RS256",
N: base64.RawURLEncoding.EncodeToString(pub.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}), // 65537
}
o := &TraefikOidc{
issuerURL: "https://test-issuer.com",
clientID: "test-client-id",
audience: "test-client-id",
clientSecret: "test-client-secret",
jwkCache: &MockJWKCache{JWKS: &JWKSet{Keys: []JWK{jwk}}, Err: nil},
jwksURL: "https://test-jwks-url.com",
tokenBlacklist: tc.addCache(NewCache()),
tokenCache: tc.addTokenCache(NewTokenCache()),
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
logger: logger,
excludedURLs: map[string]struct{}{},
httpClient: &http.Client{},
extractClaimsFunc: extractClaims,
}
o.jwtVerifier = o
o.tokenVerifier = o
return o
}
claims := func() map[string]interface{} {
return map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": "test-client-id",
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "test-subject",
"jti": "rsa-keysize-" + generateRandomString(8),
}
}
t.Run("weak 1024-bit RSA key rejected", func(t *testing.T) {
weak, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
t.Fatalf("failed to generate weak key: %v", err)
}
o := mkToidc(&weak.PublicKey)
tok, err := createTestJWT(weak, "RS256", "test-key-id", claims())
if err != nil {
t.Fatalf("failed to create JWT: %v", err)
}
err = o.VerifyToken(tok)
if err == nil {
t.Fatal("expected 1024-bit RSA token to be rejected, but verification succeeded")
}
})
t.Run("2048-bit RSA key accepted", func(t *testing.T) {
strong, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("failed to generate strong key: %v", err)
}
o := mkToidc(&strong.PublicKey)
tok, err := createTestJWT(strong, "RS256", "test-key-id", claims())
if err != nil {
t.Fatalf("failed to create JWT: %v", err)
}
if err = o.VerifyToken(tok); err != nil {
t.Fatalf("expected 2048-bit RSA token to verify, got: %v", err)
}
})
}