yakmesh 2.8.2 → 3.0.0

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.
Files changed (232) hide show
  1. package/CHANGELOG.md +637 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/Caddyfile +77 -0
  4. package/README.md +119 -29
  5. package/adapters/adapter-mlv-bible/README.md +124 -0
  6. package/adapters/adapter-mlv-bible/index.js +400 -0
  7. package/adapters/chat-mod-adapter.js +532 -0
  8. package/adapters/content-adapter.js +273 -0
  9. package/content/api.js +50 -41
  10. package/content/index.js +2 -2
  11. package/content/store.js +355 -173
  12. package/dashboard/index.html +19 -3
  13. package/database/replication.js +117 -37
  14. package/docs/CRYPTO-AGILITY.md +204 -0
  15. package/docs/MTLS-RESEARCH.md +367 -0
  16. package/docs/NAMCHE-SPEC.md +681 -0
  17. package/docs/PEERQUANTA-YAKMESH-INTEGRATION.md +407 -0
  18. package/docs/PRECISION-DISCLOSURE.md +96 -0
  19. package/docs/README.md +76 -0
  20. package/docs/ROADMAP-2.4.0.md +447 -0
  21. package/docs/ROADMAP-2.5.0.md +244 -0
  22. package/docs/SECURITY-AUDIT-REPORT.md +306 -0
  23. package/docs/SST-INTEGRATION.md +712 -0
  24. package/docs/STEADYWATCH-IMPLEMENTATION.md +303 -0
  25. package/docs/TERNARY-AUDIT-REPORT.md +247 -0
  26. package/docs/TME-FAQ.md +221 -0
  27. package/docs/WHITEPAPER.md +623 -0
  28. package/docs/adapters.html +1001 -0
  29. package/docs/advanced-systems.html +1045 -0
  30. package/docs/annex.html +1046 -0
  31. package/docs/api.html +970 -0
  32. package/docs/business/response-templates.md +160 -0
  33. package/docs/c2c.html +1225 -0
  34. package/docs/cli.html +1332 -0
  35. package/docs/configuration.html +1248 -0
  36. package/docs/darshan.html +1085 -0
  37. package/docs/dharma.html +966 -0
  38. package/docs/docs-bundle.html +1075 -0
  39. package/docs/docs.css +3120 -0
  40. package/docs/docs.js +556 -0
  41. package/docs/doko.html +969 -0
  42. package/docs/geo-proof.html +858 -0
  43. package/docs/getting-started.html +840 -0
  44. package/docs/gumba-tutorial.html +1144 -0
  45. package/docs/gumba.html +1098 -0
  46. package/docs/index.html +914 -0
  47. package/docs/jhilke.html +1312 -0
  48. package/docs/karma.html +1100 -0
  49. package/docs/katha.html +1037 -0
  50. package/docs/lama.html +978 -0
  51. package/docs/mandala.html +1067 -0
  52. package/docs/mani.html +964 -0
  53. package/docs/mantra.html +967 -0
  54. package/docs/mesh.html +1409 -0
  55. package/docs/nakpak.html +869 -0
  56. package/docs/namche.html +928 -0
  57. package/docs/nav-order.json +53 -0
  58. package/docs/prahari.html +1043 -0
  59. package/docs/prism-bash.min.js +1 -0
  60. package/docs/prism-javascript.min.js +1 -0
  61. package/docs/prism-json.min.js +1 -0
  62. package/docs/prism-tomorrow.min.css +1 -0
  63. package/docs/prism.min.js +1 -0
  64. package/docs/privacy.html +699 -0
  65. package/docs/quick-reference.html +1181 -0
  66. package/docs/sakshi.html +1402 -0
  67. package/docs/sandboxing.md +386 -0
  68. package/docs/seva.html +911 -0
  69. package/docs/sherpa.html +871 -0
  70. package/docs/studio.html +860 -0
  71. package/docs/stupa.html +995 -0
  72. package/docs/tailwind.min.css +2 -0
  73. package/docs/tattva.html +1332 -0
  74. package/docs/terms.html +686 -0
  75. package/docs/time-server-deployment.md +166 -0
  76. package/docs/time-sources.html +1392 -0
  77. package/docs/tivra.html +1127 -0
  78. package/docs/trademark-policy.html +686 -0
  79. package/docs/tribhuj.html +1183 -0
  80. package/docs/trust-security.html +1029 -0
  81. package/docs/tutorials/backup-recovery.html +654 -0
  82. package/docs/tutorials/dashboard.html +604 -0
  83. package/docs/tutorials/domain-setup.html +605 -0
  84. package/docs/tutorials/host-website.html +456 -0
  85. package/docs/tutorials/mesh-network.html +505 -0
  86. package/docs/tutorials/mobile-access.html +445 -0
  87. package/docs/tutorials/privacy.html +467 -0
  88. package/docs/tutorials/raspberry-pi.html +600 -0
  89. package/docs/tutorials/security-basics.html +539 -0
  90. package/docs/tutorials/share-files.html +431 -0
  91. package/docs/tutorials/troubleshooting.html +637 -0
  92. package/docs/tutorials/trust-karma.html +419 -0
  93. package/docs/tutorials/yak-protocol.html +456 -0
  94. package/docs/tutorials.html +1034 -0
  95. package/docs/vani.html +1270 -0
  96. package/docs/webserver.html +809 -0
  97. package/docs/yak-protocol.html +940 -0
  98. package/docs/yak-timeserver-design.md +475 -0
  99. package/docs/yakapp.html +1015 -0
  100. package/docs/ypc27.html +1069 -0
  101. package/docs/yurt.html +1344 -0
  102. package/embedded-docs/bundle.js +334 -74
  103. package/gossip/protocol.js +247 -27
  104. package/identity/key-resolver.js +262 -0
  105. package/identity/machine-seed.js +632 -0
  106. package/identity/node-key.js +669 -368
  107. package/identity/tribhuj-ratchet.js +506 -0
  108. package/knowledge-base.js +37 -8
  109. package/launcher/yakmesh.bat +62 -0
  110. package/launcher/yakmesh.sh +70 -0
  111. package/mesh/annex.js +462 -108
  112. package/mesh/beacon-broadcast.js +113 -1
  113. package/mesh/darshan.js +1718 -0
  114. package/mesh/gumba.js +1567 -0
  115. package/mesh/jhilke.js +651 -0
  116. package/mesh/katha.js +1012 -0
  117. package/mesh/nakpak-routing.js +8 -5
  118. package/mesh/network.js +724 -34
  119. package/mesh/pulse-sync.js +4 -1
  120. package/mesh/rate-limiter.js +127 -15
  121. package/mesh/seva.js +526 -0
  122. package/mesh/sherpa-discovery.js +89 -8
  123. package/mesh/sybil-defense.js +19 -5
  124. package/mesh/temporal-encoder.js +4 -3
  125. package/mesh/vani.js +1364 -0
  126. package/mesh/yurt.js +1340 -0
  127. package/models/entropy-sentinel.onnx +0 -0
  128. package/models/karma-trust.onnx +0 -0
  129. package/models/manifest.json +43 -0
  130. package/models/sakshi-anomaly.onnx +0 -0
  131. package/oracle/code-proof-protocol.js +7 -6
  132. package/oracle/codebase-lock.js +257 -28
  133. package/oracle/index.js +74 -15
  134. package/oracle/ma902-snmp.js +678 -0
  135. package/oracle/module-sealer.js +5 -3
  136. package/oracle/network-identity.js +16 -0
  137. package/oracle/packet-checksum.js +201 -0
  138. package/oracle/sst.js +579 -0
  139. package/oracle/ternary-144t.js +714 -0
  140. package/oracle/ternary-ml.js +481 -0
  141. package/oracle/time-api.js +239 -0
  142. package/oracle/time-source.js +137 -47
  143. package/oracle/validation-oracle-hardened.js +1111 -1071
  144. package/oracle/validation-oracle.js +4 -2
  145. package/oracle/ypc27.js +211 -0
  146. package/package.json +20 -3
  147. package/protocol/yak-handler.js +35 -9
  148. package/protocol/yak-protocol.js +28 -13
  149. package/reference/cpp/yakmesh_mceliece_shard.cpp +168 -0
  150. package/reference/cpp/yakmesh_ypc27.cpp +179 -0
  151. package/sbom.json +87 -0
  152. package/scripts/security-audit.mjs +264 -0
  153. package/scripts/update-docs-nav.js +194 -0
  154. package/scripts/update-docs-sidebar.cjs +164 -0
  155. package/security/crypto-config.js +4 -3
  156. package/security/dharma-moderation.js +517 -0
  157. package/security/doko-identity.js +193 -143
  158. package/security/domain-consensus.js +86 -85
  159. package/security/fs-hardening.js +620 -0
  160. package/security/hardware-attestation.js +5 -3
  161. package/security/hybrid-trust.js +227 -87
  162. package/security/karma-rate-limiter.js +692 -0
  163. package/security/khata-protocol.js +22 -21
  164. package/security/khata-trust-integration.js +277 -150
  165. package/security/memory-safety.js +635 -0
  166. package/security/mesh-auth.js +11 -10
  167. package/security/mesh-revocation.js +373 -5
  168. package/security/namche-gateway.js +298 -69
  169. package/security/sakshi.js +460 -3
  170. package/security/sangha.js +770 -0
  171. package/security/secure-config.js +473 -0
  172. package/security/silicon-parity.js +13 -10
  173. package/security/steadywatch.js +1142 -0
  174. package/security/strike-system.js +32 -3
  175. package/security/temporal-signing.js +488 -0
  176. package/security/trit-commitment.js +464 -0
  177. package/server/crypto/annex.js +247 -0
  178. package/server/darshan-api.js +343 -0
  179. package/server/index.js +3259 -362
  180. package/server/komm-api.js +668 -0
  181. package/utils/accel.js +2273 -0
  182. package/utils/ternary-id.js +79 -0
  183. package/utils/verify-worker.js +57 -0
  184. package/webserver/index.js +95 -5
  185. package/assets/yakmesh-logo.png +0 -0
  186. package/assets/yakmesh-logo.svg +0 -80
  187. package/assets/yakmesh-logo2.png +0 -0
  188. package/assets/yakmesh-logo2sm.png +0 -0
  189. package/assets/ymsm.png +0 -0
  190. package/website/assets/silhouettes/adapters.svg +0 -107
  191. package/website/assets/silhouettes/api-endpoints.svg +0 -115
  192. package/website/assets/silhouettes/atomic-clock.svg +0 -83
  193. package/website/assets/silhouettes/base-camp.svg +0 -81
  194. package/website/assets/silhouettes/bridge.svg +0 -69
  195. package/website/assets/silhouettes/docs-bundle.svg +0 -113
  196. package/website/assets/silhouettes/doko-basket.svg +0 -70
  197. package/website/assets/silhouettes/fortress.svg +0 -93
  198. package/website/assets/silhouettes/gateway.svg +0 -54
  199. package/website/assets/silhouettes/gears.svg +0 -93
  200. package/website/assets/silhouettes/globe-satellite.svg +0 -67
  201. package/website/assets/silhouettes/karma-wheel.svg +0 -137
  202. package/website/assets/silhouettes/lama-council.svg +0 -141
  203. package/website/assets/silhouettes/mandala-network.svg +0 -169
  204. package/website/assets/silhouettes/mani-stones.svg +0 -149
  205. package/website/assets/silhouettes/mantra-wheel.svg +0 -116
  206. package/website/assets/silhouettes/mesh-nodes.svg +0 -113
  207. package/website/assets/silhouettes/nakpak.svg +0 -56
  208. package/website/assets/silhouettes/peak-lightning.svg +0 -73
  209. package/website/assets/silhouettes/sherpa.svg +0 -69
  210. package/website/assets/silhouettes/stupa-tower.svg +0 -119
  211. package/website/assets/silhouettes/tattva-eye.svg +0 -78
  212. package/website/assets/silhouettes/terminal.svg +0 -74
  213. package/website/assets/silhouettes/webserver.svg +0 -145
  214. package/website/assets/silhouettes/yak.svg +0 -78
  215. package/website/assets/yakmesh-logo.png +0 -0
  216. package/website/assets/yakmesh-logo.webp +0 -0
  217. package/website/assets/yakmesh-logo128x140.webp +0 -0
  218. package/website/assets/yakmesh-logo2.png +0 -0
  219. package/website/assets/yakmesh-logo2.svg +0 -51
  220. package/website/assets/yakmesh-logo40x44.webp +0 -0
  221. package/website/assets/yakmesh.gif +0 -0
  222. package/website/assets/yakmesh.ico +0 -0
  223. package/website/assets/yakmesh.jpg +0 -0
  224. package/website/assets/yakmesh.pdf +0 -0
  225. package/website/assets/yakmesh.png +0 -0
  226. package/website/assets/yakmesh.svg +0 -70
  227. package/website/assets/yakmesh128.webp +0 -0
  228. package/website/assets/yakmesh32.png +0 -0
  229. package/website/assets/yakmesh32.svg +0 -65
  230. package/website/assets/yakmesh32o.ico +0 -2
  231. package/website/assets/yakmesh32o.svg +0 -65
  232. package/website/assets/yakmesh32o.svgz +0 -0
