Skip to content

Commit ad044a9

Browse files
Merge pull request #29 from hwupathum/mldsa
MLDSA signature validation support
2 parents b6edf2e + 64b3ec7 commit ad044a9

7 files changed

Lines changed: 109 additions & 16 deletions

File tree

packages/javascript/src/DefaultCrypto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class DefaultCrypto implements Crypto<Uint8Array> {
5555
clockTolerance?: number,
5656
validateJwtIssuer = true,
5757
): Promise<boolean> {
58-
const key: jose.KeyLike | Uint8Array = await jose.importJWK(jwk as jose.JWK);
58+
const key: jose.CryptoKey | Uint8Array = await jose.importJWK(jwk as jose.JWK);
5959

6060
await jose.jwtVerify(idToken, key, {
6161
algorithms,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
19+
import {exportJWK, generateKeyPair, SignJWT} from 'jose';
20+
import {DefaultCrypto} from '../DefaultCrypto';
21+
import TokenConstants from '../constants/TokenConstants';
22+
import {JWKInterface} from '../models/crypto';
23+
24+
describe('DefaultCrypto.verifyJwt with ML-DSA (AKP JWKs)', (): void => {
25+
it.each(['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'] as const)(
26+
'verifies an id_token signed with %s against its AKP JWK',
27+
async (alg): Promise<void> => {
28+
const {publicKey, privateKey} = await generateKeyPair(alg, {extractable: true});
29+
const jwk = (await exportJWK(publicKey)) as unknown as JWKInterface;
30+
jwk.alg = alg;
31+
jwk.kid = `test-${alg}`;
32+
jwk.use = 'sig';
33+
34+
const idToken: string = await new SignJWT({})
35+
.setProtectedHeader({alg, kid: jwk.kid})
36+
.setIssuedAt()
37+
.setIssuer('https://issuer.example')
38+
.setAudience('client-id')
39+
.setSubject('user-id')
40+
.setExpirationTime('1h')
41+
.sign(privateKey);
42+
43+
const crypto = new DefaultCrypto();
44+
const result: boolean = await crypto.verifyJwt(
45+
idToken,
46+
jwk,
47+
TokenConstants.SignatureValidation.SUPPORTED_ALGORITHMS as unknown as string[],
48+
'client-id',
49+
'https://issuer.example',
50+
'user-id',
51+
);
52+
53+
expect(result).toBe(true);
54+
},
55+
);
56+
57+
it('rejects an ML-DSA id_token with a tampered signature', async (): Promise<void> => {
58+
const alg = 'ML-DSA-65';
59+
const {publicKey, privateKey} = await generateKeyPair(alg, {extractable: true});
60+
const jwk = (await exportJWK(publicKey)) as unknown as JWKInterface;
61+
jwk.alg = alg;
62+
jwk.kid = 'test-tampered';
63+
jwk.use = 'sig';
64+
65+
const idToken: string = await new SignJWT({})
66+
.setProtectedHeader({alg, kid: jwk.kid})
67+
.setIssuedAt()
68+
.setIssuer('https://issuer.example')
69+
.setAudience('client-id')
70+
.setSubject('user-id')
71+
.setExpirationTime('1h')
72+
.sign(privateKey);
73+
74+
const parts: string[] = idToken.split('.');
75+
const tamperedSignature: string = parts[2].slice(0, -4) + (parts[2].endsWith('AAAA') ? 'BBBB' : 'AAAA');
76+
const tamperedToken = `${parts[0]}.${parts[1]}.${tamperedSignature}`;
77+
78+
const crypto = new DefaultCrypto();
79+
await expect(
80+
crypto.verifyJwt(
81+
tamperedToken,
82+
jwk,
83+
TokenConstants.SignatureValidation.SUPPORTED_ALGORITHMS as unknown as string[],
84+
'client-id',
85+
'https://issuer.example',
86+
'user-id',
87+
),
88+
).rejects.toThrow();
89+
});
90+
});

packages/javascript/src/constants/TokenConstants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ const TokenConstants: {
6060
* - `RS512` - RSASSA-PKCS1-v1_5 using SHA-512
6161
* - `RS384` - RSASSA-PKCS1-v1_5 using SHA-384
6262
* - `PS256` - RSASSA-PSS using SHA-256 and MGF1 with SHA-256
63+
* - `ML-DSA-44` / `ML-DSA-65` / `ML-DSA-87` - post-quantum ML-DSA (RFC 9864), AKP JWKs
6364
*/
64-
SUPPORTED_ALGORITHMS: ['RS256', 'RS512', 'RS384', 'PS256'],
65+
SUPPORTED_ALGORITHMS: ['RS256', 'RS512', 'RS384', 'PS256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'],
6566
},
6667

6768
/**

packages/javascript/src/models/crypto.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
*/
2222
export interface JWKInterface {
2323
alg: string;
24-
e: string;
24+
e?: string;
2525
kid: string;
2626
kty: string;
27-
n: string;
27+
n?: string;
28+
/** Raw public key bytes (base64url), for AKP JWKs (RFC 9864, e.g. ML-DSA). */
29+
pub?: string;
2830
use: string;
2931
}
3032

packages/node/src/utils/NodeCryptoUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class NodeCryptoUtils implements Crypto<Buffer | string> {
5252
subject: string,
5353
clockTolerance?: number,
5454
): Promise<boolean> {
55-
const key: jose.KeyLike | Uint8Array = await jose.importJWK(jwk);
55+
const key: jose.CryptoKey | Uint8Array = await jose.importJWK(jwk);
5656
return jose
5757
.jwtVerify(idToken, key, {
5858
algorithms,

pnpm-lock.yaml

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

pnpm-workspace.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ catalog:
3333
dompurify: 3.4.11
3434
eslint: 9.39.4
3535
fast-sha256: 1.3.0
36-
jose: 5.2.0
36+
jose: 6.2.3
3737
jsdom: 27.0.1
3838
memory-cache: 0.2.0
3939
playwright: 1.60.0

0 commit comments

Comments
 (0)