@fedify/fedify 2.1.0-dev.503 → 2.1.0-dev.523
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/{builder-BHUnSQtB.js → builder-CJkMYxxc.js} +9 -3
- package/dist/compat/mod.d.cts +3 -3
- package/dist/compat/mod.d.ts +3 -3
- package/dist/compat/transformers.test.js +12 -12
- package/dist/{context-DZJhUmzF.d.ts → context--RwChtri.d.ts} +54 -2
- package/dist/{context-D3QkEtZd.d.cts → context-DL0cPpPV.d.cts} +54 -2
- package/dist/{deno-BYerLnry.js → deno-CQdJQjC5.js} +1 -1
- package/dist/{docloader-MSkogD2T.js → docloader-Cyl0-S8m.js} +2 -2
- package/dist/federation/builder.test.js +14 -3
- package/dist/federation/handler.test.js +97 -13
- package/dist/federation/idempotency.test.js +12 -12
- package/dist/federation/inbox.test.js +2 -2
- package/dist/federation/keycache.test.js +46 -2
- package/dist/federation/middleware.test.js +206 -12
- package/dist/federation/mod.cjs +4 -4
- package/dist/federation/mod.d.cts +4 -4
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +4 -4
- package/dist/federation/send.test.js +5 -5
- package/dist/federation/webfinger.test.js +12 -12
- package/dist/{http-DkHdFfrc.d.ts → http-BbfOqHGG.d.ts} +80 -8
- package/dist/{http-DJT6NciB.cjs → http-D6a6mMc0.cjs} +305 -99
- package/dist/{http-CSX1-Mgi.js → http-DJmytoC2.js} +295 -101
- package/dist/{http-S2U3qDwN.js → http-DK0CTomU.js} +153 -57
- package/dist/{http-Cz3MlXAZ.d.cts → http-DsqqmkXi.d.cts} +80 -8
- package/dist/{inbox-BaA0g5I_.js → inbox-CWa6sqsk.js} +1 -1
- package/dist/{key-DCdTVZiK.js → key-DRgvVevp.js} +145 -47
- package/dist/keycache-C7k8s1Bk.js +102 -0
- package/dist/{kv-cache-Vtxhbo1W.cjs → kv-cache-DPtsJ1sL.cjs} +1 -1
- package/dist/{kv-cache-CQPL_aGY.js → kv-cache-MPcS_mGG.js} +1 -1
- package/dist/{ld-CrX7pQda.js → ld-s9_8WfBc.js} +2 -2
- package/dist/{middleware-CfI9C9Xy.js → middleware-2XtoTBq0.js} +12 -12
- package/dist/{middleware-MlO5iUeZ.js → middleware-Ajnk9qHB.js} +158 -22
- package/dist/middleware-BgCIhb_C.cjs +12 -0
- package/dist/{middleware-D4S6i4A_.cjs → middleware-BoCzk7-G.cjs} +158 -22
- package/dist/{middleware-C8PKuPrm.js → middleware-DGUNDGCl.js} +4 -4
- package/dist/{middleware-BelSJK7m.js → middleware-Dn9UDJZP.js} +100 -24
- package/dist/{mod-CwZXZJ9d.d.ts → mod-BugwI0JN.d.ts} +1 -1
- package/dist/{mod-DPkRU3EK.d.cts → mod-CFBU2OT3.d.cts} +1 -1
- package/dist/{mod-DUWcVv49.d.ts → mod-CvxylbuV.d.ts} +1 -1
- package/dist/{mod-DVwHUI_x.d.cts → mod-DE8MYisy.d.cts} +1 -1
- package/dist/{mod-DXsQakeS.d.cts → mod-DKG0ovjR.d.cts} +1 -1
- package/dist/{mod-DnSsduJF.d.ts → mod-DcfFNgYf.d.ts} +1 -1
- package/dist/{mod-Di3W5OdP.d.cts → mod-Dp0kK0hO.d.cts} +1 -1
- package/dist/{mod-DosD6NsG.d.ts → mod-Z7lIaCfo.d.ts} +1 -1
- package/dist/mod.cjs +8 -4
- package/dist/mod.d.cts +8 -8
- package/dist/mod.d.ts +8 -8
- package/dist/mod.js +7 -5
- package/dist/nodeinfo/handler.test.js +12 -12
- package/dist/otel/exporter.test.js +43 -2
- package/dist/otel/mod.cjs +7 -1
- package/dist/otel/mod.d.cts +12 -0
- package/dist/otel/mod.d.ts +12 -0
- package/dist/otel/mod.js +7 -1
- package/dist/{owner-BAlnLKMO.js → owner-Cx8gV-j4.js} +1 -1
- package/dist/{proof-DMgHaXNJ.js → proof-CDr3NP3R.js} +2 -2
- package/dist/{proof-BgUVmaJz.js → proof-Le4DAkqb.js} +1 -1
- package/dist/{proof-CR5RUAmy.cjs → proof-qHcNgE5i.cjs} +1 -1
- package/dist/{send-B2aZYf9A.js → send-DreBSY1U.js} +2 -2
- package/dist/sig/http.test.js +85 -5
- package/dist/sig/key.test.js +70 -3
- package/dist/sig/ld.test.js +3 -3
- package/dist/sig/mod.cjs +4 -2
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.js +3 -3
- package/dist/sig/proof.test.js +3 -3
- package/dist/testing/mod.d.ts +92 -0
- package/dist/utils/docloader.test.js +4 -4
- package/dist/utils/mod.cjs +2 -2
- package/dist/utils/mod.d.cts +2 -2
- package/dist/utils/mod.d.ts +2 -2
- package/dist/utils/mod.js +2 -2
- package/package.json +5 -5
- package/dist/keycache-DRxpZ5r9.js +0 -48
- package/dist/middleware-D4XcpSBG.cjs +0 -12
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { URLPattern } from "urlpattern-polyfill";
|
|
4
4
|
globalThis.addEventListener = () => {};
|
|
5
5
|
|
|
6
|
-
import { deno_default } from "./deno-
|
|
7
|
-
import {
|
|
6
|
+
import { deno_default } from "./deno-CQdJQjC5.js";
|
|
7
|
+
import { fetchKeyDetailed, validateCryptoKey } from "./key-DRgvVevp.js";
|
|
8
8
|
import { getLogger } from "@logtape/logtape";
|
|
9
9
|
import { CryptographicKey } from "@fedify/vocab";
|
|
10
10
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
@@ -240,6 +240,55 @@ const supportedHashAlgorithms = {
|
|
|
240
240
|
"sha-256": "SHA-256",
|
|
241
241
|
"sha-512": "SHA-512"
|
|
242
242
|
};
|
|
243
|
+
function noSignatureResult() {
|
|
244
|
+
return {
|
|
245
|
+
verified: false,
|
|
246
|
+
reason: { type: "noSignature" }
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function invalidSignatureResult(keyId) {
|
|
250
|
+
return keyId == null ? {
|
|
251
|
+
verified: false,
|
|
252
|
+
reason: { type: "invalidSignature" }
|
|
253
|
+
} : {
|
|
254
|
+
verified: false,
|
|
255
|
+
reason: {
|
|
256
|
+
type: "invalidSignature",
|
|
257
|
+
keyId
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function keyFetchErrorResult(keyId, result) {
|
|
262
|
+
return {
|
|
263
|
+
verified: false,
|
|
264
|
+
reason: {
|
|
265
|
+
type: "keyFetchError",
|
|
266
|
+
keyId,
|
|
267
|
+
result
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function parseKeyId(value) {
|
|
272
|
+
if (value == null) return null;
|
|
273
|
+
try {
|
|
274
|
+
return new URL(value);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function getKeyFetchErrorName(error) {
|
|
280
|
+
return error.name || error.constructor.name || "Error";
|
|
281
|
+
}
|
|
282
|
+
function recordVerificationResult(span, result) {
|
|
283
|
+
span.setAttribute("http_signatures.verified", result.verified);
|
|
284
|
+
if (result.verified === true) return;
|
|
285
|
+
const reason = result.reason;
|
|
286
|
+
span.setAttribute("http_signatures.failure_reason", reason.type);
|
|
287
|
+
if ("keyId" in reason && reason.keyId != null) span.setAttribute("http_signatures.key_id", reason.keyId.href);
|
|
288
|
+
if (reason.type !== "keyFetchError") return;
|
|
289
|
+
if ("status" in reason.result) span.setAttribute("http_signatures.key_fetch_status", reason.result.status);
|
|
290
|
+
else span.setAttribute("http_signatures.key_fetch_error", getKeyFetchErrorName(reason.result.error));
|
|
291
|
+
}
|
|
243
292
|
/**
|
|
244
293
|
* Verifies the signature of a request.
|
|
245
294
|
*
|
|
@@ -254,6 +303,19 @@ const supportedHashAlgorithms = {
|
|
|
254
303
|
* could not be verified.
|
|
255
304
|
*/
|
|
256
305
|
async function verifyRequest(request, options = {}) {
|
|
306
|
+
const result = await verifyRequestDetailed(request, options);
|
|
307
|
+
return result.verified ? result.key : null;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Verifies the signature of a request and returns a structured failure reason
|
|
311
|
+
* when verification does not succeed.
|
|
312
|
+
*
|
|
313
|
+
* @param request The request to verify.
|
|
314
|
+
* @param options Options for verifying the request.
|
|
315
|
+
* @returns The verified public key, or a structured verification failure.
|
|
316
|
+
* @since 2.1.0
|
|
317
|
+
*/
|
|
318
|
+
async function verifyRequestDetailed(request, options = {}) {
|
|
257
319
|
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
258
320
|
const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version);
|
|
259
321
|
return await tracer.startActiveSpan("http_signatures.verify", async (span) => {
|
|
@@ -265,11 +327,12 @@ async function verifyRequest(request, options = {}) {
|
|
|
265
327
|
try {
|
|
266
328
|
let spec = options.spec;
|
|
267
329
|
if (spec == null) spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
|
|
268
|
-
let
|
|
269
|
-
if (spec === "rfc9421")
|
|
270
|
-
else
|
|
271
|
-
|
|
272
|
-
|
|
330
|
+
let result;
|
|
331
|
+
if (spec === "rfc9421") result = await verifyRequestRfc9421(request, span, options);
|
|
332
|
+
else result = await verifyRequestDraft(request, span, options);
|
|
333
|
+
recordVerificationResult(span, result);
|
|
334
|
+
if (!result.verified) span.setStatus({ code: SpanStatusCode.ERROR });
|
|
335
|
+
return result;
|
|
273
336
|
} catch (error) {
|
|
274
337
|
span.setStatus({
|
|
275
338
|
code: SpanStatusCode.ERROR,
|
|
@@ -289,27 +352,29 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
289
352
|
]);
|
|
290
353
|
if (request.bodyUsed) {
|
|
291
354
|
logger.error("Failed to verify; the request body is already consumed.", { url: request.url });
|
|
292
|
-
return
|
|
355
|
+
return noSignatureResult();
|
|
293
356
|
} else if (request.body?.locked) {
|
|
294
357
|
logger.error("Failed to verify; the request body is locked.", { url: request.url });
|
|
295
|
-
return
|
|
358
|
+
return noSignatureResult();
|
|
296
359
|
}
|
|
297
360
|
const originalRequest = request;
|
|
298
361
|
request = request.clone();
|
|
299
|
-
const dateHeader = request.headers.get("Date");
|
|
300
|
-
if (dateHeader == null) {
|
|
301
|
-
logger.debug("Failed to verify; no Date header found.", { headers: Object.fromEntries(request.headers.entries()) });
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
362
|
const sigHeader = request.headers.get("Signature");
|
|
305
363
|
if (sigHeader == null) {
|
|
306
364
|
logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) });
|
|
307
|
-
return
|
|
365
|
+
return noSignatureResult();
|
|
366
|
+
}
|
|
367
|
+
const sigValues = Object.fromEntries(sigHeader.split(",").map((pair) => pair.match(/^\s*([A-Za-z]+)=(?:"([^"]*)"|(\d+))\s*$/)).filter((m) => m != null).map((m) => [m[1], m[2] ?? m[3]]));
|
|
368
|
+
const parsedKeyId = parseKeyId(sigValues.keyId);
|
|
369
|
+
const dateHeader = request.headers.get("Date");
|
|
370
|
+
if (dateHeader == null) {
|
|
371
|
+
logger.debug("Failed to verify; no Date header found.", { headers: Object.fromEntries(request.headers.entries()) });
|
|
372
|
+
return invalidSignatureResult(parsedKeyId);
|
|
308
373
|
}
|
|
309
374
|
const digestHeader = request.headers.get("Digest");
|
|
310
375
|
if (request.method !== "GET" && request.method !== "HEAD" && digestHeader == null) {
|
|
311
376
|
logger.debug("Failed to verify; no Digest header found.", { headers: Object.fromEntries(request.headers.entries()) });
|
|
312
|
-
return
|
|
377
|
+
return invalidSignatureResult(parsedKeyId);
|
|
313
378
|
}
|
|
314
379
|
let body = null;
|
|
315
380
|
if (digestHeader != null) {
|
|
@@ -327,7 +392,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
327
392
|
digest: digestBase64,
|
|
328
393
|
error
|
|
329
394
|
});
|
|
330
|
-
return
|
|
395
|
+
return invalidSignatureResult(parsedKeyId);
|
|
331
396
|
}
|
|
332
397
|
if (span.isRecording()) span.setAttribute(`http_signatures.digest.${algo}`, encodeHex(digest));
|
|
333
398
|
const expectedDigest = await crypto.subtle.digest(supportedHashAlgorithms[algo], body);
|
|
@@ -337,7 +402,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
337
402
|
digest: digestBase64,
|
|
338
403
|
expectedDigest: encodeBase64(expectedDigest)
|
|
339
404
|
});
|
|
340
|
-
return
|
|
405
|
+
return invalidSignatureResult(parsedKeyId);
|
|
341
406
|
}
|
|
342
407
|
matched = true;
|
|
343
408
|
}
|
|
@@ -346,7 +411,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
346
411
|
supportedAlgorithms: Object.keys(supportedHashAlgorithms),
|
|
347
412
|
algorithms: digests.map(([algo]) => algo)
|
|
348
413
|
});
|
|
349
|
-
return
|
|
414
|
+
return invalidSignatureResult(parsedKeyId);
|
|
350
415
|
}
|
|
351
416
|
}
|
|
352
417
|
const date = Temporal.Instant.from(new Date(dateHeader).toISOString());
|
|
@@ -358,25 +423,24 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
358
423
|
date: date.toString(),
|
|
359
424
|
now: now.toString()
|
|
360
425
|
});
|
|
361
|
-
return
|
|
426
|
+
return invalidSignatureResult(parsedKeyId);
|
|
362
427
|
} else if (Temporal.Instant.compare(date, now.subtract(tw)) < 0) {
|
|
363
428
|
logger.debug("Failed to verify; Date is too far in the past.", {
|
|
364
429
|
date: date.toString(),
|
|
365
430
|
now: now.toString()
|
|
366
431
|
});
|
|
367
|
-
return
|
|
432
|
+
return invalidSignatureResult(parsedKeyId);
|
|
368
433
|
}
|
|
369
434
|
}
|
|
370
|
-
const sigValues = Object.fromEntries(sigHeader.split(",").map((pair) => pair.match(/^\s*([A-Za-z]+)=(?:"([^"]*)"|(\d+))\s*$/)).filter((m) => m != null).map((m) => [m[1], m[2] ?? m[3]]));
|
|
371
435
|
if (!("keyId" in sigValues)) {
|
|
372
436
|
logger.debug("Failed to verify; no keyId field found in the Signature header.", { signature: sigHeader });
|
|
373
|
-
return null;
|
|
437
|
+
return invalidSignatureResult(null);
|
|
374
438
|
} else if (!("headers" in sigValues)) {
|
|
375
439
|
logger.debug("Failed to verify; no headers field found in the Signature header.", { signature: sigHeader });
|
|
376
|
-
return
|
|
440
|
+
return invalidSignatureResult(parsedKeyId);
|
|
377
441
|
} else if (!("signature" in sigValues)) {
|
|
378
442
|
logger.debug("Failed to verify; no signature field found in the Signature header.", { signature: sigHeader });
|
|
379
|
-
return
|
|
443
|
+
return invalidSignatureResult(parsedKeyId);
|
|
380
444
|
}
|
|
381
445
|
if ("expires" in sigValues) {
|
|
382
446
|
const expiresSeconds = parseInt(sigValues.expires);
|
|
@@ -385,7 +449,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
385
449
|
expires: sigValues.expires,
|
|
386
450
|
signature: sigHeader
|
|
387
451
|
});
|
|
388
|
-
return
|
|
452
|
+
return invalidSignatureResult(parsedKeyId);
|
|
389
453
|
}
|
|
390
454
|
const expires = Temporal.Instant.fromEpochMilliseconds(expiresSeconds * 1e3);
|
|
391
455
|
if (Temporal.Instant.compare(now, expires) > 0) {
|
|
@@ -394,7 +458,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
394
458
|
now: now.toString(),
|
|
395
459
|
signature: sigHeader
|
|
396
460
|
});
|
|
397
|
-
return
|
|
461
|
+
return invalidSignatureResult(parsedKeyId);
|
|
398
462
|
}
|
|
399
463
|
}
|
|
400
464
|
if ("created" in sigValues) {
|
|
@@ -404,7 +468,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
404
468
|
created: sigValues.created,
|
|
405
469
|
signature: sigHeader
|
|
406
470
|
});
|
|
407
|
-
return
|
|
471
|
+
return invalidSignatureResult(parsedKeyId);
|
|
408
472
|
}
|
|
409
473
|
if (timeWindow !== false) {
|
|
410
474
|
const created = Temporal.Instant.fromEpochMilliseconds(createdSeconds * 1e3);
|
|
@@ -414,34 +478,37 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
414
478
|
created: created.toString(),
|
|
415
479
|
now: now.toString()
|
|
416
480
|
});
|
|
417
|
-
return
|
|
481
|
+
return invalidSignatureResult(parsedKeyId);
|
|
418
482
|
} else if (Temporal.Instant.compare(created, now.subtract(tw)) < 0) {
|
|
419
483
|
logger.debug("Failed to verify; created is too far in the past.", {
|
|
420
484
|
created: created.toString(),
|
|
421
485
|
now: now.toString()
|
|
422
486
|
});
|
|
423
|
-
return
|
|
487
|
+
return invalidSignatureResult(parsedKeyId);
|
|
424
488
|
}
|
|
425
489
|
}
|
|
426
490
|
}
|
|
427
491
|
const { keyId, headers, signature } = sigValues;
|
|
492
|
+
const keyIdUrl = parseKeyId(keyId);
|
|
493
|
+
if (keyIdUrl == null) return invalidSignatureResult(null);
|
|
428
494
|
span?.setAttribute("http_signatures.key_id", keyId);
|
|
429
495
|
if ("algorithm" in sigValues) span?.setAttribute("http_signatures.algorithm", sigValues.algorithm);
|
|
430
|
-
const { key, cached } = await
|
|
496
|
+
const { key, cached, fetchError } = await fetchKeyDetailed(keyIdUrl, CryptographicKey, {
|
|
431
497
|
documentLoader,
|
|
432
498
|
contextLoader,
|
|
433
499
|
keyCache,
|
|
434
500
|
tracerProvider
|
|
435
501
|
});
|
|
436
|
-
if (
|
|
502
|
+
if (fetchError != null) return keyFetchErrorResult(keyIdUrl, fetchError);
|
|
503
|
+
if (key == null) return invalidSignatureResult(keyIdUrl);
|
|
437
504
|
const headerNames = headers.split(/\s+/g);
|
|
438
505
|
if (!headerNames.includes("(request-target)") || !headerNames.includes("date")) {
|
|
439
506
|
logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers });
|
|
440
|
-
return
|
|
507
|
+
return invalidSignatureResult(keyIdUrl);
|
|
441
508
|
}
|
|
442
509
|
if (body != null && !headerNames.includes("digest")) {
|
|
443
510
|
logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers });
|
|
444
|
-
return
|
|
511
|
+
return invalidSignatureResult(keyIdUrl);
|
|
445
512
|
}
|
|
446
513
|
const message = headerNames.map((name) => `${name}: ` + (name === "(request-target)" ? `${request.method.toLowerCase()} ${new URL(request.url).pathname}` : name === "(created)" ? sigValues.created ?? "" : name === "(expires)" ? sigValues.expires ?? "" : name === "host" ? request.headers.get("host") ?? new URL(request.url).host : request.headers.get(name))).join("\n");
|
|
447
514
|
const sig = decodeBase64(signature);
|
|
@@ -454,7 +521,7 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
454
521
|
signature,
|
|
455
522
|
message
|
|
456
523
|
});
|
|
457
|
-
return await
|
|
524
|
+
return await verifyRequestDetailed(originalRequest, {
|
|
458
525
|
documentLoader,
|
|
459
526
|
contextLoader,
|
|
460
527
|
timeWindow,
|
|
@@ -470,9 +537,12 @@ async function verifyRequestDraft(request, span, { documentLoader, contextLoader
|
|
|
470
537
|
signature,
|
|
471
538
|
message
|
|
472
539
|
});
|
|
473
|
-
return
|
|
540
|
+
return invalidSignatureResult(keyIdUrl);
|
|
474
541
|
}
|
|
475
|
-
return
|
|
542
|
+
return {
|
|
543
|
+
verified: true,
|
|
544
|
+
key
|
|
545
|
+
};
|
|
476
546
|
}
|
|
477
547
|
/**
|
|
478
548
|
* RFC 9421 map of algorithm identifiers to WebCrypto algorithms
|
|
@@ -543,22 +613,22 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
543
613
|
]);
|
|
544
614
|
if (request.bodyUsed) {
|
|
545
615
|
logger.error("Failed to verify; the request body is already consumed.", { url: request.url });
|
|
546
|
-
return
|
|
616
|
+
return noSignatureResult();
|
|
547
617
|
} else if (request.body?.locked) {
|
|
548
618
|
logger.error("Failed to verify; the request body is locked.", { url: request.url });
|
|
549
|
-
return
|
|
619
|
+
return noSignatureResult();
|
|
550
620
|
}
|
|
551
621
|
const originalRequest = request;
|
|
552
622
|
request = request.clone();
|
|
553
623
|
const signatureInputHeader = request.headers.get("Signature-Input");
|
|
554
624
|
if (!signatureInputHeader) {
|
|
555
625
|
logger.debug("Failed to verify; no Signature-Input header found.", { headers: Object.fromEntries(request.headers.entries()) });
|
|
556
|
-
return
|
|
626
|
+
return noSignatureResult();
|
|
557
627
|
}
|
|
558
628
|
const signatureHeader = request.headers.get("Signature");
|
|
559
629
|
if (!signatureHeader) {
|
|
560
630
|
logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) });
|
|
561
|
-
return
|
|
631
|
+
return noSignatureResult();
|
|
562
632
|
}
|
|
563
633
|
const signatureInputs = parseRfc9421SignatureInput(signatureInputHeader);
|
|
564
634
|
logger.debug("Parsed Signature-Input header: {signatureInputs}", { signatureInputs });
|
|
@@ -566,18 +636,23 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
566
636
|
const signatureNames = Object.keys(signatureInputs);
|
|
567
637
|
if (signatureNames.length === 0) {
|
|
568
638
|
logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader });
|
|
569
|
-
return null;
|
|
639
|
+
return invalidSignatureResult(null);
|
|
570
640
|
}
|
|
571
|
-
let
|
|
641
|
+
let failure = noSignatureResult();
|
|
572
642
|
for (const sigName of signatureNames) {
|
|
573
|
-
if (!signatures[sigName])
|
|
643
|
+
if (!signatures[sigName]) {
|
|
644
|
+
failure = invalidSignatureResult(parseKeyId(signatureInputs[sigName]?.keyId));
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
574
647
|
const sigInput = signatureInputs[sigName];
|
|
575
648
|
const sigBytes = signatures[sigName];
|
|
649
|
+
const keyId = parseKeyId(sigInput.keyId);
|
|
576
650
|
if (!sigInput.keyId) {
|
|
577
651
|
logger.debug("Failed to verify; missing keyId in signature {signatureName}.", {
|
|
578
652
|
signatureName: sigName,
|
|
579
653
|
signatureInput: signatureInputHeader
|
|
580
654
|
});
|
|
655
|
+
failure = invalidSignatureResult(null);
|
|
581
656
|
continue;
|
|
582
657
|
}
|
|
583
658
|
if (!sigInput.created) {
|
|
@@ -585,6 +660,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
585
660
|
signatureName: sigName,
|
|
586
661
|
signatureInput: signatureInputHeader
|
|
587
662
|
});
|
|
663
|
+
failure = invalidSignatureResult(keyId);
|
|
588
664
|
continue;
|
|
589
665
|
}
|
|
590
666
|
const signatureCreated = Temporal.Instant.fromEpochMilliseconds(sigInput.created * 1e3);
|
|
@@ -596,12 +672,14 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
596
672
|
created: signatureCreated.toString(),
|
|
597
673
|
now: now.toString()
|
|
598
674
|
});
|
|
675
|
+
failure = invalidSignatureResult(keyId);
|
|
599
676
|
continue;
|
|
600
677
|
} else if (Temporal.Instant.compare(signatureCreated, now.subtract(tw)) < 0) {
|
|
601
678
|
logger.debug("Failed to verify; signature created time is too far in the past.", {
|
|
602
679
|
created: signatureCreated.toString(),
|
|
603
680
|
now: now.toString()
|
|
604
681
|
});
|
|
682
|
+
failure = invalidSignatureResult(keyId);
|
|
605
683
|
continue;
|
|
606
684
|
}
|
|
607
685
|
}
|
|
@@ -609,25 +687,36 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
609
687
|
const contentDigestHeader = request.headers.get("Content-Digest");
|
|
610
688
|
if (!contentDigestHeader) {
|
|
611
689
|
logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components });
|
|
690
|
+
failure = invalidSignatureResult(keyId);
|
|
612
691
|
continue;
|
|
613
692
|
}
|
|
614
693
|
const body = await request.arrayBuffer();
|
|
615
694
|
const digestValid = await verifyRfc9421ContentDigest(contentDigestHeader, body);
|
|
616
695
|
if (!digestValid) {
|
|
617
696
|
logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader });
|
|
697
|
+
failure = invalidSignatureResult(keyId);
|
|
618
698
|
continue;
|
|
619
699
|
}
|
|
620
700
|
}
|
|
621
701
|
span?.setAttribute("http_signatures.key_id", sigInput.keyId);
|
|
622
702
|
span?.setAttribute("http_signatures.created", sigInput.created.toString());
|
|
623
|
-
|
|
703
|
+
if (keyId == null) {
|
|
704
|
+
failure = invalidSignatureResult(null);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const { key, cached, fetchError } = await fetchKeyDetailed(keyId, CryptographicKey, {
|
|
624
708
|
documentLoader,
|
|
625
709
|
contextLoader,
|
|
626
710
|
keyCache,
|
|
627
711
|
tracerProvider
|
|
628
712
|
});
|
|
713
|
+
if (fetchError != null) {
|
|
714
|
+
failure = keyFetchErrorResult(keyId, fetchError);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
629
717
|
if (!key) {
|
|
630
718
|
logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId });
|
|
719
|
+
failure = invalidSignatureResult(keyId);
|
|
631
720
|
continue;
|
|
632
721
|
}
|
|
633
722
|
let alg = sigInput.alg?.toLowerCase();
|
|
@@ -644,6 +733,7 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
644
733
|
algorithm: sigInput.alg,
|
|
645
734
|
supported: Object.keys(rfc9421AlgorithmMap)
|
|
646
735
|
});
|
|
736
|
+
failure = invalidSignatureResult(keyId);
|
|
647
737
|
continue;
|
|
648
738
|
}
|
|
649
739
|
let signatureBase;
|
|
@@ -654,41 +744,47 @@ async function verifyRequestRfc9421(request, span, { documentLoader, contextLoad
|
|
|
654
744
|
error,
|
|
655
745
|
signatureInput: sigInput
|
|
656
746
|
});
|
|
747
|
+
failure = invalidSignatureResult(keyId);
|
|
657
748
|
continue;
|
|
658
749
|
}
|
|
659
750
|
const signatureBaseBytes = new TextEncoder().encode(signatureBase);
|
|
660
751
|
span?.setAttribute("http_signatures.signature", encodeHex(sigBytes));
|
|
661
752
|
try {
|
|
662
753
|
const verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes.slice(), signatureBaseBytes);
|
|
663
|
-
if (verified) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
754
|
+
if (verified) return {
|
|
755
|
+
verified: true,
|
|
756
|
+
key
|
|
757
|
+
};
|
|
758
|
+
else if (cached) {
|
|
667
759
|
logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId });
|
|
668
|
-
return await
|
|
760
|
+
return await verifyRequestDetailed(originalRequest, {
|
|
669
761
|
documentLoader,
|
|
670
762
|
contextLoader,
|
|
671
763
|
timeWindow,
|
|
672
764
|
currentTime,
|
|
673
765
|
keyCache: {
|
|
674
766
|
get: () => Promise.resolve(void 0),
|
|
675
|
-
set: async (keyId, key$1) => await keyCache?.set(keyId, key$1)
|
|
767
|
+
set: async (keyId$1, key$1) => await keyCache?.set(keyId$1, key$1)
|
|
676
768
|
},
|
|
677
769
|
spec: "rfc9421"
|
|
678
770
|
});
|
|
679
|
-
} else
|
|
680
|
-
keyId
|
|
681
|
-
|
|
682
|
-
|
|
771
|
+
} else {
|
|
772
|
+
logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", {
|
|
773
|
+
keyId: sigInput.keyId,
|
|
774
|
+
signatureBase
|
|
775
|
+
});
|
|
776
|
+
failure = invalidSignatureResult(keyId);
|
|
777
|
+
}
|
|
683
778
|
} catch (error) {
|
|
684
779
|
logger.debug("Error during signature verification: {error}", {
|
|
685
780
|
error,
|
|
686
781
|
keyId: sigInput.keyId,
|
|
687
782
|
algorithm: sigInput.alg
|
|
688
783
|
});
|
|
784
|
+
failure = invalidSignatureResult(keyId);
|
|
689
785
|
}
|
|
690
786
|
}
|
|
691
|
-
return
|
|
787
|
+
return failure;
|
|
692
788
|
}
|
|
693
789
|
/**
|
|
694
790
|
* Helper function to create a new Request for redirect handling.
|
|
@@ -806,4 +902,4 @@ function timingSafeEqual(a, b) {
|
|
|
806
902
|
}
|
|
807
903
|
|
|
808
904
|
//#endregion
|
|
809
|
-
export { createRfc9421SignatureBase, doubleKnock, formatRfc9421Signature, formatRfc9421SignatureParameters, parseRfc9421Signature, parseRfc9421SignatureInput, signRequest, timingSafeEqual, verifyRequest };
|
|
905
|
+
export { createRfc9421SignatureBase, doubleKnock, formatRfc9421Signature, formatRfc9421SignatureParameters, parseRfc9421Signature, parseRfc9421SignatureInput, signRequest, timingSafeEqual, verifyRequest, verifyRequestDetailed };
|
|
@@ -70,6 +70,34 @@ interface FetchKeyResult<T extends CryptographicKey | Multikey> {
|
|
|
70
70
|
readonly cached: boolean;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
+
* Detailed fetch failure information from {@link fetchKeyDetailed}.
|
|
74
|
+
* @since 2.1.0
|
|
75
|
+
*/
|
|
76
|
+
type FetchKeyErrorResult = {
|
|
77
|
+
readonly status: number;
|
|
78
|
+
readonly response: Response;
|
|
79
|
+
} | {
|
|
80
|
+
readonly error: Error;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The result of {@link fetchKeyDetailed}.
|
|
84
|
+
* @since 2.1.0
|
|
85
|
+
*/
|
|
86
|
+
interface FetchKeyDetailedResult<T extends CryptographicKey | Multikey> extends FetchKeyResult<T> {
|
|
87
|
+
/**
|
|
88
|
+
* The error that occurred while fetching the key, if fetching failed before
|
|
89
|
+
* a document could be parsed.
|
|
90
|
+
*/
|
|
91
|
+
readonly fetchError?: FetchKeyErrorResult;
|
|
92
|
+
}
|
|
93
|
+
type FetchableKeyClass<T extends CryptographicKey | Multikey> = (new (...args: any[]) => T) & {
|
|
94
|
+
fromJsonLd(jsonLd: unknown, options: {
|
|
95
|
+
documentLoader?: DocumentLoader;
|
|
96
|
+
contextLoader?: DocumentLoader;
|
|
97
|
+
tracerProvider?: TracerProvider;
|
|
98
|
+
}): Promise<T>;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
73
101
|
* Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL.
|
|
74
102
|
* If the given URL contains an {@link Actor} object, it tries to find
|
|
75
103
|
* the corresponding key in the `publicKey` or `assertionMethod` property.
|
|
@@ -82,13 +110,22 @@ interface FetchKeyResult<T extends CryptographicKey | Multikey> {
|
|
|
82
110
|
* @returns The fetched key or `null` if the key is not found.
|
|
83
111
|
* @since 1.3.0
|
|
84
112
|
*/
|
|
85
|
-
declare function fetchKey<T extends CryptographicKey | Multikey>(keyId: URL | string, cls:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
declare function fetchKey<T extends CryptographicKey | Multikey>(keyId: URL | string, cls: FetchableKeyClass<T>, options?: FetchKeyOptions): Promise<FetchKeyResult<T>>;
|
|
114
|
+
/**
|
|
115
|
+
* Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL,
|
|
116
|
+
* preserving transport-level fetch failures for callers that need to inspect
|
|
117
|
+
* why the key could not be loaded.
|
|
118
|
+
*
|
|
119
|
+
* @template T The type of the key to fetch. Either {@link CryptographicKey}
|
|
120
|
+
* or {@link Multikey}.
|
|
121
|
+
* @param keyId The URL of the key.
|
|
122
|
+
* @param cls The class of the key to fetch. Either {@link CryptographicKey}
|
|
123
|
+
* or {@link Multikey}.
|
|
124
|
+
* @param options Options for fetching the key.
|
|
125
|
+
* @returns The fetched key, or detailed fetch failure information.
|
|
126
|
+
* @since 2.1.0
|
|
127
|
+
*/
|
|
128
|
+
declare function fetchKeyDetailed<T extends CryptographicKey | Multikey>(keyId: URL | string, cls: FetchableKeyClass<T>, options?: FetchKeyOptions): Promise<FetchKeyDetailedResult<T>>;
|
|
92
129
|
/**
|
|
93
130
|
* A cache for storing cryptographic keys.
|
|
94
131
|
* @since 0.12.0
|
|
@@ -202,6 +239,31 @@ interface VerifyRequestOptions {
|
|
|
202
239
|
tracerProvider?: TracerProvider;
|
|
203
240
|
}
|
|
204
241
|
/**
|
|
242
|
+
* The reason why {@link verifyRequestDetailed} could not verify a request.
|
|
243
|
+
* @since 2.1.0
|
|
244
|
+
*/
|
|
245
|
+
type VerifyRequestFailureReason = {
|
|
246
|
+
readonly type: "keyFetchError";
|
|
247
|
+
readonly keyId: URL;
|
|
248
|
+
readonly result: FetchKeyErrorResult;
|
|
249
|
+
} | {
|
|
250
|
+
readonly type: "invalidSignature";
|
|
251
|
+
readonly keyId?: URL;
|
|
252
|
+
} | {
|
|
253
|
+
readonly type: "noSignature";
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* The detailed result of {@link verifyRequestDetailed}.
|
|
257
|
+
* @since 2.1.0
|
|
258
|
+
*/
|
|
259
|
+
type VerifyRequestDetailedResult = {
|
|
260
|
+
readonly verified: true;
|
|
261
|
+
readonly key: CryptographicKey;
|
|
262
|
+
} | {
|
|
263
|
+
readonly verified: false;
|
|
264
|
+
readonly reason: VerifyRequestFailureReason;
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
205
267
|
* Verifies the signature of a request.
|
|
206
268
|
*
|
|
207
269
|
* Note that this function consumes the request body, so it should not be used
|
|
@@ -216,6 +278,16 @@ interface VerifyRequestOptions {
|
|
|
216
278
|
*/
|
|
217
279
|
declare function verifyRequest(request: Request, options?: VerifyRequestOptions): Promise<CryptographicKey | null>;
|
|
218
280
|
/**
|
|
281
|
+
* Verifies the signature of a request and returns a structured failure reason
|
|
282
|
+
* when verification does not succeed.
|
|
283
|
+
*
|
|
284
|
+
* @param request The request to verify.
|
|
285
|
+
* @param options Options for verifying the request.
|
|
286
|
+
* @returns The verified public key, or a structured verification failure.
|
|
287
|
+
* @since 2.1.0
|
|
288
|
+
*/
|
|
289
|
+
declare function verifyRequestDetailed(request: Request, options?: VerifyRequestOptions): Promise<VerifyRequestDetailedResult>;
|
|
290
|
+
/**
|
|
219
291
|
* A spec determiner for HTTP Message Signatures.
|
|
220
292
|
* It determines the spec to use for signing requests.
|
|
221
293
|
* It is used for double-knocking
|
|
@@ -241,4 +313,4 @@ interface HttpMessageSignaturesSpecDeterminer {
|
|
|
241
313
|
* @since 1.6.0
|
|
242
314
|
*/
|
|
243
315
|
//#endregion
|
|
244
|
-
export { FetchKeyOptions, FetchKeyResult, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, SignRequestOptions, VerifyRequestOptions, exportJwk, fetchKey, generateCryptoKeyPair, importJwk, signRequest, verifyRequest };
|
|
316
|
+
export { FetchKeyDetailedResult, FetchKeyErrorResult, FetchKeyOptions, FetchKeyResult, HttpMessageSignaturesSpec, HttpMessageSignaturesSpecDeterminer, KeyCache, SignRequestOptions, VerifyRequestDetailedResult, VerifyRequestFailureReason, VerifyRequestOptions, exportJwk, fetchKey, fetchKeyDetailed, generateCryptoKeyPair, importJwk, signRequest, verifyRequest, verifyRequestDetailed };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { URLPattern } from "urlpattern-polyfill";
|
|
4
4
|
globalThis.addEventListener = () => {};
|
|
5
5
|
|
|
6
|
-
import { deno_default } from "./deno-
|
|
6
|
+
import { deno_default } from "./deno-CQdJQjC5.js";
|
|
7
7
|
import { getLogger } from "@logtape/logtape";
|
|
8
8
|
import { Activity, getTypeId } from "@fedify/vocab";
|
|
9
9
|
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|