1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
|

A JWT module for Deno that signs and verifies tokens using **ML-DSA** (FIPS 204) or a **hybrid ML-DSA + Ed25519** scheme for defence-in-depth during the post-quantum transition.
## Features
- **ML-DSA only** — `ML-DSA-44` / `ML-DSA-65` / `ML-DSA-87` via [`@oqs/liboqs-js`](https://www.npmjs.com/package/@oqs/liboqs-js)
- **Hybrid mode** — ML-DSA + Ed25519 signed in parallel. Both signatures must verify. If either algorithm is later broken (classical or quantum), the other still protects the token.
- Standard JWT claims: `exp`, `iat`, `iss`, `sub`
- Zero-dependency verification helpers, typed errors via `JwtError`
## Install
```sh
deno add jsr:@netopwibby/pq-jwt
```
Or import directly:
```ts
import { sign, verify } from "jsr:@netopwibby/pq-jwt";
```
## Usage
### ML-DSA only
```ts
import { generateKeyPair, sign, verify } from "jsr:@netopwibby/pq-jwt";
const { publicKey, secretKey } = await generateKeyPair("ML-DSA-65");
const token = await sign(
{
role: "member",
sub: "user_123"
},
secretKey,
{
algorithm: "ML-DSA-65",
expiresIn: 3600,
issuer: "kern.neue"
}
);
const claims = await verify(token, publicKey, {
algorithm: "ML-DSA-65",
issuer: "kern.neue"
});
console.log(claims.sub); // "user_123"
```
### Hybrid ML-DSA + Ed25519
```ts
import {
generateHybridKeyPair,
hybridSign,
hybridVerify
} from "jsr:@netopwibby/pq-jwt";
const keys = await generateHybridKeyPair("ML-DSA-65");
const token = await hybridSign(
{
role: "admin",
sub: "user_123"
},
keys,
{
expiresIn: 3600,
issuer: "kern.neue"
}
);
const claims = await hybridVerify(
token,
{
ed25519PublicKey: keys.ed25519.publicKey,
mlDsaPublicKey: keys.mlDsa.publicKey
},
{
issuer: "kern.neue",
variant: "ML-DSA-65"
}
);
```
Both signatures must verify. If either fails, `hybridVerify` throws `JwtError`.
### Decode without verifying
```ts
import { decode } from "jsr:@netopwibby/pq-jwt";
const { header, payload } = decode(token);
```
Use only for inspection. Never use the result for authorization.
### Error handling
```ts
import { JwtError, verify } from "jsr:@netopwibby/pq-jwt";
try {
await verify(token, publicKey);
} catch (err) {
if (err instanceof JwtError) {
console.error(err.code, err.message);
// ERR_MALFORMED, ERR_TYPE, ERR_ALGORITHM,
// ERR_SIGNATURE, ERR_SIGNATURE_MLDSA, ERR_SIGNATURE_ED25519,
// ERR_EXPIRED, ERR_ISSUER, ERR_SUBJECT
}
}
```
## API
| Export | Description |
| ------------------------------------------- | ------------------------------------------------ |
| `generateKeyPair(variant?)` | Generate an ML-DSA keypair (default `ML-DSA-65`) |
| `generateHybridKeyPair(variant?)` | Generate a hybrid ML-DSA + Ed25519 keypair |
| `sign(payload, secretKey, options?)` | Sign a payload with ML-DSA |
| `verify(token, publicKey, options?)` | Verify an ML-DSA JWT |
| `hybridSign(payload, keyPair, options?)` | Sign with both ML-DSA and Ed25519 |
| `hybridVerify(token, publicKeys, options?)` | Verify a hybrid JWT (both must pass) |
| `decode(token)` | Decode header + payload without verifying |
| `KEY_SIZES` | Raw byte sizes of keys per variant |
| `JwtError` | Error thrown on any verification failure |
## Key sizes (raw bytes)
| Variant | ML-DSA public | ML-DSA secret | Ed25519 public | Ed25519 secret |
| --------- | ------------- | ------------- | -------------- | -------------- |
| ML-DSA-44 | 1312 | 2560 | 32 | 32 |
| ML-DSA-65 | 1952 | 4032 | 32 | 32 |
| ML-DSA-87 | 2592 | 4896 | 32 | 32 |
ML-DSA signature lengths: `44 → 2420`, `65 → 3293`, `87 → 4595` bytes.
## Hybrid signature layout
```
[0..3] uint32 big-endian ML-DSA signature length
[4..4+mlDsaSigLen-1] ML-DSA signature bytes
[4+mlDsaSigLen..] Ed25519 signature bytes (always 64)
```
The entire blob is Base64url-encoded as the JWT signature segment.
## Run the example
```sh
deno run example.ts
```
## Security notes
`@oqs/liboqs-js` is research/prototyping software and has not been formally audited. The hybrid mode provides defence-in-depth during the PQC transition. Follow [NIST PQC guidance](https://csrc.nist.gov/projects/post-quantum-cryptography) for production deployments.
## License
MIT
|