Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 167 additions & 123 deletions CHANGELOG.md

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions bin/sym.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const { logSafe } = require('../lib/wire-identity'); // peer-chosen text on the
* sym keys <name> resolve <nodeId> <key> # Operator: bind nodeId to key (pinned), clearing its conflicts
* sym keys <name> reset-floor <nodeId> # Operator: let a Legacy Import route be used again
* sym node export <name> --out <file> (--to-host <x25519 pub> | --passphrase-env VAR --allow-copies)
* sym node abort-export (<name> | --node-id <id>) abandon an export that produced no complete bundle
* sym node import <file> --expect-node <id> [--expect-key <k> | --expect-fingerprint sha256:…] [--passphrase-env VAR]
* sym node host-key # This host's X25519 key for receiving node bundles
* sym logs # Tail daemon logs
Expand Down Expand Up @@ -176,7 +177,17 @@ function cmdNode() {
// Sealed to the target host by default; a passphrase bundle opens on any host with the passphrase,
// so it is a copy, and needs --allow-copies (final re-review, Finding 7b).
const r = relocation.exportNode({ name: args[2], out: flagValue('--out'), passphrase: passphrase(), toHostKey: flagValue('--to-host') || undefined, copyable: args.includes('--allow-copies') });
console.log(`exported ${r.name} (${r.nodeId}, ${r.fingerprint}) to ${r.out}: ${r.files} file(s). It is tombstoned here and will not start on this host again.${flagValue('--to-host') ? '' : ' The bundle is a passphrase bundle: any host with the passphrase can import it.'}`);
console.log(r.resumed
? `finished the export of ${r.name} (${r.nodeId}) to ${r.out}: its bundle was complete; the key is removed from this host.`
: `exported ${r.name} (${r.nodeId}, ${r.fingerprint}) to ${r.out}: ${r.files} file(s). It is tombstoned here and will not start on this host again.${flagValue('--to-host') ? '' : ' The bundle is a passphrase bundle: any host with the passphrase can import it.'}`);
return;
}
if (sub === 'abort-export') {
// Abandon an export that began and produced no complete bundle: the tombstone goes and the node starts here again.
const r = relocation.abortExport({ name: args[2] && !args[2].startsWith('--') ? args[2] : undefined, nodeId: flagValue('--node-id') || undefined });
console.log(r.aborted
? `the export of ${r.nodeId}${r.bundle ? ` to ${r.bundle}` : ''} is abandoned: no complete bundle was produced, and the node starts on this host again${r.partialRemoved ? ` (${r.partialRemoved} half-written bundle file(s) removed)` : ''}.`
: `no export of ${r.nodeId} is under way; nothing was changed.`);
return;
}
if (sub === 'import') {
Expand All @@ -189,7 +200,7 @@ function cmdNode() {
console.error(err.message);
process.exit(1);
}
console.error('usage: sym node (export <name> --out <file> (--to-host <pub> | --passphrase-env VAR --allow-copies) | import <file> --expect-node <id> (--expect-key <k> | --expect-fingerprint sha256:…) [--passphrase-env VAR] | host-key)');
console.error('usage: sym node (export <name> --out <file> (--to-host <pub> | --passphrase-env VAR --allow-copies) | abort-export (<name> | --node-id <id>) | import <file> --expect-node <id> (--expect-key <k> | --expect-fingerprint sha256:…) [--passphrase-env VAR] | host-key)');
process.exit(1);
}

Expand Down
1,869 changes: 1,136 additions & 733 deletions lib/config.js

Large diffs are not rendered by default.

23 changes: 21 additions & 2 deletions lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const path = require('path');
const crypto = require('crypto');
const { EventEmitter } = require('events');
const { MeshNode } = require('./core');
const { nodeDirById, loadIdentity, acquireIdentityLock, log: logMsg } = require('./config');
const { nodeDirById, loadIdentity, acquireIdentityLock, assertStartable, log: logMsg } = require('./config');
const { assertTestSandbox } = require('./core/state-root');
const { MemoryStore } = require('./memory-store');
const { AttestationStore, chainHash, isPosition, isCanonicalSig } = require('./attestation-store');
Expand Down Expand Up @@ -583,6 +583,9 @@ class SymNode extends EventEmitter {
// the lock. Hosts should catch and exit cleanly (or pick a
// different SYM_NODE_NAME).
this._releaseIdentityLock = acquireIdentityLock(this.name, { dir: this._dir });
// Held, the lock is what an export takes too: an identity exported (or being exported) between the load above and
// here is refused now, so no node runs a key that a bundle carries.
this._assertStillHere();

// Core Secure (design D1/D2): a peer is the set of confirmed sessions that proved one nodeId with
// one identity key. Session keys are ephemeral, derived per handshake (§5.2.1): there is no
Expand Down Expand Up @@ -2447,14 +2450,30 @@ class SymNode extends EventEmitter {
return op;
}

/**
* @private Refuse to run an identity that was exported, or whose export is under way, once the identity lock is held
* (relocation.js takes that lock for the whole export): the lock is released and the error thrown.
*/
_assertStillHere() {
try { assertStartable(this.nodeId); } catch (e) {
const release = this._releaseIdentityLock;
this._releaseIdentityLock = null;
try { if (release) release(); } catch { /* */ }
throw e;
}
}

/**
* @private What a start needs that a stop gave up (R3-11): the identity lock, taken again here when
* this node does not hold it (EIDENTITYLOCK if another process took it), and the inbox, when no
* other node in this process owns it (a newer node built for the identity keeps it), read again
* from disk; with it the delivery ledger, which another instance may have added to.
*/
_takeOwnership() {
if (!this._releaseIdentityLock) this._releaseIdentityLock = acquireIdentityLock(this.name, { dir: this._dir });
if (!this._releaseIdentityLock) {
this._releaseIdentityLock = acquireIdentityLock(this.name, { dir: this._dir });
this._assertStillHere(); // stopped, it may have been exported meanwhile: its key in memory is a bundle's now
}
if (!INBOX_OWNERS.has(this._inboxFile)) {
INBOX_OWNERS.set(this._inboxFile, this);
this._loadInbox();
Expand Down
Loading
Loading