UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

353 lines (352 loc) • 12.2 kB
import "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { n as version, t as name } from "./deno-DMg4SgCb.mjs"; import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab"; import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime"; import { getLogger } from "@logtape/logtape"; //#region src/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") { if (key.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; } async function withFetchKeySpan(keyId, tracerProvider, fetcher) { tracerProvider ??= trace.getTracerProvider(); return await tracerProvider.getTracer(name, version).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 fetcher(); 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(); } }); } /** * 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. * @template 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 = {}) { keyId = typeof keyId === "string" ? new URL(keyId) : keyId; return withFetchKeySpan(keyId, options.tracerProvider, () => fetchKeyInternal(keyId, cls, options)); } /** * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL, * preserving transport-level fetch failures for callers that need to inspect * why the key could not be loaded. * * @template 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. * @returns The fetched key, or detailed fetch failure information. * @since 2.1.0 */ async function fetchKeyDetailed(keyId, cls, options = {}) { const cacheKey = typeof keyId === "string" ? new URL(keyId) : keyId; return await withFetchKeySpan(cacheKey, options.tracerProvider, async () => { return await fetchKeyWithResult(cacheKey, cls, options, async (cacheKey, keyId, keyCache, logger) => { const fetchError = await keyCache?.getFetchError?.(cacheKey); if (fetchError != null) { logger.debug("Entry {keyId} found in cache with preserved fetch failure details.", { keyId }); return { key: null, cached: true, fetchError }; } logger.debug("Entry {keyId} found in cache, but no fetch failure details are available.", { keyId }); return { key: null, cached: true }; }, async (error, cacheKey, keyId, keyCache, logger) => { logger.debug("Failed to fetch key {keyId}.", { keyId, error }); await keyCache?.set(cacheKey, null); if (error instanceof FetchError && error.response != null) { const fetchError = { status: error.response.status, response: error.response.clone() }; await keyCache?.setFetchError?.(cacheKey, fetchError); return { key: null, cached: false, fetchError }; } const fetchError = { error: error instanceof Error ? error : new Error(String(error)) }; await keyCache?.setFetchError?.(cacheKey, fetchError); return { key: null, cached: false, fetchError }; }); }); } async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) { if (keyCache == null) return 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 }; } return null; } async function clearFetchErrorMetadata(keyId, keyCache) { await keyCache?.setFetchError?.(keyId, null); } async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) { 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) { if (e instanceof TypeError) { logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId }); await keyCache?.set(cacheKey, null); await clearFetchErrorMetadata(cacheKey, keyCache); return { key: null, cached: false }; } throw e; } } 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); await clearFetchErrorMetadata(cacheKey, keyCache); return { key: null, cached: false }; } } else { logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId }); await keyCache?.set(cacheKey, null); await clearFetchErrorMetadata(cacheKey, keyCache); 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); await clearFetchErrorMetadata(cacheKey, keyCache); return { key: null, cached: false }; } if (keyCache != null) { await keyCache.set(cacheKey, key); logger.debug("Key {keyId} cached.", { keyId }); } await clearFetchErrorMetadata(cacheKey, keyCache); return { key, cached: false }; } async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) { const logger = getLogger([ "fedify", "sig", "key" ]); const keyId = cacheKey.href; const keyCache = options.keyCache; const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger); if (cached?.key === null && cached.cached) return await onCachedUnavailable(cacheKey, keyId, keyCache, logger); if (cached != null) return cached; logger.debug("Fetching key {keyId} to verify signature...", { keyId }); let document; try { document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document; } catch (error) { return await onFetchError(error, cacheKey, keyId, keyCache, logger); } return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger); } async function fetchKeyInternal(keyId, cls, options = {}) { return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => { return { key: null, cached: true }; }, async (error, cacheKey, keyId, keyCache, logger) => { logger.debug("Failed to fetch key {keyId}.", { keyId, error }); await keyCache?.set(cacheKey, null); if (error instanceof FetchError && error.response != null) await keyCache?.setFetchError?.(cacheKey, { status: error.response.status, response: error.response.clone() }); else await keyCache?.setFetchError?.(cacheKey, { error: error instanceof Error ? error : new Error(String(error)) }); return { key: null, cached: false }; }); } //#endregion export { importJwk as a, generateCryptoKeyPair as i, fetchKey as n, validateCryptoKey as o, fetchKeyDetailed as r, exportJwk as t };