@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 +168 -515
- package/dist/cjs/browser.d.ts +16 -6
- package/dist/cjs/browser.d.ts.map +1 -1
- package/dist/cjs/browser.js +15 -10
- package/dist/cjs/browser.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +19 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/sdk.d.ts +83 -9
- package/dist/cjs/sdk.d.ts.map +1 -1
- package/dist/cjs/sdk.js +322 -87
- package/dist/cjs/sdk.js.map +1 -1
- package/dist/cjs/types.d.ts +78 -3
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/browser.d.ts +16 -6
- package/dist/esm/browser.d.ts.map +1 -1
- package/dist/esm/browser.js +14 -10
- package/dist/esm/browser.js.map +1 -1
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/sdk.d.ts +83 -9
- package/dist/esm/sdk.d.ts.map +1 -1
- package/dist/esm/sdk.js +322 -87
- package/dist/esm/sdk.js.map +1 -1
- package/dist/esm/types.d.ts +78 -3
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/lumen-keys.js +2010 -4326
- package/dist/lumen-keys.min.js +4 -7
- package/dist/lumen-keys.min.js.map +4 -4
- package/package.json +11 -12
- package/dist/lumen-keys.js.map +0 -7
- package/dist/tsconfig.esm.tsbuildinfo +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
package/README.md
CHANGED
|
@@ -1,48 +1,13 @@
|
|
|
1
|
-
# @smartledger/keys
|
|
1
|
+
# @smartledger/keys
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
const sdk = createKeySDK();
|
|
27
|
+
const sdk = createKeySDK(); // in-memory storage; ECDSA + ML-DSA-44/65/87
|
|
72
28
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
**Last Updated**: November 28, 2025
|
|
201
|
+
MIT
|