@fedify/fedify 2.4.0-dev.1964 → 2.4.0-dev.1995
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-B4zpKIFU.mjs → builder-D0DxgOV5.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/compound-proof-CJnWc6c-.mjs +495 -0
- 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-CtG93ohj.mjs → deno-CABhzCb_.mjs} +1 -1
- package/dist/{docloader-dk_k3S8d.mjs → docloader-BbGSL0Xq.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +152 -0
- 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-Drjlrrax.cjs → federation-DCEHQk4G.cjs} +632 -57
- package/dist/{http-6TI0tI03.d.cts → http-65PYi9jY.d.ts} +34 -2
- package/dist/{http-Dj4s1UQ0.js → http-9WyuldYh.js} +170 -8
- package/dist/{http-CdrSn2ip.d.ts → http-CP-7OdSq.d.cts} +34 -2
- package/dist/{http-B3skRvGo.cjs → http-CRFns-UE.cjs} +181 -7
- package/dist/{http-D7LX-BL9.mjs → http-D9urSO2D.mjs} +3 -3
- package/dist/{key-BVjgbs_9.mjs → key-Ccs8cnd7.mjs} +169 -7
- package/dist/{keycache-ZLqIfrkL.mjs → keycache-BUDHB5rY.mjs} +12 -4
- package/dist/{kv-cache-DtsjH_bY.cjs → kv-cache-DXT9vmIg.cjs} +2 -2
- package/dist/{kv-cache-tGNbvDCF.js → kv-cache-DtmXhPXG.js} +2 -2
- package/dist/{kv-cache-xRcC_xxP.mjs → kv-cache-lKyFn_Dn.mjs} +1 -1
- package/dist/{ld-OuCeQFny.mjs → ld-qup__MAW.mjs} +3 -3
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-B9N27cCB.mjs → metrics-CISXCvB1.mjs} +1 -1
- package/dist/{middleware-TeLjzoxn.mjs → middleware-CzvJvPHK.mjs} +58 -15
- package/dist/{middleware-Cd_vPqDH.mjs → middleware-DkrU2WhY.mjs} +1 -1
- package/dist/{middleware-CPTDFMmJ.js → middleware-DsfWajaD.js} +593 -18
- 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 +20 -20
- 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-DGSfntI-.d.ts → owner-BF0L9cMt.d.cts} +12 -1
- package/dist/{proof-BO_esQii.js → owner-BIPEZGiU.js} +1255 -1098
- package/dist/{owner-r_waTiQj.d.cts → owner-BSzr5-I8.d.ts} +12 -1
- package/dist/owner-CRoq8Spb.mjs +175 -0
- package/dist/{proof-Dua0d906.cjs → owner-CVA7L9MA.cjs} +1266 -1097
- package/dist/{proof-D5Ajk4DJ.mjs → proof-BDxNVZ3E.mjs} +186 -83
- package/dist/{send-DVXyUqhO.mjs → send-BzM5GLyn.mjs} +68 -11
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +465 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +324 -0
- 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 +17 -17
- 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 +120 -1
- package/dist/{temporal-BoDw51A-.mjs → temporal-Dv6Qv-8k.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 +6 -6
- package/dist/owner-DTEqTzSr.mjs +0 -120
|
@@ -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}.
|
|
@@ -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";
|
|
9
|
-
import {
|
|
8
|
+
import { Item, decodeDict, encodeDict, encodeItem } from "structured-field-values";
|
|
10
9
|
import { decodeBase64, encodeBase64 } from "byte-encodings/base64";
|
|
10
|
+
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
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.1995+618fd4e7";
|
|
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,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}.
|
|
@@ -3,13 +3,13 @@ let _logtape_logtape = require("@logtape/logtape");
|
|
|
3
3
|
let _fedify_vocab = require("@fedify/vocab");
|
|
4
4
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
5
5
|
let byte_encodings_hex = require("byte-encodings/hex");
|
|
6
|
-
let structured_field_values = require("structured-field-values");
|
|
7
6
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
8
|
-
let
|
|
7
|
+
let structured_field_values = require("structured-field-values");
|
|
9
8
|
let byte_encodings_base64 = require("byte-encodings/base64");
|
|
9
|
+
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
10
10
|
//#region deno.json
|
|
11
11
|
var name = "@fedify/fedify";
|
|
12
|
-
var version = "2.4.0-dev.
|
|
12
|
+
var version = "2.4.0-dev.1995+618fd4e7";
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region ../../node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/root.js
|
|
15
15
|
const NativeTemporal = globalThis.Temporal;
|
|
@@ -4603,6 +4603,118 @@ async function importJwk(jwk, type) {
|
|
|
4603
4603
|
validateCryptoKey(key, type);
|
|
4604
4604
|
return key;
|
|
4605
4605
|
}
|
|
4606
|
+
/**
|
|
4607
|
+
* Fetches the document that the given actor URI dereferences to, and returns
|
|
4608
|
+
* the actor only if the host that served the document is authoritative for
|
|
4609
|
+
* the id the document claims.
|
|
4610
|
+
*
|
|
4611
|
+
* Only the origin that serves an actor id can speak for it. Without that
|
|
4612
|
+
* rule any host could serve a document describing somebody else's actor—and
|
|
4613
|
+
* listing its own keys as that actor's.
|
|
4614
|
+
*
|
|
4615
|
+
* @param actorId The URI of the actor to fetch.
|
|
4616
|
+
* @param options Options for fetching the document.
|
|
4617
|
+
* @returns The actor, or `null` if the document cannot be fetched, is not an
|
|
4618
|
+
* actor, or belongs to another origin.
|
|
4619
|
+
* @internal
|
|
4620
|
+
*/
|
|
4621
|
+
async function fetchActorDocument(actorId, options = {}) {
|
|
4622
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
4623
|
+
"fedify",
|
|
4624
|
+
"sig",
|
|
4625
|
+
"key"
|
|
4626
|
+
]);
|
|
4627
|
+
const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
4628
|
+
const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
4629
|
+
const { tracerProvider } = options;
|
|
4630
|
+
let document;
|
|
4631
|
+
let documentUrl = actorId;
|
|
4632
|
+
try {
|
|
4633
|
+
const remoteDocument = await documentLoader(actorId.href);
|
|
4634
|
+
document = remoteDocument.document;
|
|
4635
|
+
documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
|
|
4636
|
+
} catch (error) {
|
|
4637
|
+
logger.debug("Failed to fetch the actor {actorId}: {error}", {
|
|
4638
|
+
actorId: actorId.href,
|
|
4639
|
+
error
|
|
4640
|
+
});
|
|
4641
|
+
return null;
|
|
4642
|
+
}
|
|
4643
|
+
let object;
|
|
4644
|
+
try {
|
|
4645
|
+
object = await _fedify_vocab.Object.fromJsonLd(document, {
|
|
4646
|
+
documentLoader,
|
|
4647
|
+
contextLoader,
|
|
4648
|
+
tracerProvider,
|
|
4649
|
+
baseUrl: documentUrl
|
|
4650
|
+
});
|
|
4651
|
+
} catch (error) {
|
|
4652
|
+
if (!(error instanceof TypeError)) throw error;
|
|
4653
|
+
logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
|
|
4654
|
+
documentUrl: documentUrl.href,
|
|
4655
|
+
error
|
|
4656
|
+
});
|
|
4657
|
+
return null;
|
|
4658
|
+
}
|
|
4659
|
+
if (!(0, _fedify_vocab.isActor)(object)) return null;
|
|
4660
|
+
if (object.id == null || object.id.origin !== documentUrl.origin) {
|
|
4661
|
+
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.", {
|
|
4662
|
+
documentUrl: documentUrl.href,
|
|
4663
|
+
actorId: object.id?.href
|
|
4664
|
+
});
|
|
4665
|
+
return null;
|
|
4666
|
+
}
|
|
4667
|
+
return object;
|
|
4668
|
+
}
|
|
4669
|
+
/**
|
|
4670
|
+
* Resolves the owner that the given key claims, and returns it only if the
|
|
4671
|
+
* claim holds.
|
|
4672
|
+
*
|
|
4673
|
+
* The `owner` of a {@link CryptographicKey}, like the `controller` of
|
|
4674
|
+
* a {@link Multikey}, proves nothing on its own: the key document and the
|
|
4675
|
+
* claim inside it are served by the same host, so anyone able to serve a key
|
|
4676
|
+
* document can name any actor in the world as its owner. The claim becomes
|
|
4677
|
+
* meaningful only once the named actor's own document is fetched and turns
|
|
4678
|
+
* out to link back to the key. This function performs that mutual-link
|
|
4679
|
+
* check, which is the only sound way to learn a fetched key's owner.
|
|
4680
|
+
*
|
|
4681
|
+
* The claimed owner is always dereferenced, never read out of the key
|
|
4682
|
+
* document: an owner embedded there is written by whoever wrote the key.
|
|
4683
|
+
*
|
|
4684
|
+
* @param key The key whose ownership claim is to be verified. It must carry
|
|
4685
|
+
* an `id`, as that is what the owner has to link back to.
|
|
4686
|
+
* @param options Options for fetching the claimed owner.
|
|
4687
|
+
* @returns The verified owner, or `null` if the key claims no owner, the
|
|
4688
|
+
* owner cannot be fetched, or the owner does not link back to
|
|
4689
|
+
* the key.
|
|
4690
|
+
* @internal
|
|
4691
|
+
*/
|
|
4692
|
+
async function verifyKeyOwnership(key, options = {}) {
|
|
4693
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
4694
|
+
"fedify",
|
|
4695
|
+
"sig",
|
|
4696
|
+
"key"
|
|
4697
|
+
]);
|
|
4698
|
+
const keyId = key.id;
|
|
4699
|
+
if (keyId == null) return null;
|
|
4700
|
+
const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
|
|
4701
|
+
if (claimedOwnerId == null) return null;
|
|
4702
|
+
const owner = await fetchActorDocument(claimedOwnerId, options);
|
|
4703
|
+
if (owner == null) {
|
|
4704
|
+
logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
|
|
4705
|
+
keyId: keyId.href,
|
|
4706
|
+
claimedOwnerId: claimedOwnerId.href
|
|
4707
|
+
});
|
|
4708
|
+
return null;
|
|
4709
|
+
}
|
|
4710
|
+
const linkedKeyIds = key instanceof _fedify_vocab.CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
|
|
4711
|
+
for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
|
|
4712
|
+
logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
|
|
4713
|
+
keyId: keyId.href,
|
|
4714
|
+
claimedOwnerId: claimedOwnerId.href
|
|
4715
|
+
});
|
|
4716
|
+
return null;
|
|
4717
|
+
}
|
|
4606
4718
|
async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
|
|
4607
4719
|
tracerProvider ??= _opentelemetry_api.trace.getTracerProvider();
|
|
4608
4720
|
return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
|
|
@@ -4736,6 +4848,18 @@ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
|
|
|
4736
4848
|
async function clearFetchErrorMetadata(keyId, keyCache) {
|
|
4737
4849
|
await keyCache?.setFetchError?.(keyId, null);
|
|
4738
4850
|
}
|
|
4851
|
+
/**
|
|
4852
|
+
* Returns the identifier a fragment-bearing DID URL is a fragment of, or
|
|
4853
|
+
* `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
|
|
4854
|
+
* traced back to `did:web:example.com`, the DID that resolving the key went
|
|
4855
|
+
* through in the first place.
|
|
4856
|
+
*/
|
|
4857
|
+
function stripFragment(keyId) {
|
|
4858
|
+
if (keyId.protocol !== "did:") return null;
|
|
4859
|
+
const did = new URL(keyId.href);
|
|
4860
|
+
did.hash = "";
|
|
4861
|
+
return did;
|
|
4862
|
+
}
|
|
4739
4863
|
function isDidKeyUrl(keyId) {
|
|
4740
4864
|
return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
|
|
4741
4865
|
}
|
|
@@ -4791,7 +4915,7 @@ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
|
|
|
4791
4915
|
cached: false
|
|
4792
4916
|
};
|
|
4793
4917
|
}
|
|
4794
|
-
async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
|
|
4918
|
+
async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller }, logger) {
|
|
4795
4919
|
let object;
|
|
4796
4920
|
try {
|
|
4797
4921
|
object = await _fedify_vocab.Object.fromJsonLd(document, {
|
|
@@ -4821,8 +4945,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
|
|
|
4821
4945
|
}
|
|
4822
4946
|
}
|
|
4823
4947
|
let key = null;
|
|
4824
|
-
|
|
4948
|
+
let ownerDocument = null;
|
|
4949
|
+
if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
|
|
4825
4950
|
else if ((0, _fedify_vocab.isActor)(object)) {
|
|
4951
|
+
if (object.id == null || object.id.origin !== documentUrl.origin) {
|
|
4952
|
+
logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
|
|
4953
|
+
keyId,
|
|
4954
|
+
documentUrl: documentUrl.href,
|
|
4955
|
+
actorId: object.id?.href
|
|
4956
|
+
});
|
|
4957
|
+
await keyCache?.set(cacheKey, null);
|
|
4958
|
+
return {
|
|
4959
|
+
key: null,
|
|
4960
|
+
cached: false
|
|
4961
|
+
};
|
|
4962
|
+
}
|
|
4963
|
+
ownerDocument = object;
|
|
4826
4964
|
const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
|
|
4827
4965
|
documentLoader,
|
|
4828
4966
|
contextLoader,
|
|
@@ -4884,6 +5022,27 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
|
|
|
4884
5022
|
cached: false
|
|
4885
5023
|
};
|
|
4886
5024
|
}
|
|
5025
|
+
const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
|
|
5026
|
+
if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
|
|
5027
|
+
if (ownerDocument != null && claimedOwnerId == null) key = key instanceof _fedify_vocab.CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
|
|
5028
|
+
else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
|
|
5029
|
+
if (await verifyKeyOwnership(key, {
|
|
5030
|
+
documentLoader,
|
|
5031
|
+
contextLoader,
|
|
5032
|
+
tracerProvider
|
|
5033
|
+
}) == null) {
|
|
5034
|
+
logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
|
|
5035
|
+
keyId,
|
|
5036
|
+
claimedOwnerId: claimedOwnerId.href
|
|
5037
|
+
});
|
|
5038
|
+
await keyCache?.set(cacheKey, null);
|
|
5039
|
+
return {
|
|
5040
|
+
key: null,
|
|
5041
|
+
cached: false
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
4887
5046
|
if (keyCache != null) {
|
|
4888
5047
|
await keyCache.set(cacheKey, key);
|
|
4889
5048
|
logger.debug("Key {keyId} cached.", { keyId });
|
|
@@ -4922,15 +5081,18 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
|
|
|
4922
5081
|
}
|
|
4923
5082
|
logger.debug("Fetching key {keyId} to verify signature...", { keyId });
|
|
4924
5083
|
let document;
|
|
5084
|
+
let documentUrl = cacheKey;
|
|
4925
5085
|
try {
|
|
4926
|
-
|
|
5086
|
+
const remoteDocument = await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId);
|
|
5087
|
+
document = remoteDocument.document;
|
|
5088
|
+
documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
|
|
4927
5089
|
} catch (error) {
|
|
4928
5090
|
const classified = classifyFetchError(error);
|
|
4929
5091
|
const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
|
|
4930
5092
|
outcome = classified;
|
|
4931
5093
|
return errored;
|
|
4932
5094
|
}
|
|
4933
|
-
const resolved = await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
|
|
5095
|
+
const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
|
|
4934
5096
|
outcome = { result: resolved.key != null ? "fetched" : "invalid" };
|
|
4935
5097
|
return resolved;
|
|
4936
5098
|
} finally {
|
|
@@ -6064,6 +6226,12 @@ Object.defineProperty(exports, "exportJwk", {
|
|
|
6064
6226
|
return exportJwk;
|
|
6065
6227
|
}
|
|
6066
6228
|
});
|
|
6229
|
+
Object.defineProperty(exports, "fetchActorDocument", {
|
|
6230
|
+
enumerable: true,
|
|
6231
|
+
get: function() {
|
|
6232
|
+
return fetchActorDocument;
|
|
6233
|
+
}
|
|
6234
|
+
});
|
|
6067
6235
|
Object.defineProperty(exports, "fetchKey", {
|
|
6068
6236
|
enumerable: true,
|
|
6069
6237
|
get: function() {
|
|
@@ -6244,6 +6412,12 @@ Object.defineProperty(exports, "validateCryptoKey", {
|
|
|
6244
6412
|
return validateCryptoKey;
|
|
6245
6413
|
}
|
|
6246
6414
|
});
|
|
6415
|
+
Object.defineProperty(exports, "verifyKeyOwnership", {
|
|
6416
|
+
enumerable: true,
|
|
6417
|
+
get: function() {
|
|
6418
|
+
return verifyKeyOwnership;
|
|
6419
|
+
}
|
|
6420
|
+
});
|
|
6247
6421
|
Object.defineProperty(exports, "verifyRequest", {
|
|
6248
6422
|
enumerable: true,
|
|
6249
6423
|
get: function() {
|
|
@@ -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-CABhzCb_.mjs";
|
|
5
|
+
import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CISXCvB1.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-Ccs8cnd7.mjs";
|
|
8
8
|
import { getLogger } from "@logtape/logtape";
|
|
9
9
|
import { CryptographicKey } from "@fedify/vocab";
|
|
10
10
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|