Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/tools/presentation_fixture.cjs

4.3 KiB, 1 run

created by r1870400018:61162, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// Captures a named presentation signed by a real WebCrypto Ed25519 key in
2// headless Chromium, for fe2o3_net's tests/presentation.rs. The browser makes
3// both keys (an issuer's and a member's) non-extractable, signs the head and the
4// presentation over its own canonical JSON (keys sorted, JSON.stringify, no
5// whitespace), and hands back only public material. The Rust side must accept
6// the lot, which it can only do if it rebuilds the browser's bytes exactly and
7// verifies the browser's signatures.
8//
9// Run from fe2o3_net, with NODE_PATH naming a node_modules that holds playwright,
10// and CHROMIUM_PATH naming a Chromium binary when playwright's own is absent:
11//
12// NODE_PATH=<dir>/node_modules node tools/presentation_fixture.cjs \
13// > tests/data/presentation_webcrypto.json
14
15'use strict';
16
17const { chromium } = require('playwright');
18
19(async () => {
20 const browser = await chromium.launch({
21 headless: true,
22 executablePath: process.env.CHROMIUM_PATH || undefined,
23 });
24 const page = await browser.newPage();
25 // WebCrypto lives only in a secure context, so serve a blank page on https.
26 await page.route('https://fixture.test/**', route => route.fulfill({
27 status: 200,
28 contentType: 'text/html',
29 body: '<!doctype html><title>fixture</title>',
30 }));
31 await page.goto('https://fixture.test/');
32
33 const out = await page.evaluate(async () => {
34 const enc = new TextEncoder();
35 const sorted = v => Array.isArray(v) ? v.map(sorted)
36 : (v && typeof v === 'object')
37 ? Object.fromEntries(Object.keys(v).sort().map(k => [k, sorted(v[k])]))
38 : v;
39 const jcs = v => JSON.stringify(sorted(v));
40 const b64u = bytes => btoa(String.fromCharCode(...bytes))
41 .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
42 const hex = bytes => [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
43 const sha256 = async bytes => new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
44 const keyPair = () => crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify']);
45 const raw = async pair => new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
46 const sign = async (pair, text) => new Uint8Array(
47 await crypto.subtle.sign({ name: 'Ed25519' }, pair.privateKey, enc.encode(text)));
48
49 const issuer = await keyPair();
50 const member = await keyPair();
51 const issuerPub = await raw(issuer);
52 const memberPub = await raw(member);
53 const now = Math.floor(Date.now() / 1000);
54 const rp_id = 'https://app.example';
55 const nonce = crypto.getRandomValues(new Uint8Array(32));
56
57 // A head with an empty ring list, which is what a named presentation
58 // needs and what a network with no ring keys yet serves.
59 const headBody = {
60 v: 'head/1', epoch: 1, prev: null, ts: now - 30,
61 salt: b64u(crypto.getRandomValues(new Uint8Array(16))),
62 members: 1, ring_n: 0, ring_digest: b64u(await sha256(new Uint8Array(0))),
63 signer: b64u(issuerPub),
64 };
65 const headText = jcs(headBody);
66 const headId = await sha256(enc.encode(headText));
67 const head = { ...headBody, head: b64u(headId), sig: b64u(await sign(issuer, headText)) };
68
69 const body = {
70 v: 'present/1', mode: 'named', rp_id, nonce: b64u(nonce),
71 sub: hex((await sha256(memberPub)).slice(0, 5)), pub: b64u(memberPub),
72 predicates: ['adult'], head: b64u(headId), ts: now,
73 };
74 const text = jcs(body);
75 const presentation = JSON.stringify({ ...body, sig: b64u(await sign(member, text)) });
76
77 return {
78 browser: navigator.userAgent,
79 now,
80 rp_id,
81 request: {
82 v: 'present-req/1', rp_id, nonce: b64u(nonce),
83 modes: ['pairwise', 'named'], predicates: ['adult'], exp: now + 300,
84 },
85 head,
86 presentation,
87 status: { pub: b64u(memberPub), live: true },
88 };
89 });
90
91 process.stdout.write(JSON.stringify(out, null, 1) + '\n');
92 await browser.close();
93})().catch(e => {
94 process.stderr.write(String(e && e.stack || e) + '\n');
95 process.exit(1);
96});