UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

757 lines (753 loc) • 29.9 kB
import { Temporal } from "@js-temporal/polyfill"; import { URLPattern } from "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { deno_default } from "./docloader-C8mmLN-Y.js"; import { CryptographicKey } from "./vocab-dDpPQ0fF.js"; import { fetchKey, validateCryptoKey } from "./key-CPSlDkCj.js"; import { getLogger } from "@logtape/logtape"; import { SpanStatusCode, trace } from "@opentelemetry/api"; import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions"; import { decodeBase64, encodeBase64 } from "byte-encodings/base64"; import { encodeHex } from "byte-encodings/hex"; import { Item, decodeDict, encodeItem } from "structured-field-values"; //#region sig/http.ts /** * Signs a request using the given private key. * @param request The request to sign. * @param privateKey The private key to use for signing. * @param keyId The key ID to use for the signature. It will be used by the * verifier. * @returns The signed request. * @throws {TypeError} If the private key is invalid or unsupported. */ async function signRequest(request, privateKey, keyId, options = {}) { validateCryptoKey(privateKey, "private"); const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version); return await tracer.startActiveSpan("http_signatures.sign", async (span) => { try { const spec = options.spec ?? "draft-cavage-http-signatures-12"; let signed; if (spec === "rfc9421") signed = await signRequestRfc9421(request, privateKey, keyId, span, options.currentTime, options.body); else signed = await signRequestDraft(request, privateKey, keyId, span, options.currentTime, options.body); if (span.isRecording()) { span.setAttribute(ATTR_HTTP_REQUEST_METHOD, signed.method); span.setAttribute(ATTR_URL_FULL, signed.url); for (const [name, value] of signed.headers) span.setAttribute(ATTR_HTTP_REQUEST_HEADER(name), value); span.setAttribute("http_signatures.key_id", keyId.href); } return signed; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) }); throw error; } finally { span.end(); } }); } async function signRequestDraft(request, privateKey, keyId, span, currentTime, bodyBuffer) { if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name); const url = new URL(request.url); const body = bodyBuffer !== void 0 ? bodyBuffer : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null; const headers = new Headers(request.headers); if (!headers.has("Host")) headers.set("Host", url.host); if (!headers.has("Digest") && body != null) { const digest = await crypto.subtle.digest("SHA-256", body); headers.set("Digest", `SHA-256=${encodeBase64(digest)}`); if (span.isRecording()) span.setAttribute("http_signatures.digest.sha-256", encodeHex(digest)); } if (!headers.has("Date")) headers.set("Date", currentTime == null ? (/* @__PURE__ */ new Date()).toUTCString() : new Date(currentTime.toString()).toUTCString()); const serialized = [["(request-target)", `${request.method.toLowerCase()} ${url.pathname}`], ...headers]; const headerNames = serialized.map(([name]) => name); const message = serialized.map(([name, value]) => `${name}: ${value.trim()}`).join("\n"); const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, new TextEncoder().encode(message)); const sigHeader = `keyId="${keyId.href}",algorithm="rsa-sha256",headers="${headerNames.join(" ")}",signature="${encodeBase64(signature)}"`; headers.set("Signature", sigHeader); if (span.isRecording()) { span.setAttribute("http_signatures.algorithm", "rsa-sha256"); span.setAttribute("http_signatures.signature", encodeHex(signature)); } return new Request(request, { headers, body }); } function formatRfc9421SignatureParameters(params) { return `alg="${params.algorithm}";keyid="${params.keyId.href}";created=${params.created}`; } /** * Creates a signature base for a request according to RFC 9421. * @param request The request to create a signature base for. * @param components The components to include in the signature base. * @param parameters The signature parameters to include in the signature base. * @returns The signature base as a string. */ function createRfc9421SignatureBase(request, components, parameters) { const url = new URL(request.url); const baseComponents = []; for (const component of components) { let value; if (component === "@method") value = request.method.toUpperCase(); else if (component === "@target-uri") value = request.url; else if (component === "@authority") value = url.host; else if (component === "@scheme") value = url.protocol.slice(0, -1); else if (component === "@request-target") value = `${request.method.toLowerCase()} ${url.pathname}${url.search}`; else if (component === "@path") value = url.pathname; else if (component === "@query") value = url.search.startsWith("?") ? url.search.slice(1) : url.search; else if (component === "@query-param") throw new Error("@query-param requires a parameter name"); else if (component === "@status") throw new Error("@status is only valid for responses"); else if (component.startsWith("@")) throw new Error(`Unsupported derived component: ${component}`); else { const header = request.headers.get(component); if (header == null) throw new Error(`Missing header: ${component}`); value = header; } baseComponents.push(`"${component}": ${value}`); } const sigComponents = components.map((c) => `"${c}"`).join(" "); baseComponents.push(`"@signature-params": (${sigComponents});${parameters}`); return baseComponents.join("\n"); } /** * Formats a signature using rfc9421 format. * @param signature The raw signature bytes. * @param components The components that were signed. * @param parameters The signature parameters. * @returns The formatted signature string. */ function formatRfc9421Signature(signature, components, parameters) { const signatureInputValue = `sig1=("${components.join("\" \"")}");${parameters}`; const signatureValue = `sig1=:${encodeBase64(signature)}:`; return [signatureInputValue, signatureValue]; } /** * Parse RFC 9421 Signature-Input header. * @param signatureInput The Signature-Input header value. * @returns Parsed signature input parameters. */ function parseRfc9421SignatureInput(signatureInput) { let dict; try { dict = decodeDict(signatureInput); } catch (error) { getLogger([ "fedify", "sig", "http" ]).debug("Failed to parse Signature-Input header: {signatureInput}", { signatureInput, error }); return {}; } const result = {}; for (const [label, item] of Object.entries(dict)) { if (!Array.isArray(item.value) || typeof item.params.keyid !== "string" || typeof item.params.created !== "number") continue; const components = item.value.map((subitem) => subitem.value).filter((v) => typeof v === "string"); const params = encodeItem(new Item(0, item.params)); result[label] = { keyId: item.params.keyid, alg: item.params.alg, created: item.params.created, components, parameters: params.slice(params.indexOf(";") + 1) }; } return result; } /** * Parse RFC 9421 Signature header. * @param signature The Signature header value. * @returns Parsed signature values. */ function parseRfc9421Signature(signature) { let dict; try { dict = decodeDict(signature); } catch (error) { getLogger([ "fedify", "sig", "http" ]).debug("Failed to parse Signature header: {signature}", { signature, error }); return {}; } const result = {}; for (const [key, value] of Object.entries(dict)) if (value.value instanceof Uint8Array) result[key] = value.value; return result; } async function signRequestRfc9421(request, privateKey, keyId, span, currentTime, bodyBuffer) { if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name); const url = new URL(request.url); const body = bodyBuffer !== void 0 ? bodyBuffer : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null; const headers = new Headers(request.headers); if (!headers.has("Host")) headers.set("Host", url.host); if (!headers.has("Content-Digest") && body != null) { const digest = await crypto.subtle.digest("SHA-256", body); headers.set("Content-Digest", `sha-256=:${encodeBase64(digest)}:`); if (span.isRecording()) span.setAttribute("http_signatures.digest.sha-256", encodeHex(digest)); } currentTime ??= Temporal.Now.instant(); const created = currentTime.epochMilliseconds / 1e3 | 0; if (!headers.has("Date")) headers.set("Date", new Date(currentTime.toString()).toUTCString()); const components = [ "@method", "@target-uri", "@authority", "host", "date" ]; if (body != null) components.push("content-digest"); const signatureParams = formatRfc9421SignatureParameters({ algorithm: "rsa-v1_5-sha256", keyId, created }); let signatureBase; try { signatureBase = createRfc9421SignatureBase(new Request(request.url, { method: request.method, headers }), components, signatureParams); } catch (error) { throw new TypeError(`Failed to create signature base: ${String(error)}; it is probably a bug in the implementation. Please report it at Fedify's issue tracker.`); } const signatureBytes = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, new TextEncoder().encode(signatureBase)); const [signatureInput, signature] = formatRfc9421Signature(signatureBytes, components, signatureParams); headers.set("Signature-Input", signatureInput); headers.set("Signature", signature); if (span.isRecording()) { span.setAttribute("http_signatures.algorithm", "rsa-v1_5-sha256"); span.setAttribute("http_signatures.signature", encodeHex(signatureBytes)); span.setAttribute("http_signatures.created", created.toString()); } return new Request(request, { headers, body }); } const supportedHashAlgorithms = { "sha": "SHA-1", "sha-256": "SHA-256", "sha-512": "SHA-512" }; /** * Verifies the signature of a request. * * Note that this function consumes the request body, so it should not be used * if the request body is already consumed. Consuming the request body after * calling this function is okay, since this function clones the request * under the hood. * * @param request The request to verify. * @param options Options for verifying the request. * @returns The public key of the verified signature, or `null` if the signature * could not be verified. */ async function verifyRequest(request, options = {}) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(deno_default.name, deno_default.version); return await tracer.startActiveSpan("http_signatures.verify", async (span) => { if (span.isRecording()) { span.setAttribute(ATTR_HTTP_REQUEST_METHOD, request.method); span.setAttribute(ATTR_URL_FULL, request.url); for (const [name, value] of request.headers) span.setAttribute(ATTR_HTTP_REQUEST_HEADER(name), value); } try { let spec = options.spec; if (spec == null) spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12"; let key; if (spec === "rfc9421") key = await verifyRequestRfc9421(request, span, options); else key = await verifyRequestDraft(request, span, options); if (key == null) span.setStatus({ code: SpanStatusCode.ERROR }); return key; } catch (error) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) }); throw error; } finally { span.end(); } }); } async function verifyRequestDraft(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) { const logger = getLogger([ "fedify", "sig", "http" ]); if (request.bodyUsed) { logger.error("Failed to verify; the request body is already consumed.", { url: request.url }); return null; } else if (request.body?.locked) { logger.error("Failed to verify; the request body is locked.", { url: request.url }); return null; } const originalRequest = request; request = request.clone(); const dateHeader = request.headers.get("Date"); if (dateHeader == null) { logger.debug("Failed to verify; no Date header found.", { headers: Object.fromEntries(request.headers.entries()) }); return null; } const sigHeader = request.headers.get("Signature"); if (sigHeader == null) { logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) }); return null; } const digestHeader = request.headers.get("Digest"); if (request.method !== "GET" && request.method !== "HEAD" && digestHeader == null) { logger.debug("Failed to verify; no Digest header found.", { headers: Object.fromEntries(request.headers.entries()) }); return null; } let body = null; if (digestHeader != null) { body = await request.arrayBuffer(); const digests = digestHeader.split(",").map((pair) => pair.includes("=") ? pair.split("=", 2) : [pair, ""]); let matched = false; for (let [algo, digestBase64] of digests) { algo = algo.trim().toLowerCase(); if (!(algo in supportedHashAlgorithms)) continue; let digest; try { digest = decodeBase64(digestBase64); } catch (error) { logger.debug("Failed to verify; invalid base64 encoding: {digest}.", { digest: digestBase64, error }); return null; } if (span.isRecording()) span.setAttribute(`http_signatures.digest.${algo}`, encodeHex(digest)); const expectedDigest = await crypto.subtle.digest(supportedHashAlgorithms[algo], body); if (!timingSafeEqual(digest, new Uint8Array(expectedDigest))) { logger.debug("Failed to verify; digest mismatch ({algorithm}): {digest} != {expectedDigest}.", { algorithm: algo, digest: digestBase64, expectedDigest: encodeBase64(expectedDigest) }); return null; } matched = true; } if (!matched) { logger.debug("Failed to verify; no supported digest algorithm found. Supported: {supportedAlgorithms}; found: {algorithms}.", { supportedAlgorithms: Object.keys(supportedHashAlgorithms), algorithms: digests.map(([algo]) => algo) }); return null; } } const date = Temporal.Instant.from(new Date(dateHeader).toISOString()); const now = currentTime ?? Temporal.Now.instant(); if (timeWindow !== false) { const tw = timeWindow ?? { hours: 1 }; if (Temporal.Instant.compare(date, now.add(tw)) > 0) { logger.debug("Failed to verify; Date is too far in the future.", { date: date.toString(), now: now.toString() }); return null; } else if (Temporal.Instant.compare(date, now.subtract(tw)) < 0) { logger.debug("Failed to verify; Date is too far in the past.", { date: date.toString(), now: now.toString() }); return null; } } const sigValues = Object.fromEntries(sigHeader.split(",").map((pair) => pair.match(/^\s*([A-Za-z]+)="([^"]*)"\s*$/)).filter((m) => m != null).map((m) => m.slice(1, 3))); if (!("keyId" in sigValues)) { logger.debug("Failed to verify; no keyId field found in the Signature header.", { signature: sigHeader }); return null; } else if (!("headers" in sigValues)) { logger.debug("Failed to verify; no headers field found in the Signature header.", { signature: sigHeader }); return null; } else if (!("signature" in sigValues)) { logger.debug("Failed to verify; no signature field found in the Signature header.", { signature: sigHeader }); return null; } const { keyId, headers, signature } = sigValues; span?.setAttribute("http_signatures.key_id", keyId); if ("algorithm" in sigValues) span?.setAttribute("http_signatures.algorithm", sigValues.algorithm); const { key, cached } = await fetchKey(new URL(keyId), CryptographicKey, { documentLoader, contextLoader, keyCache, tracerProvider }); if (key == null) return null; const headerNames = headers.split(/\s+/g); if (!headerNames.includes("(request-target)") || !headerNames.includes("date")) { logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers }); return null; } if (body != null && !headerNames.includes("digest")) { logger.debug("Failed to verify; required headers missing in the Signature header: {headers}.", { headers }); return null; } const message = headerNames.map((name) => `${name}: ` + (name == "(request-target)" ? `${request.method.toLowerCase()} ${new URL(request.url).pathname}` : name == "host" ? request.headers.get("host") ?? new URL(request.url).host : request.headers.get(name))).join("\n"); const sig = decodeBase64(signature); span?.setAttribute("http_signatures.signature", encodeHex(sig)); const verified = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, sig, new TextEncoder().encode(message)); if (!verified) { if (cached) { logger.debug("Failed to verify with the cached key {keyId}; signature {signature} is invalid. Retrying with the freshly fetched key...", { keyId, signature, message }); return await verifyRequest(originalRequest, { documentLoader, contextLoader, timeWindow, currentTime, keyCache: { get: () => Promise.resolve(void 0), set: async (keyId$1, key$1) => await keyCache?.set(keyId$1, key$1) } }); } logger.debug("Failed to verify with the fetched key {keyId}; signature {signature} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", { keyId, signature, message }); return null; } return key; } /** * RFC 9421 map of algorithm identifiers to WebCrypto algorithms */ const rfc9421AlgorithmMap = { "rsa-v1_5-sha256": { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, "rsa-v1_5-sha512": { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" }, "rsa-pss-sha512": { name: "RSA-PSS", hash: "SHA-512" }, "ecdsa-p256-sha256": { name: "ECDSA", hash: "SHA-256" }, "ecdsa-p384-sha384": { name: "ECDSA", hash: "SHA-384" }, "ed25519": { name: "Ed25519" } }; /** * Verifies a Content-Digest header according to RFC 9421. * @param digestHeader The Content-Digest header value. * @param body The message body to verify against. * @returns Whether the digest is valid. */ async function verifyRfc9421ContentDigest(digestHeader, body) { const digests = digestHeader.split(",").map((pair) => { pair = pair.trim(); const pos = pair.indexOf("="); const algo = pos < 0 ? pair : pair.slice(0, pos); const value = pos < 0 ? "" : pair.slice(pos + 1); return { algo: algo.trim().toLowerCase(), value: value.trim() }; }); for (const { algo, value } of digests) { let hashAlgo; if (algo === "sha-256") hashAlgo = "SHA-256"; else if (algo === "sha-512") hashAlgo = "SHA-512"; else continue; const base64Match = value.match(/^:([^:]+):$/); if (!base64Match) continue; let digest; try { digest = decodeBase64(base64Match[1]); } catch { continue; } const calculatedDigest = await crypto.subtle.digest(hashAlgo, body); if (timingSafeEqual(digest, new Uint8Array(calculatedDigest))) return true; } return false; } async function verifyRequestRfc9421(request, span, { documentLoader, contextLoader, timeWindow, currentTime, keyCache, tracerProvider } = {}) { const logger = getLogger([ "fedify", "sig", "http" ]); if (request.bodyUsed) { logger.error("Failed to verify; the request body is already consumed.", { url: request.url }); return null; } else if (request.body?.locked) { logger.error("Failed to verify; the request body is locked.", { url: request.url }); return null; } const originalRequest = request; request = request.clone(); const signatureInputHeader = request.headers.get("Signature-Input"); if (!signatureInputHeader) { logger.debug("Failed to verify; no Signature-Input header found.", { headers: Object.fromEntries(request.headers.entries()) }); return null; } const signatureHeader = request.headers.get("Signature"); if (!signatureHeader) { logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) }); return null; } const signatureInputs = parseRfc9421SignatureInput(signatureInputHeader); logger.debug("Parsed Signature-Input header: {signatureInputs}", { signatureInputs }); const signatures = parseRfc9421Signature(signatureHeader); const signatureNames = Object.keys(signatureInputs); if (signatureNames.length === 0) { logger.debug("Failed to verify; no valid signatures found in Signature-Input header.", { header: signatureInputHeader }); return null; } let validKey = null; for (const sigName of signatureNames) { if (!signatures[sigName]) continue; const sigInput = signatureInputs[sigName]; const sigBytes = signatures[sigName]; if (!sigInput.keyId) { logger.debug("Failed to verify; missing keyId in signature {signatureName}.", { signatureName: sigName, signatureInput: signatureInputHeader }); continue; } if (!sigInput.created) { logger.debug("Failed to verify; missing created timestamp in signature {signatureName}.", { signatureName: sigName, signatureInput: signatureInputHeader }); continue; } const signatureCreated = Temporal.Instant.fromEpochMilliseconds(sigInput.created * 1e3); const now = currentTime ?? Temporal.Now.instant(); if (timeWindow !== false) { const tw = timeWindow ?? { hours: 1 }; if (Temporal.Instant.compare(signatureCreated, now.add(tw)) > 0) { logger.debug("Failed to verify; signature created time is too far in the future.", { created: signatureCreated.toString(), now: now.toString() }); continue; } else if (Temporal.Instant.compare(signatureCreated, now.subtract(tw)) < 0) { logger.debug("Failed to verify; signature created time is too far in the past.", { created: signatureCreated.toString(), now: now.toString() }); continue; } } if (request.method !== "GET" && request.method !== "HEAD" && sigInput.components.includes("content-digest")) { const contentDigestHeader = request.headers.get("Content-Digest"); if (!contentDigestHeader) { logger.debug("Failed to verify; Content-Digest header required but not found.", { components: sigInput.components }); continue; } const body = await request.arrayBuffer(); const digestValid = await verifyRfc9421ContentDigest(contentDigestHeader, body); if (!digestValid) { logger.debug("Failed to verify; Content-Digest verification failed.", { contentDigest: contentDigestHeader }); continue; } } span?.setAttribute("http_signatures.key_id", sigInput.keyId); span?.setAttribute("http_signatures.created", sigInput.created.toString()); const { key, cached } = await fetchKey(new URL(sigInput.keyId), CryptographicKey, { documentLoader, contextLoader, keyCache, tracerProvider }); if (!key) { logger.debug("Failed to fetch key: {keyId}", { keyId: sigInput.keyId }); continue; } let alg = sigInput.alg?.toLowerCase(); if (alg == null) { if (key.publicKey.algorithm.name === "RSASSA-PKCS1-v1_5") alg = "hash" in key.publicKey.algorithm ? key.publicKey.algorithm.hash === "SHA-512" ? "rsa-v1_5-sha512" : "rsa-v1_5-sha256" : "rsa-v1_5-sha256"; else if (key.publicKey.algorithm.name === "RSA-PSS") alg = "rsa-pss-sha512"; else if (key.publicKey.algorithm.name === "ECDSA") alg = "namedCurve" in key.publicKey.algorithm && key.publicKey.algorithm.namedCurve === "P-256" ? "ecdsa-p256-sha256" : "ecdsa-p384-sha384"; else if (key.publicKey.algorithm.name === "Ed25519") alg = "ed25519"; } if (alg) span?.setAttribute("http_signatures.algorithm", alg); const algorithm = alg && rfc9421AlgorithmMap[alg]; if (!algorithm) { logger.debug("Failed to verify; unsupported algorithm: {algorithm}", { algorithm: sigInput.alg, supported: Object.keys(rfc9421AlgorithmMap) }); continue; } let signatureBase; try { signatureBase = createRfc9421SignatureBase(request, sigInput.components, sigInput.parameters); } catch (error) { logger.debug("Failed to create signature base for verification: {error}", { error, signatureInput: sigInput }); continue; } const signatureBaseBytes = new TextEncoder().encode(signatureBase); span?.setAttribute("http_signatures.signature", encodeHex(sigBytes)); try { const verified = await crypto.subtle.verify(algorithm, key.publicKey, sigBytes, signatureBaseBytes); if (verified) { validKey = key; break; } else if (cached) { logger.debug("Failed to verify with cached key {keyId}; retrying with fresh key...", { keyId: sigInput.keyId }); return await verifyRequest(originalRequest, { documentLoader, contextLoader, timeWindow, currentTime, keyCache: { get: () => Promise.resolve(void 0), set: async (keyId, key$1) => await keyCache?.set(keyId, key$1) }, spec: "rfc9421" }); } else logger.debug("Failed to verify signature with fetched key {keyId}; signature invalid.", { keyId: sigInput.keyId, signatureBase }); } catch (error) { logger.debug("Error during signature verification: {error}", { error, keyId: sigInput.keyId, algorithm: sigInput.alg }); } } return validKey; } /** * Helper function to create a new Request for redirect handling. * @param request The original request. * @param location The redirect location. * @param body The request body as ArrayBuffer or null. * @returns A new Request object for the redirect. */ function createRedirectRequest(request, location, body) { const url = new URL(location, request.url); return new Request(url, { method: request.method, headers: request.headers, body, redirect: "manual", signal: request.signal, mode: request.mode, credentials: request.credentials, referrer: request.referrer, referrerPolicy: request.referrerPolicy, integrity: request.integrity, keepalive: request.keepalive, cache: request.cache }); } /** * Performs a double-knock request to the given URL. For the details of * double-knocking, see * <https://swicg.github.io/activitypub-http-signature/#how-to-upgrade-supported-versions>. * @param request The request to send. * @param identity The identity to use for signing the request. * @param options The options for double-knock requests. * @returns The response to the request. * @since 1.6.0 */ async function doubleKnock(request, identity, options = {}) { const { specDeterminer, log, tracerProvider } = options; const origin = new URL(request.url).origin; const firstTrySpec = specDeterminer == null ? "rfc9421" : await specDeterminer.determineSpec(origin); const body = options.body !== void 0 ? options.body : request.method !== "GET" && request.method !== "HEAD" ? await request.clone().arrayBuffer() : null; let signedRequest = await signRequest(request, identity.privateKey, identity.keyId, { spec: firstTrySpec, tracerProvider, body }); log?.(signedRequest); let response = await fetch(signedRequest, { redirect: "manual" }); if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) { const location = response.headers.get("Location"); return doubleKnock(createRedirectRequest(request, location, body), identity, { ...options, body }); } else if (response.status === 400 || response.status === 401 || response.status > 401) { const spec = firstTrySpec === "draft-cavage-http-signatures-12" ? "rfc9421" : "draft-cavage-http-signatures-12"; getLogger([ "fedify", "sig", "http" ]).debug("Failed to verify with the spec {spec} ({status} {statusText}); retrying with spec {secondSpec}... (double-knocking)", { spec: firstTrySpec, secondSpec: spec, status: response.status, statusText: response.statusText }); signedRequest = await signRequest(request, identity.privateKey, identity.keyId, { spec, tracerProvider, body }); log?.(signedRequest); response = await fetch(signedRequest, { redirect: "manual" }); if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) { const location = response.headers.get("Location"); return doubleKnock(createRedirectRequest(request, location, body), identity, { ...options, body }); } else if (response.status !== 400 && response.status !== 401) await specDeterminer?.rememberSpec(origin, spec); } else await specDeterminer?.rememberSpec(origin, firstTrySpec); return response; } /** * Performs a timing-safe equality comparison between two `Uint8Array` values. * * This function is designed to take a constant amount of time to execute, * dependent only on the length of the longer of the two arrays, * regardless of where the first difference in bytes occurs. This helps * prevent timing attacks. * * @param a The first bytes. * @param b The second bytes. * @returns `true` if the arrays are of the same length and contain the same * bytes, `false` otherwise. * @since 1.6.0 */ function timingSafeEqual(a, b) { const lenA = a.length; const lenB = b.length; const commonLength = Math.max(lenA, lenB); let result = 0; for (let i = 0; i < commonLength; i++) { const byteA = i < lenA ? a[i] : 0; const byteB = i < lenB ? b[i] : 0; result |= byteA ^ byteB; } result |= lenA ^ lenB; return result === 0; } //#endregion export { createRfc9421SignatureBase, doubleKnock, formatRfc9421Signature, formatRfc9421SignatureParameters, parseRfc9421Signature, parseRfc9421SignatureInput, signRequest, timingSafeEqual, verifyRequest };