UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

535 lines (534 loc) • 21.8 kB
import { Temporal } from "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs"; import "../std__assert-BBjXFNOb.mjs"; import { n as assertGreaterOrEqual, r as assertFalse, t as assertRejects } from "../assert_rejects-DN60FHPX.mjs"; import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs"; import { t as assert } from "../assert-OguE97r2.mjs"; import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs"; import { r as normalizeOutgoingActivityJsonLd } from "../outgoing-jsonld-L_DbOaFe.mjs"; import { a as verifyProof, i as verifyObject, n as hasProofLike, r as signObject, t as createProof } from "../proof-pXYODmNV.mjs"; import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place } from "@fedify/vocab"; import { createTestMeterProvider, mockDocumentLoader, test } from "@fedify/fixture"; import { decodeMultibase, importMultibaseKey } from "@fedify/vocab-runtime"; import { decodeHex } from "byte-encodings/hex"; //#region src/sig/proof.test.ts const fep8b32TestVectorPrivateKey = await crypto.subtle.importKey("jwk", { "kty": "OKP", "crv": "Ed25519", "d": "yW756hDF5BTEcXI6_53nLDX6W3D66X6IMuysfS4rjtY", "x": "sA2Nk45_dz1RVlqtNqYj9TRPf10ZYPnPPo4SYg6igQ8", key_ops: ["sign"], ext: true }, "Ed25519", true, ["sign"]); const fep8b32TestVectorKeyId = new URL("https://server.example/users/alice#ed25519-key"); const fep8b32TestVectorActivity = new Create({ id: new URL("https://server.example/activities/1"), actor: new URL("https://server.example/users/alice"), object: new Note({ id: new URL("https://server.example/objects/1"), attribution: new URL("https://server.example/users/alice"), content: "Hello world", location: new Place({ longitude: -71.184902, latitude: 25.273962 }) }) }); test("createProof()", async () => { const create = new Create({ actor: new URL("https://example.com/person"), object: new Note({ content: "Hello, world!" }) }); const created = Temporal.Instant.from("2023-02-24T23:36:38Z"); const proof = await createProof(create, ed25519PrivateKey, ed25519PublicKey.id, { created, contextLoader: mockDocumentLoader }); assertEquals(proof.cryptosuite, "eddsa-jcs-2022"); assertEquals(proof.verificationMethodId, ed25519PublicKey.id); assertEquals(proof.proofPurpose, "assertionMethod"); assertEquals(proof.proofValue, decodeHex("0e63238fdb50a979a7fbd906b471d328a03504de7aa3a0409fad1500b85d6fecafa2223bfde21ba2eac9446d36f6583c45cb55a98017a0a6a275f50262a4ea06")); assertEquals(proof.created, created); assertEquals(await verifyProof(await create.toJsonLd({ format: "compact", contextLoader: mockDocumentLoader }), proof, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }), ed25519Multikey); const proof2 = await createProof(fep8b32TestVectorActivity, fep8b32TestVectorPrivateKey, fep8b32TestVectorKeyId, { created, contextLoader: mockDocumentLoader, context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"] }); assertEquals(proof2.cryptosuite, "eddsa-jcs-2022"); assertEquals(proof2.verificationMethodId, fep8b32TestVectorKeyId); assertEquals(proof2.proofPurpose, "assertionMethod"); assertEquals(proof2.proofValue, decodeMultibase("zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z")); assertEquals(proof2.created, created); await assertRejects(() => createProof(create, rsaPrivateKey2, rsaPublicKey2.id, { created, contextLoader: mockDocumentLoader }), TypeError, "Unsupported algorithm"); }); test("signObject()", async () => { const options = { format: "compact", contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"] }; const created = Temporal.Instant.from("2023-02-24T23:36:38Z"); const signedObject = await signObject(fep8b32TestVectorActivity, fep8b32TestVectorPrivateKey, fep8b32TestVectorKeyId, { ...options, created }); assertEquals(await signedObject.toJsonLd(options), { "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], id: "https://server.example/activities/1", type: "Create", actor: "https://server.example/users/alice", object: { id: "https://server.example/objects/1", type: "Note", attributedTo: "https://server.example/users/alice", content: "Hello world", location: { type: "Place", longitude: -71.184902, latitude: 25.273962 } }, proof: { "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022", verificationMethod: "https://server.example/users/alice#ed25519-key", proofPurpose: "assertionMethod", proofValue: "zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z", created: "2023-02-24T23:36:38Z" } }); assertEquals(await (await signObject(signedObject, ed25519PrivateKey, ed25519Multikey.id, { ...options, created })).toJsonLd(options), { "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], id: "https://server.example/activities/1", type: "Create", actor: "https://server.example/users/alice", object: { id: "https://server.example/objects/1", type: "Note", attributedTo: "https://server.example/users/alice", content: "Hello world", location: { type: "Place", longitude: -71.184902, latitude: 25.273962 } }, proof: [{ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022", verificationMethod: "https://server.example/users/alice#ed25519-key", proofPurpose: "assertionMethod", proofValue: "zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z", created: "2023-02-24T23:36:38Z" }, { "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], created: "2023-02-24T23:36:38Z", cryptosuite: "eddsa-jcs-2022", proofPurpose: "assertionMethod", proofValue: "zVrcY69MxozB9V9hmMmsjoB4YLCXvn6ienKr6jsP2rztSEr1WhMJymPqujKofkrV3C7A2C9iKYnRNSvtPgDQBCw2", type: "DataIntegrityProof", verificationMethod: "https://example.com/person2#key4" }] }); await assertRejects(() => signObject(fep8b32TestVectorActivity, rsaPrivateKey2, rsaPublicKey2.id, { created, contextLoader: mockDocumentLoader }), TypeError, "Unsupported algorithm"); const signed = await signObject(new Create({ id: new URL("https://server.example/activities/2"), actor: new URL("https://server.example/users/alice"), object: new Note({ id: new URL("https://server.example/objects/2"), attribution: new URL("https://server.example/users/alice"), content: "Hello public", attachments: [new Document({ mediaType: "image/png", url: new URL("https://server.example/objects/2/image.png") })] }), tos: [PUBLIC_COLLECTION] }), fep8b32TestVectorPrivateKey, fep8b32TestVectorKeyId, { ...options, created }); const [proof] = await Array.fromAsync(signed.getProofs(options)); assertInstanceOf(proof, DataIntegrityProof); const signedJson = await normalizeOutgoingActivityJsonLd(await signed.toJsonLd(options), mockDocumentLoader); assertEquals(signedJson.to, PUBLIC_COLLECTION.href); const signedJsonObject = signedJson.object; assertEquals(Array.isArray(signedJsonObject.attachment), true); const verifyCache = {}; const verifyOptions = { contextLoader: mockDocumentLoader, documentLoader: mockDocumentLoader, keyCache: { get: (keyId) => Promise.resolve(verifyCache[keyId.href]), set: (keyId, key) => { verifyCache[keyId.href] = key; return Promise.resolve(); } } }; assertInstanceOf(await verifyProof(signedJson, proof, verifyOptions), Multikey); const signedJsonWithCurie = await signed.toJsonLd(options); assertEquals(signedJsonWithCurie.to, "as:Public"); const signedJsonWithCurieObject = signedJsonWithCurie.object; assertEquals(Array.isArray(signedJsonWithCurieObject.attachment), false); assertInstanceOf(await verifyProof(signedJsonWithCurie, proof, verifyOptions), Multikey); }); test("hasProofLike()", () => { assert(hasProofLike({ proof: { type: "DataIntegrityProof", verificationMethod: "https://example.com/users/alice#main-key", proofPurpose: "assertionMethod", proofValue: "signature" } })); assert(hasProofLike({ proof: { type: "DataIntegrityProof", verificationMethod: { id: "https://example.com/users/alice#main-key" }, proofPurpose: "assertionMethod", proofValue: "signature" } })); assert(hasProofLike({ proof: [{ type: "DataIntegrityProof", verificationMethod: { id: "https://example.com/users/alice#main-key" }, proofPurpose: "assertionMethod", proofValue: "signature" }] })); assert(hasProofLike({ proof: { type: ["https://w3id.org/security#DataIntegrityProof"], verificationMethod: [{ "@id": "https://example.com/users/alice#main-key" }], proofPurpose: { "@id": "https://w3id.org/security#assertionMethod" }, proofValue: "signature" } })); assert(hasProofLike({ "https://w3id.org/security#proof": { type: "DataIntegrityProof", verificationMethod: { "@id": "https://example.com/users/alice#main-key" }, proofPurpose: { "@id": "https://w3id.org/security#assertionMethod" }, proofValue: "signature" } })); assert(hasProofLike({ "https://w3id.org/security#proof": [{ "@type": ["https://w3id.org/security#DataIntegrityProof"], "https://w3id.org/security#verificationMethod": [{ "@id": "https://example.com/users/alice#main-key" }], "https://w3id.org/security#proofPurpose": [{ "@id": "https://w3id.org/security#assertionMethod" }], "https://w3id.org/security#proofValue": [{ "@value": "signature" }] }] })); assertFalse(hasProofLike({ proof: { type: "DataIntegrityProof", verificationMethod: { id: "https://example.com/users/alice#main-key" }, proofPurpose: "assertionMethod" } })); }); test("verifyProof()", async () => { const cache = {}; const options = { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, keyCache: { get(keyId) { return Promise.resolve(cache[keyId.href]); }, set(keyId, key) { cache[keyId.href] = key; return Promise.resolve(); } } }; const jsonLd = { "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], id: "https://server.example/activities/1", type: "Create", actor: "https://server.example/users/alice", object: { id: "https://server.example/objects/1", type: "Note", attributedTo: "https://server.example/users/alice", content: "Hello world", location: { type: "Place", longitude: -71.184902, latitude: 25.273962 } } }; const proof = new DataIntegrityProof({ cryptosuite: "eddsa-jcs-2022", verificationMethod: new URL("https://server.example/users/alice#ed25519-key"), proofPurpose: "assertionMethod", proofValue: decodeMultibase("zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z"), created: Temporal.Instant.from("2023-02-24T23:36:38Z") }); const expectedKey = new Multikey({ id: new URL("https://server.example/users/alice#ed25519-key"), controller: new URL("https://server.example/users/alice"), publicKey: await importMultibaseKey("z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2") }); assertEquals(await verifyProof(jsonLd, proof, options), expectedKey); assertEquals(cache["https://server.example/users/alice#ed25519-key"], expectedKey); cache["https://server.example/users/alice#ed25519-key"] = ed25519Multikey; assertEquals(await verifyProof(jsonLd, proof, options), expectedKey); assertEquals(cache["https://server.example/users/alice#ed25519-key"], expectedKey); assertEquals(await verifyProof({ ...jsonLd, object: { ...jsonLd.object, content: "bye" } }, proof, options), null); assertEquals(await verifyProof(jsonLd, proof.clone({ created: Temporal.Now.instant() }), options), null); assertEquals(await verifyProof({ ...jsonLd, "https://w3id.org/security#proof": { "@type": ["https://w3id.org/security#DataIntegrityProof"], "https://w3id.org/security#proofValue": [{ "@value": "stale" }] } }, proof, options), expectedKey); assertEquals(await verifyProof([jsonLd], proof, options), null); const attackerInput = { "@context": ["https://www.w3.org/ns/activitystreams", "https://attacker.example/ctx"], id: "https://server.example/activities/attacker", type: "Create", actor: "https://server.example/users/alice", object: { id: "https://server.example/objects/attacker", type: "Note", attributedTo: "https://server.example/users/alice", content: "n/a", to: "as:Public" } }; const contextLoaderCalls = []; assertEquals(await verifyProof(attackerInput, proof, { contextLoader: async (url) => { contextLoaderCalls.push(url); return await mockDocumentLoader(url); }, documentLoader: mockDocumentLoader, keyCache: options.keyCache }), null); assertFalse(contextLoaderCalls.includes("https://attacker.example/ctx")); }); test("verifyProof() records verification duration metric", async (t) => { const jsonLd = { "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], id: "https://server.example/activities/1", type: "Create", actor: "https://server.example/users/alice", object: { id: "https://server.example/objects/1", type: "Note", attributedTo: "https://server.example/users/alice", content: "Hello world", location: { type: "Place", longitude: -71.184902, latitude: 25.273962 } } }; const proof = new DataIntegrityProof({ cryptosuite: "eddsa-jcs-2022", verificationMethod: new URL("https://server.example/users/alice#ed25519-key"), proofPurpose: "assertionMethod", proofValue: decodeMultibase("zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z"), created: Temporal.Instant.from("2023-02-24T23:36:38Z") }); await t.step("verified path records result=verified with bounded cryptosuite", async () => { const [meterProvider, recorder] = createTestMeterProvider(); assert(await verifyProof(jsonLd, proof, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider }) != null); const measurements = recorder.getMeasurements("activitypub.signature.verification.duration"); assertEquals(measurements.length, 1); const m = measurements[0]; assertEquals(m.type, "histogram"); assertGreaterOrEqual(m.value, 0); assertEquals(m.attributes["activitypub.signature.kind"], "object_integrity"); assertEquals(m.attributes["activitypub.signature.result"], "verified"); assertEquals(m.attributes["object_integrity_proofs.cryptosuite"], "eddsa-jcs-2022"); }); await t.step("rejected path records result=rejected", async () => { const [meterProvider, recorder] = createTestMeterProvider(); assertEquals(await verifyProof({ ...jsonLd, object: { ...jsonLd.object, content: "bye" } }, proof, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider }), null); const measurements = recorder.getMeasurements("activitypub.signature.verification.duration"); assertEquals(measurements.length, 1); assertEquals(measurements[0].attributes["activitypub.signature.result"], "rejected"); assertEquals(measurements[0].attributes["object_integrity_proofs.cryptosuite"], "eddsa-jcs-2022"); }); await t.step("cached-key retry emits one measurement, not two", async () => { const [meterProvider, recorder] = createTestMeterProvider(); const cache = { ["https://server.example/users/alice#ed25519-key"]: ed25519Multikey }; assert(await verifyProof(jsonLd, proof, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider, keyCache: { get(id) { return Promise.resolve(cache[id.href]); }, set(id, k) { cache[id.href] = k; return Promise.resolve(); } } }) != null); assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1); const keyFetches = recorder.getMeasurements("activitypub.signature.key_fetch.duration"); assertEquals(keyFetches.length, 2); assertEquals(keyFetches[0].attributes["activitypub.signature.key_fetch.result"], "hit"); assertEquals(keyFetches[1].attributes["activitypub.signature.key_fetch.result"], "fetched"); }); await t.step("key fetch records result=fetched on a cold cache", async () => { const [meterProvider, recorder] = createTestMeterProvider(); assert(await verifyProof(jsonLd, proof, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider }) != null); const measurements = recorder.getMeasurements("activitypub.signature.key_fetch.duration"); assertEquals(measurements.length, 1); assertGreaterOrEqual(measurements[0].value, 0); assertEquals(measurements[0].attributes["activitypub.signature.kind"], "object_integrity"); assertEquals(measurements[0].attributes["activitypub.signature.key_fetch.result"], "fetched"); }); await t.step("key fetch records result=hit when served from the key cache", async () => { const [meterProvider, recorder] = createTestMeterProvider(); const cache = { "https://server.example/users/alice#ed25519-key": new Multikey({ id: new URL("https://server.example/users/alice#ed25519-key"), controller: new URL("https://server.example/users/alice"), publicKey: await importMultibaseKey("z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2") }) }; assert(await verifyProof(jsonLd, proof, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider, keyCache: { get(id) { return Promise.resolve(cache[id.href]); }, set(id, k) { cache[id.href] = k; return Promise.resolve(); } } }) != null); const measurements = recorder.getMeasurements("activitypub.signature.key_fetch.duration"); assertEquals(measurements.length, 1); assertEquals(measurements[0].attributes["activitypub.signature.key_fetch.result"], "hit"); }); await t.step("verifyObject() wrapper emits one measurement per inner verifyProof()", async () => { const [meterProvider, recorder] = createTestMeterProvider(); assert(await verifyObject(Create, { ...jsonLd, proof: await proof.toJsonLd({ format: "compact", contextLoader: mockDocumentLoader }) }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider }) != null); assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1); }); await t.step("unknown cryptosuite omits the cryptosuite metric attribute", async () => { const [meterProvider, recorder] = createTestMeterProvider(); const exoticProof = await DataIntegrityProof.fromJsonLd({ "@context": "https://w3id.org/security/data-integrity/v1", type: "DataIntegrityProof", cryptosuite: "made-up-suite-9999", verificationMethod: "https://server.example/users/alice#ed25519-key", proofPurpose: "assertionMethod", proofValue: "zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z", created: "2023-02-24T23:36:38Z" }, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); assertEquals(exoticProof.cryptosuite, "made-up-suite-9999"); assertEquals(await verifyProof(jsonLd, exoticProof, { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader, meterProvider }), null); const measurements = recorder.getMeasurements("activitypub.signature.verification.duration"); assertEquals(measurements.length, 1); assertEquals(measurements[0].attributes["activitypub.signature.result"], "rejected"); assertFalse("object_integrity_proofs.cryptosuite" in measurements[0].attributes); }); }); test("verifyObject()", async () => { const options = { documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }; const create = await verifyObject(Create, { "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], id: "https://server.example/activities/1", type: "Create", actor: "https://server.example/users/alice", object: { id: "https://server.example/objects/1", type: "Note", attributedTo: "https://server.example/users/alice", content: "Hello world", location: { type: "Place", longitude: -71.184902, latitude: 25.273962 } }, proof: [{ type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022", verificationMethod: "https://server.example/users/alice#ed25519-key", proofPurpose: "assertionMethod", proofValue: "zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z", created: "2023-02-24T23:36:38Z" }, { created: "2023-02-24T23:36:38Z", cryptosuite: "eddsa-jcs-2022", proofPurpose: "assertionMethod", proofValue: "zVrcY69MxozB9V9hmMmsjoB4YLCXvn6ienKr6jsP2rztSEr1WhMJymPqujKofkrV3C7A2C9iKYnRNSvtPgDQBCw2", type: "DataIntegrityProof", verificationMethod: "https://example.com/person2#key4" }] }, options); assertInstanceOf(create, Create); assertEquals(create.actorId, new URL("https://server.example/users/alice")); const note = await create.getObject(options); assertInstanceOf(note, Note); assertEquals(note.content, "Hello world"); }); //#endregion export {};