@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/dist/cjs/sdk.js
CHANGED
|
@@ -6,9 +6,29 @@
|
|
|
6
6
|
* a clean, high-level API for key operations.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.DefaultKeySDK = void 0;
|
|
9
|
+
exports.DefaultKeySDK = exports.agentIdOfKey = void 0;
|
|
10
10
|
exports.createKeySDK = createKeySDK;
|
|
11
11
|
const crypto_1 = require("@smartledger/crypto");
|
|
12
|
+
Object.defineProperty(exports, "agentIdOfKey", { enumerable: true, get: function () { return crypto_1.agentIdOfKey; } });
|
|
13
|
+
const COUNTER_SUFFIX = /^pk-[^:-]+-(\d+)$/;
|
|
14
|
+
const MAX_KEY_ID_ATTEMPTS = 8;
|
|
15
|
+
function assertAgentId(agentId) {
|
|
16
|
+
if (typeof agentId !== 'string' || agentId.length === 0) {
|
|
17
|
+
throw new Error('agentId must be a non-empty string');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function toRecord(meta, publicKey) {
|
|
21
|
+
// Copies, so a caller cannot reach registry state through a KeyRecord even
|
|
22
|
+
// when a custom storage backend hands out its own objects.
|
|
23
|
+
return { meta: (0, crypto_1.cloneKeyMeta)(meta), publicKey: Uint8Array.from(publicKey) };
|
|
24
|
+
}
|
|
25
|
+
function statusAllows(meta, policy) {
|
|
26
|
+
if (policy === 'any')
|
|
27
|
+
return true;
|
|
28
|
+
if (policy === 'not-revoked')
|
|
29
|
+
return meta.status !== 'revoked';
|
|
30
|
+
return (0, crypto_1.isKeyUsable)(meta);
|
|
31
|
+
}
|
|
12
32
|
/**
|
|
13
33
|
* Default KeySDK implementation.
|
|
14
34
|
*
|
|
@@ -19,6 +39,7 @@ class DefaultKeySDK {
|
|
|
19
39
|
this.keyCounter = new Map(); // agentId -> counter
|
|
20
40
|
this.counterInitialized = new Set();
|
|
21
41
|
this.counterInitPromises = new Map();
|
|
42
|
+
this.getOrCreateInFlight = new Map();
|
|
22
43
|
this.keyRegistry = config.keyRegistry;
|
|
23
44
|
this.suiteRegistry = config.suiteRegistry;
|
|
24
45
|
}
|
|
@@ -26,6 +47,7 @@ class DefaultKeySDK {
|
|
|
26
47
|
* Create a new key for an agent.
|
|
27
48
|
*/
|
|
28
49
|
async createKey(agentId, profile, options = {}) {
|
|
50
|
+
assertAgentId(agentId);
|
|
29
51
|
// Get suite
|
|
30
52
|
const suite = this.suiteRegistry.getSuite(profile.primarySignatureSuite);
|
|
31
53
|
// Generate keypair (with optional derivation)
|
|
@@ -47,20 +69,16 @@ class DefaultKeySDK {
|
|
|
47
69
|
// Standard random key generation
|
|
48
70
|
keypair = await suite.generateKeypair();
|
|
49
71
|
}
|
|
50
|
-
|
|
51
|
-
const counter = await this.getNextCounter(agentId);
|
|
52
|
-
const suiteName = profile.primarySignatureSuite.split('-')[0]; // 'ml-dsa' or 'bsv'
|
|
53
|
-
const keyId = `${agentId}:pk-${suiteName}-${counter}`;
|
|
54
|
-
// Register key
|
|
55
|
-
const meta = await this.keyRegistry.registerKey(keyId, keypair, profile.primarySignatureSuite, {
|
|
72
|
+
const meta = await this.registerUnderFreshId(agentId, profile.primarySignatureSuite, keyId => this.keyRegistry.registerKey(keyId, keypair, profile.primarySignatureSuite, {
|
|
56
73
|
expiresAt: options.expiresAt,
|
|
57
74
|
usage: options.usage || ['signing'],
|
|
58
75
|
cryptoProfileVersion: options.cryptoProfileVersion || '1.0.0',
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
76
|
+
// Record that (and where) the key was derived. Never the seed.
|
|
77
|
+
derivation: options.derivationContext
|
|
78
|
+
? { path: options.derivationContext.path ?? '' }
|
|
79
|
+
: undefined,
|
|
80
|
+
}));
|
|
81
|
+
return toRecord(meta, keypair.publicKey);
|
|
64
82
|
}
|
|
65
83
|
/**
|
|
66
84
|
* Get an existing key.
|
|
@@ -70,13 +88,13 @@ class DefaultKeySDK {
|
|
|
70
88
|
if (!result) {
|
|
71
89
|
return null;
|
|
72
90
|
}
|
|
73
|
-
return
|
|
74
|
-
meta: result.meta,
|
|
75
|
-
publicKey: result.publicKey,
|
|
76
|
-
};
|
|
91
|
+
return toRecord(result.meta, result.publicKey);
|
|
77
92
|
}
|
|
78
93
|
/**
|
|
79
94
|
* Sign with a key (private key never exposed).
|
|
95
|
+
*
|
|
96
|
+
* @throws if the key is unknown, not active, past its `expiresAt`, or not
|
|
97
|
+
* registered for signing
|
|
80
98
|
*/
|
|
81
99
|
async signWithKey(keyId, message) {
|
|
82
100
|
// Get full keypair (private access)
|
|
@@ -84,9 +102,15 @@ class DefaultKeySDK {
|
|
|
84
102
|
if (!result) {
|
|
85
103
|
throw new Error(`Key not found: ${keyId}`);
|
|
86
104
|
}
|
|
87
|
-
if (result.meta.status !== 'active') {
|
|
105
|
+
if (result.meta.status && result.meta.status !== 'active') {
|
|
88
106
|
throw new Error(`Key is not active: ${keyId} (status: ${result.meta.status})`);
|
|
89
107
|
}
|
|
108
|
+
// Status alone is not enough: an expired key keeps status 'active' until
|
|
109
|
+
// markExpiredKeys() runs, and used to go on signing in the meantime.
|
|
110
|
+
(0, crypto_1.assertKeyUsable)(result.meta);
|
|
111
|
+
if (!result.meta.usage.some(u => u === 'signing' || u === 'both')) {
|
|
112
|
+
throw new Error(`Key is not registered for signing: ${keyId} (usage: ${result.meta.usage.join(', ')})`);
|
|
113
|
+
}
|
|
90
114
|
// Get suite and sign
|
|
91
115
|
const suite = this.suiteRegistry.getSuite(result.meta.suiteId);
|
|
92
116
|
// Hash the message (consistent with signAgentResponse)
|
|
@@ -96,18 +120,29 @@ class DefaultKeySDK {
|
|
|
96
120
|
}
|
|
97
121
|
/**
|
|
98
122
|
* Verify a signature.
|
|
123
|
+
*
|
|
124
|
+
* By default this is the cryptographic check only: a signature made by a
|
|
125
|
+
* key that has since been rotated, expired or revoked still verifies, which
|
|
126
|
+
* is what verifying a historical signature needs. Pass `keyStatus` to also
|
|
127
|
+
* require something of the key's state now.
|
|
99
128
|
*/
|
|
100
|
-
async verifySignature(keyId, message, signature) {
|
|
129
|
+
async verifySignature(keyId, message, signature, options = {}) {
|
|
101
130
|
const result = await this.keyRegistry.getPublicKey(keyId);
|
|
102
131
|
if (!result) {
|
|
103
132
|
return false;
|
|
104
133
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
if (!statusAllows(result.meta, options.keyStatus ?? 'any')) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return this.verifyAgainst(result.meta, result.publicKey, message, signature);
|
|
138
|
+
}
|
|
139
|
+
/** The cryptographic check alone. Anything but a strict `true` is a failure. */
|
|
140
|
+
async verifyAgainst(meta, publicKey, message, signature) {
|
|
109
141
|
try {
|
|
110
|
-
|
|
142
|
+
const suite = this.suiteRegistry.getSuite(meta.suiteId);
|
|
143
|
+
const hashAlg = this.getHashAlgorithm(meta.suiteId);
|
|
144
|
+
const messageHash = await (0, crypto_1.hashBytes)(message, hashAlg);
|
|
145
|
+
return (await suite.verify(publicKey, messageHash, signature)) === true;
|
|
111
146
|
}
|
|
112
147
|
catch {
|
|
113
148
|
return false;
|
|
@@ -117,20 +152,18 @@ class DefaultKeySDK {
|
|
|
117
152
|
* List keys for an agent.
|
|
118
153
|
*/
|
|
119
154
|
async listKeysForAgent(agentId, activeOnly = true) {
|
|
155
|
+
assertAgentId(agentId);
|
|
120
156
|
const keys = activeOnly
|
|
121
157
|
? await this.keyRegistry.listActiveKeys()
|
|
122
158
|
: await this.keyRegistry.listKeys();
|
|
123
|
-
//
|
|
124
|
-
const agentKeys = keys.filter(meta => meta.keyId
|
|
159
|
+
// Exact ownership, not a prefix match (see agentIdOfKey)
|
|
160
|
+
const agentKeys = keys.filter(meta => (0, crypto_1.agentIdOfKey)(meta.keyId) === agentId);
|
|
125
161
|
// Convert to KeyRecords
|
|
126
162
|
const records = [];
|
|
127
163
|
for (const meta of agentKeys) {
|
|
128
164
|
const result = await this.keyRegistry.getPublicKey(meta.keyId);
|
|
129
165
|
if (result) {
|
|
130
|
-
records.push(
|
|
131
|
-
meta: result.meta,
|
|
132
|
-
publicKey: result.publicKey,
|
|
133
|
-
});
|
|
166
|
+
records.push(toRecord(result.meta, result.publicKey));
|
|
134
167
|
}
|
|
135
168
|
}
|
|
136
169
|
return records;
|
|
@@ -155,39 +188,87 @@ class DefaultKeySDK {
|
|
|
155
188
|
}
|
|
156
189
|
/**
|
|
157
190
|
* Rotate all keys for an agent.
|
|
191
|
+
*
|
|
192
|
+
* Each replacement gets its predecessor's lifetime again, counted from now
|
|
193
|
+
* (or `options.expiresAt`); it does not inherit the old expiry instant.
|
|
194
|
+
*
|
|
195
|
+
* A key derived from a seed cannot be re-derived here, because the SDK does
|
|
196
|
+
* not keep seeds. By default the call refuses, before changing anything,
|
|
197
|
+
* rather than silently swapping a recoverable key for a random one. See
|
|
198
|
+
* {@link RotateAgentKeysOptions.derivedKeys}.
|
|
158
199
|
*/
|
|
159
|
-
async rotateAgentKeys(agentId) {
|
|
200
|
+
async rotateAgentKeys(agentId, options = {}) {
|
|
201
|
+
const derivedPolicy = options.derivedKeys ?? 'error';
|
|
160
202
|
const activeKeys = await this.listKeysForAgent(agentId, true);
|
|
161
|
-
|
|
203
|
+
// Decide everything before changing anything.
|
|
204
|
+
const toRotate = [];
|
|
162
205
|
for (const keyRecord of activeKeys) {
|
|
163
|
-
//
|
|
206
|
+
// Resolves the suite now, so an unknown suite fails before any rotation.
|
|
207
|
+
this.suiteRegistry.getSuite(keyRecord.meta.suiteId);
|
|
208
|
+
if (keyRecord.meta.derivation) {
|
|
209
|
+
if (derivedPolicy === 'error') {
|
|
210
|
+
throw new Error(`Key ${keyRecord.meta.keyId} was derived from a seed ` +
|
|
211
|
+
`(path ${keyRecord.meta.derivation.path || 'unknown'}). Rotating it here would ` +
|
|
212
|
+
`replace it with a random key that the mnemonic cannot recover. Derive the ` +
|
|
213
|
+
`successor yourself with createKeyFromMnemonic at a new path, or pass ` +
|
|
214
|
+
`{ derivedKeys: 'skip' } or { derivedKeys: 'randomize' }. No keys were rotated.`);
|
|
215
|
+
}
|
|
216
|
+
if (derivedPolicy === 'skip')
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
toRotate.push(keyRecord);
|
|
220
|
+
}
|
|
221
|
+
// Generate every replacement before committing any, so a failing suite
|
|
222
|
+
// cannot leave the agent half rotated. The commits below are then one
|
|
223
|
+
// registry rotation per key; they are not a single transaction, so a
|
|
224
|
+
// storage failure part-way through leaves the earlier keys rotated.
|
|
225
|
+
const replacements = [];
|
|
226
|
+
for (const keyRecord of toRotate) {
|
|
164
227
|
const suite = this.suiteRegistry.getSuite(keyRecord.meta.suiteId);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
publicKey: newKeypair.publicKey,
|
|
175
|
-
});
|
|
228
|
+
replacements.push(await suite.generateKeypair());
|
|
229
|
+
}
|
|
230
|
+
const newKeys = [];
|
|
231
|
+
for (const [i, keyRecord] of toRotate.entries()) {
|
|
232
|
+
const newKeypair = replacements[i];
|
|
233
|
+
const newMeta = await this.registerUnderFreshId(agentId, keyRecord.meta.suiteId, newKeyId => this.keyRegistry.rotateKey(keyRecord.meta.keyId, newKeyId, newKeypair, {
|
|
234
|
+
expiresAt: options.expiresAt,
|
|
235
|
+
}));
|
|
236
|
+
newKeys.push(toRecord(newMeta, newKeypair.publicKey));
|
|
176
237
|
}
|
|
177
238
|
return newKeys;
|
|
178
239
|
}
|
|
179
240
|
/**
|
|
180
241
|
* Get or create key (idempotent).
|
|
242
|
+
*
|
|
243
|
+
* Concurrent calls on this SDK instance for the same agent and suite share
|
|
244
|
+
* one creation. Two SDK instances (or processes) on one storage backend can
|
|
245
|
+
* still each create a key; that needs a lock in the backend.
|
|
181
246
|
*/
|
|
182
247
|
async getOrCreateKey(agentId, profile, options = {}) {
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
248
|
+
assertAgentId(agentId);
|
|
249
|
+
const slot = JSON.stringify([agentId, profile.primarySignatureSuite]);
|
|
250
|
+
const pending = this.getOrCreateInFlight.get(slot);
|
|
251
|
+
if (pending) {
|
|
252
|
+
const record = await pending;
|
|
253
|
+
return toRecord(record.meta, record.publicKey);
|
|
254
|
+
}
|
|
255
|
+
const work = (async () => {
|
|
256
|
+
// Check if key exists
|
|
257
|
+
const existingKeys = await this.listKeysForAgent(agentId, true);
|
|
258
|
+
const primaryKey = existingKeys.find(k => k.meta.suiteId === profile.primarySignatureSuite);
|
|
259
|
+
if (primaryKey) {
|
|
260
|
+
return primaryKey;
|
|
261
|
+
}
|
|
262
|
+
// Create new key
|
|
263
|
+
return await this.createKey(agentId, profile, options);
|
|
264
|
+
})();
|
|
265
|
+
this.getOrCreateInFlight.set(slot, work);
|
|
266
|
+
try {
|
|
267
|
+
return await work;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
this.getOrCreateInFlight.delete(slot);
|
|
271
|
+
}
|
|
191
272
|
}
|
|
192
273
|
/**
|
|
193
274
|
* Create both primary and secondary keys for an agent.
|
|
@@ -196,6 +277,7 @@ class DefaultKeySDK {
|
|
|
196
277
|
if (!profile.secondarySignatureSuite) {
|
|
197
278
|
throw new Error('secondarySignatureSuite is required to create dual signature keys');
|
|
198
279
|
}
|
|
280
|
+
this.assertDualProfile(profile);
|
|
199
281
|
const primaryKey = await this.createKey(agentId, {
|
|
200
282
|
primarySignatureSuite: profile.primarySignatureSuite,
|
|
201
283
|
}, options);
|
|
@@ -211,6 +293,7 @@ class DefaultKeySDK {
|
|
|
211
293
|
if (!profile.secondarySignatureSuite) {
|
|
212
294
|
throw new Error('secondarySignatureSuite is required to create dual signature keys');
|
|
213
295
|
}
|
|
296
|
+
this.assertDualProfile(profile);
|
|
214
297
|
const primaryKey = await this.getOrCreateKey(agentId, {
|
|
215
298
|
primarySignatureSuite: profile.primarySignatureSuite,
|
|
216
299
|
}, options);
|
|
@@ -219,6 +302,18 @@ class DefaultKeySDK {
|
|
|
219
302
|
}, options);
|
|
220
303
|
return { primaryKey, secondaryKey };
|
|
221
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Fail before creating either key if the pair could not be a hybrid pair:
|
|
307
|
+
* an unknown suite, or the same suite twice (two ECDSA keys are not a
|
|
308
|
+
* classical + post-quantum pair, whatever the profile calls them).
|
|
309
|
+
*/
|
|
310
|
+
assertDualProfile(profile) {
|
|
311
|
+
this.suiteRegistry.getSuite(profile.primarySignatureSuite);
|
|
312
|
+
this.suiteRegistry.getSuite(profile.secondarySignatureSuite);
|
|
313
|
+
if (profile.primarySignatureSuite === profile.secondarySignatureSuite) {
|
|
314
|
+
throw new Error(`Dual signature keys need two different suites; both are ${profile.primarySignatureSuite}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
222
317
|
/**
|
|
223
318
|
* Sign with one or more suites for an agent.
|
|
224
319
|
*/
|
|
@@ -227,7 +322,7 @@ class DefaultKeySDK {
|
|
|
227
322
|
if (activeKeys.length === 0) {
|
|
228
323
|
throw new Error(`No active keys found for agent: ${agentId}`);
|
|
229
324
|
}
|
|
230
|
-
const suitesToUse = suites?.length ? suites :
|
|
325
|
+
const suitesToUse = Array.from(new Set(suites?.length ? suites : activeKeys.map(k => k.meta.suiteId)));
|
|
231
326
|
const signatures = [];
|
|
232
327
|
for (const suiteId of suitesToUse) {
|
|
233
328
|
const key = activeKeys.find(k => k.meta.suiteId === suiteId);
|
|
@@ -240,27 +335,92 @@ class DefaultKeySDK {
|
|
|
240
335
|
return { signatures };
|
|
241
336
|
}
|
|
242
337
|
/**
|
|
243
|
-
* Verify
|
|
338
|
+
* Verify a set of signatures as a hybrid (multi-suite) signature by `agentId`.
|
|
339
|
+
*
|
|
340
|
+
* `allValid` is true only when ALL of these hold:
|
|
341
|
+
* - at least one signature was supplied;
|
|
342
|
+
* - every supplied signature is by a key that belongs to `agentId`, names
|
|
343
|
+
* that key's real suite, passes the key-status policy, and verifies;
|
|
344
|
+
* - every REQUIRED suite has such a signature.
|
|
345
|
+
*
|
|
346
|
+
* The required suites come from the verifier: `options.requiredSuites`, or
|
|
347
|
+
* by default the suites of the agent's currently usable keys in this SDK's
|
|
348
|
+
* registry. They are never read from `signatures`. The check walks the
|
|
349
|
+
* required suites and looks each one up among the valid signatures, so an
|
|
350
|
+
* empty list, or one that merely omits the post-quantum signature, is
|
|
351
|
+
* refused instead of passing because everything in it happens to be valid.
|
|
244
352
|
*/
|
|
245
|
-
async verifyWithSuites(agentId, message, signatures) {
|
|
353
|
+
async verifyWithSuites(agentId, message, signatures, options = {}) {
|
|
354
|
+
assertAgentId(agentId);
|
|
355
|
+
const keyStatus = options.keyStatus ?? 'not-revoked';
|
|
356
|
+
const requiredSuites = Array.from(new Set(options.requiredSuites ??
|
|
357
|
+
(await this.listKeysForAgent(agentId, true)).map(k => k.meta.suiteId)));
|
|
358
|
+
const refuse = (error, results = []) => ({
|
|
359
|
+
results,
|
|
360
|
+
allValid: false,
|
|
361
|
+
requiredSuites,
|
|
362
|
+
missingSuites: requiredSuites.filter(s => !results.some(r => r.valid && r.suiteId === s)),
|
|
363
|
+
error,
|
|
364
|
+
});
|
|
365
|
+
if (!Array.isArray(signatures)) {
|
|
366
|
+
return refuse('signatures must be an array');
|
|
367
|
+
}
|
|
246
368
|
const results = [];
|
|
247
369
|
for (const entry of signatures) {
|
|
370
|
+
const suiteId = String(entry?.suiteId);
|
|
371
|
+
const keyId = String(entry?.keyId);
|
|
372
|
+
const fail = (error) => results.push({ suiteId, keyId, valid: false, error });
|
|
248
373
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
374
|
+
if ((0, crypto_1.agentIdOfKey)(keyId) !== agentId) {
|
|
375
|
+
fail(`Key does not belong to agent '${agentId}'`);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const key = await this.keyRegistry.getPublicKey(keyId);
|
|
379
|
+
if (!key) {
|
|
380
|
+
fail('Key not found');
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (key.meta.suiteId !== suiteId) {
|
|
384
|
+
fail(`Suite mismatch: key is ${key.meta.suiteId}, signature claims ${suiteId}`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!statusAllows(key.meta, keyStatus)) {
|
|
388
|
+
fail(`Key status not accepted (status: ${key.meta.status ?? 'active'}, policy: ${keyStatus})`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!(entry.signature instanceof Uint8Array)) {
|
|
392
|
+
fail('Signature must be a Uint8Array');
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const valid = await this.verifyAgainst(key.meta, key.publicKey, message, entry.signature);
|
|
396
|
+
if (valid) {
|
|
397
|
+
results.push({ suiteId, keyId, valid: true });
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
fail('Signature verification failed');
|
|
401
|
+
}
|
|
251
402
|
}
|
|
252
403
|
catch (error) {
|
|
253
|
-
|
|
254
|
-
suiteId: entry.suiteId,
|
|
255
|
-
keyId: entry.keyId,
|
|
256
|
-
valid: false,
|
|
257
|
-
error: error.message,
|
|
258
|
-
});
|
|
404
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
259
405
|
}
|
|
260
406
|
}
|
|
407
|
+
if (requiredSuites.length === 0) {
|
|
408
|
+
return refuse(`No required suites: agent '${agentId}' has no usable keys and options.requiredSuites was not given`, results);
|
|
409
|
+
}
|
|
410
|
+
if (results.length === 0) {
|
|
411
|
+
return refuse('No signatures supplied', results);
|
|
412
|
+
}
|
|
413
|
+
// Walk the REQUIRED suites and look each up among the valid signatures.
|
|
414
|
+
const missingSuites = requiredSuites.filter(required => !results.some(r => r.valid && r.suiteId === required));
|
|
415
|
+
const everySuppliedValid = results.every(r => r.valid === true);
|
|
261
416
|
return {
|
|
262
417
|
results,
|
|
263
|
-
allValid:
|
|
418
|
+
allValid: everySuppliedValid && missingSuites.length === 0,
|
|
419
|
+
requiredSuites,
|
|
420
|
+
missingSuites,
|
|
421
|
+
...(missingSuites.length > 0
|
|
422
|
+
? { error: `No valid signature for required suite(s): ${missingSuites.join(', ')}` }
|
|
423
|
+
: {}),
|
|
264
424
|
};
|
|
265
425
|
}
|
|
266
426
|
/**
|
|
@@ -280,22 +440,29 @@ class DefaultKeySDK {
|
|
|
280
440
|
*
|
|
281
441
|
* @param agentId Agent identifier
|
|
282
442
|
* @param profile Crypto profile (must use derivation-capable suite)
|
|
283
|
-
* @param mnemonic BIP39 mnemonic phrase
|
|
443
|
+
* @param mnemonic BIP39 mnemonic phrase (words and checksum are validated)
|
|
284
444
|
* @param path BIP32 derivation path (default: "m/44'/0'/0'/0/0")
|
|
285
445
|
* @param passphrase Optional BIP39 passphrase
|
|
286
446
|
* @param options Additional key creation options
|
|
287
447
|
* @returns KeyRecord with derived key
|
|
448
|
+
* @throws if the mnemonic has an unknown word, a wrong word count or a bad checksum
|
|
288
449
|
*/
|
|
289
450
|
async createKeyFromMnemonic(agentId, profile, mnemonic, path = crypto_1.BIP32Derivation.bitcoinPath(0, 0, 0), passphrase, options = {}) {
|
|
290
451
|
const bip32 = new crypto_1.BIP32Derivation();
|
|
291
452
|
const seed = bip32.mnemonicToSeed(mnemonic, passphrase);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
453
|
+
try {
|
|
454
|
+
return await this.createKey(agentId, profile, {
|
|
455
|
+
...options,
|
|
456
|
+
derivationContext: {
|
|
457
|
+
seed,
|
|
458
|
+
path,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
// The seed is the root of every key in the wallet; do not leave it behind.
|
|
464
|
+
seed.fill(0);
|
|
465
|
+
}
|
|
299
466
|
}
|
|
300
467
|
/**
|
|
301
468
|
* Get available capabilities for a specific suite.
|
|
@@ -327,10 +494,36 @@ class DefaultKeySDK {
|
|
|
327
494
|
.map(suite => suite.suiteId);
|
|
328
495
|
}
|
|
329
496
|
/**
|
|
330
|
-
* Create a
|
|
497
|
+
* Create a wallet key from a mnemonic at an explicit BIP44 coin type:
|
|
498
|
+
* `m/44'/coinType'/account'/change/index`.
|
|
499
|
+
*
|
|
500
|
+
* The coin type is required and has no default. It decides which wallets
|
|
501
|
+
* will find the key again: Bitcoin (BTC) is 0, Bitcoin SV is 236.
|
|
502
|
+
*
|
|
503
|
+
* @param agentId Agent identifier
|
|
504
|
+
* @param mnemonic BIP39 mnemonic phrase (words and checksum are validated)
|
|
505
|
+
* @param path Coin type and position in the BIP44 tree
|
|
506
|
+
* @param options Additional key creation options
|
|
507
|
+
* @returns KeyRecord with the derived secp256k1 key
|
|
508
|
+
*/
|
|
509
|
+
async createWalletKey(agentId, mnemonic, path, options = {}) {
|
|
510
|
+
if (!path || typeof path.coinType !== 'number') {
|
|
511
|
+
throw new Error('createWalletKey requires path.coinType (SLIP-44: Bitcoin is 0, Bitcoin SV is 236)');
|
|
512
|
+
}
|
|
513
|
+
const derivationPath = crypto_1.BIP32Derivation.bip44Path(path.coinType, path.account ?? 0, path.change ? 1 : 0, path.index ?? 0);
|
|
514
|
+
return this.createKeyFromMnemonic(agentId, { primarySignatureSuite: 'bsv-ecdsa-secp256k1' }, mnemonic, derivationPath, path.passphrase, options);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Create a wallet key from mnemonic at the Bitcoin (BTC) BIP44 path:
|
|
518
|
+
* m/44'/0'/account'/change/addressIndex
|
|
331
519
|
*
|
|
332
|
-
*
|
|
520
|
+
* NOTE: coin type 0 is BTC. A Bitcoin SV wallet restoring the same mnemonic
|
|
521
|
+
* looks under coin type 236 and will not find this key. The path is left
|
|
522
|
+
* as it is because changing it would move every key already created with
|
|
523
|
+
* this method. For new code use {@link createWalletKey}, which makes the
|
|
524
|
+
* coin type explicit.
|
|
333
525
|
*
|
|
526
|
+
* @deprecated Use createWalletKey(agentId, mnemonic, { coinType, ... }).
|
|
334
527
|
* @param agentId Agent identifier
|
|
335
528
|
* @param mnemonic BIP39 mnemonic phrase
|
|
336
529
|
* @param accountIndex Account index (default: 0)
|
|
@@ -352,6 +545,34 @@ class DefaultKeySDK {
|
|
|
352
545
|
}
|
|
353
546
|
return 'sha256';
|
|
354
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Helper: register a key under the agent's next free key ID.
|
|
550
|
+
*
|
|
551
|
+
* The counter is cached per SDK instance, so another instance (or process)
|
|
552
|
+
* on the same storage can have taken the ID already. The registry refuses
|
|
553
|
+
* to overwrite; on that refusal the counter is re-read from storage and the
|
|
554
|
+
* next ID is tried. Before, the second writer silently replaced the first
|
|
555
|
+
* writer's private key.
|
|
556
|
+
*/
|
|
557
|
+
async registerUnderFreshId(agentId, suiteId, register) {
|
|
558
|
+
// 'ml' for every ml-dsa suite, 'bsv' for bsv-ecdsa-secp256k1
|
|
559
|
+
const suiteName = suiteId.split('-')[0].replace(/[^A-Za-z0-9]/g, '') || 'key';
|
|
560
|
+
for (let attempt = 0; attempt < MAX_KEY_ID_ATTEMPTS; attempt++) {
|
|
561
|
+
const counter = await this.getNextCounter(agentId);
|
|
562
|
+
const keyId = `${agentId}:pk-${suiteName}-${counter}`;
|
|
563
|
+
try {
|
|
564
|
+
return await register(keyId);
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
if (!(error instanceof crypto_1.KeyAlreadyExistsError))
|
|
568
|
+
throw error;
|
|
569
|
+
// Someone else holds that ID: forget the cached counter and re-read.
|
|
570
|
+
this.counterInitialized.delete(agentId);
|
|
571
|
+
this.keyCounter.delete(agentId);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
throw new Error(`Could not allocate a key ID for agent '${agentId}' after ${MAX_KEY_ID_ATTEMPTS} attempts`);
|
|
575
|
+
}
|
|
355
576
|
/**
|
|
356
577
|
* Helper: Get next counter for an agent.
|
|
357
578
|
*/
|
|
@@ -359,19 +580,25 @@ class DefaultKeySDK {
|
|
|
359
580
|
if (!this.counterInitialized.has(agentId)) {
|
|
360
581
|
if (!this.counterInitPromises.has(agentId)) {
|
|
361
582
|
const initPromise = (async () => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
583
|
+
try {
|
|
584
|
+
const existing = await this.keyRegistry.listKeys();
|
|
585
|
+
const max = existing.reduce((acc, meta) => {
|
|
586
|
+
if ((0, crypto_1.agentIdOfKey)(meta.keyId) !== agentId)
|
|
587
|
+
return acc;
|
|
588
|
+
const local = meta.keyId.slice(agentId.length + 1);
|
|
589
|
+
const match = COUNTER_SUFFIX.exec(local);
|
|
590
|
+
if (!match)
|
|
591
|
+
return acc;
|
|
592
|
+
const num = parseInt(match[1], 10);
|
|
593
|
+
return Number.isSafeInteger(num) ? Math.max(acc, num) : acc;
|
|
594
|
+
}, 0);
|
|
595
|
+
// A counter handed out while this scan ran must not be reused.
|
|
596
|
+
this.keyCounter.set(agentId, Math.max(max, this.keyCounter.get(agentId) || 0));
|
|
597
|
+
this.counterInitialized.add(agentId);
|
|
598
|
+
}
|
|
599
|
+
finally {
|
|
600
|
+
this.counterInitPromises.delete(agentId);
|
|
601
|
+
}
|
|
375
602
|
})();
|
|
376
603
|
this.counterInitPromises.set(agentId, initPromise);
|
|
377
604
|
}
|
|
@@ -426,9 +653,17 @@ exports.DefaultKeySDK = DefaultKeySDK;
|
|
|
426
653
|
function createKeySDK(config) {
|
|
427
654
|
const keyRegistry = config?.keyRegistry || new crypto_1.KeyRegistry(new crypto_1.InMemoryKeyStorage());
|
|
428
655
|
const suiteRegistry = config?.suiteRegistry || new crypto_1.SignatureSuiteRegistry();
|
|
429
|
-
// Register default suites if not provided
|
|
656
|
+
// Register default suites if not provided.
|
|
657
|
+
//
|
|
658
|
+
// All three ML-DSA levels are registered, not just 87. The README documents
|
|
659
|
+
// ml-dsa-65 as the recommended default and ml-dsa-44 for constrained
|
|
660
|
+
// devices; registering only 87 made every one of those examples throw
|
|
661
|
+
// `Unknown signature suite` at runtime, with no way for a caller to fix it
|
|
662
|
+
// -- SignatureSuiteRegistry is not re-exported from this package.
|
|
430
663
|
if (!config?.suiteRegistry) {
|
|
431
664
|
suiteRegistry.register(new crypto_1.BsvEcdsaSuite());
|
|
665
|
+
suiteRegistry.register(new crypto_1.MlDsa44Suite());
|
|
666
|
+
suiteRegistry.register(new crypto_1.MlDsa65Suite());
|
|
432
667
|
suiteRegistry.register(new crypto_1.MlDsa87Suite());
|
|
433
668
|
}
|
|
434
669
|
return new DefaultKeySDK({
|