@hpke/ohttp
Version:
Oblivious HTTP (RFC 9458) client and server using HPKE
140 lines (139 loc) • 5.52 kB
JavaScript
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);
}