Light Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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
  • certification/oidc
    • configuration.js
  • docs
    • README.md
  • lib
    • consts
      • jwa.js
    • helpers
      • client_schema.js
      • configuration.js
      • defaults.js
      • initialize_keystore.js
      • keystore.js
    • models
      • client.js
    • shared
      • attest_client_auth.js
  • test
    • client_auth
      • client_auth.test.js
    • configuration
      • client_keystore.test.js
      • client_metadata.test.js
      • keystore_configuration.test.js
    • keys.js
    • run.js
    • signatures
      • signatures.test.js

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
@@ -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
@@ -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