Skip to content

Commit f308b09

Browse files
committed
feat: support ML-DSA JWS algorithm identifiers
1 parent bb69c7b commit f308b09

File tree

17 files changed

+312
-120
lines changed

17 files changed

+312
-120
lines changed

certification/oidc/configuration.js

Lines changed: 108 additions & 81 deletions
Large diffs are not rendered by default.

docs/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1913,7 +1913,7 @@ async function getResourceServerInfo(ctx, resourceIndicator, client) {
19131913
// Tokens will be signed
19141914
sign?:
19151915
| {
1916-
alg?: string, // 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'RS256' | 'RS384' | 'RS512' | 'EdDSA'
1916+
alg?: string, // 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'RS256' | 'RS384' | 'RS512' | 'EdDSA' | 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87'
19171917
kid?: string, // OPTIONAL `kid` to aid in signing key selection
19181918
}
19191919
| {
@@ -3598,6 +3598,7 @@ _**default value**_:
35983598
'PS256', 'PS384', 'PS512',
35993599
'ES256', 'ES384', 'ES512',
36003600
'Ed25519', 'EdDSA',
3601+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
36013602
]
36023603
```
36033604
</details>
@@ -3689,6 +3690,7 @@ _**default value**_:
36893690
'PS256', 'PS384', 'PS512',
36903691
'ES256', 'ES384', 'ES512',
36913692
'Ed25519', 'EdDSA',
3693+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
36923694
'HS256', 'HS384', 'HS512',
36933695
]
36943696
```
@@ -3721,6 +3723,7 @@ _**default value**_:
37213723
'PS256', 'PS384', 'PS512',
37223724
'ES256', 'ES384', 'ES512',
37233725
'Ed25519', 'EdDSA',
3726+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
37243727
'HS256', 'HS384', 'HS512',
37253728
]
37263729
```
@@ -3750,6 +3753,7 @@ _**default value**_:
37503753
'PS256', 'PS384', 'PS512',
37513754
'ES256', 'ES384', 'ES512',
37523755
'Ed25519', 'EdDSA',
3756+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
37533757
]
37543758
```
37553759
</details>
@@ -3841,6 +3845,7 @@ _**default value**_:
38413845
'PS256', 'PS384', 'PS512',
38423846
'ES256', 'ES384', 'ES512',
38433847
'Ed25519', 'EdDSA',
3848+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
38443849
'HS256', 'HS384', 'HS512',
38453850
]
38463851
```
@@ -3933,6 +3938,7 @@ _**default value**_:
39333938
'PS256', 'PS384', 'PS512',
39343939
'ES256', 'ES384', 'ES512',
39353940
'Ed25519', 'EdDSA',
3941+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
39363942
'HS256', 'HS384', 'HS512',
39373943
]
39383944
```
@@ -4026,6 +4032,7 @@ _**default value**_:
40264032
'PS256', 'PS384', 'PS512',
40274033
'ES256', 'ES384', 'ES512',
40284034
'Ed25519', 'EdDSA',
4035+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
40294036
'HS256', 'HS384', 'HS512',
40304037
]
40314038
```
@@ -4118,6 +4125,7 @@ _**default value**_:
41184125
'PS256', 'PS384', 'PS512',
41194126
'ES256', 'ES384', 'ES512',
41204127
'Ed25519', 'EdDSA',
4128+
'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
41214129
'HS256', 'HS384', 'HS512',
41224130
]
41234131
```

lib/consts/jwa.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const signingAlgValues = [
66
'Ed25519', 'EdDSA',
77
];
88

