@fedify/fedify 2.4.0-dev.1727 → 2.4.0-dev.1758
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-DWu8BAPs.mjs} +2 -2
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{deno-Deyoxesx.mjs → deno-D7ld9Esp.mjs} +1 -1
- package/dist/{docloader-B3kuDBg9.mjs → docloader-Cxg2qaBo.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-1xfo16fx.cjs} +3 -3
- package/dist/{http-Bw6s_dOB.js → http-5U30379W.js} +1 -1
- package/dist/{http-TjhZfiSN.mjs → http-BBDPU-Rw.mjs} +3 -3
- package/dist/{http-AyKngJ_h.cjs → http-DUm4owJl.cjs} +1 -1
- package/dist/{key-b4kf4yZT.mjs → key-BfV1x8KO.mjs} +2 -2
- package/dist/{kv-cache-CQsh-KUw.js → kv-cache-DT6mvUaV.js} +1 -1
- package/dist/{kv-cache-BTVaZrf-.cjs → kv-cache-ufxbpVJ1.cjs} +1 -1
- package/dist/{kv-cache-CpILvhUV.mjs → kv-cache-z0NLyZT_.mjs} +1 -1
- package/dist/{ld-CKujkAfG.mjs → ld-B2FQHinq.mjs} +3 -3
- package/dist/{metrics-AlCGTDrr.mjs → metrics-B9zYPaIm.mjs} +1 -1
- package/dist/{middleware-LPbZz6-1.mjs → middleware-2cCYidZp.mjs} +1 -1
- package/dist/{middleware-CZcuedl-.js → middleware-Bcc-Y7Lj.js} +3 -3
- package/dist/{middleware-C9Y5lLZN.mjs → middleware-SfRDkyWy.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-C7EZVCtD.mjs} +2 -2
- package/dist/{proof-DcSm-2eG.cjs → proof-BcFOlgpM.cjs} +607 -29
- package/dist/proof-CFbBDSR8.mjs +890 -0
- package/dist/{proof-BTBolSnd.js → proof-DEeUDySM.js} +603 -31
- package/dist/{send-DG7JG280.mjs → send-BwlAzLrv.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-D6rOK4lX.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
package/dist/sig/proof.test.mjs
CHANGED
|
@@ -10,11 +10,12 @@ import { t as assertRejects } from "../assert_rejects-wFbSPn1O.mjs";
|
|
|
10
10
|
import { t as assert } from "../assert-CebeUf0C.mjs";
|
|
11
11
|
import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
|
|
12
12
|
import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-BQSHRLhg.mjs";
|
|
13
|
-
import { a as
|
|
13
|
+
import { a as verifyPortableObjectProof, i as verifyObject, n as hasProofLike, o as verifyProof, r as signObject, t as createProof } from "../proof-CFbBDSR8.mjs";
|
|
14
14
|
import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place } from "@fedify/vocab";
|
|
15
15
|
import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
16
|
-
import { decodeMultibase, exportDidKey, exportMultibaseKey, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
|
|
16
|
+
import { decodeMultibase, encodeMultibase, exportDidKey, exportMultibaseKey, formatIri, importMultibaseKey, parseIri } from "@fedify/vocab-runtime";
|
|
17
17
|
import { decodeHex } from "byte-encodings/hex";
|
|
18
|
+
import serialize from "json-canon";
|
|
18
19
|
//#region src/sig/proof.test.ts
|
|
19
20
|
const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
|
|
20
21
|
"kty": "OKP",
|
|
@@ -25,6 +26,52 @@ const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", {
|
|
|
25
26
|
ext: true
|
|
26
27
|
}, "Ed25519", true, ["sign"]);
|
|
27
28
|
const fep8b32TestVectorKeyId = new URL("https://server.example/users/alice#ed25519-key");
|
|
29
|
+
const portableDid = await exportDidKey(ed25519PublicKey.publicKey);
|
|
30
|
+
const portableDidMethod = portableDid.substring(8);
|
|
31
|
+
const portableKeyId = new URL(`${portableDid}#${portableDidMethod}`);
|
|
32
|
+
const portableContext = ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"];
|
|
33
|
+
const portableProofCreated = "2023-02-24T23:36:38Z";
|
|
34
|
+
async function signPortableJsonLd(document, { privateKey = ed25519PrivateKey, verificationMethod = portableKeyId, proofOptions = {} } = {}) {
|
|
35
|
+
const proofConfig = {
|
|
36
|
+
"@context": document["@context"],
|
|
37
|
+
type: "DataIntegrityProof",
|
|
38
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
39
|
+
verificationMethod: verificationMethod.href,
|
|
40
|
+
proofPurpose: "assertionMethod",
|
|
41
|
+
created: portableProofCreated,
|
|
42
|
+
...proofOptions
|
|
43
|
+
};
|
|
44
|
+
const encoder = new TextEncoder();
|
|
45
|
+
const proofDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(proofConfig)));
|
|
46
|
+
const messageDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(document)));
|
|
47
|
+
const digest = new Uint8Array(proofDigest.byteLength + messageDigest.byteLength);
|
|
48
|
+
digest.set(new Uint8Array(proofDigest), 0);
|
|
49
|
+
digest.set(new Uint8Array(messageDigest), proofDigest.byteLength);
|
|
50
|
+
const signature = await crypto.subtle.sign("Ed25519", privateKey, digest);
|
|
51
|
+
return {
|
|
52
|
+
...document,
|
|
53
|
+
proof: {
|
|
54
|
+
...proofConfig,
|
|
55
|
+
proofValue: new TextDecoder().decode(encodeMultibase("base58btc", new Uint8Array(signature)))
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function parsePortableProof(document) {
|
|
60
|
+
const proof = document.proof;
|
|
61
|
+
assert(typeof proof === "object" && proof != null && !Array.isArray(proof));
|
|
62
|
+
return await DataIntegrityProof.fromJsonLd({
|
|
63
|
+
"@context": document["@context"],
|
|
64
|
+
...proof
|
|
65
|
+
}, {
|
|
66
|
+
documentLoader: mockDocumentLoader,
|
|
67
|
+
contextLoader: mockDocumentLoader
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function assertPortableProofVerified(document, options) {
|
|
71
|
+
const key = await verifyProof(document, await parsePortableProof(document), options);
|
|
72
|
+
assert(key != null);
|
|
73
|
+
assertEquals(key.id, portableKeyId);
|
|
74
|
+
}
|
|
28
75
|
const fep8b32TestVectorActivity = new Create({
|
|
29
76
|
id: new URL("https://server.example/activities/1"),
|
|
30
77
|
actor: new URL("https://server.example/users/alice"),
|
|
@@ -328,6 +375,202 @@ test("verifyProof()", async () => {
|
|
|
328
375
|
}), null);
|
|
329
376
|
assertFalse(contextLoaderCalls.includes("https://attacker.example/ctx"));
|
|
330
377
|
});
|
|
378
|
+
test("verifyProof() authenticates the complete proof configuration", async (t) => {
|
|
379
|
+
const jsonLd = {
|
|
380
|
+
"@context": portableContext,
|
|
381
|
+
id: "https://example.com/activities/complete-proof-options",
|
|
382
|
+
type: "Create",
|
|
383
|
+
actor: "https://example.com/users/alice",
|
|
384
|
+
object: {
|
|
385
|
+
type: "Note",
|
|
386
|
+
content: "Every proof option is authenticated."
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const options = {
|
|
390
|
+
documentLoader: mockDocumentLoader,
|
|
391
|
+
contextLoader: mockDocumentLoader
|
|
392
|
+
};
|
|
393
|
+
await t.step("rejects an expires option added after signing", async () => {
|
|
394
|
+
const signed = await signPortableJsonLd(jsonLd);
|
|
395
|
+
const tampered = {
|
|
396
|
+
...signed,
|
|
397
|
+
proof: {
|
|
398
|
+
...signed.proof,
|
|
399
|
+
expires: "3000-01-01T00:00:00Z"
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
|
|
403
|
+
});
|
|
404
|
+
await t.step("accepts an authenticated future expiration", async () => {
|
|
405
|
+
await assertPortableProofVerified(await signPortableJsonLd(jsonLd, { proofOptions: { expires: "3000-01-01T00:00:00Z" } }), options);
|
|
406
|
+
});
|
|
407
|
+
await t.step("rejects an authenticated expired proof", async () => {
|
|
408
|
+
const signed = await signPortableJsonLd(jsonLd, { proofOptions: { expires: "2000-01-01T00:00:00Z" } });
|
|
409
|
+
assertEquals(await verifyProof(signed, await parsePortableProof(signed), options), null);
|
|
410
|
+
});
|
|
411
|
+
await t.step("authenticates domain, challenge, and nonce", async () => {
|
|
412
|
+
const signed = await signPortableJsonLd(jsonLd, { proofOptions: {
|
|
413
|
+
domain: ["social.example", "https://social.example"],
|
|
414
|
+
challenge: "challenge-123",
|
|
415
|
+
nonce: "nonce-456"
|
|
416
|
+
} });
|
|
417
|
+
const proof = await parsePortableProof(signed);
|
|
418
|
+
const matchingKey = await verifyProof(signed, proof, {
|
|
419
|
+
...options,
|
|
420
|
+
domain: ["https://social.example", "social.example"],
|
|
421
|
+
challenge: "challenge-123"
|
|
422
|
+
});
|
|
423
|
+
assert(matchingKey != null);
|
|
424
|
+
assertEquals(matchingKey.id, portableKeyId);
|
|
425
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
426
|
+
...options,
|
|
427
|
+
domain: ["social.example"],
|
|
428
|
+
challenge: "challenge-123"
|
|
429
|
+
}), null);
|
|
430
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
431
|
+
...options,
|
|
432
|
+
domain: ["https://social.example", "social.example"],
|
|
433
|
+
challenge: "wrong-challenge"
|
|
434
|
+
}), null);
|
|
435
|
+
for (const [property, value] of [
|
|
436
|
+
["domain", ["other.example"]],
|
|
437
|
+
["challenge", "tampered-challenge"],
|
|
438
|
+
["nonce", "tampered-nonce"]
|
|
439
|
+
]) {
|
|
440
|
+
const tampered = {
|
|
441
|
+
...signed,
|
|
442
|
+
proof: {
|
|
443
|
+
...signed.proof,
|
|
444
|
+
[property]: value
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
await t.step("requires requested domain and challenge options", async () => {
|
|
451
|
+
const signed = await signPortableJsonLd(jsonLd);
|
|
452
|
+
const proof = await parsePortableProof(signed);
|
|
453
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
454
|
+
...options,
|
|
455
|
+
domain: "social.example"
|
|
456
|
+
}), null);
|
|
457
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
458
|
+
...options,
|
|
459
|
+
challenge: "challenge-123"
|
|
460
|
+
}), null);
|
|
461
|
+
});
|
|
462
|
+
await t.step("rejects malformed proof options", async () => {
|
|
463
|
+
for (const proofOptions of [
|
|
464
|
+
{ expires: "not-a-date" },
|
|
465
|
+
{ domain: ["social.example", 42] },
|
|
466
|
+
{ challenge: 42 },
|
|
467
|
+
{ nonce: { value: "not-a-string" } },
|
|
468
|
+
{ nonce: [
|
|
469
|
+
"not",
|
|
470
|
+
"a",
|
|
471
|
+
"string"
|
|
472
|
+
] },
|
|
473
|
+
{ previousProof: ["urn:uuid:proof-1", 42] }
|
|
474
|
+
]) {
|
|
475
|
+
const signed = await signPortableJsonLd(jsonLd, { proofOptions });
|
|
476
|
+
assertEquals(await verifyProof(signed, await parsePortableProof(signed), options), null);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
await t.step("authenticates aliased and extension options", async () => {
|
|
480
|
+
const signed = await signPortableJsonLd({
|
|
481
|
+
...jsonLd,
|
|
482
|
+
"@context": [...portableContext, {
|
|
483
|
+
validUntil: {
|
|
484
|
+
"@id": "https://w3id.org/security#expiration",
|
|
485
|
+
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
|
|
486
|
+
},
|
|
487
|
+
extensionOption: "https://example.com/security#extensionOption"
|
|
488
|
+
}]
|
|
489
|
+
}, { proofOptions: {
|
|
490
|
+
validUntil: "3000-01-01T00:00:00Z",
|
|
491
|
+
extensionOption: { nested: ["one", "two"] }
|
|
492
|
+
} });
|
|
493
|
+
await assertPortableProofVerified(signed, options);
|
|
494
|
+
for (const [property, value] of [["validUntil", "2999-01-01T00:00:00Z"], ["extensionOption", { nested: ["one", "changed"] }]]) {
|
|
495
|
+
const tampered = {
|
|
496
|
+
...signed,
|
|
497
|
+
proof: {
|
|
498
|
+
...signed.proof,
|
|
499
|
+
[property]: value
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
assertEquals(await verifyProof(tampered, await parsePortableProof(tampered), options), null);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
await t.step("distinguishes equivalent and ambiguous raw proof configurations", async () => {
|
|
506
|
+
const signed = await signPortableJsonLd(jsonLd);
|
|
507
|
+
const proof = signed.proof;
|
|
508
|
+
const parsed = await parsePortableProof(signed);
|
|
509
|
+
assert(await verifyProof({
|
|
510
|
+
...signed,
|
|
511
|
+
proof: [proof, structuredClone(proof)]
|
|
512
|
+
}, parsed, options) != null);
|
|
513
|
+
const inheritedContextProof = structuredClone(proof);
|
|
514
|
+
delete inheritedContextProof["@context"];
|
|
515
|
+
assert(await verifyProof({
|
|
516
|
+
...signed,
|
|
517
|
+
proof: [proof, inheritedContextProof]
|
|
518
|
+
}, parsed, options) != null);
|
|
519
|
+
assertEquals(await verifyProof({
|
|
520
|
+
...signed,
|
|
521
|
+
proof: [proof, {
|
|
522
|
+
...structuredClone(proof),
|
|
523
|
+
expires: "3000-01-01T00:00:00Z"
|
|
524
|
+
}]
|
|
525
|
+
}, parsed, options), null);
|
|
526
|
+
});
|
|
527
|
+
await t.step("accepts literal proof fields with an unresolved extra context", async () => {
|
|
528
|
+
const signed = await signPortableJsonLd({
|
|
529
|
+
...jsonLd,
|
|
530
|
+
"@context": [...portableContext, "https://context.example/unrelated"]
|
|
531
|
+
});
|
|
532
|
+
const rawProof = signed.proof;
|
|
533
|
+
const key = await verifyProof(signed, new DataIntegrityProof({
|
|
534
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
535
|
+
verificationMethod: portableKeyId,
|
|
536
|
+
proofPurpose: "assertionMethod",
|
|
537
|
+
proofValue: decodeMultibase(rawProof.proofValue),
|
|
538
|
+
created: Temporal.Instant.from(portableProofCreated)
|
|
539
|
+
}), options);
|
|
540
|
+
assert(key != null);
|
|
541
|
+
assertEquals(key.id, portableKeyId);
|
|
542
|
+
});
|
|
543
|
+
await t.step("rejects aliases whose proof context cannot be resolved safely", async () => {
|
|
544
|
+
const contextUrl = "https://context.example/proof-options";
|
|
545
|
+
const signed = await signPortableJsonLd({
|
|
546
|
+
...jsonLd,
|
|
547
|
+
"@context": [...portableContext, contextUrl]
|
|
548
|
+
}, { proofOptions: { validUntil: "2000-01-01T00:00:00Z" } });
|
|
549
|
+
const rawProof = signed.proof;
|
|
550
|
+
const proof = new DataIntegrityProof({
|
|
551
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
552
|
+
verificationMethod: portableKeyId,
|
|
553
|
+
proofPurpose: "assertionMethod",
|
|
554
|
+
proofValue: decodeMultibase(rawProof.proofValue),
|
|
555
|
+
created: Temporal.Instant.from(portableProofCreated)
|
|
556
|
+
});
|
|
557
|
+
const contextLoader = async (url) => {
|
|
558
|
+
if (url !== contextUrl) return await mockDocumentLoader(url);
|
|
559
|
+
return {
|
|
560
|
+
contextUrl: null,
|
|
561
|
+
documentUrl: url,
|
|
562
|
+
document: { "@context": { validUntil: {
|
|
563
|
+
"@id": "https://w3id.org/security#expiration",
|
|
564
|
+
"@type": "http://www.w3.org/2001/XMLSchema#dateTime"
|
|
565
|
+
} } }
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
assertEquals(await verifyProof(signed, proof, {
|
|
569
|
+
...options,
|
|
570
|
+
contextLoader
|
|
571
|
+
}), null);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
331
574
|
test("verifyProof() resolves did:key verification methods locally", async () => {
|
|
332
575
|
const multibaseKey = (await exportDidKey(ed25519PublicKey.publicKey)).slice(8);
|
|
333
576
|
const did = `did:key:${multibaseKey}`;
|
|
@@ -510,12 +753,13 @@ test("verifyProof() records verification duration metric", async (t) => {
|
|
|
510
753
|
});
|
|
511
754
|
await t.step("verifyObject() wrapper emits one measurement per inner verifyProof()", async () => {
|
|
512
755
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
756
|
+
const { "@context": _proofContext, ...embeddedProof } = await proof.toJsonLd({
|
|
757
|
+
format: "compact",
|
|
758
|
+
contextLoader: mockDocumentLoader
|
|
759
|
+
});
|
|
513
760
|
assert(await verifyObject(Create, {
|
|
514
761
|
...jsonLd,
|
|
515
|
-
proof:
|
|
516
|
-
format: "compact",
|
|
517
|
-
contextLoader: mockDocumentLoader
|
|
518
|
-
})
|
|
762
|
+
proof: embeddedProof
|
|
519
763
|
}, {
|
|
520
764
|
documentLoader: mockDocumentLoader,
|
|
521
765
|
contextLoader: mockDocumentLoader,
|
|
@@ -549,6 +793,632 @@ test("verifyProof() records verification duration metric", async (t) => {
|
|
|
549
793
|
assertFalse("object_integrity_proofs.cryptosuite" in measurements[0].attributes);
|
|
550
794
|
});
|
|
551
795
|
});
|
|
796
|
+
test("verifyPortableObjectProof()", async (t) => {
|
|
797
|
+
const options = {
|
|
798
|
+
documentLoader() {
|
|
799
|
+
throw new TypeError("did:key must not use the document loader");
|
|
800
|
+
},
|
|
801
|
+
contextLoader: mockDocumentLoader
|
|
802
|
+
};
|
|
803
|
+
const objectId = `ap://did:key:${portableDidMethod}/objects/1`;
|
|
804
|
+
const unsignedObject = {
|
|
805
|
+
"@context": portableContext,
|
|
806
|
+
id: objectId,
|
|
807
|
+
type: "Note",
|
|
808
|
+
attributedTo: `ap://did:key:${portableDidMethod}/actor`,
|
|
809
|
+
content: "Portable note"
|
|
810
|
+
};
|
|
811
|
+
await t.step("verifies a portable object with a matching DID proof", async () => {
|
|
812
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject), options);
|
|
813
|
+
assert(result.verified);
|
|
814
|
+
assertEquals(result.keys.length, 1);
|
|
815
|
+
assertEquals(result.keys[0].id, portableKeyId);
|
|
816
|
+
});
|
|
817
|
+
await t.step("rejects unauthenticated and expired proof options", async () => {
|
|
818
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
819
|
+
const tamperedResult = await verifyPortableObjectProof({
|
|
820
|
+
...signed,
|
|
821
|
+
proof: {
|
|
822
|
+
...signed.proof,
|
|
823
|
+
expires: "3000-01-01T00:00:00Z"
|
|
824
|
+
}
|
|
825
|
+
}, options);
|
|
826
|
+
assertFalse(tamperedResult.verified);
|
|
827
|
+
assertEquals(tamperedResult.reason, {
|
|
828
|
+
type: "invalidProof",
|
|
829
|
+
proofIndex: 0
|
|
830
|
+
});
|
|
831
|
+
const expiredResult = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { proofOptions: { expires: "2000-01-01T00:00:00Z" } }), options);
|
|
832
|
+
assertFalse(expiredResult.verified);
|
|
833
|
+
assertEquals(expiredResult.reason, {
|
|
834
|
+
type: "invalidProof",
|
|
835
|
+
proofIndex: 0
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
await t.step("verifies aliased proof properties", async () => {
|
|
839
|
+
const proofAlias = "integrityProof";
|
|
840
|
+
const proofIri = "https://w3id.org/security#proof";
|
|
841
|
+
for (const { context, proofProperty, typeProperty, type } of [
|
|
842
|
+
{
|
|
843
|
+
context: { [proofAlias]: proofIri },
|
|
844
|
+
proofProperty: proofAlias,
|
|
845
|
+
typeProperty: "type",
|
|
846
|
+
type: "Note"
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
context: { PortableNote: {
|
|
850
|
+
"@id": "https://www.w3.org/ns/activitystreams#Note",
|
|
851
|
+
"@context": { [proofAlias]: proofIri }
|
|
852
|
+
} },
|
|
853
|
+
proofProperty: proofAlias,
|
|
854
|
+
typeProperty: "type",
|
|
855
|
+
type: "PortableNote"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
context: {
|
|
859
|
+
kind: "@type",
|
|
860
|
+
PortableNote: {
|
|
861
|
+
"@id": "https://www.w3.org/ns/activitystreams#Note",
|
|
862
|
+
"@context": { [proofAlias]: proofIri }
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
proofProperty: proofAlias,
|
|
866
|
+
typeProperty: "kind",
|
|
867
|
+
type: "PortableNote"
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
context: { sec: "https://w3id.org/security#" },
|
|
871
|
+
proofProperty: "sec:proof",
|
|
872
|
+
typeProperty: "type",
|
|
873
|
+
type: "Note"
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
context: { sec: {
|
|
877
|
+
"@id": "https://w3id.org/security#",
|
|
878
|
+
"@prefix": true
|
|
879
|
+
} },
|
|
880
|
+
proofProperty: "sec:proof",
|
|
881
|
+
typeProperty: "type",
|
|
882
|
+
type: "Note"
|
|
883
|
+
}
|
|
884
|
+
]) {
|
|
885
|
+
const document = {
|
|
886
|
+
...unsignedObject,
|
|
887
|
+
"@context": [...portableContext, context]
|
|
888
|
+
};
|
|
889
|
+
delete document.type;
|
|
890
|
+
document[typeProperty] = type;
|
|
891
|
+
const { proof, ...signed } = await signPortableJsonLd(document);
|
|
892
|
+
const result = await verifyPortableObjectProof({
|
|
893
|
+
...signed,
|
|
894
|
+
[proofProperty]: proof
|
|
895
|
+
}, options);
|
|
896
|
+
assert(result.verified);
|
|
897
|
+
assertEquals(result.keys.length, 1);
|
|
898
|
+
assertEquals(result.keys[0].id, portableKeyId);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
await t.step("verifies proof aliases from caller-loaded contexts", async () => {
|
|
902
|
+
const contextUrl = "https://context.example/security";
|
|
903
|
+
const proofAlias = "integrityProof";
|
|
904
|
+
const proofIri = "https://w3id.org/security#proof";
|
|
905
|
+
let contextLoads = 0;
|
|
906
|
+
const contextLoader = async (url) => {
|
|
907
|
+
if (url !== contextUrl) return await mockDocumentLoader(url);
|
|
908
|
+
contextLoads++;
|
|
909
|
+
return {
|
|
910
|
+
contextUrl: null,
|
|
911
|
+
documentUrl: url,
|
|
912
|
+
document: { "@context": { [proofAlias]: proofIri } }
|
|
913
|
+
};
|
|
914
|
+
};
|
|
915
|
+
const { proof, ...signed } = await signPortableJsonLd({
|
|
916
|
+
...unsignedObject,
|
|
917
|
+
"@context": [...portableContext, contextUrl]
|
|
918
|
+
});
|
|
919
|
+
const result = await verifyPortableObjectProof({
|
|
920
|
+
...signed,
|
|
921
|
+
[proofAlias]: proof
|
|
922
|
+
}, {
|
|
923
|
+
...options,
|
|
924
|
+
contextLoader
|
|
925
|
+
});
|
|
926
|
+
assert(result.verified);
|
|
927
|
+
assertEquals(result.keys.length, 1);
|
|
928
|
+
assertEquals(result.keys[0].id, portableKeyId);
|
|
929
|
+
assertEquals(contextLoads, 1);
|
|
930
|
+
});
|
|
931
|
+
await t.step("verifies portable actors and activities by shape", async () => {
|
|
932
|
+
for (const document of [{
|
|
933
|
+
"@context": portableContext,
|
|
934
|
+
id: `ap+ef61://did:key:${portableDidMethod}/actor`,
|
|
935
|
+
type: "UnknownActorType",
|
|
936
|
+
inbox: "https://gateway.example/users/alice/inbox",
|
|
937
|
+
outbox: "https://gateway.example/users/alice/outbox"
|
|
938
|
+
}, {
|
|
939
|
+
"@context": portableContext,
|
|
940
|
+
id: `ap://did:key:${portableDidMethod}/activities/1`,
|
|
941
|
+
type: "UnknownActivityType",
|
|
942
|
+
actor: `ap://did:key:${portableDidMethod}/actor`,
|
|
943
|
+
object: objectId
|
|
944
|
+
}]) {
|
|
945
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(document), options);
|
|
946
|
+
assert(result.verified);
|
|
947
|
+
assertEquals(result.keys.length, 1);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
await t.step("uses the normative FEP-2277 precedence order", async () => {
|
|
951
|
+
for (const document of [{
|
|
952
|
+
"@context": portableContext,
|
|
953
|
+
id: `ap://did:key:${portableDidMethod}/actor`,
|
|
954
|
+
type: "Collection",
|
|
955
|
+
inbox: "https://gateway.example/users/alice/inbox",
|
|
956
|
+
outbox: "https://gateway.example/users/alice/outbox",
|
|
957
|
+
totalItems: 0
|
|
958
|
+
}, {
|
|
959
|
+
"@context": portableContext,
|
|
960
|
+
id: `ap://did:key:${portableDidMethod}/activities/1`,
|
|
961
|
+
type: "Collection",
|
|
962
|
+
actor: `ap://did:key:${portableDidMethod}/actor`,
|
|
963
|
+
totalItems: 0
|
|
964
|
+
}]) assertEquals(await verifyPortableObjectProof(document, options), {
|
|
965
|
+
verified: false,
|
|
966
|
+
reason: { type: "missingProof" }
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
await t.step("accepts portable URI spelling and location-hint variants", async () => {
|
|
970
|
+
const variants = [
|
|
971
|
+
`ap+ef61://did%3Akey%3A${portableDidMethod}/objects/1`,
|
|
972
|
+
`AP://did:key:${portableDidMethod}/objects/1`,
|
|
973
|
+
`ap://did:key:${portableDidMethod}/objects/1?%40gateway=${encodeURIComponent("https://gateway.example")}`,
|
|
974
|
+
`ap://did:key:${portableDidMethod}/objects/1#content`
|
|
975
|
+
];
|
|
976
|
+
for (const id of variants) assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
977
|
+
...unsignedObject,
|
|
978
|
+
id
|
|
979
|
+
}), options)).verified, id);
|
|
980
|
+
});
|
|
981
|
+
await t.step("supports matching DID methods resolved by a caller", async () => {
|
|
982
|
+
const verificationMethod = new URL("did:web:example.com#key");
|
|
983
|
+
const document = {
|
|
984
|
+
...unsignedObject,
|
|
985
|
+
id: "ap://did:web:example.com/objects/1"
|
|
986
|
+
};
|
|
987
|
+
let fetches = 0;
|
|
988
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd(document, { verificationMethod }), {
|
|
989
|
+
contextLoader: mockDocumentLoader,
|
|
990
|
+
documentLoader: async (url) => {
|
|
991
|
+
fetches++;
|
|
992
|
+
assertEquals(url, verificationMethod.href);
|
|
993
|
+
return {
|
|
994
|
+
contextUrl: null,
|
|
995
|
+
documentUrl: url,
|
|
996
|
+
document: {
|
|
997
|
+
"@context": "https://w3id.org/security/multikey/v1",
|
|
998
|
+
id: verificationMethod.href,
|
|
999
|
+
type: "Multikey",
|
|
1000
|
+
controller: "did:web:example.com",
|
|
1001
|
+
publicKeyMultibase: await exportMultibaseKey(ed25519PublicKey.publicKey)
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
})).verified);
|
|
1006
|
+
assertEquals(fetches, 1);
|
|
1007
|
+
});
|
|
1008
|
+
await t.step("reports a missing proof on portable objects", async () => {
|
|
1009
|
+
assertEquals(await verifyPortableObjectProof(unsignedObject, options), {
|
|
1010
|
+
verified: false,
|
|
1011
|
+
reason: { type: "missingProof" }
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
await t.step("keeps non-portable documents outside the policy", async () => {
|
|
1015
|
+
for (const document of [
|
|
1016
|
+
{
|
|
1017
|
+
...unsignedObject,
|
|
1018
|
+
id: "https://social.example/objects/1"
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
...unsignedObject,
|
|
1022
|
+
id: "https://gateway.example/.well-known/apgateway/did:key:z6MkAlice/objects/1"
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
"@context": portableContext,
|
|
1026
|
+
type: "Note",
|
|
1027
|
+
content: "Object without an ID"
|
|
1028
|
+
}
|
|
1029
|
+
]) assertEquals(await verifyPortableObjectProof(document, options), {
|
|
1030
|
+
verified: false,
|
|
1031
|
+
reason: { type: "notPortableObject" }
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
await t.step("short-circuits clearly non-portable raw IDs", async () => {
|
|
1035
|
+
let contextLoads = 0;
|
|
1036
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1037
|
+
"@context": "https://attacker.example/context",
|
|
1038
|
+
"@id": "https://social.example/objects/1"
|
|
1039
|
+
}, {
|
|
1040
|
+
...options,
|
|
1041
|
+
contextLoader() {
|
|
1042
|
+
contextLoads++;
|
|
1043
|
+
throw new TypeError("unexpected context load");
|
|
1044
|
+
}
|
|
1045
|
+
}), {
|
|
1046
|
+
verified: false,
|
|
1047
|
+
reason: { type: "notPortableObject" }
|
|
1048
|
+
});
|
|
1049
|
+
assertEquals(contextLoads, 0);
|
|
1050
|
+
});
|
|
1051
|
+
await t.step("distinguishes unsecured and signed collections", async () => {
|
|
1052
|
+
const collection = {
|
|
1053
|
+
"@context": portableContext,
|
|
1054
|
+
id: `ap://did:key:${portableDidMethod}/collections/1`,
|
|
1055
|
+
type: "Note",
|
|
1056
|
+
totalItems: 0
|
|
1057
|
+
};
|
|
1058
|
+
assertEquals(await verifyPortableObjectProof(collection, options), {
|
|
1059
|
+
verified: false,
|
|
1060
|
+
reason: { type: "unsecuredCollection" }
|
|
1061
|
+
});
|
|
1062
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd(collection), options)).verified);
|
|
1063
|
+
});
|
|
1064
|
+
await t.step("classifies unsupported core types without using type", async () => {
|
|
1065
|
+
const cases = [
|
|
1066
|
+
{
|
|
1067
|
+
document: {
|
|
1068
|
+
...unsignedObject,
|
|
1069
|
+
href: "https://example.com/"
|
|
1070
|
+
},
|
|
1071
|
+
objectType: "link"
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
document: {
|
|
1075
|
+
...unsignedObject,
|
|
1076
|
+
"https://w3id.org/security#publicKeyMultibase": "z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2"
|
|
1077
|
+
},
|
|
1078
|
+
objectType: "verificationMethod"
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
document: {
|
|
1082
|
+
...unsignedObject,
|
|
1083
|
+
"https://w3id.org/security#publicKeyPem": "not-a-real-key"
|
|
1084
|
+
},
|
|
1085
|
+
objectType: "publicKey"
|
|
1086
|
+
}
|
|
1087
|
+
];
|
|
1088
|
+
for (const { document, objectType } of cases) assertEquals(await verifyPortableObjectProof(document, options), {
|
|
1089
|
+
verified: false,
|
|
1090
|
+
reason: {
|
|
1091
|
+
type: "unsupportedObjectType",
|
|
1092
|
+
objectType
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
await t.step("rejects non-DID verification methods before fetching", async () => {
|
|
1097
|
+
let fetched = false;
|
|
1098
|
+
const verificationMethod = new URL("https://attacker.example/keys/ed25519");
|
|
1099
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), {
|
|
1100
|
+
contextLoader: mockDocumentLoader,
|
|
1101
|
+
documentLoader() {
|
|
1102
|
+
fetched = true;
|
|
1103
|
+
throw new TypeError("unexpected fetch");
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
assertFalse(result.verified);
|
|
1107
|
+
assertEquals(result.reason, {
|
|
1108
|
+
type: "unsupportedVerificationMethod",
|
|
1109
|
+
proofIndex: 0,
|
|
1110
|
+
verificationMethod
|
|
1111
|
+
});
|
|
1112
|
+
assertFalse(fetched);
|
|
1113
|
+
});
|
|
1114
|
+
await t.step("rejects malformed DID verification methods", async () => {
|
|
1115
|
+
const verificationMethod = new URL("did:invalid");
|
|
1116
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), options);
|
|
1117
|
+
assertFalse(result.verified);
|
|
1118
|
+
assertEquals(result.reason, {
|
|
1119
|
+
type: "unsupportedVerificationMethod",
|
|
1120
|
+
proofIndex: 0,
|
|
1121
|
+
verificationMethod
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
await t.step("rejects a proof from another DID before fetching", async () => {
|
|
1125
|
+
let fetched = false;
|
|
1126
|
+
const verificationMethod = new URL("did:key:z6MkMallory#z6MkMallory");
|
|
1127
|
+
const result = await verifyPortableObjectProof(await signPortableJsonLd(unsignedObject, { verificationMethod }), {
|
|
1128
|
+
contextLoader: mockDocumentLoader,
|
|
1129
|
+
documentLoader() {
|
|
1130
|
+
fetched = true;
|
|
1131
|
+
throw new TypeError("unexpected fetch");
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
assertFalse(result.verified);
|
|
1135
|
+
assertEquals(result.reason, {
|
|
1136
|
+
type: "verificationMethodMismatch",
|
|
1137
|
+
proofIndex: 0,
|
|
1138
|
+
objectId: parseIri(objectId),
|
|
1139
|
+
verificationMethod
|
|
1140
|
+
});
|
|
1141
|
+
assertFalse(fetched);
|
|
1142
|
+
});
|
|
1143
|
+
await t.step("rejects proofs with multiple verification methods", async () => {
|
|
1144
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1145
|
+
const proof = signed.proof;
|
|
1146
|
+
for (const additionalVerificationMethod of ["did:key:z6MkMallory#z6MkMallory", "https://attacker.example/key"]) assertEquals(await verifyPortableObjectProof({
|
|
1147
|
+
...signed,
|
|
1148
|
+
proof: {
|
|
1149
|
+
...proof,
|
|
1150
|
+
verificationMethod: [portableKeyId.href, additionalVerificationMethod]
|
|
1151
|
+
}
|
|
1152
|
+
}, options), {
|
|
1153
|
+
verified: false,
|
|
1154
|
+
reason: {
|
|
1155
|
+
type: "invalidProof",
|
|
1156
|
+
proofIndex: 0
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
await t.step("rejects multiple values in functional proof fields", async () => {
|
|
1161
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1162
|
+
const proof = signed.proof;
|
|
1163
|
+
const cases = [
|
|
1164
|
+
["cryptosuite", "eddsa-jcs-2022"],
|
|
1165
|
+
["proofPurpose", "authentication"],
|
|
1166
|
+
["proofValue", proof.proofValue],
|
|
1167
|
+
["created", "2024-01-01T00:00:00Z"]
|
|
1168
|
+
];
|
|
1169
|
+
for (const [field, additionalValue] of cases) {
|
|
1170
|
+
const originalValue = proof[field];
|
|
1171
|
+
assert(originalValue != null);
|
|
1172
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1173
|
+
...signed,
|
|
1174
|
+
proof: {
|
|
1175
|
+
...proof,
|
|
1176
|
+
[field]: [originalValue, additionalValue]
|
|
1177
|
+
}
|
|
1178
|
+
}, options), {
|
|
1179
|
+
verified: false,
|
|
1180
|
+
reason: {
|
|
1181
|
+
type: "invalidProof",
|
|
1182
|
+
proofIndex: 0
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
await t.step("requires exactly the DataIntegrityProof type", async () => {
|
|
1188
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1189
|
+
const proof = signed.proof;
|
|
1190
|
+
const missingType = { ...proof };
|
|
1191
|
+
delete missingType.type;
|
|
1192
|
+
for (const invalidProof of [
|
|
1193
|
+
missingType,
|
|
1194
|
+
{
|
|
1195
|
+
...proof,
|
|
1196
|
+
type: "Object"
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
...proof,
|
|
1200
|
+
type: ["DataIntegrityProof", "Object"]
|
|
1201
|
+
}
|
|
1202
|
+
]) assertEquals(await verifyPortableObjectProof({
|
|
1203
|
+
...signed,
|
|
1204
|
+
proof: invalidProof
|
|
1205
|
+
}, options), {
|
|
1206
|
+
verified: false,
|
|
1207
|
+
reason: {
|
|
1208
|
+
type: "invalidProof",
|
|
1209
|
+
proofIndex: 0
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
await t.step("reports cryptographic failures separately", async () => {
|
|
1214
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1215
|
+
...await signPortableJsonLd(unsignedObject),
|
|
1216
|
+
content: "Tampered"
|
|
1217
|
+
}, options), {
|
|
1218
|
+
verified: false,
|
|
1219
|
+
reason: {
|
|
1220
|
+
type: "invalidProof",
|
|
1221
|
+
proofIndex: 0
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
await t.step("reports malformed proof structures as invalid", async () => {
|
|
1226
|
+
for (const proof of [{ type: "DataIntegrityProof" }, {
|
|
1227
|
+
type: "DataIntegrityProof",
|
|
1228
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
1229
|
+
verificationMethod: portableKeyId.href,
|
|
1230
|
+
proofPurpose: "assertionMethod",
|
|
1231
|
+
proofValue: "zInvalid",
|
|
1232
|
+
created: "not-an-instant"
|
|
1233
|
+
}]) assertEquals(await verifyPortableObjectProof({
|
|
1234
|
+
...unsignedObject,
|
|
1235
|
+
proof
|
|
1236
|
+
}, options), {
|
|
1237
|
+
verified: false,
|
|
1238
|
+
reason: {
|
|
1239
|
+
type: "invalidProof",
|
|
1240
|
+
proofIndex: 0
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
await t.step("accepts multiple valid proofs in input order", async () => {
|
|
1245
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1246
|
+
const proof = signed.proof;
|
|
1247
|
+
const result = await verifyPortableObjectProof({
|
|
1248
|
+
...signed,
|
|
1249
|
+
proof: [proof, structuredClone(proof)]
|
|
1250
|
+
}, options);
|
|
1251
|
+
assert(result.verified);
|
|
1252
|
+
assertEquals(result.keys.length, 2);
|
|
1253
|
+
assertEquals(result.keys.map((key) => key.id), [portableKeyId, portableKeyId]);
|
|
1254
|
+
});
|
|
1255
|
+
await t.step("rejects a tampered duplicate proof", async () => {
|
|
1256
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1257
|
+
const proof = signed.proof;
|
|
1258
|
+
assertFalse((await verifyPortableObjectProof({
|
|
1259
|
+
...signed,
|
|
1260
|
+
proof: [proof, {
|
|
1261
|
+
...structuredClone(proof),
|
|
1262
|
+
expires: "3000-01-01T00:00:00Z"
|
|
1263
|
+
}]
|
|
1264
|
+
}, options)).verified);
|
|
1265
|
+
});
|
|
1266
|
+
await t.step("matches proofs across aliased properties", async () => {
|
|
1267
|
+
const document = {
|
|
1268
|
+
...unsignedObject,
|
|
1269
|
+
"@context": [...portableContext, {
|
|
1270
|
+
firstProof: "https://w3id.org/security#proof",
|
|
1271
|
+
secondProof: "https://w3id.org/security#proof"
|
|
1272
|
+
}]
|
|
1273
|
+
};
|
|
1274
|
+
const firstSigned = await signPortableJsonLd(document, { proofOptions: { domain: "first.example" } });
|
|
1275
|
+
const secondSigned = await signPortableJsonLd(document, { proofOptions: { domain: "second.example" } });
|
|
1276
|
+
const result = await verifyPortableObjectProof({
|
|
1277
|
+
...document,
|
|
1278
|
+
secondProof: secondSigned.proof,
|
|
1279
|
+
firstProof: firstSigned.proof
|
|
1280
|
+
}, options);
|
|
1281
|
+
assert(result.verified);
|
|
1282
|
+
assertEquals(result.keys.length, 2);
|
|
1283
|
+
});
|
|
1284
|
+
await t.step("canonicalizes the document only once for multiple proofs", async () => {
|
|
1285
|
+
async function countContentReads(proofCount) {
|
|
1286
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1287
|
+
const proof = signed.proof;
|
|
1288
|
+
const document = {
|
|
1289
|
+
...signed,
|
|
1290
|
+
proof: Array.from({ length: proofCount }, () => structuredClone(proof))
|
|
1291
|
+
};
|
|
1292
|
+
let contentReads = 0;
|
|
1293
|
+
Object.defineProperty(document, "content", {
|
|
1294
|
+
configurable: true,
|
|
1295
|
+
enumerable: true,
|
|
1296
|
+
get() {
|
|
1297
|
+
contentReads++;
|
|
1298
|
+
return unsignedObject.content;
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
const result = await verifyPortableObjectProof(document, options);
|
|
1302
|
+
assert(result.verified);
|
|
1303
|
+
assertEquals(result.keys.length, proofCount);
|
|
1304
|
+
return contentReads;
|
|
1305
|
+
}
|
|
1306
|
+
assertEquals(await countContentReads(3), await countContentReads(1));
|
|
1307
|
+
});
|
|
1308
|
+
await t.step("requires every proof to verify", async () => {
|
|
1309
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1310
|
+
const proof = signed.proof;
|
|
1311
|
+
assertEquals(await verifyPortableObjectProof({
|
|
1312
|
+
...signed,
|
|
1313
|
+
proof: [proof, {
|
|
1314
|
+
...proof,
|
|
1315
|
+
proofValue: "zInvalid"
|
|
1316
|
+
}]
|
|
1317
|
+
}, options), {
|
|
1318
|
+
verified: false,
|
|
1319
|
+
reason: {
|
|
1320
|
+
type: "invalidProof",
|
|
1321
|
+
proofIndex: 1
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
await t.step("checks every proof policy before resolving keys", async () => {
|
|
1326
|
+
const didWebId = "ap://did:web:example.com/objects/1";
|
|
1327
|
+
const didWebMethod = new URL("did:web:example.com#key");
|
|
1328
|
+
const didWebSigned = await signPortableJsonLd({
|
|
1329
|
+
...unsignedObject,
|
|
1330
|
+
id: didWebId
|
|
1331
|
+
}, { verificationMethod: didWebMethod });
|
|
1332
|
+
const httpMethod = new URL("https://attacker.example/key");
|
|
1333
|
+
const httpSigned = await signPortableJsonLd({
|
|
1334
|
+
...unsignedObject,
|
|
1335
|
+
id: didWebId
|
|
1336
|
+
}, { verificationMethod: httpMethod });
|
|
1337
|
+
let fetched = false;
|
|
1338
|
+
const result = await verifyPortableObjectProof({
|
|
1339
|
+
...didWebSigned,
|
|
1340
|
+
proof: [didWebSigned.proof, httpSigned.proof]
|
|
1341
|
+
}, {
|
|
1342
|
+
contextLoader: mockDocumentLoader,
|
|
1343
|
+
documentLoader() {
|
|
1344
|
+
fetched = true;
|
|
1345
|
+
throw new TypeError("unexpected fetch");
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
assertFalse(result.verified);
|
|
1349
|
+
assertEquals(result.reason, {
|
|
1350
|
+
type: "unsupportedVerificationMethod",
|
|
1351
|
+
proofIndex: 1,
|
|
1352
|
+
verificationMethod: httpMethod
|
|
1353
|
+
});
|
|
1354
|
+
assertFalse(fetched);
|
|
1355
|
+
});
|
|
1356
|
+
await t.step("accepts the expanded proof property", async () => {
|
|
1357
|
+
const { proof, ...document } = await signPortableJsonLd(unsignedObject);
|
|
1358
|
+
const expandedProof = await (await DataIntegrityProof.fromJsonLd(proof, {
|
|
1359
|
+
contextLoader: mockDocumentLoader,
|
|
1360
|
+
documentLoader: mockDocumentLoader
|
|
1361
|
+
})).toJsonLd({
|
|
1362
|
+
format: "expand",
|
|
1363
|
+
contextLoader: mockDocumentLoader
|
|
1364
|
+
});
|
|
1365
|
+
assert((await verifyPortableObjectProof({
|
|
1366
|
+
...document,
|
|
1367
|
+
"https://w3id.org/security#proof": expandedProof
|
|
1368
|
+
}, options)).verified);
|
|
1369
|
+
});
|
|
1370
|
+
await t.step("classifies JSON-LD aliases by their expanded properties", async () => {
|
|
1371
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1372
|
+
"@context": [...portableContext, { performedBy: "https://www.w3.org/ns/activitystreams#actor" }],
|
|
1373
|
+
id: `ap://did:key:${portableDidMethod}/activities/aliased`,
|
|
1374
|
+
type: "UnknownActivity",
|
|
1375
|
+
performedBy: `ap://did:key:${portableDidMethod}/actor`
|
|
1376
|
+
}), options)).verified);
|
|
1377
|
+
});
|
|
1378
|
+
await t.step("does not claim to verify embedded portable objects", async () => {
|
|
1379
|
+
assert((await verifyPortableObjectProof(await signPortableJsonLd({
|
|
1380
|
+
"@context": portableContext,
|
|
1381
|
+
id: `ap://did:key:${portableDidMethod}/activities/outer`,
|
|
1382
|
+
type: "Create",
|
|
1383
|
+
actor: `ap://did:key:${portableDidMethod}/actor`,
|
|
1384
|
+
object: {
|
|
1385
|
+
id: `ap://did:key:${portableDidMethod}/objects/embedded`,
|
|
1386
|
+
type: "Note",
|
|
1387
|
+
content: "Unsigned embedded object"
|
|
1388
|
+
}
|
|
1389
|
+
}), options)).verified);
|
|
1390
|
+
});
|
|
1391
|
+
await t.step("rejects malformed roots and portable IDs", async () => {
|
|
1392
|
+
await assertRejects(() => verifyPortableObjectProof([], options), TypeError, "single JSON-LD object");
|
|
1393
|
+
await assertRejects(() => verifyPortableObjectProof({
|
|
1394
|
+
...unsignedObject,
|
|
1395
|
+
id: `ap://did:key:${portableDidMethod}`
|
|
1396
|
+
}, options), TypeError);
|
|
1397
|
+
await assertRejects(() => verifyPortableObjectProof({
|
|
1398
|
+
...unsignedObject,
|
|
1399
|
+
id: "ap://did%ZZkey/objects/1"
|
|
1400
|
+
}, options), TypeError);
|
|
1401
|
+
});
|
|
1402
|
+
await t.step("emits metrics only for attempted proof verification", async () => {
|
|
1403
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1404
|
+
const signed = await signPortableJsonLd(unsignedObject);
|
|
1405
|
+
const proof = signed.proof;
|
|
1406
|
+
assert((await verifyPortableObjectProof({
|
|
1407
|
+
...signed,
|
|
1408
|
+
proof: [proof, structuredClone(proof)]
|
|
1409
|
+
}, {
|
|
1410
|
+
...options,
|
|
1411
|
+
meterProvider
|
|
1412
|
+
})).verified);
|
|
1413
|
+
assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 2);
|
|
1414
|
+
const [rejectedMeterProvider, rejectedRecorder] = createTestMeterProvider();
|
|
1415
|
+
await verifyPortableObjectProof(unsignedObject, {
|
|
1416
|
+
...options,
|
|
1417
|
+
meterProvider: rejectedMeterProvider
|
|
1418
|
+
});
|
|
1419
|
+
assertEquals(rejectedRecorder.getMeasurements("activitypub.signature.verification.duration").length, 0);
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
552
1422
|
test("verifyObject()", async () => {
|
|
553
1423
|
const options = {
|
|
554
1424
|
documentLoader: mockDocumentLoader,
|
|
@@ -595,7 +1465,7 @@ test("verifyObject()", async () => {
|
|
|
595
1465
|
test("verifyObject() accepts did:key proofs for matching portable attribution origin", async () => {
|
|
596
1466
|
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
597
1467
|
const keyId = new URL(`${did}#${did.substring(8)}`);
|
|
598
|
-
const
|
|
1468
|
+
const jsonLd = await (await signObject(new Note({
|
|
599
1469
|
id: parseIri(`ap://did:key:${did.substring(8)}/objects/1`),
|
|
600
1470
|
attribution: parseIri(`ap://did:key:${did.substring(8)}/actor`),
|
|
601
1471
|
content: "Portable note"
|
|
@@ -606,7 +1476,8 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
|
|
|
606
1476
|
format: "compact",
|
|
607
1477
|
contextLoader: mockDocumentLoader,
|
|
608
1478
|
context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
|
|
609
|
-
})
|
|
1479
|
+
});
|
|
1480
|
+
const verified = await verifyObject(Note, jsonLd, {
|
|
610
1481
|
documentLoader() {
|
|
611
1482
|
throw new TypeError("did:key must not use the document loader");
|
|
612
1483
|
},
|
|
@@ -614,6 +1485,83 @@ test("verifyObject() accepts did:key proofs for matching portable attribution or
|
|
|
614
1485
|
});
|
|
615
1486
|
assertInstanceOf(verified, Note);
|
|
616
1487
|
assertEquals(verified.content, "Portable note");
|
|
1488
|
+
assert(typeof jsonLd === "object" && jsonLd != null && !Array.isArray(jsonLd));
|
|
1489
|
+
const rawProof = jsonLd.proof;
|
|
1490
|
+
assert(typeof rawProof === "object" && rawProof != null && !Array.isArray(rawProof));
|
|
1491
|
+
assertEquals(await verifyObject(Note, {
|
|
1492
|
+
...jsonLd,
|
|
1493
|
+
proof: [rawProof, {
|
|
1494
|
+
...structuredClone(rawProof),
|
|
1495
|
+
expires: "3000-01-01T00:00:00Z"
|
|
1496
|
+
}]
|
|
1497
|
+
}, {
|
|
1498
|
+
documentLoader() {
|
|
1499
|
+
throw new TypeError("did:key must not use the document loader");
|
|
1500
|
+
},
|
|
1501
|
+
contextLoader: mockDocumentLoader
|
|
1502
|
+
}), null);
|
|
1503
|
+
async function verifyRemotelyReferencedProof(proofOptions = {}, proofUrl = "https://proof.example/proofs/1") {
|
|
1504
|
+
const remotelyReferencedJsonLd = { ...jsonLd };
|
|
1505
|
+
delete remotelyReferencedJsonLd.proof;
|
|
1506
|
+
remotelyReferencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
|
|
1507
|
+
let proofFetches = 0;
|
|
1508
|
+
const result = await verifyObject(Note, remotelyReferencedJsonLd, {
|
|
1509
|
+
documentLoader(url) {
|
|
1510
|
+
if (url !== formatIri(parseIri(proofUrl))) throw new TypeError(`unexpected document URL: ${url}`);
|
|
1511
|
+
proofFetches++;
|
|
1512
|
+
return Promise.resolve({
|
|
1513
|
+
contextUrl: null,
|
|
1514
|
+
document: {
|
|
1515
|
+
"@context": jsonLd["@context"],
|
|
1516
|
+
...structuredClone(rawProof),
|
|
1517
|
+
...proofOptions
|
|
1518
|
+
},
|
|
1519
|
+
documentUrl: url
|
|
1520
|
+
});
|
|
1521
|
+
},
|
|
1522
|
+
contextLoader: mockDocumentLoader
|
|
1523
|
+
});
|
|
1524
|
+
assertEquals(proofFetches, 1);
|
|
1525
|
+
return result;
|
|
1526
|
+
}
|
|
1527
|
+
assertInstanceOf(await verifyRemotelyReferencedProof(), Note);
|
|
1528
|
+
assertInstanceOf(await verifyRemotelyReferencedProof({}, `ap://did:key:${did.substring(8)}/proofs/1`), Note);
|
|
1529
|
+
assertEquals(await verifyRemotelyReferencedProof({ expires: "3000-01-01T00:00:00Z" }), null);
|
|
1530
|
+
});
|
|
1531
|
+
test("verifyObject() hydrates pending proof references", async () => {
|
|
1532
|
+
const did = await exportDidKey(ed25519PublicKey.publicKey);
|
|
1533
|
+
const method = did.substring(8);
|
|
1534
|
+
const keyId = new URL(`${did}#${method}`);
|
|
1535
|
+
const proofUrl = `ap://did:key:${method}/proofs/1`;
|
|
1536
|
+
const signed = await signPortableJsonLd({
|
|
1537
|
+
"@context": portableContext,
|
|
1538
|
+
id: `ap://did:key:${method}/objects/1`,
|
|
1539
|
+
type: "Note",
|
|
1540
|
+
attributedTo: `ap://did:key:${method}/actor`,
|
|
1541
|
+
content: "Portable note with a referenced proof"
|
|
1542
|
+
}, {
|
|
1543
|
+
verificationMethod: keyId,
|
|
1544
|
+
proofOptions: { id: proofUrl }
|
|
1545
|
+
});
|
|
1546
|
+
const rawProof = signed.proof;
|
|
1547
|
+
const remotelyReferencedJsonLd = { ...signed };
|
|
1548
|
+
delete remotelyReferencedJsonLd.proof;
|
|
1549
|
+
remotelyReferencedJsonLd["https://w3id.org/security#proof"] = [{ "@graph": [rawProof] }, { "@graph": [{ "@id": proofUrl }] }];
|
|
1550
|
+
let proofFetches = 0;
|
|
1551
|
+
const verified = await verifyObject(Note, remotelyReferencedJsonLd, {
|
|
1552
|
+
documentLoader(url) {
|
|
1553
|
+
assertEquals(url, formatIri(parseIri(proofUrl)));
|
|
1554
|
+
proofFetches++;
|
|
1555
|
+
return Promise.resolve({
|
|
1556
|
+
contextUrl: null,
|
|
1557
|
+
document: structuredClone(rawProof),
|
|
1558
|
+
documentUrl: url
|
|
1559
|
+
});
|
|
1560
|
+
},
|
|
1561
|
+
contextLoader: mockDocumentLoader
|
|
1562
|
+
});
|
|
1563
|
+
assertEquals(proofFetches, 1);
|
|
1564
|
+
assertInstanceOf(verified, Note);
|
|
617
1565
|
});
|
|
618
1566
|
test("verifyObject() accepts multiple portable attributions from the same did:key origin", async () => {
|
|
619
1567
|
const did = await exportDidKey(ed25519PublicKey.publicKey);
|