Noise protocol initiator

This commit is contained in:
Tony Garnock-Jones 2023-01-31 15:19:29 +01:00
parent c60bd13cdc
commit 2608e259ba
3 changed files with 171 additions and 53 deletions

View File

@ -13,7 +13,7 @@
}, },
"scripts": { "scripts": {
"prepare": "yarn regenerate && yarn compile && yarn rollup", "prepare": "yarn regenerate && yarn compile && yarn rollup",
"regenerate": "rm -rf ./src/gen && preserves-schema-ts --module EntityRef=@syndicate-lang/core --module transportAddress=@syndicate-lang/core:Schemas.transportAddress --module sturdy=@syndicate-lang/core:Schemas.sturdy --xref ../../node_modules/@syndicate-lang/core/protocols/schemas --output ./src/gen ./protocols/schemas", "regenerate": "rm -rf ./src/gen && preserves-schema-ts --module EntityRef=@syndicate-lang/core --module transportAddress=@syndicate-lang/core:Schemas.transportAddress --module noise=@syndicate-lang/core:Schemas.noise --module sturdy=@syndicate-lang/core:Schemas.sturdy --xref ../../node_modules/@syndicate-lang/core/protocols/schemas --output ./src/gen ./protocols/schemas",
"regenerate:watch": "yarn regenerate --watch", "regenerate:watch": "yarn regenerate --watch",
"compile": "syndicate-tsc", "compile": "syndicate-tsc",
"compile:watch": "syndicate-tsc -w --verbose --intermediate-directory src.ts", "compile:watch": "syndicate-tsc -w --verbose --intermediate-directory src.ts",
@ -29,7 +29,8 @@
"dependencies": { "dependencies": {
"@preserves/core": ">=0.20.2", "@preserves/core": ">=0.20.2",
"@preserves/schema": ">=0.21.2", "@preserves/schema": ">=0.21.2",
"@syndicate-lang/core": "^0.11.9" "@syndicate-lang/core": "^0.11.9",
"salty-crypto": "0.2"
}, },
"devDependencies": { "devDependencies": {
"@syndicate-lang/ts-plugin": "^0.11.10", "@syndicate-lang/ts-plugin": "^0.11.10",

View File

@ -1,6 +1,6 @@
version 1 . version 1 .
Resolved = <resolved @addr RelayAddress @sturdyref sturdy.SturdyRef @resolved #!any> . Resolved = <resolved @route noise.Route @addr RelayAddress @resolved #!any> .
ViaRelay = <via-relay @addr RelayAddress @assertion any> . ViaRelay = <via-relay @addr RelayAddress @assertion any> .
ForceRelayDisconnect = <force-relay-disconnect @addr RelayAddress> . ForceRelayDisconnect = <force-relay-disconnect @addr RelayAddress> .

View File

@ -1,71 +1,188 @@
/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com> /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { QuasiValue as Q, Ref, Relay, Turn, Supervisor, SupervisorRestartPolicy, Observe, Schemas, assertionFacetObserver, isEmbedded } from "@syndicate-lang/core"; import { QuasiValue as Q, Assertion, Bytes, Ref, Relay, Turn, Supervisor, SupervisorRestartPolicy, Observe, Schemas, assertionFacetObserver, canonicalEncode, isEmbedded, underlying } from "@syndicate-lang/core";
import * as G from "./gen/ws"; import * as G from "./gen/ws";
export * from "./gen/ws"; export * from "./gen/ws";
import * as N from "@syndicate-lang/core/lib/gen/noise";
export * as Noise from "@syndicate-lang/core/lib/gen/noise";
import * as SaltyCrypto from 'salty-crypto';
export function boot(ds: Ref, debug: boolean = false) { export function boot(ds: Ref, debug: boolean = false) {
spawn named 'wsRelay' { spawn named 'wsRelay' {
at ds { at ds {
during Observe({ "pattern": :pattern G.Resolved({ during Observe({ "pattern": :pattern G.Resolved({
"addr": \$addrPatValue, "route": \$routePatValue,
"sturdyref": \$sturdyRefPatValue, "addr": \_,
"resolved": \Q.bind(), "resolved": \Q.bind(),
}) }) => { }) }) => {
const addr = Q.drop_lit(addrPatValue, G.toRelayAddress); const route = Q.drop_lit(routePatValue, N.toRoute);
const sturdyref = Q.drop_lit(sturdyRefPatValue, Schemas.sturdy.toSturdyRef); if (!route) return;
if (addr && sturdyref) { let addr0: G.RelayAddress | undefined;
assert G.ViaRelay({ route.transports.forEach(t => addr0 = addr0 ?? G.toRelayAddress(t));
if (!addr0) return;
const addr = addr0;
wsConnect(addr, e => resolve(e, route.steps, e => {
assert G.Resolved({
"route": route,
"addr": addr, "addr": addr,
"assertion": Schemas.gatekeeper.fromResolve(Schemas.gatekeeper.Resolve({ "resolved": e,
"sturdyref": sturdyref,
"observer": create assertionFacetObserver(e => {
if (isEmbedded(e)) {
assert G.fromResolved(G.Resolved({
"addr": addr,
"sturdyref": sturdyref,
"resolved": e.embeddedValue,
}));
}
}),
})),
}); });
}));
at ds {
stop on message G.ForceRelayDisconnect(addr);
} }
} }
}
during G.ViaRelay({ "addr": $addrValue }) => spawn named ['wsRelay', addrValue] { function wsConnect(addr: G.RelayAddress, k: (e: Ref) => void) {
let counter = 0; console.log('wsConnect', addr);
new Supervisor({ let counter = 0;
restartPolicy: SupervisorRestartPolicy.ALWAYS, new Supervisor({
}, () => ['wsRelay', addrValue, counter++], () => { restartPolicy: SupervisorRestartPolicy.ALWAYS,
const addr = G.toRelayAddress(addrValue); }, () => ['wsRelay', G.fromRelayAddress(addr), counter++], () => {
if (addr !== void 0) { console.log('try!', counter - 1);
stop on message G.ForceRelayDisconnect(addrValue); const facet = Turn.activeFacet;
const facet = Turn.activeFacet; const ws = new WebSocket(addr.url);
const ws = new WebSocket(addr.url); facet.preventInertCheck();
ws.binaryType = 'arraybuffer'; on stop {
ws.onclose = () => facet.turn(() => { stop {} }); console.log('on stop triggered');
ws.onerror = () => facet.turn(() => ws.close();
Turn.active.crash(new Error("WebSocket error"))); }
ws.onopen = () => facet.turn(() => { ws.binaryType = 'arraybuffer';
const relay = new Relay.Relay({ ws.onclose = () => facet.turn(() => { stop {} });
debug, ws.onerror = () => facet.turn(() =>
trustPeer: true, Turn.active.crash(new Error("WebSocket error")));
packetWriter: bs => ws.send(bs), ws.onopen = () => facet.turn(() => {
setup(r: Relay.Relay) { console.log('hey cool');
ws.onmessage = e => facet.turn(() => const relay = new Relay.Relay({
r.accept(new Uint8Array(e.data))); debug,
}, trustPeer: true,
initialOid: 0, packetWriter: bs => ws.send(bs),
}); setup(r: Relay.Relay) {
during G.ViaRelay({ "addr": addrValue, "assertion": $a }) => { ws.onmessage = e => facet.turn(() =>
at relay.peer! { r.accept(new Uint8Array(e.data)));
assert a; },
} initialOid: 0,
} });
}); k(relay.peer!);
});
});
}
function resolve(e: Ref, steps: N.RouteStep<Ref>[], k: (e: Ref) => void) {
console.log('resolve', e, steps);
if (steps.length === 0) {
k(e);
} else {
const [step, ...more] = steps;
switch (step._variant) {
case "NoiseStep": {
noiseConnect(e, step.value.spec, e => resolve(e, more, k));
break;
} }
case "GatekeeperStep": {
at e {
assert Schemas.gatekeeper.Resolve({
"sturdyref": step.value,
"observer": create assertionFacetObserver(e => {
if (isEmbedded(e)) {
resolve(e.embeddedValue, more, k);
}
}),
});
}
break;
}
default: ((_: never) => {})(step);
}
}
}
function noiseConnect(e: Ref, spec: N.NoiseSpec<Ref>, k: (e: Ref) => void) {
const noiseSessionFacet = Turn.activeFacet;
const algorithms = SaltyCrypto.Noise_25519_ChaChaPoly_BLAKE2s;
const protocol =
spec.protocol._variant === "present" ? spec.protocol.protocol :
spec.protocol._variant === "absent" ? N.fromDefaultProtocol(N.DefaultProtocol()) as string :
(() => { throw new Error("Invalid noise protocol name"); })();
const patternName = SaltyCrypto.matchPattern(algorithms, protocol);
if (patternName === null) throw new Error("Unsupported protocol " + protocol);
const preSharedKeys =
spec.preSharedKeys._variant === "present" ? spec.preSharedKeys.preSharedKeys :
spec.preSharedKeys._variant === "absent" ? [] :
(() => { throw new Error("Invalid pre-shared keys"); })();
const prologue = underlying(canonicalEncode(spec.service));
const H = new SaltyCrypto.Handshake(
algorithms,
patternName,
'initiator',
{
prologue,
remoteStaticPublicKey: underlying(spec.key),
preSharedKeys: preSharedKeys.map(underlying),
});
console.log('HANDSHAKE', H);
let transportState: SaltyCrypto.TransportState | null = null;
let responderSession: Ref | null = null;
let relay: Relay.Relay | null = null;
function maybeTransition(s: SaltyCrypto.TransportState | null) {
if (transportState !== null) {
throw new Error("Unexpected double-transition to transport state");
}
transportState = s;
if (transportState !== null) {
k(new Relay.Relay({
debug,
trustPeer: true,
packetWriter: bs => noiseSessionFacet.turn(() => {
const fragments = transportState!.send.encrypt_large(bs).map(Bytes.from);
at responderSession! {
send message ((fragments.length === 1)
? N.Packet.complete(fragments[0])
: N.Packet.fragmented(fragments));
}
}),
setup(r: Relay.Relay) {
relay = r;
},
initialOid: 0,
}).peer!);
}
}
at e {
assert N.Connect({
"serviceSelector": spec.service,
"initiatorSession": create ({
... assertionFacetObserver(e => {
const accept = N.asAccept(e);
responderSession = accept.responderSession;
const { packet, finished } = H.writeMessage(new Uint8Array());
at responderSession {
send message Bytes.from(packet);
}
maybeTransition(finished);
}),
message(body: Assertion) {
const p = N.asPacket(body);
if (transportState) {
const packet = transportState.recv.decrypt_large(
p._variant === 'complete'
? [underlying(p.value)]
: p.value.map(underlying));
relay!.accept(packet);
} else {
if (p._variant !== 'complete') {
throw new Error("Unexpected fragmentation in handshake");
}
const { message, finished } = H.readMessage(underlying(p.value));
if (message.byteLength !== 0) {
throw new Error("Unexpected payload during handshake");
}
maybeTransition(finished);
}
},
}),
}); });
} }
} }