const encoder = new TextEncoder();
|
|
function stable(value) {
|
if (Array.isArray(value)) return value.map(stable);
|
if (value && typeof value === "object") {
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
}
|
return value;
|
}
|
|
export function canonicalJson(value) {
|
return JSON.stringify(stable(value));
|
}
|
|
async function hmacHex(secretHex, value) {
|
const secret = Uint8Array.from(secretHex.match(/../gu) || [], (item) => Number.parseInt(item, 16));
|
const key = await crypto.subtle.importKey("raw", secret, {name: "HMAC", hash: "SHA-256"}, false, ["sign"]);
|
const result = await crypto.subtle.sign("HMAC", key, encoder.encode(canonicalJson(value)));
|
return [...new Uint8Array(result)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
}
|
|
async function verify(secret, message) {
|
if (!message || typeof message !== "object" || typeof message.hmac !== "string") throw new Error("E_PROTOCOL");
|
const unsigned = {...message};
|
const supplied = unsigned.hmac;
|
delete unsigned.hmac;
|
if (!/^[0-9a-f]{64}$/u.test(supplied)) throw new Error("E_HMAC");
|
const rawSecret = Uint8Array.from(secret.match(/../gu) || [], (item) => Number.parseInt(item, 16));
|
const signature = Uint8Array.from(supplied.match(/../gu) || [], (item) => Number.parseInt(item, 16));
|
const key = await crypto.subtle.importKey("raw", rawSecret, {name: "HMAC", hash: "SHA-256"}, false, ["verify"]);
|
const valid = await crypto.subtle.verify("HMAC", key, signature, encoder.encode(canonicalJson(unsigned)));
|
if (!valid) throw new Error("E_HMAC");
|
}
|
|
export class TrustedRuntimeSession {
|
constructor(api, hello) {
|
this.api = api;
|
this.hello = Object.freeze({...hello});
|
this.secret = null;
|
this.runId = null;
|
this.requestId = null;
|
this.sequence = 0;
|
this.prepared = null;
|
this.dispatched = false;
|
this.actionResult = null;
|
}
|
|
helloFrame() {
|
return {schema_version: 1, type: "EXTENSION_HELLO", sequence: 1, ...this.hello};
|
}
|
|
async accept(message) {
|
if (message?.type === "HOST_CHALLENGE") {
|
if (this.secret !== null || !/^[0-9a-f]{64}$/u.test(String(message.secret || ""))) throw new Error("E_CHALLENGE");
|
this.secret = message.secret;
|
await verify(this.secret, message);
|
this.runId = message.run_id;
|
this.requestId = message.request_id;
|
this.sequence = 2;
|
return this.sign("EXTENSION_CHALLENGE_ACCEPTED", 3, {challenge_id: message.challenge_id});
|
}
|
if (!this.secret) throw new Error("E_CHALLENGE_REQUIRED");
|
await verify(this.secret, message);
|
if (message.run_id !== this.runId || message.request_id !== this.requestId || message.sequence !== this.sequence + 2) {
|
throw new Error("E_SEQUENCE");
|
}
|
this.sequence = message.sequence;
|
if (message.type === "HOST_ACTION_PREPARE") {
|
if (this.prepared) throw new Error("E_REPLAY");
|
this.prepared = await this.api.prepare(message.action);
|
return this.sign("EXTENSION_READY_TO_DISPATCH", message.sequence + 1, {
|
action_id: message.action.action_id,
|
prepared: this.prepared
|
});
|
}
|
if (message.type === "HOST_DISPATCH_PERMIT") {
|
if (!this.prepared || this.dispatched || message.permit.action_id !== this.prepared.action_id) throw new Error("E_PERMIT");
|
if (Date.now() > Date.parse(message.permit.deadline_at)) throw new Error("E_DEADLINE");
|
this.dispatched = true;
|
this.actionResult = await this.api.dispatch(this.prepared);
|
return this.sign("EXTENSION_ACTION_RESULT", message.sequence + 1, {
|
permit_id: message.permit.permit_id,
|
result: this.actionResult
|
});
|
}
|
if (message.type === "HOST_OBSERVATION_REQUEST") {
|
if (!this.dispatched || !this.actionResult) throw new Error("E_ACTION_RESULT_REQUIRED");
|
const observation = await this.api.observe(this.actionResult);
|
return this.sign("EXTENSION_OBSERVATION", message.sequence + 1, {observation});
|
}
|
if (message.type === "HOST_COMMIT_RESULT") {
|
return null;
|
}
|
throw new Error("E_MESSAGE_TYPE");
|
}
|
|
async sign(type, sequence, fields) {
|
const unsigned = {schema_version: 1, type, run_id: this.runId, request_id: this.requestId, sequence, ...fields};
|
return {...unsigned, hmac: await hmacHex(this.secret, unsigned)};
|
}
|
}
|