feat(broker): verify ed25519 hello signature against member pubkey
WS handshake is now authenticated end-to-end. The broker proves that
every connected peer actually holds the secret key for the pubkey
they claim as identity — not just that they know the pubkey.
wire format change:
{type:"hello", meshId, memberId, pubkey, sessionId, pid, cwd,
timestamp, signature}
where signature = ed25519_sign(canonical, secretKey)
and canonical = `${meshId}|${memberId}|${pubkey}|${timestamp}`
broker verifies on every hello:
1. timestamp within ±60s of broker clock → else close(1008, timestamp_skew)
2. pubkey is 64 hex chars, signature is 128 hex chars → else malformed
3. crypto_sign_verify_detached(signature, canonical, pubkey) → else bad_signature
4. (existing) mesh.member row exists for (meshId, pubkey) → else unauthorized
All rejection paths close the WS with code 1008 + structured error
message + metrics counter increment (connections_rejected_total by
reason).
new modules:
- apps/broker/src/crypto.ts: canonicalHello, verifyHelloSignature,
HELLO_SKEW_MS constant
- apps/cli/src/crypto/hello-sig.ts: matching signHello helper
clients updated:
- apps/cli/src/ws/client.ts: signs hello before send
- apps/broker/scripts/{peer-a,peer-b}.ts (smoke-test): sign hellos
with seed-provided secret keys
new regression tests — tests/hello-signature.test.ts (7):
- valid signature accepted
- bad signature (signed with wrong key) rejected
- timestamp too old rejected (>60s)
- timestamp too far in future rejected (>60s)
- tampered canonical field (different meshId at verify time) rejected
- malformed hex pubkey rejected
- malformed signature length rejected
verified live:
- apps/broker/scripts/smoke-test.sh: full hello+ack+send+push flow
- apps/cli/scripts/roundtrip.ts: signed hello + encrypted message
- 55/55 tests pass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,12 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
|
import sodium from "libsodium-wrappers";
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
|
|
||||||
const seed = JSON.parse(readFileSync("/tmp/smoke-seed.json", "utf-8")) as {
|
const seed = JSON.parse(readFileSync("/tmp/smoke-seed.json", "utf-8")) as {
|
||||||
meshId: string;
|
meshId: string;
|
||||||
peerA: { memberId: string; pubkey: string };
|
peerA: { memberId: string; pubkey: string; secretKey: string };
|
||||||
peerB: { memberId: string; pubkey: string };
|
peerB: { memberId: string; pubkey: string; secretKey: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const BROKER = process.env.BROKER_WS_URL ?? "ws://localhost:7900/ws";
|
const BROKER = process.env.BROKER_WS_URL ?? "ws://localhost:7900/ws";
|
||||||
@@ -21,8 +22,17 @@ const ws = new WebSocket(BROKER);
|
|||||||
|
|
||||||
let helloAcked = false;
|
let helloAcked = false;
|
||||||
|
|
||||||
ws.on("open", () => {
|
ws.on("open", async () => {
|
||||||
console.log("[peer-a] connected, sending hello");
|
await sodium.ready;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const canonical = `${seed.meshId}|${seed.peerA.memberId}|${seed.peerA.pubkey}|${timestamp}`;
|
||||||
|
const signature = sodium.to_hex(
|
||||||
|
sodium.crypto_sign_detached(
|
||||||
|
sodium.from_string(canonical),
|
||||||
|
sodium.from_hex(seed.peerA.secretKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log("[peer-a] connected, sending signed hello");
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "hello",
|
type: "hello",
|
||||||
@@ -32,8 +42,8 @@ ws.on("open", () => {
|
|||||||
sessionId: "peer-a-session",
|
sessionId: "peer-a-session",
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
cwd: "/tmp/peer-a",
|
cwd: "/tmp/peer-a",
|
||||||
signature: "stub",
|
timestamp,
|
||||||
nonce: "stub",
|
signature,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,12 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
|
import sodium from "libsodium-wrappers";
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
|
|
||||||
const seed = JSON.parse(readFileSync("/tmp/smoke-seed.json", "utf-8")) as {
|
const seed = JSON.parse(readFileSync("/tmp/smoke-seed.json", "utf-8")) as {
|
||||||
meshId: string;
|
meshId: string;
|
||||||
peerA: { memberId: string; pubkey: string };
|
peerA: { memberId: string; pubkey: string; secretKey: string };
|
||||||
peerB: { memberId: string; pubkey: string };
|
peerB: { memberId: string; pubkey: string; secretKey: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const BROKER = process.env.BROKER_WS_URL ?? "ws://localhost:7900/ws";
|
const BROKER = process.env.BROKER_WS_URL ?? "ws://localhost:7900/ws";
|
||||||
@@ -21,8 +22,17 @@ const ws = new WebSocket(BROKER);
|
|||||||
|
|
||||||
let received = false;
|
let received = false;
|
||||||
|
|
||||||
ws.on("open", () => {
|
ws.on("open", async () => {
|
||||||
console.log("[peer-b] connected, sending hello");
|
await sodium.ready;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const canonical = `${seed.meshId}|${seed.peerB.memberId}|${seed.peerB.pubkey}|${timestamp}`;
|
||||||
|
const signature = sodium.to_hex(
|
||||||
|
sodium.crypto_sign_detached(
|
||||||
|
sodium.from_string(canonical),
|
||||||
|
sodium.from_hex(seed.peerB.secretKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
console.log("[peer-b] connected, sending signed hello");
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "hello",
|
type: "hello",
|
||||||
@@ -32,8 +42,8 @@ ws.on("open", () => {
|
|||||||
sessionId: "peer-b-session",
|
sessionId: "peer-b-session",
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
cwd: "/tmp/peer-b",
|
cwd: "/tmp/peer-b",
|
||||||
signature: "stub",
|
timestamp,
|
||||||
nonce: "stub",
|
signature,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
79
apps/broker/src/crypto.ts
Normal file
79
apps/broker/src/crypto.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Broker-side ed25519 verification helpers.
|
||||||
|
*
|
||||||
|
* Used to authenticate the WS hello handshake: clients sign a canonical
|
||||||
|
* byte string with their mesh.member.peerPubkey's secret key, broker
|
||||||
|
* verifies with the claimed pubkey, then cross-checks the pubkey is a
|
||||||
|
* current member of the claimed mesh.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import sodium from "libsodium-wrappers";
|
||||||
|
|
||||||
|
let ready = false;
|
||||||
|
async function ensureSodium(): Promise<typeof sodium> {
|
||||||
|
if (!ready) {
|
||||||
|
await sodium.ready;
|
||||||
|
ready = true;
|
||||||
|
}
|
||||||
|
return sodium;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canonical hello bytes: clients sign this, broker verifies this. */
|
||||||
|
export function canonicalHello(
|
||||||
|
meshId: string,
|
||||||
|
memberId: string,
|
||||||
|
pubkey: string,
|
||||||
|
timestamp: number,
|
||||||
|
): string {
|
||||||
|
return `${meshId}|${memberId}|${pubkey}|${timestamp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HELLO_SKEW_MS = 60_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a hello's ed25519 signature + timestamp skew.
|
||||||
|
* Returns { ok: true } on success, or { ok: false, reason } describing
|
||||||
|
* which check failed (for structured error response).
|
||||||
|
*/
|
||||||
|
export async function verifyHelloSignature(args: {
|
||||||
|
meshId: string;
|
||||||
|
memberId: string;
|
||||||
|
pubkey: string;
|
||||||
|
timestamp: number;
|
||||||
|
signature: string;
|
||||||
|
now?: number;
|
||||||
|
}): Promise<
|
||||||
|
| { ok: true }
|
||||||
|
| { ok: false; reason: "timestamp_skew" | "bad_signature" | "malformed" }
|
||||||
|
> {
|
||||||
|
const now = args.now ?? Date.now();
|
||||||
|
if (
|
||||||
|
!Number.isFinite(args.timestamp) ||
|
||||||
|
Math.abs(now - args.timestamp) > HELLO_SKEW_MS
|
||||||
|
) {
|
||||||
|
return { ok: false, reason: "timestamp_skew" };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!/^[0-9a-f]{64}$/i.test(args.pubkey) ||
|
||||||
|
!/^[0-9a-f]{128}$/i.test(args.signature)
|
||||||
|
) {
|
||||||
|
return { ok: false, reason: "malformed" };
|
||||||
|
}
|
||||||
|
const s = await ensureSodium();
|
||||||
|
try {
|
||||||
|
const canonical = canonicalHello(
|
||||||
|
args.meshId,
|
||||||
|
args.memberId,
|
||||||
|
args.pubkey,
|
||||||
|
args.timestamp,
|
||||||
|
);
|
||||||
|
const ok = s.crypto_sign_verify_detached(
|
||||||
|
s.from_hex(args.signature),
|
||||||
|
s.from_string(canonical),
|
||||||
|
s.from_hex(args.pubkey),
|
||||||
|
);
|
||||||
|
return ok ? { ok: true } : { ok: false, reason: "bad_signature" };
|
||||||
|
} catch {
|
||||||
|
return { ok: false, reason: "malformed" };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import { metrics, metricsToText } from "./metrics";
|
|||||||
import { TokenBucket } from "./rate-limit";
|
import { TokenBucket } from "./rate-limit";
|
||||||
import { isDbHealthy, startDbHealth, stopDbHealth } from "./db-health";
|
import { isDbHealthy, startDbHealth, stopDbHealth } from "./db-health";
|
||||||
import { buildInfo } from "./build-info";
|
import { buildInfo } from "./build-info";
|
||||||
|
import { verifyHelloSignature } from "./crypto";
|
||||||
|
|
||||||
const PORT = env.BROKER_PORT;
|
const PORT = env.BROKER_PORT;
|
||||||
const WS_PATH = "/ws";
|
const WS_PATH = "/ws";
|
||||||
@@ -364,6 +365,26 @@ async function handleHello(
|
|||||||
ws.close(1008, "capacity");
|
ws.close(1008, "capacity");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
// Signature + skew check. Proves the client holds the secret key
|
||||||
|
// for the pubkey they're claiming as identity.
|
||||||
|
const sig = await verifyHelloSignature({
|
||||||
|
meshId: hello.meshId,
|
||||||
|
memberId: hello.memberId,
|
||||||
|
pubkey: hello.pubkey,
|
||||||
|
timestamp: hello.timestamp,
|
||||||
|
signature: hello.signature,
|
||||||
|
});
|
||||||
|
if (!sig.ok) {
|
||||||
|
metrics.connectionsRejected.inc({ reason: sig.reason });
|
||||||
|
log.warn("hello sig rejected", {
|
||||||
|
reason: sig.reason,
|
||||||
|
mesh_id: hello.meshId,
|
||||||
|
pubkey: hello.pubkey?.slice(0, 12),
|
||||||
|
});
|
||||||
|
sendError(ws, sig.reason, `hello rejected: ${sig.reason}`);
|
||||||
|
ws.close(1008, sig.reason);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const member = await findMemberByPubkey(hello.meshId, hello.pubkey);
|
const member = await findMemberByPubkey(hello.meshId, hello.pubkey);
|
||||||
if (!member) {
|
if (!member) {
|
||||||
metrics.connectionsRejected.inc({ reason: "unauthorized" });
|
metrics.connectionsRejected.inc({ reason: "unauthorized" });
|
||||||
|
|||||||
@@ -55,8 +55,11 @@ export interface WSHelloMessage {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
pid: number;
|
pid: number;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
signature: string; // ed25519 over (meshId||memberId||sessionId||nonce)
|
/** ms epoch; broker rejects if outside ±60s of its own clock. */
|
||||||
nonce: string;
|
timestamp: number;
|
||||||
|
/** ed25519 signature (hex) over the canonical hello bytes:
|
||||||
|
* `${meshId}|${memberId}|${pubkey}|${timestamp}` */
|
||||||
|
signature: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Client → broker: send an E2E-encrypted envelope to a target. */
|
/** Client → broker: send an E2E-encrypted envelope to a target. */
|
||||||
|
|||||||
159
apps/broker/tests/hello-signature.test.ts
Normal file
159
apps/broker/tests/hello-signature.test.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* Hello signature verification — unit tests on the verifyHelloSignature
|
||||||
|
* function directly. Covers valid signature, bad signature, timestamp
|
||||||
|
* skew, and cross-member attacks (signing with wrong key).
|
||||||
|
*
|
||||||
|
* Integration WS-level testing happens implicitly via the smoke-test
|
||||||
|
* scripts (apps/broker/scripts/smoke-test.sh, apps/cli/scripts/
|
||||||
|
* roundtrip.ts), which exercise the full hello handshake.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { beforeAll, describe, expect, test } from "vitest";
|
||||||
|
import sodium from "libsodium-wrappers";
|
||||||
|
import {
|
||||||
|
canonicalHello,
|
||||||
|
verifyHelloSignature,
|
||||||
|
HELLO_SKEW_MS,
|
||||||
|
} from "../src/crypto";
|
||||||
|
|
||||||
|
interface Keypair {
|
||||||
|
publicKey: string;
|
||||||
|
secretKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function makeKeypair(): Promise<Keypair> {
|
||||||
|
await sodium.ready;
|
||||||
|
const kp = sodium.crypto_sign_keypair();
|
||||||
|
return {
|
||||||
|
publicKey: sodium.to_hex(kp.publicKey),
|
||||||
|
secretKey: sodium.to_hex(kp.privateKey),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sign(canonical: string, secretKeyHex: string): string {
|
||||||
|
return sodium.to_hex(
|
||||||
|
sodium.crypto_sign_detached(
|
||||||
|
sodium.from_string(canonical),
|
||||||
|
sodium.from_hex(secretKeyHex),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("verifyHelloSignature", () => {
|
||||||
|
let kp: Keypair;
|
||||||
|
beforeAll(async () => {
|
||||||
|
kp = await makeKeypair();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("valid signature accepted", async () => {
|
||||||
|
const meshId = "mesh-x";
|
||||||
|
const memberId = "member-y";
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const canonical = canonicalHello(meshId, memberId, kp.publicKey, timestamp);
|
||||||
|
const signature = sign(canonical, kp.secretKey);
|
||||||
|
const result = await verifyHelloSignature({
|
||||||
|
meshId,
|
||||||
|
memberId,
|
||||||
|
pubkey: kp.publicKey,
|
||||||
|
timestamp,
|
||||||
|
signature,
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bad signature rejected", async () => {
|
||||||
|
const meshId = "mesh-x";
|
||||||
|
const memberId = "member-y";
|
||||||
|
const timestamp = Date.now();
|
||||||
|
// Sign with a DIFFERENT key than the one we claim
|
||||||
|
const otherKp = await makeKeypair();
|
||||||
|
const canonical = canonicalHello(meshId, memberId, kp.publicKey, timestamp);
|
||||||
|
const signature = sign(canonical, otherKp.secretKey);
|
||||||
|
const result = await verifyHelloSignature({
|
||||||
|
meshId,
|
||||||
|
memberId,
|
||||||
|
pubkey: kp.publicKey, // claim kp's identity
|
||||||
|
timestamp,
|
||||||
|
signature, // but signed with otherKp — mismatch
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) expect(result.reason).toBe("bad_signature");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("timestamp too old rejected", async () => {
|
||||||
|
const timestamp = Date.now() - HELLO_SKEW_MS - 1000;
|
||||||
|
const canonical = canonicalHello("m", "mem", kp.publicKey, timestamp);
|
||||||
|
const signature = sign(canonical, kp.secretKey);
|
||||||
|
const result = await verifyHelloSignature({
|
||||||
|
meshId: "m",
|
||||||
|
memberId: "mem",
|
||||||
|
pubkey: kp.publicKey,
|
||||||
|
timestamp,
|
||||||
|
signature,
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) expect(result.reason).toBe("timestamp_skew");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("timestamp too far in future rejected", async () => {
|
||||||
|
const timestamp = Date.now() + HELLO_SKEW_MS + 1000;
|
||||||
|
const canonical = canonicalHello("m", "mem", kp.publicKey, timestamp);
|
||||||
|
const signature = sign(canonical, kp.secretKey);
|
||||||
|
const result = await verifyHelloSignature({
|
||||||
|
meshId: "m",
|
||||||
|
memberId: "mem",
|
||||||
|
pubkey: kp.publicKey,
|
||||||
|
timestamp,
|
||||||
|
signature,
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) expect(result.reason).toBe("timestamp_skew");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tampered canonical field fails verification", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
// Sign over one meshId, claim a different one at verify time
|
||||||
|
const canonical = canonicalHello(
|
||||||
|
"original-mesh",
|
||||||
|
"mem",
|
||||||
|
kp.publicKey,
|
||||||
|
timestamp,
|
||||||
|
);
|
||||||
|
const signature = sign(canonical, kp.secretKey);
|
||||||
|
const result = await verifyHelloSignature({
|
||||||
|
meshId: "different-mesh",
|
||||||
|
memberId: "mem",
|
||||||
|
pubkey: kp.publicKey,
|
||||||
|
timestamp,
|
||||||
|
signature,
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) expect(result.reason).toBe("bad_signature");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("malformed hex pubkey rejected", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const result = await verifyHelloSignature({
|
||||||
|
meshId: "m",
|
||||||
|
memberId: "mem",
|
||||||
|
pubkey: "not-hex",
|
||||||
|
timestamp,
|
||||||
|
signature: "a".repeat(128),
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) expect(result.reason).toBe("malformed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("malformed signature length rejected", async () => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const result = await verifyHelloSignature({
|
||||||
|
meshId: "m",
|
||||||
|
memberId: "mem",
|
||||||
|
pubkey: kp.publicKey,
|
||||||
|
timestamp,
|
||||||
|
signature: "abc123", // wrong length
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) expect(result.reason).toBe("malformed");
|
||||||
|
});
|
||||||
|
});
|
||||||
28
apps/cli/src/crypto/hello-sig.ts
Normal file
28
apps/cli/src/crypto/hello-sig.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Client-side signing of the WS hello handshake.
|
||||||
|
*
|
||||||
|
* Canonical bytes: `${meshId}|${memberId}|${pubkey}|${timestamp}` —
|
||||||
|
* MUST match the broker's `canonicalHello()` exactly. Any mismatch
|
||||||
|
* (delimiter, field order, whitespace) produces a bad_signature reject.
|
||||||
|
*
|
||||||
|
* Uses the full ed25519 secret key (64 bytes) that libsodium returns
|
||||||
|
* from crypto_sign_keypair — seed || pubkey layout.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ensureSodium } from "./keypair";
|
||||||
|
|
||||||
|
export async function signHello(
|
||||||
|
meshId: string,
|
||||||
|
memberId: string,
|
||||||
|
pubkey: string,
|
||||||
|
secretKeyHex: string,
|
||||||
|
): Promise<{ timestamp: number; signature: string }> {
|
||||||
|
const s = await ensureSodium();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const canonical = `${meshId}|${memberId}|${pubkey}|${timestamp}`;
|
||||||
|
const sig = s.crypto_sign_detached(
|
||||||
|
s.from_string(canonical),
|
||||||
|
s.from_hex(secretKeyHex),
|
||||||
|
);
|
||||||
|
return { timestamp, signature: s.to_hex(sig) };
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
encryptDirect,
|
encryptDirect,
|
||||||
isDirectTarget,
|
isDirectTarget,
|
||||||
} from "../crypto/envelope";
|
} from "../crypto/envelope";
|
||||||
|
import { signHello } from "../crypto/hello-sig";
|
||||||
|
|
||||||
export type Priority = "now" | "next" | "low";
|
export type Priority = "now" | "next" | "low";
|
||||||
export type ConnStatus = "connecting" | "open" | "closed" | "reconnecting";
|
export type ConnStatus = "connecting" | "open" | "closed" | "reconnecting";
|
||||||
@@ -97,21 +98,36 @@ export class BrokerClient {
|
|||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const onOpen = (): void => {
|
const onOpen = async (): Promise<void> => {
|
||||||
this.debug("ws open → sending hello");
|
this.debug("ws open → signing + sending hello");
|
||||||
ws.send(
|
try {
|
||||||
JSON.stringify({
|
const { timestamp, signature } = await signHello(
|
||||||
type: "hello",
|
this.mesh.meshId,
|
||||||
meshId: this.mesh.meshId,
|
this.mesh.memberId,
|
||||||
memberId: this.mesh.memberId,
|
this.mesh.pubkey,
|
||||||
pubkey: this.mesh.pubkey,
|
this.mesh.secretKey,
|
||||||
sessionId: `${process.pid}-${Date.now()}`,
|
);
|
||||||
pid: process.pid,
|
ws.send(
|
||||||
cwd: process.cwd(),
|
JSON.stringify({
|
||||||
signature: "stub", // libsodium sign_detached lands in Step 18
|
type: "hello",
|
||||||
nonce: randomNonce(),
|
meshId: this.mesh.meshId,
|
||||||
}),
|
memberId: this.mesh.memberId,
|
||||||
);
|
pubkey: this.mesh.pubkey,
|
||||||
|
sessionId: `${process.pid}-${Date.now()}`,
|
||||||
|
pid: process.pid,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
timestamp,
|
||||||
|
signature,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`hello sign failed: ${e instanceof Error ? e.message : e}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Arm the hello_ack timeout.
|
// Arm the hello_ack timeout.
|
||||||
this.helloTimer = setTimeout(() => {
|
this.helloTimer = setTimeout(() => {
|
||||||
this.debug("hello_ack timeout");
|
this.debug("hello_ack timeout");
|
||||||
|
|||||||
Reference in New Issue
Block a user