Skip to content

Commit 0de25ba

Browse files
yaronfcursoragent
andcommitted
fix: treat EdDSA and Ed25519 as equivalent for JWK infer
Map OKP crv Ed25519 to RFC 9864 EdDSAEd25519, and accept legacy JWK alg "EdDSA" (and matching allowlist entries) as the same crypto. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a374445 commit 0de25ba

6 files changed

Lines changed: 83 additions & 9 deletions

File tree

crypto.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ func NewEd25519Verifier(key ed25519.PublicKey, config *VerifyConfig, fields Fiel
335335
// Preferred over NewJWSVerifierWithAlg when the key type uniquely determines the alg.
336336
//
337337
// Allowed key types:
338-
// - jwk.Key with alg, or EC/OKP with unambiguous crv (P-256→ES256, …, Ed25519→EdDSA)
338+
// - jwk.Key with alg, or EC/OKP with unambiguous crv (P-256→ES256, …, Ed25519→EdDSAEd25519; legacy JWK alg "EdDSA" also accepted)
339339
// - *ecdsa.PublicKey (curve → ES256/384/512)
340340
// - *mldsa.PublicKey (Parameters → ML-DSA-44/65/87)
341341
//

crypto_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,14 @@ func TestJWSAlgAllowlist(t *testing.T) {
618618
require.False(t, a.Contains(jwa.ES384()))
619619
var nilAllow *JWSAlgAllowlist
620620
require.False(t, nilAllow.Contains(jwa.ES256()))
621+
622+
edModern, err := NewJWSAlgAllowlist(jwa.EdDSAEd25519())
623+
require.NoError(t, err)
624+
require.True(t, edModern.Contains(jwa.EdDSAEd25519()))
625+
require.True(t, edModern.Contains(jwa.EdDSA()), "legacy EdDSA aliases RFC 9864 Ed25519")
626+
edLegacy, err := NewJWSAlgAllowlist(jwa.EdDSA())
627+
require.NoError(t, err)
628+
require.True(t, edLegacy.Contains(jwa.EdDSAEd25519()))
621629
}
622630

623631
func TestNewJWSSignerSignAlg(t *testing.T) {

internal-docs/JWS-ALG-POLICY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,13 @@ config.SetFetchVerifier(func(r *http.Request) (string, *httpsign.Verifier) {
135135
| `key` type | How alg is chosen |
136136
| ---------- | ----------------- |
137137
| `jwk.Key` with `alg` | Use JWK `alg` |
138-
| `jwk.Key` EC/OKP without `alg` | `crv` → ES256/384/512 or EdDSA |
138+
| `jwk.Key` EC/OKP without `alg` | `crv` → ES256/384/512 or **EdDSAEd25519** (RFC 9864); legacy JWK `alg` `"EdDSA"` agrees with that mapping |
139139
| `jwk.Key` RSA/`oct`/AKP without `alg` | Error (RSA/HMAC ambiguous; AKP requires `alg` per RFC 9964) |
140140
| `*ecdsa.PublicKey` | Curve → ES256/384/512 |
141141
| `*mldsa.PublicKey` | `Parameters()` → ML-DSA-44/65/87 |
142142
| `*rsa.PublicKey`, `[]byte`, … | Error — `NewJWSVerifierWithAlg(allowed, alg, …)` or a JWK with `alg` |
143143

144-
If JWK has both `alg` and a structural mapping and they **disagree** → error.
144+
If JWK has both `alg` and a structural mapping and they **disagree** → error. Legacy `"EdDSA"` and RFC 9864 `"Ed25519"` are **not** a disagreement (same Ed25519 crypto). `JWSAlgAllowlist.Contains` treats those two names as equivalent.
145145

146146
After JWK → raw key for ML-DSA: cross-check `Parameters()` vs claimed `alg`.
147147

jwsallow.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,20 @@ func NewJWSAlgAllowlist(algs ...jwa.SignatureAlgorithm) (*JWSAlgAllowlist, error
5252
// Contains reports whether alg is permitted. A nil receiver does not contain any alg
5353
// (callers should treat nil allowlist as “skip policy” before calling Contains).
5454
// Comparison is by algorithm name string; constructors resolve to registry values first.
55+
// Legacy "EdDSA" and RFC 9864 "Ed25519" are treated as equivalent.
5556
func (a *JWSAlgAllowlist) Contains(alg jwa.SignatureAlgorithm) bool {
5657
if a == nil {
5758
return false
5859
}
59-
_, ok := a.algs[alg.String()]
60-
return ok
60+
if _, ok := a.algs[alg.String()]; ok {
61+
return true
62+
}
63+
if !isEd25519JWSAlg(alg) {
64+
return false
65+
}
66+
_, legacy := a.algs[jwa.EdDSA().String()]
67+
_, modern := a.algs[jwa.EdDSAEd25519().String()]
68+
return legacy || modern
6169
}
6270

6371
func checkJWSAlgAllowed(allowed *JWSAlgAllowlist, alg jwa.SignatureAlgorithm) error {

jwsinfer.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,12 @@ func inferFromJWK(key jwk.Key) (jwa.SignatureAlgorithm, any, error) {
6666
var alg jwa.SignatureAlgorithm
6767
switch {
6868
case hasAlg && hasStruct:
69-
if fromAlg.String() != fromStruct.String() {
69+
if !jwsSignatureAlgsAgree(fromAlg, fromStruct) {
7070
return jwa.EmptySignatureAlgorithm(), nil, fmt.Errorf(
7171
"JWK alg %s disagrees with structural mapping %s", fromAlg, fromStruct,
7272
)
7373
}
74+
// Prefer the JWK's stated alg; EdDSA ↔ Ed25519 are treated as agreeing (RFC 9864).
7475
alg = fromAlg
7576
case hasAlg:
7677
alg = fromAlg
@@ -144,12 +145,31 @@ func algFromJWKCurve(crv jwa.EllipticCurveAlgorithm) (jwa.SignatureAlgorithm, er
144145
case jwa.P521():
145146
return jwa.ES512(), nil
146147
case jwa.Ed25519():
147-
return jwa.EdDSA(), nil
148+
// RFC 9864 name; legacy JWK alg "EdDSA" still agrees via jwsSignatureAlgsAgree.
149+
return jwa.EdDSAEd25519(), nil
148150
default:
149151
return jwa.EmptySignatureAlgorithm(), fmt.Errorf("cannot infer JWS algorithm from crv %s", crv)
150152
}
151153
}
152154

155+
// jwsSignatureAlgsAgree reports whether two registry algs are the same crypto choice.
156+
// Legacy "EdDSA" and RFC 9864 "Ed25519" both mean Ed25519 signatures in jwx v4.
157+
func jwsSignatureAlgsAgree(a, b jwa.SignatureAlgorithm) bool {
158+
if a.String() == b.String() {
159+
return true
160+
}
161+
return isEd25519JWSAlg(a) && isEd25519JWSAlg(b)
162+
}
163+
164+
func isEd25519JWSAlg(alg jwa.SignatureAlgorithm) bool {
165+
switch alg {
166+
case jwa.EdDSA(), jwa.EdDSAEd25519():
167+
return true
168+
default:
169+
return false
170+
}
171+
}
172+
153173
func algFromECDSACurve(curve elliptic.Curve) (jwa.SignatureAlgorithm, error) {
154174
if curve == nil {
155175
return jwa.EmptySignatureAlgorithm(), fmt.Errorf("ECDSA key has nil curve")

jwsinfer_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,44 @@ func TestNewJWSVerifierFromJWK(t *testing.T) {
139139
require.NotNil(t, v)
140140
})
141141

142+
t.Run("ed25519 with RFC 9864 alg", func(t *testing.T) {
143+
edJWK, err := jwk.Import[jwk.Key](edPub)
144+
require.NoError(t, err)
145+
require.NoError(t, edJWK.Set(jwk.AlgorithmKey, jwa.EdDSAEd25519()))
146+
allowed, err := NewJWSAlgAllowlist(jwa.EdDSAEd25519())
147+
require.NoError(t, err)
148+
v, err := NewJWSVerifier(allowed, edJWK, nil, *NewFields())
149+
require.NoError(t, err)
150+
require.NotNil(t, v)
151+
})
152+
153+
t.Run("ed25519 crv with modern allowlist", func(t *testing.T) {
154+
edJWK, err := jwk.Import[jwk.Key](edPub)
155+
require.NoError(t, err)
156+
allowed, err := NewJWSAlgAllowlist(jwa.EdDSAEd25519())
157+
require.NoError(t, err)
158+
v, err := NewJWSVerifier(allowed, edJWK, nil, *NewFields())
159+
require.NoError(t, err)
160+
require.NotNil(t, v)
161+
})
162+
163+
t.Run("ed25519 legacy alg with modern allowlist", func(t *testing.T) {
164+
edJWK, err := jwk.Import[jwk.Key](edPub)
165+
require.NoError(t, err)
166+
require.NoError(t, edJWK.Set(jwk.AlgorithmKey, jwa.EdDSA()))
167+
allowed, err := NewJWSAlgAllowlist(jwa.EdDSAEd25519())
168+
require.NoError(t, err)
169+
v, err := NewJWSVerifier(allowed, edJWK, nil, *NewFields())
170+
require.NoError(t, err)
171+
require.NotNil(t, v)
172+
})
173+
142174
t.Run("ed25519 private jwk exports public", func(t *testing.T) {
143175
edJWK, err := jwk.Import[jwk.Key](edPriv)
144176
require.NoError(t, err)
145177
alg, raw, err := inferFromJWK(edJWK)
146178
require.NoError(t, err)
147-
require.Equal(t, jwa.EdDSA().String(), alg.String())
179+
require.Equal(t, jwa.EdDSAEd25519().String(), alg.String())
148180
got, ok := raw.(ed25519.PublicKey)
149181
require.True(t, ok)
150182
require.Equal(t, edPub, got)
@@ -235,7 +267,7 @@ func TestAlgFromCurveHelpers(t *testing.T) {
235267
{jwa.P256(), jwa.ES256()},
236268
{jwa.P384(), jwa.ES384()},
237269
{jwa.P521(), jwa.ES512()},
238-
{jwa.Ed25519(), jwa.EdDSA()},
270+
{jwa.Ed25519(), jwa.EdDSAEd25519()},
239271
}
240272
for _, tc := range cases {
241273
got, err := algFromJWKCurve(tc.crv)
@@ -246,6 +278,12 @@ func TestAlgFromCurveHelpers(t *testing.T) {
246278
require.Error(t, err)
247279
})
248280

281+
t.Run("ed25519 alg aliases agree", func(t *testing.T) {
282+
require.True(t, jwsSignatureAlgsAgree(jwa.EdDSA(), jwa.EdDSAEd25519()))
283+
require.True(t, jwsSignatureAlgsAgree(jwa.EdDSAEd25519(), jwa.EdDSA()))
284+
require.False(t, jwsSignatureAlgsAgree(jwa.EdDSA(), jwa.ES256()))
285+
})
286+
249287
t.Run("ecdsa nil and unsupported", func(t *testing.T) {
250288
_, err := algFromECDSACurve(nil)
251289
require.Error(t, err)

0 commit comments

Comments
 (0)