@arcblock/jwt 1.29.21 → 1.29.23
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/esm/index.d.mts +40 -1
- package/esm/index.mjs +117 -10
- package/lib/index.cjs +120 -10
- package/lib/index.d.cts +40 -1
- package/package.json +6 -8
package/esm/index.d.mts
CHANGED
|
@@ -35,6 +35,28 @@ type JwtVerifyOptions = Partial<{
|
|
|
35
35
|
*/
|
|
36
36
|
declare function sign(signer: string, sk?: BytesType, payload?: {}, doSign?: boolean, version?: string): Promise<string>;
|
|
37
37
|
declare function signV2(signer: string, sk?: BytesType, payload?: any): Promise<string>;
|
|
38
|
+
type PasskeyAssertion = {
|
|
39
|
+
authenticatorData: string;
|
|
40
|
+
clientDataJSON: string;
|
|
41
|
+
signature: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Compute the WebAuthn challenge for an unsigned JWT token.
|
|
45
|
+
* Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
|
|
46
|
+
*
|
|
47
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
|
|
48
|
+
* @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
|
|
49
|
+
*/
|
|
50
|
+
declare function getChallenge(unsignedToken: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Assemble a complete JWT from an unsigned token and a passkey assertion.
|
|
53
|
+
* The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
|
|
54
|
+
*
|
|
55
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64)
|
|
56
|
+
* @param assertion - WebAuthn assertion with base64url-encoded fields
|
|
57
|
+
* @returns complete 3-part JWT string
|
|
58
|
+
*/
|
|
59
|
+
declare function assemble(unsignedToken: string, assertion: PasskeyAssertion): string;
|
|
38
60
|
declare function decode(token: string, bodyOnly?: true): JwtBody;
|
|
39
61
|
declare function decode(token: string, bodyOnly?: false): JwtToken;
|
|
40
62
|
/**
|
|
@@ -88,6 +110,23 @@ declare function signDelegationToken(signer: string, sk: BytesType, payload: {
|
|
|
88
110
|
nbf?: string;
|
|
89
111
|
exp?: string;
|
|
90
112
|
}): Promise<string>;
|
|
113
|
+
/**
|
|
114
|
+
* Create an unsigned delegation token for passkey two-phase signing.
|
|
115
|
+
* Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
|
|
116
|
+
*/
|
|
117
|
+
declare function signDelegationTokenUnsigned(signer: string, pk: BytesType, payload: {
|
|
118
|
+
sub: string;
|
|
119
|
+
ops: string[];
|
|
120
|
+
deny?: string[];
|
|
121
|
+
delegation?: string;
|
|
122
|
+
iat?: string;
|
|
123
|
+
nbf?: string;
|
|
124
|
+
exp?: string;
|
|
125
|
+
}): Promise<string>;
|
|
126
|
+
/**
|
|
127
|
+
* Assemble a complete delegation token from an unsigned token and a passkey assertion.
|
|
128
|
+
*/
|
|
129
|
+
declare function assembleDelegationToken(unsignedToken: string, assertion: PasskeyAssertion): string;
|
|
91
130
|
declare function decodeDelegationToken(token: string): DelegationToken;
|
|
92
131
|
declare function verifyDelegationToken(token: string): Promise<boolean>;
|
|
93
132
|
declare function checkDelegationTokenScope(scope: string, {
|
|
@@ -98,4 +137,4 @@ declare function checkDelegationTokenScope(scope: string, {
|
|
|
98
137
|
deny?: string[];
|
|
99
138
|
}): boolean;
|
|
100
139
|
//#endregion
|
|
101
|
-
export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, checkDelegationTokenScope, decode, decodeDelegationToken, sign, signDelegationToken, signV2, verify, verifyDelegationToken };
|
|
140
|
+
export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, PasskeyAssertion, assemble, assembleDelegationToken, checkDelegationTokenScope, decode, decodeDelegationToken, getChallenge, sign, signDelegationToken, signDelegationTokenUnsigned, signV2, verify, verifyDelegationToken };
|
package/esm/index.mjs
CHANGED
|
@@ -3,12 +3,24 @@ import { Hasher, getSigner, types } from "@ocap/mcrypto";
|
|
|
3
3
|
import { fromBase64, scopeMatchAny, toBase64, toHex } from "@ocap/util";
|
|
4
4
|
import Debug from "debug";
|
|
5
5
|
import stringify from "json-stable-stringify";
|
|
6
|
-
import semver from "semver";
|
|
7
6
|
|
|
8
7
|
//#region src/index.ts
|
|
9
8
|
const debug = Debug("@arcblock/jwt");
|
|
10
9
|
const JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN = "1.1.0";
|
|
11
10
|
const hasher = Hasher.SHA3.hash256;
|
|
11
|
+
function coerceVersion(str) {
|
|
12
|
+
const m = str.match(/(\d+\.\d+\.\d+)/);
|
|
13
|
+
return m ? m[1] : null;
|
|
14
|
+
}
|
|
15
|
+
function semverGte(a, b) {
|
|
16
|
+
const pa = a.split(".").map(Number);
|
|
17
|
+
const pb = b.split(".").map(Number);
|
|
18
|
+
for (let i = 0; i < 3; i++) {
|
|
19
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
20
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
12
24
|
/**
|
|
13
25
|
*
|
|
14
26
|
*
|
|
@@ -59,8 +71,8 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
|
|
|
59
71
|
const bodyB64 = toBase64(stringify(body));
|
|
60
72
|
debug("sign.body", body);
|
|
61
73
|
const msgHex = toHex(`${headerB64}.${bodyB64}`);
|
|
62
|
-
const coercedVersion =
|
|
63
|
-
const msgHash = coercedVersion &&
|
|
74
|
+
const coercedVersion = coerceVersion(version);
|
|
75
|
+
const msgHash = coercedVersion && semverGte(coercedVersion, JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) ? hasher(msgHex) : msgHex;
|
|
64
76
|
// istanbul ignore if
|
|
65
77
|
if (!doSign) return `${headerB64}.${bodyB64}`;
|
|
66
78
|
return [
|
|
@@ -72,6 +84,32 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
|
|
|
72
84
|
async function signV2(signer, sk, payload = {}) {
|
|
73
85
|
return sign(signer, sk, payload, !!sk, "1.1.0");
|
|
74
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Compute the WebAuthn challenge for an unsigned JWT token.
|
|
89
|
+
* Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
|
|
90
|
+
*
|
|
91
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
|
|
92
|
+
* @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
|
|
93
|
+
*/
|
|
94
|
+
function getChallenge(unsignedToken) {
|
|
95
|
+
if (!unsignedToken) throw new Error("Cannot compute challenge from empty token");
|
|
96
|
+
const parts = unsignedToken.split(".");
|
|
97
|
+
return hasher(toHex(`${parts[0]}.${parts[1]}`));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Assemble a complete JWT from an unsigned token and a passkey assertion.
|
|
101
|
+
* The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
|
|
102
|
+
*
|
|
103
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64)
|
|
104
|
+
* @param assertion - WebAuthn assertion with base64url-encoded fields
|
|
105
|
+
* @returns complete 3-part JWT string
|
|
106
|
+
*/
|
|
107
|
+
function assemble(unsignedToken, assertion) {
|
|
108
|
+
if (!unsignedToken) throw new Error("Cannot assemble JWT from empty token");
|
|
109
|
+
if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble JWT: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
|
|
110
|
+
if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble JWT: assertion must contain authenticatorData, clientDataJSON, and signature");
|
|
111
|
+
return `${unsignedToken}.${toBase64(JSON.stringify(assertion))}`;
|
|
112
|
+
}
|
|
75
113
|
function decode(token, bodyOnly = true) {
|
|
76
114
|
const [headerB64, bodyB64, sigB64] = token.split(".");
|
|
77
115
|
const header = JSON.parse(fromBase64(headerB64).toString());
|
|
@@ -154,18 +192,30 @@ async function verify(token, signerPk, options) {
|
|
|
154
192
|
return false;
|
|
155
193
|
}
|
|
156
194
|
}
|
|
195
|
+
const alg = header.alg.toLowerCase();
|
|
196
|
+
if (alg === "passkey") {
|
|
197
|
+
const [, , sigB64] = token.split(".");
|
|
198
|
+
const assertionRaw = JSON.parse(fromBase64(sigB64).toString());
|
|
199
|
+
if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
|
|
200
|
+
debug("verify.error.incompletePasskeyAssertion");
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const challenge = hasher(toHex(`${headerB64}.${bodyB64}`));
|
|
204
|
+
const extra = JSON.stringify({
|
|
205
|
+
authenticatorData: assertionRaw.authenticatorData,
|
|
206
|
+
clientDataJSON: assertionRaw.clientDataJSON
|
|
207
|
+
});
|
|
208
|
+
return await getSigner(types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, signerPk, extra);
|
|
209
|
+
}
|
|
157
210
|
const signers = {
|
|
158
211
|
secp256k1: getSigner(types.KeyType.SECP256K1),
|
|
159
212
|
es256k: getSigner(types.KeyType.SECP256K1),
|
|
160
213
|
ed25519: getSigner(types.KeyType.ED25519),
|
|
161
|
-
ethereum: getSigner(types.KeyType.ETHEREUM)
|
|
162
|
-
passkey: getSigner(types.KeyType.PASSKEY)
|
|
214
|
+
ethereum: getSigner(types.KeyType.ETHEREUM)
|
|
163
215
|
};
|
|
164
|
-
const alg = header.alg.toLowerCase();
|
|
165
216
|
if (signers[alg]) {
|
|
166
217
|
const msgHex = toHex(`${headerB64}.${bodyB64}`);
|
|
167
|
-
const
|
|
168
|
-
const version = coercedBodyVersion ? coercedBodyVersion.version : "";
|
|
218
|
+
const version = (body.version ? coerceVersion(body.version) : null) || "";
|
|
169
219
|
if (version && version === JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) return signers[alg].verify(hasher(msgHex), signature, signerPk);
|
|
170
220
|
return signers[alg].verify(msgHex, signature, signerPk);
|
|
171
221
|
}
|
|
@@ -190,6 +240,10 @@ const delegationTokenHeaders = {
|
|
|
190
240
|
[types.KeyType.ETHEREUM]: {
|
|
191
241
|
alg: "Ethereum",
|
|
192
242
|
typ: "DelegationToken"
|
|
243
|
+
},
|
|
244
|
+
[types.KeyType.PASSKEY]: {
|
|
245
|
+
alg: "Passkey",
|
|
246
|
+
typ: "DelegationToken"
|
|
193
247
|
}
|
|
194
248
|
};
|
|
195
249
|
async function signDelegationToken(signer, sk, payload) {
|
|
@@ -223,6 +277,45 @@ async function signDelegationToken(signer, sk, payload) {
|
|
|
223
277
|
toBase64(getSigner(type.pk).sign(msgHash, sk))
|
|
224
278
|
].join(".");
|
|
225
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Create an unsigned delegation token for passkey two-phase signing.
|
|
282
|
+
* Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
|
|
283
|
+
*/
|
|
284
|
+
async function signDelegationTokenUnsigned(signer, pk, payload) {
|
|
285
|
+
if (isValid(signer) === false) throw new Error("Cannot sign DelegationToken with invalid signer");
|
|
286
|
+
const type = toTypeInfo(signer);
|
|
287
|
+
if (type.pk === void 0) throw new Error("Cannot determine key type from signer");
|
|
288
|
+
const header = delegationTokenHeaders[type.pk];
|
|
289
|
+
const headerB64 = toBase64(stringify(header));
|
|
290
|
+
const delegationSignerMethod = extractMethod(signer) || "abt";
|
|
291
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
292
|
+
const pkHex = typeof pk === "string" ? pk : toHex(pk);
|
|
293
|
+
const body = {
|
|
294
|
+
iss: toDid(signer, delegationSignerMethod),
|
|
295
|
+
sub: payload.sub,
|
|
296
|
+
iat: payload.iat || String(now),
|
|
297
|
+
nbf: payload.nbf || String(now),
|
|
298
|
+
exp: payload.exp || String(now + 300),
|
|
299
|
+
version: DELEGATION_TOKEN_VERSION,
|
|
300
|
+
ops: payload.ops,
|
|
301
|
+
pk: pkHex
|
|
302
|
+
};
|
|
303
|
+
if (payload.deny && payload.deny.length > 0) {
|
|
304
|
+
for (const d of payload.deny) if (!d.startsWith("fg:")) throw new Error(`Invalid deny pattern "${d}": must start with "fg:"`);
|
|
305
|
+
body.deny = payload.deny;
|
|
306
|
+
}
|
|
307
|
+
if (payload.delegation) body.delegation = payload.delegation;
|
|
308
|
+
return `${headerB64}.${toBase64(stringify(body))}`;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Assemble a complete delegation token from an unsigned token and a passkey assertion.
|
|
312
|
+
*/
|
|
313
|
+
function assembleDelegationToken(unsignedToken, assertion) {
|
|
314
|
+
if (!unsignedToken) throw new Error("Cannot assemble DelegationToken from empty token");
|
|
315
|
+
if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble DelegationToken: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
|
|
316
|
+
if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble DelegationToken: assertion must contain authenticatorData, clientDataJSON, and signature");
|
|
317
|
+
return `${unsignedToken}.${toBase64(JSON.stringify(assertion))}`;
|
|
318
|
+
}
|
|
226
319
|
function decodeDelegationToken(token) {
|
|
227
320
|
const parts = token.split(".");
|
|
228
321
|
if (parts.length !== 3) throw new Error("Invalid delegation token format: expected 3 parts");
|
|
@@ -265,13 +358,27 @@ async function verifyDelegationToken(token) {
|
|
|
265
358
|
debug("delegationToken.verify.error.futureIat");
|
|
266
359
|
return false;
|
|
267
360
|
}
|
|
361
|
+
const alg = header.alg.toLowerCase();
|
|
362
|
+
if (alg === "passkey") {
|
|
363
|
+
const [, , sigB64] = token.split(".");
|
|
364
|
+
const assertionRaw = JSON.parse(fromBase64(sigB64).toString());
|
|
365
|
+
if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
|
|
366
|
+
debug("delegationToken.verify.error.incompletePasskeyAssertion");
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
const challenge = hasher(toHex(`${headerB64}.${bodyB64}`));
|
|
370
|
+
const extra = JSON.stringify({
|
|
371
|
+
authenticatorData: assertionRaw.authenticatorData,
|
|
372
|
+
clientDataJSON: assertionRaw.clientDataJSON
|
|
373
|
+
});
|
|
374
|
+
return await getSigner(types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, body.pk, extra);
|
|
375
|
+
}
|
|
268
376
|
const signers = {
|
|
269
377
|
ed25519: getSigner(types.KeyType.ED25519),
|
|
270
378
|
es256k: getSigner(types.KeyType.SECP256K1),
|
|
271
379
|
secp256k1: getSigner(types.KeyType.SECP256K1),
|
|
272
380
|
ethereum: getSigner(types.KeyType.ETHEREUM)
|
|
273
381
|
};
|
|
274
|
-
const alg = header.alg.toLowerCase();
|
|
275
382
|
if (!signers[alg]) {
|
|
276
383
|
debug("delegationToken.verify.error.unknownAlg");
|
|
277
384
|
return false;
|
|
@@ -289,4 +396,4 @@ function checkDelegationTokenScope(scope, { ops, deny }) {
|
|
|
289
396
|
}
|
|
290
397
|
|
|
291
398
|
//#endregion
|
|
292
|
-
export { checkDelegationTokenScope, decode, decodeDelegationToken, sign, signDelegationToken, signV2, verify, verifyDelegationToken };
|
|
399
|
+
export { assemble, assembleDelegationToken, checkDelegationTokenScope, decode, decodeDelegationToken, getChallenge, sign, signDelegationToken, signDelegationTokenUnsigned, signV2, verify, verifyDelegationToken };
|
package/lib/index.cjs
CHANGED
|
@@ -6,13 +6,24 @@ let debug = require("debug");
|
|
|
6
6
|
debug = require_rolldown_runtime.__toESM(debug);
|
|
7
7
|
let json_stable_stringify = require("json-stable-stringify");
|
|
8
8
|
json_stable_stringify = require_rolldown_runtime.__toESM(json_stable_stringify);
|
|
9
|
-
let semver = require("semver");
|
|
10
|
-
semver = require_rolldown_runtime.__toESM(semver);
|
|
11
9
|
|
|
12
10
|
//#region src/index.ts
|
|
13
11
|
const debug$1 = (0, debug.default)("@arcblock/jwt");
|
|
14
12
|
const JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN = "1.1.0";
|
|
15
13
|
const hasher = _ocap_mcrypto.Hasher.SHA3.hash256;
|
|
14
|
+
function coerceVersion(str) {
|
|
15
|
+
const m = str.match(/(\d+\.\d+\.\d+)/);
|
|
16
|
+
return m ? m[1] : null;
|
|
17
|
+
}
|
|
18
|
+
function semverGte(a, b) {
|
|
19
|
+
const pa = a.split(".").map(Number);
|
|
20
|
+
const pb = b.split(".").map(Number);
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return true;
|
|
23
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
16
27
|
/**
|
|
17
28
|
*
|
|
18
29
|
*
|
|
@@ -63,8 +74,8 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
|
|
|
63
74
|
const bodyB64 = (0, _ocap_util.toBase64)((0, json_stable_stringify.default)(body));
|
|
64
75
|
debug$1("sign.body", body);
|
|
65
76
|
const msgHex = (0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`);
|
|
66
|
-
const coercedVersion =
|
|
67
|
-
const msgHash = coercedVersion &&
|
|
77
|
+
const coercedVersion = coerceVersion(version);
|
|
78
|
+
const msgHash = coercedVersion && semverGte(coercedVersion, JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) ? hasher(msgHex) : msgHex;
|
|
68
79
|
// istanbul ignore if
|
|
69
80
|
if (!doSign) return `${headerB64}.${bodyB64}`;
|
|
70
81
|
return [
|
|
@@ -76,6 +87,32 @@ async function sign(signer, sk, payload = {}, doSign = true, version = "1.0.0")
|
|
|
76
87
|
async function signV2(signer, sk, payload = {}) {
|
|
77
88
|
return sign(signer, sk, payload, !!sk, "1.1.0");
|
|
78
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Compute the WebAuthn challenge for an unsigned JWT token.
|
|
92
|
+
* Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
|
|
93
|
+
*
|
|
94
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
|
|
95
|
+
* @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
|
|
96
|
+
*/
|
|
97
|
+
function getChallenge(unsignedToken) {
|
|
98
|
+
if (!unsignedToken) throw new Error("Cannot compute challenge from empty token");
|
|
99
|
+
const parts = unsignedToken.split(".");
|
|
100
|
+
return hasher((0, _ocap_util.toHex)(`${parts[0]}.${parts[1]}`));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Assemble a complete JWT from an unsigned token and a passkey assertion.
|
|
104
|
+
* The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
|
|
105
|
+
*
|
|
106
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64)
|
|
107
|
+
* @param assertion - WebAuthn assertion with base64url-encoded fields
|
|
108
|
+
* @returns complete 3-part JWT string
|
|
109
|
+
*/
|
|
110
|
+
function assemble(unsignedToken, assertion) {
|
|
111
|
+
if (!unsignedToken) throw new Error("Cannot assemble JWT from empty token");
|
|
112
|
+
if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble JWT: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
|
|
113
|
+
if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble JWT: assertion must contain authenticatorData, clientDataJSON, and signature");
|
|
114
|
+
return `${unsignedToken}.${(0, _ocap_util.toBase64)(JSON.stringify(assertion))}`;
|
|
115
|
+
}
|
|
79
116
|
function decode(token, bodyOnly = true) {
|
|
80
117
|
const [headerB64, bodyB64, sigB64] = token.split(".");
|
|
81
118
|
const header = JSON.parse((0, _ocap_util.fromBase64)(headerB64).toString());
|
|
@@ -158,18 +195,30 @@ async function verify(token, signerPk, options) {
|
|
|
158
195
|
return false;
|
|
159
196
|
}
|
|
160
197
|
}
|
|
198
|
+
const alg = header.alg.toLowerCase();
|
|
199
|
+
if (alg === "passkey") {
|
|
200
|
+
const [, , sigB64] = token.split(".");
|
|
201
|
+
const assertionRaw = JSON.parse((0, _ocap_util.fromBase64)(sigB64).toString());
|
|
202
|
+
if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
|
|
203
|
+
debug$1("verify.error.incompletePasskeyAssertion");
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const challenge = hasher((0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`));
|
|
207
|
+
const extra = JSON.stringify({
|
|
208
|
+
authenticatorData: assertionRaw.authenticatorData,
|
|
209
|
+
clientDataJSON: assertionRaw.clientDataJSON
|
|
210
|
+
});
|
|
211
|
+
return await (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, signerPk, extra);
|
|
212
|
+
}
|
|
161
213
|
const signers = {
|
|
162
214
|
secp256k1: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
|
|
163
215
|
es256k: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
|
|
164
216
|
ed25519: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ED25519),
|
|
165
|
-
ethereum: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ETHEREUM)
|
|
166
|
-
passkey: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.PASSKEY)
|
|
217
|
+
ethereum: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ETHEREUM)
|
|
167
218
|
};
|
|
168
|
-
const alg = header.alg.toLowerCase();
|
|
169
219
|
if (signers[alg]) {
|
|
170
220
|
const msgHex = (0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`);
|
|
171
|
-
const
|
|
172
|
-
const version = coercedBodyVersion ? coercedBodyVersion.version : "";
|
|
221
|
+
const version = (body.version ? coerceVersion(body.version) : null) || "";
|
|
173
222
|
if (version && version === JWT_VERSION_REQUIRE_HASH_BEFORE_SIGN) return signers[alg].verify(hasher(msgHex), signature, signerPk);
|
|
174
223
|
return signers[alg].verify(msgHex, signature, signerPk);
|
|
175
224
|
}
|
|
@@ -194,6 +243,10 @@ const delegationTokenHeaders = {
|
|
|
194
243
|
[_ocap_mcrypto.types.KeyType.ETHEREUM]: {
|
|
195
244
|
alg: "Ethereum",
|
|
196
245
|
typ: "DelegationToken"
|
|
246
|
+
},
|
|
247
|
+
[_ocap_mcrypto.types.KeyType.PASSKEY]: {
|
|
248
|
+
alg: "Passkey",
|
|
249
|
+
typ: "DelegationToken"
|
|
197
250
|
}
|
|
198
251
|
};
|
|
199
252
|
async function signDelegationToken(signer, sk, payload) {
|
|
@@ -227,6 +280,45 @@ async function signDelegationToken(signer, sk, payload) {
|
|
|
227
280
|
(0, _ocap_util.toBase64)((0, _ocap_mcrypto.getSigner)(type.pk).sign(msgHash, sk))
|
|
228
281
|
].join(".");
|
|
229
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Create an unsigned delegation token for passkey two-phase signing.
|
|
285
|
+
* Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
|
|
286
|
+
*/
|
|
287
|
+
async function signDelegationTokenUnsigned(signer, pk, payload) {
|
|
288
|
+
if ((0, _arcblock_did.isValid)(signer) === false) throw new Error("Cannot sign DelegationToken with invalid signer");
|
|
289
|
+
const type = (0, _arcblock_did.toTypeInfo)(signer);
|
|
290
|
+
if (type.pk === void 0) throw new Error("Cannot determine key type from signer");
|
|
291
|
+
const header = delegationTokenHeaders[type.pk];
|
|
292
|
+
const headerB64 = (0, _ocap_util.toBase64)((0, json_stable_stringify.default)(header));
|
|
293
|
+
const delegationSignerMethod = (0, _arcblock_did.extractMethod)(signer) || "abt";
|
|
294
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
295
|
+
const pkHex = typeof pk === "string" ? pk : (0, _ocap_util.toHex)(pk);
|
|
296
|
+
const body = {
|
|
297
|
+
iss: (0, _arcblock_did.toDid)(signer, delegationSignerMethod),
|
|
298
|
+
sub: payload.sub,
|
|
299
|
+
iat: payload.iat || String(now),
|
|
300
|
+
nbf: payload.nbf || String(now),
|
|
301
|
+
exp: payload.exp || String(now + 300),
|
|
302
|
+
version: DELEGATION_TOKEN_VERSION,
|
|
303
|
+
ops: payload.ops,
|
|
304
|
+
pk: pkHex
|
|
305
|
+
};
|
|
306
|
+
if (payload.deny && payload.deny.length > 0) {
|
|
307
|
+
for (const d of payload.deny) if (!d.startsWith("fg:")) throw new Error(`Invalid deny pattern "${d}": must start with "fg:"`);
|
|
308
|
+
body.deny = payload.deny;
|
|
309
|
+
}
|
|
310
|
+
if (payload.delegation) body.delegation = payload.delegation;
|
|
311
|
+
return `${headerB64}.${(0, _ocap_util.toBase64)((0, json_stable_stringify.default)(body))}`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Assemble a complete delegation token from an unsigned token and a passkey assertion.
|
|
315
|
+
*/
|
|
316
|
+
function assembleDelegationToken(unsignedToken, assertion) {
|
|
317
|
+
if (!unsignedToken) throw new Error("Cannot assemble DelegationToken from empty token");
|
|
318
|
+
if (unsignedToken.split(".").length !== 2) throw new Error("Cannot assemble DelegationToken: expected unsigned token with exactly 2 parts (headerB64.bodyB64)");
|
|
319
|
+
if (!assertion || !assertion.authenticatorData || !assertion.clientDataJSON || !assertion.signature) throw new Error("Cannot assemble DelegationToken: assertion must contain authenticatorData, clientDataJSON, and signature");
|
|
320
|
+
return `${unsignedToken}.${(0, _ocap_util.toBase64)(JSON.stringify(assertion))}`;
|
|
321
|
+
}
|
|
230
322
|
function decodeDelegationToken(token) {
|
|
231
323
|
const parts = token.split(".");
|
|
232
324
|
if (parts.length !== 3) throw new Error("Invalid delegation token format: expected 3 parts");
|
|
@@ -269,13 +361,27 @@ async function verifyDelegationToken(token) {
|
|
|
269
361
|
debug$1("delegationToken.verify.error.futureIat");
|
|
270
362
|
return false;
|
|
271
363
|
}
|
|
364
|
+
const alg = header.alg.toLowerCase();
|
|
365
|
+
if (alg === "passkey") {
|
|
366
|
+
const [, , sigB64] = token.split(".");
|
|
367
|
+
const assertionRaw = JSON.parse((0, _ocap_util.fromBase64)(sigB64).toString());
|
|
368
|
+
if (!assertionRaw.authenticatorData || !assertionRaw.clientDataJSON || !assertionRaw.signature) {
|
|
369
|
+
debug$1("delegationToken.verify.error.incompletePasskeyAssertion");
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
const challenge = hasher((0, _ocap_util.toHex)(`${headerB64}.${bodyB64}`));
|
|
373
|
+
const extra = JSON.stringify({
|
|
374
|
+
authenticatorData: assertionRaw.authenticatorData,
|
|
375
|
+
clientDataJSON: assertionRaw.clientDataJSON
|
|
376
|
+
});
|
|
377
|
+
return await (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.PASSKEY).verify(challenge, assertionRaw.signature, body.pk, extra);
|
|
378
|
+
}
|
|
272
379
|
const signers = {
|
|
273
380
|
ed25519: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ED25519),
|
|
274
381
|
es256k: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
|
|
275
382
|
secp256k1: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.SECP256K1),
|
|
276
383
|
ethereum: (0, _ocap_mcrypto.getSigner)(_ocap_mcrypto.types.KeyType.ETHEREUM)
|
|
277
384
|
};
|
|
278
|
-
const alg = header.alg.toLowerCase();
|
|
279
385
|
if (!signers[alg]) {
|
|
280
386
|
debug$1("delegationToken.verify.error.unknownAlg");
|
|
281
387
|
return false;
|
|
@@ -293,11 +399,15 @@ function checkDelegationTokenScope(scope, { ops, deny }) {
|
|
|
293
399
|
}
|
|
294
400
|
|
|
295
401
|
//#endregion
|
|
402
|
+
exports.assemble = assemble;
|
|
403
|
+
exports.assembleDelegationToken = assembleDelegationToken;
|
|
296
404
|
exports.checkDelegationTokenScope = checkDelegationTokenScope;
|
|
297
405
|
exports.decode = decode;
|
|
298
406
|
exports.decodeDelegationToken = decodeDelegationToken;
|
|
407
|
+
exports.getChallenge = getChallenge;
|
|
299
408
|
exports.sign = sign;
|
|
300
409
|
exports.signDelegationToken = signDelegationToken;
|
|
410
|
+
exports.signDelegationTokenUnsigned = signDelegationTokenUnsigned;
|
|
301
411
|
exports.signV2 = signV2;
|
|
302
412
|
exports.verify = verify;
|
|
303
413
|
exports.verifyDelegationToken = verifyDelegationToken;
|
package/lib/index.d.cts
CHANGED
|
@@ -35,6 +35,28 @@ type JwtVerifyOptions = Partial<{
|
|
|
35
35
|
*/
|
|
36
36
|
declare function sign(signer: string, sk?: BytesType, payload?: {}, doSign?: boolean, version?: string): Promise<string>;
|
|
37
37
|
declare function signV2(signer: string, sk?: BytesType, payload?: any): Promise<string>;
|
|
38
|
+
type PasskeyAssertion = {
|
|
39
|
+
authenticatorData: string;
|
|
40
|
+
clientDataJSON: string;
|
|
41
|
+
signature: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Compute the WebAuthn challenge for an unsigned JWT token.
|
|
45
|
+
* Always uses SHA3 hash-before-sign (v1.1.0 semantics, required for passkey).
|
|
46
|
+
*
|
|
47
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64), may also accept 3-part tokens (ignores third segment)
|
|
48
|
+
* @returns hex-encoded SHA3 hash suitable as WebAuthn challenge
|
|
49
|
+
*/
|
|
50
|
+
declare function getChallenge(unsignedToken: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Assemble a complete JWT from an unsigned token and a passkey assertion.
|
|
53
|
+
* The assertion is JSON-serialized and base64-encoded as the JWT's third segment.
|
|
54
|
+
*
|
|
55
|
+
* @param unsignedToken - the unsigned token (headerB64.bodyB64)
|
|
56
|
+
* @param assertion - WebAuthn assertion with base64url-encoded fields
|
|
57
|
+
* @returns complete 3-part JWT string
|
|
58
|
+
*/
|
|
59
|
+
declare function assemble(unsignedToken: string, assertion: PasskeyAssertion): string;
|
|
38
60
|
declare function decode(token: string, bodyOnly?: true): JwtBody;
|
|
39
61
|
declare function decode(token: string, bodyOnly?: false): JwtToken;
|
|
40
62
|
/**
|
|
@@ -88,6 +110,23 @@ declare function signDelegationToken(signer: string, sk: BytesType, payload: {
|
|
|
88
110
|
nbf?: string;
|
|
89
111
|
exp?: string;
|
|
90
112
|
}): Promise<string>;
|
|
113
|
+
/**
|
|
114
|
+
* Create an unsigned delegation token for passkey two-phase signing.
|
|
115
|
+
* Unlike signDelegationToken, this does not sign (passkey has no sk), and pk is passed directly.
|
|
116
|
+
*/
|
|
117
|
+
declare function signDelegationTokenUnsigned(signer: string, pk: BytesType, payload: {
|
|
118
|
+
sub: string;
|
|
119
|
+
ops: string[];
|
|
120
|
+
deny?: string[];
|
|
121
|
+
delegation?: string;
|
|
122
|
+
iat?: string;
|
|
123
|
+
nbf?: string;
|
|
124
|
+
exp?: string;
|
|
125
|
+
}): Promise<string>;
|
|
126
|
+
/**
|
|
127
|
+
* Assemble a complete delegation token from an unsigned token and a passkey assertion.
|
|
128
|
+
*/
|
|
129
|
+
declare function assembleDelegationToken(unsignedToken: string, assertion: PasskeyAssertion): string;
|
|
91
130
|
declare function decodeDelegationToken(token: string): DelegationToken;
|
|
92
131
|
declare function verifyDelegationToken(token: string): Promise<boolean>;
|
|
93
132
|
declare function checkDelegationTokenScope(scope: string, {
|
|
@@ -98,4 +137,4 @@ declare function checkDelegationTokenScope(scope: string, {
|
|
|
98
137
|
deny?: string[];
|
|
99
138
|
}): boolean;
|
|
100
139
|
//#endregion
|
|
101
|
-
export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, checkDelegationTokenScope, decode, decodeDelegationToken, sign, signDelegationToken, signV2, verify, verifyDelegationToken };
|
|
140
|
+
export { DelegationToken, DelegationTokenBody, DelegationTokenHeader, JwtBody, JwtHeader, JwtToken, JwtVerifyOptions, PasskeyAssertion, assemble, assembleDelegationToken, checkDelegationTokenScope, decode, decodeDelegationToken, getChallenge, sign, signDelegationToken, signDelegationTokenUnsigned, signV2, verify, verifyDelegationToken };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@arcblock/jwt",
|
|
3
3
|
"description": "JSON Web Token variant for arcblock DID solutions",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.29.
|
|
5
|
+
"version": "1.29.23",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "wangshijun",
|
|
8
8
|
"email": "shijun@arcblock.io",
|
|
@@ -19,18 +19,16 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@arcblock/did": "1.29.
|
|
23
|
-
"@ocap/mcrypto": "1.29.
|
|
24
|
-
"@ocap/util": "1.29.
|
|
22
|
+
"@arcblock/did": "1.29.23",
|
|
23
|
+
"@ocap/mcrypto": "1.29.23",
|
|
24
|
+
"@ocap/util": "1.29.23",
|
|
25
25
|
"debug": "^4.4.3",
|
|
26
|
-
"json-stable-stringify": "^1.0.1"
|
|
27
|
-
"semver": "^7.6.3"
|
|
26
|
+
"json-stable-stringify": "^1.0.1"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|
|
30
|
-
"@ocap/wallet": "1.29.
|
|
29
|
+
"@ocap/wallet": "1.29.23",
|
|
31
30
|
"@types/json-stable-stringify": "^1.0.36",
|
|
32
31
|
"@types/node": "^22.7.5",
|
|
33
|
-
"@types/semver": "^7.5.8",
|
|
34
32
|
"tsdown": "^0.18.4",
|
|
35
33
|
"tslib": "^2.4.0"
|
|
36
34
|
},
|