@fedify/fedify 2.4.0-dev.1727 → 2.4.0-dev.1740
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-f5-YwHfj.mjs → builder-Bw6LVKGw.mjs} +2 -2
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{deno-Deyoxesx.mjs → deno-tRdp8MCl.mjs} +1 -1
- package/dist/{docloader-B3kuDBg9.mjs → docloader-Dg_7gMC7.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/handler.test.mjs +3 -3
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.js +1 -1
- package/dist/federation/send.test.mjs +3 -3
- 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-BzRjeOky.cjs → federation-BJBpt_Am.cjs} +3 -3
- package/dist/{http-Bw6s_dOB.js → http-BIxGa6AG.js} +1 -1
- package/dist/{http-TjhZfiSN.mjs → http-Day1kcpT.mjs} +3 -3
- package/dist/{http-AyKngJ_h.cjs → http-DggG1scN.cjs} +1 -1
- package/dist/{key-b4kf4yZT.mjs → key-igh0AMsL.mjs} +2 -2
- package/dist/{kv-cache-CQsh-KUw.js → kv-cache-BIAB0HKx.js} +1 -1
- package/dist/{kv-cache-CpILvhUV.mjs → kv-cache-Bz6aQOOX.mjs} +1 -1
- package/dist/{kv-cache-BTVaZrf-.cjs → kv-cache-C4U_uBqD.cjs} +1 -1
- package/dist/{ld-CKujkAfG.mjs → ld-DnT9sCWa.mjs} +3 -3
- package/dist/{metrics-AlCGTDrr.mjs → metrics-DbdNnWIt.mjs} +1 -1
- package/dist/{middleware-LPbZz6-1.mjs → middleware-B6GweVcg.mjs} +1 -1
- package/dist/{middleware-CZcuedl-.js → middleware-CnsPXG0O.js} +3 -3
- package/dist/{middleware-C9Y5lLZN.mjs → middleware-DPIS4fsB.mjs} +12 -12
- package/dist/{mod-CuDDaIIt.d.cts → mod-CnGLNoVw.d.cts} +85 -3
- package/dist/{mod-D-IhOEOP.d.ts → mod-DzRXACM5.d.ts} +85 -3
- package/dist/mod.cjs +5 -4
- package/dist/mod.d.cts +3 -3
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-D1O8v8FB.mjs → owner-B3ef8GfJ.mjs} +2 -2
- package/dist/{proof-BTBolSnd.js → proof-BBLQXRWw.js} +603 -31
- package/dist/{proof-DcSm-2eG.cjs → proof-Cqk2TcLX.cjs} +607 -29
- package/dist/proof-DVoDZ8d7.mjs +890 -0
- package/dist/{send-DG7JG280.mjs → send-PbBSZ7Qk.mjs} +3 -3
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +3 -2
- package/dist/sig/mod.d.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/proof.test.mjs +956 -8
- package/dist/{temporal-DA3jx8x1.mjs → temporal-DLQIjuTn.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +6 -6
- package/dist/proof-DeMR7Akd.mjs +0 -316
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { n as version, t as name } from "./deno-tRdp8MCl.mjs";
|
|
5
|
+
import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-DbdNnWIt.mjs";
|
|
6
|
+
import { n as fetchKey, o as validateCryptoKey } from "./key-igh0AMsL.mjs";
|
|
7
|
+
import { c as getNormalizationContextLoader } from "./ld-DnT9sCWa.mjs";
|
|
8
|
+
import { n as preloadedOnlyDocumentLoader } from "./public-audience--9hx7IGx.mjs";
|
|
9
|
+
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BQSHRLhg.mjs";
|
|
10
|
+
import { getLogger } from "@logtape/logtape";
|
|
11
|
+
import { Activity, DataIntegrityProof, Multikey, getTypeId } from "@fedify/vocab";
|
|
12
|
+
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
13
|
+
import { formatIri, getDocumentLoader, getFe34Origin, haveSameFe34Origin, parseIri } from "@fedify/vocab-runtime";
|
|
14
|
+
import { encodeHex } from "byte-encodings/hex";
|
|
15
|
+
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
16
|
+
import serialize from "json-canon";
|
|
17
|
+
//#region src/sig/proof.ts
|
|
18
|
+
/**
|
|
19
|
+
* Known Object Integrity Proof `cryptosuite` values, used to keep
|
|
20
|
+
* `object_integrity_proofs.cryptosuite` on a bounded set of spec-defined
|
|
21
|
+
* string values. Fedify currently signs and verifies only
|
|
22
|
+
* `eddsa-jcs-2022`; other values come in only from external proofs and are
|
|
23
|
+
* dropped from the metric attribute to avoid attacker-controlled
|
|
24
|
+
* cardinality.
|
|
25
|
+
*/
|
|
26
|
+
const OIP_KNOWN_CRYPTOSUITES = /* @__PURE__ */ new Set(["eddsa-jcs-2022"]);
|
|
27
|
+
const logger = getLogger([
|
|
28
|
+
"fedify",
|
|
29
|
+
"sig",
|
|
30
|
+
"proof"
|
|
31
|
+
]);
|
|
32
|
+
const SECURITY_NAMESPACE = "https://w3id.org/security#";
|
|
33
|
+
const SECURITY_PROOF = `${SECURITY_NAMESPACE}proof`;
|
|
34
|
+
const SECURITY_PROOF_VALUE = `${SECURITY_NAMESPACE}proofValue`;
|
|
35
|
+
const DATA_INTEGRITY_PROOF = `${SECURITY_NAMESPACE}DataIntegrityProof`;
|
|
36
|
+
const SECURITY_VERIFICATION_METHOD = `${SECURITY_NAMESPACE}verificationMethod`;
|
|
37
|
+
const SECURITY_EXPIRATION = `${SECURITY_NAMESPACE}expiration`;
|
|
38
|
+
const SECURITY_DOMAIN = `${SECURITY_NAMESPACE}domain`;
|
|
39
|
+
const SECURITY_CHALLENGE = `${SECURITY_NAMESPACE}challenge`;
|
|
40
|
+
const SECURITY_NONCE = `${SECURITY_NAMESPACE}nonce`;
|
|
41
|
+
const SECURITY_PREVIOUS_PROOF = `${SECURITY_NAMESPACE}previousProof`;
|
|
42
|
+
/**
|
|
43
|
+
* Checks if the given JSON-LD document has a DataIntegrityProof-like object,
|
|
44
|
+
* without fully deserializing it into vocabulary classes.
|
|
45
|
+
* @param jsonLd The JSON-LD document to check.
|
|
46
|
+
* @returns `true` if the document has a proof-like object; `false` otherwise.
|
|
47
|
+
* @since 2.2.0
|
|
48
|
+
*/
|
|
49
|
+
function hasProofLike(jsonLd) {
|
|
50
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
51
|
+
const record = jsonLd;
|
|
52
|
+
const proof = record.proof ?? record["https://w3id.org/security#proof"];
|
|
53
|
+
const getField = (source, compact, expanded) => source[compact] ?? source[expanded];
|
|
54
|
+
const isReference = (value) => {
|
|
55
|
+
if (typeof value === "string") return true;
|
|
56
|
+
if (Array.isArray(value)) return value.some(isReference);
|
|
57
|
+
return typeof value === "object" && value != null && ("id" in value && typeof value.id === "string" || "@id" in value && typeof value["@id"] === "string" || "@value" in value && typeof value["@value"] === "string");
|
|
58
|
+
};
|
|
59
|
+
const hasType = (value) => {
|
|
60
|
+
if (typeof value === "string") return value === "DataIntegrityProof" || value === "https://w3id.org/security#DataIntegrityProof";
|
|
61
|
+
if (Array.isArray(value)) return value.some(hasType);
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
const isProofLike = (value) => {
|
|
65
|
+
if (typeof value !== "object" || value == null) return false;
|
|
66
|
+
const proofRecord = value;
|
|
67
|
+
return hasType(proofRecord.type ?? proofRecord["@type"]) && isReference(getField(proofRecord, "verificationMethod", "https://w3id.org/security#verificationMethod")) && isReference(getField(proofRecord, "proofPurpose", "https://w3id.org/security#proofPurpose")) && isReference(getField(proofRecord, "proofValue", "https://w3id.org/security#proofValue"));
|
|
68
|
+
};
|
|
69
|
+
return Array.isArray(proof) ? proof.some(isProofLike) : isProofLike(proof);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a proof for the given object.
|
|
73
|
+
* @param object The object to create a proof for.
|
|
74
|
+
* @param privateKey The private key to sign the proof with.
|
|
75
|
+
* @param keyId The key ID to use in the proof. It will be used by the verifier.
|
|
76
|
+
* @param options Additional options. See also {@link CreateProofOptions}.
|
|
77
|
+
* @returns The created proof.
|
|
78
|
+
* @throws {TypeError} If the private key is invalid or unsupported.
|
|
79
|
+
* @since 0.10.0
|
|
80
|
+
*/
|
|
81
|
+
async function createProof(object, privateKey, keyId, { contextLoader, context, created } = {}) {
|
|
82
|
+
validateCryptoKey(privateKey, "private");
|
|
83
|
+
if (privateKey.algorithm.name !== "Ed25519") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
|
|
84
|
+
let compactMsg = await object.clone({ proofs: [] }).toJsonLd({
|
|
85
|
+
format: "compact",
|
|
86
|
+
contextLoader,
|
|
87
|
+
context
|
|
88
|
+
});
|
|
89
|
+
compactMsg = await normalizeOutgoingActivityJsonLd(compactMsg, contextLoader);
|
|
90
|
+
const msgCanon = serialize(compactMsg);
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
const msgBytes = encoder.encode(msgCanon);
|
|
93
|
+
const msgDigest = await crypto.subtle.digest("SHA-256", msgBytes);
|
|
94
|
+
created ??= Temporal.Now.instant();
|
|
95
|
+
const proofCanon = serialize({
|
|
96
|
+
"@context": compactMsg["@context"],
|
|
97
|
+
type: "DataIntegrityProof",
|
|
98
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
99
|
+
verificationMethod: keyId.href,
|
|
100
|
+
proofPurpose: "assertionMethod",
|
|
101
|
+
created: created.toString()
|
|
102
|
+
});
|
|
103
|
+
const proofBytes = encoder.encode(proofCanon);
|
|
104
|
+
const proofDigest = await crypto.subtle.digest("SHA-256", proofBytes);
|
|
105
|
+
const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength);
|
|
106
|
+
digest.set(new Uint8Array(proofDigest), 0);
|
|
107
|
+
digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
|
|
108
|
+
const sig = await crypto.subtle.sign("Ed25519", privateKey, digest);
|
|
109
|
+
return new DataIntegrityProof({
|
|
110
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
111
|
+
verificationMethod: keyId,
|
|
112
|
+
proofPurpose: "assertionMethod",
|
|
113
|
+
created: created ?? Temporal.Now.instant(),
|
|
114
|
+
proofValue: new Uint8Array(sig)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Signs the given object with the private key and returns the signed object.
|
|
119
|
+
* @param object The object to create a proof for.
|
|
120
|
+
* @param privateKey The private key to sign the proof with.
|
|
121
|
+
* @param keyId The key ID to use in the proof. It will be used by the verifier.
|
|
122
|
+
* @param options Additional options. See also {@link SignObjectOptions}.
|
|
123
|
+
* @returns The signed object.
|
|
124
|
+
* @throws {TypeError} If the private key is invalid or unsupported.
|
|
125
|
+
* @since 0.10.0
|
|
126
|
+
*/
|
|
127
|
+
async function signObject(object, privateKey, keyId, options = {}) {
|
|
128
|
+
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("object_integrity_proofs.sign", { attributes: { "activitypub.object.type": getTypeId(object).href } }, async (span) => {
|
|
129
|
+
try {
|
|
130
|
+
if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
|
|
131
|
+
const existingProofs = [];
|
|
132
|
+
for await (const proof of object.getProofs(options)) existingProofs.push(proof);
|
|
133
|
+
const proof = await createProof(object, privateKey, keyId, options);
|
|
134
|
+
if (span.isRecording()) {
|
|
135
|
+
if (proof.cryptosuite != null) span.setAttribute("object_integrity_proofs.cryptosuite", proof.cryptosuite);
|
|
136
|
+
if (proof.verificationMethodId != null) span.setAttribute("object_integrity_proofs.key_id", proof.verificationMethodId.href);
|
|
137
|
+
if (proof.proofValue != null) span.setAttribute("object_integrity_proofs.signature", encodeHex(proof.proofValue));
|
|
138
|
+
}
|
|
139
|
+
return object.clone({ proofs: [...existingProofs, proof] });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
span.setStatus({
|
|
142
|
+
code: SpanStatusCode.ERROR,
|
|
143
|
+
message: String(error)
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
} finally {
|
|
147
|
+
span.end();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Verifies the given proof for the object.
|
|
153
|
+
* @param jsonLd The JSON-LD object to verify the proof for. Its proof
|
|
154
|
+
* properties are excluded from the message, but matching proof
|
|
155
|
+
* occurrences are used to authenticate the received proof
|
|
156
|
+
* configuration.
|
|
157
|
+
* @param proof The proof to verify.
|
|
158
|
+
* @param options Additional options. See also {@link VerifyProofOptions}.
|
|
159
|
+
* @returns The public key that was used to sign the proof, or `null` if the
|
|
160
|
+
* proof is invalid.
|
|
161
|
+
* @since 0.10.0
|
|
162
|
+
* @since 2.4.0 Matching proof configurations are authenticated.
|
|
163
|
+
*/
|
|
164
|
+
async function verifyProof(jsonLd, proof, options = {}) {
|
|
165
|
+
return await verifyProofWithMessageDigestCache(jsonLd, proof, options);
|
|
166
|
+
}
|
|
167
|
+
async function verifyProofWithMessageDigestCache(jsonLd, proof, options, messageDigestCache = {}, rawProofCandidate) {
|
|
168
|
+
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("object_integrity_proofs.verify", async (span) => {
|
|
169
|
+
const start = performance.now();
|
|
170
|
+
let verified = false;
|
|
171
|
+
let threw = false;
|
|
172
|
+
const cryptosuite = proof.cryptosuite != null && OIP_KNOWN_CRYPTOSUITES.has(proof.cryptosuite) ? proof.cryptosuite : void 0;
|
|
173
|
+
if (span.isRecording()) {
|
|
174
|
+
if (proof.cryptosuite != null) span.setAttribute("object_integrity_proofs.cryptosuite", proof.cryptosuite);
|
|
175
|
+
if (proof.verificationMethodId != null) span.setAttribute("object_integrity_proofs.key_id", proof.verificationMethodId.href);
|
|
176
|
+
if (proof.proofValue != null) span.setAttribute("object_integrity_proofs.signature", encodeHex(proof.proofValue));
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const key = await verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate);
|
|
180
|
+
if (key == null) span.setStatus({ code: SpanStatusCode.ERROR });
|
|
181
|
+
else verified = true;
|
|
182
|
+
return key;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
threw = true;
|
|
185
|
+
span.setStatus({
|
|
186
|
+
code: SpanStatusCode.ERROR,
|
|
187
|
+
message: String(error)
|
|
188
|
+
});
|
|
189
|
+
throw error;
|
|
190
|
+
} finally {
|
|
191
|
+
const classified = threw ? "error" : verified ? "verified" : "rejected";
|
|
192
|
+
getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "object_integrity", classified, { cryptosuite });
|
|
193
|
+
span.end();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function expandContextPropertyIri(activeContext, key) {
|
|
198
|
+
const termId = jsonld.getContextValue(activeContext, key, "@id");
|
|
199
|
+
if (termId != null) return termId;
|
|
200
|
+
const colon = key.indexOf(":");
|
|
201
|
+
if (colon < 1) return key;
|
|
202
|
+
const prefix = key.substring(0, colon);
|
|
203
|
+
const suffix = key.substring(colon + 1);
|
|
204
|
+
if (prefix === "_" || suffix.startsWith("//")) return key;
|
|
205
|
+
const mapping = jsonld.getContextValue(activeContext, prefix);
|
|
206
|
+
if (typeof mapping === "object" && mapping != null && mapping._prefix === true && typeof mapping["@id"] === "string") return mapping["@id"] + suffix;
|
|
207
|
+
return key;
|
|
208
|
+
}
|
|
209
|
+
async function getJsonLdPropertyNames(jsonLd, propertyIri, defaults, documentLoader = preloadedOnlyDocumentLoader, inheritedContext, rejectOnContextError = false) {
|
|
210
|
+
const names = new Set(defaults);
|
|
211
|
+
const context = jsonLd["@context"] ?? inheritedContext;
|
|
212
|
+
if (context == null) return names;
|
|
213
|
+
try {
|
|
214
|
+
const options = { documentLoader };
|
|
215
|
+
let activeContext = await jsonld.processContext(null, null, options);
|
|
216
|
+
activeContext = await jsonld.processContext(activeContext, context, options);
|
|
217
|
+
const typeScopedContext = activeContext;
|
|
218
|
+
for (const key of globalThis.Object.keys(jsonLd).sort()) {
|
|
219
|
+
if (key !== "@type" && expandContextPropertyIri(activeContext, key) !== "@type") continue;
|
|
220
|
+
const value = jsonLd[key];
|
|
221
|
+
const types = Array.isArray(value) ? value.slice().sort() : [value];
|
|
222
|
+
for (const type of types) {
|
|
223
|
+
if (typeof type !== "string") continue;
|
|
224
|
+
const scopedContext = jsonld.getContextValue(typeScopedContext, type, "@context");
|
|
225
|
+
if (scopedContext != null) activeContext = await jsonld.processContext(activeContext, scopedContext, options);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const key of globalThis.Object.keys(jsonLd)) if (expandContextPropertyIri(activeContext, key) === propertyIri) names.add(key);
|
|
229
|
+
} catch {
|
|
230
|
+
if (rejectOnContextError) return null;
|
|
231
|
+
}
|
|
232
|
+
return names;
|
|
233
|
+
}
|
|
234
|
+
async function getProofPropertyNames(jsonLd, documentLoader = preloadedOnlyDocumentLoader) {
|
|
235
|
+
return await getJsonLdPropertyNames(jsonLd, SECURITY_PROOF, ["proof", SECURITY_PROOF], documentLoader) ?? /* @__PURE__ */ new Set(["proof", SECURITY_PROOF]);
|
|
236
|
+
}
|
|
237
|
+
async function createProofMessageDigests(jsonLd, proofContextLoader, context) {
|
|
238
|
+
const msg = { ...jsonLd };
|
|
239
|
+
for (const property of await getProofPropertyNames(msg, proofContextLoader)) delete msg[property];
|
|
240
|
+
if (context != null) msg["@context"] = structuredClone(context);
|
|
241
|
+
const encoder = new TextEncoder();
|
|
242
|
+
const digest = async (value) => {
|
|
243
|
+
const bytes = encoder.encode(serialize(value));
|
|
244
|
+
return await crypto.subtle.digest("SHA-256", bytes);
|
|
245
|
+
};
|
|
246
|
+
const onWire = await digest(msg);
|
|
247
|
+
let normalizedPromise;
|
|
248
|
+
return {
|
|
249
|
+
onWire,
|
|
250
|
+
normalized() {
|
|
251
|
+
normalizedPromise ??= (async () => {
|
|
252
|
+
const normalized = await normalizeOutgoingActivityJsonLd(msg, preloadedOnlyDocumentLoader);
|
|
253
|
+
return normalized === msg ? null : await digest(normalized);
|
|
254
|
+
})();
|
|
255
|
+
return normalizedPromise;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function contextValues(context) {
|
|
260
|
+
return Array.isArray(context) ? context : [context];
|
|
261
|
+
}
|
|
262
|
+
function equalJsonValues(left, right) {
|
|
263
|
+
try {
|
|
264
|
+
return serialize(left) === serialize(right);
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function contextStartsWith(documentContext, proofContext) {
|
|
270
|
+
if (documentContext == null) return false;
|
|
271
|
+
const documentValues = contextValues(documentContext);
|
|
272
|
+
const proofValues = contextValues(proofContext);
|
|
273
|
+
return proofValues.length <= documentValues.length && proofValues.every((value, index) => equalJsonValues(value, documentValues[index]));
|
|
274
|
+
}
|
|
275
|
+
function appendProofValues(values, value) {
|
|
276
|
+
if (Array.isArray(value)) for (const item of value) appendProofValues(values, item);
|
|
277
|
+
else if (isJsonLdNode(value) && Array.isArray(value["@graph"])) for (const item of value["@graph"]) appendProofValues(values, item);
|
|
278
|
+
else values.push(value);
|
|
279
|
+
}
|
|
280
|
+
async function getRawProofValues(jsonLd, documentLoader) {
|
|
281
|
+
const propertyNames = await getProofPropertyNames(jsonLd, documentLoader);
|
|
282
|
+
const values = [];
|
|
283
|
+
for (const [property, value] of globalThis.Object.entries(jsonLd)) if (propertyNames.has(property)) appendProofValues(values, value);
|
|
284
|
+
return values;
|
|
285
|
+
}
|
|
286
|
+
function sameBytes(left, right) {
|
|
287
|
+
if (left == null || right == null) return left === right;
|
|
288
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
289
|
+
}
|
|
290
|
+
function sameProof(left, right) {
|
|
291
|
+
return left.cryptosuite === right.cryptosuite && left.verificationMethodId?.href === right.verificationMethodId?.href && left.proofPurpose === right.proofPurpose && sameBytes(left.proofValue, right.proofValue) && left.created?.toString() === right.created?.toString();
|
|
292
|
+
}
|
|
293
|
+
function normalizeDocumentUrl(url) {
|
|
294
|
+
try {
|
|
295
|
+
return formatIri(parseIri(url));
|
|
296
|
+
} catch {
|
|
297
|
+
return URL.canParse(url) ? new URL(url).href : url;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function getRawProofReference(value) {
|
|
301
|
+
const node = Array.isArray(value) && value.length === 1 ? value[0] : value;
|
|
302
|
+
if (typeof node === "string") return normalizeDocumentUrl(node);
|
|
303
|
+
if (!isJsonLdNode(node)) return void 0;
|
|
304
|
+
const id = node["@id"] ?? node.id;
|
|
305
|
+
return typeof id === "string" ? normalizeDocumentUrl(id) : void 0;
|
|
306
|
+
}
|
|
307
|
+
async function parseRawProofCandidates(jsonLd, values, options, documentLoader) {
|
|
308
|
+
const candidates = [];
|
|
309
|
+
for (const value of values) {
|
|
310
|
+
let parsed = null;
|
|
311
|
+
if (isJsonLdNode(value)) {
|
|
312
|
+
const proofJsonLd = value["@context"] == null && jsonLd["@context"] != null ? {
|
|
313
|
+
"@context": jsonLd["@context"],
|
|
314
|
+
...value
|
|
315
|
+
} : value;
|
|
316
|
+
try {
|
|
317
|
+
parsed = await DataIntegrityProof.fromJsonLd(proofJsonLd, {
|
|
318
|
+
...options,
|
|
319
|
+
contextLoader: documentLoader
|
|
320
|
+
});
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
candidates.push({
|
|
324
|
+
value,
|
|
325
|
+
proof: parsed,
|
|
326
|
+
reference: getRawProofReference(value) ?? parsed?.id?.href
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return candidates;
|
|
330
|
+
}
|
|
331
|
+
async function findRawProofCandidate(jsonLd, proof, options, documentLoader) {
|
|
332
|
+
const candidates = await parseRawProofCandidates(jsonLd, await getRawProofValues(jsonLd, documentLoader), options, documentLoader);
|
|
333
|
+
const matches = [];
|
|
334
|
+
for (const candidate of candidates) if (candidate.proof != null && sameProof(candidate.proof, proof)) matches.push(candidate);
|
|
335
|
+
if (matches.length < 1) return void 0;
|
|
336
|
+
const first = matches[0];
|
|
337
|
+
const configurations = await Promise.all(matches.map((candidate) => normalizeProofConfiguration(candidate.value, jsonLd["@context"], documentLoader)));
|
|
338
|
+
const firstConfiguration = configurations[0];
|
|
339
|
+
return firstConfiguration != null && configurations.every((configuration) => configuration != null && equalJsonValues(configuration.value, firstConfiguration.value)) ? first : null;
|
|
340
|
+
}
|
|
341
|
+
function takeRawProofCandidate(pool, proof) {
|
|
342
|
+
for (let index = 0; index < pool.candidates.length; index++) {
|
|
343
|
+
if (pool.used.has(index)) continue;
|
|
344
|
+
const candidate = pool.candidates[index];
|
|
345
|
+
if (candidate.proof != null && sameProof(candidate.proof, proof)) {
|
|
346
|
+
pool.used.add(index);
|
|
347
|
+
return candidate;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const STANDARD_COMPACT_PROOF_PROPERTIES = /* @__PURE__ */ new Set([
|
|
352
|
+
"@context",
|
|
353
|
+
"type",
|
|
354
|
+
"cryptosuite",
|
|
355
|
+
"verificationMethod",
|
|
356
|
+
"proofPurpose",
|
|
357
|
+
"proofValue",
|
|
358
|
+
"created"
|
|
359
|
+
]);
|
|
360
|
+
const STANDARD_EXPANDED_PROOF_PROPERTIES = /* @__PURE__ */ new Set([
|
|
361
|
+
"@context",
|
|
362
|
+
"@type",
|
|
363
|
+
`${SECURITY_NAMESPACE}cryptosuite`,
|
|
364
|
+
SECURITY_VERIFICATION_METHOD,
|
|
365
|
+
`${SECURITY_NAMESPACE}proofPurpose`,
|
|
366
|
+
`${SECURITY_NAMESPACE}proofValue`,
|
|
367
|
+
"http://purl.org/dc/terms/created"
|
|
368
|
+
]);
|
|
369
|
+
function hasAdditionalProofOptions(value) {
|
|
370
|
+
const node = Array.isArray(value) && value.length === 1 ? value[0] : value;
|
|
371
|
+
return isJsonLdNode(node) && globalThis.Object.keys(node).some((property) => !STANDARD_COMPACT_PROOF_PROPERTIES.has(property) && !STANDARD_EXPANDED_PROOF_PROPERTIES.has(property));
|
|
372
|
+
}
|
|
373
|
+
function isExpandedJsonLdNode(value) {
|
|
374
|
+
const properties = globalThis.Object.keys(value).filter((key) => !key.startsWith("@"));
|
|
375
|
+
return properties.length > 0 && properties.every((property) => URL.canParse(property));
|
|
376
|
+
}
|
|
377
|
+
async function normalizeProofConfiguration(rawProof, documentContext, documentLoader) {
|
|
378
|
+
let node = Array.isArray(rawProof) && rawProof.length === 1 ? rawProof[0] : rawProof;
|
|
379
|
+
if (!isJsonLdNode(node)) return null;
|
|
380
|
+
if (isExpandedJsonLdNode(node)) {
|
|
381
|
+
if (documentContext == null) return null;
|
|
382
|
+
node = await jsonld.compact(node, documentContext, { documentLoader });
|
|
383
|
+
if (!isJsonLdNode(node)) return null;
|
|
384
|
+
} else node = structuredClone(node);
|
|
385
|
+
const receivedContext = node["@context"];
|
|
386
|
+
if (receivedContext != null && !contextStartsWith(documentContext, receivedContext)) return null;
|
|
387
|
+
const context = receivedContext ?? documentContext;
|
|
388
|
+
if (context != null) node["@context"] = structuredClone(context);
|
|
389
|
+
const proofValueProperties = await getJsonLdPropertyNames(node, SECURITY_PROOF_VALUE, ["proofValue", SECURITY_PROOF_VALUE], documentLoader, context) ?? /* @__PURE__ */ new Set(["proofValue", SECURITY_PROOF_VALUE]);
|
|
390
|
+
for (const property of proofValueProperties) delete node[property];
|
|
391
|
+
return {
|
|
392
|
+
value: node,
|
|
393
|
+
context
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
async function createProofConfiguration(jsonLd, proof, options, documentLoader, rawProofCandidate) {
|
|
397
|
+
let rawProof;
|
|
398
|
+
if (rawProofCandidate == null) {
|
|
399
|
+
const match = await findRawProofCandidate(jsonLd, proof, options, documentLoader);
|
|
400
|
+
if (match === null) return null;
|
|
401
|
+
rawProof = match?.value;
|
|
402
|
+
} else rawProof = rawProofCandidate.value;
|
|
403
|
+
if (rawProof == null) {
|
|
404
|
+
const serializedProof = await proof.toJsonLd();
|
|
405
|
+
if (hasAdditionalProofOptions(serializedProof)) rawProof = serializedProof;
|
|
406
|
+
}
|
|
407
|
+
if (rawProof != null) return await normalizeProofConfiguration(rawProof, jsonLd["@context"], documentLoader);
|
|
408
|
+
const context = jsonLd["@context"];
|
|
409
|
+
return {
|
|
410
|
+
value: {
|
|
411
|
+
...context == null ? {} : { "@context": context },
|
|
412
|
+
type: "DataIntegrityProof",
|
|
413
|
+
cryptosuite: proof.cryptosuite,
|
|
414
|
+
verificationMethod: proof.verificationMethodId.href,
|
|
415
|
+
proofPurpose: proof.proofPurpose,
|
|
416
|
+
created: proof.created.toString()
|
|
417
|
+
},
|
|
418
|
+
context
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const KNOWN_PROOF_CONFIGURATION_PROPERTIES = /* @__PURE__ */ new Set([
|
|
422
|
+
"@context",
|
|
423
|
+
"@id",
|
|
424
|
+
"@type",
|
|
425
|
+
"id",
|
|
426
|
+
"type",
|
|
427
|
+
"cryptosuite",
|
|
428
|
+
`${SECURITY_NAMESPACE}cryptosuite`,
|
|
429
|
+
"verificationMethod",
|
|
430
|
+
SECURITY_VERIFICATION_METHOD,
|
|
431
|
+
"proofPurpose",
|
|
432
|
+
`${SECURITY_NAMESPACE}proofPurpose`,
|
|
433
|
+
"created",
|
|
434
|
+
"http://purl.org/dc/terms/created",
|
|
435
|
+
"expires",
|
|
436
|
+
SECURITY_EXPIRATION,
|
|
437
|
+
"domain",
|
|
438
|
+
SECURITY_DOMAIN,
|
|
439
|
+
"challenge",
|
|
440
|
+
SECURITY_CHALLENGE,
|
|
441
|
+
"nonce",
|
|
442
|
+
SECURITY_NONCE,
|
|
443
|
+
"previousProof",
|
|
444
|
+
SECURITY_PREVIOUS_PROOF
|
|
445
|
+
]);
|
|
446
|
+
async function getProofOption(proofConfig, propertyIri, defaults, documentLoader) {
|
|
447
|
+
let names = await getJsonLdPropertyNames(proofConfig, propertyIri, defaults, documentLoader, proofConfig["@context"], true);
|
|
448
|
+
if (names == null) {
|
|
449
|
+
if (globalThis.Object.keys(proofConfig).some((property) => !KNOWN_PROOF_CONFIGURATION_PROPERTIES.has(property))) return null;
|
|
450
|
+
names = new Set(defaults);
|
|
451
|
+
}
|
|
452
|
+
const present = globalThis.Object.keys(proofConfig).filter((property) => names.has(property));
|
|
453
|
+
if (present.length > 1) return null;
|
|
454
|
+
return present.length < 1 ? { present: false } : {
|
|
455
|
+
present: true,
|
|
456
|
+
value: proofConfig[present[0]]
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function parseStringSet(value) {
|
|
460
|
+
if (typeof value === "string") return /* @__PURE__ */ new Set([value]);
|
|
461
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) return null;
|
|
462
|
+
return new Set(value);
|
|
463
|
+
}
|
|
464
|
+
function equalStringSets(left, right) {
|
|
465
|
+
return left.size === right.size && [...left].every((value) => right.has(value));
|
|
466
|
+
}
|
|
467
|
+
async function hasValidProofOptions(proofConfig, options, documentLoader) {
|
|
468
|
+
const expires = await getProofOption(proofConfig, SECURITY_EXPIRATION, ["expires", SECURITY_EXPIRATION], documentLoader);
|
|
469
|
+
if (expires == null) return false;
|
|
470
|
+
if (expires.present) {
|
|
471
|
+
if (typeof expires.value !== "string") return false;
|
|
472
|
+
let expiration;
|
|
473
|
+
try {
|
|
474
|
+
expiration = Temporal.Instant.from(expires.value);
|
|
475
|
+
} catch {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
if (Temporal.Instant.compare(Temporal.Now.instant(), expiration) >= 0) return false;
|
|
479
|
+
}
|
|
480
|
+
const domain = await getProofOption(proofConfig, SECURITY_DOMAIN, ["domain", SECURITY_DOMAIN], documentLoader);
|
|
481
|
+
if (domain == null) return false;
|
|
482
|
+
const proofDomains = domain.present ? parseStringSet(domain.value) : null;
|
|
483
|
+
if (domain.present && proofDomains == null) return false;
|
|
484
|
+
if (options.domain != null) {
|
|
485
|
+
const expectedDomains = parseStringSet(options.domain);
|
|
486
|
+
if (expectedDomains == null || proofDomains == null || !equalStringSets(proofDomains, expectedDomains)) return false;
|
|
487
|
+
}
|
|
488
|
+
const challenge = await getProofOption(proofConfig, SECURITY_CHALLENGE, ["challenge", SECURITY_CHALLENGE], documentLoader);
|
|
489
|
+
if (challenge == null || challenge.present && typeof challenge.value !== "string" || options.challenge != null && (!challenge.present || challenge.value !== options.challenge)) return false;
|
|
490
|
+
const nonce = await getProofOption(proofConfig, SECURITY_NONCE, ["nonce", SECURITY_NONCE], documentLoader);
|
|
491
|
+
if (nonce == null || nonce.present && typeof nonce.value !== "string") return false;
|
|
492
|
+
const previousProof = await getProofOption(proofConfig, SECURITY_PREVIOUS_PROOF, ["previousProof", SECURITY_PREVIOUS_PROOF], documentLoader);
|
|
493
|
+
if (previousProof == null || previousProof.present && typeof previousProof.value !== "string" && (!Array.isArray(previousProof.value) || previousProof.value.some((item) => typeof item !== "string"))) return false;
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, rawProofCandidate) {
|
|
497
|
+
if (!isJsonLdNode(jsonLd) || proof.cryptosuite !== "eddsa-jcs-2022" || proof.verificationMethodId == null || proof.proofPurpose !== "assertionMethod" || proof.proofValue == null || proof.created == null) return null;
|
|
498
|
+
const proofContextLoader = messageDigestCache.proofContextLoader ?? preloadedOnlyDocumentLoader;
|
|
499
|
+
const proofConfiguration = await createProofConfiguration(jsonLd, proof, options, proofContextLoader, rawProofCandidate);
|
|
500
|
+
if (proofConfiguration == null || !await hasValidProofOptions(proofConfiguration.value, options, proofContextLoader)) return null;
|
|
501
|
+
const publicKeyPromise = measureSignatureKeyFetch(options.meterProvider, "object_integrity", () => fetchKey(proof.verificationMethodId, Multikey, options));
|
|
502
|
+
const proofBytes = new TextEncoder().encode(serialize(proofConfiguration.value));
|
|
503
|
+
const proofDigest = await crypto.subtle.digest("SHA-256", proofBytes);
|
|
504
|
+
let fetchedKey;
|
|
505
|
+
try {
|
|
506
|
+
fetchedKey = await publicKeyPromise;
|
|
507
|
+
} catch (error) {
|
|
508
|
+
logger.debug("Failed to get the key (verificationMethod) for the proof:\n{proof}", {
|
|
509
|
+
proof,
|
|
510
|
+
keyId: proof.verificationMethodId.href,
|
|
511
|
+
error
|
|
512
|
+
});
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
const publicKey = fetchedKey.key;
|
|
516
|
+
if (publicKey == null) {
|
|
517
|
+
logger.debug("Failed to get the key (verificationMethod) for the proof:\n{proof}", {
|
|
518
|
+
proof,
|
|
519
|
+
keyId: proof.verificationMethodId.href
|
|
520
|
+
});
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
if (publicKey.publicKey.algorithm.name !== "Ed25519") {
|
|
524
|
+
if (fetchedKey.cached) {
|
|
525
|
+
logger.debug("The cached key (verificationMethod) for the proof is not a valid Ed25519 key:\n{keyId}; retrying with the freshly fetched key...", {
|
|
526
|
+
proof,
|
|
527
|
+
keyId: proof.verificationMethodId.href
|
|
528
|
+
});
|
|
529
|
+
return await verifyProofInternal(jsonLd, proof, {
|
|
530
|
+
...options,
|
|
531
|
+
keyCache: {
|
|
532
|
+
get: () => Promise.resolve(void 0),
|
|
533
|
+
set: async (keyId, key) => await options.keyCache?.set(keyId, key)
|
|
534
|
+
}
|
|
535
|
+
}, messageDigestCache, rawProofCandidate);
|
|
536
|
+
}
|
|
537
|
+
logger.debug("The fetched key (verificationMethod) for the proof is not a valid Ed25519 key:\n{keyId}", {
|
|
538
|
+
proof,
|
|
539
|
+
keyId: proof.verificationMethodId.href
|
|
540
|
+
});
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
const digest = new Uint8Array(proofDigest.byteLength + 32);
|
|
544
|
+
digest.set(new Uint8Array(proofDigest), 0);
|
|
545
|
+
const proofValue = proof.proofValue;
|
|
546
|
+
const verifyCandidate = async (msgDigest) => {
|
|
547
|
+
digest.set(new Uint8Array(msgDigest), proofDigest.byteLength);
|
|
548
|
+
return await crypto.subtle.verify("Ed25519", publicKey.publicKey, proofValue.slice(), digest);
|
|
549
|
+
};
|
|
550
|
+
const messageDigestKey = serialize(proofConfiguration.context ?? null);
|
|
551
|
+
const messageDigestValues = messageDigestCache.values ??= /* @__PURE__ */ new Map();
|
|
552
|
+
let messageDigestsPromise = messageDigestValues.get(messageDigestKey);
|
|
553
|
+
if (messageDigestsPromise == null) {
|
|
554
|
+
messageDigestsPromise = createProofMessageDigests(jsonLd, messageDigestCache.proofContextLoader, proofConfiguration.context);
|
|
555
|
+
messageDigestValues.set(messageDigestKey, messageDigestsPromise);
|
|
556
|
+
}
|
|
557
|
+
const messageDigests = await messageDigestsPromise;
|
|
558
|
+
if (await verifyCandidate(messageDigests.onWire)) return publicKey;
|
|
559
|
+
const normalizedDigest = await messageDigests.normalized();
|
|
560
|
+
if (normalizedDigest != null && await verifyCandidate(normalizedDigest)) return publicKey;
|
|
561
|
+
if (fetchedKey.cached) {
|
|
562
|
+
logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
|
|
563
|
+
keyId: proof.verificationMethodId.href,
|
|
564
|
+
proof
|
|
565
|
+
});
|
|
566
|
+
return await verifyProofInternal(jsonLd, proof, {
|
|
567
|
+
...options,
|
|
568
|
+
keyCache: {
|
|
569
|
+
get: () => Promise.resolve(void 0),
|
|
570
|
+
set: async (keyId, key) => await options.keyCache?.set(keyId, key)
|
|
571
|
+
}
|
|
572
|
+
}, messageDigestCache, rawProofCandidate);
|
|
573
|
+
}
|
|
574
|
+
logger.debug("Failed to verify the proof with the fetched key {keyId}:\n{proof}", {
|
|
575
|
+
keyId: proof.verificationMethodId.href,
|
|
576
|
+
proof
|
|
577
|
+
});
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const AS_NAMESPACE = "https://www.w3.org/ns/activitystreams#";
|
|
581
|
+
const FEP_2277_ACTOR_PROPERTIES = ["http://www.w3.org/ns/ldp#inbox", `${AS_NAMESPACE}outbox`];
|
|
582
|
+
const FEP_2277_COLLECTION_PROPERTIES = [
|
|
583
|
+
"items",
|
|
584
|
+
"orderedItems",
|
|
585
|
+
"totalItems",
|
|
586
|
+
"partOf",
|
|
587
|
+
"first",
|
|
588
|
+
"last",
|
|
589
|
+
"next",
|
|
590
|
+
"prev",
|
|
591
|
+
"current"
|
|
592
|
+
].map((property) => AS_NAMESPACE + property);
|
|
593
|
+
const PORTABLE_OBJECT_ID_PATTERN = /^ap(?:\+ef61)?:\/\//i;
|
|
594
|
+
const FUNCTIONAL_PROOF_PROPERTIES = [
|
|
595
|
+
`${SECURITY_NAMESPACE}cryptosuite`,
|
|
596
|
+
SECURITY_VERIFICATION_METHOD,
|
|
597
|
+
`${SECURITY_NAMESPACE}proofPurpose`,
|
|
598
|
+
`${SECURITY_NAMESPACE}proofValue`,
|
|
599
|
+
"http://purl.org/dc/terms/created"
|
|
600
|
+
];
|
|
601
|
+
function isJsonLdNode(value) {
|
|
602
|
+
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
603
|
+
}
|
|
604
|
+
function hasValidPortableProofShape(proofValue) {
|
|
605
|
+
if (!isJsonLdNode(proofValue)) return false;
|
|
606
|
+
let proofNode = proofValue;
|
|
607
|
+
if ("@graph" in proofNode) {
|
|
608
|
+
const graph = proofNode["@graph"];
|
|
609
|
+
if (!Array.isArray(graph) || graph.length !== 1 || !isJsonLdNode(graph[0])) return false;
|
|
610
|
+
proofNode = graph[0];
|
|
611
|
+
}
|
|
612
|
+
const types = proofNode["@type"];
|
|
613
|
+
return Array.isArray(types) && types.length === 1 && types[0] === DATA_INTEGRITY_PROOF && FUNCTIONAL_PROOF_PROPERTIES.every((property) => {
|
|
614
|
+
const values = proofNode[property];
|
|
615
|
+
return Array.isArray(values) && values.length === 1 && !(isJsonLdNode(values[0]) && "@list" in values[0]);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function classifyFep2277CoreType(node) {
|
|
619
|
+
if (FEP_2277_ACTOR_PROPERTIES.every((property) => property in node)) return "actor";
|
|
620
|
+
if (`${SECURITY_NAMESPACE}publicKeyMultibase` in node) return "verificationMethod";
|
|
621
|
+
if (`${SECURITY_NAMESPACE}publicKeyPem` in node) return "publicKey";
|
|
622
|
+
if (`${AS_NAMESPACE}href` in node) return "link";
|
|
623
|
+
if (`${AS_NAMESPACE}actor` in node) return "activity";
|
|
624
|
+
if (FEP_2277_COLLECTION_PROPERTIES.some((property) => property in node)) return "collection";
|
|
625
|
+
return "object";
|
|
626
|
+
}
|
|
627
|
+
async function expandPortableObjectRoot(jsonLd, contextLoader) {
|
|
628
|
+
if (!isJsonLdNode(jsonLd)) throw new TypeError("Expected a single JSON-LD object.");
|
|
629
|
+
const loadedContexts = /* @__PURE__ */ new Map();
|
|
630
|
+
const loader = getNormalizationContextLoader(contextLoader);
|
|
631
|
+
const recordingLoader = async (url, options) => {
|
|
632
|
+
const remoteDocument = await loader(url, options);
|
|
633
|
+
const key = URL.canParse(url) ? new URL(url).href : url;
|
|
634
|
+
loadedContexts.set(key, structuredClone(remoteDocument));
|
|
635
|
+
return remoteDocument;
|
|
636
|
+
};
|
|
637
|
+
const expanded = await jsonld.expand(jsonLd, {
|
|
638
|
+
documentLoader: recordingLoader,
|
|
639
|
+
keepFreeFloatingNodes: true
|
|
640
|
+
});
|
|
641
|
+
if (expanded.length !== 1 || !isJsonLdNode(expanded[0])) throw new TypeError("Expected a single JSON-LD object.");
|
|
642
|
+
return {
|
|
643
|
+
root: expanded[0],
|
|
644
|
+
proofContextLoader: async (url, options) => {
|
|
645
|
+
const key = URL.canParse(url) ? new URL(url).href : url;
|
|
646
|
+
const remoteDocument = loadedContexts.get(key);
|
|
647
|
+
if (remoteDocument != null) return structuredClone(remoteDocument);
|
|
648
|
+
return await preloadedOnlyDocumentLoader(url, options);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Verifies the FEP-ef61 Object Integrity Proof policy for a portable object.
|
|
654
|
+
*
|
|
655
|
+
* This applies the FEP-2277 core-type classification to the top-level JSON-LD
|
|
656
|
+
* node. Portable actors, activities, and objects require proofs. A portable
|
|
657
|
+
* collection without a proof is reported separately so a caller can apply a
|
|
658
|
+
* gateway trust policy. Embedded portable objects are not traversed.
|
|
659
|
+
*
|
|
660
|
+
* Every proof must use a DID URL whose DID matches the portable object's
|
|
661
|
+
* authority, and every proof must pass {@link verifyProof}.
|
|
662
|
+
*
|
|
663
|
+
* @param jsonLd The JSON-LD document to verify.
|
|
664
|
+
* @param options Additional options. See also
|
|
665
|
+
* {@link VerifyPortableObjectProofOptions}.
|
|
666
|
+
* @returns The detailed portable proof-policy result.
|
|
667
|
+
* @throws {TypeError} If the input is not a single JSON-LD object or has a
|
|
668
|
+
* malformed portable ID.
|
|
669
|
+
* @since 2.4.0
|
|
670
|
+
*/
|
|
671
|
+
async function verifyPortableObjectProof(jsonLd, options = {}) {
|
|
672
|
+
if (isJsonLdNode(jsonLd) && typeof jsonLd["@id"] === "string" && !PORTABLE_OBJECT_ID_PATTERN.test(jsonLd["@id"])) return {
|
|
673
|
+
verified: false,
|
|
674
|
+
reason: { type: "notPortableObject" }
|
|
675
|
+
};
|
|
676
|
+
const { root, proofContextLoader } = await expandPortableObjectRoot(jsonLd, options.contextLoader);
|
|
677
|
+
const id = root["@id"];
|
|
678
|
+
if (typeof id !== "string" || !PORTABLE_OBJECT_ID_PATTERN.test(id)) return {
|
|
679
|
+
verified: false,
|
|
680
|
+
reason: { type: "notPortableObject" }
|
|
681
|
+
};
|
|
682
|
+
const objectId = parseIri(id);
|
|
683
|
+
getFe34Origin(objectId);
|
|
684
|
+
const objectType = classifyFep2277CoreType(root);
|
|
685
|
+
if (objectType === "verificationMethod" || objectType === "publicKey" || objectType === "link") return {
|
|
686
|
+
verified: false,
|
|
687
|
+
reason: {
|
|
688
|
+
type: "unsupportedObjectType",
|
|
689
|
+
objectType
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
const proofValues = root[SECURITY_PROOF];
|
|
693
|
+
if (proofValues == null || Array.isArray(proofValues) && proofValues.length < 1) return objectType === "collection" ? {
|
|
694
|
+
verified: false,
|
|
695
|
+
reason: { type: "unsecuredCollection" }
|
|
696
|
+
} : {
|
|
697
|
+
verified: false,
|
|
698
|
+
reason: { type: "missingProof" }
|
|
699
|
+
};
|
|
700
|
+
if (!Array.isArray(proofValues)) return {
|
|
701
|
+
verified: false,
|
|
702
|
+
reason: {
|
|
703
|
+
type: "invalidProof",
|
|
704
|
+
proofIndex: 0
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const rawProofValues = isJsonLdNode(jsonLd) ? await getRawProofValues(jsonLd, proofContextLoader) : [];
|
|
708
|
+
const proofs = [];
|
|
709
|
+
for (let proofIndex = 0; proofIndex < proofValues.length; proofIndex++) {
|
|
710
|
+
const proofValue = proofValues[proofIndex];
|
|
711
|
+
if (!hasValidPortableProofShape(proofValue)) return {
|
|
712
|
+
verified: false,
|
|
713
|
+
reason: {
|
|
714
|
+
type: "invalidProof",
|
|
715
|
+
proofIndex
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
let proof;
|
|
719
|
+
try {
|
|
720
|
+
proof = await DataIntegrityProof.fromJsonLd(proofValue, options);
|
|
721
|
+
} catch {
|
|
722
|
+
return {
|
|
723
|
+
verified: false,
|
|
724
|
+
reason: {
|
|
725
|
+
type: "invalidProof",
|
|
726
|
+
proofIndex
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
proofs.push(proof);
|
|
731
|
+
}
|
|
732
|
+
for (let proofIndex = 0; proofIndex < proofs.length; proofIndex++) {
|
|
733
|
+
const verificationMethod = proofs[proofIndex].verificationMethodId;
|
|
734
|
+
if (verificationMethod == null) return {
|
|
735
|
+
verified: false,
|
|
736
|
+
reason: {
|
|
737
|
+
type: "invalidProof",
|
|
738
|
+
proofIndex
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
if (verificationMethod.protocol !== "did:") return {
|
|
742
|
+
verified: false,
|
|
743
|
+
reason: {
|
|
744
|
+
type: "unsupportedVerificationMethod",
|
|
745
|
+
proofIndex,
|
|
746
|
+
verificationMethod
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
try {
|
|
750
|
+
getFe34Origin(verificationMethod);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (!(error instanceof TypeError)) throw error;
|
|
753
|
+
return {
|
|
754
|
+
verified: false,
|
|
755
|
+
reason: {
|
|
756
|
+
type: "unsupportedVerificationMethod",
|
|
757
|
+
proofIndex,
|
|
758
|
+
verificationMethod
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (!haveSameFe34Origin(objectId, verificationMethod)) return {
|
|
763
|
+
verified: false,
|
|
764
|
+
reason: {
|
|
765
|
+
type: "verificationMethodMismatch",
|
|
766
|
+
proofIndex,
|
|
767
|
+
objectId,
|
|
768
|
+
verificationMethod
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
const keys = [];
|
|
773
|
+
const rawProofCandidatePool = {
|
|
774
|
+
candidates: await parseRawProofCandidates(jsonLd, rawProofValues, options, proofContextLoader),
|
|
775
|
+
used: /* @__PURE__ */ new Set()
|
|
776
|
+
};
|
|
777
|
+
const messageDigestCache = { proofContextLoader };
|
|
778
|
+
for (let proofIndex = 0; proofIndex < proofs.length; proofIndex++) {
|
|
779
|
+
const rawProofCandidate = takeRawProofCandidate(rawProofCandidatePool, proofs[proofIndex]);
|
|
780
|
+
if (rawProofCandidate == null) return {
|
|
781
|
+
verified: false,
|
|
782
|
+
reason: {
|
|
783
|
+
type: "invalidProof",
|
|
784
|
+
proofIndex
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
const key = await verifyProofWithMessageDigestCache(jsonLd, proofs[proofIndex], options, messageDigestCache, rawProofCandidate);
|
|
788
|
+
if (key == null) return {
|
|
789
|
+
verified: false,
|
|
790
|
+
reason: {
|
|
791
|
+
type: "invalidProof",
|
|
792
|
+
proofIndex
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
keys.push(key);
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
verified: true,
|
|
799
|
+
keys
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Verifies the given object. It will verify all the proofs in the object,
|
|
804
|
+
* and succeed only if all the proofs are valid and all attributions and
|
|
805
|
+
* actors are authenticated by the proofs.
|
|
806
|
+
* @template T The type of the object to verify.
|
|
807
|
+
* @param cls The class of the object to verify. It must be a subclass of
|
|
808
|
+
* the {@link Object}.
|
|
809
|
+
* @param jsonLd The JSON-LD object to verify. It's assumed that the object
|
|
810
|
+
* is a compacted JSON-LD representation of a `T` with `@context`.
|
|
811
|
+
* @param options Additional options. See also {@link VerifyObjectOptions}.
|
|
812
|
+
* @returns The object if it's verified, or `null` if it's not.
|
|
813
|
+
* @throws {TypeError} If the object is invalid or unsupported.
|
|
814
|
+
* @since 0.10.0
|
|
815
|
+
*/
|
|
816
|
+
async function verifyObject(cls, jsonLd, options = {}) {
|
|
817
|
+
const logger = getLogger([
|
|
818
|
+
"fedify",
|
|
819
|
+
"sig",
|
|
820
|
+
"proof"
|
|
821
|
+
]);
|
|
822
|
+
const object = await cls.fromJsonLd(jsonLd, options);
|
|
823
|
+
const defaultDocumentLoader = getDocumentLoader();
|
|
824
|
+
const proofContextLoader = options.contextLoader ?? defaultDocumentLoader;
|
|
825
|
+
const attributions = new Set(object.attributionIds.map((uri) => uri.href));
|
|
826
|
+
if (object instanceof Activity) for (const uri of object.actorIds) attributions.add(uri.href);
|
|
827
|
+
const rawProofValues = isJsonLdNode(jsonLd) ? await getRawProofValues(jsonLd, proofContextLoader) : [];
|
|
828
|
+
const rawProofCandidates = isJsonLdNode(jsonLd) ? await parseRawProofCandidates(jsonLd, rawProofValues, options, proofContextLoader) : [];
|
|
829
|
+
const rawProofCandidatePool = {
|
|
830
|
+
candidates: rawProofCandidates,
|
|
831
|
+
used: /* @__PURE__ */ new Set()
|
|
832
|
+
};
|
|
833
|
+
const baseDocumentLoader = options.documentLoader ?? defaultDocumentLoader;
|
|
834
|
+
const hydratedCandidates = /* @__PURE__ */ new Set();
|
|
835
|
+
const proofDocumentLoader = async (url, loaderOptions) => {
|
|
836
|
+
const remoteDocument = await baseDocumentLoader(url, loaderOptions);
|
|
837
|
+
const reference = normalizeDocumentUrl(url);
|
|
838
|
+
const candidateIndex = rawProofCandidates.findIndex((candidate, index) => !hydratedCandidates.has(index) && !rawProofCandidatePool.used.has(index) && candidate.reference === reference);
|
|
839
|
+
if (candidateIndex >= 0) {
|
|
840
|
+
hydratedCandidates.add(candidateIndex);
|
|
841
|
+
let parsed = null;
|
|
842
|
+
try {
|
|
843
|
+
parsed = await DataIntegrityProof.fromJsonLd(remoteDocument.document, {
|
|
844
|
+
documentLoader: baseDocumentLoader,
|
|
845
|
+
contextLoader: proofContextLoader,
|
|
846
|
+
tracerProvider: options.tracerProvider,
|
|
847
|
+
baseUrl: parseIri(remoteDocument.documentUrl)
|
|
848
|
+
});
|
|
849
|
+
} catch {}
|
|
850
|
+
rawProofCandidates[candidateIndex] = {
|
|
851
|
+
value: structuredClone(remoteDocument.document),
|
|
852
|
+
proof: parsed,
|
|
853
|
+
reference
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return remoteDocument;
|
|
857
|
+
};
|
|
858
|
+
for await (const proof of object.getProofs({
|
|
859
|
+
...options,
|
|
860
|
+
documentLoader: proofDocumentLoader
|
|
861
|
+
})) {
|
|
862
|
+
const rawProofCandidate = takeRawProofCandidate(rawProofCandidatePool, proof);
|
|
863
|
+
if (rawProofCandidate == null) return null;
|
|
864
|
+
const key = await verifyProofWithMessageDigestCache(jsonLd, proof, options, { proofContextLoader }, rawProofCandidate);
|
|
865
|
+
if (key === null) return null;
|
|
866
|
+
if (proof.verificationMethodId == null) return null;
|
|
867
|
+
if (key.controllerId == null) {
|
|
868
|
+
logger.debug("Key {keyId} does not have a controller.", { keyId: key.id?.href });
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
deleteAuthenticatedAttribution(attributions, key.controllerId, proof.verificationMethodId);
|
|
872
|
+
}
|
|
873
|
+
if (attributions.size > 0) {
|
|
874
|
+
logger.debug("Some attributions are not authenticated by the proofs: {attributions}.", { attributions: [...attributions] });
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
return object;
|
|
878
|
+
}
|
|
879
|
+
function deleteAuthenticatedAttribution(attributions, controllerId, verificationMethodId) {
|
|
880
|
+
const controllerHasCryptographicOrigin = hasCryptographicOrigin(controllerId.href);
|
|
881
|
+
const verificationMethodMatchesController = controllerHasCryptographicOrigin && hasCryptographicOrigin(verificationMethodId.href) && haveSameFe34Origin(controllerId, verificationMethodId);
|
|
882
|
+
if (!controllerHasCryptographicOrigin || verificationMethodMatchesController) attributions.delete(controllerId.href);
|
|
883
|
+
if (!verificationMethodMatchesController) return;
|
|
884
|
+
for (const attribution of [...attributions]) if (hasCryptographicOrigin(attribution) && haveSameFe34Origin(controllerId, attribution)) attributions.delete(attribution);
|
|
885
|
+
}
|
|
886
|
+
function hasCryptographicOrigin(iri) {
|
|
887
|
+
return /^did:/i.test(iri) || /^ap(?:\+ef61)?:\/\//i.test(iri);
|
|
888
|
+
}
|
|
889
|
+
//#endregion
|
|
890
|
+
export { verifyPortableObjectProof as a, verifyObject as i, hasProofLike as n, verifyProof as o, signObject as r, createProof as t };
|