9+
const version = globalThis.process?.version?.substring(1).split('.').map((i) => parseInt(i, 10));
10+
11+
if (version[0] > 24 || (version[0] === 24 && version[1] >= 7)) {
12+
signingAlgValues.push('ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87');
13+
}
14+
915
const encryptionAlgValues = [
1016
// asymmetric
1117
'RSA-OAEP',

lib/helpers/client_schema.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import omitBy from './_/omit_by.js';
1111
const W3CEmailRegExp = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
1212
const needsJwks = {
1313
jwe: /^(RSA|ECDH)/,
14-
jws: /^(?:(?:P|E|R)S(?:256|384|512)|Ed(?:DSA|25519))$/,
14+
jws: /^(?:(?:P|E|R)S(?:256|384|512)|Ed(?:DSA|25519)|ML-DSA-(?:44|65|87))$/,
1515
};
1616
const {
1717
ARYS,

lib/helpers/configuration.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,21 @@ function filterHS(alg) {
2222
return alg.startsWith('HS');
2323
}
2424

25-
const filterAsymmetricSig = RegExp.prototype.test.bind(/^(?:PS(?:256|384|512)|RS(?:256|384|512)|ES(?:256K?|384|512)|Ed(?:25519|DSA))$/);
25+
function filterAsymmetricSig(alg) {
26+
switch (alg.slice(0, 2)) {
27+
case 'ML': // ML-DSA-*, ML-KEM-*
28+
case 'RS': // RS\d{3}, RSA-OAEP
29+
case 'PS': // PS\d{3}
30+
case 'ES': // ECDSA
31+
case 'EC': // ECDH
32+
case 'Ed': // Ed*
33+
case 'X2': // X25519
34+
case 'X4': // X448
35+
return true;
36+
default:
37+
return false;
38+
}
39+
}
2640

2741
const supportedResponseTypes = new Set(['none', 'code', 'id_token', 'token']);
2842
const fapiProfiles = new Set(['1.0 Final', '2.0']);

lib/helpers/defaults.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2107,7 +2107,7 @@ function makeDefaults() {
21072107
* // Tokens will be signed
21082108
* sign?:
21092109
* | {
2110-
* alg?: string, // 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'RS256' | 'RS384' | 'RS512' | 'EdDSA'
2110+
* alg?: string, // 'PS256' | 'PS384' | 'PS512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'RS256' | 'RS384' | 'RS512' | 'EdDSA' | 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87'
21112111
* kid?: string, // OPTIONAL `kid` to aid in signing key selection
21122112
* }
21132113
* | {
@@ -2855,6 +2855,7 @@ function makeDefaults() {
28552855
* 'PS256', 'PS384', 'PS512',
28562856
* 'ES256', 'ES384', 'ES512',
28572857
* 'Ed25519', 'EdDSA',
2858+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
28582859
* 'HS256', 'HS384', 'HS512',
28592860
* ]
28602861
* ```
@@ -2881,6 +2882,7 @@ function makeDefaults() {
28812882
* 'PS256', 'PS384', 'PS512',
28822883
* 'ES256', 'ES384', 'ES512',
28832884
* 'Ed25519', 'EdDSA',
2885+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
28842886
* 'HS256', 'HS384', 'HS512',
28852887
* ]
28862888
* ```
@@ -2900,6 +2902,7 @@ function makeDefaults() {
29002902
* 'PS256', 'PS384', 'PS512',
29012903
* 'ES256', 'ES384', 'ES512',
29022904
* 'Ed25519', 'EdDSA',
2905+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
29032906
* 'HS256', 'HS384', 'HS512',
29042907
* ]
29052908
* ```
@@ -2926,6 +2929,7 @@ function makeDefaults() {
29262929
* 'PS256', 'PS384', 'PS512',
29272930
* 'ES256', 'ES384', 'ES512',
29282931
* 'Ed25519', 'EdDSA',
2932+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
29292933
* 'HS256', 'HS384', 'HS512',
29302934
* ]
29312935
* ```
@@ -2945,6 +2949,7 @@ function makeDefaults() {
29452949
* 'PS256', 'PS384', 'PS512',
29462950
* 'ES256', 'ES384', 'ES512',
29472951
* 'Ed25519', 'EdDSA',
2952+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
29482953
* 'HS256', 'HS384', 'HS512',
29492954
* ]
29502955
* ```
@@ -2970,6 +2975,7 @@ function makeDefaults() {
29702975
* 'PS256', 'PS384', 'PS512',
29712976
* 'ES256', 'ES384', 'ES512',
29722977
* 'Ed25519', 'EdDSA',
2978+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
29732979
* 'HS256', 'HS384', 'HS512',
29742980
* ]
29752981
* ```
@@ -3242,6 +3248,7 @@ function makeDefaults() {
32423248
* 'PS256', 'PS384', 'PS512',
32433249
* 'ES256', 'ES384', 'ES512',
32443250
* 'Ed25519', 'EdDSA',
3251+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
32453252
* ]
32463253
* ```
32473254
*/
@@ -3260,6 +3267,7 @@ function makeDefaults() {
32603267
* 'PS256', 'PS384', 'PS512',
32613268
* 'ES256', 'ES384', 'ES512',
32623269
* 'Ed25519', 'EdDSA',
3270+
* 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', // available in Node.js >= 24.7.0
32633271
* ]
32643272
* ```
32653273
*/

lib/helpers/initialize_keystore.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,18 @@ const calculateKid = (jwk) => {
2626
crv: jwk.crv, kty: 'OKP', x: jwk.x,
2727
};
2828
break;
29+
case 'AKP':
30+
components = {
31+
alg: jwk.alg, kty: 'AKP', pub: jwk.pub,
32+
};
33+
break;
2934
default:
3035
return undefined;
3136
}
3237

3338
return crypto.hash('sha256', JSON.stringify(components), 'base64url');
3439
};
35-
const KEY_TYPES = new Set(['RSA', 'EC', 'OKP']);
40+
const KEY_TYPES = new Set(['RSA', 'EC', 'OKP', 'AKP']);
3641

3742
const jwkSignatureAlgorithms = (jwk) => {
3843
if (jwk.use !== 'sig' && jwk.use !== undefined) {
@@ -67,6 +72,16 @@ const jwkSignatureAlgorithms = (jwk) => {
6772
default:
6873
}
6974
break;
75+
case 'AKP':
76+
switch (jwk.alg) {
77+
case 'ML-DSA-44':
78+
case 'ML-DSA-65':
79+
case 'ML-DSA-87':
80+
available = [jwk.alg];
81+
break;
82+
default:
83+
}
84+
break;
7085
default:
7186
}
7287

@@ -140,14 +155,21 @@ function registerKey(input, i, keystore, kids) {
140155
} else {
141156
key = structuredClone(input);
142157
}
143-
assert(KEY_TYPES.has(key.kty), `only RSA, EC, or OKP keys should be part of jwks configuration (index ${i})`);
158+
assert(KEY_TYPES.has(key.kty), `only RSA, EC, OKP, or AKP keys should be part of jwks configuration (index ${i})`);
144159
key.kid ??= calculateKid(key);
145160
checkString(key.kid, 'kid', i);
146161

147162
assert(!kids.has(key.kid), 'jwks.keys configuration must not contain duplicate "kid" values');
148163
kids.add(key.kid);
149164

150165
switch (key.kty) {
166+
case 'AKP':
167+
checkString(key.alg, 'alg', i);
168+
checkString(key.pub, 'pub', i);
169+
if (!(key instanceof ExternalSigningKey)) {
170+
checkString(key.priv, 'priv', i);
171+
}
172+
break;
151173
case 'OKP':
152174
checkString(key.crv, 'crv', i);
153175
checkString(key.x, 'x', i);
@@ -282,6 +304,7 @@ provide your own in the configuration "jwks" property');
282304
x: key.x,
283305
x5c: key.x5c ? [...key.x5c] : undefined,
284306
y: key.y,
307+
pub: key.pub,
285308
}));
286309
instance(this).jwks = { keys };
287310
}

