UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

251 lines (250 loc) • 9.69 kB
import * as dntShim from "../_dnt.shims.js"; import { getLogger } from "@logtape/logtape"; import { SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api"; import metadata from "../deno.js"; import { getDocumentLoader, } from "../runtime/docloader.js"; import { isActor } from "../vocab/actor.js"; import { CryptographicKey, Object } from "../vocab/vocab.js"; /** * 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. */ export 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") { // @ts-ignore TS2304 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. */ export 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 dntShim.crypto.subtle.generateKey({ name: "RSASSA-PKCS1-v1_5", modulusLength: 4096, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: "SHA-256", }, true, ["sign", "verify"]); } else if (algorithm === "Ed25519") { return dntShim.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. */ export async function exportJwk(key) { validateCryptoKey(key); return await dntShim.crypto.subtle.exportKey("jwk", key); } /** * 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. */ export async function importJwk(jwk, type) { let key; if (jwk.kty === "RSA" && jwk.alg === "RS256") { key = await dntShim.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") { key = await dntShim.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 */ export function fetchKey(keyId, // deno-lint-ignore no-explicit-any cls, options = {}) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(metadata.name, metadata.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, // deno-lint-ignore no-explicit-any 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.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); return { key: null, cached: false }; } throw e; } } let key = null; if (object instanceof cls) key = object; else if (isActor(object)) { // @ts-ignore: cls is either CryptographicKey or Multikey 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: key, cached: false, }; }