diff --git a/jwt.go b/jwt.go index 875e4da..4ad463d 100644 --- a/jwt.go +++ b/jwt.go @@ -322,6 +322,9 @@ func verifyTimeClaims(claims map[string]interface{}) error { 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 // public key, skipping the PEM-encode/decode round trip. This is the hot // path used by VerifyJWTSignatureAndClaims and verifyLogoutTokenSignature. @@ -369,7 +372,13 @@ func verifySignatureWithKey(tokenString string, pubKey crypto.PublicKey, alg str if strings.HasPrefix(alg, "ES") { var r, s big.Int 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") } r.SetBytes(signature[:sigLen/2]) diff --git a/jwt_ecdsa_strict_test.go b/jwt_ecdsa_strict_test.go new file mode 100644 index 0000000..aeb7a98 --- /dev/null +++ b/jwt_ecdsa_strict_test.go @@ -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") + } +}