fix(jwt): bind ECDSA curve to the algorithm and require fixed signature length

ES256, ES384 and ES512 verified against any curve and any even-length
signature. RFC 7518 fixes the curve per algorithm and the signature to
R||S at the curve's octet length. Verification now enforces both.
This commit is contained in:
2026-11-03 01:21:01 +00:00
parent 2f0ee00e51
commit 92c311fde1
2 changed files with 63 additions and 1 deletions
+10 -1
View File
@@ -322,6 +322,9 @@ func verifyTimeClaims(claims map[string]interface{}) error {
return nil return nil
} }
// ecdsaAlgBits maps an ES* algorithm to its curve size (RFC 7518 3.4).
var ecdsaAlgBits = map[string]int{"ES256": 256, "ES384": 384, "ES512": 521}
// verifySignatureWithKey verifies a JWT signature using an already-parsed // verifySignatureWithKey verifies a JWT signature using an already-parsed
// public key, skipping the PEM-encode/decode round trip. This is the hot // public key, skipping the PEM-encode/decode round trip. This is the hot
// path used by VerifyJWTSignatureAndClaims and verifyLogoutTokenSignature. // path used by VerifyJWTSignatureAndClaims and verifyLogoutTokenSignature.
@@ -369,7 +372,13 @@ func verifySignatureWithKey(tokenString string, pubKey crypto.PublicKey, alg str
if strings.HasPrefix(alg, "ES") { if strings.HasPrefix(alg, "ES") {
var r, s big.Int var r, s big.Int
sigLen := len(signature) sigLen := len(signature)
if sigLen%2 != 0 { // RFC 7518 3.4: the curve is fixed by the algorithm and the
// signature is R||S at exactly the curve's octet length.
bits := ecdsaAlgBits[alg]
if bits == 0 || pubKey.Curve.Params().BitSize != bits {
return fmt.Errorf("ECDSA key curve does not match algorithm %s", alg)
}
if sigLen != 2*((bits+7)/8) {
return fmt.Errorf("invalid ECDSA signature length") return fmt.Errorf("invalid ECDSA signature length")
} }
r.SetBytes(signature[:sigLen/2]) r.SetBytes(signature[:sigLen/2])
+53
View File
@@ -0,0 +1,53 @@
package traefikoidc
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"testing"
)
func signES(t *testing.T, key *ecdsa.PrivateKey, signed string, h256 bool, size int) (sigRS []byte) {
t.Helper()
var digest []byte
if h256 {
d := sha256.Sum256([]byte(signed))
digest = d[:]
} else {
d := sha512.Sum384([]byte(signed))
digest = d[:]
}
r, s, err := ecdsa.Sign(rand.Reader, key, digest)
if err != nil {
t.Fatal(err)
}
out := make([]byte, 2*size)
r.FillBytes(out[:size])
s.FillBytes(out[size:])
return out
}
func TestECDSAVerifyIsStrict(t *testing.T) {
const signed = "eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ1In0"
enc := base64.RawURLEncoding.EncodeToString
p256, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
p384, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err := verifySignatureWithKey(signed+"."+enc(signES(t, p256, signed, true, 32)), &p256.PublicKey, "ES256"); err != nil {
t.Fatalf("valid ES256 rejected: %v", err)
}
if err := verifySignatureWithKey(signed+"."+enc(signES(t, p384, signed, false, 48)), &p384.PublicKey, "ES384"); err != nil {
t.Fatalf("valid ES384 rejected: %v", err)
}
// ES256 token verified against a P-384 key.
if err := verifySignatureWithKey(signed+"."+enc(signES(t, p384, signed, true, 48)), &p384.PublicKey, "ES256"); err == nil {
t.Error("ES256 accepted with a P-384 key")
}
// Zero-padded halves: 66 bytes for ES256.
if err := verifySignatureWithKey(signed+"."+enc(signES(t, p256, signed, true, 33)), &p256.PublicKey, "ES256"); err == nil {
t.Error("ES256 accepted a 66-byte signature")
}
}