@cello-protocol/crypto 0.0.58 → 0.0.60
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/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest-test-fixture.d.ts.map +1 -1
- package/dist/manifest-test-fixture.js +12 -1
- package/dist/manifest-test-fixture.js.map +1 -1
- package/dist/session-content-seal.d.ts +86 -0
- package/dist/session-content-seal.d.ts.map +1 -0
- package/dist/session-content-seal.js +124 -0
- package/dist/session-content-seal.js.map +1 -0
- package/dist/session-ephemeral-auth.d.ts +96 -0
- package/dist/session-ephemeral-auth.d.ts.map +1 -0
- package/dist/session-ephemeral-auth.js +137 -0
- package/dist/session-ephemeral-auth.js.map +1 -0
- package/dist/session-key-agreement.d.ts +207 -0
- package/dist/session-key-agreement.d.ts.map +1 -0
- package/dist/session-key-agreement.js +320 -0
- package/dist/session-key-agreement.js.map +1 -0
- package/dist/session-salt.d.ts +129 -0
- package/dist/session-salt.d.ts.map +1 -0
- package/dist/session-salt.js +205 -0
- package/dist/session-salt.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M15-KEYAGREE-1 — CELLO's own per-session key agreement.
|
|
3
|
+
*
|
|
4
|
+
* ─── Why CELLO needs its own, when Noise already encrypts the link ─────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Live content today is plaintext inside libp2p's Noise session. That confidentiality is real — but
|
|
7
|
+
* it is **libp2p's** key agreement over **libp2p's** ephemeral transport keys, so CELLO cannot
|
|
8
|
+
* upgrade its own guarantee: a post-quantum migration would happen on libp2p's timeline, with
|
|
9
|
+
* libp2p's algorithm choices, whenever libp2p chose to make it.
|
|
10
|
+
*
|
|
11
|
+
* The threat is harvest-now-decrypt-later, and it is why this is urgent rather than later: every
|
|
12
|
+
* cross-NAT conversation is relayed today, therefore recordable at fixed endpoints today, and adding
|
|
13
|
+
* this layer next year does not protect traffic already sent.
|
|
14
|
+
*
|
|
15
|
+
* ─── Construction (SPARC Phase P) ──────────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* generateSessionEphemeral(): # per SESSION, never reused
|
|
18
|
+
* 1. sk = random X25519 secret # RFC 7748
|
|
19
|
+
* pk = X25519 base * sk
|
|
20
|
+
*
|
|
21
|
+
* deriveSessionSecrets(ownSk, peerPk, sessionId, extra?):
|
|
22
|
+
* 1. shared = X25519(ownSk, peerPk) # EPHEMERAL-ephemeral ECDH
|
|
23
|
+
* 2. REFUSE if shared is all-zero # RFC 7748 §6.1
|
|
24
|
+
* 3. ikm = shared || extra? # the PQ hook
|
|
25
|
+
* 4. bind = sort(ownPk, peerPk) # canonical, role-independent
|
|
26
|
+
* 5. key = HKDF-SHA256(ikm, salt=sessionId, info="cello/session/v1/content-key" || bind, 32)
|
|
27
|
+
*
|
|
28
|
+
* THERE IS NO SECOND OUTPUT. This block used to specify a `csalt` derived from the same secret;
|
|
29
|
+
* the salt is agreed INDEPENDENTLY in `session-salt.ts` (Decisions Carried #8). Re-deriving it
|
|
30
|
+
* here brings back everything #7 was retracted for — epochs, per-leaf attribution, lockstep
|
|
31
|
+
* switching.
|
|
32
|
+
*
|
|
33
|
+
* RFCs: X25519 — RFC 7748. HKDF — RFC 5869. (The AEAD that consumes the key is NIST SP 800-38D,
|
|
34
|
+
* in `content-seal.ts`, which is the in-tree pattern this extends.)
|
|
35
|
+
*
|
|
36
|
+
* ─── The three ways this could be WORSE than no layer at all ───────────────────────────────────
|
|
37
|
+
*
|
|
38
|
+
* Named before the code, and each has a test:
|
|
39
|
+
*
|
|
40
|
+
* **1. Static-static.** A key derived only from long-term identity keys is the same key forever, so
|
|
41
|
+
* anyone who ever obtains an identity key decrypts every conversation that agent ever had — strictly
|
|
42
|
+
* worse than the Noise session it replaces. Hence ephemeral-EPHEMERAL: both sides mint fresh, and
|
|
43
|
+
* the caller destroys the secret at close.
|
|
44
|
+
*
|
|
45
|
+
* **2. A degenerate agreement accepted silently.** X25519 against a small-order point yields an
|
|
46
|
+
* all-zero shared secret; both sides then derive the same key, encryption appears to work, and the
|
|
47
|
+
* attacker who supplied the point knows it too. Encryption that *looks* like it is working is worse
|
|
48
|
+
* than none, because nobody investigates it. This throws.
|
|
49
|
+
*
|
|
50
|
+
* **3. A PQ hook that exists only in prose.** The line is blunt — *"the derivation accepts an
|
|
51
|
+
* additional shared secret from day one… omitting the hook defeats the entire reason for the
|
|
52
|
+
* work."* A parameter that is accepted and ignored reads as done and is not, so a test proves a
|
|
53
|
+
* different extra secret produces a different key.
|
|
54
|
+
*
|
|
55
|
+
* ─── WHAT THIS DOES NOT DEFEND AGAINST, stated plainly (review F6) ─────────────────────────────
|
|
56
|
+
*
|
|
57
|
+
* The ephemerals reaching THIS FUNCTION are raw public keys — nothing in this API takes an identity
|
|
58
|
+
* key, so it cannot tell whose they are. On its own that is sufficient only against the threat the
|
|
59
|
+
* DoD line names: harvest-now-decrypt-later, i.e. a PASSIVE recorder, which is what a relay storing
|
|
60
|
+
* traffic is. It is NOT sufficient against an ACTIVE on-path relay, which substitutes both
|
|
61
|
+
* ephemerals and reads everything while both ends see a working conversation.
|
|
62
|
+
*
|
|
63
|
+
* ✅ THE SYSTEM DOES DEFEND AGAINST THAT NOW — `DOD-M15-EPHEMERAL-AUTH-1` shipped it. The ephemeral
|
|
64
|
+
* public is signed with the agent's Ed25519 identity, and `verifySessionEphemeral` checks the peer's
|
|
65
|
+
* against the counterparty identity the SESSION was opened with, BEFORE this function is called. A
|
|
66
|
+
* failure stops the session rather than continuing in the open.
|
|
67
|
+
*
|
|
68
|
+
* The distinction is kept rather than replaced with "MITM is covered", because it is a property of
|
|
69
|
+
* the CALL ORDER and not of this module: a future caller that derives before verifying has an
|
|
70
|
+
* agreement with whoever sent the bytes, and this file would not notice.
|
|
71
|
+
*
|
|
72
|
+
* ─── ONE OUTPUT. The salt used to live here, and that was the defect. ─────────────────────────
|
|
73
|
+
*
|
|
74
|
+
* This module produced a content-hash salt as a second HKDF output, and Andre corrected it before
|
|
75
|
+
* `SEALWIRE-1` encoded anything (Decisions Carried #8, superseding the "one agreement, two outputs"
|
|
76
|
+
* bullet). The two are unrelated goals that merely both need a shared secret:
|
|
77
|
+
*
|
|
78
|
+
* the **envelope key** stops the relay reading messages in flight and MUST be destroyed at close;
|
|
79
|
+
* the **session salt** stops anyone holding stored hashes from confirming a guessed message and
|
|
80
|
+
* MUST survive for the life of the session.
|
|
81
|
+
*
|
|
82
|
+
* **Deriving both from one secret tied "must be forgotten" to "must be kept forever."** Everything
|
|
83
|
+
* that followed — salt epochs, per-leaf epoch attribution, lockstep switching, and my own Decision
|
|
84
|
+
* #7 ruling all of that — was a symptom of the coupling, not a requirement. The salt now lives in
|
|
85
|
+
* `session-salt.ts`, agreed in the SAME exchange from both sides' random contributions, and none of
|
|
86
|
+
* those consequences exist.
|
|
87
|
+
*
|
|
88
|
+
* ─── The remaining output, and its lifetime ───────────────────────────────────────────────────
|
|
89
|
+
*
|
|
90
|
+
**The envelope key NEVER touches disk**, and `destroySessionEphemeral` is how a caller discards the
|
|
91
|
+
* secret behind it at session close. A revived session RE-KEYS (Decisions Carried #5) — and because
|
|
92
|
+
* the salt no longer rides on this secret, re-keying no longer disturbs the transcript's
|
|
93
|
+
* verifiability.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ ALL THREE ARE WIRED NOW, and the history is kept because this file has been wrong in BOTH
|
|
96
|
+
* directions. It first claimed *"that is the forward secrecy, and `destroySessionEphemeral` is what
|
|
97
|
+
* makes it real"* while nothing called it. The correction over-swung to *"nothing in the daemon
|
|
98
|
+
* mints an ephemeral, derives a content key, or destroys one"*, which `006-CRYPTO` made two thirds
|
|
99
|
+
* false the same day. Both were confident; neither was measured.
|
|
100
|
+
*
|
|
101
|
+
* What is true today, and it is checkable in one grep each:
|
|
102
|
+
*
|
|
103
|
+
* `generateSessionEphemeral` / `destroySessionEphemeral` — `session-node-manager.ts` mints one
|
|
104
|
+
* per session at activation and destroys it at every site that drops the session's entry, and at
|
|
105
|
+
* shutdown (`006-CRYPTO`).
|
|
106
|
+
* `deriveSessionSecrets` — called from the same file, on an inbound signed ephemeral, AFTER the
|
|
107
|
+
* peer's signature is verified against the counterparty identity the session was opened with
|
|
108
|
+
* (`007-CRYPTO`). Its output encrypts the message body in `session-content-seal.ts`.
|
|
109
|
+
*
|
|
110
|
+
* ─── WHAT THIS FILE ALONE STILL DOES NOT DEFEND AGAINST ───────────────────────────────────────
|
|
111
|
+
*
|
|
112
|
+
* The ephemerals ARRIVING here are unauthenticated — this module takes raw public keys and cannot
|
|
113
|
+
* tell whose they are. That check is real but it lives one layer up, in
|
|
114
|
+
* `session-ephemeral-auth.ts`, which the daemon runs BEFORE calling this. So the property holds for
|
|
115
|
+
* the system and not for this function: a caller that derives from an unverified peer key has an
|
|
116
|
+
* agreement with whoever sent it, and against an on-path relay that is a relay who can read
|
|
117
|
+
* everything.
|
|
118
|
+
*
|
|
119
|
+
* Said this precisely because `session-salt.ts` states its own reachability boundary and this file
|
|
120
|
+
* once stated none, so the two halves of the same exchange read as though both were live.
|
|
121
|
+
*
|
|
122
|
+
* ─── The key and the salt must never be EQUAL, and what actually keeps them apart ──────────────
|
|
123
|
+
*
|
|
124
|
+
* The salt travels wherever a content hash does and the relay sees it, so a salt that equalled the
|
|
125
|
+
* key would hand the key to everyone who can see a hash.
|
|
126
|
+
*
|
|
127
|
+
* An earlier version said *"domain separation by label is what keeps them independent."* That was
|
|
128
|
+
* true when the salt was a second HKDF output of this function and is not true now. The salt is
|
|
129
|
+
* computed in `session-salt.ts` from a DIFFERENT input — the two sides' random contributions, not
|
|
130
|
+
* this ECDH secret — under its own label, with no HKDF salt. Different inputs, different module,
|
|
131
|
+
* different function. Label separation is not the mechanism; it is not even reachable, because
|
|
132
|
+
* there is only one label here.
|
|
133
|
+
*/
|
|
134
|
+
export declare const SESSION_KEY_BYTES = 32;
|
|
135
|
+
export interface SessionEphemeral {
|
|
136
|
+
/** X25519 secret. The caller MUST destroy this at session close — that is what forward secrecy is. */
|
|
137
|
+
secretKey: Uint8Array;
|
|
138
|
+
/** X25519 public, sent to the peer in the session handshake. */
|
|
139
|
+
publicKey: Uint8Array;
|
|
140
|
+
}
|
|
141
|
+
export interface SessionSecrets {
|
|
142
|
+
/**
|
|
143
|
+
* AEAD key for message content (consumed by the `content-seal.ts` AES-256-GCM pattern).
|
|
144
|
+
*
|
|
145
|
+
* The ONLY output. It never touches disk and is destroyed at session close — see
|
|
146
|
+
* `destroySessionEphemeral`. The content-hash salt is NOT here: it is agreed separately in
|
|
147
|
+
* `session-salt.ts`, because its lifetime is the opposite of this one's.
|
|
148
|
+
*/
|
|
149
|
+
contentKey: Uint8Array;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Mint this side's per-SESSION ephemeral keypair.
|
|
153
|
+
*
|
|
154
|
+
* Fresh every session, deliberately. Reusing one across sessions would collapse to static-static and
|
|
155
|
+
* void the forward secrecy that `design-problems` already claims as structural.
|
|
156
|
+
*/
|
|
157
|
+
export declare function generateSessionEphemeral(): SessionEphemeral;
|
|
158
|
+
/**
|
|
159
|
+
* DESTROY THIS SIDE'S EPHEMERAL SECRET — `DOD-M15-KEYAGREE-1`, review F4.
|
|
160
|
+
*
|
|
161
|
+
* The line's clause is *"destroys the ephemerals at close"*, and it existed only as a sentence in a
|
|
162
|
+
* docstring telling the caller to do it. Forward secrecy is not a property of generating a fresh
|
|
163
|
+
* key; it is a property of the old one being GONE. A comment asserting that is the failure mode this
|
|
164
|
+
* milestone has caught five times in seal and persistence code, so it is code now.
|
|
165
|
+
*
|
|
166
|
+
* HONEST LIMIT: JavaScript cannot guarantee no copy survives. The garbage collector may have moved
|
|
167
|
+
* the buffer, and a `Uint8Array` handed across a module boundary may have been copied. Zeroing the
|
|
168
|
+
* buffer we hold removes the value from the one place we control, which is strictly better than not
|
|
169
|
+
* doing it and strictly weaker than the guarantee a language with explicit memory would give.
|
|
170
|
+
*/
|
|
171
|
+
export declare function destroySessionEphemeral(e: SessionEphemeral): void;
|
|
172
|
+
export declare function deriveSessionSecrets(opts: {
|
|
173
|
+
ownEphemeralSecret: Uint8Array;
|
|
174
|
+
peerEphemeralPublic: Uint8Array;
|
|
175
|
+
/** The session this agreement is for. Bound in as the HKDF salt. */
|
|
176
|
+
sessionId: Uint8Array;
|
|
177
|
+
/**
|
|
178
|
+
* THE PQ HOOK — additional agreed secret, mixed into the IKM.
|
|
179
|
+
*
|
|
180
|
+
* Present from day one, before there is a PQ contribution to put in it, because retrofitting it
|
|
181
|
+
* later is a wire change and a rewrite rather than an addition. Hybrid PQ becomes: run a KEM,
|
|
182
|
+
* pass its shared secret here. Any length — an ML-KEM secret is 32 bytes but a hybrid may
|
|
183
|
+
* concatenate more than one contribution, and fixing the length would force the rewrite this
|
|
184
|
+
* parameter exists to avoid.
|
|
185
|
+
*/
|
|
186
|
+
extraSharedSecret?: Uint8Array;
|
|
187
|
+
/**
|
|
188
|
+
* THE PQ TRANSCRIPT — review F8, and it is added NOW precisely because it cannot be added later.
|
|
189
|
+
*
|
|
190
|
+
* `extraSharedSecret` alone is not a complete hybrid combiner. Concatenating shared secrets is the
|
|
191
|
+
* right shape and matches TLS's X25519MLKEM768 and NIST SP 800-56C Rev 2 §2 — that part was
|
|
192
|
+
* checked and is sound. What it lacks is the KEM's PUBLIC material: X-Wing's combiner hashes
|
|
193
|
+
* `ss_pq ‖ ss_x ‖ ct_x ‖ pk_x`, binding the ciphertext and public key, and the current analysis
|
|
194
|
+
* ("On the Necessity of Public Contexts in Hybrid KEMs", eprint 2026/140) is that this is
|
|
195
|
+
* NECESSARY rather than belt-and-braces.
|
|
196
|
+
*
|
|
197
|
+
* A caller doing the obvious thing — passing only the ML-KEM shared secret — would get a hybrid
|
|
198
|
+
* whose ciphertext and public key are unbound. This parameter is where `ct_pq ‖ pk_pq` goes.
|
|
199
|
+
*
|
|
200
|
+
* It is empty today and that is the point: the DoD line's whole justification for building the
|
|
201
|
+
* hook before there is anything to put in it is that hybrid PQ must be *"an addition, not a
|
|
202
|
+
* rewrite."* Added after a wire format exists, this is a wire change — the exact rewrite the hook
|
|
203
|
+
* was meant to avoid.
|
|
204
|
+
*/
|
|
205
|
+
pqTranscript?: Uint8Array;
|
|
206
|
+
}): SessionSecrets;
|
|
207
|
+
//# sourceMappingURL=session-key-agreement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-key-agreement.d.ts","sourceRoot":"","sources":["../src/session-key-agreement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoIG;AAQH,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAepC,MAAM,WAAW,gBAAgB;IAC/B,sGAAsG;IACtG,SAAS,EAAE,UAAU,CAAC;IACtB,gEAAgE;IAChE,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,UAAU,EAAE,UAAU,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,gBAAgB,CAG3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAEjE;AAmBD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,kBAAkB,EAAE,UAAU,CAAC;IAC/B,mBAAmB,EAAE,UAAU,CAAC;IAChC,oEAAoE;IACpE,SAAS,EAAE,UAAU,CAAC;IACtB;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,UAAU,CAAC;IAC/B;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,UAAU,CAAC;CAC3B,GAAG,cAAc,CAgJjB"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-M15-KEYAGREE-1 — CELLO's own per-session key agreement.
|
|
3
|
+
*
|
|
4
|
+
* ─── Why CELLO needs its own, when Noise already encrypts the link ─────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Live content today is plaintext inside libp2p's Noise session. That confidentiality is real — but
|
|
7
|
+
* it is **libp2p's** key agreement over **libp2p's** ephemeral transport keys, so CELLO cannot
|
|
8
|
+
* upgrade its own guarantee: a post-quantum migration would happen on libp2p's timeline, with
|
|
9
|
+
* libp2p's algorithm choices, whenever libp2p chose to make it.
|
|
10
|
+
*
|
|
11
|
+
* The threat is harvest-now-decrypt-later, and it is why this is urgent rather than later: every
|
|
12
|
+
* cross-NAT conversation is relayed today, therefore recordable at fixed endpoints today, and adding
|
|
13
|
+
* this layer next year does not protect traffic already sent.
|
|
14
|
+
*
|
|
15
|
+
* ─── Construction (SPARC Phase P) ──────────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* generateSessionEphemeral(): # per SESSION, never reused
|
|
18
|
+
* 1. sk = random X25519 secret # RFC 7748
|
|
19
|
+
* pk = X25519 base * sk
|
|
20
|
+
*
|
|
21
|
+
* deriveSessionSecrets(ownSk, peerPk, sessionId, extra?):
|
|
22
|
+
* 1. shared = X25519(ownSk, peerPk) # EPHEMERAL-ephemeral ECDH
|
|
23
|
+
* 2. REFUSE if shared is all-zero # RFC 7748 §6.1
|
|
24
|
+
* 3. ikm = shared || extra? # the PQ hook
|
|
25
|
+
* 4. bind = sort(ownPk, peerPk) # canonical, role-independent
|
|
26
|
+
* 5. key = HKDF-SHA256(ikm, salt=sessionId, info="cello/session/v1/content-key" || bind, 32)
|
|
27
|
+
*
|
|
28
|
+
* THERE IS NO SECOND OUTPUT. This block used to specify a `csalt` derived from the same secret;
|
|
29
|
+
* the salt is agreed INDEPENDENTLY in `session-salt.ts` (Decisions Carried #8). Re-deriving it
|
|
30
|
+
* here brings back everything #7 was retracted for — epochs, per-leaf attribution, lockstep
|
|
31
|
+
* switching.
|
|
32
|
+
*
|
|
33
|
+
* RFCs: X25519 — RFC 7748. HKDF — RFC 5869. (The AEAD that consumes the key is NIST SP 800-38D,
|
|
34
|
+
* in `content-seal.ts`, which is the in-tree pattern this extends.)
|
|
35
|
+
*
|
|
36
|
+
* ─── The three ways this could be WORSE than no layer at all ───────────────────────────────────
|
|
37
|
+
*
|
|
38
|
+
* Named before the code, and each has a test:
|
|
39
|
+
*
|
|
40
|
+
* **1. Static-static.** A key derived only from long-term identity keys is the same key forever, so
|
|
41
|
+
* anyone who ever obtains an identity key decrypts every conversation that agent ever had — strictly
|
|
42
|
+
* worse than the Noise session it replaces. Hence ephemeral-EPHEMERAL: both sides mint fresh, and
|
|
43
|
+
* the caller destroys the secret at close.
|
|
44
|
+
*
|
|
45
|
+
* **2. A degenerate agreement accepted silently.** X25519 against a small-order point yields an
|
|
46
|
+
* all-zero shared secret; both sides then derive the same key, encryption appears to work, and the
|
|
47
|
+
* attacker who supplied the point knows it too. Encryption that *looks* like it is working is worse
|
|
48
|
+
* than none, because nobody investigates it. This throws.
|
|
49
|
+
*
|
|
50
|
+
* **3. A PQ hook that exists only in prose.** The line is blunt — *"the derivation accepts an
|
|
51
|
+
* additional shared secret from day one… omitting the hook defeats the entire reason for the
|
|
52
|
+
* work."* A parameter that is accepted and ignored reads as done and is not, so a test proves a
|
|
53
|
+
* different extra secret produces a different key.
|
|
54
|
+
*
|
|
55
|
+
* ─── WHAT THIS DOES NOT DEFEND AGAINST, stated plainly (review F6) ─────────────────────────────
|
|
56
|
+
*
|
|
57
|
+
* The ephemerals reaching THIS FUNCTION are raw public keys — nothing in this API takes an identity
|
|
58
|
+
* key, so it cannot tell whose they are. On its own that is sufficient only against the threat the
|
|
59
|
+
* DoD line names: harvest-now-decrypt-later, i.e. a PASSIVE recorder, which is what a relay storing
|
|
60
|
+
* traffic is. It is NOT sufficient against an ACTIVE on-path relay, which substitutes both
|
|
61
|
+
* ephemerals and reads everything while both ends see a working conversation.
|
|
62
|
+
*
|
|
63
|
+
* ✅ THE SYSTEM DOES DEFEND AGAINST THAT NOW — `DOD-M15-EPHEMERAL-AUTH-1` shipped it. The ephemeral
|
|
64
|
+
* public is signed with the agent's Ed25519 identity, and `verifySessionEphemeral` checks the peer's
|
|
65
|
+
* against the counterparty identity the SESSION was opened with, BEFORE this function is called. A
|
|
66
|
+
* failure stops the session rather than continuing in the open.
|
|
67
|
+
*
|
|
68
|
+
* The distinction is kept rather than replaced with "MITM is covered", because it is a property of
|
|
69
|
+
* the CALL ORDER and not of this module: a future caller that derives before verifying has an
|
|
70
|
+
* agreement with whoever sent the bytes, and this file would not notice.
|
|
71
|
+
*
|
|
72
|
+
* ─── ONE OUTPUT. The salt used to live here, and that was the defect. ─────────────────────────
|
|
73
|
+
*
|
|
74
|
+
* This module produced a content-hash salt as a second HKDF output, and Andre corrected it before
|
|
75
|
+
* `SEALWIRE-1` encoded anything (Decisions Carried #8, superseding the "one agreement, two outputs"
|
|
76
|
+
* bullet). The two are unrelated goals that merely both need a shared secret:
|
|
77
|
+
*
|
|
78
|
+
* the **envelope key** stops the relay reading messages in flight and MUST be destroyed at close;
|
|
79
|
+
* the **session salt** stops anyone holding stored hashes from confirming a guessed message and
|
|
80
|
+
* MUST survive for the life of the session.
|
|
81
|
+
*
|
|
82
|
+
* **Deriving both from one secret tied "must be forgotten" to "must be kept forever."** Everything
|
|
83
|
+
* that followed — salt epochs, per-leaf epoch attribution, lockstep switching, and my own Decision
|
|
84
|
+
* #7 ruling all of that — was a symptom of the coupling, not a requirement. The salt now lives in
|
|
85
|
+
* `session-salt.ts`, agreed in the SAME exchange from both sides' random contributions, and none of
|
|
86
|
+
* those consequences exist.
|
|
87
|
+
*
|
|
88
|
+
* ─── The remaining output, and its lifetime ───────────────────────────────────────────────────
|
|
89
|
+
*
|
|
90
|
+
**The envelope key NEVER touches disk**, and `destroySessionEphemeral` is how a caller discards the
|
|
91
|
+
* secret behind it at session close. A revived session RE-KEYS (Decisions Carried #5) — and because
|
|
92
|
+
* the salt no longer rides on this secret, re-keying no longer disturbs the transcript's
|
|
93
|
+
* verifiability.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ ALL THREE ARE WIRED NOW, and the history is kept because this file has been wrong in BOTH
|
|
96
|
+
* directions. It first claimed *"that is the forward secrecy, and `destroySessionEphemeral` is what
|
|
97
|
+
* makes it real"* while nothing called it. The correction over-swung to *"nothing in the daemon
|
|
98
|
+
* mints an ephemeral, derives a content key, or destroys one"*, which `006-CRYPTO` made two thirds
|
|
99
|
+
* false the same day. Both were confident; neither was measured.
|
|
100
|
+
*
|
|
101
|
+
* What is true today, and it is checkable in one grep each:
|
|
102
|
+
*
|
|
103
|
+
* `generateSessionEphemeral` / `destroySessionEphemeral` — `session-node-manager.ts` mints one
|
|
104
|
+
* per session at activation and destroys it at every site that drops the session's entry, and at
|
|
105
|
+
* shutdown (`006-CRYPTO`).
|
|
106
|
+
* `deriveSessionSecrets` — called from the same file, on an inbound signed ephemeral, AFTER the
|
|
107
|
+
* peer's signature is verified against the counterparty identity the session was opened with
|
|
108
|
+
* (`007-CRYPTO`). Its output encrypts the message body in `session-content-seal.ts`.
|
|
109
|
+
*
|
|
110
|
+
* ─── WHAT THIS FILE ALONE STILL DOES NOT DEFEND AGAINST ───────────────────────────────────────
|
|
111
|
+
*
|
|
112
|
+
* The ephemerals ARRIVING here are unauthenticated — this module takes raw public keys and cannot
|
|
113
|
+
* tell whose they are. That check is real but it lives one layer up, in
|
|
114
|
+
* `session-ephemeral-auth.ts`, which the daemon runs BEFORE calling this. So the property holds for
|
|
115
|
+
* the system and not for this function: a caller that derives from an unverified peer key has an
|
|
116
|
+
* agreement with whoever sent it, and against an on-path relay that is a relay who can read
|
|
117
|
+
* everything.
|
|
118
|
+
*
|
|
119
|
+
* Said this precisely because `session-salt.ts` states its own reachability boundary and this file
|
|
120
|
+
* once stated none, so the two halves of the same exchange read as though both were live.
|
|
121
|
+
*
|
|
122
|
+
* ─── The key and the salt must never be EQUAL, and what actually keeps them apart ──────────────
|
|
123
|
+
*
|
|
124
|
+
* The salt travels wherever a content hash does and the relay sees it, so a salt that equalled the
|
|
125
|
+
* key would hand the key to everyone who can see a hash.
|
|
126
|
+
*
|
|
127
|
+
* An earlier version said *"domain separation by label is what keeps them independent."* That was
|
|
128
|
+
* true when the salt was a second HKDF output of this function and is not true now. The salt is
|
|
129
|
+
* computed in `session-salt.ts` from a DIFFERENT input — the two sides' random contributions, not
|
|
130
|
+
* this ECDH secret — under its own label, with no HKDF salt. Different inputs, different module,
|
|
131
|
+
* different function. Label separation is not the mechanism; it is not even reachable, because
|
|
132
|
+
* there is only one label here.
|
|
133
|
+
*/
|
|
134
|
+
import { x25519 } from "@noble/curves/ed25519.js";
|
|
135
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
136
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
137
|
+
/** X25519 keys and the derived outputs are all 32 bytes. */
|
|
138
|
+
const X25519_KEY_BYTES = 32;
|
|
139
|
+
export const SESSION_KEY_BYTES = 32;
|
|
140
|
+
const ENC = new TextEncoder();
|
|
141
|
+
/**
|
|
142
|
+
* THE ONE label. Versioned so a future derivation change is a new label rather than a silent
|
|
143
|
+
* reinterpretation of the same bytes.
|
|
144
|
+
*
|
|
145
|
+
* An earlier version read *"distinct labels are the ONLY thing separating the two outputs — same
|
|
146
|
+
* IKM, same salt, same binding."* There are no longer two outputs to separate: the content-hash
|
|
147
|
+
* salt moved to `session-salt.ts` and is derived from different inputs entirely (Decisions Carried
|
|
148
|
+
* #8). The label still earns its place — it is bound into `info`, so it is what a future second
|
|
149
|
+
* output WOULD be separated by — but it is not currently holding two values apart.
|
|
150
|
+
*/
|
|
151
|
+
const INFO_CONTENT_KEY = ENC.encode("cello/session/v1/content-key");
|
|
152
|
+
/**
|
|
153
|
+
* Mint this side's per-SESSION ephemeral keypair.
|
|
154
|
+
*
|
|
155
|
+
* Fresh every session, deliberately. Reusing one across sessions would collapse to static-static and
|
|
156
|
+
* void the forward secrecy that `design-problems` already claims as structural.
|
|
157
|
+
*/
|
|
158
|
+
export function generateSessionEphemeral() {
|
|
159
|
+
const secretKey = x25519.utils.randomSecretKey();
|
|
160
|
+
return { secretKey, publicKey: x25519.getPublicKey(secretKey) };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* DESTROY THIS SIDE'S EPHEMERAL SECRET — `DOD-M15-KEYAGREE-1`, review F4.
|
|
164
|
+
*
|
|
165
|
+
* The line's clause is *"destroys the ephemerals at close"*, and it existed only as a sentence in a
|
|
166
|
+
* docstring telling the caller to do it. Forward secrecy is not a property of generating a fresh
|
|
167
|
+
* key; it is a property of the old one being GONE. A comment asserting that is the failure mode this
|
|
168
|
+
* milestone has caught five times in seal and persistence code, so it is code now.
|
|
169
|
+
*
|
|
170
|
+
* HONEST LIMIT: JavaScript cannot guarantee no copy survives. The garbage collector may have moved
|
|
171
|
+
* the buffer, and a `Uint8Array` handed across a module boundary may have been copied. Zeroing the
|
|
172
|
+
* buffer we hold removes the value from the one place we control, which is strictly better than not
|
|
173
|
+
* doing it and strictly weaker than the guarantee a language with explicit memory would give.
|
|
174
|
+
*/
|
|
175
|
+
export function destroySessionEphemeral(e) {
|
|
176
|
+
e.secretKey.fill(0);
|
|
177
|
+
}
|
|
178
|
+
/** Constant-time-ish all-zero check. Not secret-dependent branching — the input is already known bad. */
|
|
179
|
+
function isAllZero(b) {
|
|
180
|
+
let acc = 0;
|
|
181
|
+
for (const x of b)
|
|
182
|
+
acc |= x;
|
|
183
|
+
return acc === 0;
|
|
184
|
+
}
|
|
185
|
+
/** Lexicographic compare, so both sides order the two public keys identically. */
|
|
186
|
+
function lexLess(a, b) {
|
|
187
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
188
|
+
const x = a[i];
|
|
189
|
+
const y = b[i];
|
|
190
|
+
if (x !== y)
|
|
191
|
+
return x < y;
|
|
192
|
+
}
|
|
193
|
+
return a.length < b.length;
|
|
194
|
+
}
|
|
195
|
+
export function deriveSessionSecrets(opts) {
|
|
196
|
+
if (opts.peerEphemeralPublic.length !== X25519_KEY_BYTES) {
|
|
197
|
+
throw new Error(`KEYAGREE: peer ephemeral public key must be ${X25519_KEY_BYTES} bytes, got ${opts.peerEphemeralPublic.length}. ` +
|
|
198
|
+
"Refusing rather than padding — a short key silently zero-extended is an agreement with " +
|
|
199
|
+
"something that is not the peer. This check ALSO keeps the HKDF `info` unambiguous (review " +
|
|
200
|
+
"F9): with a fixed-length label, two exactly-32-byte public keys and a trailing transcript, " +
|
|
201
|
+
"no two different (keys, transcript) inputs can encode to the same info bytes. A " +
|
|
202
|
+
"variable-length public would break that, and two peers whose info collided would derive the " +
|
|
203
|
+
"same key from different material.");
|
|
204
|
+
}
|
|
205
|
+
if (opts.ownEphemeralSecret.length !== X25519_KEY_BYTES) {
|
|
206
|
+
// Review F11: symmetric with the peer check above. `@noble` catches it, but names its own
|
|
207
|
+
// parameter rather than CELLO's key — the same substitution as F7, one layer down.
|
|
208
|
+
throw new Error(`KEYAGREE: own ephemeral secret must be ${X25519_KEY_BYTES} bytes, got ${opts.ownEphemeralSecret.length}. ` +
|
|
209
|
+
"This is a local defect, not something the peer did.");
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* REFUSE A NON-CANONICAL PEER KEY — review F10, and it is a one-bit attack with no diagnosis.
|
|
213
|
+
*
|
|
214
|
+
* RFC 7748 §5 has X25519 MASK bit 255 of the u-coordinate, so `pk` and `pk | 0x80…` produce the
|
|
215
|
+
* SAME shared secret — but they are different BYTES, and the binding below uses the bytes as
|
|
216
|
+
* received, including in the sort comparison.
|
|
217
|
+
*
|
|
218
|
+
* So a relay that flips the top bit of one relayed ephemeral costs itself nothing: ECDH still
|
|
219
|
+
* agrees, but one side binds `pk` and the other binds `pk'`, possibly in a different sorted order.
|
|
220
|
+
* The two derive different keys, the session never decrypts, and nothing anywhere explains why —
|
|
221
|
+
* precisely the failure the sorted binding exists to prevent, achieved for one flipped bit.
|
|
222
|
+
*
|
|
223
|
+
* Refusing rather than masking, deliberately: masking would make the tamper invisible, and a peer
|
|
224
|
+
* sending a non-canonical encoding is either broken or probing. Say so.
|
|
225
|
+
*/
|
|
226
|
+
if (opts.peerEphemeralPublic[31] & 0x80) {
|
|
227
|
+
throw new Error("KEYAGREE: the peer's ephemeral public key is non-canonical — bit 255 is set. X25519 masks " +
|
|
228
|
+
"that bit (RFC 7748 §5) so the agreement would still succeed, but the raw bytes are bound into " +
|
|
229
|
+
"the key derivation, so the two sides would derive DIFFERENT keys and the session would never " +
|
|
230
|
+
"decrypt with nothing explaining why. Refusing: a correct peer never sets it, and a flipped " +
|
|
231
|
+
"bit in transit is exactly what this catches.");
|
|
232
|
+
}
|
|
233
|
+
if (opts.sessionId.length === 0) {
|
|
234
|
+
throw new Error("KEYAGREE: sessionId must not be empty. It is bound in as the HKDF salt — the BACKSTOP against " +
|
|
235
|
+
"catastrophic ephemeral reuse. (Review F13: what ordinarily stops two sessions between the " +
|
|
236
|
+
"same peers sharing a key is the fresh ephemerals, not this. An earlier version of this " +
|
|
237
|
+
"message claimed the stronger thing.)");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* WRAPPED — review F7. `@noble` rejects a small-order or otherwise invalid point here, and its
|
|
241
|
+
* message is *"invalid private or public key received"*: it names neither CELLO, nor which of the
|
|
242
|
+
* two keys, nor the session. That is a third-party exit-point label standing in for the cause, on
|
|
243
|
+
* the path that ACTUALLY fires — while the carefully-written message below sits on the branch
|
|
244
|
+
* documented as unreachable.
|
|
245
|
+
*/
|
|
246
|
+
let shared;
|
|
247
|
+
try {
|
|
248
|
+
shared = x25519.getSharedSecret(opts.ownEphemeralSecret, opts.peerEphemeralPublic);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
throw new Error("KEYAGREE: the peer's ephemeral public key is unusable for X25519 — it is invalid or a " +
|
|
252
|
+
`small-order point (RFC 7748 §6.1), so no session key can be agreed. Peer key began ` +
|
|
253
|
+
`${Buffer.from(opts.peerEphemeralPublic.subarray(0, 8)).toString("hex")}…. This is the peer's ` +
|
|
254
|
+
"key, not yours; a correct client never sends one. Refusing rather than deriving: a degenerate " +
|
|
255
|
+
"agreement yields a key the sender of that point also holds, while every message appears to " +
|
|
256
|
+
"encrypt normally.", { cause: err });
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* FAIL CLOSED ON A DEGENERATE AGREEMENT — RFC 7748 §6.1.
|
|
260
|
+
*
|
|
261
|
+
* A small-order peer point drives the shared secret to all zeros. Both sides would then derive the
|
|
262
|
+
* same key, every message would encrypt and decrypt correctly, and whoever supplied the point
|
|
263
|
+
* would hold the key. There is no symptom to notice.
|
|
264
|
+
*
|
|
265
|
+
* ⚠️ THIS BRANCH IS UNREACHABLE TODAY, and saying so is the point. `@noble/curves` already refuses
|
|
266
|
+
* — `getSharedSecret` throws *"invalid private or public key received"* before control arrives
|
|
267
|
+
* here — which the revert test proved: deleting this check left every test green. So this is a
|
|
268
|
+
* BACKSTOP against that dependency behaviour changing, not the thing currently providing the
|
|
269
|
+
* property, and an earlier version of this comment claimed otherwise.
|
|
270
|
+
*
|
|
271
|
+
* It is kept rather than deleted because the cost is one branch and the failure it guards has no
|
|
272
|
+
* symptom. The test pins the PROPERTY (a degenerate agreement is refused) rather than which layer
|
|
273
|
+
* refuses, so it keeps its teeth either way — and would catch a `@noble` upgrade that stopped
|
|
274
|
+
* rejecting.
|
|
275
|
+
*/
|
|
276
|
+
if (isAllZero(shared)) {
|
|
277
|
+
throw new Error("KEYAGREE: degenerate X25519 agreement — the shared secret is all zeros, which means the peer " +
|
|
278
|
+
"supplied a small-order point (RFC 7748 §6.1). Refusing: deriving from it would produce a key " +
|
|
279
|
+
"the attacker who sent that point also knows, while every message appeared to encrypt normally.");
|
|
280
|
+
}
|
|
281
|
+
const ownPublic = x25519.getPublicKey(opts.ownEphemeralSecret);
|
|
282
|
+
/**
|
|
283
|
+
* REFLECTION — the peer sent back our own ephemeral public. Standard hygiene: it is not a
|
|
284
|
+
* key-recovery attack against X25519, but it means the "peer" contributed nothing to the
|
|
285
|
+
* agreement, and both sorted halves would be identical. One line, refused.
|
|
286
|
+
*/
|
|
287
|
+
if (Buffer.compare(Buffer.from(ownPublic), Buffer.from(opts.peerEphemeralPublic)) === 0) {
|
|
288
|
+
throw new Error("KEYAGREE: the peer's ephemeral public key is identical to our own — the peer contributed " +
|
|
289
|
+
"nothing to the agreement. Refusing: this is a reflection, not a handshake.");
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Both public keys bound into `info`, in CANONICAL (sorted) order.
|
|
293
|
+
*
|
|
294
|
+
* Sorted rather than by role, because the two daemons reach this point from different code paths
|
|
295
|
+
* and a disagreement about who "initiated" would produce two different keys — a conversation that
|
|
296
|
+
* fails to decrypt with nothing anywhere explaining why. Binding both also ties the derivation to
|
|
297
|
+
* this exact pair of ephemerals, so a key from one handshake cannot be replayed into another.
|
|
298
|
+
*/
|
|
299
|
+
const [first, second] = lexLess(ownPublic, opts.peerEphemeralPublic)
|
|
300
|
+
? [ownPublic, opts.peerEphemeralPublic]
|
|
301
|
+
: [opts.peerEphemeralPublic, ownPublic];
|
|
302
|
+
const extra = opts.extraSharedSecret ?? new Uint8Array(0);
|
|
303
|
+
const ikm = new Uint8Array(shared.length + extra.length);
|
|
304
|
+
ikm.set(shared, 0);
|
|
305
|
+
ikm.set(extra, shared.length);
|
|
306
|
+
const transcript = opts.pqTranscript ?? new Uint8Array(0);
|
|
307
|
+
const info = (label) => {
|
|
308
|
+
const out = new Uint8Array(label.length + first.length + second.length + transcript.length);
|
|
309
|
+
out.set(label, 0);
|
|
310
|
+
out.set(first, label.length);
|
|
311
|
+
out.set(second, label.length + first.length);
|
|
312
|
+
// TRAILING, so the label remains recoverable as info[0 : len-64-|transcript|] for a caller that
|
|
313
|
+
// knows the transcript length. It is empty today; when a hybrid fills it, both sides supply the
|
|
314
|
+
// same bytes or they diverge — which is the safe direction.
|
|
315
|
+
out.set(transcript, label.length + first.length + second.length);
|
|
316
|
+
return out;
|
|
317
|
+
};
|
|
318
|
+
return { contentKey: hkdf(sha256, ikm, opts.sessionId, info(INFO_CONTENT_KEY), SESSION_KEY_BYTES) };
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=session-key-agreement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-key-agreement.js","sourceRoot":"","sources":["../src/session-key-agreement.ts"],"names":[],"mappings":"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"}
|