feat: add remote trust and server identity pinning
Testing / remote-protocol-compat (0.16.0) (push) Successful in 1m1s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 1m3s
Testing / test (push) Failing after 1m15s
Build & Publish Package / publish (push) Successful in 51s
Package Extension / package-extension (push) Successful in 1m6s

- Add SSH-style server identity keys and known-host verification for remote serve endpoints.
- Add remote add/list/remove commands for explicit endpoint persistence.
- Fix remote clients listing to fan out through target discovery instead of ambiguous auto-routing.
- Add URL glob matching for tabs filter and count with extension tests.
- Add n8n credential pinning for server public keys or SHA256 fingerprints.
- Remove obsolete compat shim behavior while keeping empty compat seams for future protocol changes.
- Bump browser-cli to 0.16.4 and n8n node to 0.3.1.
- Cover known-hosts, remote registry, compat seams, n8n protocol verification, and URL matching with tests.
This commit is contained in:
2026-06-26 08:53:21 +02:00
parent 1ae9c33f00
commit 6270d8c956
28 changed files with 981 additions and 297 deletions
@@ -11,7 +11,10 @@ import {
pqDecrypt,
pqEncrypt,
pqTransportKey,
serverFingerprint,
signAuth,
verifyServerChallengeSignature,
verifyServerIdentity,
} from '../nodes/BrowserCli/protocol';
// Known-answer vectors produced by the real Python implementation
@@ -49,6 +52,19 @@ const DECRYPT_ENV = {
ciphertext: '7e4f75fa68098ea9a162dfd49af7824526186b77e9ac346b58f30d73df2bef88d5e6cd',
};
const DECRYPT_PLAIN = 'hello world payload';
const SERVER_PUB_HEX = '982c13bda72ef7b2bf4a8cd9756e4f283faaf4a34f9dec8e6c65585b64d9a902';
const SERVER_SIG =
'fa6897bb7f00f711ee8af151384eda736a503008f8b8e11b972cfea46a0366d5' +
'3127c2f1e9ba01e72805b267d023c552756645ae7d95fd00fa277ed20a43ee09';
const SERVER_FP = 'SHA256:wsYDqD4OnF/Sfvr3RKvVCOW8ET802H2qHSvWfnQwQrs';
const SERVER_CHALLENGE = {
type: 'challenge',
nonce: NONCE_HEX,
server_version: '0.16.4',
min_client_version: '0.9.0',
server_pubkey: SERVER_PUB_HEX,
server_sig: SERVER_SIG,
};
test('canonicalJson matches Python json.dumps(sort_keys, ensure_ascii)', () => {
assert.equal(canonicalJson(MSG), CANON);
@@ -139,3 +155,26 @@ test('decodeResponse parses plain JSON and decrypts PQ envelopes', () => {
const raw = Buffer.from(JSON.stringify({ encrypted: env }));
assert.deepEqual(decodeResponse(raw, secret), { success: true, data: 1 });
});
test('server identity fingerprint and signature match Python challenge signing', () => {
assert.equal(serverFingerprint(SERVER_PUB_HEX), SERVER_FP);
assert.equal(verifyServerChallengeSignature(SERVER_CHALLENGE), true);
assert.equal(verifyServerChallengeSignature({ ...SERVER_CHALLENGE, nonce: '22'.repeat(32) }), false);
});
test('verifyServerIdentity accepts pinned pubkey or fingerprint', () => {
assert.doesNotThrow(() => verifyServerIdentity(SERVER_CHALLENGE, SERVER_PUB_HEX, 'browser-host.example:8765', false));
assert.doesNotThrow(() => verifyServerIdentity(SERVER_CHALLENGE, SERVER_FP, 'browser-host.example:8765', false));
});
test('verifyServerIdentity rejects unknown and changed identities', () => {
assert.throws(
() => verifyServerIdentity(SERVER_CHALLENGE, null, 'browser-host.example:8765', false),
/Unknown browser-cli server identity/,
);
assert.throws(
() => verifyServerIdentity(SERVER_CHALLENGE, '00'.repeat(32), 'browser-host.example:8765', false),
/REMOTE SERVER IDENTITY CHANGED/,
);
assert.doesNotThrow(() => verifyServerIdentity(SERVER_CHALLENGE, null, 'browser-host.example:8765', true));
});