diff --git a/src/ohttp.ts b/src/ohttp.ts index 75cd6da..9772ef0 100644 --- a/src/ohttp.ts +++ b/src/ohttp.ts @@ -81,6 +81,25 @@ export class KeyConfig { } } +export class DeterministicKeyConfig extends KeyConfig { + constructor(keyId: number, ikm: Uint8Array) { + super(keyId); + if (keyId < 0 || keyId > 255) { + throw new InvalidConfigIdError(invalidKeyIdErrorString); + } + this.keyId = keyId; + this.kem = Kem.DhkemX25519HkdfSha256; + this.kdf = Kdf.HkdfSha256; + this.aead = Aead.Aes128Gcm; + const suite = new CipherSuite({ + kem: this.kem, + kdf: this.kdf, + aead: this.aead, + }); + this.keyPair = suite.deriveKeyPair(ikm); + } +} + export class PublicKeyConfig { public keyId: number; public kem: Kem; diff --git a/test/ohttp.test.ts b/test/ohttp.test.ts index a979ba4..022cdaa 100644 --- a/test/ohttp.test.ts +++ b/test/ohttp.test.ts @@ -1,7 +1,25 @@ -import { assertEquals, assertStrictEquals } from "testing/asserts.ts"; +import { + assertEquals, + assertNotEquals, + assertStrictEquals, +} from "testing/asserts.ts"; import { describe, it } from "testing/bdd.ts"; -import { Client, ClientConstructor, KeyConfig, Server } from "../src/ohttp.ts"; +import { loadCrypto } from "../src/webCrypto.ts"; +import { + Client, + ClientConstructor, + DeterministicKeyConfig, + KeyConfig, + Server, +} from "../src/ohttp.ts"; + +async function randomBytes(l: number): Promise { + const buffer = new Uint8Array(l); + const cryptoApi = await loadCrypto(); + cryptoApi.getRandomValues(buffer); + return buffer; +} describe("test OHTTP end-to-end", () => { it("Happy Path", async () => { @@ -87,4 +105,51 @@ describe("test OHTTP end-to-end", () => { const body = await finalResponse.arrayBuffer(); assertStrictEquals(new TextDecoder().decode(new Uint8Array(body)), "baz"); }); + + it("Happy Path with a deterministic KeyConfig", async () => { + const keyId = 0x01; + const seed = await randomBytes(32); + + // Create a pair of servers with the same config and make sure they result in the same public key configuration + const keyConfig = new DeterministicKeyConfig(keyId, seed); + const server = new Server(keyConfig); + const sameConfig = new DeterministicKeyConfig(keyId, seed); + const sameServer = new Server(sameConfig); + const diffConfig = new KeyConfig(keyId); + const diffServer = new Server(diffConfig); + + const configA = await server.encodeKeyConfig(); + const configB = await sameServer.encodeKeyConfig(); + const configC = await diffServer.encodeKeyConfig(); + assertEquals(configA, configB); + assertNotEquals(configA, configC); + + const publicKeyConfig = await keyConfig.publicConfig(); + + const requestUrl = "https://target.example/query?foo=bar"; + const request = new Request(requestUrl); + const response = new Response("baz", { + headers: { "Content-Type": "text/plain" }, + }); + + const client = new Client(publicKeyConfig); + const requestContext = await client.encapsulateRequest(request); + const clientRequest = requestContext.request; + const encodedClientRequest = clientRequest.encode(); + + const responseContext = await server.decodeAndDecapsulate( + encodedClientRequest, + ); + const receivedRequest = responseContext.request(); + assertStrictEquals(receivedRequest.url, "https://target.example/query"); + + const serverResponse = await responseContext.encapsulateResponse(response); + + const finalResponse = await requestContext.decapsulateResponse( + serverResponse, + ); + assertStrictEquals(finalResponse.headers.get("Content-Type"), "text/plain"); + const body = await finalResponse.arrayBuffer(); + assertStrictEquals(new TextDecoder().decode(new Uint8Array(body)), "baz"); + }); });