UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

308 lines (307 loc) • 12.8 kB
import "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs"; import "../std__assert-CRDpx_HF.mjs"; import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs"; import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs"; import { a as importJwk, i as generateCryptoKeyPair, n as fetchKey, o as validateCryptoKey, r as fetchKeyDetailed, t as exportJwk } from "../key-BAQuZEU1.mjs"; import { c as rsaPublicKey3, i as rsaPrivateKey2, o as rsaPublicKey1, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs"; import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture"; import { CryptographicKey, Multikey } from "@fedify/vocab"; import { FetchError } from "@fedify/vocab-runtime"; //#region src/sig/key.test.ts test("validateCryptoKey()", async () => { const pkcs1v15 = await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([ 1, 0, 1 ]), hash: "SHA-256" }, true, ["sign", "verify"]); validateCryptoKey(pkcs1v15.privateKey, "private"); validateCryptoKey(pkcs1v15.privateKey); validateCryptoKey(pkcs1v15.publicKey, "public"); validateCryptoKey(pkcs1v15.publicKey); const ed25519 = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); validateCryptoKey(ed25519.privateKey, "private"); validateCryptoKey(ed25519.privateKey); validateCryptoKey(ed25519.publicKey, "public"); validateCryptoKey(ed25519.publicKey); assertThrows(() => validateCryptoKey(pkcs1v15.privateKey, "public"), TypeError, "The key is not a public key."); assertThrows(() => validateCryptoKey(pkcs1v15.publicKey, "private"), TypeError, "The key is not a private key."); assertThrows(() => validateCryptoKey(ed25519.privateKey, "public"), TypeError, "The key is not a public key."); assertThrows(() => validateCryptoKey(ed25519.publicKey, "private"), TypeError, "The key is not a private key."); const ecdsa = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]); assertThrows(() => validateCryptoKey(ecdsa.publicKey), TypeError, "only RSASSA-PKCS1-v1_5 and Ed25519"); const pkcs1v15Sha512 = await crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([ 1, 0, 1 ]), hash: "SHA-512" }, true, ["sign", "verify"]); assertThrows(() => validateCryptoKey(pkcs1v15Sha512.privateKey), TypeError, "hash algorithm for RSASSA-PKCS1-v1_5 keys must be SHA-256"); }); test("generateCryptoKeyPair()", async () => { const rsaKeyPair = await generateCryptoKeyPair(); assertEquals(rsaKeyPair.privateKey.algorithm, { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" }, modulusLength: 4096, publicExponent: new Uint8Array([ 1, 0, 1 ]) }); validateCryptoKey(rsaKeyPair.privateKey, "private"); validateCryptoKey(rsaKeyPair.publicKey, "public"); const rsaKeyPair2 = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5"); assertEquals(rsaKeyPair2.privateKey.algorithm, { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" }, modulusLength: 4096, publicExponent: new Uint8Array([ 1, 0, 1 ]) }); validateCryptoKey(rsaKeyPair2.privateKey, "private"); validateCryptoKey(rsaKeyPair2.publicKey, "public"); const ed25519KeyPair = await generateCryptoKeyPair("Ed25519"); assertEquals(ed25519KeyPair.privateKey.algorithm, { name: "Ed25519" }); validateCryptoKey(ed25519KeyPair.privateKey, "private"); validateCryptoKey(ed25519KeyPair.publicKey, "public"); }); const rsaPublicJwk = { alg: "RS256", kty: "RSA", e: "AQAB", n: "oRmBtnxbdFutoRd1GLGwwGTrsqlRRWUe11hHQaoRLGf5LwQ0tIc6I9q-dynliw-2kxYsLn9SH2je6HcTYOolgW7F_cOWXZQN04b-OiYcU1ConAhLjmn4k1uKawJ614y0ScPNd8PQ-CljsnlPxbq9ofaCMe2BV3B6y09aCuGFJ0nxn1_ubjmIBIWWFTAznoz1J9BhJDGyt3IO3ABy3f9zDVlR32L_n5VIkXnxkjUKdzMAOzYb62kuKOp1iznRTPrV71SNtivJMwSh_LVgBrmZjtIn_oim-KyX_fdLU3tQ7VClyqmJzyAjccOH6Qj6nFTPh-vX07gqN8IlLT2uye4waw", key_ops: ["verify"], ext: true }; const rsaPrivateJwk = { alg: "RS256", kty: "RSA", d: "f-Pa2L7Sb4YUSa1wlSEC-0li35uQ3DFRkY0QTG2xYnpMFGoXWTV9D1epGrqU8pePzias_mCvFiZPx2Y4aRiYm68P2Mu7hCBz9XfWPN1iYTXIFM51BOLVpk3mjdsTICkgOusJI0m9jDR3ZAjwLj14K6qhYvd0VbECmoItLjQoW64Sc9iDgD3CvGoTqv71oTfW70cy-Ve1xQ9CThAmMOTKe6rYCUTA8tMZcPszifZ4iOasOjgvRxyel86LqGNtyslY8k86gQlMtFpR3VeZV_8otAWZn0mDc4vVU8HUO-DzYiIFdAcVxfPJh6tx7snCTsdzze_98OEAK4EWYBn7vsGFeQ", dp: "lrXReSkZQXSmSxQ1TimV5kMt96gSu4_r-OGIabVmoG5irhjMyN08Jjc3qK9oZS3uNM-LxAOg4OdzefjsF9IMfZJl6wuLd85g_l4BHSaEk5zC8l3QugX1IU9XZ7wDxXUrutMoNtZXDtdbveAMtHNZlIu-qmEBDWzkqJiz2WpW-AE", dq: "TCLoYcX0ywuNA9DSU6v94KmBh1e_IELEFVbJb5vvLKlAK-ycMK0rfzC1co9Hhkski1LskTnxnoqwZ5oF-7X10eZvy3Te_FHSl0IsTar8ST2-MRtGh2UjTdvP_nnygj4GcXvKfngjPEfthDzVfVMeR38oDhDxMFD5AaY_v9aMH_U", e: "AQAB", n: "oRmBtnxbdFutoRd1GLGwwGTrsqlRRWUe11hHQaoRLGf5LwQ0tIc6I9q-dynliw-2kxYsLn9SH2je6HcTYOolgW7F_cOWXZQN04b-OiYcU1ConAhLjmn4k1uKawJ614y0ScPNd8PQ-CljsnlPxbq9ofaCMe2BV3B6y09aCuGFJ0nxn1_ubjmIBIWWFTAznoz1J9BhJDGyt3IO3ABy3f9zDVlR32L_n5VIkXnxkjUKdzMAOzYb62kuKOp1iznRTPrV71SNtivJMwSh_LVgBrmZjtIn_oim-KyX_fdLU3tQ7VClyqmJzyAjccOH6Qj6nFTPh-vX07gqN8IlLT2uye4waw", p: "xuDd7tE_47NWwvDTpB403X13EPA3768MlNpl_v_BGiuP-1uvWUnsOVZB0F3HXSVg1sBVNtec46v7OU0P693gvYUhouTmSQpayY_VFqMklprWgs7cfneqbeDzv3C4Fw5waY-vjoINDsE1jYELUnl5cVjXXyxuGFG-IaLJKmHmHX0", q: "z17X2t9zO6WcMp6W04gXdKmniJlxekOrOmWnrX9AwaM8NYCLN3y23r59nqNP9aUAWG1eoGFmav2rYQitWhz_VsEu2pQUsfsYKZYHchu5p_jCYwuM3rIg7aCbhtGv_tBoWAf1NvKMhtp2es0ZaHZCzKDGSOkIYDOB-ZDmNigWigc", qi: "KC6gWhVM_x7iQgl-gEoSh_iM1Jf314ZLJKAAz1DsTHMi5yuCkCMmmY7h6jlkAJVngK3KIf5LPoAeUoGJ26E1kocbRU_nZBftMDVXHCYICz8qMQXR5euN_5SeJnu_VWXH-CY83MKhPYAorWSZ1-G9gh-C16LlRMzJwoE6h5QNeNo", key_ops: ["sign"], ext: true }; test("exportJwk()", async () => { assertEquals(await exportJwk(rsaPrivateKey2), rsaPrivateJwk); assertEquals(await exportJwk(rsaPublicKey2.publicKey), rsaPublicJwk); }); test("importJwk()", async () => { assertEquals(await importJwk(rsaPrivateJwk, "private"), rsaPrivateKey2); assertEquals(await importJwk(rsaPublicJwk, "public"), rsaPublicKey2.publicKey); assertRejects(() => importJwk(rsaPublicJwk, "private")); assertRejects(() => importJwk(rsaPrivateJwk, "public")); }); test("fetchKey()", 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(); } } }; assertEquals(await fetchKey("https://example.com/nothing", CryptographicKey, options), { key: null, cached: false }); assertEquals(cache, { "https://example.com/nothing": null }); assertEquals(await fetchKey("https://example.com/nothing", CryptographicKey, options), { key: null, cached: true }); assertEquals(cache, { "https://example.com/nothing": null }); assertEquals(await fetchKey("https://example.com/object", CryptographicKey, options), { key: null, cached: false }); assertEquals(cache, { "https://example.com/nothing": null, "https://example.com/object": null }); assertEquals(await fetchKey("https://example.com/key", CryptographicKey, options), { key: rsaPublicKey1, cached: false }); assertEquals(cache, { "https://example.com/nothing": null, "https://example.com/object": null, "https://example.com/key": rsaPublicKey1 }); assertEquals(await fetchKey("https://example.com/key", CryptographicKey, options), { key: rsaPublicKey1, cached: true }); assertEquals(cache, { "https://example.com/nothing": null, "https://example.com/object": null, "https://example.com/key": rsaPublicKey1 }); assertEquals(await fetchKey("https://example.com/person#no-key", CryptographicKey, options), { key: null, cached: false }); assertEquals(cache, { "https://example.com/nothing": null, "https://example.com/object": null, "https://example.com/key": rsaPublicKey1, "https://example.com/person#no-key": null }); assertEquals(await fetchKey("https://example.com/person2#key3", CryptographicKey, options), { key: rsaPublicKey3, cached: false }); assertEquals(cache, { "https://example.com/nothing": null, "https://example.com/object": null, "https://example.com/key": rsaPublicKey1, "https://example.com/person#no-key": null, "https://example.com/person2#key3": rsaPublicKey3 }); assertEquals(await fetchKey("https://example.com/person2#key3", CryptographicKey, options), { key: rsaPublicKey3, cached: true }); assertEquals(cache, { "https://example.com/nothing": null, "https://example.com/object": null, "https://example.com/key": rsaPublicKey1, "https://example.com/person#no-key": null, "https://example.com/person2#key3": rsaPublicKey3 }); assertEquals(await fetchKey("https://example.com/person2#key4", Multikey, options), { key: ed25519Multikey, cached: false }); assertEquals(cache, { "https://example.com/nothing": null, "https://example.com/object": null, "https://example.com/key": rsaPublicKey1, "https://example.com/person#no-key": null, "https://example.com/person2#key3": rsaPublicKey3, "https://example.com/person2#key4": ed25519Multikey }); assertEquals(await fetchKey("https://example.com/person2#key4", Multikey, options), { key: ed25519Multikey, cached: true }); assertEquals(await fetchKey("https://example.com/key", CryptographicKey, { ...options, keyCache: void 0 }), { key: rsaPublicKey1, cached: false }); assertEquals(await fetchKey("https://example.com/users/handle", CryptographicKey, options), { key: new CryptographicKey({ id: new URL("https://example.com/users/handle#main-key"), publicKey: await importJwk({ kty: "RSA", alg: "RS256", n: "oRmBtnxbdFutoRd1GLGwwGTrsqlRRWUe11hHQaoRLGf5LwQ0tIc6I9q-dynliw-2kxYsLn9SH2je6HcTYOolgW7F_cOWXZQN04b-OiYcU1ConAhLjmn4k1uKawJ614y0ScPNd8PQ-CljsnlPxbq9ofaCMe2BV3B6y09aCuGFJ0nxn1_ubjmIBIWWFTAznoz1J9BhJDGyt3IO3ABy3f9zDVlR32L_n5VIkXnxkjUKdzMAOzYb62kuKOp1iznRTPrV71SNtivJMwSh_LVgBrmZjtIn_oim-KyX_fdLU3tQ7VClyqmJzyAjccOH6Qj6nFTPh-vX07gqN8IlLT2uye4waw", e: "AQAB", key_ops: ["verify"], ext: true }, "public") }), cached: false }); }); test("fetchKeyDetailed()", async () => { const cache = { "https://example.com/nothing": null }; let documentLoaderCalls = 0; const [tracerProvider, exporter] = createTestTracerProvider(); const options = { documentLoader(url) { documentLoaderCalls++; return mockDocumentLoader(url); }, contextLoader: mockDocumentLoader, tracerProvider, keyCache: { get(keyId) { return Promise.resolve(cache[keyId.href]); }, set(keyId, key) { cache[keyId.href] = key; return Promise.resolve(); } } }; assertEquals(await fetchKeyDetailed("https://example.com/nothing", CryptographicKey, options), { key: null, cached: true }); assertEquals(documentLoaderCalls, 0); assertEquals(await fetchKeyDetailed("https://example.com/key", CryptographicKey, options), { key: rsaPublicKey1, cached: false }); assertEquals(documentLoaderCalls, 1); const spans = exporter.getSpans("activitypub.fetch_key"); assertEquals(spans.length, 2); assertEquals(spans[0].attributes["activitypub.actor.key.cached"], true); assertEquals(spans[1].attributes["activitypub.actor.key.cached"], false); }); test("fetchKeyDetailed() returns detailed fetch errors", async () => { const goneKeyId = new URL("https://example.com/gone-key"); const goneResult = await fetchKeyDetailed(goneKeyId, CryptographicKey, { documentLoader(url) { if (url === goneKeyId.href) throw new FetchError(goneKeyId, `HTTP 410: ${goneKeyId.href}`, new Response(null, { status: 410 })); return mockDocumentLoader(url); }, contextLoader: mockDocumentLoader }); assertEquals(goneResult.key, null); assertEquals(goneResult.cached, false); const goneError = goneResult.fetchError; assertEquals(goneError != null && "status" in goneError, true); if (goneError == null || !("status" in goneError)) throw new Error("Expected HTTP fetch error details."); assertEquals(goneError.status, 410); assertEquals(goneError.response.status, 410); const failure = /* @__PURE__ */ new TypeError("boom"); const errorResult = await fetchKeyDetailed("https://example.com/error-key", CryptographicKey, { documentLoader() { throw failure; }, contextLoader: mockDocumentLoader }); assertEquals(errorResult.key, null); assertEquals(errorResult.cached, false); const detailedError = errorResult.fetchError; assertEquals(detailedError != null && "error" in detailedError, true); if (detailedError == null || !("error" in detailedError)) throw new Error("Expected non-HTTP fetch error details."); assertEquals(detailedError.error, failure); }); //#endregion export {};