@sideband/secure-relay 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/.tsbuildinfo +1 -0
- package/dist/constants.d.ts +49 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/crypto.d.ts +70 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +144 -0
- package/dist/crypto.js.map +1 -0
- package/dist/frame.d.ts +213 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +547 -0
- package/dist/frame.js.map +1 -0
- package/dist/handshake.d.ts +39 -0
- package/dist/handshake.d.ts.map +1 -0
- package/dist/handshake.js +93 -0
- package/dist/handshake.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +32 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +88 -0
- package/dist/replay.js.map +1 -0
- package/dist/session.d.ts +67 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +122 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +80 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +3 -3
- package/src/crypto.test.ts +5 -5
- package/src/frame.test.ts +59 -10
- package/src/frame.ts +101 -77
- package/src/handshake.test.ts +29 -41
- package/src/handshake.ts +25 -27
- package/src/index.ts +4 -10
- package/src/integration.test.ts +97 -138
- package/src/session.test.ts +12 -10
- package/src/types.ts +1 -12
package/src/handshake.test.ts
CHANGED
|
@@ -21,14 +21,14 @@ describe("handshake", () => {
|
|
|
21
21
|
createHandshakeInit();
|
|
22
22
|
|
|
23
23
|
// Daemon processes init and creates accept
|
|
24
|
-
const { message: accept,
|
|
24
|
+
const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
|
|
25
25
|
init,
|
|
26
26
|
daemonId,
|
|
27
27
|
daemonIdentity,
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
// Client processes accept
|
|
31
|
-
const
|
|
31
|
+
const clientKeys = processHandshakeAccept(
|
|
32
32
|
accept,
|
|
33
33
|
daemonId,
|
|
34
34
|
daemonIdentity.publicKey,
|
|
@@ -36,8 +36,8 @@ describe("handshake", () => {
|
|
|
36
36
|
);
|
|
37
37
|
|
|
38
38
|
// Both should have valid session keys
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
39
|
+
expect(clientKeys).toBeDefined();
|
|
40
|
+
expect(daemonKeys).toBeDefined();
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -47,12 +47,12 @@ describe("handshake", () => {
|
|
|
47
47
|
|
|
48
48
|
const { message: init, ephemeralKeyPair: clientEphemeral } =
|
|
49
49
|
createHandshakeInit();
|
|
50
|
-
const { message: accept,
|
|
50
|
+
const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
|
|
51
51
|
init,
|
|
52
52
|
daemonId,
|
|
53
53
|
daemonIdentity,
|
|
54
54
|
);
|
|
55
|
-
const
|
|
55
|
+
const clientKeys = processHandshakeAccept(
|
|
56
56
|
accept,
|
|
57
57
|
daemonId,
|
|
58
58
|
daemonIdentity.publicKey,
|
|
@@ -60,12 +60,8 @@ describe("handshake", () => {
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
// Keys should be byte-for-byte identical
|
|
63
|
-
expect(
|
|
64
|
-
|
|
65
|
-
);
|
|
66
|
-
expect(clientResult.sessionKeys.daemonToClient).toEqual(
|
|
67
|
-
daemonResult.sessionKeys.daemonToClient,
|
|
68
|
-
);
|
|
63
|
+
expect(clientKeys.clientToDaemon).toEqual(daemonKeys.clientToDaemon);
|
|
64
|
+
expect(clientKeys.daemonToClient).toEqual(daemonKeys.daemonToClient);
|
|
69
65
|
});
|
|
70
66
|
|
|
71
67
|
it("derives directional keys (clientToDaemon != daemonToClient)", () => {
|
|
@@ -73,12 +69,12 @@ describe("handshake", () => {
|
|
|
73
69
|
|
|
74
70
|
const { message: init, ephemeralKeyPair: clientEphemeral } =
|
|
75
71
|
createHandshakeInit();
|
|
76
|
-
const { message: accept,
|
|
72
|
+
const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
|
|
77
73
|
init,
|
|
78
74
|
daemonId,
|
|
79
75
|
daemonIdentity,
|
|
80
76
|
);
|
|
81
|
-
const
|
|
77
|
+
const clientKeys = processHandshakeAccept(
|
|
82
78
|
accept,
|
|
83
79
|
daemonId,
|
|
84
80
|
daemonIdentity.publicKey,
|
|
@@ -86,12 +82,8 @@ describe("handshake", () => {
|
|
|
86
82
|
);
|
|
87
83
|
|
|
88
84
|
// Directional keys must differ to prevent reflection attacks
|
|
89
|
-
expect(
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
expect(daemonResult.sessionKeys.clientToDaemon).not.toEqual(
|
|
93
|
-
daemonResult.sessionKeys.daemonToClient,
|
|
94
|
-
);
|
|
85
|
+
expect(clientKeys.clientToDaemon).not.toEqual(clientKeys.daemonToClient);
|
|
86
|
+
expect(daemonKeys.clientToDaemon).not.toEqual(daemonKeys.daemonToClient);
|
|
95
87
|
});
|
|
96
88
|
|
|
97
89
|
it("produces different keys for different handshakes (ephemeral randomness)", () => {
|
|
@@ -105,7 +97,7 @@ describe("handshake", () => {
|
|
|
105
97
|
daemonId,
|
|
106
98
|
daemonIdentity,
|
|
107
99
|
);
|
|
108
|
-
const
|
|
100
|
+
const keys1 = processHandshakeAccept(
|
|
109
101
|
accept1,
|
|
110
102
|
daemonId,
|
|
111
103
|
daemonIdentity.publicKey,
|
|
@@ -120,7 +112,7 @@ describe("handshake", () => {
|
|
|
120
112
|
daemonId,
|
|
121
113
|
daemonIdentity,
|
|
122
114
|
);
|
|
123
|
-
const
|
|
115
|
+
const keys2 = processHandshakeAccept(
|
|
124
116
|
accept2,
|
|
125
117
|
daemonId,
|
|
126
118
|
daemonIdentity.publicKey,
|
|
@@ -128,17 +120,13 @@ describe("handshake", () => {
|
|
|
128
120
|
);
|
|
129
121
|
|
|
130
122
|
// Keys from different handshakes should differ
|
|
131
|
-
expect(
|
|
132
|
-
|
|
133
|
-
);
|
|
134
|
-
expect(result1.sessionKeys.daemonToClient).not.toEqual(
|
|
135
|
-
result2.sessionKeys.daemonToClient,
|
|
136
|
-
);
|
|
123
|
+
expect(keys1.clientToDaemon).not.toEqual(keys2.clientToDaemon);
|
|
124
|
+
expect(keys1.daemonToClient).not.toEqual(keys2.daemonToClient);
|
|
137
125
|
});
|
|
138
126
|
});
|
|
139
127
|
|
|
140
128
|
describe("signature verification", () => {
|
|
141
|
-
it("fails with wrong identity key (
|
|
129
|
+
it("fails with wrong identity key (IdentityKeyChanged)", () => {
|
|
142
130
|
const daemonIdentity = generateIdentityKeyPair();
|
|
143
131
|
const wrongIdentity = generateIdentityKeyPair();
|
|
144
132
|
|
|
@@ -169,7 +157,7 @@ describe("handshake", () => {
|
|
|
169
157
|
);
|
|
170
158
|
} catch (err) {
|
|
171
159
|
expect(err).toBeInstanceOf(SbrpError);
|
|
172
|
-
expect((err as SbrpError).code).toBe(SbrpErrorCode.
|
|
160
|
+
expect((err as SbrpError).code).toBe(SbrpErrorCode.IdentityKeyChanged);
|
|
173
161
|
}
|
|
174
162
|
});
|
|
175
163
|
|
|
@@ -189,7 +177,7 @@ describe("handshake", () => {
|
|
|
189
177
|
...accept,
|
|
190
178
|
signature: new Uint8Array(accept.signature),
|
|
191
179
|
};
|
|
192
|
-
tamperedAccept.signature[0]
|
|
180
|
+
tamperedAccept.signature[0] = tamperedAccept.signature[0]! ^ 0xff; // flip bits
|
|
193
181
|
|
|
194
182
|
expect(() =>
|
|
195
183
|
processHandshakeAccept(
|
|
@@ -262,11 +250,11 @@ describe("handshake", () => {
|
|
|
262
250
|
});
|
|
263
251
|
|
|
264
252
|
describe("processHandshakeInit", () => {
|
|
265
|
-
it("returns
|
|
253
|
+
it("returns accept message with correct field sizes", () => {
|
|
266
254
|
const daemonIdentity = generateIdentityKeyPair();
|
|
267
255
|
const { message: init } = createHandshakeInit();
|
|
268
256
|
|
|
269
|
-
const { message: accept,
|
|
257
|
+
const { message: accept, sessionKeys } = processHandshakeInit(
|
|
270
258
|
init,
|
|
271
259
|
daemonId,
|
|
272
260
|
daemonIdentity,
|
|
@@ -277,8 +265,8 @@ describe("handshake", () => {
|
|
|
277
265
|
expect(accept.acceptPublicKey.length).toBe(32);
|
|
278
266
|
expect(accept.signature).toBeInstanceOf(Uint8Array);
|
|
279
267
|
expect(accept.signature.length).toBe(64);
|
|
280
|
-
expect(
|
|
281
|
-
expect(
|
|
268
|
+
expect(sessionKeys.clientToDaemon.length).toBe(32);
|
|
269
|
+
expect(sessionKeys.daemonToClient.length).toBe(32);
|
|
282
270
|
});
|
|
283
271
|
|
|
284
272
|
it("generates different ephemeral keys and signatures each time", () => {
|
|
@@ -304,22 +292,22 @@ describe("handshake", () => {
|
|
|
304
292
|
|
|
305
293
|
const { message: init, ephemeralKeyPair: clientEphemeral } =
|
|
306
294
|
createHandshakeInit();
|
|
307
|
-
const { message: accept,
|
|
295
|
+
const { message: accept, sessionKeys: daemonKeys } = processHandshakeInit(
|
|
308
296
|
init,
|
|
309
297
|
daemonId,
|
|
310
298
|
daemonIdentity,
|
|
311
299
|
);
|
|
312
|
-
const
|
|
300
|
+
const clientKeys = processHandshakeAccept(
|
|
313
301
|
accept,
|
|
314
302
|
daemonId,
|
|
315
303
|
daemonIdentity.publicKey,
|
|
316
304
|
clientEphemeral,
|
|
317
305
|
);
|
|
318
306
|
|
|
319
|
-
expect(
|
|
320
|
-
expect(
|
|
321
|
-
expect(
|
|
322
|
-
expect(
|
|
307
|
+
expect(clientKeys.clientToDaemon.length).toBe(32);
|
|
308
|
+
expect(clientKeys.daemonToClient.length).toBe(32);
|
|
309
|
+
expect(daemonKeys.clientToDaemon.length).toBe(32);
|
|
310
|
+
expect(daemonKeys.daemonToClient.length).toBe(32);
|
|
323
311
|
});
|
|
324
312
|
});
|
|
325
313
|
});
|
package/src/handshake.ts
CHANGED
|
@@ -27,19 +27,6 @@ import type {
|
|
|
27
27
|
} from "./types.js";
|
|
28
28
|
import { SbrpError, SbrpErrorCode } from "./types.js";
|
|
29
29
|
|
|
30
|
-
/** Result of a successful daemon handshake */
|
|
31
|
-
export interface DaemonHandshakeResult {
|
|
32
|
-
sessionKeys: SessionKeys;
|
|
33
|
-
ephemeralKeyPair: EphemeralKeyPair;
|
|
34
|
-
signature: Uint8Array;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Result of a successful client handshake */
|
|
38
|
-
export interface ClientHandshakeResult {
|
|
39
|
-
sessionKeys: SessionKeys;
|
|
40
|
-
ephemeralKeyPair: EphemeralKeyPair;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
30
|
/**
|
|
44
31
|
* Create a handshake init message (client side).
|
|
45
32
|
*
|
|
@@ -73,7 +60,7 @@ export function processHandshakeInit(
|
|
|
73
60
|
init: HandshakeInit,
|
|
74
61
|
daemonId: DaemonId,
|
|
75
62
|
identityKeyPair: IdentityKeyPair,
|
|
76
|
-
): { message: HandshakeAccept;
|
|
63
|
+
): { message: HandshakeAccept; sessionKeys: SessionKeys } {
|
|
77
64
|
const ephemeralKeyPair = generateEphemeralKeyPair();
|
|
78
65
|
|
|
79
66
|
// Sign ephemeral key with context binding
|
|
@@ -97,20 +84,18 @@ export function processHandshakeInit(
|
|
|
97
84
|
);
|
|
98
85
|
const sessionKeys = deriveSessionKeys(sharedSecret, transcriptHash);
|
|
99
86
|
|
|
100
|
-
// Best-effort zeroize
|
|
87
|
+
// Best-effort zeroize secrets
|
|
101
88
|
zeroize(sharedSecret);
|
|
89
|
+
zeroize(ephemeralKeyPair.privateKey);
|
|
102
90
|
|
|
103
91
|
return {
|
|
104
92
|
message: {
|
|
105
93
|
type: "handshake.accept",
|
|
94
|
+
identityPublicKey: identityKeyPair.publicKey,
|
|
106
95
|
acceptPublicKey: ephemeralKeyPair.publicKey,
|
|
107
96
|
signature,
|
|
108
97
|
},
|
|
109
|
-
|
|
110
|
-
sessionKeys,
|
|
111
|
-
ephemeralKeyPair,
|
|
112
|
-
signature,
|
|
113
|
-
},
|
|
98
|
+
sessionKeys,
|
|
114
99
|
};
|
|
115
100
|
}
|
|
116
101
|
|
|
@@ -123,14 +108,29 @@ export function processHandshakeInit(
|
|
|
123
108
|
* NOTE: Callers MUST enforce a 30-second handshake timeout per SBRP §1.4.
|
|
124
109
|
* This function does not track time; timeout enforcement is a transport concern.
|
|
125
110
|
*
|
|
126
|
-
* @
|
|
111
|
+
* @param ephemeralKeyPair The privateKey is zeroized in-place after key derivation.
|
|
112
|
+
* @throws {SbrpError} IdentityKeyChanged if advertised key doesn't match pinned key
|
|
113
|
+
* @throws {SbrpError} HandshakeFailed if signature verification fails
|
|
127
114
|
*/
|
|
128
115
|
export function processHandshakeAccept(
|
|
129
116
|
accept: HandshakeAccept,
|
|
130
117
|
daemonId: DaemonId,
|
|
131
118
|
pinnedIdentityPublicKey: Uint8Array,
|
|
132
119
|
ephemeralKeyPair: EphemeralKeyPair,
|
|
133
|
-
):
|
|
120
|
+
): SessionKeys {
|
|
121
|
+
// Reject if advertised identity key doesn't match pinned key.
|
|
122
|
+
// Signature is verified against pinnedIdentityPublicKey, but an attacker
|
|
123
|
+
// could swap the advertised field to mislead higher layers.
|
|
124
|
+
if (
|
|
125
|
+
accept.identityPublicKey.length !== pinnedIdentityPublicKey.length ||
|
|
126
|
+
!accept.identityPublicKey.every((b, i) => b === pinnedIdentityPublicKey[i])
|
|
127
|
+
) {
|
|
128
|
+
throw new SbrpError(
|
|
129
|
+
SbrpErrorCode.IdentityKeyChanged,
|
|
130
|
+
"Advertised identity key does not match pinned key",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
134
|
// Verify daemon signature using PINNED key (not relay-provided!)
|
|
135
135
|
const signaturePayload = createSignaturePayload(
|
|
136
136
|
daemonId,
|
|
@@ -164,11 +164,9 @@ export function processHandshakeAccept(
|
|
|
164
164
|
);
|
|
165
165
|
const sessionKeys = deriveSessionKeys(sharedSecret, transcriptHash);
|
|
166
166
|
|
|
167
|
-
// Best-effort zeroize
|
|
167
|
+
// Best-effort zeroize secrets
|
|
168
168
|
zeroize(sharedSecret);
|
|
169
|
+
zeroize(ephemeralKeyPair.privateKey);
|
|
169
170
|
|
|
170
|
-
return
|
|
171
|
-
sessionKeys,
|
|
172
|
-
ephemeralKeyPair,
|
|
173
|
-
};
|
|
171
|
+
return sessionKeys;
|
|
174
172
|
}
|
package/src/index.ts
CHANGED
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
* const { message: init, ephemeralKeyPair } = createHandshakeInit();
|
|
23
23
|
*
|
|
24
24
|
* // Daemon side: process init and create accept
|
|
25
|
-
* const { message: accept,
|
|
26
|
-
* const clientSession = createClientSession(clientId,
|
|
25
|
+
* const { message: accept, sessionKeys } = processHandshakeInit(init, daemonId, identity);
|
|
26
|
+
* const clientSession = createClientSession(clientId, sessionKeys);
|
|
27
27
|
*
|
|
28
28
|
* // Client side: process accept (with TOFU-pinned identity)
|
|
29
|
-
* const
|
|
30
|
-
* const daemonSession = createDaemonSession(
|
|
29
|
+
* const clientKeys = processHandshakeAccept(accept, daemonId, pinnedKey, ephemeralKeyPair);
|
|
30
|
+
* const daemonSession = createDaemonSession(clientKeys);
|
|
31
31
|
*
|
|
32
32
|
* // Encrypt/decrypt messages
|
|
33
33
|
* const encrypted = encryptClientToDaemon(daemonSession, plaintext);
|
|
@@ -44,7 +44,6 @@ export type {
|
|
|
44
44
|
HandshakeAccept,
|
|
45
45
|
HandshakeInit,
|
|
46
46
|
IdentityKeyPair,
|
|
47
|
-
PinnedIdentity,
|
|
48
47
|
SessionId,
|
|
49
48
|
SessionKeys,
|
|
50
49
|
} from "./types.js";
|
|
@@ -106,11 +105,6 @@ export {
|
|
|
106
105
|
} from "./crypto.js";
|
|
107
106
|
|
|
108
107
|
// Handshake
|
|
109
|
-
export type {
|
|
110
|
-
ClientHandshakeResult,
|
|
111
|
-
DaemonHandshakeResult,
|
|
112
|
-
} from "./handshake.js";
|
|
113
|
-
|
|
114
108
|
export {
|
|
115
109
|
createHandshakeInit,
|
|
116
110
|
processHandshakeAccept,
|