wb-auth 0.3.0 → 0.4.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/dist/index.cjs +714 -200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +158 -69
- package/dist/index.d.ts +158 -69
- package/dist/index.js +714 -206
- package/dist/index.js.map +1 -1
- package/package.json +73 -77
package/dist/index.cjs
CHANGED
|
@@ -22,29 +22,372 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DEFAULT_KEY_PATH: () => DEFAULT_KEY_PATH,
|
|
24
24
|
Identity: () => Identity,
|
|
25
|
+
KEY_PASSWORD_ENV: () => KEY_PASSWORD_ENV,
|
|
26
|
+
NonceStore: () => NonceStore,
|
|
25
27
|
VERSION: () => VERSION,
|
|
28
|
+
VerificationError: () => VerificationError,
|
|
29
|
+
Verifier: () => Verifier,
|
|
26
30
|
applyTo: () => applyTo,
|
|
27
31
|
createSignedFetch: () => createSignedFetch,
|
|
28
|
-
|
|
32
|
+
createVerifierMiddleware: () => createVerifierMiddleware,
|
|
33
|
+
normalizeAuthority: () => normalizeAuthority,
|
|
34
|
+
sign: () => sign,
|
|
35
|
+
thumbprint: () => thumbprint,
|
|
36
|
+
validateSignatureAgentUrl: () => validateSignatureAgentUrl
|
|
29
37
|
});
|
|
30
38
|
module.exports = __toCommonJS(index_exports);
|
|
31
39
|
|
|
32
40
|
// src/signer.ts
|
|
33
|
-
var
|
|
41
|
+
var import_node_crypto2 = require("crypto");
|
|
42
|
+
|
|
43
|
+
// src/identity.ts
|
|
44
|
+
var import_node_crypto = require("crypto");
|
|
45
|
+
var import_promises = require("fs/promises");
|
|
46
|
+
var import_node_os = require("os");
|
|
47
|
+
var import_node_path = require("path");
|
|
48
|
+
var DEFAULT_KEY_PATH = (0, import_node_path.resolve)(
|
|
49
|
+
(0, import_node_os.homedir)(),
|
|
50
|
+
".config",
|
|
51
|
+
"wbauth",
|
|
52
|
+
"key.pem"
|
|
53
|
+
);
|
|
54
|
+
var KEY_PASSWORD_ENV = "WBAUTH_KEY_PASSWORD";
|
|
55
|
+
var KEYRING_ACTIVE = "active.pem";
|
|
56
|
+
var KEYRING_RETIRING = "retiring.pem";
|
|
57
|
+
var TEST_KEY_JWK = {
|
|
58
|
+
kty: "OKP",
|
|
59
|
+
crv: "Ed25519",
|
|
60
|
+
d: "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU",
|
|
61
|
+
x: "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
|
|
62
|
+
};
|
|
63
|
+
var warnedWindows = false;
|
|
64
|
+
function validateSignatureAgentUrl(url) {
|
|
65
|
+
if (typeof url !== "string" || !url.startsWith("https://")) {
|
|
66
|
+
throw new Error(`signatureAgentUrl must be https://, got: ${String(url)}`);
|
|
67
|
+
}
|
|
68
|
+
for (const ch of url) {
|
|
69
|
+
const c = ch.charCodeAt(0);
|
|
70
|
+
if (c < 33 || c > 126 || ch === '"' || ch === "\\") {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`signatureAgentUrl must be printable ASCII without spaces, quotes or backslashes, got: ${JSON.stringify(url)}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let host = "";
|
|
77
|
+
try {
|
|
78
|
+
host = new URL(url).hostname;
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
if (!host) throw new Error(`signatureAgentUrl has no host: ${url}`);
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
function validateUserAgent(ua) {
|
|
85
|
+
if (ua !== void 0 && [...ua].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`userAgent must not contain control characters: ${JSON.stringify(ua)}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return ua;
|
|
91
|
+
}
|
|
92
|
+
var Identity = class _Identity {
|
|
93
|
+
#active;
|
|
94
|
+
#retiring;
|
|
95
|
+
#signatureAgentUrl;
|
|
96
|
+
#userAgent;
|
|
97
|
+
constructor(active, opts, retiring = null) {
|
|
98
|
+
this.#signatureAgentUrl = validateSignatureAgentUrl(opts.signatureAgentUrl);
|
|
99
|
+
this.#userAgent = validateUserAgent(opts.userAgent);
|
|
100
|
+
this.#active = active;
|
|
101
|
+
this.#retiring = retiring;
|
|
102
|
+
}
|
|
103
|
+
// ---------- Public read-only API ----------
|
|
104
|
+
get signatureAgentUrl() {
|
|
105
|
+
return this.#signatureAgentUrl;
|
|
106
|
+
}
|
|
107
|
+
get userAgent() {
|
|
108
|
+
return this.#userAgent;
|
|
109
|
+
}
|
|
110
|
+
/** The active key's kid (used as `keyid=` param in Signature-Input). */
|
|
111
|
+
get kid() {
|
|
112
|
+
return this.#active.kid;
|
|
113
|
+
}
|
|
114
|
+
/** The active key's public JWK (copy; never contains private material). */
|
|
115
|
+
get publicJwk() {
|
|
116
|
+
return { ...this.#active.publicJwk };
|
|
117
|
+
}
|
|
118
|
+
/** Module-internal: returns the active Signer for sign() to use. */
|
|
119
|
+
_signer() {
|
|
120
|
+
return this.#active.signer;
|
|
121
|
+
}
|
|
122
|
+
/** Public JWKS: active first, then the retiring key (if any). */
|
|
123
|
+
exportJwks() {
|
|
124
|
+
const keys = [{ ...this.#active.publicJwk }];
|
|
125
|
+
if (this.#retiring) keys.push({ ...this.#retiring.publicJwk });
|
|
126
|
+
return { keys };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* IDENT-07: generate a new active key, demote current to retiring.
|
|
130
|
+
* In-memory only — use `rotateKeyring()` to persist. The default path is
|
|
131
|
+
* a fresh timestamped file next to DEFAULT_KEY_PATH (never collides).
|
|
132
|
+
*/
|
|
133
|
+
async rotate(newPath, keyOpts = {}) {
|
|
134
|
+
const path = newPath ? expandUser(newPath) : (0, import_node_path.join)(
|
|
135
|
+
(0, import_node_path.dirname)(DEFAULT_KEY_PATH),
|
|
136
|
+
`key-${Math.floor(Date.now() / 1e3)}.pem`
|
|
137
|
+
);
|
|
138
|
+
const newPair = await generateKeypairTo(path, keyOpts);
|
|
139
|
+
return new _Identity(newPair, this.#opts(), this.#active);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Persisted rotation: `<dir>/active.pem` → `retiring.pem` (replacing the
|
|
143
|
+
* previous retiring key), then a fresh `active.pem` is generated.
|
|
144
|
+
*/
|
|
145
|
+
async rotateKeyring(dir, keyOpts = {}) {
|
|
146
|
+
const d = expandUser(dir);
|
|
147
|
+
const active = (0, import_node_path.join)(d, KEYRING_ACTIVE);
|
|
148
|
+
const retiring = (0, import_node_path.join)(d, KEYRING_RETIRING);
|
|
149
|
+
await (0, import_promises.stat)(active);
|
|
150
|
+
await (0, import_promises.rename)(active, retiring);
|
|
151
|
+
let pair;
|
|
152
|
+
try {
|
|
153
|
+
pair = await generateKeypairTo(active, keyOpts);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
await (0, import_promises.rename)(retiring, active);
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
return new _Identity(
|
|
159
|
+
pair,
|
|
160
|
+
this.#opts(),
|
|
161
|
+
await loadKeypair(retiring, keyOpts)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
#opts() {
|
|
165
|
+
return {
|
|
166
|
+
signatureAgentUrl: this.#signatureAgentUrl,
|
|
167
|
+
userAgent: this.#userAgent
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// ---------- Constructors ----------
|
|
171
|
+
/**
|
|
172
|
+
* Loads `path` if it exists, generates a fresh key there otherwise.
|
|
173
|
+
* Concurrent first starts are safe: the loser of the O_EXCL race loads the
|
|
174
|
+
* winner's key. `retiringPath` optionally loads the previous key.
|
|
175
|
+
*/
|
|
176
|
+
static async loadOrGenerate(path = DEFAULT_KEY_PATH, opts) {
|
|
177
|
+
validateSignatureAgentUrl(opts.signatureAgentUrl);
|
|
178
|
+
const resolved = expandUser(path);
|
|
179
|
+
let pair = null;
|
|
180
|
+
if (!await exists(resolved)) {
|
|
181
|
+
try {
|
|
182
|
+
pair = await generateKeypairTo(resolved, opts);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (e.code !== "EEXIST" && !await exists(resolved))
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
pair ??= await loadKeypair(resolved, opts);
|
|
189
|
+
let retiring = null;
|
|
190
|
+
if (opts.retiringPath && await exists(expandUser(opts.retiringPath))) {
|
|
191
|
+
retiring = await loadKeypair(expandUser(opts.retiringPath), opts);
|
|
192
|
+
}
|
|
193
|
+
return new _Identity(pair, opts, retiring);
|
|
194
|
+
}
|
|
195
|
+
/** Load an existing key; never creates one. */
|
|
196
|
+
static async load(path, opts) {
|
|
197
|
+
return new _Identity(await loadKeypair(expandUser(path), opts), opts);
|
|
198
|
+
}
|
|
199
|
+
/** Generate a new key at `path`; never overwrites. */
|
|
200
|
+
static async generate(path, opts) {
|
|
201
|
+
validateSignatureAgentUrl(opts.signatureAgentUrl);
|
|
202
|
+
return new _Identity(await generateKeypairTo(expandUser(path), opts), opts);
|
|
203
|
+
}
|
|
204
|
+
/** `<dir>/active.pem` (+ `retiring.pem`), generating active on first use. */
|
|
205
|
+
static async loadKeyring(dir, opts) {
|
|
206
|
+
const d = expandUser(dir);
|
|
207
|
+
return _Identity.loadOrGenerate((0, import_node_path.join)(d, KEYRING_ACTIVE), {
|
|
208
|
+
...opts,
|
|
209
|
+
retiringPath: (0, import_node_path.join)(d, KEYRING_RETIRING)
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Wrap an external signer (KMS, HSM, keychain). */
|
|
213
|
+
static async fromSigner(backend, opts) {
|
|
214
|
+
const jwk = backend.publicJwk;
|
|
215
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || !jwk.x) {
|
|
216
|
+
throw new TypeError("fromSigner: publicJwk must be an Ed25519 OKP JWK");
|
|
217
|
+
}
|
|
218
|
+
const kid = thumbprint(jwk);
|
|
219
|
+
const enc = new TextEncoder();
|
|
220
|
+
return new _Identity(
|
|
221
|
+
{
|
|
222
|
+
kid,
|
|
223
|
+
publicJwk: { kty: "OKP", crv: "Ed25519", x: jwk.x, kid },
|
|
224
|
+
signer: {
|
|
225
|
+
alg: "ed25519",
|
|
226
|
+
keyid: kid,
|
|
227
|
+
sign: async (data) => backend.sign(enc.encode(data))
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
opts
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
/** RFC 9421 Appendix B.1.4 test key. NEVER use in production. */
|
|
234
|
+
static async fromTestKey(signatureAgentUrl, opts = {}) {
|
|
235
|
+
const key = (0, import_node_crypto.createPrivateKey)({ key: TEST_KEY_JWK, format: "jwk" });
|
|
236
|
+
return new _Identity(keyPairFromKeyObject(key), {
|
|
237
|
+
...opts,
|
|
238
|
+
signatureAgentUrl
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// ---------- Redaction guarantees (IDENT-08) ----------
|
|
242
|
+
toString() {
|
|
243
|
+
return `<Identity REDACTED kid='${this.kid}' sig_agent='${this.#signatureAgentUrl}'>`;
|
|
244
|
+
}
|
|
245
|
+
/** JSON.stringify(identity) → public data only. */
|
|
246
|
+
toJSON() {
|
|
247
|
+
return {
|
|
248
|
+
kid: this.kid,
|
|
249
|
+
signatureAgentUrl: this.#signatureAgentUrl,
|
|
250
|
+
...this.#userAgent !== void 0 ? { userAgent: this.#userAgent } : {},
|
|
251
|
+
jwks: this.exportJwks()
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** util.inspect / console.log render the same REDACTED form (IDENT-08). */
|
|
255
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
256
|
+
return this.toString();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
function expandUser(path) {
|
|
260
|
+
if (path === "~") return (0, import_node_os.homedir)();
|
|
261
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) {
|
|
262
|
+
return (0, import_node_path.resolve)((0, import_node_os.homedir)(), path.slice(2));
|
|
263
|
+
}
|
|
264
|
+
return (0, import_node_path.resolve)(path);
|
|
265
|
+
}
|
|
266
|
+
async function exists(path) {
|
|
267
|
+
try {
|
|
268
|
+
await (0, import_promises.stat)(path);
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function b64url(buf) {
|
|
275
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
276
|
+
}
|
|
277
|
+
function thumbprint(jwk) {
|
|
278
|
+
const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x });
|
|
279
|
+
return b64url((0, import_node_crypto.createHash)("sha256").update(canonical).digest());
|
|
280
|
+
}
|
|
281
|
+
function keyPairFromKeyObject(privateKey) {
|
|
282
|
+
const pub = (0, import_node_crypto.createPublicKey)(privateKey).export({
|
|
283
|
+
format: "jwk"
|
|
284
|
+
});
|
|
285
|
+
if (pub.kty !== "OKP" || pub.crv !== "Ed25519") {
|
|
286
|
+
throw new TypeError(`Expected Ed25519, got kty=${pub.kty}, crv=${pub.crv}`);
|
|
287
|
+
}
|
|
288
|
+
const kid = thumbprint(pub);
|
|
289
|
+
return {
|
|
290
|
+
kid,
|
|
291
|
+
publicJwk: { kty: "OKP", crv: "Ed25519", x: pub.x, kid },
|
|
292
|
+
signer: {
|
|
293
|
+
alg: "ed25519",
|
|
294
|
+
keyid: kid,
|
|
295
|
+
// Sync node:crypto Ed25519 — no WebCrypto import / promise hop.
|
|
296
|
+
sign: async (data) => new Uint8Array((0, import_node_crypto.sign)(null, Buffer.from(data, "utf8"), privateKey))
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function passwordFor(opts) {
|
|
301
|
+
return opts.password ?? (process.env[KEY_PASSWORD_ENV] || void 0);
|
|
302
|
+
}
|
|
303
|
+
async function loadKeypair(path, opts = {}) {
|
|
304
|
+
if (process.platform !== "win32") {
|
|
305
|
+
const mode = (await (0, import_promises.stat)(path)).mode & 511;
|
|
306
|
+
if ((mode & 63) !== 0) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
} else if (!warnedWindows) {
|
|
312
|
+
warnedWindows = true;
|
|
313
|
+
process.emitWarning(
|
|
314
|
+
`Windows detected \u2014 file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const pem = await (0, import_promises.readFile)(path, "utf8");
|
|
318
|
+
const encrypted = pem.includes("ENCRYPTED");
|
|
319
|
+
const passphrase = passwordFor(opts);
|
|
320
|
+
if (encrypted && !passphrase) {
|
|
321
|
+
throw new TypeError(
|
|
322
|
+
`Key file ${path} is encrypted; pass { password } or set $${KEY_PASSWORD_ENV}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const key = (0, import_node_crypto.createPrivateKey)(
|
|
326
|
+
encrypted ? { key: pem, format: "pem", passphrase } : pem
|
|
327
|
+
);
|
|
328
|
+
return keyPairFromKeyObject(key);
|
|
329
|
+
}
|
|
330
|
+
async function generateKeypairTo(path, opts = {}) {
|
|
331
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
332
|
+
if (await exists(path)) {
|
|
333
|
+
const err = new Error(
|
|
334
|
+
`Key already exists at ${path}; refuse overwrite`
|
|
335
|
+
);
|
|
336
|
+
err.code = "EEXIST";
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
const { privateKey } = (0, import_node_crypto.generateKeyPairSync)("ed25519");
|
|
340
|
+
const passphrase = passwordFor(opts);
|
|
341
|
+
const pem = privateKey.export(
|
|
342
|
+
passphrase ? { format: "pem", type: "pkcs8", cipher: "aes-256-cbc", passphrase } : { format: "pem", type: "pkcs8" }
|
|
343
|
+
);
|
|
344
|
+
const fh = await (0, import_promises.open)(path, "wx", 384);
|
|
345
|
+
try {
|
|
346
|
+
await fh.writeFile(pem);
|
|
347
|
+
await fh.sync();
|
|
348
|
+
} finally {
|
|
349
|
+
await fh.close();
|
|
350
|
+
}
|
|
351
|
+
return keyPairFromKeyObject(privateKey);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/signer.ts
|
|
355
|
+
var WEB_BOT_AUTH_TAG = "web-bot-auth";
|
|
34
356
|
var DEFAULT_LABEL = "sig1";
|
|
35
357
|
var DEFAULT_EXPIRES_SECONDS = 60;
|
|
36
358
|
var MAX_EXPIRES_SECONDS = 3600;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
359
|
+
var NONCE_BYTES = 64;
|
|
360
|
+
var SF_KEY_RE = /^[a-z*][a-z0-9_\-.*]*$/;
|
|
361
|
+
var DEFAULT_PORTS = {
|
|
362
|
+
"https:": "443",
|
|
363
|
+
"http:": "80"
|
|
364
|
+
};
|
|
365
|
+
function sfString(value) {
|
|
366
|
+
for (const ch of value) {
|
|
367
|
+
const c = ch.charCodeAt(0);
|
|
368
|
+
if (c < 32 || c > 126) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`value is not a valid structured-field string: ${JSON.stringify(value)}`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
41
373
|
}
|
|
42
|
-
return
|
|
374
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
375
|
+
}
|
|
376
|
+
function normalizeAuthority(url) {
|
|
377
|
+
const u = new URL(url);
|
|
378
|
+
if (!u.hostname) throw new Error(`URL has no host: ${url}`);
|
|
379
|
+
const host = u.hostname.toLowerCase();
|
|
380
|
+
const port = u.port && u.port !== DEFAULT_PORTS[u.protocol] ? `:${u.port}` : "";
|
|
381
|
+
return `${host}${port}`;
|
|
382
|
+
}
|
|
383
|
+
function componentsFor(hasBody) {
|
|
384
|
+
return hasBody ? ["@authority", "signature-agent", "content-digest"] : ["@authority", "signature-agent"];
|
|
43
385
|
}
|
|
44
386
|
var RESIGN_HEADERS = ["signature", "signature-input", "signature-agent"];
|
|
45
|
-
function
|
|
387
|
+
function getHeader(headers, name) {
|
|
46
388
|
const n = name.toLowerCase();
|
|
47
|
-
|
|
389
|
+
const k = Object.keys(headers).find((key) => key.toLowerCase() === n);
|
|
390
|
+
return k === void 0 ? void 0 : headers[k];
|
|
48
391
|
}
|
|
49
392
|
function removeHeaders(headers, names) {
|
|
50
393
|
for (const k of Object.keys(headers)) {
|
|
@@ -52,57 +395,54 @@ function removeHeaders(headers, names) {
|
|
|
52
395
|
}
|
|
53
396
|
}
|
|
54
397
|
async function sign(request, identity, opts = {}) {
|
|
55
|
-
|
|
56
|
-
throw new Error(
|
|
57
|
-
`signatureAgentUrl must be https://, got: ${identity.signatureAgentUrl}`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
398
|
+
validateSignatureAgentUrl(identity.signatureAgentUrl);
|
|
60
399
|
const expiresAfterSeconds = opts.expiresAfterSeconds ?? DEFAULT_EXPIRES_SECONDS;
|
|
61
400
|
if (typeof expiresAfterSeconds !== "number" || !Number.isFinite(expiresAfterSeconds) || expiresAfterSeconds <= 0 || expiresAfterSeconds > MAX_EXPIRES_SECONDS) {
|
|
62
401
|
throw new RangeError(
|
|
63
402
|
`expiresAfterSeconds must be a number in (0, ${MAX_EXPIRES_SECONDS}], got: ${expiresAfterSeconds}`
|
|
64
403
|
);
|
|
65
404
|
}
|
|
66
|
-
removeHeaders(request.headers, RESIGN_HEADERS);
|
|
67
|
-
const signatureAgentHeader = `"${identity.signatureAgentUrl}"`;
|
|
68
|
-
request.headers["Signature-Agent"] = signatureAgentHeader;
|
|
69
|
-
const created = opts.created ?? /* @__PURE__ */ new Date();
|
|
70
|
-
const expires = new Date(created.getTime() + expiresAfterSeconds * 1e3);
|
|
71
405
|
const label = opts.label ?? DEFAULT_LABEL;
|
|
72
|
-
|
|
73
|
-
const components = componentsFor(request.method, hasBody);
|
|
74
|
-
if (components.includes("content-digest") && !hasHeader(request.headers, "content-digest")) {
|
|
406
|
+
if (!SF_KEY_RE.test(label)) {
|
|
75
407
|
throw new Error(
|
|
76
|
-
|
|
408
|
+
`label must be an RFC 8941 key, got: ${JSON.stringify(label)}`
|
|
77
409
|
);
|
|
78
410
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
key: label,
|
|
91
|
-
components,
|
|
92
|
-
...opts.nonce !== void 0 ? { nonce: opts.nonce } : {}
|
|
93
|
-
};
|
|
94
|
-
const result = await (0, import_web_bot_auth.signatureHeaders)(req, identity._signer(), params);
|
|
95
|
-
request.headers.Signature = result.Signature;
|
|
96
|
-
request.headers["Signature-Input"] = result["Signature-Input"];
|
|
97
|
-
return {
|
|
98
|
-
signature: result.Signature,
|
|
99
|
-
signatureInput: result["Signature-Input"],
|
|
100
|
-
signatureAgent: signatureAgentHeader
|
|
411
|
+
removeHeaders(request.headers, RESIGN_HEADERS);
|
|
412
|
+
const signatureAgentHeader = sfString(identity.signatureAgentUrl);
|
|
413
|
+
request.headers["Signature-Agent"] = signatureAgentHeader;
|
|
414
|
+
const created = Math.floor((opts.created ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
415
|
+
const expires = created + Math.floor(expiresAfterSeconds);
|
|
416
|
+
const nonce = opts.nonce ?? (0, import_node_crypto2.randomBytes)(NONCE_BYTES).toString("base64");
|
|
417
|
+
const hasBody = request.body != null && request.body.byteLength > 0;
|
|
418
|
+
const components = componentsFor(hasBody);
|
|
419
|
+
const values = {
|
|
420
|
+
"@authority": normalizeAuthority(request.url),
|
|
421
|
+
"signature-agent": signatureAgentHeader
|
|
101
422
|
};
|
|
423
|
+
if (hasBody) {
|
|
424
|
+
const cd = getHeader(request.headers, "content-digest");
|
|
425
|
+
if (cd === void 0) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
'Covered header field "content-digest" not found in the message. Call ensureContentDigest() or use createSignedFetch()/applyTo(), which add it automatically.'
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
values["content-digest"] = cd.trim();
|
|
431
|
+
}
|
|
432
|
+
const signer = identity._signer();
|
|
433
|
+
const params = `(${components.map((c) => `"${c}"`).join(" ")});created=${created};keyid=${sfString(signer.keyid)};alg="ed25519";expires=${expires};nonce=${sfString(nonce)};tag=${sfString(WEB_BOT_AUTH_TAG)}`;
|
|
434
|
+
const base = `${components.map((c) => `"${c}": ${values[c]}`).join("\n")}
|
|
435
|
+
"@signature-params": ${params}`;
|
|
436
|
+
const raw = await signer.sign(base);
|
|
437
|
+
const signatureInput = `${label}=${params}`;
|
|
438
|
+
const signature = `${label}=:${Buffer.from(raw).toString("base64")}:`;
|
|
439
|
+
request.headers["Signature-Input"] = signatureInput;
|
|
440
|
+
request.headers.Signature = signature;
|
|
441
|
+
return { signature, signatureInput, signatureAgent: signatureAgentHeader };
|
|
102
442
|
}
|
|
103
443
|
|
|
104
444
|
// src/adapters/_utils.ts
|
|
105
|
-
var
|
|
445
|
+
var import_node_crypto3 = require("crypto");
|
|
106
446
|
var LIBRARY_DEFAULT_UA = /^(node|undici|node-fetch)(\/|$)/i;
|
|
107
447
|
function findHeader(headers, name) {
|
|
108
448
|
const n = name.toLowerCase();
|
|
@@ -111,10 +451,41 @@ function findHeader(headers, name) {
|
|
|
111
451
|
function ensureContentDigest(method, headers, body) {
|
|
112
452
|
if (!body || body.byteLength === 0) return;
|
|
113
453
|
void method;
|
|
114
|
-
|
|
115
|
-
const
|
|
454
|
+
const b64 = (0, import_node_crypto3.createHash)("sha256").update(body).digest("base64");
|
|
455
|
+
const key = findHeader(headers, "content-digest");
|
|
456
|
+
if (key !== void 0) {
|
|
457
|
+
const m = /(?:^|,)\s*sha-256=:([A-Za-z0-9+/=]+):/.exec(headers[key] ?? "");
|
|
458
|
+
if (m === null || m[1] === b64) return;
|
|
459
|
+
delete headers[key];
|
|
460
|
+
}
|
|
116
461
|
headers["Content-Digest"] = `sha-256=:${b64}:`;
|
|
117
462
|
}
|
|
463
|
+
var SIGNATURE_HEADERS = /* @__PURE__ */ new Set([
|
|
464
|
+
"signature",
|
|
465
|
+
"signature-input",
|
|
466
|
+
"signature-agent"
|
|
467
|
+
]);
|
|
468
|
+
function stripSignatureHeaders(headers) {
|
|
469
|
+
for (const k of Object.keys(headers)) {
|
|
470
|
+
if (SIGNATURE_HEADERS.has(k.toLowerCase())) delete headers[k];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function makeSignDecider(opts = {}) {
|
|
474
|
+
return (raw) => {
|
|
475
|
+
let u;
|
|
476
|
+
try {
|
|
477
|
+
u = new URL(raw);
|
|
478
|
+
} catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") return false;
|
|
482
|
+
if (!u.hostname) return false;
|
|
483
|
+
if (u.protocol === "http:" && !opts.allowInsecure) return false;
|
|
484
|
+
if (opts.shouldSign) return opts.shouldSign(u);
|
|
485
|
+
if (opts.hosts) return hostMatches(u.hostname, opts.hosts);
|
|
486
|
+
return true;
|
|
487
|
+
};
|
|
488
|
+
}
|
|
118
489
|
function applyUserAgent(headers, userAgent) {
|
|
119
490
|
if (!userAgent) return;
|
|
120
491
|
const key = findHeader(headers, "user-agent");
|
|
@@ -160,6 +531,7 @@ function dropHeaders(headers, names) {
|
|
|
160
531
|
}
|
|
161
532
|
}
|
|
162
533
|
function createSignedFetch(identity, options = {}) {
|
|
534
|
+
const decide = makeSignDecider(options);
|
|
163
535
|
return async function signedFetch(input, init = {}) {
|
|
164
536
|
const req = new Request(input, init);
|
|
165
537
|
let method = req.method;
|
|
@@ -179,8 +551,12 @@ function createSignedFetch(identity, options = {}) {
|
|
|
179
551
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
180
552
|
for (let hop = 0; ; hop++) {
|
|
181
553
|
const headers = { ...baseHeaders };
|
|
182
|
-
|
|
183
|
-
|
|
554
|
+
if (decide(url)) {
|
|
555
|
+
ensureContentDigest(method, headers, body);
|
|
556
|
+
await sign({ method, url, headers, body }, identity);
|
|
557
|
+
} else {
|
|
558
|
+
stripSignatureHeaders(headers);
|
|
559
|
+
}
|
|
184
560
|
applyUserAgent(headers, identity.userAgent);
|
|
185
561
|
const outInit = {
|
|
186
562
|
...init,
|
|
@@ -227,19 +603,8 @@ function createSignedFetch(identity, options = {}) {
|
|
|
227
603
|
// src/adapters/playwright.ts
|
|
228
604
|
async function applyTo(page, identity, options = {}) {
|
|
229
605
|
const pattern = options.pattern ?? "**/*";
|
|
230
|
-
const decide = (
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
u = new URL(raw);
|
|
234
|
-
} catch {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
if (u.protocol !== "https:" && u.protocol !== "http:") return false;
|
|
238
|
-
if (options.shouldSign) return options.shouldSign(u);
|
|
239
|
-
if (options.hosts) return hostMatches(u.hostname, options.hosts);
|
|
240
|
-
return true;
|
|
241
|
-
};
|
|
242
|
-
await page.route(pattern, async (route, request) => {
|
|
606
|
+
const decide = makeSignDecider(options);
|
|
607
|
+
const handler = async (route, request) => {
|
|
243
608
|
const url = request.url();
|
|
244
609
|
if (!decide(url)) {
|
|
245
610
|
const r = route;
|
|
@@ -253,168 +618,317 @@ async function applyTo(page, identity, options = {}) {
|
|
|
253
618
|
await sign({ method: request.method(), url, headers, body }, identity);
|
|
254
619
|
applyUserAgent(headers, identity.userAgent);
|
|
255
620
|
await route.continue({ headers });
|
|
256
|
-
}
|
|
621
|
+
};
|
|
622
|
+
await page.route(pattern, handler);
|
|
623
|
+
return async () => {
|
|
624
|
+
await page.unroute(pattern, handler);
|
|
625
|
+
};
|
|
257
626
|
}
|
|
258
627
|
|
|
259
|
-
// src/
|
|
260
|
-
var
|
|
261
|
-
var
|
|
262
|
-
var
|
|
263
|
-
var
|
|
264
|
-
|
|
265
|
-
var DEFAULT_KEY_PATH = (0, import_node_path.resolve)(
|
|
266
|
-
(0, import_node_os.homedir)(),
|
|
267
|
-
".config",
|
|
268
|
-
"wbauth",
|
|
269
|
-
"key.pem"
|
|
270
|
-
);
|
|
271
|
-
var TEST_KEY_JWK = {
|
|
272
|
-
kty: "OKP",
|
|
273
|
-
crv: "Ed25519",
|
|
274
|
-
d: "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU",
|
|
275
|
-
x: "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
|
|
628
|
+
// src/verifier.ts
|
|
629
|
+
var import_node_crypto4 = require("crypto");
|
|
630
|
+
var DIRECTORY_PATH = "/.well-known/http-message-signatures-directory";
|
|
631
|
+
var DIRECTORY_MAX_BYTES = 64 * 1024;
|
|
632
|
+
var VerificationError = class extends Error {
|
|
633
|
+
name = "VerificationError";
|
|
276
634
|
};
|
|
277
|
-
var
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
signatureAgentUrl;
|
|
281
|
-
userAgent;
|
|
282
|
-
constructor(active, opts, retiring = null) {
|
|
283
|
-
if (!opts.signatureAgentUrl.startsWith("https://")) {
|
|
284
|
-
throw new Error(
|
|
285
|
-
`signatureAgentUrl must be https://, got: ${opts.signatureAgentUrl}`
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
this._active = active;
|
|
289
|
-
this._retiring = retiring;
|
|
290
|
-
this.signatureAgentUrl = opts.signatureAgentUrl;
|
|
291
|
-
this.userAgent = opts.userAgent;
|
|
635
|
+
var NonceStore = class {
|
|
636
|
+
constructor(maxSize = 1e5) {
|
|
637
|
+
this.maxSize = maxSize;
|
|
292
638
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
639
|
+
maxSize;
|
|
640
|
+
#seen = /* @__PURE__ */ new Map();
|
|
641
|
+
seen(nonce, expiresAt) {
|
|
642
|
+
const now = Date.now() / 1e3;
|
|
643
|
+
if (this.#seen.size >= this.maxSize) {
|
|
644
|
+
for (const [n, e] of this.#seen) if (e <= now) this.#seen.delete(n);
|
|
645
|
+
for (const n of [...this.#seen.keys()].slice(
|
|
646
|
+
0,
|
|
647
|
+
Math.ceil(this.maxSize / 10)
|
|
648
|
+
)) {
|
|
649
|
+
if (this.#seen.size < this.maxSize) break;
|
|
650
|
+
this.#seen.delete(n);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const prev = this.#seen.get(nonce);
|
|
654
|
+
if (prev !== void 0 && prev > now) return true;
|
|
655
|
+
this.#seen.set(nonce, expiresAt);
|
|
656
|
+
return false;
|
|
301
657
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
658
|
+
};
|
|
659
|
+
var SF_STRING = String.raw`"(?:[^"\\]|\\.)*"`;
|
|
660
|
+
var PARAM = String.raw`;\s*([a-z*][a-z0-9_.*-]*)(?:=(${SF_STRING}|-?\d+(?:\.\d+)?|\?[01]|[A-Za-z*][^;,\s]*))?`;
|
|
661
|
+
var SIG_INPUT_RE = new RegExp(
|
|
662
|
+
String.raw`\s*([a-z*][a-z0-9_.*-]*)=(\((?:\s*${SF_STRING}(?:;[^\s)"]+)*)*\s*\))((?:${PARAM})*)`,
|
|
663
|
+
"y"
|
|
664
|
+
);
|
|
665
|
+
var unquote = (v) => v.startsWith('"') ? v.slice(1, -1).replace(/\\(.)/g, "$1") : v;
|
|
666
|
+
function parseSignatureInput(value) {
|
|
667
|
+
const out = [];
|
|
668
|
+
let pos = 0;
|
|
669
|
+
while (pos < value.length) {
|
|
670
|
+
SIG_INPUT_RE.lastIndex = pos;
|
|
671
|
+
const m = SIG_INPUT_RE.exec(value);
|
|
672
|
+
if (!m) throw new VerificationError("malformed Signature-Input");
|
|
673
|
+
const [, label = "", inner = "", rest = ""] = m;
|
|
674
|
+
if (/"\s*;/.test(inner))
|
|
675
|
+
throw new VerificationError("component parameters are not supported");
|
|
676
|
+
const params = {};
|
|
677
|
+
for (const p of rest.matchAll(new RegExp(PARAM, "g"))) {
|
|
678
|
+
const k = p[1];
|
|
679
|
+
const v = p[2];
|
|
680
|
+
params[k] = v === void 0 ? true : v.startsWith('"') ? unquote(v) : /^-?\d+$/.test(v) ? Number(v) : v;
|
|
681
|
+
}
|
|
682
|
+
out.push({
|
|
683
|
+
label,
|
|
684
|
+
rawParams: inner + rest,
|
|
685
|
+
components: [...inner.matchAll(new RegExp(SF_STRING, "g"))].map(
|
|
686
|
+
(x) => unquote(x[0])
|
|
687
|
+
),
|
|
688
|
+
params
|
|
689
|
+
});
|
|
690
|
+
pos = SIG_INPUT_RE.lastIndex;
|
|
691
|
+
while (pos < value.length && (value[pos] === " " || value[pos] === ","))
|
|
692
|
+
pos++;
|
|
313
693
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return new _Identity(
|
|
323
|
-
newPair,
|
|
324
|
-
{ signatureAgentUrl: this.signatureAgentUrl, userAgent: this.userAgent },
|
|
325
|
-
this._active
|
|
326
|
-
);
|
|
694
|
+
return out;
|
|
695
|
+
}
|
|
696
|
+
function parseSignature(value) {
|
|
697
|
+
const out = /* @__PURE__ */ new Map();
|
|
698
|
+
for (const m of value.matchAll(
|
|
699
|
+
/\s*([a-z*][a-z0-9_.*-]*)=:([A-Za-z0-9+/=]*):/g
|
|
700
|
+
)) {
|
|
701
|
+
out.set(m[1], Buffer.from(m[2], "base64"));
|
|
327
702
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
function componentValue(name, req, headers, authority) {
|
|
706
|
+
const u = new URL(req.url);
|
|
707
|
+
switch (name) {
|
|
708
|
+
case "@authority":
|
|
709
|
+
return authority ?? normalizeAuthority(req.url);
|
|
710
|
+
case "@method":
|
|
711
|
+
return req.method.toUpperCase();
|
|
712
|
+
case "@scheme":
|
|
713
|
+
return u.protocol.slice(0, -1);
|
|
714
|
+
case "@target-uri":
|
|
715
|
+
return u.href;
|
|
716
|
+
case "@path":
|
|
717
|
+
return u.pathname || "/";
|
|
718
|
+
case "@query":
|
|
719
|
+
return u.search || "?";
|
|
338
720
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
721
|
+
if (name.startsWith("@"))
|
|
722
|
+
throw new VerificationError(`unsupported derived component ${name}`);
|
|
723
|
+
const v = headers.get(name);
|
|
724
|
+
if (v === null) throw new VerificationError(`covered header ${name} missing`);
|
|
725
|
+
return v.trim();
|
|
726
|
+
}
|
|
727
|
+
function directoryUrl(signatureAgent) {
|
|
728
|
+
const u = new URL(signatureAgent);
|
|
729
|
+
if (u.protocol !== "https:")
|
|
730
|
+
throw new VerificationError("Signature-Agent must be https://");
|
|
731
|
+
if (u.pathname === "/" || u.pathname === "") {
|
|
732
|
+
u.pathname = DIRECTORY_PATH;
|
|
733
|
+
u.search = "";
|
|
347
734
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
735
|
+
u.hash = "";
|
|
736
|
+
return u.href;
|
|
737
|
+
}
|
|
738
|
+
var Verifier = class {
|
|
739
|
+
#opts;
|
|
740
|
+
#nonces;
|
|
741
|
+
#jwks = /* @__PURE__ */ new Map();
|
|
742
|
+
constructor(opts = {}) {
|
|
743
|
+
this.#opts = opts;
|
|
744
|
+
this.#nonces = opts.nonceStore ?? new NonceStore();
|
|
351
745
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
746
|
+
async #directoryKeys(url) {
|
|
747
|
+
const hit = this.#jwks.get(url);
|
|
748
|
+
if (hit && hit.until > Date.now()) return hit.keys;
|
|
749
|
+
const f = this.#opts.fetch ?? globalThis.fetch;
|
|
750
|
+
let res;
|
|
751
|
+
try {
|
|
752
|
+
res = await f(url, {
|
|
753
|
+
headers: {
|
|
754
|
+
accept: "application/http-message-signatures-directory+json, application/json"
|
|
755
|
+
},
|
|
756
|
+
redirect: "error",
|
|
757
|
+
signal: AbortSignal.timeout(3e3)
|
|
758
|
+
});
|
|
759
|
+
} catch (e) {
|
|
760
|
+
throw new VerificationError(`directory unreachable: ${String(e)}`);
|
|
761
|
+
}
|
|
762
|
+
if (res.status !== 200)
|
|
763
|
+
throw new VerificationError(`directory ${url} answered ${res.status}`);
|
|
764
|
+
const buf = await res.arrayBuffer();
|
|
765
|
+
if (buf.byteLength > DIRECTORY_MAX_BYTES)
|
|
766
|
+
throw new VerificationError("directory too large");
|
|
767
|
+
let keys;
|
|
768
|
+
try {
|
|
769
|
+
keys = JSON.parse(new TextDecoder().decode(buf)).keys;
|
|
770
|
+
} catch {
|
|
771
|
+
throw new VerificationError("directory is not a JWKS");
|
|
772
|
+
}
|
|
773
|
+
if (!Array.isArray(keys))
|
|
774
|
+
throw new VerificationError("directory is not a JWKS");
|
|
775
|
+
this.#jwks.set(url, {
|
|
776
|
+
until: Date.now() + (this.#opts.jwksTtlSeconds ?? 300) * 1e3,
|
|
777
|
+
keys
|
|
778
|
+
});
|
|
779
|
+
return keys;
|
|
355
780
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
crv: privateJwk.crv,
|
|
366
|
-
x: privateJwk.x,
|
|
367
|
-
kid
|
|
368
|
-
};
|
|
369
|
-
return { privateJwk, publicJwk, kid, signer };
|
|
370
|
-
}
|
|
371
|
-
async function loadKeypair(path) {
|
|
372
|
-
if (process.platform !== "win32") {
|
|
373
|
-
const mode = (0, import_node_fs.statSync)(path).mode & 511;
|
|
374
|
-
if ((mode & 63) !== 0) {
|
|
375
|
-
throw new Error(
|
|
376
|
-
`Key file ${path} has mode 0o${mode.toString(8)}; expected 0o600. Fix: chmod 600 ${path}`
|
|
781
|
+
async #resolve(kid, signatureAgent) {
|
|
782
|
+
if (this.#opts.keyResolver) {
|
|
783
|
+
const jwk = await this.#opts.keyResolver(kid, signatureAgent);
|
|
784
|
+
if (!jwk) throw new VerificationError(`unknown keyid ${kid}`);
|
|
785
|
+
return jwk;
|
|
786
|
+
}
|
|
787
|
+
if (!signatureAgent)
|
|
788
|
+
throw new VerificationError(
|
|
789
|
+
"no Signature-Agent and no keyResolver configured"
|
|
377
790
|
);
|
|
791
|
+
for (const k of await this.#directoryKeys(directoryUrl(signatureAgent))) {
|
|
792
|
+
if (k && k.kty === "OKP" && k.crv === "Ed25519" && k.x && thumbprint(k) === kid)
|
|
793
|
+
return k;
|
|
378
794
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
`Windows detected \u2014 file permissions on ${path} cannot be enforced via POSIX mode. Use NTFS ACLs or store the key in a per-user secrets vault.`
|
|
795
|
+
throw new VerificationError(
|
|
796
|
+
`keyid ${kid} not published by ${signatureAgent}`
|
|
382
797
|
);
|
|
383
798
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
(
|
|
799
|
+
/**
|
|
800
|
+
* Verify a WHATWG Request (body is read from a clone). `authority`
|
|
801
|
+
* overrides the value derived from the URL (e.g. behind a proxy).
|
|
802
|
+
*/
|
|
803
|
+
async verify(request, opts = {}) {
|
|
804
|
+
const h = request.headers;
|
|
805
|
+
const sigInput = h.get("signature-input");
|
|
806
|
+
const sigHeader = h.get("signature");
|
|
807
|
+
if (!sigInput || !sigHeader)
|
|
808
|
+
throw new VerificationError("request is not signed");
|
|
809
|
+
const sigs = parseSignature(sigHeader);
|
|
810
|
+
const cand = parseSignatureInput(sigInput).find(
|
|
811
|
+
(c) => c.params.tag === WEB_BOT_AUTH_TAG
|
|
812
|
+
);
|
|
813
|
+
if (!cand)
|
|
814
|
+
throw new VerificationError('no signature with tag="web-bot-auth"');
|
|
815
|
+
const { label, rawParams, components, params } = cand;
|
|
816
|
+
const raw = sigs.get(label);
|
|
817
|
+
if (!raw) throw new VerificationError(`Signature has no ${label} member`);
|
|
818
|
+
if ((params.alg ?? "ed25519") !== "ed25519")
|
|
819
|
+
throw new VerificationError("only alg=ed25519 is supported");
|
|
820
|
+
const { created, expires, keyid } = params;
|
|
821
|
+
if (typeof created !== "number" || typeof expires !== "number" || typeof keyid !== "string") {
|
|
822
|
+
throw new VerificationError("created, expires and keyid are required");
|
|
823
|
+
}
|
|
824
|
+
const skew = this.#opts.maxSkew ?? 5;
|
|
825
|
+
const now = opts.now ?? Date.now() / 1e3;
|
|
826
|
+
if (created > now + skew)
|
|
827
|
+
throw new VerificationError("signature created in the future");
|
|
828
|
+
if (expires < now - skew) throw new VerificationError("signature expired");
|
|
829
|
+
if (expires <= created || expires - created > MAX_EXPIRES_SECONDS) {
|
|
830
|
+
throw new VerificationError("invalid validity window");
|
|
831
|
+
}
|
|
832
|
+
if (!components.includes("@authority"))
|
|
833
|
+
throw new VerificationError("@authority must be covered");
|
|
834
|
+
let signatureAgent = null;
|
|
835
|
+
const sa = h.get("signature-agent");
|
|
836
|
+
if (sa !== null) {
|
|
837
|
+
if (!components.includes("signature-agent")) {
|
|
838
|
+
throw new VerificationError("Signature-Agent sent but not covered");
|
|
839
|
+
}
|
|
840
|
+
signatureAgent = unquote(sa.trim());
|
|
841
|
+
}
|
|
842
|
+
const body = request.body ? new Uint8Array(await request.clone().arrayBuffer()) : new Uint8Array();
|
|
843
|
+
if (body.byteLength > 0) {
|
|
844
|
+
if (!components.includes("content-digest")) {
|
|
845
|
+
throw new VerificationError(
|
|
846
|
+
"request has a body but content-digest is not covered"
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const m = /(?:^|,)\s*sha-256=:([A-Za-z0-9+/=]+):/.exec(
|
|
850
|
+
h.get("content-digest") ?? ""
|
|
851
|
+
);
|
|
852
|
+
const actual = (0, import_node_crypto4.createHash)("sha256").update(body).digest("base64");
|
|
853
|
+
if (!m || m[1] !== actual)
|
|
854
|
+
throw new VerificationError("Content-Digest does not match the body");
|
|
855
|
+
}
|
|
856
|
+
const nonce = typeof params.nonce === "string" ? params.nonce : null;
|
|
857
|
+
if (this.#opts.requireNonce && !nonce)
|
|
858
|
+
throw new VerificationError("nonce required");
|
|
859
|
+
const base = [
|
|
860
|
+
...components.map(
|
|
861
|
+
(c) => `"${c}": ${componentValue(c, request, h, opts.authority)}`
|
|
862
|
+
),
|
|
863
|
+
`"@signature-params": ${rawParams}`
|
|
864
|
+
].join("\n");
|
|
865
|
+
const jwk = await this.#resolve(keyid, signatureAgent);
|
|
866
|
+
let ok = false;
|
|
867
|
+
try {
|
|
868
|
+
const key = (0, import_node_crypto4.createPublicKey)({
|
|
869
|
+
key: { kty: "OKP", crv: "Ed25519", x: jwk.x },
|
|
870
|
+
format: "jwk"
|
|
871
|
+
});
|
|
872
|
+
ok = (0, import_node_crypto4.verify)(null, Buffer.from(base, "utf8"), key, raw);
|
|
873
|
+
} catch {
|
|
874
|
+
ok = false;
|
|
875
|
+
}
|
|
876
|
+
if (!ok) throw new VerificationError("signature does not verify");
|
|
877
|
+
if (nonce && this.#nonces.seen(nonce, expires + skew))
|
|
878
|
+
throw new VerificationError("nonce replayed");
|
|
879
|
+
return {
|
|
880
|
+
kid: keyid,
|
|
881
|
+
signatureAgentUrl: signatureAgent,
|
|
882
|
+
label,
|
|
883
|
+
created,
|
|
884
|
+
expires,
|
|
885
|
+
nonce,
|
|
886
|
+
covered: components,
|
|
887
|
+
jwk
|
|
888
|
+
};
|
|
404
889
|
}
|
|
405
|
-
|
|
406
|
-
|
|
890
|
+
};
|
|
891
|
+
function createVerifierMiddleware(opts = {}) {
|
|
892
|
+
const verifier = new Verifier(opts);
|
|
893
|
+
return async (request) => {
|
|
894
|
+
const signed = request.headers.has("signature-input");
|
|
895
|
+
if (!signed && !opts.required) return null;
|
|
896
|
+
try {
|
|
897
|
+
if (!signed) throw new VerificationError("request is not signed");
|
|
898
|
+
return await verifier.verify(request);
|
|
899
|
+
} catch (e) {
|
|
900
|
+
if (!opts.required) return null;
|
|
901
|
+
return new Response(
|
|
902
|
+
JSON.stringify({
|
|
903
|
+
error: "web_bot_auth",
|
|
904
|
+
reason: String(e.message)
|
|
905
|
+
}),
|
|
906
|
+
{
|
|
907
|
+
status: 401,
|
|
908
|
+
headers: { "content-type": "application/json" }
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
};
|
|
407
913
|
}
|
|
408
914
|
|
|
409
915
|
// src/index.ts
|
|
410
|
-
var VERSION = "0.
|
|
916
|
+
var VERSION = "0.4.0";
|
|
411
917
|
// Annotate the CommonJS export names for ESM import in node:
|
|
412
918
|
0 && (module.exports = {
|
|
413
919
|
DEFAULT_KEY_PATH,
|
|
414
920
|
Identity,
|
|
921
|
+
KEY_PASSWORD_ENV,
|
|
922
|
+
NonceStore,
|
|
415
923
|
VERSION,
|
|
924
|
+
VerificationError,
|
|
925
|
+
Verifier,
|
|
416
926
|
applyTo,
|
|
417
927
|
createSignedFetch,
|
|
418
|
-
|
|
928
|
+
createVerifierMiddleware,
|
|
929
|
+
normalizeAuthority,
|
|
930
|
+
sign,
|
|
931
|
+
thumbprint,
|
|
932
|
+
validateSignatureAgentUrl
|
|
419
933
|
});
|
|
420
934
|
//# sourceMappingURL=index.cjs.map
|