Skip to content

Commit 5e9e629

Browse files
committed
feat: support ML-DSA JWS Algorithm Identifiers
1 parent b7bd5fe commit 5e9e629

File tree

9 files changed

+102
-68
lines changed

9 files changed

+102
-68
lines changed

docs/interfaces/JWK.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ Support from the community to continue maintaining and improving this module is
5454

5555
***
5656

57+
### pub?
58+
59+
`readonly` `optional` **pub**: `string`
60+
61+
***
62+
5763
### use?
5864

5965
`readonly` `optional` **use**: `string`

docs/type-aliases/JWSAlgorithm.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Support from the community to continue maintaining and improving this module is
66

77
***
88

9-
**JWSAlgorithm**: `"PS256"` \| `"ES256"` \| `"RS256"` \| `"Ed25519"` \| `"ES384"` \| `"PS384"` \| `"RS384"` \| `"ES512"` \| `"PS512"` \| `"RS512"` \| `"EdDSA"`
9+
**JWSAlgorithm**: `"PS256"` \| `"ES256"` \| `"RS256"` \| `"Ed25519"` \| `"ES384"` \| `"PS384"` \| `"RS384"` \| `"ES512"` \| `"PS512"` \| `"RS512"` \| `"ML-DSA-44"` \| `"ML-DSA-65"` \| `"ML-DSA-87"` \| `"EdDSA"`
1010

1111
JWS `alg` Algorithm identifiers from the
1212
[JSON Web Signature and Encryption Algorithms IANA registry](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms)

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
"esbuild": "^0.25.8",
8686
"happy-dom": "^18.0.1",
8787
"http-cookie-agent": "^7.0.2",
88-
"jose": "^6.0.12",
88+
"jose": "^6.1.0",
8989
"oidc-provider": "^9.4.0",
9090
"patch-package": "^8.0.0",
9191
"prettier": "^3.6.2",

src/index.ts

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ export type JWSAlgorithm =
112112
| 'ES512'
113113
| 'PS512'
114114
| 'RS512'
115+
| 'ML-DSA-44'
116+
| 'ML-DSA-65'
117+
| 'ML-DSA-87'
115118
// Deprecated
116119
| 'EdDSA'
117120

@@ -126,6 +129,7 @@ export interface JWK {
126129
readonly crv?: string
127130
readonly x?: string
128131
readonly y?: string
132+
readonly pub?: string
129133

130134
readonly [parameter: string]: JsonValue | undefined
131135
}
@@ -1043,6 +1047,44 @@ function OPE(message: string, code?: string, cause?: unknown) {
10431047
return new OperationProcessingError(message, { code, cause })
10441048
}
10451049