@@ -1,368 +1,669 @@
1
- /**
2
- * Yakmesh Node Identity - Post-Quantum Secure
3
- * Uses ML-DSA-65 (Dilithium3) from FIPS 204
4
- *
5
- * Security Level: NIST Level 3 (~192-bit classical security)
6
- * Quantum Resistant: Yes (lattice-based)
7
- *
8
- * ═══════════════════════════════════════════════════════════════════════════════
9
- * ⚠️ SECURITY ARCHITECTURE - DO NOT MODIFY WITHOUT EXPLICIT REVIEW ⚠️
10
- * ═══════════════════════════════════════════════════════════════════════════════
11
- *
12
- * IMPORTANT: Node IDs are composed of TWO parts:
13
- * 1. NETWORK NAME - Derived from codebase hash (SAME for all nodes on network)
14
- * 2. INSTANCE ID - Derived from public key hash (UNIQUE per node instance)
15
- *
16
- * This design ensures:
17
- * - Nodes running identical code share the same network name
18
- * - Each node instance has a unique identifier
19
- * - Human-readable verification: same network name = same code = can peer
20
- * - Codebase integrity is cryptographically bound to identity
21
- *
22
- * Format: "node-[network-name]-[instance-id]"
23
- * Example: "node-qubit-lattice-prism-pq-a7x9"
24
- * ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^
25
- * Same for all nodes Unique per instance
26
- *
27
- * ═══════════════════════════════════════════════════════════════════════════════
28
- * 🚫 REJECTED SIMPLIFICATIONS (Security Anti-Patterns):
29
- * ═══════════════════════════════════════════════════════════════════════════════
30
- *
31
- * The following "simplifications" have been proposed and MUST BE REJECTED:
32
- *
33
- * ❌ NodeID = SHA3-256(publicKey)
34
- * WHY REJECT: Removes codebase integrity verification. A malicious node
35
- * running modified code would be indistinguishable from honest nodes.
36
- *
37
- * ❌ NodeID = base64(publicKey)
38
- * WHY REJECT: Same as above, plus loses human-readable verification.
39
- *
40
- * ❌ NodeID = UUID or random string
41
- * WHY REJECT: Breaks deterministic derivation. Cannot verify identity
42
- * matches public key. Enables identity spoofing.
43
- *
44
- * ❌ Removing the codebase hash component
45
- * WHY REJECT: Fundamental to network segmentation by code version.
46
- * Prevents incompatible or malicious code from joining honest networks.
47
- *
48
- * The current two-part design (network + instance) via iO obfuscation is
49
- * INTENTIONAL and provides security properties that simpler schemes lack.
50
- *
51
- * This warning added 2026-01-18 after a spec draft attempted to simplify
52
- * NodeID to just SHA3-256(publicKey), which would have undermined the
53
- * entire network integrity model.
54
- *
55
- * ═══════════════════════════════════════════════════════════════════════════════
56
- *
57
- * @module identity/node-key
58
- * @version 1.6.0
59
- */
60
-
61
- import { createLogger } from '../utils/logger.js';
62
-
63
- const log = createLogger('identity:node-key');
64
-
65
- import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
66
- import { sha3_256 } from '@noble/hashes/sha3.js';
67
- import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
68
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
69
- import { join } from 'path';
70
-
71
- // Import iO network identity for obfuscated node IDs
72
- import { deriveNetworkName, deriveNetworkId } from '../oracle/network-identity.js';
73
-
74
- // Cached codebase hash - set during first identity generation
75
- let cachedCodebaseHash = null;
76
-
77
- /**
78
- * Set the codebase hash for node identity generation
79
- * This MUST be called before generating any node IDs
80
- *
81
- * @param {string} hash - The codebase hash from the validation oracle
82
- */
83
- export function setCodebaseHash(hash) {
84
- if (cachedCodebaseHash && cachedCodebaseHash !== hash) {
85
- console.warn('⚠️ Codebase hash changed - network identity will change!');
86
- }
87
- cachedCodebaseHash = hash;
88
- }
89
-
90
- /**
91
- * Get the current codebase hash
92
- * @returns {string|null} The codebase hash or null if not set
93
- */
94
- export function getCodebaseHash() {
95
- return cachedCodebaseHash;
96
- }
97
-
98
- /**
99
- * Generate a node ID using iO obfuscation
100
- *
101
- * The node ID is composed of:
102
- * 1. Network name - derived from CODEBASE hash (same for all nodes on network)
103
- * 2. Instance ID - derived from PUBLIC KEY hash (unique per node)
104
- *
105
- * @param {Uint8Array} publicKey - The node's public key
106
- * @param {string} codebaseHash - The codebase hash (optional, uses cached if not provided)
107
- * @returns {string} Node ID like "node-qubit-lattice-prism-pq-a7x9"
108
- */
109
- export function generateNodeId(publicKey, codebaseHash = null) {
110
- const effectiveCodebaseHash = codebaseHash || cachedCodebaseHash;
111
-
112
- if (!effectiveCodebaseHash) {
113
- throw new Error(
114
- 'Codebase hash not set! Call setCodebaseHash() with the oracle\'s selfHash ' +
115
- 'before generating node identities. This ensures all nodes on the same ' +
116
- 'network share the same network name.'
117
- );
118
- }
119
-
120
- // NETWORK NAME: Derived from codebase hash
121
- // This is the SAME for all nodes running identical code
122
- const networkName = deriveNetworkName(effectiveCodebaseHash, 3);
123
-
124
- // INSTANCE ID: Derived from public key hash
125
- // This is UNIQUE per node instance
126
- const publicKeyHash = sha3_256(publicKey);
127
- const publicKeyHashHex = bytesToHex(publicKeyHash);
128
- const instanceId = deriveNetworkId(publicKeyHashHex);
129
-
130
- // Format: "node-[network-name]-[instance-id]"
131
- // e.g., "node-qubit-lattice-prism-pq-a7x9"
132
- return `node-${networkName}-${instanceId}`;
133
- }
134
-
135
- /**
136
- * Generate internal hash for private operations (NOT exposed externally)
137
- * This is kept for signature verification and internal lookups
138
- */
139
- export function generateInternalHash(publicKey) {
140
- const hash = sha3_256(publicKey);
141
- return bytesToHex(hash.slice(0, 16));
142
- }
143
-
144
- /**
145
- * Generate ML-DSA-65 (Dilithium3) keypair for node identity
146
- * This is NIST FIPS 204 standardized post-quantum signature
147
- */
148
- export function generateKeyPair() {
149
- const seed = crypto.getRandomValues(new Uint8Array(32));
150
- const keyPair = ml_dsa65.keygen(seed);
151
- return {
152
- publicKey: bytesToHex(keyPair.publicKey),
153
- secretKey: bytesToHex(keyPair.secretKey),
154
- algorithm: 'ML-DSA-65',
155
- nistLevel: 3,
156
- };
157
- }
158
-
159
- /**
160
- * Sign a message with the node's secret key
161
- */
162
- export function signMessage(message, secretKeyHex) {
163
- const secretKey = hexToBytes(secretKeyHex);
164
- const messageBytes = typeof message === 'string'
165
- ? new TextEncoder().encode(message)
166
- : message;
167
- // ml_dsa65.sign takes (message, secretKey)
168
- const signature = ml_dsa65.sign(messageBytes, secretKey);
169
- return bytesToHex(signature);
170
- }
171
-
172
- /**
173
- * Verify a signature from another node
174
- */
175
- export function verifySignature(message, signatureHex, publicKeyHex) {
176
- try {
177
- const publicKey = hexToBytes(publicKeyHex);
178
- const signature = hexToBytes(signatureHex);
179
- const messageBytes = typeof message === 'string'
180
- ? new TextEncoder().encode(message)
181
- : message;
182
- // ml_dsa65.verify takes (signature, message, publicKey)
183
- return ml_dsa65.verify(signature, messageBytes, publicKey);
184
- } catch (e) {
185
- console.error('Signature verification failed:', e.message);
186
- return false;
187
- }
188
- }
189
-
190
- /**
191
- * Node Identity Manager
192
- * Handles persistent storage and management of node identity
193
- *
194
- * IMPORTANT: The oracle must be initialized before creating node identities
195
- * to ensure the codebase hash is available for network name derivation.
196
- */
197
- export class NodeIdentity {
198
- constructor(dataDir = './data') {
199
- this.dataDir = dataDir;
200
- this.keyPath = join(dataDir, 'node-key.json');
201
- this.identity = null;
202
- this.networkName = null;
203
- this.verificationPhrase = null;
204
- }
205
-
206
- /**
207
- * Initialize or load node identity
208
- *
209
- * @param {string} nodeName - Human-readable node name
210
- * @param {string} region - Node region/location
211
- * @param {Object} oracle - The validation oracle instance (provides codebase hash)
212
- */
213
- async init(nodeName = 'Yakmesh Node', region = 'local', oracle = null) {
214
- // Ensure data directory exists
215
- if (!existsSync(this.dataDir)) {
216
- mkdirSync(this.dataDir, { recursive: true });
217
- }
218
-
219
- // Get codebase hash from oracle if provided
220
- let codebaseHash = cachedCodebaseHash;
221
- if (oracle) {
222
- const proof = oracle.generateCodeProof();
223
- codebaseHash = proof.selfHash;
224
- setCodebaseHash(codebaseHash);
225
-
226
- // Import and store network identity info
227
- const { deriveVerificationPhrase } = await import('../oracle/network-identity.js');
228
- this.networkName = deriveNetworkName(codebaseHash, 3);
229
- this.verificationPhrase = deriveVerificationPhrase(codebaseHash);
230
- }
231
-
232
- // Load existing identity or generate new one
233
- if (existsSync(this.keyPath)) {
234
- const data = JSON.parse(readFileSync(this.keyPath, 'utf8'));
235
- this.identity = data;
236
-
237
- // Check if identity needs regeneration (codebase changed)
238
- if (codebaseHash && this.identity.codebaseHash &&
239
- this.identity.codebaseHash !== codebaseHash) {
240
- log.warn('Codebase changed - regenerating node identity', {
241
- oldNetwork: this.identity.networkName || 'unknown',
242
- newNetwork: this.networkName
243
- });
244
- // Delete old identity and regenerate
245
- this.identity = null;
246
- } else {
247
- log.info('Loaded node identity', {
248
- nodeId: this.identity.nodeId,
249
- network: this.identity.networkName || this.networkName || 'unknown',
250
- algorithm: this.identity.algorithm,
251
- nistLevel: this.identity.nistLevel,
252
- verificationPhrase: this.verificationPhrase || undefined
253
- });
254
- return this.identity;
255
- }
256
- }
257
-
258
- // Generate new identity
259
- if (!codebaseHash) {
260
- throw new Error(
261
- 'Cannot generate node identity: codebase hash not available. ' +
262
- 'Pass the oracle instance to init() or call setCodebaseHash() first.'
263
- );
264
- }
265
-
266
- log.info('Generating post-quantum keypair (ML-DSA-65)');
267
- const keyPair = generateKeyPair();
268
- const publicKeyBytes = hexToBytes(keyPair.publicKey);
269
- const nodeId = generateNodeId(publicKeyBytes, codebaseHash);
270
-
271
- this.identity = {
272
- nodeId,
273
- networkName: this.networkName,
274
- verificationPhrase: this.verificationPhrase,
275
- codebaseHash, // Store for change detection
276
- name: nodeName,
277
- region,
278
- publicKey: keyPair.publicKey,
279
- secretKey: keyPair.secretKey,
280
- algorithm: keyPair.algorithm,
281
- nistLevel: keyPair.nistLevel,
282
- createdAt: Date.now(),
283
- capabilities: ['listings', 'chat', 'forum', 'qcoa'],
284
- };
285
-
286
- writeFileSync(this.keyPath, JSON.stringify(this.identity, null, 2));
287
- log.info('Generated new node identity', {
288
- nodeId,
289
- network: this.networkName,
290
- algorithm: 'ML-DSA-65 (FIPS 204, NIST Level 3)',
291
- publicKeySize: keyPair.publicKey.length / 2,
292
- verificationPhrase: this.verificationPhrase || undefined
293
- });
294
-
295
- return this.identity;
296
- }
297
-
298
- /**
299
- * Get public identity (safe to share with other nodes)
300
- * Includes network identity for peer verification
301
- */
302
- getPublicIdentity() {
303
- if (!this.identity) throw new Error('Identity not initialized');
304
-
305
- return {
306
- nodeId: this.identity.nodeId,
307
- networkName: this.identity.networkName || this.networkName,
308
- verificationPhrase: this.identity.verificationPhrase || this.verificationPhrase,
309
- name: this.identity.name,
310
- region: this.identity.region,
311
- publicKey: this.identity.publicKey,
312
- algorithm: this.identity.algorithm,
313
- nistLevel: this.identity.nistLevel,
314
- capabilities: this.identity.capabilities,
315
- createdAt: this.identity.createdAt,
316
- };
317
- }
318
-
319
- /**
320
- * Get network identity info (for human verification)
321
- * All nodes on the same network should have matching values
322
- */
323
- getNetworkIdentity() {
324
- return {
325
- networkName: this.identity?.networkName || this.networkName,
326
- verificationPhrase: this.identity?.verificationPhrase || this.verificationPhrase,
327
- };
328
- }
329
-
330
- /**
331
- * Sign a message with this node's identity
332
- */
333
- sign(message) {
334
- if (!this.identity) throw new Error('Identity not initialized');
335
- return signMessage(message, this.identity.secretKey);
336
- }
337
-
338
- /**
339
- * Verify a message signature from another node
340
- */
341
- verify(message, signature, publicKey) {
342
- return verifySignature(message, signature, publicKey);
343
- }
344
-
345
- /**
346
- * Sign a JSON object (for mesh protocol messages)
347
- */
348
- signObject(obj) {
349
- const payload = JSON.stringify(obj);
350
- return {
351
- ...obj,
352
- _signature: this.sign(payload),
353
- _signer: this.identity.nodeId,
354
- _signedAt: Date.now(),
355
- };
356
- }
357
-
358
- /**
359
- * Verify a signed object from another node
360
- */
361
- verifyObject(signedObj, publicKey) {
362
- const { _signature, _signer, _signedAt, ...obj } = signedObj;
363
- const payload = JSON.stringify(obj);
364
- return this.verify(payload, _signature, publicKey);
365
- }
366
- }
367
-
368
- export default NodeIdentity;
1
+ /**
2
+ * Yakmesh Node Identity - Post-Quantum Secure
3
+ * Uses ML-DSA-65 (Dilithium3) from FIPS 204
4
+ *
5
+ * Security Level: NIST Level 3 (~192-bit classical security)
6
+ * Quantum Resistant: Yes (lattice-based)
7
+ *
8
+ * ═══════════════════════════════════════════════════════════════════════════════
9
+ * ⚠️ SECURITY ARCHITECTURE - DO NOT MODIFY WITHOUT EXPLICIT REVIEW ⚠️
10
+ * ═══════════════════════════════════════════════════════════════════════════════
11
+ *
12
+ * IMPORTANT: Node IDs are composed of TWO parts:
13
+ * 1. NETWORK NAME - Derived from codebase hash (SAME for all nodes on network)
14
+ * 2. INSTANCE ID - Derived from public key hash (UNIQUE per node instance)
15
+ *
16
+ * This design ensures:
17
+ * - Nodes running identical code share the same network name
18
+ * - Each node instance has a unique identifier
19
+ * - Human-readable verification: same network name = same code = can peer
20
+ * - Codebase integrity is cryptographically bound to identity
21
+ *
22
+ * Format: "node-[network-name]-[instance-id]"
23
+ * Example: "node-qubit-lattice-prism-pq-a7x9"
24
+ * ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^
25
+ * Same for all nodes Unique per instance
26
+ *
27
+ * ═══════════════════════════════════════════════════════════════════════════════
28
+ * 🚫 REJECTED SIMPLIFICATIONS (Security Anti-Patterns):
29
+ * ═══════════════════════════════════════════════════════════════════════════════
30
+ *
31
+ * The following "simplifications" have been proposed and MUST BE REJECTED:
32
+ *
33
+ * ❌ NodeID = SHA3-256(publicKey)
34
+ * WHY REJECT: Removes codebase integrity verification. A malicious node
35
+ * running modified code would be indistinguishable from honest nodes.
36
+ *
37
+ * ❌ NodeID = base64(publicKey)
38
+ * WHY REJECT: Same as above, plus loses human-readable verification.
39
+ *
40
+ * ❌ NodeID = UUID or random string
41
+ * WHY REJECT: Breaks deterministic derivation. Cannot verify identity
42
+ * matches public key. Enables identity spoofing.
43
+ *
44
+ * ❌ Removing the codebase hash component
45
+ * WHY REJECT: Fundamental to network segmentation by code version.
46
+ * Prevents incompatible or malicious code from joining honest networks.
47
+ *
48
+ * The current two-part design (network + instance) via iO obfuscation is
49
+ * INTENTIONAL and provides security properties that simpler schemes lack.
50
+ *
51
+ * This warning added 2026-01-18 after a spec draft attempted to simplify
52
+ * NodeID to just SHA3-256(publicKey), which would have undermined the
53
+ * entire network integrity model.
54
+ *
55
+ * ═══════════════════════════════════════════════════════════════════════════════
56
+ *
57
+ * @module identity/node-key
58
+ * @version 2.0.0 — Two-Layer Deterministic Identity
59
+ */
60
+
61
+ import { createLogger } from '../utils/logger.js';
62
+
63
+ const log = createLogger('identity:node-key');
64
+
65
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
66
+ import { sha3_256 as _nobleSha3 } from '@noble/hashes/sha3.js';
67
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
68
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, unlinkSync } from 'fs';
69
+ import { join } from 'path';
70
+ import { randomBytes } from 'crypto';
71
+ import { platform } from 'os';
72
+
73
+ // ACCEL: Hardware-accelerated crypto (native SHA3 via OpenSSL/SHA-NI, future liboqs)
74
+ import { sha3_256, mlDsa65Sign, mlDsa65Verify } from '../utils/accel.js';
75
+
76
+ // Import iO network identity for obfuscated node IDs
77
+ import { deriveNetworkName, deriveNetworkId, deriveVerificationPhrase } from '../oracle/network-identity.js';
78
+
79
+ // SLH-DSA (FIPS 205) backup signature defense-in-depth against lattice breaks
80
+ import {
81
+ generateBackupSignatureKeyPair,
82
+ signBackup as slhDsaSign,
83
+ verifyBackup as slhDsaVerify,
84
+ } from '../security/crypto-config.js';
85
+
86
+ // Two-Layer Identity: Machine seed + deterministic derivation
87
+ import { MachineSeed, deriveNodeSecret, deriveBackupSecret } from './machine-seed.js';
88
+
89
+ // Cached codebase hash - set during first identity generation
90
+ let cachedCodebaseHash = null;
91
+
92
+ /**
93
+ * Set the codebase hash for node identity generation
94
+ * This MUST be called before generating any node IDs
95
+ *
96
+ * @param {string} hash - The codebase hash from the validation oracle
97
+ */
98
+ export function setCodebaseHash(hash) {
99
+ if (cachedCodebaseHash && cachedCodebaseHash !== hash) {
100
+ console.warn('⚠️ Codebase hash changed - network identity will change!');
101
+ }
102
+ cachedCodebaseHash = hash;
103
+ }
104
+
105
+ /**
106
+ * Get the current codebase hash
107
+ * @returns {string|null} The codebase hash or null if not set
108
+ */
109
+ export function getCodebaseHash() {
110
+ return cachedCodebaseHash;
111
+ }
112
+
113
+ /**
114
+ * Generate a node ID using iO obfuscation
115
+ *
116
+ * The node ID is composed of:
117
+ * 1. Network name - derived from CODEBASE hash (same for all nodes on network)
118
+ * 2. Instance ID - derived from PUBLIC KEY hash (unique per node)
119
+ *
120
+ * @param {Uint8Array} publicKey - The node's public key
121
+ * @param {string} codebaseHash - The codebase hash (optional, uses cached if not provided)
122
+ * @returns {string} Node ID like "node-qubit-lattice-prism-pq-a7x9"
123
+ */
124
+ export function generateNodeId(publicKey, codebaseHash = null) {
125
+ const effectiveCodebaseHash = codebaseHash || cachedCodebaseHash;
126
+
127
+ if (!effectiveCodebaseHash) {
128
+ throw new Error(
129
+ 'Codebase hash not set! Call setCodebaseHash() with the oracle\'s selfHash ' +
130
+ 'before generating node identities. This ensures all nodes on the same ' +
131
+ 'network share the same network name.'
132
+ );
133
+ }
134
+
135
+ // NETWORK NAME: Derived from codebase hash
136
+ // This is the SAME for all nodes running identical code
137
+ const networkName = deriveNetworkName(effectiveCodebaseHash, 3);
138
+
139
+ // INSTANCE ID: Derived from public key hash
140
+ // This is UNIQUE per node instance
141
+ const publicKeyHash = sha3_256(publicKey);
142
+ const publicKeyHashHex = bytesToHex(publicKeyHash);
143
+ const instanceId = deriveNetworkId(publicKeyHashHex);
144
+
145
+ // Format: "node-[network-name]-[instance-id]"
146
+ // e.g., "node-qubit-lattice-prism-pq-a7x9"
147
+ return `node-${networkName}-${instanceId}`;
148
+ }
149
+
150
+ /**
151
+ * Generate internal hash for private operations (NOT exposed externally)
152
+ * This is kept for signature verification and internal lookups
153
+ */
154
+ export function generateInternalHash(publicKey) {
155
+ const hash = sha3_256(publicKey);
156
+ return bytesToHex(hash.slice(0, 16));
157
+ }
158
+
159
+ /**
160
+ * Generate ML-DSA-65 (Dilithium3) keypair for node identity.
161
+ *
162
+ * v2.0: Accepts an optional deterministic seed. When provided, the keypair
163
+ * is fully deterministic — same seed always produces the same keys.
164
+ * When omitted, falls back to random generation (for testing/standalone use).
165
+ *
166
+ * @param {Uint8Array} [deterministicSeed] - 32-byte seed from HKDF derivation
167
+ * @returns {{ publicKey: string, secretKey: string, algorithm: string, nistLevel: number }}
168
+ */
169
+ export function generateKeyPair(deterministicSeed = null) {
170
+ const seed = deterministicSeed || crypto.getRandomValues(new Uint8Array(32));
171
+ const keyPair = ml_dsa65.keygen(seed);
172
+ return {
173
+ publicKey: bytesToHex(keyPair.publicKey),
174
+ secretKey: bytesToHex(keyPair.secretKey),
175
+ algorithm: 'ML-DSA-65',
176
+ nistLevel: 3,
177
+ deterministic: !!deterministicSeed,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Sign a message with the node's secret key
183
+ */
184
+ export function signMessage(message, secretKeyHex) {
185
+ const secretKey = hexToBytes(secretKeyHex);
186
+ const messageBytes = typeof message === 'string'
187
+ ? new TextEncoder().encode(message)
188
+ : message;
189
+ // ml_dsa65.sign takes (message, secretKey) — ACCEL-accelerated
190
+ const signature = mlDsa65Sign(messageBytes, secretKey);
191
+ return bytesToHex(signature);
192
+ }
193
+
194
+ /**
195
+ * Verify a signature from another node
196
+ */
197
+ export function verifySignature(message, signatureHex, publicKeyHex) {
198
+ try {
199
+ const publicKey = hexToBytes(publicKeyHex);
200
+ const signature = hexToBytes(signatureHex);
201
+ const messageBytes = typeof message === 'string'
202
+ ? new TextEncoder().encode(message)
203
+ : message;
204
+ // ml_dsa65.verify takes (signature, message, publicKey) — ACCEL-accelerated
205
+ return mlDsa65Verify(signature, messageBytes, publicKey);
206
+ } catch (e) {
207
+ console.error('Signature verification failed:', e.message);
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Node Identity Manager
214
+ * Handles persistent storage and management of node identity
215
+ *
216
+ * ═══════════════════════════════════════════════════════════════════════════════
217
+ * v2.0 — TWO-LAYER DETERMINISTIC IDENTITY
218
+ * ═══════════════════════════════════════════════════════════════════════════════
219
+ *
220
+ * The oracle MUST be initialized before creating node identities.
221
+ *
222
+ * Layer 1 (Network): oracleHash → networkName, networkId, verPhrase (SHARED)
223
+ * Layer 2 (Node): seed + oracleHash + verPhrase → HKDF → keypair (UNIQUE)
224
+ *
225
+ * Private keys are NEVER stored on disk. They are derived fresh each startup
226
+ * from the machine seed + codebase hash + verification phrase.
227
+ * ═══════════════════════════════════════════════════════════════════════════════
228
+ */
229
+ export class NodeIdentity {
230
+ constructor(dataDir = './data') {
231
+ this.dataDir = dataDir;
232
+ this.keyPath = join(dataDir, 'node-key.json'); // Public identity file
233
+ this.identity = null;
234
+ this.networkName = null;
235
+ this.verificationPhrase = null;
236
+ this.machineSeed = new MachineSeed(dataDir); // Layer 2 anchor
237
+ }
238
+
239
+ /**
240
+ * Set restrictive file permissions on key file (owner read/write only).
241
+ * On Windows this is a no-op (NTFS ACLs handle access differently).
242
+ */
243
+ _secureKeyFile() {
244
+ if (platform() !== 'win32') {
245
+ try {
246
+ chmodSync(this.keyPath, 0o600); // rw------- (owner only)
247
+ } catch (e) {
248
+ log.warn('Could not set restrictive permissions on key file', { error: e.message });
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Initialize or load node identity using two-layer deterministic derivation.
255
+ *
256
+ * Flow:
257
+ * 1. Get oracleHash → derive Layer 1 (networkName, verPhrase)
258
+ * 2. Load/create machine seed (Layer 2 anchor)
259
+ * 3. HKDF(seed, oracleHash, verPhrase) → deterministic keypair
260
+ * 4. Record migration entry (seed + oracleHash → pubKeyHash)
261
+ * 5. Store public identity to disk (private key stays in memory only)
262
+ *
263
+ * @param {string} nodeName - Human-readable node name
264
+ * @param {string} region - Node region/location
265
+ * @param {Object} oracle - The validation oracle instance (provides codebase hash)
266
+ */
267
+ async init(nodeName = 'Yakmesh Node', region = 'local', oracle = null) {
268
+ // Ensure data directory exists
269
+ if (!existsSync(this.dataDir)) {
270
+ mkdirSync(this.dataDir, { recursive: true });
271
+ }
272
+
273
+ // ─── Layer 1: Network Identity (shared, code-derived) ───
274
+ let codebaseHash = cachedCodebaseHash;
275
+ if (oracle) {
276
+ const proof = oracle.generateCodeProof();
277
+ codebaseHash = proof.selfHash;
278
+ setCodebaseHash(codebaseHash);
279
+ }
280
+
281
+ if (!codebaseHash) {
282
+ throw new Error(
283
+ 'Cannot initialize identity: codebase hash not available. ' +
284
+ 'Pass the oracle instance to init() or call setCodebaseHash() first.'
285
+ );
286
+ }
287
+
288
+ this.networkName = deriveNetworkName(codebaseHash, 3);
289
+ this.verificationPhrase = deriveVerificationPhrase(codebaseHash);
290
+
291
+ // ─── Layer 2: Machine Seed (unique, hardware-bound) ───
292
+ await this.machineSeed.init();
293
+
294
+ // ─── Legacy migration: detect old encrypted-private-key format ───
295
+ if (existsSync(this.keyPath)) {
296
+ const oldData = JSON.parse(readFileSync(this.keyPath, 'utf8'));
297
+ if (oldData.secretKeyEnc || oldData.secretKey) {
298
+ log.warn('═══════════════════════════════════════════════════════════');
299
+ log.warn('LEGACY IDENTITY DETECTED migrating to two-layer system');
300
+ log.warn('Old format: encrypted private key on disk');
301
+ log.warn('New format: deterministic derivation from machine seed');
302
+ log.warn('Old node-key.json will be archived as node-key.legacy.json');
303
+ log.warn('═══════════════════════════════════════════════════════════');
304
+ const legacyPath = join(this.dataDir, 'node-key.legacy.json');
305
+ writeFileSync(legacyPath, JSON.stringify(oldData, null, 2));
306
+ unlinkSync(this.keyPath);
307
+ }
308
+ }
309
+
310
+ // ─── Derive keypair deterministically ───
311
+ const seed = this.machineSeed.getSeed();
312
+ const nodeSecret = deriveNodeSecret(seed, codebaseHash, this.verificationPhrase);
313
+ const keyPair = generateKeyPair(nodeSecret);
314
+
315
+ const publicKeyBytes = hexToBytes(keyPair.publicKey);
316
+ const nodeId = generateNodeId(publicKeyBytes, codebaseHash);
317
+ const pubKeyHash = bytesToHex(sha3_256(publicKeyBytes));
318
+
319
+ // ─── Derive SLH-DSA backup keypair deterministically ───
320
+ const backupSecret = deriveBackupSecret(seed, codebaseHash, this.verificationPhrase);
321
+ // SLH-DSA keygen uses the first 32 bytes as seed material
322
+ const backupKeyPair = generateBackupSignatureKeyPair(backupSecret);
323
+ const backupPublicKeyHex = bytesToHex(backupKeyPair.publicKey);
324
+ const backupSecretKeyHex = bytesToHex(backupKeyPair.secretKey);
325
+
326
+ // ─── Record migration entry ───
327
+ this.machineSeed.recordMigration(codebaseHash, pubKeyHash, this.networkName);
328
+
329
+ // ─── Build in-memory identity (private keys NEVER touch disk) ───
330
+ this.identity = {
331
+ nodeId,
332
+ networkName: this.networkName,
333
+ verificationPhrase: this.verificationPhrase,
334
+ codebaseHash,
335
+ name: nodeName,
336
+ region,
337
+ publicKey: keyPair.publicKey,
338
+ secretKey: keyPair.secretKey, // IN MEMORY ONLY — derived, not stored
339
+ backupPublicKey: backupPublicKeyHex,
340
+ backupSecretKey: backupSecretKeyHex, // IN MEMORY ONLY — derived, not stored
341
+ algorithm: keyPair.algorithm,
342
+ backupAlgorithm: 'SLH-DSA-SHA2-192f',
343
+ nistLevel: keyPair.nistLevel,
344
+ deterministic: true,
345
+ createdAt: Date.now(),
346
+ capabilities: ['listings', 'chat', 'forum', 'qcoa'],
347
+ };
348
+
349
+ // ─── Store PUBLIC identity to disk (no secrets!) ───
350
+ const toStore = {
351
+ nodeId,
352
+ networkName: this.networkName,
353
+ verificationPhrase: this.verificationPhrase,
354
+ codebaseHash,
355
+ name: nodeName,
356
+ region,
357
+ publicKey: keyPair.publicKey,
358
+ backupPublicKey: backupPublicKeyHex,
359
+ algorithm: keyPair.algorithm,
360
+ backupAlgorithm: 'SLH-DSA-SHA2-192f',
361
+ nistLevel: keyPair.nistLevel,
362
+ deterministic: true,
363
+ migrationEntries: this.machineSeed.getMigrationChain().length,
364
+ createdAt: Date.now(),
365
+ };
366
+ writeFileSync(this.keyPath, JSON.stringify(toStore, null, 2));
367
+ this._secureKeyFile();
368
+
369
+ // ─── Log identity info ───
370
+ const migrationChain = this.machineSeed.getMigrationChain();
371
+ const isNewNetwork = this.machineSeed.isFirstRun() || migrationChain.length <= 1;
372
+ const previousId = this.machineSeed.getPreviousIdentity();
373
+
374
+ log.info(isNewNetwork ? 'Generated new deterministic identity' : 'Derived identity for network version', {
375
+ nodeId,
376
+ network: this.networkName,
377
+ algorithm: 'ML-DSA-65 (FIPS 204, NIST Level 3)',
378
+ backupAlgorithm: 'SLH-DSA-SHA2-192f (FIPS 205)',
379
+ derivation: 'HKDF-SHA3-256(seed + oracleHash + verPhrase)',
380
+ privateKeysOnDisk: false,
381
+ migrationChainLength: migrationChain.length,
382
+ previousNetwork: previousId?.networkName || 'none (first run)',
383
+ verificationPhrase: this.verificationPhrase,
384
+ });
385
+
386
+ return this.identity;
387
+ }
388
+
389
+ /**
390
+ * Get public identity (safe to share with other nodes)
391
+ * Includes network identity for peer verification
392
+ */
393
+ getPublicIdentity() {
394
+ if (!this.identity) throw new Error('Identity not initialized');
395
+
396
+ return {
397
+ nodeId: this.identity.nodeId,
398
+ persistentId: this.machineSeed.getPersistentId(), // Constant across upgrades
399
+ networkName: this.identity.networkName || this.networkName,
400
+ verificationPhrase: this.identity.verificationPhrase || this.verificationPhrase,
401
+ name: this.identity.name,
402
+ region: this.identity.region,
403
+ publicKey: this.identity.publicKey,
404
+ backupPublicKey: this.identity.backupPublicKey || null, // SLH-DSA (FIPS 205)
405
+ algorithm: this.identity.algorithm,
406
+ backupAlgorithm: this.identity.backupAlgorithm || null,
407
+ nistLevel: this.identity.nistLevel,
408
+ capabilities: this.identity.capabilities,
409
+ createdAt: this.identity.createdAt,
410
+ };
411
+ }
412
+
413
+ /**
414
+ * Get network identity info (for human verification)
415
+ * All nodes on the same network should have matching values
416
+ */
417
+ getNetworkIdentity() {
418
+ return {
419
+ networkName: this.identity?.networkName || this.networkName,
420
+ verificationPhrase: this.identity?.verificationPhrase || this.verificationPhrase,
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Get the migration chain — history of this seed's identities across network versions.
426
+ * @returns {Object[]} Array of { oracleHash, pubKeyHash, networkName, timestamp }
427
+ */
428
+ getMigrationChain() {
429
+ return this.machineSeed.getMigrationChain();
430
+ }
431
+
432
+ /**
433
+ * Get the persistent 144T machine identity.
434
+ * This ID is CONSTANT across all code upgrades — it identifies the
435
+ * physical machine/node owner regardless of network version.
436
+ *
437
+ * Use this to link reputation across network upgrades.
438
+ *
439
+ * @returns {string} Persistent 144T identifier like "yak-TT00TTT00:TTT00TTT0:..."
440
+ */
441
+ getPersistentId() {
442
+ return this.machineSeed.getPersistentId();
443
+ }
444
+
445
+ /**
446
+ * Generate a migration proof that cryptographically links the current identity
447
+ * to the previous identity on the migration chain.
448
+ *
449
+ * Both signatures together prove the same physical entity (seed) controls
450
+ * both identities — without revealing the seed.
451
+ *
452
+ * Peers verify: ML-DSA-65.verify(sigOld, "MIGRATE:"+newPubKey, oldPubKey)
453
+ * ML-DSA-65.verify(sigNew, "MIGRATE:"+oldPubKey, newPubKey)
454
+ *
455
+ * @param {string} oldOracleHash - Oracle hash of the previous network version
456
+ * @param {string} oldVerPhrase - Verification phrase of the previous network
457
+ * @returns {{ oldPubKey: string, newPubKey: string, sigOld: string, sigNew: string, timestamp: number } | null}
458
+ */
459
+ generateMigrationProof(oldOracleHash, oldVerPhrase) {
460
+ if (!this.identity) throw new Error('Identity not initialized');
461
+
462
+ const seed = this.machineSeed.getSeed();
463
+
464
+ // Derive the OLD keypair from the same seed + old oracle hash
465
+ const oldSecret = deriveNodeSecret(seed, oldOracleHash, oldVerPhrase);
466
+ const oldKeyPair = generateKeyPair(oldSecret);
467
+
468
+ const newPubKey = this.identity.publicKey;
469
+ const timestamp = Date.now();
470
+
471
+ // Sign with OLD key: "I (old identity) vouch for this new identity"
472
+ const migrateMsg1 = `MIGRATE:${newPubKey}:${timestamp}`;
473
+ const sigOld = signMessage(migrateMsg1, oldKeyPair.secretKey);
474
+
475
+ // Sign with NEW key: "I (new identity) claim this old identity"
476
+ const migrateMsg2 = `MIGRATE:${oldKeyPair.publicKey}:${timestamp}`;
477
+ const sigNew = signMessage(migrateMsg2, this.identity.secretKey);
478
+
479
+ return {
480
+ oldPubKey: oldKeyPair.publicKey,
481
+ newPubKey,
482
+ sigOld,
483
+ sigNew,
484
+ timestamp,
485
+ oldNetwork: deriveNetworkName(oldOracleHash, 3),
486
+ newNetwork: this.networkName,
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Verify a migration proof from another node.
492
+ *
493
+ * @param {Object} proof - The migration proof object
494
+ * @returns {{ valid: boolean, oldValid: boolean, newValid: boolean }}
495
+ */
496
+ static verifyMigrationProof(proof) {
497
+ const { oldPubKey, newPubKey, sigOld, sigNew, timestamp } = proof;
498
+
499
+ const migrateMsg1 = `MIGRATE:${newPubKey}:${timestamp}`;
500
+ const migrateMsg2 = `MIGRATE:${oldPubKey}:${timestamp}`;
501
+
502
+ const oldValid = verifySignature(migrateMsg1, sigOld, oldPubKey);
503
+ const newValid = verifySignature(migrateMsg2, sigNew, newPubKey);
504
+
505
+ return {
506
+ valid: oldValid && newValid,
507
+ oldValid,
508
+ newValid,
509
+ };
510
+ }
511
+
512
+ /**
513
+ * Sign a message with this node's identity
514
+ */
515
+ sign(message) {
516
+ if (!this.identity) throw new Error('Identity not initialized');
517
+ return signMessage(message, this.identity.secretKey);
518
+ }
519
+
520
+ /**
521
+ * Verify a message signature from another node
522
+ */
523
+ verify(message, signature, publicKey) {
524
+ return verifySignature(message, signature, publicKey);
525
+ }
526
+
527
+ /**
528
+ * Sign a JSON object (for mesh protocol messages)
529
+ */
530
+ signObject(obj) {
531
+ const payload = JSON.stringify(obj);
532
+ return {
533
+ ...obj,
534
+ _signature: this.sign(payload),
535
+ _signer: this.identity.nodeId,
536
+ _signedAt: Date.now(),
537
+ };
538
+ }
539
+
540
+ /**
541
+ * Verify a signed object from another node
542
+ */
543
+ verifyObject(signedObj, publicKey) {
544
+ const { _signature, _signer, _signedAt, ...obj } = signedObj;
545
+ const payload = JSON.stringify(obj);
546
+ return this.verify(payload, _signature, publicKey);
547
+ }
548
+
549
+ // ═══════════════════════════════════════════════════════════════════════════
550
+ // SLH-DSA Dual Signature — Defense-in-Depth for Critical Operations
551
+ // ═══════════════════════════════════════════════════════════════════════════
552
+ //
553
+ // Critical ops (content publish, identity registration, relay registration)
554
+ // produce BOTH an ML-DSA-65 signature (fast, lattice-based) AND an SLH-DSA
555
+ // signature (slower ~100-160ms, hash-based). An attacker must break BOTH
556
+ // lattice AND hash assumptions to forge a critical signature.
557
+ //
558
+ // Non-critical ops (regular messages, peer auth) continue using ML-DSA-65
559
+ // only for performance.
560
+ // ═══════════════════════════════════════════════════════════════════════════
561
+
562
+ /**
563
+ * Check if this node has SLH-DSA dual-sig capability
564
+ * @returns {boolean}
565
+ */
566
+ hasDualSignature() {
567
+ return !!(this.identity?.backupSecretKey && this.identity?.backupPublicKey);
568
+ }
569
+
570
+ /**
571
+ * Sign a critical message with BOTH ML-DSA-65 AND SLH-DSA (defense-in-depth)
572
+ *
573
+ * Use for: content publish, identity registration, relay registration,
574
+ * revocation certificates — any operation worth the ~100-160ms cost.
575
+ *
576
+ * @param {string} message - Message to sign (string)
577
+ * @returns {{ primary: string, backup: string }} Both signatures as hex strings
578
+ * @throws {Error} If backup key not available
579
+ */
580
+ signCritical(message) {
581
+ if (!this.identity) throw new Error('Identity not initialized');
582
+ if (!this.identity.backupSecretKey) {
583
+ throw new Error('SLH-DSA backup key not available — cannot create dual signature');
584
+ }
585
+
586
+ const messageBytes = typeof message === 'string'
587
+ ? new TextEncoder().encode(message)
588
+ : message;
589
+
590
+ // ML-DSA-65 primary signature (fast, ~1ms)
591
+ const primarySig = this.sign(message);
592
+
593
+ // SLH-DSA backup signature (slower, ~100-160ms, but hash-based security)
594
+ const backupSecretKey = hexToBytes(this.identity.backupSecretKey);
595
+ const backupSig = slhDsaSign(messageBytes, backupSecretKey);
596
+
597
+ return {
598
+ primary: primarySig,
599
+ backup: bytesToHex(backupSig),
600
+ };
601
+ }
602
+
603
+ /**
604
+ * Verify a dual signature from another node (BOTH must be valid)
605
+ *
606
+ * @param {string} message - Original message
607
+ * @param {string} primarySigHex - ML-DSA-65 signature (hex)
608
+ * @param {string} backupSigHex - SLH-DSA signature (hex)
609
+ * @param {string} primaryPKHex - ML-DSA-65 public key (hex)
610
+ * @param {string} backupPKHex - SLH-DSA public key (hex)
611
+ * @returns {{ valid: boolean, primaryValid: boolean, backupValid: boolean }}
612
+ */
613
+ verifyCritical(message, primarySigHex, backupSigHex, primaryPKHex, backupPKHex) {
614
+ const messageBytes = typeof message === 'string'
615
+ ? new TextEncoder().encode(message)
616
+ : message;
617
+
618
+ // Verify ML-DSA-65 (primary)
619
+ const primaryValid = this.verify(message, primarySigHex, primaryPKHex);
620
+
621
+ // Verify SLH-DSA (backup)
622
+ let backupValid = false;
623
+ try {
624
+ const backupSig = hexToBytes(backupSigHex);
625
+ const backupPK = hexToBytes(backupPKHex);
626
+ backupValid = slhDsaVerify(backupSig, messageBytes, backupPK);
627
+ } catch (e) {
628
+ log.error('SLH-DSA backup verification failed', { error: e.message });
629
+ }
630
+
631
+ return {
632
+ valid: primaryValid && backupValid,
633
+ primaryValid,
634
+ backupValid,
635
+ };
636
+ }
637
+
638
+ /**
639
+ * Sign a JSON object with dual signature (for critical mesh protocol messages)
640
+ * @param {Object} obj - Object to sign
641
+ * @returns {Object} Object with _signature, _backupSignature, _signer, _signedAt
642
+ */
643
+ signCriticalObject(obj) {
644
+ const payload = JSON.stringify(obj);
645
+ const sigs = this.signCritical(payload);
646
+ return {
647
+ ...obj,
648
+ _signature: sigs.primary,
649
+ _backupSignature: sigs.backup,
650
+ _signer: this.identity.nodeId,
651
+ _signedAt: Date.now(),
652
+ };
653
+ }
654
+
655
+ /**
656
+ * Verify a dual-signed object from another node
657
+ * @param {Object} signedObj - Object with _signature and _backupSignature
658
+ * @param {string} primaryPK - ML-DSA-65 public key (hex)
659
+ * @param {string} backupPK - SLH-DSA public key (hex)
660
+ * @returns {{ valid: boolean, primaryValid: boolean, backupValid: boolean }}
661
+ */
662
+ verifyCriticalObject(signedObj, primaryPK, backupPK) {
663
+ const { _signature, _backupSignature, _signer, _signedAt, ...obj } = signedObj;
664
+ const payload = JSON.stringify(obj);
665
+ return this.verifyCritical(payload, _signature, _backupSignature, primaryPK, backupPK);
666
+ }
667
+ }
668
+
669
+ export default NodeIdentity;