mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
feat(beehive): add offline standalone infrastructure registration
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -10,6 +10,34 @@ can close while the host/runner continues. No direct-host HTTP API or controller
|
||||
service exists. This package intentionally lives alongside, not inside, the
|
||||
native clients: it is a separately authorized product experiment.
|
||||
|
||||
## Offline standalone host enrollment (partial, no network)
|
||||
|
||||
`node src/cli.ts setup <directory>` without an identity file opens Beehive's
|
||||
own enrollment interface. No Buzz Desktop is needed. Use **create** on a fresh
|
||||
host directory: enter its label, relay URL and your existing owner **public** key.
|
||||
The host generates its durable independent key and exports a private pairing file.
|
||||
Compare the full displayed fingerprint in your owner context.
|
||||
|
||||
Run the same command in your separate owner context and choose **approve**. Read
|
||||
the pairing file, verify the host/key/label/relay/fingerprint, confirm registration,
|
||||
choose an expiration and a new approval file, then enter your existing owner key
|
||||
through hidden local input. Do not run owner approval on a host you do not trust
|
||||
with that input. No new controller key is created and the owner key is not saved.
|
||||
Transfer the approval privately back to the host and choose **import**. **export**
|
||||
can repeat pairing-file export without replacing the retained host key.
|
||||
|
||||
Files must be owner-only regular files; transfer them privately and retain mode
|
||||
0600. Registration is domain-separated from agent NIP-OA, binds this exact pairing
|
||||
request, and grants neither agent placement nor owner account permissions.
|
||||
Registration expiration is checked locally; offline verification cannot discover
|
||||
remote revocation, prove a physical machine, or erase a copied key.
|
||||
|
||||
**This milestone stops at registered/pending/no-network.** Narrow relay admission,
|
||||
private host inventory in the TUI, and Start/reply/Move/reply over the new transport
|
||||
are not implemented. Broad NIP-OA cannot enroll a host. The legacy diagnostic
|
||||
workflow below remains separate and stores a shared owner key; do not use it for
|
||||
real host enrollment. Existing a8 OA identity files are rejected, not migrated.
|
||||
|
||||
## Run an isolated fixture
|
||||
|
||||
Requires Node >=22.18 (tested with Hermit 24.15.0), POSIX process groups and
|
||||
|
||||
@@ -7,6 +7,7 @@ import { claudeGuidance } from './claude.ts';
|
||||
import { readAgentSecret } from './key-input.ts';
|
||||
import { conversationInput } from './conversation-input.ts';
|
||||
import { localSetup } from './local-setup.ts';
|
||||
import { enrollmentInput } from './enrollment-input.ts';
|
||||
import { Profiles, profile, profileRevision, type Profile } from './profiles.ts';
|
||||
import { createInterface } from 'node:readline/promises';
|
||||
import { stdin, stdout } from 'node:process';
|
||||
@@ -26,7 +27,8 @@ const shellQuote = (s: string) => `'${s.replaceAll("'", "'\"'\"'")}'`;
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
const help = `Beehive — isolated development preview (loopback relay only)
|
||||
identity <new-directory> Create a NEW local owner identity
|
||||
setup <host-directory> [identity-file] New host or existing local wizard
|
||||
setup <host-directory> Offline host pairing/owner approval/import
|
||||
setup <host-directory> <identity-file> LEGACY loopback diagnostic setup (owner key copied)
|
||||
presets Local process-free preset discovery/setup guidance
|
||||
local-setup <host-directory> Bindings; new/reuse/hidden standby/restore identity
|
||||
migrate-slots <host-directory> Explicit stopped upgrade, preserves journal
|
||||
@@ -54,6 +56,9 @@ async function main() {
|
||||
showPresets();
|
||||
} else if (command === 'setup') {
|
||||
const dir = resolve(text(args[0]));
|
||||
if (args.length === 1 && !existsSync(join(dir, 'setup.json'))) {
|
||||
await enrollmentInput(dir); return;
|
||||
}
|
||||
if (existsSync(dir)) {
|
||||
if (args.length !== 1) throw Error('Existing setup: supply host directory only; retained owner cannot be replaced');
|
||||
await localSetup(dir); return;
|
||||
|
||||
39
beehive/src/enrollment-input.ts
Normal file
39
beehive/src/enrollment-input.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createInterface } from 'node:readline/promises';
|
||||
import { stdin, stdout } from 'node:process';
|
||||
import { resolve } from 'node:path';
|
||||
import { bootstrapHostIdentity, enrollHostIdentity, readHostIdentity } from './host-identity.ts';
|
||||
import { hostPairing, pairingFingerprint, registerHost } from './host-registration.ts';
|
||||
import { readPrivate, writePrivate } from './storage.ts';
|
||||
import { readAgentSecret } from './key-input.ts';
|
||||
import { text } from './protocol.ts';
|
||||
|
||||
/** Standalone offline enrollment UI. File exchange avoids circular relay bootstrap. */
|
||||
export async function enrollmentInput(directory: string): Promise<void> {
|
||||
const ui = createInterface({ input: stdin, output: stdout });
|
||||
try {
|
||||
console.log('Private infrastructure enrollment. No relay connection, agent authority, or runtime launch. Approval must run in your owner context, not on an untrusted host.');
|
||||
const action = await ui.question('Enrollment [create / export / approve / import / cancel]: ');
|
||||
if (action === 'cancel') return;
|
||||
if (action === 'create' || action === 'export') {
|
||||
const identity = action === 'create' ? bootstrapHostIdentity(directory, text(await ui.question('Host label: ')), text(await ui.question('Owner PUBLIC key (hex): ')), text(await ui.question('Relay URL: '))) : readHostIdentity(directory);
|
||||
console.log(`Pairing fingerprint: ${pairingFingerprint(identity.pairing)}`);
|
||||
const file = resolve(text(await ui.question('NEW private pairing file: ')));
|
||||
writePrivate(file, identity.pairing, true);
|
||||
console.log('Pairing exported. Transfer privately to your standalone owner context. Pending/no network. Export can be repeated if interrupted.');
|
||||
} else if (action === 'approve') {
|
||||
const request = hostPairing(readPrivate(resolve(text(await ui.question('Private pairing file: ')))));
|
||||
console.log(`Host: ${request.host}\nLabel: ${request.label}\nOwner: ${request.owner}\nRelay: ${request.relay}\nFingerprint: ${pairingFingerprint(request)}`);
|
||||
if (await ui.question('Compare full fingerprint with host. Register ONLY this infrastructure (not agents or relay delegation)? [yes/no]: ') !== 'yes') return;
|
||||
const expires = Number(await ui.question('Registration expiration (Unix seconds): '));
|
||||
if (!Number.isSafeInteger(expires) || expires <= Math.floor(Date.now() / 1000)) throw Error('Expiration must be in the future');
|
||||
const file = resolve(text(await ui.question('NEW private approval file: ')));
|
||||
ui.close();
|
||||
const secret = await readAgentSecret('Owner');
|
||||
writePrivate(file, registerHost(request, secret, expires), true);
|
||||
console.log('Registration signed. Owner secret not persisted. Transfer approval privately back to host. Relay admission remains pending.');
|
||||
} else if (action === 'import') {
|
||||
enrollHostIdentity(directory, readPrivate(resolve(text(await ui.question('Private approval file: ')))));
|
||||
console.log('Host registered. Pending/no network: narrow relay admission is not implemented. No agent authorized or started.');
|
||||
} else throw Error('Unknown enrollment action');
|
||||
} finally { ui.close(); }
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { schnorr } from '@noble/curves/secp256k1';
|
||||
import { digest, publicKey } from './protocol.ts';
|
||||
|
||||
/** NIP-OA tag as consumed by Buzz 051c3a2 nip_oa.rs. No owner secret is needed to verify/import. */
|
||||
/** Historical source-contract fixture only: NIP-OA as consumed by Buzz 051c3a2.
|
||||
* NOT infrastructure enrollment. AUTH ignores kind clauses and may establish an
|
||||
* agent-owner relationship. Production host registration must never use this tag.
|
||||
*/
|
||||
export type HostAttestation = ['auth', string, string, string];
|
||||
function clauses(conditions: string): string[] {
|
||||
if (conditions.length > 1024) throw Error('Attestation conditions too long');
|
||||
|
||||
@@ -1,45 +1,47 @@
|
||||
import { existsSync, mkdirSync, rmdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fields, newKey, object, publicKey, text } from './protocol.ts';
|
||||
import { fields, newKey, object, publicKey } from './protocol.ts';
|
||||
import { readPrivate, writePrivate } from './storage.ts';
|
||||
import { verifyHostAttestation, type HostAttestation } from './host-attestation.ts';
|
||||
import { hostPairing, verifyHostRegistration, type HostPairing, type HostRegistration } from './host-registration.ts';
|
||||
|
||||
/** Independent management identity. Agent slots and assignment authority are separate. */
|
||||
export type HostIdentity = { version: 1; label: string; owner: string; secret: string; attestation: HostAttestation | null };
|
||||
/** Infrastructure registration is separate from relay admission and agent placement. */
|
||||
export type HostIdentity = { version: 2; pairing: HostPairing; secret: string; registration: HostRegistration | null };
|
||||
function locked<T>(directory: string, operation: () => T): T {
|
||||
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
const lock = join(directory, 'host.lock');
|
||||
mkdirSync(lock, { mode: 0o700 });
|
||||
try { return operation(); } finally { rmdirSync(lock); }
|
||||
}
|
||||
/** Owner-public-only bootstrap. Pending until genuine owner attestation import; never migrates legacy keys. */
|
||||
export function bootstrapHostIdentity(directory: string, label: string, owner: string): HostIdentity {
|
||||
text(label);
|
||||
if (!/^[0-9a-f]{64}$/.test(owner)) throw Error('Expected owner public key');
|
||||
/** Offline bootstrap; neither requires admission nor copies the owner's secret. */
|
||||
export function bootstrapHostIdentity(directory: string, label: string, owner: string, relay: string): HostIdentity {
|
||||
return locked(directory, () => {
|
||||
if (existsSync(join(directory, 'setup.json'))) throw Error('Legacy installation retained; use a clean directory');
|
||||
const identity: HostIdentity = { version: 1, label, owner, secret: newKey(), attestation: null };
|
||||
const secret = newKey();
|
||||
const pairing = hostPairing({ version: 1, purpose: 'beehive-host-registration', host: publicKey(secret), owner, label, relay, nonce: newKey() });
|
||||
const identity: HostIdentity = { version: 2, pairing, secret, registration: null };
|
||||
writePrivate(join(directory, 'host-identity.json'), identity, true);
|
||||
return identity;
|
||||
});
|
||||
}
|
||||
/** Read local identity without converting absent/expired enrollment into trust. */
|
||||
/** Reject legacy broad OA records rather than silently migrating their authority. */
|
||||
export function readHostIdentity(directory: string): HostIdentity {
|
||||
const value = object(readPrivate(join(directory, 'host-identity.json')));
|
||||
fields(value, ['version', 'label', 'owner', 'secret', 'attestation']);
|
||||
if (value.version !== 1 || typeof value.owner !== 'string' || !/^[0-9a-f]{64}$/.test(value.owner) || typeof value.secret !== 'string' || !/^[0-9a-f]{64}$/.test(value.secret)) throw Error('Invalid host identity');
|
||||
text(value.label); publicKey(value.secret);
|
||||
return value as HostIdentity;
|
||||
fields(value, ['version', 'pairing', 'secret', 'registration']);
|
||||
if (value.version !== 2 || typeof value.secret !== 'string' || !/^[0-9a-f]{64}$/.test(value.secret)) throw Error('Invalid host identity; legacy OA is not infrastructure enrollment');
|
||||
const pairing = hostPairing(value.pairing);
|
||||
if (publicKey(value.secret) !== pairing.host) throw Error('Host key differs from pairing request');
|
||||
return { version: 2, pairing, secret: value.secret, registration: value.registration as HostRegistration | null };
|
||||
}
|
||||
/** Atomically import genuine authorization under the same installation lock as lifecycle setup. */
|
||||
export function enrollHostIdentity(directory: string, attestation: unknown, now = Math.floor(Date.now() / 1000)): void {
|
||||
/** Atomically import only an owner registration matching the exact retained request. */
|
||||
export function enrollHostIdentity(directory: string, registration: unknown, now = Math.floor(Date.now() / 1000)): void {
|
||||
locked(directory, () => {
|
||||
const identity = readHostIdentity(directory);
|
||||
identity.attestation = verifyHostAttestation(attestation, publicKey(identity.secret), identity.owner, now);
|
||||
identity.registration = verifyHostRegistration(registration, identity.pairing, now);
|
||||
writePrivate(join(directory, 'host-identity.json'), identity);
|
||||
});
|
||||
}
|
||||
/** Admission prerequisite, not a substitute for message/assignment authorization. */
|
||||
export function requireHostEnrollment(identity: HostIdentity, now = Math.floor(Date.now() / 1000)): HostAttestation {
|
||||
return verifyHostAttestation(identity.attestation, publicKey(identity.secret), identity.owner, now);
|
||||
/** Valid registration is NOT relay admission. Remains no-network until a narrow contract exists. */
|
||||
export function requireHostEnrollment(identity: HostIdentity, now = Math.floor(Date.now() / 1000)): never {
|
||||
verifyHostRegistration(identity.registration, identity.pairing, now);
|
||||
throw Error('Host registered locally; relay admission pending: broad NIP-OA owner delegation is prohibited');
|
||||
}
|
||||
|
||||
43
beehive/src/host-registration.ts
Normal file
43
beehive/src/host-registration.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { schnorr } from '@noble/curves/secp256k1';
|
||||
import { digest, fields, object, publicKey, text } from './protocol.ts';
|
||||
|
||||
/** Offline pairing request. This is private file-exchange material, not a relay event. */
|
||||
export type HostPairing = { version: 1; purpose: 'beehive-host-registration'; host: string; owner: string; label: string; relay: string; nonce: string };
|
||||
/** Owner endorsement of infrastructure only; never a NIP-OA tag or relay admission. */
|
||||
export type HostRegistration = { request: HostPairing; expires: number; signature: string };
|
||||
/** Validate untrusted pairing files before displaying or signing them. */
|
||||
export function hostPairing(value: unknown): HostPairing {
|
||||
const p = object(value);
|
||||
fields(p, ['version', 'purpose', 'host', 'owner', 'label', 'relay', 'nonce']);
|
||||
if (p.version !== 1 || p.purpose !== 'beehive-host-registration') throw Error('Invalid registration purpose');
|
||||
for (const key of ['host', 'owner', 'nonce']) if (typeof p[key] !== 'string' || !/^[0-9a-f]{64}$/.test(p[key] as string)) throw Error('Invalid pairing identity/nonce');
|
||||
if (p.host === p.owner) throw Error('Host must have independent identity');
|
||||
const label = text(p.label);
|
||||
if (label.length > 128 || /[\x00-\x1f\x7f]/.test(label)) throw Error('Invalid host label');
|
||||
const relay = text(p.relay); const url = new URL(relay);
|
||||
if (relay.length > 2048 || url.username || url.password || url.hash || url.search || !(url.protocol === 'wss:' || (url.protocol === 'ws:' && url.hostname === '127.0.0.1'))) throw Error('Invalid pairing relay');
|
||||
return { version: 1, purpose: 'beehive-host-registration', host: p.host as string, owner: p.owner as string, label, relay, nonce: p.nonce as string };
|
||||
}
|
||||
/** Full fingerprint binds host, owner, label, relay and this installation request. */
|
||||
export function pairingFingerprint(value: unknown): string {
|
||||
return digest(JSON.stringify(hostPairing(value))).toString('hex');
|
||||
}
|
||||
function preimage(request: HostPairing, expires: number): Buffer {
|
||||
if (!Number.isSafeInteger(expires) || expires <= 0) throw Error('Invalid registration expiration');
|
||||
return digest(`beehive:host-registration:v1:${pairingFingerprint(request)}:${expires}`);
|
||||
}
|
||||
/** Explicit existing-owner signing; the caller must not persist the owner secret. */
|
||||
export function registerHost(request: unknown, ownerSecret: string, expires: number): HostRegistration {
|
||||
const pairing = hostPairing(request);
|
||||
if (pairing.owner !== publicKey(ownerSecret)) throw Error('Wrong owner signer');
|
||||
return { request: pairing, expires, signature: Buffer.from(schnorr.sign(preimage(pairing, expires), ownerSecret)).toString('hex') };
|
||||
}
|
||||
/** Verifies only registration and locally observed expiration, not remote revocation or admission. */
|
||||
export function verifyHostRegistration(value: unknown, expected: unknown, now = Math.floor(Date.now() / 1000)): HostRegistration {
|
||||
const r = object(value); fields(r, ['request', 'expires', 'signature']);
|
||||
const request = hostPairing(r.request);
|
||||
if (pairingFingerprint(request) !== pairingFingerprint(expected)) throw Error('Registration does not match pairing request');
|
||||
if (typeof r.expires !== 'number' || !Number.isSafeInteger(now) || now >= r.expires || typeof r.signature !== 'string' || !/^[0-9a-f]{128}$/.test(r.signature)) throw Error('Invalid/expired host registration');
|
||||
if (!schnorr.verify(r.signature, preimage(request, r.expires), request.owner)) throw Error('Invalid owner registration signature');
|
||||
return { request, expires: r.expires, signature: r.signature };
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import { publicKey } from './protocol.ts';
|
||||
|
||||
/** Local secret entry only: disable terminal echo and never route key bytes to output.
|
||||
* Piped input is supported for protected automation; no secret argv or environment. */
|
||||
export async function readAgentSecret(): Promise<string> {
|
||||
export async function readAgentSecret(role: 'Agent' | 'Owner' = 'Agent'): Promise<string> {
|
||||
const muted = new Writable({ write(_chunk, _encoding, done) { done(); } });
|
||||
const ui = createInterface({ input: stdin, output: muted, terminal: Boolean(stdin.isTTY) });
|
||||
try {
|
||||
stdout.write('Agent private key (64 hex; hidden, never passed as an argument): ');
|
||||
stdout.write(`${role} private key (64 hex; hidden, never passed as an argument): `);
|
||||
const value = await ui.question('');
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(value)) throw Error('Invalid private key; expected 64 hex characters');
|
||||
const secret = value.toLowerCase();
|
||||
|
||||
63
beehive/test/enrollment-input.test.ts
Normal file
63
beehive/test/enrollment-input.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { newKey, publicKey } from '../src/protocol.ts';
|
||||
import { readHostIdentity, requireHostEnrollment } from '../src/host-identity.ts';
|
||||
import { pairingFingerprint } from '../src/host-registration.ts';
|
||||
|
||||
async function wizard(directory: string, steps: [string, string][]): Promise<string> {
|
||||
const env = { ...process.env };
|
||||
delete env.BUZZ_PRIVATE_KEY; delete env.BUZZ_AUTH_TAG; delete env.BUZZ_RELAY_URL;
|
||||
const child = spawn(process.execPath, [fileURLToPath(new URL('../src/cli.ts', import.meta.url)), 'setup', directory], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
let output = '', cursor = 0, next = 0;
|
||||
const timer = setTimeout(() => child.kill(), 10000);
|
||||
child.stdout.on('data', data => {
|
||||
output += data.toString();
|
||||
if (next < steps.length) {
|
||||
const [prompt, answer] = steps[next]!;
|
||||
const index = output.indexOf(prompt, cursor);
|
||||
if (index !== -1) { cursor = index + prompt.length; next++; child.stdin.write(`${answer}\n`); }
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', data => { output += data.toString(); });
|
||||
try {
|
||||
const code = await new Promise<number | null>((resolve, reject) => { child.on('error', reject); child.on('exit', resolve); });
|
||||
assert.equal(code, 0, output); assert.equal(next, steps.length, output);
|
||||
return output;
|
||||
} finally { clearTimeout(timer); child.stdin.end(); }
|
||||
}
|
||||
|
||||
test('actual standalone CLI private pairing, existing-owner approval and bootstrap return stay offline', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-'));
|
||||
const host = join(root, 'host'), ownerContext = join(root, 'owner-context');
|
||||
const request = join(root, 'request.json'), approval = join(root, 'approval.json');
|
||||
const ownerSecret = newKey();
|
||||
try {
|
||||
const created = await wizard(host, [
|
||||
['Enrollment [', 'create'], ['Host label: ', 'desktop'], ['Owner PUBLIC key (hex): ', publicKey(ownerSecret)],
|
||||
['Relay URL: ', 'wss://example.invalid'], ['NEW private pairing file: ', request],
|
||||
]);
|
||||
const original = readHostIdentity(host);
|
||||
assert.ok(created.includes(pairingFingerprint(original.pairing)));
|
||||
assert.equal(original.registration, null);
|
||||
const signed = await wizard(ownerContext, [
|
||||
['Enrollment [', 'approve'], ['Private pairing file: ', request], ['[yes/no]: ', 'yes'],
|
||||
['Registration expiration (Unix seconds): ', String(Math.floor(Date.now() / 1000) + 300)],
|
||||
['NEW private approval file: ', approval], ['Owner private key (64 hex; hidden, never passed as an argument): ', ownerSecret],
|
||||
]);
|
||||
assert.ok(signed.includes(pairingFingerprint(original.pairing)));
|
||||
assert.ok(!signed.includes(ownerSecret));
|
||||
const imported = await wizard(host, [['Enrollment [', 'import'], ['Private approval file: ', approval]]);
|
||||
assert.ok(imported.includes('Pending/no network'));
|
||||
const enrolled = readHostIdentity(host);
|
||||
assert.equal(enrolled.secret, original.secret);
|
||||
assert.deepEqual(enrolled.pairing, original.pairing);
|
||||
assert.throws(() => requireHostEnrollment(enrolled), /relay admission pending/);
|
||||
assert.deepEqual(readdirSync(host), ['host-identity.json']);
|
||||
for (const path of [request, approval, join(host, 'host-identity.json')]) assert.ok(!readFileSync(path, 'utf8').includes(ownerSecret));
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { registerHost, verifyHostRegistration } from '../src/host-registration.ts';
|
||||
import test from 'node:test';
|
||||
import { WebSocket } from 'ws';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -68,20 +69,29 @@ test('durable owner-public-only pending bootstrap, genuine enrollment and instal
|
||||
const directory = mkdtempSync(join(tmpdir(), 'beehive-host-key-'));
|
||||
try {
|
||||
const owner = generateSecretKey();
|
||||
const identity = bootstrapHostIdentity(directory, 'desktop', getPublicKey(owner));
|
||||
assert.equal(identity.attestation, null);
|
||||
const identity = bootstrapHostIdentity(directory, 'desktop', getPublicKey(owner), 'wss://example.invalid');
|
||||
assert.equal(identity.registration, null);
|
||||
assert.notEqual(identity.secret, hex(owner));
|
||||
assert.throws(() => requireHostEnrollment(identity));
|
||||
assert.equal(statSync(join(directory, 'host-identity.json')).mode & 0o777, 0o600);
|
||||
assert.deepEqual(readHostIdentity(directory), identity);
|
||||
assert.throws(() => bootstrapHostIdentity(directory, 'other', identity.owner));
|
||||
assert.throws(() => bootstrapHostIdentity(directory, 'other', identity.pairing.owner, 'wss://example.invalid'));
|
||||
const tag = attestHost(hex(owner), getPublicKey(Buffer.from(identity.secret, 'hex')), 'kind=1059');
|
||||
mkdirSync(join(directory, 'host.lock'));
|
||||
assert.throws(() => enrollHostIdentity(directory, tag));
|
||||
rmdirSync(join(directory, 'host.lock'));
|
||||
assert.throws(() => enrollHostIdentity(directory, attestHost(hex(generateSecretKey()), getPublicKey(Buffer.from(identity.secret, 'hex')), '')));
|
||||
enrollHostIdentity(directory, tag);
|
||||
assert.deepEqual(requireHostEnrollment(readHostIdentity(directory)), tag);
|
||||
assert.throws(() => enrollHostIdentity(directory, tag), 'broad OA must never enroll infrastructure');
|
||||
const registration = registerHost(identity.pairing, hex(owner), Math.floor(Date.now() / 1000) + 60);
|
||||
for (const key of ['host', 'owner', 'label', 'relay', 'nonce'] as const) {
|
||||
const changed = { ...identity.pairing, [key]: key === 'label' ? 'other' : key === 'relay' ? 'wss://other.invalid' : hex(generateSecretKey()) };
|
||||
assert.throws(() => verifyHostRegistration(registration, changed));
|
||||
}
|
||||
assert.throws(() => verifyHostRegistration({ ...registration, expires: registration.expires + 1 }, identity.pairing));
|
||||
assert.throws(() => verifyHostRegistration(registration, identity.pairing, registration.expires));
|
||||
enrollHostIdentity(directory, registration);
|
||||
assert.deepEqual(readHostIdentity(directory).registration, registration);
|
||||
assert.throws(() => requireHostEnrollment(readHostIdentity(directory)), /relay admission pending/);
|
||||
} finally { rmSync(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user