@fedify/fedify 2.4.0-dev.1962 → 2.4.0-dev.1988
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-CWYQXC2W.mjs → builder-Cts78G9N.mjs} +2 -2
- package/dist/{client-CtxvmazV.mjs → client-DpnpgjyX.mjs} +78 -4
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{context-COLwHogJ.d.cts → context-BeDnDuF3.d.cts} +4 -3
- package/dist/{context-DiiCt4S7.d.ts → context-uyRUWoZQ.d.ts} +4 -3
- package/dist/{deno-CfLMX4HF.mjs → deno-BgMEefEr.mjs} +1 -1
- package/dist/{docloader-CDmmYxYb.mjs → docloader-DEYs99FZ.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +122 -4
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/keycache.test.mjs +52 -9
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +111 -15
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/send.test.mjs +139 -4
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{federation-CIeDMHxZ.cjs → federation-DzPUG_AP.cjs} +93 -17
- package/dist/{http-6TI0tI03.d.cts → http-65PYi9jY.d.ts} +34 -2
- package/dist/{http-CdrSn2ip.d.ts → http-CP-7OdSq.d.cts} +34 -2
- package/dist/{http-DWZZicb2.js → http-CgwqtxAy.js} +169 -7
- package/dist/{http-BR190IDB.mjs → http-CtidZS_t.mjs} +3 -3
- package/dist/{http-C3kkrvAE.cjs → http-Z3anvCd-.cjs} +180 -6
- package/dist/{key-BcA-AzPp.mjs → key-Cwbg5ggV.mjs} +169 -7
- package/dist/{keycache-ZLqIfrkL.mjs → keycache-BUDHB5rY.mjs} +12 -4
- package/dist/{kv-cache-DCItgZUM.cjs → kv-cache-BM2-w-Wq.cjs} +2 -2
- package/dist/{kv-cache-5d2HgB_-.js → kv-cache-CykuL0ya.js} +2 -2
- package/dist/{kv-cache-m962_cok.mjs → kv-cache-DHiR8yTf.mjs} +1 -1
- package/dist/{ld-QpPkmUjy.mjs → ld-CzhdWyTt.mjs} +3 -3
- package/dist/{metrics-Dkyzq7ml.mjs → metrics-CrqsHP0l.mjs} +1 -1
- package/dist/{middleware-BdKtWJK1.mjs → middleware-CZpYxSKD.mjs} +1 -1
- package/dist/{middleware-BNktRUnj.js → middleware-CeCj5caM.js} +101 -25
- package/dist/{middleware-BaZ32nCi.mjs → middleware-DCpiAsZi.mjs} +34 -23
- package/dist/{mod-DypxPS9H.d.ts → mod-C0BYc0OU.d.ts} +1 -1
- package/dist/{mod-CnGLNoVw.d.cts → mod-CbKDOnCt.d.cts} +1 -1
- package/dist/{mod-CLHkCwLm.d.cts → mod-CyiSjRkk.d.cts} +1 -1
- package/dist/{mod-CSH1Ee-I.d.ts → mod-D882_Xsg.d.ts} +1 -1
- package/dist/{mod-DzRXACM5.d.ts → mod-UDwMjuHM.d.ts} +1 -1
- package/dist/{mod-BZldCEU0.d.cts → mod-x9W0d0Pk.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/client.test.mjs +28 -1
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/{owner-r_waTiQj.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-DGSfntI-.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-BqVXJLW8.mjs +175 -0
- package/dist/{proof-DrWB_vel.js → proof-CUghqhAf.js} +122 -65
- package/dist/{proof-CYdLcTFg.cjs → proof-DHu5zI0Z.cjs} +122 -65
- package/dist/{proof-Mov6-gSw.mjs → proof-DL8Iid3a.mjs} +14 -11
- package/dist/{send-DB1mkFKO.mjs → send-6jS4xFsX.mjs} +68 -11
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +169 -2
- package/dist/sig/ld.test.mjs +55 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +103 -4
- package/dist/sig/proof.test.mjs +143 -2
- package/dist/{temporal-Yi0JAV9z.mjs → temporal-DcyzeytP.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +2 -1
- package/dist/{types-RXm5YxpC.cjs → types-Be2zbyeG.cjs} +94 -2
- package/dist/{types-Dtyf8lVj.js → types-C_cNlNVK.js} +78 -4
- package/dist/utils/docloader.test.mjs +27 -3
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
- package/dist/owner-B795xXwU.mjs +0 -120
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
2
2
|
const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
|
|
3
3
|
const require_transformers = require("./transformers-BalCrSsr.cjs");
|
|
4
|
-
const require_http = require("./http-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const require_kv_cache = require("./kv-cache-
|
|
4
|
+
const require_http = require("./http-Z3anvCd-.cjs");
|
|
5
|
+
const require_types = require("./types-Be2zbyeG.cjs");
|
|
6
|
+
const require_proof = require("./proof-DHu5zI0Z.cjs");
|
|
7
|
+
const require_kv_cache = require("./kv-cache-BM2-w-Wq.cjs");
|
|
8
8
|
let _logtape_logtape = require("@logtape/logtape");
|
|
9
9
|
let _fedify_uri_template = require("@fedify/uri-template");
|
|
10
10
|
let _fedify_vocab = require("@fedify/vocab");
|
|
11
11
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
12
12
|
let byte_encodings_hex = require("byte-encodings/hex");
|
|
13
|
-
let es_toolkit = require("es-toolkit");
|
|
14
13
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
14
|
+
let es_toolkit = require("es-toolkit");
|
|
15
15
|
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
16
16
|
let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
|
|
17
17
|
_fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
|
|
@@ -1458,6 +1458,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
1458
1458
|
//#endregion
|
|
1459
1459
|
//#region src/federation/keycache.ts
|
|
1460
1460
|
const Temporal$8 = require_http.Temporal;
|
|
1461
|
+
const KEY_CACHE_GENERATION = "2";
|
|
1461
1462
|
var KvKeyCache = class {
|
|
1462
1463
|
kv;
|
|
1463
1464
|
prefix;
|
|
@@ -1480,13 +1481,20 @@ var KvKeyCache = class {
|
|
|
1480
1481
|
keyId.href
|
|
1481
1482
|
];
|
|
1482
1483
|
}
|
|
1484
|
+
#entryKey(keyId) {
|
|
1485
|
+
return [
|
|
1486
|
+
...this.prefix,
|
|
1487
|
+
KEY_CACHE_GENERATION,
|
|
1488
|
+
keyId.href
|
|
1489
|
+
];
|
|
1490
|
+
}
|
|
1483
1491
|
async get(keyId) {
|
|
1484
1492
|
const negativeExpiration = this.nullKeys.get(keyId.href);
|
|
1485
1493
|
if (negativeExpiration != null) {
|
|
1486
1494
|
if (Temporal$8.Now.instant().until(negativeExpiration).sign >= 0) return null;
|
|
1487
1495
|
this.nullKeys.delete(keyId.href);
|
|
1488
1496
|
}
|
|
1489
|
-
const serialized = await this.kv.get(
|
|
1497
|
+
const serialized = await this.kv.get(this.#entryKey(keyId));
|
|
1490
1498
|
if (serialized === void 0) return void 0;
|
|
1491
1499
|
if (serialized === null) {
|
|
1492
1500
|
this.nullKeys.set(keyId.href, Temporal$8.Now.instant().add(this.unavailableKeyTtl));
|
|
@@ -1498,7 +1506,7 @@ var KvKeyCache = class {
|
|
|
1498
1506
|
try {
|
|
1499
1507
|
return await _fedify_vocab.Multikey.fromJsonLd(serialized, this.options);
|
|
1500
1508
|
} catch {
|
|
1501
|
-
await this.kv.delete(
|
|
1509
|
+
await this.kv.delete(this.#entryKey(keyId));
|
|
1502
1510
|
return;
|
|
1503
1511
|
}
|
|
1504
1512
|
}
|
|
@@ -1506,12 +1514,12 @@ var KvKeyCache = class {
|
|
|
1506
1514
|
async set(keyId, key) {
|
|
1507
1515
|
if (key == null) {
|
|
1508
1516
|
this.nullKeys.set(keyId.href, Temporal$8.Now.instant().add(this.unavailableKeyTtl));
|
|
1509
|
-
await this.kv.set(
|
|
1517
|
+
await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
|
|
1510
1518
|
return;
|
|
1511
1519
|
}
|
|
1512
1520
|
this.nullKeys.delete(keyId.href);
|
|
1513
1521
|
const serialized = await key.toJsonLd(this.options);
|
|
1514
|
-
await this.kv.set(
|
|
1522
|
+
await this.kv.set(this.#entryKey(keyId), serialized, { ttl: this.keyTtl });
|
|
1515
1523
|
}
|
|
1516
1524
|
async getFetchError(keyId) {
|
|
1517
1525
|
const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
|
|
@@ -2516,8 +2524,16 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
2516
2524
|
}
|
|
2517
2525
|
let json;
|
|
2518
2526
|
try {
|
|
2519
|
-
json = await request.clone().
|
|
2527
|
+
json = JSON.parse(await require_types.readBoundedText(request.clone(), require_types.MAX_BODY_SIZE, request.url));
|
|
2520
2528
|
} catch (error) {
|
|
2529
|
+
if (error instanceof require_types.BodyTooLargeError) {
|
|
2530
|
+
request.body?.cancel(error).catch(() => {});
|
|
2531
|
+
span.setStatus({
|
|
2532
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
2533
|
+
message: String(error)
|
|
2534
|
+
});
|
|
2535
|
+
return new Response("Inbox body too large.", { status: 413 });
|
|
2536
|
+
}
|
|
2521
2537
|
logger.error("Failed to parse JSON:\n{error}", {
|
|
2522
2538
|
recipient,
|
|
2523
2539
|
error
|
|
@@ -3737,7 +3753,7 @@ async function readLimitedResponseBody(response, maxBytes) {
|
|
|
3737
3753
|
if (truncated) result += "… (truncated)";
|
|
3738
3754
|
return result;
|
|
3739
3755
|
}
|
|
3740
|
-
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
3756
|
+
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
3741
3757
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
3742
3758
|
"fedify",
|
|
3743
3759
|
"federation",
|
|
@@ -3746,12 +3762,25 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
3746
3762
|
const federationMetrics = require_http.getFederationMetrics(meterProvider);
|
|
3747
3763
|
const started = performance.now();
|
|
3748
3764
|
let deliverySuccess = false;
|
|
3765
|
+
async function validateUrl(url) {
|
|
3766
|
+
if (!allowPrivateAddress) try {
|
|
3767
|
+
await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
|
|
3768
|
+
} catch (error) {
|
|
3769
|
+
if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
|
|
3770
|
+
url,
|
|
3771
|
+
error
|
|
3772
|
+
});
|
|
3773
|
+
throw error;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
await validateUrl(inbox.href);
|
|
3749
3777
|
headers = new Headers(headers);
|
|
3750
3778
|
headers.set("Content-Type", "application/activity+json");
|
|
3751
3779
|
const request = new Request(inbox, {
|
|
3752
3780
|
method: "POST",
|
|
3753
3781
|
headers,
|
|
3754
|
-
body: JSON.stringify(activity)
|
|
3782
|
+
body: JSON.stringify(activity),
|
|
3783
|
+
redirect: "manual"
|
|
3755
3784
|
});
|
|
3756
3785
|
let rsaKey = null;
|
|
3757
3786
|
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
@@ -3767,19 +3796,20 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
3767
3796
|
});
|
|
3768
3797
|
let response;
|
|
3769
3798
|
try {
|
|
3770
|
-
response = rsaKey == null ? await
|
|
3799
|
+
response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
|
|
3771
3800
|
tracerProvider,
|
|
3772
|
-
specDeterminer
|
|
3801
|
+
specDeterminer,
|
|
3802
|
+
validateRedirect: validateUrl
|
|
3773
3803
|
});
|
|
3774
3804
|
} catch (error) {
|
|
3775
|
-
const
|
|
3805
|
+
const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
|
|
3776
3806
|
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
3777
3807
|
activityId,
|
|
3778
3808
|
inbox: inbox.href,
|
|
3779
|
-
error:
|
|
3809
|
+
error: failure
|
|
3780
3810
|
});
|
|
3781
3811
|
federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
|
|
3782
|
-
throw
|
|
3812
|
+
throw failure;
|
|
3783
3813
|
}
|
|
3784
3814
|
try {
|
|
3785
3815
|
if (!response.ok) {
|
|
@@ -3816,6 +3846,49 @@ function createFetchError(url, cause) {
|
|
|
3816
3846
|
error.cause = cause;
|
|
3817
3847
|
return error;
|
|
3818
3848
|
}
|
|
3849
|
+
async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
3850
|
+
let body = await request.clone().text();
|
|
3851
|
+
for (let redirects = 0;; redirects++) {
|
|
3852
|
+
const response = await fetch(request, { redirect: "manual" });
|
|
3853
|
+
const location = response.headers.get("Location");
|
|
3854
|
+
if (![
|
|
3855
|
+
301,
|
|
3856
|
+
302,
|
|
3857
|
+
303,
|
|
3858
|
+
307,
|
|
3859
|
+
308
|
|
3860
|
+
].includes(response.status) || location == null) return response;
|
|
3861
|
+
await response.body?.cancel();
|
|
3862
|
+
if (redirects >= 20) throw new TypeError("Too many redirects");
|
|
3863
|
+
const url = new URL(location, request.url);
|
|
3864
|
+
await validateUrl(url.href);
|
|
3865
|
+
const headers = new Headers(request.headers);
|
|
3866
|
+
if (url.origin !== new URL(request.url).origin) {
|
|
3867
|
+
headers.delete("Authorization");
|
|
3868
|
+
headers.delete("Proxy-Authorization");
|
|
3869
|
+
headers.delete("Cookie");
|
|
3870
|
+
headers.delete("Host");
|
|
3871
|
+
}
|
|
3872
|
+
let method = request.method;
|
|
3873
|
+
if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
|
|
3874
|
+
method = "GET";
|
|
3875
|
+
body = void 0;
|
|
3876
|
+
for (const name of [
|
|
3877
|
+
"Content-Encoding",
|
|
3878
|
+
"Content-Language",
|
|
3879
|
+
"Content-Length",
|
|
3880
|
+
"Content-Location",
|
|
3881
|
+
"Content-Type"
|
|
3882
|
+
]) headers.delete(name);
|
|
3883
|
+
}
|
|
3884
|
+
request = new Request(url, {
|
|
3885
|
+
method,
|
|
3886
|
+
headers,
|
|
3887
|
+
body,
|
|
3888
|
+
redirect: "manual"
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3819
3892
|
/**
|
|
3820
3893
|
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
3821
3894
|
* It contains structured information about the failure, including the HTTP
|
|
@@ -5309,6 +5382,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5309
5382
|
if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
5310
5383
|
}
|
|
5311
5384
|
await sendActivity({
|
|
5385
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
5312
5386
|
keys,
|
|
5313
5387
|
activity: message.activity,
|
|
5314
5388
|
activityId: message.activityId,
|
|
@@ -5930,6 +6004,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5930
6004
|
});
|
|
5931
6005
|
const promises = [];
|
|
5932
6006
|
for (const inbox in inboxes) promises.push(sendActivity({
|
|
6007
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
5933
6008
|
keys,
|
|
5934
6009
|
activity: jsonLd,
|
|
5935
6010
|
activityId: activity.id?.href,
|
|
@@ -7264,6 +7339,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
7264
7339
|
else logger.debug("Forwarding activity immediately without queue since queue is not set.");
|
|
7265
7340
|
const promises = [];
|
|
7266
7341
|
for (const inbox in inboxes) promises.push(sendActivity({
|
|
7342
|
+
allowPrivateAddress: ctx.federation.allowPrivateAddress,
|
|
7267
7343
|
keys,
|
|
7268
7344
|
activity: ctx.activity,
|
|
7269
7345
|
activityId: ctx.activityId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { CryptographicKey, Multikey } from "@fedify/vocab";
|
|
3
|
-
import { DocumentLoader } from "@fedify/vocab-runtime";
|
|
2
|
+
import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
|
|
4
3
|
import { MeterProvider, TracerProvider } from "@opentelemetry/api";
|
|
4
|
+
import { DocumentLoader } from "@fedify/vocab-runtime";
|
|
5
5
|
|
|
6
6
|
//#region src/sig/key.d.ts
|
|
7
7
|
/**
|
|
@@ -59,6 +59,38 @@ interface FetchKeyOptions {
|
|
|
59
59
|
* @since 2.3.0
|
|
60
60
|
*/
|
|
61
61
|
meterProvider?: MeterProvider;
|
|
62
|
+
/**
|
|
63
|
+
* An assertion by the caller that it has already bound this key's id to
|
|
64
|
+
* whoever the key will be treated as speaking for, by means of its own that
|
|
65
|
+
* key resolution cannot see.
|
|
66
|
+
*
|
|
67
|
+
* Key resolution normally settles ownership itself: it dereferences the
|
|
68
|
+
* `owner`/`controller` a key claims and requires that actor's own document
|
|
69
|
+
* to link back to the key, because the claim and the key document come from
|
|
70
|
+
* the same host and so the claim proves nothing on its own. See
|
|
71
|
+
* GHSA-q9f8-5hc7-898f.
|
|
72
|
+
*
|
|
73
|
+
* That check presumes the ActivityPub shape of ownership, where the owner
|
|
74
|
+
* is an actor carrying a `publicKey`/`assertionMethod` list. It does not
|
|
75
|
+
* fit every caller. Portable objects (FEP-ef61) bind a key to an object
|
|
76
|
+
* structurally instead: {@link verifyPortableObjectProof} requires the
|
|
77
|
+
* verification method's FEP-ef34 origin to equal the object id's own
|
|
78
|
+
* authority, so the signer is pinned by the identifier being verified,
|
|
79
|
+
* before any key is fetched. Demanding the ActivityPub check as well would
|
|
80
|
+
* be impossible rather than merely redundant, since a DID dereferences to
|
|
81
|
+
* a DID document, which is not an actor.
|
|
82
|
+
*
|
|
83
|
+
* This is not a way to accept whatever a key says about itself. The claim
|
|
84
|
+
* still has to be corroborated, just from the key id rather than from
|
|
85
|
+
* a second document: a key may name an owner only when the key id is that
|
|
86
|
+
* owner's own (a `did:…#fragment` belongs to the DID it is a fragment of,
|
|
87
|
+
* exactly as a key embedded in an actor document belongs to that actor).
|
|
88
|
+
* A key naming anyone else falls back to the ordinary check and is rejected
|
|
89
|
+
* if it cannot pass it, so a key admitted here is as sound as any other and
|
|
90
|
+
* is cached like any other.
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
keyIdBoundByCaller?: boolean;
|
|
62
94
|
}
|
|
63
95
|
/**
|
|
64
96
|
* The result of {@link fetchKey}.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import { CryptographicKey, Multikey } from "@fedify/vocab";
|
|
3
|
-
import { MeterProvider, TracerProvider } from "@opentelemetry/api";
|
|
2
|
+
import { Actor, CryptographicKey, Multikey } from "@fedify/vocab";
|
|
4
3
|
import { DocumentLoader } from "@fedify/vocab-runtime";
|
|
4
|
+
import { MeterProvider, TracerProvider } from "@opentelemetry/api";
|
|
5
5
|
|
|
6
6
|
//#region src/sig/key.d.ts
|
|
7
7
|
/**
|
|
@@ -59,6 +59,38 @@ interface FetchKeyOptions {
|
|
|
59
59
|
* @since 2.3.0
|
|
60
60
|
*/
|
|
61
61
|
meterProvider?: MeterProvider;
|
|
62
|
+
/**
|
|
63
|
+
* An assertion by the caller that it has already bound this key's id to
|
|
64
|
+
* whoever the key will be treated as speaking for, by means of its own that
|
|
65
|
+
* key resolution cannot see.
|
|
66
|
+
*
|
|
67
|
+
* Key resolution normally settles ownership itself: it dereferences the
|
|
68
|
+
* `owner`/`controller` a key claims and requires that actor's own document
|
|
69
|
+
* to link back to the key, because the claim and the key document come from
|
|
70
|
+
* the same host and so the claim proves nothing on its own. See
|
|
71
|
+
* GHSA-q9f8-5hc7-898f.
|
|
72
|
+
*
|
|
73
|
+
* That check presumes the ActivityPub shape of ownership, where the owner
|
|
74
|
+
* is an actor carrying a `publicKey`/`assertionMethod` list. It does not
|
|
75
|
+
* fit every caller. Portable objects (FEP-ef61) bind a key to an object
|
|
76
|
+
* structurally instead: {@link verifyPortableObjectProof} requires the
|
|
77
|
+
* verification method's FEP-ef34 origin to equal the object id's own
|
|
78
|
+
* authority, so the signer is pinned by the identifier being verified,
|
|
79
|
+
* before any key is fetched. Demanding the ActivityPub check as well would
|
|
80
|
+
* be impossible rather than merely redundant, since a DID dereferences to
|
|
81
|
+
* a DID document, which is not an actor.
|
|
82
|
+
*
|
|
83
|
+
* This is not a way to accept whatever a key says about itself. The claim
|
|
84
|
+
* still has to be corroborated, just from the key id rather than from
|
|
85
|
+
* a second document: a key may name an owner only when the key id is that
|
|
86
|
+
* owner's own (a `did:…#fragment` belongs to the DID it is a fragment of,
|
|
87
|
+
* exactly as a key embedded in an actor document belongs to that actor).
|
|
88
|
+
* A key naming anyone else falls back to the ordinary check and is rejected
|
|
89
|
+
* if it cannot pass it, so a key admitted here is as sound as any other and
|
|
90
|
+
* is cached like any other.
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
keyIdBoundByCaller?: boolean;
|
|
62
94
|
}
|
|
63
95
|
/**
|
|
64
96
|
* The result of {@link fetchKey}.
|
|
@@ -4,13 +4,13 @@ import { getLogger } from "@logtape/logtape";
|
|
|
4
4
|
import { CryptographicKey, Multikey, Object as Object$1, isActor } from "@fedify/vocab";
|
|
5
5
|
import { SpanKind, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
6
6
|
import { encodeHex } from "byte-encodings/hex";
|
|
7
|
-
import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
|
|
8
7
|
import { FetchError, getDocumentLoader, parseDidKeyVerificationMethod } from "@fedify/vocab-runtime";
|
|
8
|
+
import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
|
|
9
9
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
10
10
|
import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
|
|
11
11
|
//#region deno.json
|
|
12
12
|
var name = "@fedify/fedify";
|
|
13
|
-
var version = "2.4.0-dev.
|
|
13
|
+
var version = "2.4.0-dev.1988+c5c9d617";
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/sig/accept.ts
|
|
16
16
|
/**
|
|
@@ -1040,6 +1040,118 @@ async function importJwk(jwk, type) {
|
|
|
1040
1040
|
validateCryptoKey(key, type);
|
|
1041
1041
|
return key;
|
|
1042
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Fetches the document that the given actor URI dereferences to, and returns
|
|
1045
|
+
* the actor only if the host that served the document is authoritative for
|
|
1046
|
+
* the id the document claims.
|
|
1047
|
+
*
|
|
1048
|
+
* Only the origin that serves an actor id can speak for it. Without that
|
|
1049
|
+
* rule any host could serve a document describing somebody else's actor—and
|
|
1050
|
+
* listing its own keys as that actor's.
|
|
1051
|
+
*
|
|
1052
|
+
* @param actorId The URI of the actor to fetch.
|
|
1053
|
+
* @param options Options for fetching the document.
|
|
1054
|
+
* @returns The actor, or `null` if the document cannot be fetched, is not an
|
|
1055
|
+
* actor, or belongs to another origin.
|
|
1056
|
+
* @internal
|
|
1057
|
+
*/
|
|
1058
|
+
async function fetchActorDocument(actorId, options = {}) {
|
|
1059
|
+
const logger = getLogger([
|
|
1060
|
+
"fedify",
|
|
1061
|
+
"sig",
|
|
1062
|
+
"key"
|
|
1063
|
+
]);
|
|
1064
|
+
const documentLoader = options.documentLoader ?? getDocumentLoader();
|
|
1065
|
+
const contextLoader = options.contextLoader ?? getDocumentLoader();
|
|
1066
|
+
const { tracerProvider } = options;
|
|
1067
|
+
let document;
|
|
1068
|
+
let documentUrl = actorId;
|
|
1069
|
+
try {
|
|
1070
|
+
const remoteDocument = await documentLoader(actorId.href);
|
|
1071
|
+
document = remoteDocument.document;
|
|
1072
|
+
documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
logger.debug("Failed to fetch the actor {actorId}: {error}", {
|
|
1075
|
+
actorId: actorId.href,
|
|
1076
|
+
error
|
|
1077
|
+
});
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
let object;
|
|
1081
|
+
try {
|
|
1082
|
+
object = await Object$1.fromJsonLd(document, {
|
|
1083
|
+
documentLoader,
|
|
1084
|
+
contextLoader,
|
|
1085
|
+
tracerProvider,
|
|
1086
|
+
baseUrl: documentUrl
|
|
1087
|
+
});
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
if (!(error instanceof TypeError)) throw error;
|
|
1090
|
+
logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
|
|
1091
|
+
documentUrl: documentUrl.href,
|
|
1092
|
+
error
|
|
1093
|
+
});
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
if (!isActor(object)) return null;
|
|
1097
|
+
if (object.id == null || object.id.origin !== documentUrl.origin) {
|
|
1098
|
+
logger.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as that actor's document.", {
|
|
1099
|
+
documentUrl: documentUrl.href,
|
|
1100
|
+
actorId: object.id?.href
|
|
1101
|
+
});
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
return object;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Resolves the owner that the given key claims, and returns it only if the
|
|
1108
|
+
* claim holds.
|
|
1109
|
+
*
|
|
1110
|
+
* The `owner` of a {@link CryptographicKey}, like the `controller` of
|
|
1111
|
+
* a {@link Multikey}, proves nothing on its own: the key document and the
|
|
1112
|
+
* claim inside it are served by the same host, so anyone able to serve a key
|
|
1113
|
+
* document can name any actor in the world as its owner. The claim becomes
|
|
1114
|
+
* meaningful only once the named actor's own document is fetched and turns
|
|
1115
|
+
* out to link back to the key. This function performs that mutual-link
|
|
1116
|
+
* check, which is the only sound way to learn a fetched key's owner.
|
|
1117
|
+
*
|
|
1118
|
+
* The claimed owner is always dereferenced, never read out of the key
|
|
1119
|
+
* document: an owner embedded there is written by whoever wrote the key.
|
|
1120
|
+
*
|
|
1121
|
+
* @param key The key whose ownership claim is to be verified. It must carry
|
|
1122
|
+
* an `id`, as that is what the owner has to link back to.
|
|
1123
|
+
* @param options Options for fetching the claimed owner.
|
|
1124
|
+
* @returns The verified owner, or `null` if the key claims no owner, the
|
|
1125
|
+
* owner cannot be fetched, or the owner does not link back to
|
|
1126
|
+
* the key.
|
|
1127
|
+
* @internal
|
|
1128
|
+
*/
|
|
1129
|
+
async function verifyKeyOwnership(key, options = {}) {
|
|
1130
|
+
const logger = getLogger([
|
|
1131
|
+
"fedify",
|
|
1132
|
+
"sig",
|
|
1133
|
+
"key"
|
|
1134
|
+
]);
|
|
1135
|
+
const keyId = key.id;
|
|
1136
|
+
if (keyId == null) return null;
|
|
1137
|
+
const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
|
|
1138
|
+
if (claimedOwnerId == null) return null;
|
|
1139
|
+
const owner = await fetchActorDocument(claimedOwnerId, options);
|
|
1140
|
+
if (owner == null) {
|
|
1141
|
+
logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
|
|
1142
|
+
keyId: keyId.href,
|
|
1143
|
+
claimedOwnerId: claimedOwnerId.href
|
|
1144
|
+
});
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
|
|
1148
|
+
for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
|
|
1149
|
+
logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
|
|
1150
|
+
keyId: keyId.href,
|
|
1151
|
+
claimedOwnerId: claimedOwnerId.href
|
|
1152
|
+
});
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1043
1155
|
async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
|
|
1044
1156
|
tracerProvider ??= trace.getTracerProvider();
|
|
1045
1157
|
return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
|
|
@@ -1173,6 +1285,18 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
|
|
|
1173
1285
|
async function clearFetchErrorMetadata(keyId, keyCache) {
|
|
1174
1286
|
await keyCache?.setFetchError?.(keyId, null);
|
|
1175
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Returns the identifier a fragment-bearing DID URL is a fragment of, or
|
|
1290
|
+
* `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
|
|
1291
|
+
* traced back to `did:web:example.com`, the DID that resolving the key went
|
|
1292
|
+
* through in the first place.
|
|
1293
|
+
*/
|
|
1294
|
+
function stripFragment(keyId) {
|
|
1295
|
+
if (keyId.protocol !== "did:") return null;
|
|
1296
|
+
const did = new URL(keyId.href);
|
|
1297
|
+
did.hash = "";
|
|
1298
|
+
return did;
|
|
1299
|
+
}
|
|
1176
1300
|
function isDidKeyUrl(keyId) {
|
|
1177
1301
|
return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
|
|
1178
1302
|
}
|
|
@@ -1228,7 +1352,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
|
|
|
1228
1352
|
cached: false
|
|
1229
1353
|
};
|
|
1230
1354
|
}
|
|
1231
|
-
async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
|
|
1355
|
+
async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller }, logger) {
|
|
1232
1356
|
let object;
|
|
1233
1357
|
try {
|
|
1234
1358
|
object = await Object$1.fromJsonLd(document, {
|
|
@@ -1258,8 +1382,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
|
|
|
1258
1382
|
}
|
|
1259
1383
|
}
|
|
1260
1384
|
let key = null;
|
|
1261
|
-
|
|
1385
|
+
let ownerDocument = null;
|
|
1386
|
+
if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
|
|
1262
1387
|
else if (isActor(object)) {
|
|
1388
|
+
if (object.id == null || object.id.origin !== documentUrl.origin) {
|
|
1389
|
+
logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
|
|
1390
|
+
keyId,
|
|
1391
|
+
documentUrl: documentUrl.href,
|
|
1392
|
+
actorId: object.id?.href
|
|
1393
|
+
});
|
|
1394
|
+
await keyCache?.set(cacheKey, null);
|
|
1395
|
+
return {
|
|
1396
|
+
key: null,
|
|
1397
|
+
cached: false
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
ownerDocument = object;
|
|
1263
1401
|
const keys = cls === CryptographicKey ? object.getPublicKeys({
|
|
1264
1402
|
documentLoader,
|
|
1265
1403
|
contextLoader,
|
|
@@ -1321,6 +1459,27 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
|
|
|
1321
1459
|
cached: false
|
|
1322
1460
|
};
|
|
1323
1461
|
}
|
|
1462
|
+
const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
|
|
1463
|
+
if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
|
|
1464
|
+
if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
|
|
1465
|
+
else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
|
|
1466
|
+
if (await verifyKeyOwnership(key, {
|
|
1467
|
+
documentLoader,
|
|
1468
|
+
contextLoader,
|
|
1469
|
+
tracerProvider
|
|
1470
|
+
}) == null) {
|
|
1471
|
+
logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
|
|
1472
|
+
keyId,
|
|
1473
|
+
claimedOwnerId: claimedOwnerId.href
|
|
1474
|
+
});
|
|
1475
|
+
await keyCache?.set(cacheKey, null);
|
|
1476
|
+
return {
|
|
1477
|
+
key: null,
|
|
1478
|
+
cached: false
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1324
1483
|
if (keyCache != null) {
|
|
1325
1484
|
await keyCache.set(cacheKey, key);
|
|
1326
1485
|
logger.debug("Key {keyId} cached.", { keyId });
|
|
@@ -1359,15 +1518,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
|
|
|
1359
1518
|
}
|
|
1360
1519
|
logger.debug("Fetching key {keyId} to verify signature...", { keyId });
|
|
1361
1520
|
let document;
|
|
1521
|
+
let documentUrl = cacheKey;
|
|
1362
1522
|
try {
|
|
1363
|
-
|
|
1523
|
+
const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
|
|
1524
|
+
document = remoteDocument.document;
|
|
1525
|
+
documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
|
|
1364
1526
|
} catch (error) {
|
|
1365
1527
|
const classified = classifyFetchError(error);
|
|
1366
1528
|
const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
|
|
1367
1529
|
outcome = classified;
|
|
1368
1530
|
return errored;
|
|
1369
1531
|
}
|
|
1370
|
-
const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
|
|
1532
|
+
const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
|
|
1371
1533
|
outcome = { result: resolved.key != null ? "fetched" : "invalid" };
|
|
1372
1534
|
return resolved;
|
|
1373
1535
|
} finally {
|
|
@@ -2482,4 +2644,4 @@ function timingSafeEqual(a, b) {
|
|
|
2482
2644
|
return result === 0;
|
|
2483
2645
|
}
|
|
2484
2646
|
//#endregion
|
|
2485
|
-
export {
|
|
2647
|
+
export { recordWebFingerHandle as A, recordCollectionRequest as C, recordInboxActivity as D, recordFanoutRecipients as E, validateAcceptSignature as F, name as I, version as L, formatAcceptSignature as M, fulfillAcceptSignature as N, recordOutboxActivity as O, parseAcceptSignature as P, recordCollectionPageItems as S, recordDocumentCache as T, instrumentDocumentLoader as _, verifyRequestDetailed as a, recordCircuitBreakerStateChange as b, fetchKey as c, importJwk as d, validateCryptoKey as f, getRemoteHost as g, getFederationMetrics as h, verifyRequest as i, registerQueueDepthGauge as j, recordOutboxEnqueue as k, fetchKeyDetailed as l, getDurationMs as m, parseRfc9421SignatureInput as n, exportJwk as o, verifyKeyOwnership as p, signRequest as r, fetchActorDocument as s, doubleKnock as t, generateCryptoKeyPair as u, isAbortError$1 as v, recordCollectionTotalItems as w, recordCollectionDispatchDuration as x, measureSignatureKeyFetch as y };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as version, t as name } from "./deno-
|
|
5
|
-
import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-
|
|
4
|
+
import { n as version, t as name } from "./deno-BgMEefEr.mjs";
|
|
5
|
+
import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CrqsHP0l.mjs";
|
|
6
6
|
import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { i as fetchKeyDetailed, s as validateCryptoKey } from "./key-Cwbg5ggV.mjs";
|
|
8
8
|
import { getLogger } from "@logtape/logtape";
|
|
9
9
|
import { CryptographicKey } from "@fedify/vocab";
|
|
10
10
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|