UNPKG

@hpke/ohttp

Version:

Oblivious HTTP (RFC 9458) client and server using HPKE

140 lines (139 loc) • 5.52 kB
import { CipherSuite } from "@hpke/core"; import { decodeRequest } from "./bhttp/decoder.js"; import { encodeResponse } from "./bhttp/encoder.js"; import { decapsulateRequest, encapsulateResponse } from "./encapsulation.js"; import { OhttpError } from "./errors.js"; import { serializeKeyConfig } from "./keyConfig.js"; import { bindKdf } from "./suite.js"; const MEDIA_TYPE_REQUEST = "message/ohttp-req"; const MEDIA_TYPE_RESPONSE = "message/ohttp-res"; /** Problem type for key configuration errors (RFC 9458 Section 5.3). */ const PROBLEM_TYPE_KEY = "https://iana.org/assignments/http-problem-types#ohttp-key"; class OhttpServerImpl { constructor(keyId, kemId, privateKey, suites, keyConfig) { Object.defineProperty(this, "keyConfig", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_keyId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_kemId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_privateKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_suites", { enumerable: true, configurable: true, writable: true, value: void 0 }); this._keyId = keyId; this._kemId = kemId; this._privateKey = privateKey; this._suites = suites; this.keyConfig = keyConfig; } async handle(request, handler) { if (request.method !== "POST") { return new Response(null, { status: 405, headers: { "Allow": "POST" } }); } const mediaType = (request.headers.get("Content-Type") ?? "") .split(";")[0].trim().toLowerCase(); if (mediaType !== MEDIA_TYPE_REQUEST) { return new Response(null, { status: 415 }); } const encapsulatedRequest = new Uint8Array(await request.arrayBuffer()); if (encapsulatedRequest.byteLength > 0 && encapsulatedRequest[0] !== this._keyId) { return new Response(JSON.stringify({ type: PROBLEM_TYPE_KEY, title: "key identifier unknown", }), { status: 400, headers: { "Content-Type": "application/problem+json" }, }); } let ctx; try { ctx = await this.decapsulateRequest(encapsulatedRequest); } catch { return new Response(null, { status: 400 }); } const encapsulatedResponse = await ctx.encapsulateResponse(await handler(ctx.request)); return new Response(encapsulatedResponse.buffer, { status: 200, headers: { "Content-Type": MEDIA_TYPE_RESPONSE }, }); } async decapsulateRequest(encapsulatedRequest) { if (encapsulatedRequest.byteLength < 7) { throw new OhttpError("Encapsulated request too short"); } const view = new DataView(encapsulatedRequest.buffer, encapsulatedRequest.byteOffset, encapsulatedRequest.byteLength); const kdfId = view.getUint16(3); const aeadId = view.getUint16(5); const suite = this._suites.find((s) => s.kdf.id === kdfId && s.aead.id === aeadId); if (!suite) { throw new OhttpError(`Unsupported cipher suite: KDF=0x${kdfId.toString(16)}, AEAD=0x${aeadId.toString(16)}`); } const result = await decapsulateRequest(suite, this._privateKey, this._keyId, this._kemId, encapsulatedRequest); const request = decodeRequest(result.binaryRequest); const respCtx = result.context; return { request, async encapsulateResponse(response) { return encapsulateResponse(respCtx, await encodeResponse(response)); }, }; } } /** * Creates an OHTTP server, i.e. an Oblivious Gateway Resource (RFC 9458). * * A key pair is generated unless `keyPair` is given, and the key * configuration to be published is built up front. * * @throws {OhttpError} if the parameters are invalid. */ export async function createOhttpServer(params) { if (!Number.isInteger(params.keyId) || params.keyId < 0 || params.keyId > 255) { throw new OhttpError(`Invalid keyId: ${params.keyId}`); } if (params.suites.length === 0) { throw new OhttpError("At least one KDF/AEAD pair is required"); } // Each suite gets its own KDF state so that a KDF instance shared by // multiple suites (or with the caller) is not overwritten. const suites = params.suites.map((s) => new CipherSuite({ kem: params.kem, kdf: bindKdf(s.kdf), aead: s.aead, })); const kem = suites[0].kem; const keyPair = params.keyPair ?? await kem.generateKeyPair(); const keyConfig = serializeKeyConfig({ keyId: params.keyId, kem: kem.id, publicKey: new Uint8Array(await kem.serializePublicKey(keyPair.publicKey)), cipherSuites: params.suites.map((s) => ({ kdf: s.kdf.id, aead: s.aead.id, })), }); return new OhttpServerImpl(params.keyId, kem.id, keyPair.privateKey, suites, keyConfig); }