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