/** * Deterministic handshake transcript builder. Both endpoints concatenate the SAME length-prefixed * fields in the SAME fixed order (client_hello fields, then host_hello fields EXCLUDING `sig`), so * the transcript the host signs is byte-identical to the one the client verifies. `deviceAuthProof` * is excluded (it is verified separately and is not part of the signed key-agreement transcript). */ import type { AeadAlg } from 'relay-contracts' import { concatBytes, writeUint32BE } from 'relay-contracts' const encoder = new TextEncoder() const DOMAIN = encoder.encode('relay-e2e/transcript/v1') function lp(chunk: Uint8Array): Uint8Array { const head = new Uint8Array(4) writeUint32BE(head, 0, chunk.length) return concatBytes([head, chunk]) } export interface TranscriptParts { readonly clientEphPub: Uint8Array readonly clientNonce: Uint8Array readonly aeadOffer: readonly AeadAlg[] readonly hostEphPub: Uint8Array readonly hostNonce: Uint8Array readonly aeadChoice: AeadAlg readonly enrollFpr: string } /** Build the byte-identical transcript both sides bind the Ed25519 signature to. */ export function buildTranscript(p: TranscriptParts): Uint8Array { return concatBytes([ lp(DOMAIN), lp(p.clientEphPub), lp(p.clientNonce), lp(encoder.encode(p.aeadOffer.join(','))), lp(p.hostEphPub), lp(p.hostNonce), lp(encoder.encode(p.aeadChoice)), lp(encoder.encode(p.enrollFpr)), ]) }