@amritk/nish-x86_64-linux 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +8 -5
- package/bin/nish +0 -0
- package/docs/AI.md +99 -24
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -1
- package/runtime/core/host.mjs +4 -0
- package/runtime/nish.d.ts +127 -0
- package/runtime/nish.h +72 -3
- package/runtime/nish.mjs +18 -4
- package/runtime/runtime-net.c +386 -5
- package/runtime/runtime-os.c +59 -8
- package/runtime/runtime-parallel.c +60 -5
- package/runtime/runtime-sandbox.c +43 -34
- package/runtime/runtime-shm.c +541 -0
- package/runtime/runtime.c +827 -791
- package/scripts/build.sh +42 -12
- package/std/README.md +10 -3
- package/std/collections.ts +5 -5
- package/std/crypto/hmac.ts +305 -2
- package/std/crypto/jwt.ts +1661 -0
- package/std/json-typed.ts +34 -0
- package/std/json.ts +35 -6
- package/std/net/handler.ts +1348 -0
- package/std/net/http-client.ts +97 -16
- package/std/net/http-compress.ts +54 -12
- package/std/net/http1-server.ts +77 -23
- package/std/net/http1.ts +24 -4
- package/std/net/outbound.ts +409 -0
- package/std/net/prefork.ts +344 -20
- package/std/secret.ts +1 -1
- package/std/sync/ring.ts +587 -0
- package/std/sync/shm.ts +284 -0
- package/std/term-rate.ts +13 -1
- package/std/threads.ts +35 -3
- /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
|
@@ -0,0 +1,1661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/crypto/jwt` — JSON Web Tokens in the JWS compact serialization: verify
|
|
3
|
+
* one and check its claims, or sign one. RFC 7519 (JWT), RFC 7515 (JWS),
|
|
4
|
+
* RFC 7518 (the HS, RS and ES algorithms), RFC 8037 (EdDSA) and the RFC 8725
|
|
5
|
+
* best current practice.
|
|
6
|
+
*
|
|
7
|
+
* import { JWT_ALG_HS256, JWT_MAX_TOKEN, JWT_OK, JwtVerifier, jwtHmacKey, jwtSignString } from "nish/crypto/jwt";
|
|
8
|
+
*
|
|
9
|
+
* const key = jwtHmacKey(JWT_ALG_HS256, secretKey); // once; null for a short key
|
|
10
|
+
* const v = new JwtVerifier(JWT_MAX_TOKEN); // once, per thread
|
|
11
|
+
* v.issuer = "https://issuer.example";
|
|
12
|
+
* v.audience = "api";
|
|
13
|
+
* v.leeway = toI64(30);
|
|
14
|
+
* if (key !== null && v.verifyString(key, token, now) === JWT_OK) {
|
|
15
|
+
* // v.claims[0 .. v.claimsLen) is the payload's JSON; v.exp, v.nbf, v.iat are read
|
|
16
|
+
* }
|
|
17
|
+
* const minted: string | null = key === null ? null : jwtSignString(key, "{\"sub\":\"ana\"}");
|
|
18
|
+
*
|
|
19
|
+
* **The key decides the algorithm, never the token.** A `JwtKey` is made for
|
|
20
|
+
* one algorithm, and a token verifies only when its header's `alg` is that
|
|
21
|
+
* algorithm's name, spelled exactly. `none` names no key, so a token that
|
|
22
|
+
* says it is unsigned is refused like any other mismatch (`JWT_ERR_ALG`); an
|
|
23
|
+
* `HS256` header in front of an RSA key is refused before any MAC is taken,
|
|
24
|
+
* so an RSA public key can never be used as an HMAC secret (RFC 8725 §2.1,
|
|
25
|
+
* §3.1). The header must hold `alg` exactly once, as a string; `typ` and `kid`
|
|
26
|
+
* at most once, as strings; and no `crit` and no `b64` (RFC 7515 §4.1.11,
|
|
27
|
+
* RFC 7797), since this verifier understands no extension and a header that
|
|
28
|
+
* names one must be refused. Other header members are skipped.
|
|
29
|
+
*
|
|
30
|
+
* **Nothing in the payload is read before the signature holds.** The verifier
|
|
31
|
+
* reads the header, decodes the signature and checks it, and only then decodes
|
|
32
|
+
* and parses the claims. The claims are then checked against the clock and the
|
|
33
|
+
* verifier's settings:
|
|
34
|
+
*
|
|
35
|
+
* - `exp` (RFC 7519 §4.1.4): the token is good while `now - leeway < exp`.
|
|
36
|
+
* A token with no `exp` is refused unless `requireExp` is cleared.
|
|
37
|
+
* - `nbf` (§4.1.5): good once `nbf <= now + leeway`.
|
|
38
|
+
* - `iat` (§4.1.6): refused when it is later than `now + leeway`.
|
|
39
|
+
* - `iss`, `sub` (§4.1.1, §4.1.2): when the verifier names one, the token must
|
|
40
|
+
* carry it, spelled exactly.
|
|
41
|
+
* - `aud` (§4.1.3): one string or an array of strings. When the verifier names
|
|
42
|
+
* an audience, one of them must be it. A token that carries `aud` is refused
|
|
43
|
+
* by a verifier that names none, as §4.1.3 requires.
|
|
44
|
+
*
|
|
45
|
+
* Each registered claim may appear once; the time claims are integers
|
|
46
|
+
* (seconds since 1970). `now` is the caller's, so a test passes its own clock.
|
|
47
|
+
*
|
|
48
|
+
* **Bounded once made.** A `JwtVerifier` holds its decode buffers, its
|
|
49
|
+
* tokenizer and its results, sized once from the largest token it takes
|
|
50
|
+
* (`JWT_MAX_TOKEN` by default, 8 KiB); a longer token is refused before a
|
|
51
|
+
* byte is decoded. An HS token is checked against `HmacKey`'s pads, keyed
|
|
52
|
+
* once when the key was made, through `hmacKeyedVerify`, and the tag is
|
|
53
|
+
* compared in constant time: an HS verify allocates nothing past its arena
|
|
54
|
+
* block. An RS, ES or EdDSA verify allocates the signed input's copy and
|
|
55
|
+
* whatever the public-key check itself allocates (bignums, points), which
|
|
56
|
+
* the caller's scope releases. One verifier is one thread's: it is not safe
|
|
57
|
+
* to share while a `verify` runs.
|
|
58
|
+
*
|
|
59
|
+
* **Base64url is strict**, as `nish/crypto/base64url`: no padding, no
|
|
60
|
+
* whitespace, no character outside `A-Z a-z 0-9 - _`, and no stray bits in the
|
|
61
|
+
* last character, so each token has one spelling. The token itself is public,
|
|
62
|
+
* so the decoder here branches freely.
|
|
63
|
+
*
|
|
64
|
+
* **Signing** covers HS256/384/512, ES256 and EdDSA, the algorithms the
|
|
65
|
+
* standard library holds a signer for; `jwtSign` answers `null` for an RS or
|
|
66
|
+
* ES384 key, which can verify only. The header written is
|
|
67
|
+
* `{"alg":"…","typ":"JWT"}`, with a `kid` when one is given.
|
|
68
|
+
*
|
|
69
|
+
* **Secrets.** An HMAC key and a private key are `Secret<u8[]>`
|
|
70
|
+
* (`nish:secret`). A `JwtKey` copies what it is handed and the caller still
|
|
71
|
+
* owns, and wipes, its own; `JwtKey.wipe()` zeroes the key's copy and its HMAC
|
|
72
|
+
* pads. The threat model and what was checked are in
|
|
73
|
+
* `docs/security/crypto-jwt.md`.
|
|
74
|
+
*
|
|
75
|
+
* Private names carry the `jwt` prefix because a `std/` module's private
|
|
76
|
+
* functions share the importing program's flat symbol namespace
|
|
77
|
+
* (`docs/wp26-stdlib.md` §3e).
|
|
78
|
+
*/
|
|
79
|
+
import {
|
|
80
|
+
HMAC_SHA256,
|
|
81
|
+
HMAC_SHA384,
|
|
82
|
+
HMAC_SHA512,
|
|
83
|
+
HmacKey,
|
|
84
|
+
hmacKeyedInto,
|
|
85
|
+
hmacKeyedVerify,
|
|
86
|
+
} from "nish/crypto/hmac"
|
|
87
|
+
import {
|
|
88
|
+
RSA_HASH_SHA256,
|
|
89
|
+
RSA_HASH_SHA384,
|
|
90
|
+
RSA_HASH_SHA512,
|
|
91
|
+
RsaPublicKey,
|
|
92
|
+
rsaParsePublicKey,
|
|
93
|
+
rsaVerifyPkcs1v15,
|
|
94
|
+
} from "nish/crypto/rsa"
|
|
95
|
+
import {
|
|
96
|
+
P256_POINT_SIZE,
|
|
97
|
+
P256_SIGNATURE_SIZE,
|
|
98
|
+
p256PublicKey,
|
|
99
|
+
p256SignSha256,
|
|
100
|
+
p256VerifySha256,
|
|
101
|
+
} from "nish/crypto/p256"
|
|
102
|
+
import { P384_POINT_SIZE, P384_SIGNATURE_SIZE, p384VerifySha384 } from "nish/crypto/p384"
|
|
103
|
+
import { ed25519PublicKey, ed25519Sign, ed25519Verify } from "nish/crypto/ed25519"
|
|
104
|
+
import { base64urlEncode } from "nish/crypto/base64url"
|
|
105
|
+
import { Secret, expose, secret, wipe } from "nish:secret"
|
|
106
|
+
|
|
107
|
+
/** HMAC with SHA-256 (RFC 7518 §3.2). */
|
|
108
|
+
export const JWT_ALG_HS256: i32 = 1
|
|
109
|
+
/** HMAC with SHA-384. */
|
|
110
|
+
export const JWT_ALG_HS384: i32 = 2
|
|
111
|
+
/** HMAC with SHA-512. */
|
|
112
|
+
export const JWT_ALG_HS512: i32 = 3
|
|
113
|
+
/** RSASSA-PKCS1-v1_5 with SHA-256 (RFC 7518 §3.3). */
|
|
114
|
+
export const JWT_ALG_RS256: i32 = 4
|
|
115
|
+
/** RSASSA-PKCS1-v1_5 with SHA-384. */
|
|
116
|
+
export const JWT_ALG_RS384: i32 = 5
|
|
117
|
+
/** RSASSA-PKCS1-v1_5 with SHA-512. */
|
|
118
|
+
export const JWT_ALG_RS512: i32 = 6
|
|
119
|
+
/** ECDSA on P-256 with SHA-256 (RFC 7518 §3.4). */
|
|
120
|
+
export const JWT_ALG_ES256: i32 = 7
|
|
121
|
+
/** ECDSA on P-384 with SHA-384. */
|
|
122
|
+
export const JWT_ALG_ES384: i32 = 8
|
|
123
|
+
/** Ed25519 (RFC 8037 §3.1). */
|
|
124
|
+
export const JWT_ALG_EDDSA: i32 = 9
|
|
125
|
+
|
|
126
|
+
/** The token verified and every claim check passed. */
|
|
127
|
+
export const JWT_OK: i32 = 0
|
|
128
|
+
/** The token is longer than the verifier takes, or its window is outside its array. */
|
|
129
|
+
export const JWT_ERR_SIZE: i32 = -1
|
|
130
|
+
/** Not three base64url parts joined by two dots. */
|
|
131
|
+
export const JWT_ERR_FORMAT: i32 = -2
|
|
132
|
+
/** The header is not a JSON object, repeats a member, or names `crit` or `b64`. */
|
|
133
|
+
export const JWT_ERR_HEADER: i32 = -3
|
|
134
|
+
/** The header's `alg` is missing, is not a string, or is not the key's algorithm (`none` included). */
|
|
135
|
+
export const JWT_ERR_ALG: i32 = -4
|
|
136
|
+
/** The signature is the wrong length or does not verify. */
|
|
137
|
+
export const JWT_ERR_SIGNATURE: i32 = -5
|
|
138
|
+
/** The payload is not a JSON object, repeats or mistypes a registered claim, or lacks `exp`. */
|
|
139
|
+
export const JWT_ERR_CLAIMS: i32 = -6
|
|
140
|
+
/** `exp` has passed. */
|
|
141
|
+
export const JWT_ERR_EXPIRED: i32 = -7
|
|
142
|
+
/** `nbf` has not come. */
|
|
143
|
+
export const JWT_ERR_NOT_YET: i32 = -8
|
|
144
|
+
/** `iat` is in the future. */
|
|
145
|
+
export const JWT_ERR_IAT: i32 = -9
|
|
146
|
+
/** `iss` is missing or not the verifier's issuer. */
|
|
147
|
+
export const JWT_ERR_ISSUER: i32 = -10
|
|
148
|
+
/** `aud` does not name the verifier's audience, or is present when the verifier names none. */
|
|
149
|
+
export const JWT_ERR_AUDIENCE: i32 = -11
|
|
150
|
+
/** `sub` is missing or not the verifier's subject. */
|
|
151
|
+
export const JWT_ERR_SUBJECT: i32 = -12
|
|
152
|
+
/** The header's `typ` is not the verifier's `typ`. */
|
|
153
|
+
export const JWT_ERR_TYPE: i32 = -13
|
|
154
|
+
|
|
155
|
+
/** The longest token a default `JwtVerifier` takes, in bytes. */
|
|
156
|
+
export const JWT_MAX_TOKEN: i32 = 8192
|
|
157
|
+
/** The longest token any `JwtVerifier` takes: 1 MiB. */
|
|
158
|
+
export const JWT_MAX_TOKEN_LIMIT: i32 = 1048576
|
|
159
|
+
|
|
160
|
+
/** The longest signature: an 8192-bit RSA modulus. */
|
|
161
|
+
const JWT_MAX_SIGNATURE: i32 = 1024
|
|
162
|
+
/** How deep a header or a payload may nest. */
|
|
163
|
+
const JWT_MAX_DEPTH: i32 = 32
|
|
164
|
+
/** The largest leeway, in seconds: about 68 years, which `now ± leeway` cannot overflow. */
|
|
165
|
+
const JWT_MAX_LEEWAY: i64 = 2147483647
|
|
166
|
+
/** The latest `now`: 2^62 seconds, so `now + leeway` stays inside `i64`. */
|
|
167
|
+
const JWT_MAX_NOW: i64 = 1073741824 * 1073741824 * 4
|
|
168
|
+
const JWT_DOT: i32 = 46
|
|
169
|
+
|
|
170
|
+
/** The `alg` header value for `alg`, `""` for no algorithm this module knows. */
|
|
171
|
+
export const jwtAlgName = (alg: i32): string => {
|
|
172
|
+
if (alg === JWT_ALG_HS256) {
|
|
173
|
+
return "HS256"
|
|
174
|
+
}
|
|
175
|
+
if (alg === JWT_ALG_HS384) {
|
|
176
|
+
return "HS384"
|
|
177
|
+
}
|
|
178
|
+
if (alg === JWT_ALG_HS512) {
|
|
179
|
+
return "HS512"
|
|
180
|
+
}
|
|
181
|
+
if (alg === JWT_ALG_RS256) {
|
|
182
|
+
return "RS256"
|
|
183
|
+
}
|
|
184
|
+
if (alg === JWT_ALG_RS384) {
|
|
185
|
+
return "RS384"
|
|
186
|
+
}
|
|
187
|
+
if (alg === JWT_ALG_RS512) {
|
|
188
|
+
return "RS512"
|
|
189
|
+
}
|
|
190
|
+
if (alg === JWT_ALG_ES256) {
|
|
191
|
+
return "ES256"
|
|
192
|
+
}
|
|
193
|
+
if (alg === JWT_ALG_ES384) {
|
|
194
|
+
return "ES384"
|
|
195
|
+
}
|
|
196
|
+
if (alg === JWT_ALG_EDDSA) {
|
|
197
|
+
return "EdDSA"
|
|
198
|
+
}
|
|
199
|
+
return ""
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Whether `alg` is one of the HS algorithms, checked with an HMAC key. */
|
|
203
|
+
const jwtIsHmac = (alg: i32): boolean =>
|
|
204
|
+
alg === JWT_ALG_HS256 || alg === JWT_ALG_HS384 || alg === JWT_ALG_HS512
|
|
205
|
+
|
|
206
|
+
/** The signature length `alg` gives under `key`, in bytes. */
|
|
207
|
+
const jwtSignatureSize = (key: JwtKey): i32 => {
|
|
208
|
+
const alg: i32 = key.alg
|
|
209
|
+
const hmac: HmacKey | null = key.hmac
|
|
210
|
+
if (hmac !== null) {
|
|
211
|
+
return hmac.size()
|
|
212
|
+
}
|
|
213
|
+
const rsa: RsaPublicKey | null = key.rsa
|
|
214
|
+
if (rsa !== null) {
|
|
215
|
+
return (rsa.bits + 7) >> 3
|
|
216
|
+
}
|
|
217
|
+
if (alg === JWT_ALG_ES256) {
|
|
218
|
+
return P256_SIGNATURE_SIZE
|
|
219
|
+
}
|
|
220
|
+
if (alg === JWT_ALG_ES384) {
|
|
221
|
+
return P384_SIGNATURE_SIZE
|
|
222
|
+
}
|
|
223
|
+
return 64
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const jwtCopy = (a: u8[]): u8[] => jwtSlice(a, toI32(0), toI32(a.length))
|
|
227
|
+
|
|
228
|
+
const jwtLength = (a: u8[]): i32 => toI32(a.length)
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A key for one algorithm: what verifies its tokens and, for a key that can,
|
|
232
|
+
* signs them. Make one with `jwtHmacKey`, `jwtRsaKey`, `jwtRsaKeyParts`,
|
|
233
|
+
* `jwtEcKey`, `jwtEd25519Key`, `jwtEs256SigningKey` or
|
|
234
|
+
* `jwtEd25519SigningKey`; `wipe()` it when it is done with. An HS key holds
|
|
235
|
+
* its HMAC work hashers, so a key is one thread's, like a `JwtVerifier`: give
|
|
236
|
+
* each worker its own.
|
|
237
|
+
*/
|
|
238
|
+
export class JwtKey {
|
|
239
|
+
/** An HS key's pads, keyed once; `null` for any other. */
|
|
240
|
+
readonly hmac: HmacKey | null
|
|
241
|
+
/** An RS key; `null` for any other. */
|
|
242
|
+
readonly rsa: RsaPublicKey | null
|
|
243
|
+
/** An ES key's uncompressed point or an EdDSA key's 32 bytes; empty for HS and RS. */
|
|
244
|
+
readonly pub: u8[]
|
|
245
|
+
/** An ES256 or EdDSA signing key's private scalar or seed; empty for any other. */
|
|
246
|
+
readonly priv: Secret<u8[]>
|
|
247
|
+
/** The one algorithm this key verifies, `JWT_ALG_*`. */
|
|
248
|
+
readonly alg: i32
|
|
249
|
+
/** Whether `jwtSign` signs with this key. */
|
|
250
|
+
readonly canSign: boolean
|
|
251
|
+
/** Whether the key is in use: `wipe()` clears it, and a wiped key signs and verifies nothing. */
|
|
252
|
+
live: boolean
|
|
253
|
+
|
|
254
|
+
constructor(
|
|
255
|
+
alg: i32,
|
|
256
|
+
hmac: HmacKey | null,
|
|
257
|
+
rsa: RsaPublicKey | null,
|
|
258
|
+
pub: u8[],
|
|
259
|
+
priv: Secret<u8[]>,
|
|
260
|
+
canSign: boolean
|
|
261
|
+
) {
|
|
262
|
+
this.alg = alg
|
|
263
|
+
this.hmac = hmac
|
|
264
|
+
this.rsa = rsa
|
|
265
|
+
this.pub = pub
|
|
266
|
+
this.priv = secret(expose(priv, jwtCopy))
|
|
267
|
+
this.canSign = canSign
|
|
268
|
+
this.live = true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Zero the key's private copy and its HMAC pads. It neither signs nor
|
|
273
|
+
* verifies after: its zeroed secret is one anyone could hold.
|
|
274
|
+
*/
|
|
275
|
+
wipe(): void {
|
|
276
|
+
this.live = false
|
|
277
|
+
wipe(this.priv)
|
|
278
|
+
const hmac: HmacKey | null = this.hmac
|
|
279
|
+
if (hmac !== null) {
|
|
280
|
+
hmac.wipe()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** A key with no private half: the private slot is an empty array, made and wiped here. */
|
|
286
|
+
const jwtPublicKey = (
|
|
287
|
+
alg: i32,
|
|
288
|
+
hmac: HmacKey | null,
|
|
289
|
+
rsa: RsaPublicKey | null,
|
|
290
|
+
pub: u8[],
|
|
291
|
+
canSign: boolean
|
|
292
|
+
): JwtKey => {
|
|
293
|
+
const none: Secret<u8[]> = secret(new Array<u8>(0))
|
|
294
|
+
const key = new JwtKey(alg, hmac, rsa, pub, none, canSign)
|
|
295
|
+
wipe(none)
|
|
296
|
+
return key
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* An HS256, HS384 or HS512 key that verifies and signs. RFC 7518 §3.2 asks for
|
|
301
|
+
* a key at least as long as the hash, so a shorter one answers `null`, as does
|
|
302
|
+
* an `alg` that is not HS. The pads are keyed once, here; `key` stays the
|
|
303
|
+
* caller's to wipe.
|
|
304
|
+
*/
|
|
305
|
+
export const jwtHmacKey = (alg: i32, key: Secret<u8[]>): JwtKey | null => {
|
|
306
|
+
let hash: i32 = 0
|
|
307
|
+
let size: i32 = 0
|
|
308
|
+
if (alg === JWT_ALG_HS256) {
|
|
309
|
+
hash = HMAC_SHA256
|
|
310
|
+
size = 32
|
|
311
|
+
} else if (alg === JWT_ALG_HS384) {
|
|
312
|
+
hash = HMAC_SHA384
|
|
313
|
+
size = 48
|
|
314
|
+
} else if (alg === JWT_ALG_HS512) {
|
|
315
|
+
hash = HMAC_SHA512
|
|
316
|
+
size = 64
|
|
317
|
+
} else {
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
if (expose(key, jwtLength) < size) {
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
return jwtPublicKey(alg, new HmacKey(hash, key), null, new Array<u8>(0), true)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* An RS256, RS384 or RS512 key from the DER of a PKCS#1 `RSAPublicKey`
|
|
328
|
+
* (`rsaParsePublicKey`'s input). A key that does not parse, a modulus below
|
|
329
|
+
* 2048 bits (RFC 7518 §3.3) or an `alg` that is not RS answers `null`.
|
|
330
|
+
*/
|
|
331
|
+
export const jwtRsaKey = (alg: i32, der: u8[]): JwtKey | null => {
|
|
332
|
+
if (alg !== JWT_ALG_RS256 && alg !== JWT_ALG_RS384 && alg !== JWT_ALG_RS512) {
|
|
333
|
+
return null
|
|
334
|
+
}
|
|
335
|
+
const rsa: RsaPublicKey | null = rsaParsePublicKey(der)
|
|
336
|
+
if (rsa === null || rsa.bits < 2048) {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
return jwtPublicKey(alg, null, rsa, new Array<u8>(0), false)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Appends a DER length. */
|
|
343
|
+
const jwtDerLength = (out: u8[], n: i32): void => {
|
|
344
|
+
if (n < 128) {
|
|
345
|
+
out.push(toU8(n))
|
|
346
|
+
} else if (n < 256) {
|
|
347
|
+
out.push(toU8(0x81))
|
|
348
|
+
out.push(toU8(n))
|
|
349
|
+
} else {
|
|
350
|
+
out.push(toU8(0x82))
|
|
351
|
+
out.push(toU8(n >> 8))
|
|
352
|
+
out.push(toU8(n & 255))
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Appends `v` as a DER INTEGER: leading zero bytes dropped, one put back when the top bit is set. */
|
|
357
|
+
const jwtDerInteger = (out: u8[], v: u8[]): void => {
|
|
358
|
+
let start: i32 = 0
|
|
359
|
+
const last: i32 = toI32(v.length) - 1
|
|
360
|
+
for (let i: i32 = 0; i < last && i < toI32(v.length); i += 1) {
|
|
361
|
+
if (toI32(v[i]) !== 0) {
|
|
362
|
+
break
|
|
363
|
+
}
|
|
364
|
+
start = i + 1
|
|
365
|
+
}
|
|
366
|
+
const pad: boolean = start < toI32(v.length) && (toI32(v[start]) & 0x80) !== 0
|
|
367
|
+
out.push(toU8(2))
|
|
368
|
+
jwtDerLength(out, toI32(v.length) - start + (pad ? 1 : 0))
|
|
369
|
+
if (pad) {
|
|
370
|
+
out.push(toU8(0))
|
|
371
|
+
}
|
|
372
|
+
for (let i: i32 = start; i >= 0 && i < toI32(v.length); i += 1) {
|
|
373
|
+
out.push(v[i])
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* An RS key from its big-endian modulus and exponent, as a JWK's `n` and `e`
|
|
379
|
+
* spell them once base64url-decoded (RFC 7518 §6.3.1). The same checks as
|
|
380
|
+
* `jwtRsaKey`; parts too long for any key this module takes answer `null`.
|
|
381
|
+
*/
|
|
382
|
+
export const jwtRsaKeyParts = (alg: i32, n: u8[], e: u8[]): JwtKey | null => {
|
|
383
|
+
if (toI32(n.length) > 2048 || toI32(e.length) > 8 || toI32(n.length) === 0 || toI32(e.length) === 0) {
|
|
384
|
+
return null
|
|
385
|
+
}
|
|
386
|
+
const body: u8[] = []
|
|
387
|
+
jwtDerInteger(body, n)
|
|
388
|
+
jwtDerInteger(body, e)
|
|
389
|
+
const der: u8[] = [toU8(0x30)]
|
|
390
|
+
jwtDerLength(der, toI32(body.length))
|
|
391
|
+
for (let i: i32 = 0; i < toI32(body.length); i += 1) {
|
|
392
|
+
der.push(body[i])
|
|
393
|
+
}
|
|
394
|
+
return jwtRsaKey(alg, der)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* An ES256 or ES384 verifying key from its uncompressed point, `04 ‖ x ‖ y`
|
|
399
|
+
* (65 or 97 bytes). A point of the wrong length or form answers `null`; one
|
|
400
|
+
* off the curve makes every verification fail.
|
|
401
|
+
*/
|
|
402
|
+
export const jwtEcKey = (alg: i32, point: u8[]): JwtKey | null => {
|
|
403
|
+
let size: i32 = 0
|
|
404
|
+
if (alg === JWT_ALG_ES256) {
|
|
405
|
+
size = P256_POINT_SIZE
|
|
406
|
+
} else if (alg === JWT_ALG_ES384) {
|
|
407
|
+
size = P384_POINT_SIZE
|
|
408
|
+
}
|
|
409
|
+
if (size === 0 || toI32(point.length) !== size || toI32(point[0]) !== 4) {
|
|
410
|
+
return null
|
|
411
|
+
}
|
|
412
|
+
return jwtPublicKey(alg, null, null, jwtCopy(point), false)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** An EdDSA (Ed25519) verifying key from its 32 bytes (RFC 8037 §2's `x`); another length answers `null`. */
|
|
416
|
+
export const jwtEd25519Key = (pub: u8[]): JwtKey | null => {
|
|
417
|
+
if (toI32(pub.length) !== 32) {
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
return jwtPublicKey(JWT_ALG_EDDSA, null, null, jwtCopy(pub), false)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* An ES256 key that signs and verifies, from its 32-byte private scalar. A
|
|
425
|
+
* scalar `p256PublicKey` refuses answers `null`. The key keeps a copy;
|
|
426
|
+
* `priv` stays the caller's to wipe.
|
|
427
|
+
*/
|
|
428
|
+
export const jwtEs256SigningKey = (priv: Secret<u8[]>): JwtKey | null => {
|
|
429
|
+
const pub: u8[] | null = p256PublicKey(priv)
|
|
430
|
+
if (pub === null) {
|
|
431
|
+
return null
|
|
432
|
+
}
|
|
433
|
+
return new JwtKey(JWT_ALG_ES256, null, null, pub, priv, true)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* An EdDSA key that signs and verifies, from its 32-byte seed (RFC 8037 §2's
|
|
438
|
+
* `d`). A seed of another length answers `null`. The key keeps a copy; `seed`
|
|
439
|
+
* stays the caller's to wipe.
|
|
440
|
+
*/
|
|
441
|
+
export const jwtEd25519SigningKey = (seed: Secret<u8[]>): JwtKey | null => {
|
|
442
|
+
const pub: u8[] | null = ed25519PublicKey(seed)
|
|
443
|
+
if (pub === null) {
|
|
444
|
+
return null
|
|
445
|
+
}
|
|
446
|
+
return new JwtKey(JWT_ALG_EDDSA, null, null, pub, seed, true)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ---- base64url --------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
/** The sextet `c` spells, or -1. */
|
|
452
|
+
const jwtSextet = (c: i32): i32 => {
|
|
453
|
+
if (c >= 65 && c <= 90) {
|
|
454
|
+
return c - 65
|
|
455
|
+
}
|
|
456
|
+
if (c >= 97 && c <= 122) {
|
|
457
|
+
return c - 71
|
|
458
|
+
}
|
|
459
|
+
if (c >= 48 && c <= 57) {
|
|
460
|
+
return c + 4
|
|
461
|
+
}
|
|
462
|
+
if (c === 45) {
|
|
463
|
+
return 62
|
|
464
|
+
}
|
|
465
|
+
if (c === 95) {
|
|
466
|
+
return 63
|
|
467
|
+
}
|
|
468
|
+
return -1
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Decodes the unpadded base64url `src[off .. off + len)` into `out` from 0 and
|
|
473
|
+
* answers the byte count, or -1 for a length of 1 mod 4, a character outside
|
|
474
|
+
* the alphabet, stray low bits in the last character, or more bytes than
|
|
475
|
+
* `out` holds.
|
|
476
|
+
*/
|
|
477
|
+
const jwtDecode = (src: u8[], off: i32, len: i32, out: u8[]): i32 => {
|
|
478
|
+
const tail: i32 = len & 3
|
|
479
|
+
if (tail === 1) {
|
|
480
|
+
return -1
|
|
481
|
+
}
|
|
482
|
+
const size: i32 = (len >> 2) * 3 + (tail === 0 ? 0 : tail - 1)
|
|
483
|
+
if (size > toI32(out.length)) {
|
|
484
|
+
return -1
|
|
485
|
+
}
|
|
486
|
+
let acc: i32 = 0
|
|
487
|
+
let bits: i32 = 0
|
|
488
|
+
let at: i32 = 0
|
|
489
|
+
for (let i: i32 = 0; i < len; i += 1) {
|
|
490
|
+
const v: i32 = jwtSextet(toI32(src[off + i]))
|
|
491
|
+
if (v < 0) {
|
|
492
|
+
return -1
|
|
493
|
+
}
|
|
494
|
+
acc = ((acc << 6) | v) & 0xffffff
|
|
495
|
+
bits += 6
|
|
496
|
+
if (bits >= 8 && at >= 0 && at < toI32(out.length)) {
|
|
497
|
+
bits -= 8
|
|
498
|
+
out[at] = toU8((acc >> bits) & 255)
|
|
499
|
+
at += 1
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if ((acc & ((1 << bits) - 1)) !== 0) {
|
|
503
|
+
return -1
|
|
504
|
+
}
|
|
505
|
+
return at
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---- JSON -------------------------------------------------------------------
|
|
509
|
+
//
|
|
510
|
+
// The header and the claims are read by `JwtJson`, a pull reader of RFC 8259
|
|
511
|
+
// written for this module, rather than by `nish/json-tokenizer`: that one
|
|
512
|
+
// steps over blanks and string runs with `nish/bytes`'s scan kernels, which
|
|
513
|
+
// `std/crypto` keeps out of (docs/security/bytes.md, BYTES-2). This one reads
|
|
514
|
+
// a byte at a time. It answers the token kinds `jwt.ts` asks about and one
|
|
515
|
+
// error, `JWT_JSON_ERR`; it has no string or number limits of its own, since
|
|
516
|
+
// the window it reads is a decoded token part of at most `maxToken` bytes.
|
|
517
|
+
|
|
518
|
+
/** The top-level value is complete and only blanks follow it. */
|
|
519
|
+
const JWT_JSON_END: i32 = 0
|
|
520
|
+
const JWT_JSON_OBJECT_BEGIN: i32 = 1
|
|
521
|
+
const JWT_JSON_OBJECT_END: i32 = 2
|
|
522
|
+
const JWT_JSON_ARRAY_BEGIN: i32 = 3
|
|
523
|
+
const JWT_JSON_ARRAY_END: i32 = 4
|
|
524
|
+
/** A member's name and its colon; the value is the next token. */
|
|
525
|
+
const JWT_JSON_KEY: i32 = 5
|
|
526
|
+
const JWT_JSON_STRING: i32 = 6
|
|
527
|
+
const JWT_JSON_NUMBER: i32 = 7
|
|
528
|
+
/** `true`, `false` or `null`. */
|
|
529
|
+
const JWT_JSON_LITERAL: i32 = 8
|
|
530
|
+
/** Anything RFC 8259 refuses, the window ending inside a value, or a container past the depth. */
|
|
531
|
+
const JWT_JSON_ERR: i32 = -1
|
|
532
|
+
|
|
533
|
+
/** What `next()` reads at the cursor. */
|
|
534
|
+
const JWT_JSON_WANT_VALUE: i32 = 0
|
|
535
|
+
const JWT_JSON_WANT_VALUE_OR_CLOSE: i32 = 1
|
|
536
|
+
const JWT_JSON_WANT_KEY: i32 = 2
|
|
537
|
+
const JWT_JSON_WANT_KEY_OR_CLOSE: i32 = 3
|
|
538
|
+
const JWT_JSON_WANT_COMMA_OR_CLOSE: i32 = 4
|
|
539
|
+
const JWT_JSON_WANT_NOTHING: i32 = 5
|
|
540
|
+
|
|
541
|
+
/** What the depth stack holds for an open container. */
|
|
542
|
+
const JWT_JSON_IN_OBJECT: i32 = 1
|
|
543
|
+
const JWT_JSON_IN_ARRAY: i32 = 2
|
|
544
|
+
|
|
545
|
+
const JWT_CH_TAB: i32 = 9
|
|
546
|
+
const JWT_CH_NEWLINE: i32 = 10
|
|
547
|
+
const JWT_CH_RETURN: i32 = 13
|
|
548
|
+
const JWT_CH_SPACE: i32 = 32
|
|
549
|
+
const JWT_CH_QUOTE: i32 = 34
|
|
550
|
+
const JWT_CH_PLUS: i32 = 43
|
|
551
|
+
const JWT_CH_COMMA: i32 = 44
|
|
552
|
+
const JWT_CH_MINUS: i32 = 45
|
|
553
|
+
const JWT_CH_DOT: i32 = 46
|
|
554
|
+
const JWT_CH_0: i32 = 48
|
|
555
|
+
const JWT_CH_9: i32 = 57
|
|
556
|
+
const JWT_CH_COLON: i32 = 58
|
|
557
|
+
const JWT_CH_OPEN_BRACKET: i32 = 91
|
|
558
|
+
const JWT_CH_BACKSLASH: i32 = 92
|
|
559
|
+
const JWT_CH_CLOSE_BRACKET: i32 = 93
|
|
560
|
+
const JWT_CH_OPEN_BRACE: i32 = 123
|
|
561
|
+
const JWT_CH_CLOSE_BRACE: i32 = 125
|
|
562
|
+
|
|
563
|
+
/** The value of hex digit `b`, or `-1`. */
|
|
564
|
+
const jwtHexValue = (b: i32): i32 => {
|
|
565
|
+
if (b >= 48 && b <= 57) {
|
|
566
|
+
return b - 48
|
|
567
|
+
}
|
|
568
|
+
if (b >= 65 && b <= 70) {
|
|
569
|
+
return b - 55
|
|
570
|
+
}
|
|
571
|
+
if (b >= 97 && b <= 102) {
|
|
572
|
+
return b - 87
|
|
573
|
+
}
|
|
574
|
+
return -1
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** How many bytes code point `cp` takes in UTF-8. */
|
|
578
|
+
const jwtUtf8Width = (cp: i32): i32 => {
|
|
579
|
+
if (cp < 0x80) {
|
|
580
|
+
return 1
|
|
581
|
+
}
|
|
582
|
+
if (cp < 0x800) {
|
|
583
|
+
return 2
|
|
584
|
+
}
|
|
585
|
+
if (cp < 0x10000) {
|
|
586
|
+
return 3
|
|
587
|
+
}
|
|
588
|
+
return 4
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Byte `k` of the `width`-byte UTF-8 form of `cp`. */
|
|
592
|
+
const jwtUtf8Byte = (cp: i32, width: i32, k: i32): i32 => {
|
|
593
|
+
if (width === 1) {
|
|
594
|
+
return cp
|
|
595
|
+
}
|
|
596
|
+
let lead: i32 = 0xf0
|
|
597
|
+
if (width === 2) {
|
|
598
|
+
lead = 0xc0
|
|
599
|
+
} else if (width === 3) {
|
|
600
|
+
lead = 0xe0
|
|
601
|
+
}
|
|
602
|
+
const shift: i32 = (width - 1 - k) * 6
|
|
603
|
+
const tail: i32 = 0x80
|
|
604
|
+
return k === 0 ? lead | (cp >> shift) : tail | ((cp >> shift) & 0x3f)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* A pull reader of one JSON value in `src[off, off + len)`, a token per
|
|
609
|
+
* `next()`, as strict as RFC 8259: UTF-8 checked, every escape and surrogate
|
|
610
|
+
* pair checked, no control byte in a string, no trailing bytes. Keys and
|
|
611
|
+
* strings compare decoded (`keyEquals`), so an escaped name is the name.
|
|
612
|
+
*/
|
|
613
|
+
class JwtJson {
|
|
614
|
+
/** The current integer's value, negated, when `integral` holds. */
|
|
615
|
+
value: i64
|
|
616
|
+
src: u8[]
|
|
617
|
+
readonly stack: u8[]
|
|
618
|
+
pos: i32
|
|
619
|
+
end: i32
|
|
620
|
+
depth: i32
|
|
621
|
+
want: i32
|
|
622
|
+
kind: i32
|
|
623
|
+
failed: i32
|
|
624
|
+
/** The current token's bytes, `src[tokStart, tokEnd)`; a string's and a key's include their quotes. */
|
|
625
|
+
tokStart: i32
|
|
626
|
+
tokEnd: i32
|
|
627
|
+
/** The current key's or string's length once decoded. */
|
|
628
|
+
textBytes: i32
|
|
629
|
+
/** Whether the current number has no fraction or exponent and fits an `i64`. */
|
|
630
|
+
integral: boolean
|
|
631
|
+
|
|
632
|
+
/** A reader for values nested at most `maxDepth` containers deep; `reset` gives it a window. */
|
|
633
|
+
constructor(src: u8[], maxDepth: i32) {
|
|
634
|
+
this.value = toI64(0)
|
|
635
|
+
this.src = src
|
|
636
|
+
this.stack = new Array<u8>(maxDepth)
|
|
637
|
+
this.pos = 0
|
|
638
|
+
this.end = 0
|
|
639
|
+
this.depth = 0
|
|
640
|
+
this.want = JWT_JSON_WANT_VALUE
|
|
641
|
+
this.kind = JWT_JSON_ERR
|
|
642
|
+
this.failed = JWT_JSON_ERR
|
|
643
|
+
this.tokStart = 0
|
|
644
|
+
this.tokEnd = 0
|
|
645
|
+
this.textBytes = 0
|
|
646
|
+
this.integral = false
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Starts over on `src[off, off + len)`; a window outside `src` fails the first `next()`. */
|
|
650
|
+
reset(src: u8[], off: i32, len: i32): void {
|
|
651
|
+
this.src = src
|
|
652
|
+
this.pos = off
|
|
653
|
+
this.end = off + len
|
|
654
|
+
this.depth = 0
|
|
655
|
+
this.want = JWT_JSON_WANT_VALUE
|
|
656
|
+
this.kind = JWT_JSON_ERR
|
|
657
|
+
this.failed = off < 0 || len < 0 || len > toI32(src.length) - off ? JWT_JSON_ERR : 0
|
|
658
|
+
this.integral = false
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** The byte at `i`, or `-1` past the window. */
|
|
662
|
+
at(i: i32): i32 {
|
|
663
|
+
return i >= 0 && i < this.end && i < toI32(this.src.length) ? toI32(this.src[i]) : -1
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** Fails the reader for good. */
|
|
667
|
+
fail(): i32 {
|
|
668
|
+
this.failed = JWT_JSON_ERR
|
|
669
|
+
this.kind = JWT_JSON_ERR
|
|
670
|
+
return JWT_JSON_ERR
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Steps over the four blanks RFC 8259 §2 allows. */
|
|
674
|
+
skipBlanks(): void {
|
|
675
|
+
let b: i32 = this.at(this.pos)
|
|
676
|
+
while (b === JWT_CH_SPACE || b === JWT_CH_TAB || b === JWT_CH_NEWLINE || b === JWT_CH_RETURN) {
|
|
677
|
+
this.pos += 1
|
|
678
|
+
b = this.at(this.pos)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** The token after the current one, or `JWT_JSON_ERR`, answered again on every later call. */
|
|
683
|
+
next(): i32 {
|
|
684
|
+
if (this.failed !== 0) {
|
|
685
|
+
return this.failed
|
|
686
|
+
}
|
|
687
|
+
this.skipBlanks()
|
|
688
|
+
const want: i32 = this.want
|
|
689
|
+
const b: i32 = this.at(this.pos)
|
|
690
|
+
if (want === JWT_JSON_WANT_NOTHING) {
|
|
691
|
+
if (b >= 0) {
|
|
692
|
+
return this.fail()
|
|
693
|
+
}
|
|
694
|
+
this.kind = JWT_JSON_END
|
|
695
|
+
return JWT_JSON_END
|
|
696
|
+
}
|
|
697
|
+
if (want === JWT_JSON_WANT_COMMA_OR_CLOSE) {
|
|
698
|
+
if (b === JWT_CH_COMMA) {
|
|
699
|
+
this.pos += 1
|
|
700
|
+
this.skipBlanks()
|
|
701
|
+
return toI32(this.stack[this.depth - 1]) === JWT_JSON_IN_OBJECT ? this.readKey() : this.readValue()
|
|
702
|
+
}
|
|
703
|
+
return this.readClose(b)
|
|
704
|
+
}
|
|
705
|
+
if (want === JWT_JSON_WANT_KEY_OR_CLOSE) {
|
|
706
|
+
return b === JWT_CH_CLOSE_BRACE ? this.readClose(b) : this.readKey()
|
|
707
|
+
}
|
|
708
|
+
if (want === JWT_JSON_WANT_KEY) {
|
|
709
|
+
return this.readKey()
|
|
710
|
+
}
|
|
711
|
+
if (want === JWT_JSON_WANT_VALUE_OR_CLOSE && b === JWT_CH_CLOSE_BRACKET) {
|
|
712
|
+
return this.readClose(b)
|
|
713
|
+
}
|
|
714
|
+
return this.readValue()
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** `b`, the closer of the innermost container, popped. */
|
|
718
|
+
readClose(b: i32): i32 {
|
|
719
|
+
if (this.depth === 0) {
|
|
720
|
+
return this.fail()
|
|
721
|
+
}
|
|
722
|
+
const inObject: boolean = toI32(this.stack[this.depth - 1]) === JWT_JSON_IN_OBJECT
|
|
723
|
+
if (b !== (inObject ? JWT_CH_CLOSE_BRACE : JWT_CH_CLOSE_BRACKET)) {
|
|
724
|
+
return this.fail()
|
|
725
|
+
}
|
|
726
|
+
this.tokStart = this.pos
|
|
727
|
+
this.pos += 1
|
|
728
|
+
this.tokEnd = this.pos
|
|
729
|
+
this.depth -= 1
|
|
730
|
+
this.afterValue()
|
|
731
|
+
this.kind = inObject ? JWT_JSON_OBJECT_END : JWT_JSON_ARRAY_END
|
|
732
|
+
return this.kind
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** What follows a complete value: nothing at the top, else a comma or a closer. */
|
|
736
|
+
afterValue(): void {
|
|
737
|
+
this.want = this.depth === 0 ? JWT_JSON_WANT_NOTHING : JWT_JSON_WANT_COMMA_OR_CLOSE
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** A member's name, its blanks and its colon. */
|
|
741
|
+
readKey(): i32 {
|
|
742
|
+
if (this.at(this.pos) !== JWT_CH_QUOTE || !this.scanString()) {
|
|
743
|
+
return this.fail()
|
|
744
|
+
}
|
|
745
|
+
this.skipBlanks()
|
|
746
|
+
if (this.at(this.pos) !== JWT_CH_COLON) {
|
|
747
|
+
return this.fail()
|
|
748
|
+
}
|
|
749
|
+
this.pos += 1
|
|
750
|
+
this.want = JWT_JSON_WANT_VALUE
|
|
751
|
+
this.kind = JWT_JSON_KEY
|
|
752
|
+
return JWT_JSON_KEY
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** A value at the cursor: an opener, a string, a number or a literal. */
|
|
756
|
+
readValue(): i32 {
|
|
757
|
+
const b: i32 = this.at(this.pos)
|
|
758
|
+
if (b === JWT_CH_OPEN_BRACE || b === JWT_CH_OPEN_BRACKET) {
|
|
759
|
+
if (this.depth >= toI32(this.stack.length)) {
|
|
760
|
+
return this.fail()
|
|
761
|
+
}
|
|
762
|
+
const inObject: boolean = b === JWT_CH_OPEN_BRACE
|
|
763
|
+
this.stack[this.depth] = toU8(inObject ? JWT_JSON_IN_OBJECT : JWT_JSON_IN_ARRAY)
|
|
764
|
+
this.depth += 1
|
|
765
|
+
this.tokStart = this.pos
|
|
766
|
+
this.pos += 1
|
|
767
|
+
this.tokEnd = this.pos
|
|
768
|
+
this.want = inObject ? JWT_JSON_WANT_KEY_OR_CLOSE : JWT_JSON_WANT_VALUE_OR_CLOSE
|
|
769
|
+
this.kind = inObject ? JWT_JSON_OBJECT_BEGIN : JWT_JSON_ARRAY_BEGIN
|
|
770
|
+
return this.kind
|
|
771
|
+
}
|
|
772
|
+
let k: i32 = JWT_JSON_ERR
|
|
773
|
+
if (b === JWT_CH_QUOTE) {
|
|
774
|
+
k = this.scanString() ? JWT_JSON_STRING : JWT_JSON_ERR
|
|
775
|
+
} else if (b === JWT_CH_MINUS || (b >= JWT_CH_0 && b <= JWT_CH_9)) {
|
|
776
|
+
k = this.scanNumber() ? JWT_JSON_NUMBER : JWT_JSON_ERR
|
|
777
|
+
} else if (this.scanWord("true") || this.scanWord("false") || this.scanWord("null")) {
|
|
778
|
+
k = JWT_JSON_LITERAL
|
|
779
|
+
}
|
|
780
|
+
if (k < 0) {
|
|
781
|
+
return this.fail()
|
|
782
|
+
}
|
|
783
|
+
this.afterValue()
|
|
784
|
+
this.kind = k
|
|
785
|
+
return k
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Whether `word` is at the cursor, stepped over if so. */
|
|
789
|
+
scanWord(word: string): boolean {
|
|
790
|
+
const n: i32 = toI32(word.length)
|
|
791
|
+
for (let i: i32 = 0; i < n; i += 1) {
|
|
792
|
+
if (this.at(this.pos + i) !== toI32(word.charCodeAt(i))) {
|
|
793
|
+
return false
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
this.tokStart = this.pos
|
|
797
|
+
this.pos += n
|
|
798
|
+
this.tokEnd = this.pos
|
|
799
|
+
return true
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** The digits at the cursor, accumulated negated into `value`; how many, and `integral` cleared on overflow. */
|
|
803
|
+
scanDigits(bound: i64): i32 {
|
|
804
|
+
const ten: i64 = toI64(10)
|
|
805
|
+
let n: i32 = 0
|
|
806
|
+
let b: i32 = this.at(this.pos)
|
|
807
|
+
while (b >= JWT_CH_0 && b <= JWT_CH_9) {
|
|
808
|
+
const digit: i64 = toI64(b - JWT_CH_0)
|
|
809
|
+
if (this.integral) {
|
|
810
|
+
if (this.value < (bound + digit) / ten) {
|
|
811
|
+
this.integral = false
|
|
812
|
+
} else {
|
|
813
|
+
this.value = this.value * ten - digit
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
n += 1
|
|
817
|
+
this.pos += 1
|
|
818
|
+
b = this.at(this.pos)
|
|
819
|
+
}
|
|
820
|
+
return n
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* A number of RFC 8259 §6: `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`.
|
|
825
|
+
* `integral` says whether it has neither fraction nor exponent and fits an
|
|
826
|
+
* `i64` for its sign; accumulating negated reaches -2^63, where a positive
|
|
827
|
+
* number stops at 2^63 - 1.
|
|
828
|
+
*/
|
|
829
|
+
scanNumber(): boolean {
|
|
830
|
+
this.tokStart = this.pos
|
|
831
|
+
const negative: boolean = this.at(this.pos) === JWT_CH_MINUS
|
|
832
|
+
if (negative) {
|
|
833
|
+
this.pos += 1
|
|
834
|
+
}
|
|
835
|
+
// -2^63 has no literal (NL2055): -2^31 shifted up 32 bits. A bare literal
|
|
836
|
+
// would be a double under Node, where an `i64` is a BigInt.
|
|
837
|
+
const min: i64 = toI64(-2147483648) << toI64(32)
|
|
838
|
+
const bound: i64 = negative ? min : min + toI64(1)
|
|
839
|
+
this.value = toI64(0)
|
|
840
|
+
this.integral = true
|
|
841
|
+
const first: i32 = this.at(this.pos)
|
|
842
|
+
if (first === JWT_CH_0) {
|
|
843
|
+
this.pos += 1
|
|
844
|
+
} else if (this.scanDigits(bound) === 0) {
|
|
845
|
+
return false
|
|
846
|
+
}
|
|
847
|
+
if (this.at(this.pos) === JWT_CH_DOT) {
|
|
848
|
+
this.pos += 1
|
|
849
|
+
this.integral = false
|
|
850
|
+
if (this.scanDigits(bound) === 0) {
|
|
851
|
+
return false
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const e: i32 = this.at(this.pos)
|
|
855
|
+
if (e === 101 || e === 69) {
|
|
856
|
+
this.pos += 1
|
|
857
|
+
this.integral = false
|
|
858
|
+
const sign: i32 = this.at(this.pos)
|
|
859
|
+
if (sign === JWT_CH_PLUS || sign === JWT_CH_MINUS) {
|
|
860
|
+
this.pos += 1
|
|
861
|
+
}
|
|
862
|
+
if (this.scanDigits(bound) === 0) {
|
|
863
|
+
return false
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
this.tokEnd = this.pos
|
|
867
|
+
return true
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* The code point of the escape at `i` (a backslash), a surrogate pair
|
|
872
|
+
* joined, or `-1` for one RFC 8259 §7 refuses or a lone surrogate.
|
|
873
|
+
*/
|
|
874
|
+
escapeCodePoint(i: i32): i32 {
|
|
875
|
+
const c: i32 = this.at(i + 1)
|
|
876
|
+
if (c === JWT_CH_QUOTE || c === JWT_CH_BACKSLASH || c === 47) {
|
|
877
|
+
return c
|
|
878
|
+
}
|
|
879
|
+
if (c === 98) {
|
|
880
|
+
return 8
|
|
881
|
+
}
|
|
882
|
+
if (c === 102) {
|
|
883
|
+
return 12
|
|
884
|
+
}
|
|
885
|
+
if (c === 110) {
|
|
886
|
+
return JWT_CH_NEWLINE
|
|
887
|
+
}
|
|
888
|
+
if (c === 114) {
|
|
889
|
+
return JWT_CH_RETURN
|
|
890
|
+
}
|
|
891
|
+
if (c === 116) {
|
|
892
|
+
return JWT_CH_TAB
|
|
893
|
+
}
|
|
894
|
+
if (c !== 117) {
|
|
895
|
+
return -1
|
|
896
|
+
}
|
|
897
|
+
const hi: i32 = this.hex4(i + 2)
|
|
898
|
+
if (hi < 0 || (hi >= 0xdc00 && hi <= 0xdfff)) {
|
|
899
|
+
return -1
|
|
900
|
+
}
|
|
901
|
+
if (hi < 0xd800 || hi > 0xdbff) {
|
|
902
|
+
return hi
|
|
903
|
+
}
|
|
904
|
+
if (this.at(i + 6) !== JWT_CH_BACKSLASH || this.at(i + 7) !== 117) {
|
|
905
|
+
return -1
|
|
906
|
+
}
|
|
907
|
+
const lo: i32 = this.hex4(i + 8)
|
|
908
|
+
if (lo < 0xdc00 || lo > 0xdfff) {
|
|
909
|
+
return -1
|
|
910
|
+
}
|
|
911
|
+
const plane: i32 = 0x10000
|
|
912
|
+
return plane + ((hi - 0xd800) << 10) + (lo - 0xdc00)
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** The four hex digits at `i` as a number, or `-1`. */
|
|
916
|
+
hex4(i: i32): i32 {
|
|
917
|
+
let v: i32 = 0
|
|
918
|
+
for (let k: i32 = 0; k < 4; k += 1) {
|
|
919
|
+
const d: i32 = jwtHexValue(this.at(i + k))
|
|
920
|
+
if (d < 0) {
|
|
921
|
+
return -1
|
|
922
|
+
}
|
|
923
|
+
v = (v << 4) | d
|
|
924
|
+
}
|
|
925
|
+
return v
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** How many bytes the escape at `i` takes, given the code point it decodes to. */
|
|
929
|
+
escapeLength(i: i32, cp: i32): i32 {
|
|
930
|
+
if (this.at(i + 1) !== 117) {
|
|
931
|
+
return 2
|
|
932
|
+
}
|
|
933
|
+
return cp >= 0x10000 ? 12 : 6
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* How many continuation bytes the lead byte at `i` takes, checked against
|
|
938
|
+
* RFC 3629 §4 (no overlong form, no surrogate, nothing past U+10FFFF), or
|
|
939
|
+
* `-1`.
|
|
940
|
+
*/
|
|
941
|
+
utf8Tail(i: i32): i32 {
|
|
942
|
+
const b: i32 = this.at(i)
|
|
943
|
+
let n: i32 = 0
|
|
944
|
+
let lo: i32 = 0x80
|
|
945
|
+
let hi: i32 = 0xbf
|
|
946
|
+
if (b >= 0xc2 && b <= 0xdf) {
|
|
947
|
+
n = 1
|
|
948
|
+
} else if (b >= 0xe0 && b <= 0xef) {
|
|
949
|
+
n = 2
|
|
950
|
+
lo = b === 0xe0 ? 0xa0 : 0x80
|
|
951
|
+
hi = b === 0xed ? 0x9f : 0xbf
|
|
952
|
+
} else if (b >= 0xf0 && b <= 0xf4) {
|
|
953
|
+
n = 3
|
|
954
|
+
lo = b === 0xf0 ? 0x90 : 0x80
|
|
955
|
+
hi = b === 0xf4 ? 0x8f : 0xbf
|
|
956
|
+
} else {
|
|
957
|
+
return -1
|
|
958
|
+
}
|
|
959
|
+
const second: i32 = this.at(i + 1)
|
|
960
|
+
if (second < lo || second > hi) {
|
|
961
|
+
return -1
|
|
962
|
+
}
|
|
963
|
+
for (let k: i32 = 2; k <= n; k += 1) {
|
|
964
|
+
const c: i32 = this.at(i + k)
|
|
965
|
+
if (c < 0x80 || c > 0xbf) {
|
|
966
|
+
return -1
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return n
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** The string at the cursor, checked, stepped over, its decoded length in `textBytes`. */
|
|
973
|
+
scanString(): boolean {
|
|
974
|
+
this.tokStart = this.pos
|
|
975
|
+
let i: i32 = this.pos + 1
|
|
976
|
+
let decoded: i32 = 0
|
|
977
|
+
while (true) {
|
|
978
|
+
const b: i32 = this.at(i)
|
|
979
|
+
if (b < 0x20) {
|
|
980
|
+
// A control byte, or `-1`: the window ended inside the string.
|
|
981
|
+
return false
|
|
982
|
+
}
|
|
983
|
+
if (b === JWT_CH_QUOTE) {
|
|
984
|
+
break
|
|
985
|
+
}
|
|
986
|
+
if (b === JWT_CH_BACKSLASH) {
|
|
987
|
+
const cp: i32 = this.escapeCodePoint(i)
|
|
988
|
+
if (cp < 0) {
|
|
989
|
+
return false
|
|
990
|
+
}
|
|
991
|
+
decoded += jwtUtf8Width(cp)
|
|
992
|
+
i += this.escapeLength(i, cp)
|
|
993
|
+
} else if (b < 0x80) {
|
|
994
|
+
decoded += 1
|
|
995
|
+
i += 1
|
|
996
|
+
} else {
|
|
997
|
+
const tail: i32 = this.utf8Tail(i)
|
|
998
|
+
if (tail < 0) {
|
|
999
|
+
return false
|
|
1000
|
+
}
|
|
1001
|
+
decoded += tail + 1
|
|
1002
|
+
i += tail + 1
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
this.pos = i + 1
|
|
1006
|
+
this.tokEnd = this.pos
|
|
1007
|
+
this.textBytes = decoded
|
|
1008
|
+
return true
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Whether the current token is a key or a string. */
|
|
1012
|
+
isText(): boolean {
|
|
1013
|
+
return this.kind === JWT_JSON_KEY || this.kind === JWT_JSON_STRING
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** The current key's or string's length once decoded, or `-1`. */
|
|
1017
|
+
stringLength(): i32 {
|
|
1018
|
+
return this.isText() ? this.textBytes : -1
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Whether the current key or string, decoded, is exactly `name`'s bytes;
|
|
1023
|
+
* `false` when the token is neither. Under Node, `charCodeAt` is a UTF-16
|
|
1024
|
+
* code unit, so only an ASCII name answers as it does natively, as with
|
|
1025
|
+
* `nish/json-tokenizer`'s `keyEquals`.
|
|
1026
|
+
*/
|
|
1027
|
+
keyEquals(name: string): boolean {
|
|
1028
|
+
const n: i32 = toI32(name.length)
|
|
1029
|
+
if (!this.isText() || n !== this.textBytes) {
|
|
1030
|
+
return false
|
|
1031
|
+
}
|
|
1032
|
+
const end: i32 = this.tokEnd - 1
|
|
1033
|
+
let i: i32 = this.tokStart + 1
|
|
1034
|
+
let o: i32 = 0
|
|
1035
|
+
while (i < end) {
|
|
1036
|
+
const b: i32 = this.at(i)
|
|
1037
|
+
if (b !== JWT_CH_BACKSLASH) {
|
|
1038
|
+
if (o >= n || b !== toI32(name.charCodeAt(o))) {
|
|
1039
|
+
return false
|
|
1040
|
+
}
|
|
1041
|
+
o += 1
|
|
1042
|
+
i += 1
|
|
1043
|
+
continue
|
|
1044
|
+
}
|
|
1045
|
+
const cp: i32 = this.escapeCodePoint(i)
|
|
1046
|
+
const width: i32 = jwtUtf8Width(cp)
|
|
1047
|
+
for (let k: i32 = 0; k < width; k += 1) {
|
|
1048
|
+
if (o >= n || jwtUtf8Byte(cp, width, k) !== toI32(name.charCodeAt(o))) {
|
|
1049
|
+
return false
|
|
1050
|
+
}
|
|
1051
|
+
o += 1
|
|
1052
|
+
}
|
|
1053
|
+
i += this.escapeLength(i, cp)
|
|
1054
|
+
}
|
|
1055
|
+
return o === n
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Writes the current key's or string's decoded bytes to `out` from `at` and
|
|
1060
|
+
* answers how many; `-1`, writing nothing, when the token is neither or
|
|
1061
|
+
* `out` has too little room.
|
|
1062
|
+
*/
|
|
1063
|
+
stringInto(out: u8[], at: i32): i32 {
|
|
1064
|
+
if (!this.isText() || at < 0 || at > toI32(out.length) - this.textBytes) {
|
|
1065
|
+
return -1
|
|
1066
|
+
}
|
|
1067
|
+
const end: i32 = this.tokEnd - 1
|
|
1068
|
+
let i: i32 = this.tokStart + 1
|
|
1069
|
+
let o: i32 = at
|
|
1070
|
+
while (i < end) {
|
|
1071
|
+
const b: i32 = this.at(i)
|
|
1072
|
+
if (b !== JWT_CH_BACKSLASH) {
|
|
1073
|
+
if (o >= 0 && o < toI32(out.length)) {
|
|
1074
|
+
out[o] = toU8(b)
|
|
1075
|
+
}
|
|
1076
|
+
o += 1
|
|
1077
|
+
i += 1
|
|
1078
|
+
continue
|
|
1079
|
+
}
|
|
1080
|
+
const cp: i32 = this.escapeCodePoint(i)
|
|
1081
|
+
const width: i32 = jwtUtf8Width(cp)
|
|
1082
|
+
for (let k: i32 = 0; k < width; k += 1) {
|
|
1083
|
+
if (o >= 0 && o < toI32(out.length)) {
|
|
1084
|
+
out[o] = toU8(jwtUtf8Byte(cp, width, k))
|
|
1085
|
+
}
|
|
1086
|
+
o += 1
|
|
1087
|
+
}
|
|
1088
|
+
i += this.escapeLength(i, cp)
|
|
1089
|
+
}
|
|
1090
|
+
return o - at
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Whether the current token is a number without fraction or exponent that fits an `i64`. */
|
|
1094
|
+
isInteger(): boolean {
|
|
1095
|
+
return this.kind === JWT_JSON_NUMBER && this.integral
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** The current number as an `i64` when `isInteger()` holds, else `0`. */
|
|
1099
|
+
numberI64(): i64 {
|
|
1100
|
+
if (!this.isInteger()) {
|
|
1101
|
+
return toI64(0)
|
|
1102
|
+
}
|
|
1103
|
+
return this.at(this.tokStart) === JWT_CH_MINUS ? this.value : -this.value
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Steps over the value the current token starts: after a key, its value;
|
|
1108
|
+
* after an opener, everything through its closer; after a scalar or a
|
|
1109
|
+
* closer, nothing. Answers the kind of the last token read, or `JWT_JSON_ERR`.
|
|
1110
|
+
*/
|
|
1111
|
+
skipValue(): i32 {
|
|
1112
|
+
if (this.failed !== 0) {
|
|
1113
|
+
return this.failed
|
|
1114
|
+
}
|
|
1115
|
+
let k: i32 = this.kind
|
|
1116
|
+
if (k === JWT_JSON_KEY) {
|
|
1117
|
+
k = this.next()
|
|
1118
|
+
}
|
|
1119
|
+
if (k !== JWT_JSON_OBJECT_BEGIN && k !== JWT_JSON_ARRAY_BEGIN) {
|
|
1120
|
+
return k
|
|
1121
|
+
}
|
|
1122
|
+
const outer: i32 = this.depth - 1
|
|
1123
|
+
while (true) {
|
|
1124
|
+
k = this.next()
|
|
1125
|
+
if (k < 0 || this.depth === outer) {
|
|
1126
|
+
return k
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ---- verify -----------------------------------------------------------------
|
|
1133
|
+
|
|
1134
|
+
/** `v` held to `[lo, hi]`. */
|
|
1135
|
+
const jwtClamp = (v: i64, lo: i64, hi: i64): i64 => {
|
|
1136
|
+
if (v < lo) {
|
|
1137
|
+
return lo
|
|
1138
|
+
}
|
|
1139
|
+
return v > hi ? hi : v
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/** The bytes `[off, off + len)` of `src` as a fresh array. */
|
|
1143
|
+
const jwtSlice = (src: u8[], off: i32, len: i32): u8[] => {
|
|
1144
|
+
const out: u8[] = new Array<u8>(len)
|
|
1145
|
+
for (let i: i32 = 0; i < len && i < toI32(out.length); i += 1) {
|
|
1146
|
+
out[i] = src[off + i]
|
|
1147
|
+
}
|
|
1148
|
+
return out
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Whether `sig[0 .. sigLen)` is `key`'s public-key signature over
|
|
1153
|
+
* `src[off .. off + len)`. The verifiers take whole arrays, so the message and
|
|
1154
|
+
* the signature are copied, and the RSA, P-256, P-384 and Ed25519 verifiers
|
|
1155
|
+
* store allocations of their own, which no arena block may release around
|
|
1156
|
+
* (NL2424): a public-key verify allocates, and the collector takes it back.
|
|
1157
|
+
* Only HMAC verifies allocation-free (`hmacKeyedVerify`).
|
|
1158
|
+
*/
|
|
1159
|
+
const jwtVerifyPublic = (key: JwtKey, src: u8[], off: i32, len: i32, sig: u8[], sigLen: i32): boolean => {
|
|
1160
|
+
const msg: u8[] = jwtSlice(src, off, len)
|
|
1161
|
+
const s: u8[] = jwtSlice(sig, 0, sigLen)
|
|
1162
|
+
const alg: i32 = key.alg
|
|
1163
|
+
const rsa: RsaPublicKey | null = key.rsa
|
|
1164
|
+
if (rsa !== null) {
|
|
1165
|
+
let hash: i32 = RSA_HASH_SHA512
|
|
1166
|
+
if (alg === JWT_ALG_RS256) {
|
|
1167
|
+
hash = RSA_HASH_SHA256
|
|
1168
|
+
} else if (alg === JWT_ALG_RS384) {
|
|
1169
|
+
hash = RSA_HASH_SHA384
|
|
1170
|
+
}
|
|
1171
|
+
return rsaVerifyPkcs1v15(rsa, hash, msg, s)
|
|
1172
|
+
}
|
|
1173
|
+
if (alg === JWT_ALG_ES256) {
|
|
1174
|
+
return p256VerifySha256(key.pub, msg, s)
|
|
1175
|
+
}
|
|
1176
|
+
if (alg === JWT_ALG_ES384) {
|
|
1177
|
+
return p384VerifySha384(key.pub, msg, s)
|
|
1178
|
+
}
|
|
1179
|
+
if (alg === JWT_ALG_EDDSA) {
|
|
1180
|
+
return ed25519Verify(key.pub, msg, s)
|
|
1181
|
+
}
|
|
1182
|
+
return false
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Verifies compact JWS tokens and checks their claims, reusing its buffers:
|
|
1187
|
+
* make one per thread and call `verify` per token. The settings are public
|
|
1188
|
+
* fields, read on every `verify`. After `JWT_OK`, `claims[0 .. claimsLen)`
|
|
1189
|
+
* is the payload's JSON for the caller's own claims, and `exp`, `nbf` and
|
|
1190
|
+
* `iat` hold the time claims when `hasExp`, `hasNbf` and `hasIat` say so.
|
|
1191
|
+
*/
|
|
1192
|
+
export class JwtVerifier {
|
|
1193
|
+
/** Seconds of clock skew allowed to `exp`, `nbf` and `iat`; held to `[0, 2^31 - 1]`. */
|
|
1194
|
+
leeway: i64
|
|
1195
|
+
/** The `iss` a token must carry, or `null` for any. */
|
|
1196
|
+
issuer: string | null
|
|
1197
|
+
/** The audience a token's `aud` must name, or `null` to refuse a token with any `aud`. */
|
|
1198
|
+
audience: string | null
|
|
1199
|
+
/** The `sub` a token must carry, or `null` for any. */
|
|
1200
|
+
subject: string | null
|
|
1201
|
+
/** The header `typ` a token must carry (RFC 8725 §3.11), or `null` for any. */
|
|
1202
|
+
typ: string | null
|
|
1203
|
+
/** The decoded payload of the last token, `claimsLen` bytes of it valid after `JWT_OK`. */
|
|
1204
|
+
readonly claims: u8[]
|
|
1205
|
+
exp: i64
|
|
1206
|
+
nbf: i64
|
|
1207
|
+
iat: i64
|
|
1208
|
+
readonly part: u8[]
|
|
1209
|
+
readonly sig: u8[]
|
|
1210
|
+
readonly text: u8[]
|
|
1211
|
+
readonly tok: JwtJson
|
|
1212
|
+
/** The largest token taken, in bytes. */
|
|
1213
|
+
readonly maxToken: i32
|
|
1214
|
+
claimsLen: i32
|
|
1215
|
+
/** Whether a token without `exp` is refused; on by default. */
|
|
1216
|
+
requireExp: boolean
|
|
1217
|
+
hasExp: boolean
|
|
1218
|
+
hasNbf: boolean
|
|
1219
|
+
hasIat: boolean
|
|
1220
|
+
|
|
1221
|
+
/** A verifier for tokens of up to `maxToken` bytes; outside `[16, JWT_MAX_TOKEN_LIMIT]` panics. */
|
|
1222
|
+
constructor(maxToken: i32) {
|
|
1223
|
+
if (maxToken < 16 || maxToken > JWT_MAX_TOKEN_LIMIT) {
|
|
1224
|
+
panic("JwtVerifier: maxToken outside [16, JWT_MAX_TOKEN_LIMIT]")
|
|
1225
|
+
}
|
|
1226
|
+
this.leeway = toI64(0)
|
|
1227
|
+
this.issuer = null
|
|
1228
|
+
this.audience = null
|
|
1229
|
+
this.subject = null
|
|
1230
|
+
this.typ = null
|
|
1231
|
+
this.requireExp = true
|
|
1232
|
+
this.maxToken = maxToken
|
|
1233
|
+
const decoded: i32 = (maxToken >> 2) * 3 + 2
|
|
1234
|
+
this.claims = new Array<u8>(decoded)
|
|
1235
|
+
this.claimsLen = 0
|
|
1236
|
+
this.exp = toI64(0)
|
|
1237
|
+
this.nbf = toI64(0)
|
|
1238
|
+
this.iat = toI64(0)
|
|
1239
|
+
this.hasExp = false
|
|
1240
|
+
this.hasNbf = false
|
|
1241
|
+
this.hasIat = false
|
|
1242
|
+
this.part = new Array<u8>(decoded)
|
|
1243
|
+
this.sig = new Array<u8>(JWT_MAX_SIGNATURE)
|
|
1244
|
+
this.text = new Array<u8>(maxToken)
|
|
1245
|
+
this.tok = new JwtJson(this.part, JWT_MAX_DEPTH)
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* `verify` on the bytes of `token`, copied into the verifier's own buffer
|
|
1250
|
+
* first (a string's UTF-8 bytes); a token longer than `maxToken` is refused
|
|
1251
|
+
* before the copy.
|
|
1252
|
+
*/
|
|
1253
|
+
verifyString(key: JwtKey, token: string, now: i64): i32 {
|
|
1254
|
+
if (toI32(token.length) > this.maxToken) {
|
|
1255
|
+
return JWT_ERR_SIZE
|
|
1256
|
+
}
|
|
1257
|
+
const n: i32 = toI32(token.length)
|
|
1258
|
+
const text: u8[] = this.text
|
|
1259
|
+
for (let i: i32 = 0; i < n && i < toI32(text.length); i += 1) {
|
|
1260
|
+
const c: i32 = toI32(token.charCodeAt(i))
|
|
1261
|
+
// No token character is above 0x7F; where a string holds UTF-16 code
|
|
1262
|
+
// units, cutting one to its low byte could make it a base64url one.
|
|
1263
|
+
if (c > 0x7f) {
|
|
1264
|
+
return JWT_ERR_FORMAT
|
|
1265
|
+
}
|
|
1266
|
+
text[i] = toU8(c)
|
|
1267
|
+
}
|
|
1268
|
+
return this.verify(key, text, toI32(0), n, now)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Verifies the token `src[off .. off + len)` under `key` and checks its
|
|
1273
|
+
* claims at `now` (seconds since 1970): `JWT_OK`, or the first
|
|
1274
|
+
* `JWT_ERR_*` that applies, in the order the module comment lists.
|
|
1275
|
+
*/
|
|
1276
|
+
verify(key: JwtKey, src: u8[], off: i32, len: i32, now: i64): i32 {
|
|
1277
|
+
this.claimsLen = 0
|
|
1278
|
+
this.hasExp = false
|
|
1279
|
+
this.hasNbf = false
|
|
1280
|
+
this.hasIat = false
|
|
1281
|
+
if (len < 0 || len > this.maxToken || off < 0 || off > toI32(src.length) - len) {
|
|
1282
|
+
return JWT_ERR_SIZE
|
|
1283
|
+
}
|
|
1284
|
+
let d1: i32 = -1
|
|
1285
|
+
let d2: i32 = -1
|
|
1286
|
+
for (let i: i32 = off; i < off + len && i < toI32(src.length); i += 1) {
|
|
1287
|
+
if (toI32(src[i]) === JWT_DOT) {
|
|
1288
|
+
if (d1 < 0) {
|
|
1289
|
+
d1 = i
|
|
1290
|
+
} else if (d2 < 0) {
|
|
1291
|
+
d2 = i
|
|
1292
|
+
} else {
|
|
1293
|
+
return JWT_ERR_FORMAT
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (d2 < 0) {
|
|
1298
|
+
return JWT_ERR_FORMAT
|
|
1299
|
+
}
|
|
1300
|
+
const part: u8[] = this.part
|
|
1301
|
+
const headerLen: i32 = jwtDecode(src, off, d1 - off, part)
|
|
1302
|
+
if (headerLen <= 0) {
|
|
1303
|
+
return JWT_ERR_FORMAT
|
|
1304
|
+
}
|
|
1305
|
+
const header: i32 = this.checkHeader(key, headerLen)
|
|
1306
|
+
if (header !== JWT_OK) {
|
|
1307
|
+
return header
|
|
1308
|
+
}
|
|
1309
|
+
const sigText: i32 = off + len - d2 - 1
|
|
1310
|
+
const sigSize: i32 = jwtSignatureSize(key)
|
|
1311
|
+
if (sigText > ((JWT_MAX_SIGNATURE + 2) / 3) * 4) {
|
|
1312
|
+
return JWT_ERR_SIGNATURE
|
|
1313
|
+
}
|
|
1314
|
+
const sig: u8[] = this.sig
|
|
1315
|
+
const sigLen: i32 = jwtDecode(src, d2 + 1, sigText, sig)
|
|
1316
|
+
if (sigLen < 0) {
|
|
1317
|
+
return JWT_ERR_FORMAT
|
|
1318
|
+
}
|
|
1319
|
+
if (sigLen !== sigSize) {
|
|
1320
|
+
return JWT_ERR_SIGNATURE
|
|
1321
|
+
}
|
|
1322
|
+
const hmac: HmacKey | null = key.hmac
|
|
1323
|
+
// The check follows the algorithm: an HS name with an HMAC key, any other name without one.
|
|
1324
|
+
if (!key.live || jwtIsHmac(key.alg) !== (hmac !== null)) {
|
|
1325
|
+
return JWT_ERR_SIGNATURE
|
|
1326
|
+
}
|
|
1327
|
+
const good: boolean =
|
|
1328
|
+
hmac !== null
|
|
1329
|
+
? hmacKeyedVerify(hmac, src, off, d2 - off, sig, 0)
|
|
1330
|
+
: jwtVerifyPublic(key, src, off, d2 - off, sig, sigLen)
|
|
1331
|
+
if (!good) {
|
|
1332
|
+
return JWT_ERR_SIGNATURE
|
|
1333
|
+
}
|
|
1334
|
+
const claims: u8[] = this.claims
|
|
1335
|
+
const claimsLen: i32 = jwtDecode(src, d1 + 1, d2 - d1 - 1, claims)
|
|
1336
|
+
if (claimsLen < 0) {
|
|
1337
|
+
return JWT_ERR_FORMAT
|
|
1338
|
+
}
|
|
1339
|
+
return this.checkClaims(claimsLen, jwtClamp(now, toI64(0), toI64(JWT_MAX_NOW)))
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** The header in `part[0 .. len)` checked against `key`'s algorithm and the verifier's `typ`. */
|
|
1343
|
+
checkHeader(key: JwtKey, len: i32): i32 {
|
|
1344
|
+
const tok: JwtJson = this.tok
|
|
1345
|
+
tok.reset(this.part, toI32(0), len)
|
|
1346
|
+
if (tok.next() !== JWT_JSON_OBJECT_BEGIN) {
|
|
1347
|
+
return JWT_ERR_HEADER
|
|
1348
|
+
}
|
|
1349
|
+
let seenAlg: boolean = false
|
|
1350
|
+
let seenTyp: boolean = false
|
|
1351
|
+
let seenKid: boolean = false
|
|
1352
|
+
let typOk: boolean = false
|
|
1353
|
+
const name: string = jwtAlgName(key.alg)
|
|
1354
|
+
const typ: string | null = this.typ
|
|
1355
|
+
while (true) {
|
|
1356
|
+
const k: i32 = tok.next()
|
|
1357
|
+
if (k === JWT_JSON_OBJECT_END) {
|
|
1358
|
+
break
|
|
1359
|
+
}
|
|
1360
|
+
if (k !== JWT_JSON_KEY) {
|
|
1361
|
+
return JWT_ERR_HEADER
|
|
1362
|
+
}
|
|
1363
|
+
if (tok.keyEquals("alg")) {
|
|
1364
|
+
if (seenAlg) {
|
|
1365
|
+
return JWT_ERR_HEADER
|
|
1366
|
+
}
|
|
1367
|
+
seenAlg = true
|
|
1368
|
+
if (tok.next() !== JWT_JSON_STRING || !tok.keyEquals(name)) {
|
|
1369
|
+
return JWT_ERR_ALG
|
|
1370
|
+
}
|
|
1371
|
+
} else if (tok.keyEquals("typ") || tok.keyEquals("kid")) {
|
|
1372
|
+
const isTyp: boolean = tok.keyEquals("typ")
|
|
1373
|
+
if ((isTyp && seenTyp) || (!isTyp && seenKid)) {
|
|
1374
|
+
return JWT_ERR_HEADER
|
|
1375
|
+
}
|
|
1376
|
+
if (tok.next() !== JWT_JSON_STRING) {
|
|
1377
|
+
return JWT_ERR_HEADER
|
|
1378
|
+
}
|
|
1379
|
+
if (isTyp) {
|
|
1380
|
+
seenTyp = true
|
|
1381
|
+
typOk = typ !== null && tok.keyEquals(typ)
|
|
1382
|
+
} else {
|
|
1383
|
+
seenKid = true
|
|
1384
|
+
}
|
|
1385
|
+
} else if (tok.keyEquals("crit") || tok.keyEquals("b64")) {
|
|
1386
|
+
return JWT_ERR_HEADER
|
|
1387
|
+
} else if (tok.skipValue() < 0) {
|
|
1388
|
+
return JWT_ERR_HEADER
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (tok.next() !== JWT_JSON_END) {
|
|
1392
|
+
return JWT_ERR_HEADER
|
|
1393
|
+
}
|
|
1394
|
+
if (!seenAlg) {
|
|
1395
|
+
return JWT_ERR_ALG
|
|
1396
|
+
}
|
|
1397
|
+
if (typ !== null && !typOk) {
|
|
1398
|
+
return JWT_ERR_TYPE
|
|
1399
|
+
}
|
|
1400
|
+
return JWT_OK
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** The payload in `claims[0 .. len)` parsed and checked at `now`. */
|
|
1404
|
+
checkClaims(len: i32, now: i64): i32 {
|
|
1405
|
+
const tok: JwtJson = this.tok
|
|
1406
|
+
tok.reset(this.claims, toI32(0), len)
|
|
1407
|
+
if (tok.next() !== JWT_JSON_OBJECT_BEGIN) {
|
|
1408
|
+
return JWT_ERR_CLAIMS
|
|
1409
|
+
}
|
|
1410
|
+
const issuer: string | null = this.issuer
|
|
1411
|
+
const audience: string | null = this.audience
|
|
1412
|
+
const subject: string | null = this.subject
|
|
1413
|
+
let hasIss: boolean = false
|
|
1414
|
+
let hasSub: boolean = false
|
|
1415
|
+
let hasAud: boolean = false
|
|
1416
|
+
let issOk: boolean = false
|
|
1417
|
+
let subOk: boolean = false
|
|
1418
|
+
let audOk: boolean = false
|
|
1419
|
+
while (true) {
|
|
1420
|
+
const k: i32 = tok.next()
|
|
1421
|
+
if (k === JWT_JSON_OBJECT_END) {
|
|
1422
|
+
break
|
|
1423
|
+
}
|
|
1424
|
+
if (k !== JWT_JSON_KEY) {
|
|
1425
|
+
return JWT_ERR_CLAIMS
|
|
1426
|
+
}
|
|
1427
|
+
if (tok.keyEquals("exp") || tok.keyEquals("nbf") || tok.keyEquals("iat")) {
|
|
1428
|
+
let which: i32 = 2
|
|
1429
|
+
let seen: boolean = this.hasIat
|
|
1430
|
+
if (tok.keyEquals("exp")) {
|
|
1431
|
+
which = 0
|
|
1432
|
+
seen = this.hasExp
|
|
1433
|
+
} else if (tok.keyEquals("nbf")) {
|
|
1434
|
+
which = 1
|
|
1435
|
+
seen = this.hasNbf
|
|
1436
|
+
}
|
|
1437
|
+
if (seen || tok.next() !== JWT_JSON_NUMBER || !tok.isInteger()) {
|
|
1438
|
+
return JWT_ERR_CLAIMS
|
|
1439
|
+
}
|
|
1440
|
+
const v: i64 = tok.numberI64()
|
|
1441
|
+
if (which === 0) {
|
|
1442
|
+
this.exp = v
|
|
1443
|
+
this.hasExp = true
|
|
1444
|
+
} else if (which === 1) {
|
|
1445
|
+
this.nbf = v
|
|
1446
|
+
this.hasNbf = true
|
|
1447
|
+
} else {
|
|
1448
|
+
this.iat = v
|
|
1449
|
+
this.hasIat = true
|
|
1450
|
+
}
|
|
1451
|
+
} else if (tok.keyEquals("iss") || tok.keyEquals("sub")) {
|
|
1452
|
+
const isIss: boolean = tok.keyEquals("iss")
|
|
1453
|
+
if ((isIss && hasIss) || (!isIss && hasSub) || tok.next() !== JWT_JSON_STRING) {
|
|
1454
|
+
return JWT_ERR_CLAIMS
|
|
1455
|
+
}
|
|
1456
|
+
if (isIss) {
|
|
1457
|
+
hasIss = true
|
|
1458
|
+
issOk = issuer !== null && tok.keyEquals(issuer)
|
|
1459
|
+
} else {
|
|
1460
|
+
hasSub = true
|
|
1461
|
+
subOk = subject !== null && tok.keyEquals(subject)
|
|
1462
|
+
}
|
|
1463
|
+
} else if (tok.keyEquals("aud")) {
|
|
1464
|
+
if (hasAud) {
|
|
1465
|
+
return JWT_ERR_CLAIMS
|
|
1466
|
+
}
|
|
1467
|
+
hasAud = true
|
|
1468
|
+
const v: i32 = tok.next()
|
|
1469
|
+
if (v === JWT_JSON_STRING) {
|
|
1470
|
+
audOk = audience !== null && tok.keyEquals(audience)
|
|
1471
|
+
} else if (v === JWT_JSON_ARRAY_BEGIN) {
|
|
1472
|
+
while (true) {
|
|
1473
|
+
const e: i32 = tok.next()
|
|
1474
|
+
if (e === JWT_JSON_ARRAY_END) {
|
|
1475
|
+
break
|
|
1476
|
+
}
|
|
1477
|
+
if (e !== JWT_JSON_STRING) {
|
|
1478
|
+
return JWT_ERR_CLAIMS
|
|
1479
|
+
}
|
|
1480
|
+
if (audience !== null && tok.keyEquals(audience)) {
|
|
1481
|
+
audOk = true
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
} else {
|
|
1485
|
+
return JWT_ERR_CLAIMS
|
|
1486
|
+
}
|
|
1487
|
+
} else if (tok.skipValue() < 0) {
|
|
1488
|
+
return JWT_ERR_CLAIMS
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (tok.next() !== JWT_JSON_END) {
|
|
1492
|
+
return JWT_ERR_CLAIMS
|
|
1493
|
+
}
|
|
1494
|
+
if (this.requireExp && !this.hasExp) {
|
|
1495
|
+
return JWT_ERR_CLAIMS
|
|
1496
|
+
}
|
|
1497
|
+
const leeway: i64 = jwtClamp(this.leeway, toI64(0), toI64(JWT_MAX_LEEWAY))
|
|
1498
|
+
if (this.hasExp && !(now - leeway < this.exp)) {
|
|
1499
|
+
return JWT_ERR_EXPIRED
|
|
1500
|
+
}
|
|
1501
|
+
if (this.hasNbf && this.nbf > now + leeway) {
|
|
1502
|
+
return JWT_ERR_NOT_YET
|
|
1503
|
+
}
|
|
1504
|
+
if (this.hasIat && this.iat > now + leeway) {
|
|
1505
|
+
return JWT_ERR_IAT
|
|
1506
|
+
}
|
|
1507
|
+
if (issuer !== null && !issOk) {
|
|
1508
|
+
return JWT_ERR_ISSUER
|
|
1509
|
+
}
|
|
1510
|
+
if (hasAud ? !audOk : audience !== null) {
|
|
1511
|
+
return JWT_ERR_AUDIENCE
|
|
1512
|
+
}
|
|
1513
|
+
if (subject !== null && !subOk) {
|
|
1514
|
+
return JWT_ERR_SUBJECT
|
|
1515
|
+
}
|
|
1516
|
+
this.claimsLen = len
|
|
1517
|
+
return JWT_OK
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* The header's `kid` of the token `src[off .. off + len)`, written at
|
|
1522
|
+
* `out[0 ..)`: its length, or -1 when the token is malformed, has no `kid`
|
|
1523
|
+
* or its `kid` does not fit `out`. This reads the header **before** any
|
|
1524
|
+
* signature is checked, so it only chooses which key to verify with; the
|
|
1525
|
+
* `kid` proves nothing until `verify` answers `JWT_OK` under that key.
|
|
1526
|
+
*/
|
|
1527
|
+
peekKid(src: u8[], off: i32, len: i32, out: u8[]): i32 {
|
|
1528
|
+
if (len < 0 || len > this.maxToken || off < 0 || off > toI32(src.length) - len) {
|
|
1529
|
+
return -1
|
|
1530
|
+
}
|
|
1531
|
+
let d1: i32 = -1
|
|
1532
|
+
for (let i: i32 = off; i < off + len && i < toI32(src.length) && d1 < 0; i += 1) {
|
|
1533
|
+
if (toI32(src[i]) === JWT_DOT) {
|
|
1534
|
+
d1 = i
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (d1 < 0) {
|
|
1538
|
+
return -1
|
|
1539
|
+
}
|
|
1540
|
+
const n: i32 = jwtDecode(src, off, d1 - off, this.part)
|
|
1541
|
+
if (n <= 0) {
|
|
1542
|
+
return -1
|
|
1543
|
+
}
|
|
1544
|
+
const tok: JwtJson = this.tok
|
|
1545
|
+
tok.reset(this.part, toI32(0), n)
|
|
1546
|
+
if (tok.next() !== JWT_JSON_OBJECT_BEGIN) {
|
|
1547
|
+
return -1
|
|
1548
|
+
}
|
|
1549
|
+
while (true) {
|
|
1550
|
+
const k: i32 = tok.next()
|
|
1551
|
+
if (k !== JWT_JSON_KEY) {
|
|
1552
|
+
return -1
|
|
1553
|
+
}
|
|
1554
|
+
if (tok.keyEquals("kid")) {
|
|
1555
|
+
if (tok.next() !== JWT_JSON_STRING || tok.stringLength() > toI32(out.length)) {
|
|
1556
|
+
return -1
|
|
1557
|
+
}
|
|
1558
|
+
return tok.stringInto(out, toI32(0))
|
|
1559
|
+
}
|
|
1560
|
+
if (tok.skipValue() < 0) {
|
|
1561
|
+
return -1
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// ---- sign -------------------------------------------------------------------
|
|
1568
|
+
|
|
1569
|
+
/** Whether `src[0 .. len)` is one JSON object and nothing more. */
|
|
1570
|
+
const jwtIsObject = (src: u8[]): boolean => {
|
|
1571
|
+
const tok = new JwtJson(src, JWT_MAX_DEPTH)
|
|
1572
|
+
tok.reset(src, toI32(0), toI32(src.length))
|
|
1573
|
+
if (tok.next() !== JWT_JSON_OBJECT_BEGIN || tok.skipValue() !== JWT_JSON_OBJECT_END) {
|
|
1574
|
+
return false
|
|
1575
|
+
}
|
|
1576
|
+
return tok.next() === JWT_JSON_END
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/** Whether `kid` is 1 to 256 printable ASCII characters without `"` or `\`, so it needs no JSON escape. */
|
|
1580
|
+
const jwtKidFits = (kid: string): boolean => {
|
|
1581
|
+
if (kid.length === 0 || kid.length > 256) {
|
|
1582
|
+
return false
|
|
1583
|
+
}
|
|
1584
|
+
for (let i: i32 = 0; i < toI32(kid.length); i += 1) {
|
|
1585
|
+
const c: i32 = toI32(kid.charCodeAt(i))
|
|
1586
|
+
if (c < 0x20 || c > 0x7e || c === 34 || c === 92) {
|
|
1587
|
+
return false
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return true
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** Whether every code unit of `text` is ASCII, so `jwtBytesOf` keeps it whole. */
|
|
1594
|
+
const jwtIsAscii = (text: string): boolean => {
|
|
1595
|
+
for (let i: i32 = 0; i < toI32(text.length); i += 1) {
|
|
1596
|
+
if (toI32(text.charCodeAt(i)) > 0x7f) {
|
|
1597
|
+
return false
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return true
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/** The code units of `text` as bytes; the caller has checked they are ASCII. */
|
|
1604
|
+
const jwtBytesOf = (text: string): u8[] => {
|
|
1605
|
+
const n: i32 = toI32(text.length)
|
|
1606
|
+
const out: u8[] = new Array<u8>(n)
|
|
1607
|
+
for (let i: i32 = 0; i < n && i < toI32(out.length); i += 1) {
|
|
1608
|
+
out[i] = toU8(text.charCodeAt(i))
|
|
1609
|
+
}
|
|
1610
|
+
return out
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* A compact JWS over the JSON object `claims`, signed with `key`, with a
|
|
1615
|
+
* `kid` header member when `kid` is not `null`. Answers `null` when `key`
|
|
1616
|
+
* cannot sign (RS, ES384 or a verify-only key), when `claims` is not one JSON
|
|
1617
|
+
* object, or when `kid` is empty, longer than 256 bytes, or holds a byte that
|
|
1618
|
+
* would need a JSON escape. The claims are signed as given: this function
|
|
1619
|
+
* neither adds nor checks any claim.
|
|
1620
|
+
*/
|
|
1621
|
+
export const jwtSignKid = (key: JwtKey, kid: string | null, claims: u8[]): string | null => {
|
|
1622
|
+
if (!key.canSign || !key.live || !jwtIsObject(claims)) {
|
|
1623
|
+
return null
|
|
1624
|
+
}
|
|
1625
|
+
let header: string = '{"alg":"' + jwtAlgName(key.alg) + '"'
|
|
1626
|
+
if (kid !== null) {
|
|
1627
|
+
if (!jwtKidFits(kid)) {
|
|
1628
|
+
return null
|
|
1629
|
+
}
|
|
1630
|
+
header = header + ',"kid":"' + kid + '"'
|
|
1631
|
+
}
|
|
1632
|
+
header = header + ',"typ":"JWT"}'
|
|
1633
|
+
const input: string = base64urlEncode(jwtBytesOf(header)) + "." + base64urlEncode(claims)
|
|
1634
|
+
const msg: u8[] = jwtBytesOf(input)
|
|
1635
|
+
const hmac: HmacKey | null = key.hmac
|
|
1636
|
+
let sig: u8[] | null = null
|
|
1637
|
+
if (hmac !== null) {
|
|
1638
|
+
const tag: u8[] = new Array<u8>(hmac.size())
|
|
1639
|
+
hmacKeyedInto(hmac, msg, 0, toI32(msg.length), tag, 0)
|
|
1640
|
+
sig = tag
|
|
1641
|
+
} else if (key.alg === JWT_ALG_ES256) {
|
|
1642
|
+
sig = p256SignSha256(key.priv, msg)
|
|
1643
|
+
} else if (key.alg === JWT_ALG_EDDSA) {
|
|
1644
|
+
sig = ed25519Sign(key.priv, msg)
|
|
1645
|
+
}
|
|
1646
|
+
if (sig === null) {
|
|
1647
|
+
return null
|
|
1648
|
+
}
|
|
1649
|
+
return input + "." + base64urlEncode(sig)
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/** `jwtSignKid` with no `kid`. */
|
|
1653
|
+
export const jwtSign = (key: JwtKey, claims: u8[]): string | null => jwtSignKid(key, null, claims)
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* `jwtSign` over the bytes of the string `claims`. Answers `null` for claims
|
|
1657
|
+
* outside ASCII, as `verifyString` refuses them: escape such text (`\u00e9`)
|
|
1658
|
+
* or pass its UTF-8 bytes to `jwtSign`.
|
|
1659
|
+
*/
|
|
1660
|
+
export const jwtSignString = (key: JwtKey, claims: string): string | null =>
|
|
1661
|
+
jwtIsAscii(claims) ? jwtSignKid(key, null, jwtBytesOf(claims)) : null
|