1050+
async function calculateJwkThumbprint(jwk: JWK): Promise<string> {
1051+
let components: JsonObject
1052+
switch (jwk.kty) {
1053+
case 'EC':
1054+
components = {
1055+
crv: jwk.crv,
1056+
kty: jwk.kty,
1057+
x: jwk.x,
1058+
y: jwk.y,
1059+
}
1060+
break
1061+
case 'OKP':
1062+
components = {
1063+
crv: jwk.crv,
1064+
kty: jwk.kty,
1065+
x: jwk.x,
1066+
}
1067+
break
1068+
case 'AKP':
1069+
components = {
1070+
alg: jwk.alg,
1071+
kty: jwk.kty,
1072+
pub: jwk.pub,
1073+
}
1074+
break
1075+
case 'RSA':
1076+
components = {
1077+
e: jwk.e,
1078+
kty: jwk.kty,
1079+
n: jwk.n,
1080+
}
1081+
break
1082+
default:
1083+
throw new UnsupportedOperationError('unsupported JWK key type', { cause: jwk })
1084+
}
1085+
return b64u(await crypto.subtle.digest('SHA-256', buf(JSON.stringify(components))))
1086+
}
1087+
10461088
function assertCryptoKey(key: unknown, it: string): asserts key is CryptoKey {
10471089
if (!(key instanceof CryptoKey)) {
10481090
throw CodedTypeError(`${it} must be a CryptoKey`, ERR_INVALID_ARG_TYPE)
@@ -1591,7 +1633,11 @@ function keyToJws(key: CryptoKey) {
15911633
return rsAlg(key)
15921634
case 'ECDSA':
15931635
return esAlg(key)
1594-
case 'Ed25519': // Fall through
1636+
case 'Ed25519':
1637+
case 'ML-DSA-44':
1638+
case 'ML-DSA-65':
1639+
case 'ML-DSA-87':
1640+
return key.algorithm.name
15951641
case 'EdDSA':
15961642
return 'Ed25519'
15971643
default:
@@ -2244,24 +2290,7 @@ class DPoPHandler implements DPoPHandle {
22442290
async calculateThumbprint() {
22452291
if (!this.#jkt) {
22462292
const jwk = await crypto.subtle.exportKey('jwk', this.#publicKey)
2247-
let components: JsonValue
2248-
switch (jwk.kty) {
2249-
case 'EC':
2250-
components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }
2251-
break
2252-
case 'OKP':
2253-
components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }
2254-
break
2255-
case 'RSA':
2256-
components = { e: jwk.e, kty: jwk.kty, n: jwk.n }
2257-
break
2258-
default:
2259-
throw new UnsupportedOperationError('unsupported JWK', { cause: { jwk } })
2260-
}
2261-
2262-
this.#jkt ||= b64u(
2263-
await crypto.subtle.digest({ name: 'SHA-256' }, buf(JSON.stringify(components))),
2264-
)
2293+
this.#jkt ||= await calculateJwkThumbprint(jwk as JWK)
22652294
}
22662295

22672296
return this.#jkt
@@ -3047,6 +3076,9 @@ async function getPublicSigKeyFromIssuerJwksUri(
30473076
case 'Ed':
30483077
kty = 'OKP'
30493078
break
3079+
case 'ML':
3080+
kty = 'AKP'
3081+
break
30503082
default:
30513083
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg } })
30523084
}
@@ -4713,6 +4745,9 @@ function supported(alg: string) {
47134745
case 'RS512':
47144746
case 'Ed25519':
47154747
case 'EdDSA':
4748+
case 'ML-DSA-44':
4749+
case 'ML-DSA-65':
4750+
case 'ML-DSA-87':
47164751
return true
47174752
default:
47184753
return false
@@ -4775,6 +4810,9 @@ function keyToSubtle(key: CryptoKey): AlgorithmIdentifier | RsaPssParams | Ecdsa
47754810
case 'RSASSA-PKCS1-v1_5':
47764811
checkRsaKeyAlgorithm(key)
47774812
return key.algorithm.name
4813+
case 'ML-DSA-44':
4814+
case 'ML-DSA-65':
4815+
case 'ML-DSA-87':
47784816
case 'Ed25519':
47794817
return key.algorithm.name
47804818
}
@@ -4985,8 +5023,13 @@ export async function validateJwtAuthResponse(
49855023
return validateAuthResponse(as, client, result, expectedState)
49865024
}
49875025

