UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

262 lines (258 loc) • 8.62 kB
import { Temporal } from "@js-temporal/polyfill"; import { URLPattern } from "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { deno_default, getDocumentLoader } from "./docloader-C8mmLN-Y.js"; import { CryptographicKey, Object as Object$1 } from "./vocab-dDpPQ0fF.js"; import { isActor } from "./actor-BP-NTJ2m.js"; import { getLogger } from "@logtape/logtape"; import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; //#region sig/key.ts /** * Checks if the given key is valid and supported. No-op if the key is valid, * otherwise throws an error. * @param key The key to check. * @param type Which type of key to check. If not specified, the key can be * either public or private. * @throws {TypeError} If the key is invalid or unsupported. */ function validateCryptoKey(key, type) { if (type != null && key.type !== type) throw new TypeError(`The key is not a ${type} key.`); if (!key.extractable) throw new TypeError("The key is not extractable."); if (key.algorithm.name !== "RSASSA-PKCS1-v1_5" && key.algorithm.name !== "Ed25519") throw new TypeError("Currently only RSASSA-PKCS1-v1_5 and Ed25519 keys are supported. More algorithms will be added in the future!"); if (key.algorithm.name === "RSASSA-PKCS1-v1_5") { const algorithm = key.algorithm; if (algorithm.hash.name !== "SHA-256") throw new TypeError("For compatibility with the existing Fediverse software (e.g., Mastodon), hash algorithm for RSASSA-PKCS1-v1_5 keys must be SHA-256."); } } /** * Generates a key pair which is appropriate for Fedify. * @param algorithm The algorithm to use. Currently only RSASSA-PKCS1-v1_5 and * Ed25519 are supported. * @returns The generated key pair. * @throws {TypeError} If the algorithm is unsupported. */ function generateCryptoKeyPair(algorithm) { if (algorithm == null) getLogger([ "fedify", "sig", "key" ]).warn("No algorithm specified. Using RSASSA-PKCS1-v1_5 by default, but it is recommended to specify the algorithm explicitly as the parameter will be required in the future."); if (algorithm == null || algorithm === "RSASSA-PKCS1-v1_5") return crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 4096, publicExponent: new Uint8Array([ 1, 0, 1 ]), hash: "SHA-256" }, true, ["sign", "verify"]); else if (algorithm === "Ed25519") return crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]); throw new TypeError("Unsupported algorithm: " + algorithm); } /** * Exports a key in JWK format. * @param key The key to export. Either public or private key. * @returns The exported key in JWK format. The key is suitable for * serialization and storage. * @throws {TypeError} If the key is invalid or unsupported. */ async function exportJwk(key) { validateCryptoKey(key); const jwk = await crypto.subtle.exportKey("jwk", key); if (jwk.crv === "Ed25519") jwk.alg = "Ed25519"; return jwk; } /** * Imports a key from JWK format. * @param jwk The key in JWK format. * @param type Which type of key to import, either `"public"` or `"private"`. * @returns The imported key. * @throws {TypeError} If the key is invalid or unsupported. */ async function importJwk(jwk, type) { let key; if (jwk.kty === "RSA" && jwk.alg === "RS256") key = await crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, type === "public" ? ["verify"] : ["sign"]); else if (jwk.kty === "OKP" && jwk.crv === "Ed25519") { if (navigator?.userAgent === "Cloudflare-Workers") { jwk = { ...jwk }; delete jwk.alg; } key = await crypto.subtle.importKey("jwk", jwk, "Ed25519", true, type === "public" ? ["verify"] : ["sign"]); } else throw new TypeError("Unsupported JWK format."); validateCryptoKey(key, type); return key; } /** * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL. * If the given URL contains an {@link Actor} object, it tries to find * the corresponding key in the `publicKey` or `assertionMethod` property. * @typeParam T The type of the key to fetch. Either {@link CryptographicKey} * or {@link Multikey}. * @param keyId The URL of the key. * @param cls The class of the key to fetch. Either {@link CryptographicKey} * or {@link Multikey}. * @param options Options for fetching the key. See {@link FetchKeyOptions}. * @returns The fetched key or `null` if the key is not found. * @since 1.3.0 */ function fetchKey(keyId, cls, options = {}) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version); keyId = typeof keyId === "string" ? new URL(keyId) : keyId; return tracer.startActiveSpan("activitypub.fetch_key", { kind: SpanKind.CLIENT, attributes: { "http.method": "GET", "url.full": keyId.href, "url.scheme": keyId.protocol.replace(/:$/, ""), "url.domain": keyId.hostname, "url.path": keyId.pathname, "url.query": keyId.search.replace(/^\?/, ""), "url.fragment": keyId.hash.replace(/^#/, "") } }, async (span) => { try { const result = await fetchKeyInternal(keyId, cls, options); span.setAttribute("activitypub.actor.key.cached", result.cached); return result; } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }); } async function fetchKeyInternal(keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider } = {}) { const logger = getLogger([ "fedify", "sig", "key" ]); const cacheKey = typeof keyId === "string" ? new URL(keyId) : keyId; keyId = typeof keyId === "string" ? keyId : keyId.href; if (keyCache != null) { const cachedKey = await keyCache.get(cacheKey); if (cachedKey instanceof cls && cachedKey.publicKey != null) { logger.debug("Key {keyId} found in cache.", { keyId }); return { key: cachedKey, cached: true }; } else if (cachedKey === null) { logger.debug("Entry {keyId} found in cache, but it is unavailable.", { keyId }); return { key: null, cached: true }; } } logger.debug("Fetching key {keyId} to verify signature...", { keyId }); let document; try { const remoteDocument = await (documentLoader ?? getDocumentLoader())(keyId); document = remoteDocument.document; } catch (_) { logger.debug("Failed to fetch key {keyId}.", { keyId }); await keyCache?.set(cacheKey, null); return { key: null, cached: false }; } let object; try { object = await Object$1.fromJsonLd(document, { documentLoader, contextLoader, tracerProvider }); } catch (e) { if (!(e instanceof TypeError)) throw e; try { object = await cls.fromJsonLd(document, { documentLoader, contextLoader, tracerProvider }); } catch (e$1) { if (e$1 instanceof TypeError) { logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId }); await keyCache?.set(cacheKey, null); return { key: null, cached: false }; } throw e$1; } } let key = null; if (object instanceof cls) key = object; else if (isActor(object)) { const keys = cls === CryptographicKey ? object.getPublicKeys({ documentLoader, contextLoader, tracerProvider }) : object.getAssertionMethods({ documentLoader, contextLoader, tracerProvider }); let length = 0; let lastKey = null; for await (const k of keys) { length++; lastKey = k; if (k.id?.href === keyId) { key = k; break; } } const keyIdUrl = new URL(keyId); if (key == null && keyIdUrl.hash === "" && length === 1) key = lastKey; if (key == null) { logger.debug("Failed to verify; object {keyId} returned an {actorType}, but has no key matching {keyId}.", { keyId, actorType: object.constructor.name }); await keyCache?.set(cacheKey, null); return { key: null, cached: false }; } } else { logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId }); await keyCache?.set(cacheKey, null); return { key: null, cached: false }; } if (key.publicKey == null) { logger.debug("Failed to verify; key {keyId} has no publicKeyPem field.", { keyId }); await keyCache?.set(cacheKey, null); return { key: null, cached: false }; } if (keyCache != null) { await keyCache.set(cacheKey, key); logger.debug("Key {keyId} cached.", { keyId }); } return { key, cached: false }; } //#endregion export { exportJwk, fetchKey, generateCryptoKeyPair, importJwk, validateCryptoKey };