@smartledger/keys 3.0.0 → 3.1.1

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 CHANGED
@@ -1,48 +1,13 @@
1
- # @smartledger/keys SDK Documentation
1
+ # @smartledger/keys
2
2
 
3
- **Single source of truth for cryptographic key operations.**
3
+ A key-management SDK for hybrid classical + post-quantum signing: create keys,
4
+ sign, verify, rotate. It sits on [`@smartledger/crypto`](../crypto), which
5
+ supplies ML-DSA-44/65/87 (FIPS 204, via `@noble/post-quantum`) and ECDSA
6
+ secp256k1 (via `@noble/secp256k1`).
4
7
 
5
- Version: 1.5.1
6
- Status: ✅ Production Ready
7
- Tests: 52/52 passing
8
-
9
- **New in 1.5.1:** All three ML-DSA security levels now available (44/65/87)
10
-
11
- ---
12
-
13
- ## Why This SDK?
14
-
15
- Keys are the **root of everything** in your crypto architecture:
16
- - Identity
17
- - Signatures
18
- - PQ migration
19
- - Regulatory/audit story
20
-
21
- Without a clean SDK layer, key handling becomes **ad hoc** and **brittle**:
22
- - ❌ Random `generateKeypair()` implementations scattered across repos
23
- - ❌ Slightly different formats (hex here, base64 there)
24
- - ❌ Surprise refactors when adding PQ suites
25
- - ❌ Private keys exposed to calling code
26
-
27
- **With `@smartledger/keys`**, all your agents/modules/bridge code simply ask:
28
- ```typescript
29
- "Give me a signing key of suite X"
30
- "Sign this payload with keyId Y"
31
- ```
32
-
33
- And **don't care** how keys are stored, rotated, or implemented.
34
-
35
- ---
36
-
37
- ## Design Principles
38
-
39
- 1. **Private keys hidden** - Never exposed to most callers
40
- 2. **Crypto agility** - Suite is config, not code
41
- 3. **Single entry point** - Easy to audit
42
- 4. **Idempotent operations** - Safe for retries
43
- 5. **Minimal API surface** - Small, focused, easy to learn
44
-
45
- ---
8
+ **Read [Limits](#limits) before depending on it.** The only storage backend
9
+ shipped is in-memory, and ML-DSA signatures are made over a digest rather than
10
+ the message.
46
11
 
47
12
  ## Installation
48
13
 
@@ -50,499 +15,187 @@ And **don't care** how keys are stored, rotated, or implemented.
50
15
  npm install @smartledger/keys
51
16
  ```
52
17
 
53
- ### Supported ML-DSA Variants
18
+ Node ≥ 20.19. Works in browsers through any bundler (the ESM build uses only
19
+ Web-standard APIs). For a `<script>` tag, `dist/lumen-keys.min.js` exposes
20
+ `window.LumenKeys`.
54
21
 
55
- Choose the right security level for your use case:
56
-
57
- | Variant | NIST Level | Equivalent | Use Case |
58
- |---------|------------|------------|----------|
59
- | **ml-dsa-44** | Level 2 | AES-128 | IoT devices, high-throughput systems |
60
- | **ml-dsa-65** | Level 3 | AES-192 | **General purpose (recommended)** |
61
- | **ml-dsa-87** | Level 5 | AES-256 | High-security, long-term confidential data |
62
-
63
- ---
64
-
65
- ## Quick Start
22
+ ## Quick start
66
23
 
67
24
  ```typescript
68
25
  import { createKeySDK } from '@smartledger/keys';
69
26
 
70
- // One line to get a ready-to-use SDK with ML-DSA + ECDSA suites registered
71
- const sdk = createKeySDK();
27
+ const sdk = createKeySDK(); // in-memory storage; ECDSA + ML-DSA-44/65/87
72
28
 
73
- // Create both PQ + ECDSA keys for an agent
74
- // Using ml-dsa-65 (Level 3) - recommended for most use cases
75
- const { primaryKey, secondaryKey } = await sdk.createDualSignatureKeys(
76
- 'agent-resonance',
77
- {
78
- primarySignatureSuite: 'ml-dsa-65', // or 'ml-dsa-44', 'ml-dsa-87'
79
- secondarySignatureSuite: 'bsv-ecdsa-secp256k1',
80
- }
81
- );
29
+ const HYBRID = {
30
+ primarySignatureSuite: 'ml-dsa-65',
31
+ secondarySignatureSuite: 'bsv-ecdsa-secp256k1',
32
+ };
33
+ await sdk.createDualSignatureKeys('agent-resonance', HYBRID);
82
34
 
83
- // Sign with both suites (private keys stay hidden)
84
35
  const message = new TextEncoder().encode('agent output');
85
36
  const { signatures } = await sdk.signWithSuites('agent-resonance', message);
86
37
 
87
- // Verify both signatures
88
- const verify = await sdk.verifyWithSuites('agent-resonance', message, signatures);
89
- console.log(verify.allValid); // true
90
- ```
91
-
92
- ---
93
-
94
- ## API Reference
95
-
96
- ### Factory Helper
97
-
98
- `createKeySDK(config?)` returns a ready-to-use SDK wired with in-memory storage and the default ML-DSA + ECDSA suites. Pass your own `keyRegistry` or `suiteRegistry` to override defaults.
99
-
100
- ### KeySDK Interface
101
-
102
- #### `createKey(agentId, profile, options?)`
103
-
104
- Create a new key for an agent/module.
105
-
106
- **Parameters:**
107
- - `agentId` (string) - Agent identifier (e.g. `'agent-resonance'`)
108
- - `profile` (CryptoProfile) - Which signature suites to use
109
- - `options` (CreateKeyOptions) - Optional expiration, usage, etc.
110
-
111
- **Returns:** `Promise<KeyRecord>` - KeyRecord with metadata and public key
112
-
113
- **Example:**
114
- ```typescript
115
- const key = await sdk.createKey(
116
- 'agent-schema',
117
- { primarySignatureSuite: 'ml-dsa-65' }, // Recommended: Level 3
118
- {
119
- expiresAt: new Date(Date.now() + 365 * 86400000).toISOString(), // 1 year
120
- usage: ['signing'],
121
- }
122
- );
123
- ```
124
-
125
- ---
126
-
127
- #### `getKey(keyId)`
128
-
129
- Get an existing key by ID.
130
-
131
- **Parameters:**
132
- - `keyId` (string) - Key identifier
133
-
134
- **Returns:** `Promise<KeyRecord | null>` - KeyRecord or null if not found
135
-
136
- **Example:**
137
- ```typescript
138
- const key = await sdk.getKey('agent-resonance:pk-ml-1');
139
- if (key) {
140
- console.log(key.meta.suiteId); // 'ml-dsa-65' or 'ml-dsa-44', 'ml-dsa-87'
141
- console.log(key.publicKey); // Uint8Array
142
- }
143
- ```
144
-
145
- ---
146
-
147
- #### `signWithKey(keyId, message)`
148
-
149
- Sign a message with a key. **Private key never leaves the SDK.**
150
-
151
- **Parameters:**
152
- - `keyId` (string) - Which key to sign with
153
- - `message` (Uint8Array) - Message to sign (will be hashed internally)
154
-
155
- **Returns:** `Promise<Uint8Array>` - Signature bytes
156
-
157
- **Throws:** Error if key not found or not active
158
-
159
- **Example:**
160
- ```typescript
161
- const message = new TextEncoder().encode('agent output');
162
- const signature = await sdk.signWithKey('agent-resonance:pk-ml-1', message);
163
- ```
164
-
165
- ---
166
-
167
- #### `verifySignature(keyId, message, signature)`
168
-
169
- Verify a signature against a public key.
170
-
171
- **Parameters:**
172
- - `keyId` (string) - Which key to verify with
173
- - `message` (Uint8Array) - Original message
174
- - `signature` (Uint8Array) - Signature to verify
175
-
176
- **Returns:** `Promise<boolean>` - true if valid
177
-
178
- **Example:**
179
- ```typescript
180
- const valid = await sdk.verifySignature(
181
- 'agent-resonance:pk-ml-1',
182
- message,
183
- signature
184
- );
185
- ```
186
-
187
- ---
188
-
189
- #### `listKeysForAgent(agentId, activeOnly?)`
190
-
191
- List all keys for an agent.
192
-
193
- **Parameters:**
194
- - `agentId` (string) - Agent identifier
195
- - `activeOnly` (boolean) - If true, only return active keys (default: true)
196
-
197
- **Returns:** `Promise<KeyRecord[]>` - Array of KeyRecords
198
-
199
- **Example:**
200
- ```typescript
201
- const keys = await sdk.listKeysForAgent('agent-schema');
202
- for (const key of keys) {
203
- console.log(`${key.meta.keyId} (${key.meta.suiteId})`);
204
- }
205
- ```
206
-
207
- ---
208
-
209
- #### `getActiveCryptoProfile(agentId)`
210
-
211
- Get the active crypto profile for an agent.
212
-
213
- Looks up the most recent active keys and infers the profile.
214
-
215
- **Parameters:**
216
- - `agentId` (string) - Agent identifier
217
-
218
- **Returns:** `Promise<CryptoProfile | null>` - CryptoProfile or null if no keys found
219
-
220
- **Example:**
221
- ```typescript
222
- const profile = await sdk.getActiveCryptoProfile('agent-schema');
223
- console.log(profile.primarySignatureSuite); // 'ml-dsa-65'
224
- console.log(profile.secondarySignatureSuite); // 'bsv-ecdsa-secp256k1'
225
- ```
226
-
227
- ---
228
-
229
- #### `rotateAgentKeys(agentId)`
230
-
231
- Rotate an agent's keys.
232
-
233
- Generates new keypairs for all active keys, marks old ones as rotated.
234
-
235
- **Parameters:**
236
- - `agentId` (string) - Agent identifier
237
-
238
- **Returns:** `Promise<KeyRecord[]>` - Array of new KeyRecords
239
-
240
- **Example:**
241
- ```typescript
242
- const newKeys = await sdk.rotateAgentKeys('agent-schema');
243
- for (const key of newKeys) {
244
- console.log(`New: ${key.meta.keyId}`);
245
- console.log(`Rotated from: ${key.meta.rotatedFrom}`);
246
- }
247
- ```
248
-
249
- ---
250
-
251
- #### `getOrCreateKey(agentId, profile, options?)`
252
-
253
- Create key if it doesn't exist, otherwise return existing.
254
-
255
- **Idempotent** key creation for agent setup.
256
-
257
- **Parameters:**
258
- - `agentId` (string) - Agent identifier
259
- - `profile` (CryptoProfile) - Crypto configuration
260
- - `options` (CreateKeyOptions) - Creation options
261
-
262
- **Returns:** `Promise<KeyRecord>` - KeyRecord (new or existing)
263
-
264
- **Example:**
265
- ```typescript
266
- // Safe to call multiple times
267
- const key1 = await sdk.getOrCreateKey('agent-validator', {
268
- primarySignatureSuite: 'ml-dsa-65',
269
- });
270
-
271
- const key2 = await sdk.getOrCreateKey('agent-validator', {
272
- primarySignatureSuite: 'ml-dsa-65',
273
- });
274
-
275
- console.log(key1.meta.keyId === key2.meta.keyId); // true
276
- ```
277
-
278
- ---
279
-
280
- #### `createDualSignatureKeys(agentId, profile, options?)`
281
-
282
- Create both primary and secondary keys in one call. Requires `profile.secondarySignatureSuite`.
283
-
284
- **Returns:** `{ primaryKey, secondaryKey }`
285
-
286
- **Example:**
287
- ```typescript
288
- const { primaryKey, secondaryKey } = await sdk.createDualSignatureKeys(
289
- 'agent-bridge',
290
- {
291
- primarySignatureSuite: 'ml-dsa-65', // Recommended
292
- secondarySignatureSuite: 'bsv-ecdsa-secp256k1',
293
- }
294
- );
295
- ```
296
-
297
- ---
298
-
299
- #### `getOrCreateDualSignatureKeys(agentId, profile, options?)`
300
-
301
- Idempotent version of the above; reuses active keys when present.
302
-
303
- **Returns:** `{ primaryKey, secondaryKey }`
304
-
305
- ---
306
-
307
- #### `signWithSuites(agentId, message, suites?)`
308
-
309
- Sign once per suite. When `suites` is omitted, active primary + secondary are used.
310
-
311
- **Returns:** `{ signatures: Array<{ suiteId, keyId, signature }> }`
312
-
313
- **Example:**
314
- ```typescript
315
- const message = new TextEncoder().encode('hybrid payload');
316
- const { signatures } = await sdk.signWithSuites('agent-bridge', message);
317
- ```
318
-
319
- ---
320
-
321
- #### `verifyWithSuites(agentId, message, signatures)`
322
-
323
- Verify multiple signatures and get per-suite results.
324
-
325
- **Returns:** `{ results: Array<{ suiteId, keyId, valid }>, allValid: boolean }`
326
-
327
- **Example:**
328
- ```typescript
329
- const verify = await sdk.verifyWithSuites('agent-bridge', message, signatures);
330
- console.log(verify.allValid); // true when every suite verifies
331
- ```
332
- ---
333
-
334
- ---
335
-
336
- ## Types
337
-
338
- ### KeyRecord
339
-
340
- Public key record (safe to expose, no private key).
341
-
342
- ```typescript
343
- interface KeyRecord {
344
- meta: KeyMeta; // Key metadata
345
- publicKey: Uint8Array; // Public key bytes
346
- }
347
- ```
348
-
349
- ### CreateKeyOptions
350
-
351
- Options for key creation.
352
-
353
- ```typescript
354
- interface CreateKeyOptions {
355
- expiresAt?: string; // ISO 8601
356
- usage?: Array<'signing' | 'encryption' | 'both'>; // Default: ['signing']
357
- cryptoProfileVersion?: string; // Default: '1.0.0'
358
- }
359
- ```
360
-
361
- ### CryptoProfile
362
-
363
- Per-agent/module crypto configuration.
364
-
365
- ```typescript
366
- interface CryptoProfile {
367
- primarySignatureSuite: string; // e.g. 'ml-dsa-65' (recommended), 'ml-dsa-44', 'ml-dsa-87'
368
- secondarySignatureSuite?: string; // e.g. 'bsv-ecdsa-secp256k1' (hybrid)
369
- keyEncapsulationSuite?: string; // e.g. 'ml-kem-768' (future)
370
- }
371
- ```
372
-
373
- ---
374
-
375
- ## Usage Patterns
376
-
377
- ### Single Signature (PQ-only)
378
-
379
- ```typescript
380
- // Agent uses only ML-DSA (recommended: ml-dsa-65)
381
- const key = await sdk.createKey('agent-research', {
382
- primarySignatureSuite: 'ml-dsa-65', // or 'ml-dsa-44' for IoT, 'ml-dsa-87' for max security
38
+ const result = await sdk.verifyWithSuites('agent-resonance', message, signatures, {
39
+ requiredSuites: ['ml-dsa-65', 'bsv-ecdsa-secp256k1'],
383
40
  });
384
-
385
- const message = new TextEncoder().encode('research results');
386
- const signature = await sdk.signWithKey(key.meta.keyId, message);
387
- ```
388
-
389
- ### Hybrid Signature (ECDSA + PQ)
390
-
391
- ```typescript
392
- // One call to create both keys
393
- await sdk.getOrCreateDualSignatureKeys('agent-bridge', {
394
- primarySignatureSuite: 'ml-dsa-65', // Balanced security (recommended)
395
- secondarySignatureSuite: 'bsv-ecdsa-secp256k1',
396
- });
397
-
398
- // Sign with both suites
399
- const message = new TextEncoder().encode('bridge output');
400
- const { signatures } = await sdk.signWithSuites('agent-bridge', message);
401
-
402
- // Verify both signatures
403
- const verified = await sdk.verifyWithSuites('agent-bridge', message, signatures);
404
- console.log(verified.allValid); // true
405
- ```
406
-
407
- ### Agent Setup (Idempotent)
408
-
409
- ```typescript
410
- // Safe to call on every startup
411
- async function setupAgent(agentId: string) {
412
- const key = await sdk.getOrCreateKey(agentId, {
413
- primarySignatureSuite: 'ml-dsa-65', // Recommended default
414
- }, {
415
- expiresAt: new Date(Date.now() + 365 * 86400000).toISOString(),
416
- });
417
-
418
- return key;
419
- }
420
- ```
421
-
422
- ### Monthly Key Rotation
41
+ console.log(result.allValid); // true
42
+ ```
43
+
44
+ | Suite | NIST category | Public key | Signature |
45
+ |---|---|---|---|
46
+ | `ml-dsa-44` | 2 | 1,312 B | 2,420 B |
47
+ | `ml-dsa-65` | 3 | 1,952 B | 3,309 B |
48
+ | `ml-dsa-87` | 5 | 2,592 B | 4,627 B |
49
+ | `bsv-ecdsa-secp256k1` | not post-quantum | 33 B | 64 B (compact `r‖s`, low S) |
50
+
51
+ ## Verifying a hybrid signature
52
+
53
+ `verifyWithSuites(agentId, message, signatures, options?)` returns
54
+ `allValid: true` only when **all** of these hold:
55
+
56
+ - at least one signature was supplied;
57
+ - every supplied signature is by a key that belongs to `agentId`, names that
58
+ key's real suite, passes the key-status policy, and verifies;
59
+ - every **required** suite has such a signature.
60
+
61
+ The required suites are the verifier's policy. Pass them as
62
+ `options.requiredSuites` from your own configuration. If you omit them they
63
+ default to the suites of the agent's currently usable keys in this SDK's
64
+ registry. They are never taken from `signatures`: a bundle that simply leaves
65
+ out the post-quantum signature is refused, and `missingSuites` names what is
66
+ absent.
67
+
68
+ > **If you used 3.0.x:** `verifyWithSuites` returned `allValid: true` for an
69
+ > empty list, for a list missing a suite, and for signatures made by another
70
+ > agent's keys. Any stored result computed by 3.0.x over an incomplete list
71
+ > means nothing and should be recomputed.
72
+
73
+ `options.keyStatus` (also on `verifySignature`) decides what is asked of the
74
+ key's state today:
75
+
76
+ | Value | Accepts |
77
+ |---|---|
78
+ | `'any'` | any key; the cryptographic check only (default for `verifySignature`) |
79
+ | `'not-revoked'` | anything but revoked keys (default for `verifyWithSuites`) |
80
+ | `'usable'` | only keys that are active and unexpired now |
81
+
82
+ There is no signing-time evidence here: the SDK cannot tell whether a
83
+ signature by a now-rotated key was made before or after the rotation. If that
84
+ matters, timestamp the signature independently when it is made.
85
+
86
+ ## API
87
+
88
+ `createKeySDK(config?)` returns a `KeySDK`. Pass `keyRegistry` or
89
+ `suiteRegistry` to override the defaults.
90
+
91
+ | Method | Notes |
92
+ |---|---|
93
+ | `createKey(agentId, profile, options?)` | `options`: `expiresAt` (ISO 8601, validated), `usage`, `derivationContext` |
94
+ | `getKey(keyId)` | Returns a copy; editing it changes nothing in the registry |
95
+ | `signWithKey(keyId, message)` | Throws if the key is not active, is past `expiresAt`, or is not a signing key |
96
+ | `verifySignature(keyId, message, signature, options?)` | Cryptographic check; `options.keyStatus` |
97
+ | `listKeysForAgent(agentId, activeOnly = true)` | Exact ownership (see below) |
98
+ | `getActiveCryptoProfile(agentId)` | Inferred from active keys |
99
+ | `rotateAgentKeys(agentId, options?)` | See [Rotation](#rotation) |
100
+ | `getOrCreateKey(agentId, profile, options?)` | Concurrent calls on one SDK instance share one creation |
101
+ | `createDualSignatureKeys(agentId, profile, options?)` | Two different suites required |
102
+ | `getOrCreateDualSignatureKeys(agentId, profile, options?)` | Idempotent form |
103
+ | `signWithSuites(agentId, message, suites?)` | One signature per suite |
104
+ | `verifyWithSuites(agentId, message, signatures, options?)` | See above |
105
+ | `generateMnemonic(strength?)` | BIP39, 128 or 256 bits |
106
+ | `createKeyFromMnemonic(agentId, profile, mnemonic, path?, passphrase?, options?)` | Mnemonic words and checksum are validated |
107
+ | `getCapabilities(suiteId)`, `getSuitesWithCapability(capability)` | |
108
+
109
+ `DefaultKeySDK` additionally has `createWalletKey` and the deprecated
110
+ `createBitcoinWalletKey` (see [Wallet keys](#wallet-keys)).
111
+
112
+ **Key IDs and ownership.** A key ID is `<agentId>:pk-<suite>-<n>`. A key
113
+ belongs to the agent named by everything before its last `:`, so agent IDs may
114
+ themselves contain colons (`did:web:example.com`) and agent `a` never sees the
115
+ keys of agent `a:b`. `agentIdOfKey(keyId)` is exported.
116
+
117
+ **Registration never overwrites.** The registry throws `KeyAlreadyExistsError`
118
+ rather than replace the key behind an existing ID. If two SDK instances share
119
+ a storage backend, the loser of an ID race re-reads the counter and takes the
120
+ next ID.
121
+
122
+ ## Rotation
423
123
 
424
124
  ```typescript
425
- // Automated rotation job
426
- async function monthlyRotation() {
427
- const agents = ['agent-resonance', 'agent-schema', 'agent-validator'];
428
-
429
- for (const agentId of agents) {
430
- const newKeys = await sdk.rotateAgentKeys(agentId);
431
- console.log(`✓ Rotated ${agentId}: ${newKeys.length} keys`);
432
- }
433
- }
125
+ const newKeys = await sdk.rotateAgentKeys('agent-schema');
434
126
  ```
435
127
 
436
- ---
437
-
438
- ## Benefits
439
-
440
- ### 🔒 Security
441
-
442
- - **Private keys hidden** - Never exposed to calling code
443
- - **Single audit point** - All key operations go through SDK
444
- - **No key leakage** - Keys stay in KeyRegistry storage
445
- - **Consistent hashing** - Suite-appropriate algorithms
446
-
447
- ### 🔄 Crypto Agility
448
-
449
- - **Suite is config** - Change ML-DSA → Falcon without code changes
450
- - **Profile-driven** - CryptoProfile defines agent's crypto setup
451
- - **Easy migration** - Hybrid mode for ECDSA → PQ transition
452
-
453
- ### 🛠️ Developer Experience
454
-
455
- - **Clean API** - 8 methods, easy to learn
456
- - **Idempotent** - `getOrCreateKey` safe for retries
457
- - **TypeScript** - Full type safety
458
- - **Well-tested** - 23 tests covering all scenarios
459
-
460
- ### 📊 Operations
128
+ - Each new key gets its predecessor's **lifetime** again, counted from the
129
+ rotation. Pass `{ expiresAt }` for a specific instant or `{ expiresAt: null }`
130
+ for none.
131
+ - A key derived from a mnemonic cannot be re-derived, because the SDK does not
132
+ keep seeds. By default the call refuses, before changing anything. Pass
133
+ `{ derivedKeys: 'skip' }` to rotate the other keys, or
134
+ `{ derivedKeys: 'randomize' }` to replace derived keys with random ones that
135
+ the mnemonic will not recover.
136
+ - All replacements are generated before any is committed. The commits are one
137
+ registry operation per key, not a transaction.
138
+ - Revoked and already-rotated keys cannot be rotated.
461
139
 
462
- - **Centralized** - One SDK for all agents
463
- - **Auditable** - Easy to track key operations
464
- - **Scalable** - Storage backends (in-memory, file, KMS)
465
- - **Rotation-friendly** - Built-in rotation support
466
-
467
- ---
468
-
469
- ## Best Practices
470
-
471
- ### ✅ Do
472
-
473
- - Use SDK for **all** key operations
474
- - Call `getOrCreateKey` for idempotent setup
475
- - Set expiration dates on keys
476
- - Rotate keys regularly (monthly/quarterly)
477
- - Use `signWithKey` (not direct registry access)
478
-
479
- ### ❌ Don't
480
-
481
- - Expose private keys to calling code
482
- - Create keys manually outside SDK
483
- - Hard-code suite IDs in business logic
484
- - Skip key rotation
485
-
486
- ---
487
-
488
- ## Integration with Existing Code
489
-
490
- ### Before (Direct Registry)
491
-
492
- ```typescript
493
- // Scattered key operations
494
- const suite = globalRegistry.getSuite('ml-dsa-87');
495
- const keypair = await suite.generateKeypair();
496
- await keyRegistry.registerKey('agent-1:pk-1', keypair, 'ml-dsa-87');
497
-
498
- const key = await keyRegistry.getKey('agent-1:pk-1');
499
- const sig = await suite.sign(key.keypair.privateKey, message);
500
- ```
501
-
502
- ### After (SDK)
140
+ ## Wallet keys
503
141
 
504
142
  ```typescript
505
- // Clean, centralized
506
- const key = await sdk.createKey('agent-1', {
507
- primarySignatureSuite: 'ml-dsa-65', // Choose: 'ml-dsa-44', '65', or '87'
143
+ const key = await sdk.createWalletKey('agent-wallet', mnemonic, {
144
+ coinType: 236, // SLIP-44: Bitcoin SV. Bitcoin (BTC) is 0.
145
+ account: 0,
146
+ index: 0,
508
147
  });
148
+ // m/44'/236'/0'/0/0
149
+ ```
150
+
151
+ `coinType` is required and has no default, because it decides which wallets
152
+ will find the key again.
153
+
154
+ `createBitcoinWalletKey` still derives at `m/44'/0'/…` (BTC), exactly as
155
+ before: **upgrading does not move any existing key.** A Bitcoin SV wallet
156
+ restoring the same mnemonic looks under coin type 236 and will not find those
157
+ keys, which is why the method is deprecated in favour of `createWalletKey`.
158
+
159
+ The ECDSA suite produces 64-byte compact signatures over a single SHA-256.
160
+ That suits notarisation-style signing. It is **not** a transaction signature:
161
+ Bitcoin-family transactions need DER plus a sighash byte over a double-SHA-256
162
+ sighash preimage, which this SDK does not build.
163
+
164
+ ## Limits
165
+
166
+ - **Storage is in-memory only.** Private keys are held unencrypted in process
167
+ memory and are lost on exit. There is no file, KMS or HSM backend in this
168
+ package; implement `KeyStorage` for one. A backend shared between processes
169
+ should implement `storeIfAbsent` atomically.
170
+ - **Private keys are reachable.** `signWithKey` keeps them out of the common
171
+ path, but anyone holding the `KeyRegistry` can call `getKey`. This is an API
172
+ convenience, not an isolation boundary.
173
+ - **ML-DSA signs a digest.** The SDK signs `SHA3-256(message)` with pure
174
+ ML-DSA and an empty context string (and ECDSA signs `SHA-256(message)`).
175
+ A standard FIPS 204 verifier must therefore be given the 32-byte digest as
176
+ its message. The 256-bit digest also bounds collision resistance at about
177
+ 128 bits whatever the ML-DSA level, which matters where an attacker can
178
+ choose the messages that get signed. Signing the message directly (or
179
+ HashML-DSA with a context string) is planned for the next major version,
180
+ because it changes every signature.
181
+ - **No domain separation between uses.** A key that signs for several purposes
182
+ can have a signature replayed from one to another. Put the purpose in the
183
+ message.
184
+ - **`getOrCreateKey` across instances.** Two SDK instances or processes on one
185
+ backend can each create a key; this needs a lock in the backend.
186
+ - **Dependencies.** `@noble/post-quantum` is pre-1.0 and, unlike the other
187
+ noble libraries used here, has not had an independent audit.
188
+
189
+ ## Development
509
190
 
510
- const sig = await sdk.signWithKey(key.meta.keyId, message);
191
+ ```bash
192
+ npm run build:all # CJS, ESM and the browser bundle
193
+ npm test
511
194
  ```
512
195
 
513
- ---
514
-
515
- ## What's Next?
516
-
517
- - **File Storage** - Persistent key storage (not just in-memory)
518
- - **KMS Integration** - AWS KMS, Azure Key Vault, HSM
519
- - **Key Backup** - Encrypted backup/recovery procedures
520
- - **Multi-tenant** - Isolate keys per tenant
521
- - **Hardware Keys** - Support for non-exportable hardware keys
522
-
523
- ---
524
-
525
- ## Summary
526
-
527
- **`@smartledger/keys` is the single source of truth for cryptographic keys:**
528
-
529
- ✅ 23 tests passing
530
- ✅ Private keys hidden from callers
531
- ✅ Crypto-agnostic API (suite is config)
532
- ✅ Idempotent operations
533
- ✅ Easy to audit
534
- ✅ Production-ready
535
-
536
- **Use it for all key operations. Make crypto agility a reality.**
537
-
538
- ---
539
-
540
- **See Also:**
541
- - [Key Management Guide](../docs/KEY_MANAGEMENT_GUIDE.md) - Lower-level KeyRegistry details
542
- - [Algorithm Selection Guide](../docs/ALGORITHM_SELECTION_GUIDE.md) - Which suite to use?
543
- - [ECDSA Implementation Strategy](../docs/ECDSA_IMPLEMENTATION_STRATEGY.md) - Why Noble
196
+ `test/hardening.test.ts` holds the regression tests for the defects fixed in
197
+ 3.1.0; see the repository CHANGELOG.
544
198
 
545
- ---
199
+ ## License
546
200
 
547
- **Version**: 1.0.0
548
- **Last Updated**: November 28, 2025
201
+ MIT