5026+
interface CShakeParams {
5027+
name: string
5028+
length: number
5029+
}
5030+
49885031
async function idTokenHash(data: string, header: CompactJWSHeaderParameters, claimName: string) {
4989-
let algorithm: string
5032+
let algorithm: string | CShakeParams
49905033
switch (header.alg) {
49915034
case 'RS256': // Fall through
49925035
case 'PS256': // Fall through
@@ -5005,6 +5048,11 @@ async function idTokenHash(data: string, header: CompactJWSHeaderParameters, cla
50055048
case 'EdDSA':
50065049
algorithm = 'SHA-512'
50075050
break
5051+
case 'ML-DSA-44':
5052+
case 'ML-DSA-65':
5053+
case 'ML-DSA-87':
5054+
algorithm = { name: 'cSHAKE256', length: 512 }
5055+
break
50085056
default:
50095057
throw new UnsupportedOperationError(
50105058
`unsupported JWS algorithm for ${claimName} calculation`,
@@ -5563,9 +5611,13 @@ function algToSubtle(alg: string): RsaHashedImportParams | EcKeyImportParams | A
55635611
return { name: 'ECDSA', namedCurve: `P-${alg.slice(-3)}` }
55645612
case 'ES512':
55655613
return { name: 'ECDSA', namedCurve: 'P-521' }
5566-
case 'Ed25519':
55675614
case 'EdDSA':
55685615
return 'Ed25519'
5616+
case 'Ed25519':
5617+
case 'ML-DSA-44':
5618+
case 'ML-DSA-65':
5619+
case 'ML-DSA-87':
5620+
return alg
55695621
default:
55705622
throw new UnsupportedOperationError('unsupported JWS algorithm', { cause: { alg } })
55715623
}
@@ -5970,34 +6022,7 @@ async function validateDPoP(
59706022
}
59716023

59726024
{
5973-
let components: JWK
5974-
switch (proof.header.jwk!.kty) {
5975-
case 'EC':
5976-
components = {
5977-
crv: proof.header.jwk!.crv,
5978-
kty: proof.header.jwk!.kty,
5979-
x: proof.header.jwk!.x,
5980-
y: proof.header.jwk!.y,
5981-
}
5982-
break
5983-
case 'OKP':
5984-
components = {
5985-
crv: proof.header.jwk!.crv,
5986-
kty: proof.header.jwk!.kty,
5987-
x: proof.header.jwk!.x,
5988-
}
5989-
break
5990-
case 'RSA':
5991-
components = {
5992-
e: proof.header.jwk!.e,
5993-
kty: proof.header.jwk!.kty,
5994-
n: proof.header.jwk!.n,
5995-
}
5996-
break
5997-
default:
5998-
throw new UnsupportedOperationError('unsupported JWK key type', { cause: proof.header.jwk })
5999-
}
6000-
const expected = b64u(await crypto.subtle.digest('SHA-256', buf(JSON.stringify(components))))
6025+
const expected = await calculateJwkThumbprint(proof.header.jwk!)
60016026

60026027
if (accessTokenClaims.cnf.jkt !== expected) {
60036028
throw OPE('JWT Access Token confirmation mismatch', JWT_CLAIM_COMPARISON, {

tap/env.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,9 @@ export const isGecko = isBrowser && (await isEngine('Gecko'))
4040

4141
export const isWorkerd =
4242
typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers'
43+
44+
export function isNodeVersionAtLeast(major: number, minor: number) {
45+
// @ts-ignore
46+
const parts = globalThis.process.versions.node.split('.').map((i: string) => parseInt(i, 10))
47+
return parts[0] >= major || (parts[0] === major && parts[1] > minor)
48+
}

tap/generate.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ export default (QUnit: QUnit) => {
5151
case 'RS':
5252
t.equal(key.algorithm.name, 'RSASSA-PKCS1-v1_5')
5353
break
54+
case 'ML':
55+
t.equal(key.algorithm.name, alg)
56+
break
5457
}
5558

5659
if (isRSA(alg)) {

tap/keys.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ if (!env.isDeno) {
1919
algs.push('ES512')
2020
}
2121

22+
if (env.isNode && env.isNodeVersionAtLeast(24, 7)) {
23+
algs.push('ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87')
24+
} else {
25+
fails.push('ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87')
26+
}
27+
2228
export const keys = algs.reduce(
2329
(acc, alg) => {
2430
acc[alg] = lib.generateKeyPair(alg)

tap/request_object.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,7 @@ export default (QUnit: QUnit) => {
2020
['resource', 'urn:example:resource'],
2121
],
2222
]) {
23-
const jwt = await lib.issueRequestObject(
24-
issuer,
25-
client,
26-
parameters,
27-
{ key: privateKey },
28-
{
29-
[lib.modifyAssertion]: (header) => {
30-
if (header.alg === 'Ed25519') {
31-
header.alg = 'EdDSA'
32-
}
33-
},
34-
},
35-
)
23+
const jwt = await lib.issueRequestObject(issuer, client, parameters, { key: privateKey })
3624

3725
const { payload, protectedHeader } = await jose.jwtVerify(jwt, publicKey)
3826
t.propEqual(protectedHeader, {

0 commit comments

Comments
 (0)