@fedify/fedify
Version:
An ActivityPub server framework
374 lines (373 loc) • 15 kB
JavaScript
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 {};