UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

374 lines (373 loc) • 15 kB
import { Temporal } from "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs"; import "../std__assert-CRDpx_HF.mjs"; import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs"; import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs"; import { t as assert } from "../assert-DikXweDx.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-CNmZLixq.mjs"; import { a as verifyProof, i as verifyObject, n as hasProofLike, r as signObject, t as createProof } from "../proof-DLhLRv3m.mjs"; import { mockDocumentLoader, test } from "@fedify/fixture"; import { Create, DataIntegrityProof, Document, Multikey, Note, PUBLIC_COLLECTION, Place } from "@fedify/vocab"; 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("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 {};