lib/helpers/keystore.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export class ExternalSigningKey {
3636
return this.#publicJwk.kty;
3737
}
3838

39+
get pub() {
40+
this.#ensurePublicJwk();
41+
return this.#publicJwk.pub;
42+
}
43+
3944
get e() {
4045
this.#ensurePublicJwk();
4146
return this.#publicJwk.e;
@@ -99,6 +104,7 @@ const getKtyFromJWSAlg = (alg) => {
99104
case 'HS': return 'oct';
100105
case 'ES': return 'EC';
101106
case 'Ed': return 'OKP';
107+
case 'ML': return 'AKP';
102108
default:
103109
throw new Error();
104110
}
@@ -138,7 +144,7 @@ const filter = Symbol();
138144

139145
function stripPrivate(jwk) {
140146
const {
141-
d, p, q, dp, dq, qi, oth, ...pub
147+
d, p, q, dp, dq, qi, oth, priv, ...pub
142148
} = jwk;
143149
return pub;
144150
}
@@ -163,13 +169,13 @@ class KeyStore {
163169
const scoring = { alg, use: 'sig' };
164170

165171
return this[filter]((jwk) => {
166-
let candidate = typeof kty === 'string' ? jwk.kty === kty : kty.includes(jwk.kty);
172+
let candidate = jwk.kty === kty;
167173

168174
if (candidate && typeof kid === 'string') {
169175
candidate = kid === jwk.kid;
170176
}
171177

172-
if (candidate && typeof jwk.alg === 'string') {
178+
if (candidate && (typeof jwk.alg === 'string' || kty === 'AKP')) {
173179
candidate = alg === jwk.alg;
174180
}
175181

@@ -272,7 +278,7 @@ class KeyStore {
272278
return getPublic ? input.keyObject() : input;
273279
}
274280

275-
if (input.kty === 'oct' || !input.d || !getPublic) {
281+
if (input.kty === 'oct' || (!input.d && !input.priv) || !getPublic) {
276282
return input;
277283
}
278284

lib/models/client.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,23 +77,30 @@ function checkJWK(jwk) {
7777
if (!EC_CURVES.has(jwk.crv)) return undefined;
7878
if (!(typeof jwk.x === 'string' && jwk.x)) throw new Error();
7979
if (!(typeof jwk.y === 'string' && jwk.y)) throw new Error();
80+
if (jwk.d !== undefined) throw new Error();
8081
break;
8182
case 'OKP':
8283
if (!(typeof jwk.crv === 'string' && jwk.crv)) throw new Error();
8384
if (!OKP_SUBTYPES.has(jwk.crv)) return undefined;
8485
if (!(typeof jwk.x === 'string' && jwk.x)) throw new Error();
86+
if (jwk.d !== undefined) throw new Error();
87+
break;
88+
case 'AKP':
89+
if (!(typeof jwk.alg === 'string' && jwk.alg)) throw new Error();
90+
if (!(typeof jwk.pub === 'string' && jwk.pub)) throw new Error();
91+
if (jwk.priv !== undefined) throw new Error();
8592
break;
8693
case 'RSA':
8794
if (!(typeof jwk.e === 'string' && jwk.e)) throw new Error();
8895
if (!(typeof jwk.n === 'string' && jwk.n)) throw new Error();
96+
if (jwk.d !== undefined) throw new Error();
8997
break;
9098
case 'oct':
91-
break;
99+
throw new Error();
92100
default:
93101
return undefined;
94102
}
95103

96-
if (!(jwk.d === undefined && jwk.kty !== 'oct')) throw new Error();
97104
if (!(jwk.alg === undefined || (typeof jwk.alg === 'string' && jwk.alg))) throw new Error();
98105
if (!(jwk.kid === undefined || (typeof jwk.kid === 'string' && jwk.kid))) throw new Error();
99106
if (!(jwk.use === undefined || (typeof jwk.use === 'string' && jwk.use))) throw new Error();

lib/shared/attest_client_auth.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ export default async function attestationClientAuth(ctx) {
4646
if (verifiedAttestation.key.type !== 'public') {
4747
throw new Error('the resolved key must be a public key');
4848
}
49-
if (typeof verifiedAttestation.payload.cnf?.jwk?.kty !== 'string' || verifiedAttestation.payload.cnf?.jwk?.d !== undefined || verifiedAttestation.payload.cnf?.jwk?.k !== undefined) {
49+
if (
50+
typeof verifiedAttestation.payload.cnf?.jwk?.kty !== 'string'
51+
|| verifiedAttestation.payload.cnf?.jwk?.d !== undefined
52+
|| verifiedAttestation.payload.cnf?.jwk?.priv !== undefined
53+
|| verifiedAttestation.payload.cnf?.jwk?.k !== undefined
54+
) {
5055
throw new Error('invalid cnf.jwk');
5156
}
5257
} catch (err) {

0 commit comments

Comments
 (0)