provared 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/signatures.js CHANGED
@@ -13,7 +13,8 @@
13
13
  // this file still loads in a browser. In a browser the method is reported as
14
14
  // not built in, which is never a pass.
15
15
 
16
- import { importPublicKey, weakEd25519 } from './keys.js';
16
+ import { Refusal } from './encoding.js';
17
+ import { checkKey, importPublicKey, thumbprint, weakEd25519 } from './keys.js';
17
18
 
18
19
  const SLH = 'SLH-DSA-SHA2-256s';
19
20
 
@@ -122,6 +123,280 @@ export async function sign(alg, privateKey, data) {
122
123
  return new Uint8Array(await globalThis.crypto.subtle.sign({ name: alg }, privateKey, data));
123
124
  }
124
125
 
126
+ // --- signers ---
127
+ //
128
+ // Wherever a writer takes "privateKeys", each item may be a private key of
129
+ // the platform (a Web Crypto CryptoKey, or a Node.js KeyObject), used as it
130
+ // always was, or a signer: an object with "key", the public key as a JSON
131
+ // Web Key in one of the shapes of the format description, section 3.8, and
132
+ // "sign(data)", which gives the signature over exactly those bytes. A
133
+ // signer lets a key live elsewhere: in a key service, or a hardware module.
134
+ // It changes where a signature is made, not how: the record is the same.
135
+ //
136
+ // A signer is not taken on trust. Its two members are read once. Its key
137
+ // must be of the method of its place in the list, a method this device can
138
+ // check, and, where the writer knows the key the record must be signed
139
+ // with, that key. All of this is settled before any signer is asked to
140
+ // sign. Every signature it gives is checked against its key before it is
141
+ // used. A signer that fails in any way leaves no record, and the error
142
+ // names the method and the key. Signers earlier in the list may have
143
+ // signed by then: a key service's own log may hold signatures that match
144
+ // no record.
145
+
146
+ /**
147
+ * Whether an item of "privateKeys" is a private key of the platform for the
148
+ * method of its place: a private Web Crypto CryptoKey of that method that
149
+ * may sign, or for SLH-DSA a private Node.js KeyObject of that kind. What
150
+ * only looks like one (an object made from CryptoKey's own prototype, a
151
+ * public key, a key of another method) is not one.
152
+ * @param {any} item
153
+ * @param {string} alg
154
+ * @returns {boolean}
155
+ */
156
+ function platformKey(item, alg) {
157
+ try {
158
+ if (alg === SLH) return Boolean(nodeCrypto) && item instanceof nodeCrypto.KeyObject && item.type === 'private' && item.asymmetricKeyType === 'slh-dsa-sha2-256s';
159
+ return typeof globalThis.CryptoKey === 'function' && item instanceof globalThis.CryptoKey && item.type === 'private' && item.algorithm.name === alg && item.usages.includes('sign');
160
+ } catch {
161
+ return false; // a value that cannot even be looked into (a revoked Proxy), or one that only looks like a key
162
+ }
163
+ }
164
+
165
+ // What a place in "privateKeys" must hold.
166
+ const signerShape = (alg) =>
167
+ `must be a private key for ${alg} (${alg === SLH ? 'a Node.js KeyObject, or what Node.js reads as one: PEM text, DER bytes with their format, or a JSON Web Key with its format' : 'a Web Crypto CryptoKey'}), or a signer: an object with "key", the public key, and "sign", a function that gives the signature bytes.`;
168
+
169
+ /**
170
+ * In the SLH-DSA place, the seal writer has always handed the key to
171
+ * Node.js's own signing call, which also reads a key written out: PEM text,
172
+ * DER bytes with their format, or a JSON Web Key with its format. Such a
173
+ * key is still taken: it is read exactly as that call read it (with
174
+ * crypto.createPrivateKey, which the call itself uses), and then it must be
175
+ * a private SLH-DSA-SHA2-256s key, as a KeyObject must.
176
+ * @param {any} item
177
+ * @param {string} where
178
+ * @returns {any} the key, as a KeyObject
179
+ */
180
+ function writtenSlhKey(item, where) {
181
+ let made;
182
+ try {
183
+ made = nodeCrypto.createPrivateKey(item);
184
+ } catch {
185
+ made = null; // not a private key Node.js can read, or no Node.js here
186
+ }
187
+ if (!platformKey(made, SLH)) throw new Refusal('bad-field', `${where}: ${signerShape(SLH)}`);
188
+ return made;
189
+ }
190
+
191
+ // A signer that gave no answer in the time the stub writer gives it.
192
+ class NoAnswer extends Error {
193
+ constructor(ms) {
194
+ super(`no answer within ${ms} milliseconds`);
195
+ this.ms = ms;
196
+ }
197
+ }
198
+
199
+ // Two public keys that have passed checkKey: the same key, member for member.
200
+ function sameKey(a, b) {
201
+ const names = Object.keys(b);
202
+ return Object.keys(a).length === names.length && names.every((n) => a[n] === b[n]);
203
+ }
204
+
205
+ // The signature bytes a signer gave, copied, or null where it gave anything
206
+ // else. Bytes are a Uint8Array (a Node.js Buffer is one) or an ArrayBuffer,
207
+ // which is what Web Crypto's own "sign" gives.
208
+ function bytesOf(given) {
209
+ try {
210
+ if (given instanceof Uint8Array) return new Uint8Array(given);
211
+ if (given instanceof ArrayBuffer) return new Uint8Array(given.slice(0));
212
+ } catch {
213
+ // fall through: not bytes that can be read
214
+ }
215
+ return null;
216
+ }
217
+
218
+ /**
219
+ * Read one item of "privateKeys" for the method of its place.
220
+ * @param {any} item
221
+ * @param {string} alg
222
+ * @param {string} where its place, for messages
223
+ * @param {object} [expected] the public key the record must be signed with, where the writer knows it
224
+ * @returns {Promise<{platform: boolean, item: any, key?: object, call?: (data: Uint8Array) => any, sign: (data: Uint8Array) => Promise<Uint8Array>}>}
225
+ */
226
+ async function takeItem(item, alg, where, expected) {
227
+ const platform = (privateKey) => ({ platform: true, item: privateKey, sign: (data) => sign(alg, privateKey, data) });
228
+ if (platformKey(item, alg)) return platform(item);
229
+ let key;
230
+ let fn;
231
+ if (item !== null && (typeof item === 'object' || typeof item === 'function')) {
232
+ try {
233
+ key = item.key;
234
+ fn = item.sign;
235
+ } catch {
236
+ throw new Refusal('bad-field', `${where}: could not be read.`);
237
+ }
238
+ }
239
+ if (typeof fn !== 'function') {
240
+ // Not a signer: in the SLH-DSA place, perhaps a key written out.
241
+ if (alg === SLH && item !== null && item !== undefined) return platform(writtenSlhKey(item, where));
242
+ throw new Refusal('bad-field', `${where}: ${signerShape(alg)}`);
243
+ }
244
+ if (key === undefined || key === null) throw new Refusal('bad-field', `${where}: ${signerShape(alg)}`);
245
+ let copy;
246
+ try {
247
+ copy = structuredClone(key);
248
+ } catch {
249
+ throw new Refusal('bad-key', `${where}.key: must be a public key, as plain data.`);
250
+ }
251
+ checkKey(copy, [alg], `${where}.key`);
252
+ Object.freeze(copy);
253
+ const label = `the ${alg} key ${await thumbprint(copy)}`;
254
+ if (expected && !sameKey(copy, expected)) {
255
+ throw new Refusal('bad-key', `${where}: the signer holds ${label}, which is not the key this record must be signed with (${await thumbprint(expected)}).`);
256
+ }
257
+ // Nothing is signed that this device cannot check: so a signer for a
258
+ // method it does not have built in is refused before any signer is asked.
259
+ if (!(await methodAvailable(alg))) {
260
+ throw new Refusal('bad-key', `${where}: this device does not have ${alg} built in, so it cannot check a signature from the signer for ${label}. No record was made.`);
261
+ }
262
+ const call = (data) => fn.call(item, data);
263
+ return { platform: false, item, key: copy, call, sign: (data) => signerSignature(call, copy, alg, label, where, data) };
264
+ }
265
+
266
+ // One signature from a signer, checked before it is used.
267
+ async function signerSignature(call, key, alg, label, where, data) {
268
+ const failed = (why, cause) => {
269
+ const e = new Refusal('signature-invalid', `${where}: the signer for ${label} ${why} No record was made.`);
270
+ if (cause !== undefined) e.cause = cause;
271
+ return e;
272
+ };
273
+ let given;
274
+ try {
275
+ // The signer is handed a copy: nothing it does to it changes what is checked.
276
+ given = await call(data.slice());
277
+ } catch (e) {
278
+ if (e instanceof NoAnswer) throw failed(`gave no answer within ${e.ms} milliseconds.`);
279
+ throw failed('failed with an error.', e);
280
+ }
281
+ const signature = bytesOf(given);
282
+ if (signature === null) throw failed('gave something other than bytes.');
283
+ if (Object.hasOwn(SIGNATURE_BYTES, alg) && signature.length !== SIGNATURE_BYTES[alg]) {
284
+ throw failed(`gave a signature of ${signature.length} bytes, where an ${alg} signature is ${SIGNATURE_BYTES[alg]}.`);
285
+ }
286
+ // Whether the method is built in was settled before any signer was asked;
287
+ // a signature that cannot be checked even so is not used.
288
+ const state = await verifySignature(key, signature, data);
289
+ if (state !== 'valid') throw failed('gave a signature that does not check against its key: it holds another key, or signed other bytes.');
290
+ return signature;
291
+ }
292
+
293
+ /**
294
+ * Read every item of "privateKeys", one for each method, before anything is
295
+ * signed.
296
+ * @param {any} privateKeys
297
+ * @param {string[]} methods
298
+ * @param {object[]} [expected] the public keys the record must be signed with, where the writer knows them
299
+ * @returns {Promise<Awaited<ReturnType<typeof takeItem>>[]>}
300
+ */
301
+ async function takeItems(privateKeys, methods, expected) {
302
+ let list;
303
+ try {
304
+ list = Array.isArray(privateKeys);
305
+ } catch {
306
+ list = false; // a revoked Proxy
307
+ }
308
+ if (!list) throw new Refusal('bad-field', `privateKeys: must be a list, one private key or signer for each of ${methods.join(', ')}.`);
309
+ const taken = [];
310
+ for (let i = 0; i < methods.length; i++) {
311
+ let item;
312
+ try {
313
+ item = privateKeys[i];
314
+ } catch {
315
+ throw new Refusal('bad-field', `privateKeys[${i}]: could not be read.`);
316
+ }
317
+ taken.push(await takeItem(item, methods[i], `privateKeys[${i}]`, expected ? expected[i] : undefined));
318
+ }
319
+ return taken;
320
+ }
321
+
322
+ /**
323
+ * The functions that sign, one for each method, for a writer that signs
324
+ * now. Keys of the platform sign as they always did; each signer is read
325
+ * and checked as set out above.
326
+ * @param {any} privateKeys
327
+ * @param {string[]} methods
328
+ * @param {object[]} [expected]
329
+ * @returns {Promise<((data: Uint8Array) => Promise<Uint8Array>)[]>}
330
+ */
331
+ export async function signingKeys(privateKeys, methods, expected) {
332
+ return (await takeItems(privateKeys, methods, expected)).map((t) => t.sign);
333
+ }
334
+
335
+ /**
336
+ * For a writer that holds its keys, such as the stub writer: the list to
337
+ * use in place of "privateKeys". Keys of the platform are kept as they
338
+ * are. Each signer is read once, here, and checked against the key the
339
+ * records must be signed with where that is known; in its place stands a
340
+ * signer that waits "ms" milliseconds at most for each signature, through
341
+ * "run". A late answer is not used.
342
+ * @param {any} privateKeys
343
+ * @param {string[]} methods
344
+ * @param {object[]|undefined} expected
345
+ * @param {number} ms
346
+ * @param {(work: () => Promise<any>) => Promise<any>} [run]
347
+ * @returns {Promise<any[]>}
348
+ */
349
+ export async function holdSigningKeys(privateKeys, methods, expected, ms, run = (work) => work()) {
350
+ return (await takeItems(privateKeys, methods, expected)).map((t) => {
351
+ if (t.platform) return t.item;
352
+ const waited = (data) =>
353
+ new Promise((resolve, reject) => {
354
+ const timer = setTimeout(() => reject(new NoAnswer(ms)), ms);
355
+ let answer;
356
+ try {
357
+ answer = Promise.resolve(t.call(data));
358
+ } catch (e) {
359
+ clearTimeout(timer);
360
+ reject(e);
361
+ return;
362
+ }
363
+ answer.then(
364
+ (value) => {
365
+ clearTimeout(timer);
366
+ resolve(value);
367
+ },
368
+ (e) => {
369
+ clearTimeout(timer);
370
+ reject(e);
371
+ },
372
+ );
373
+ });
374
+ return Object.freeze({ key: t.key, sign: (data) => run(() => waited(data)) });
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Wrap a private key held in memory as a signer, so that a key in memory
380
+ * and a key held elsewhere take one path.
381
+ * @param {string} alg 'Ed25519', 'ML-DSA-87' or 'SLH-DSA-SHA2-256s'
382
+ * @param {any} privateKey a Web Crypto CryptoKey, or for SLH-DSA a Node.js KeyObject
383
+ * @param {object} publicJwk its public key, in one of the shapes of the format description, section 3.8
384
+ * @returns {{key: object, sign: (data: Uint8Array) => Promise<Uint8Array>}}
385
+ */
386
+ export function signerFromKey(alg, privateKey, publicJwk) {
387
+ if (alg !== 'Ed25519' && alg !== 'ML-DSA-87' && alg !== SLH) throw new Refusal('bad-field', `alg: must be Ed25519, ML-DSA-87 or ${SLH}.`);
388
+ // Web Crypto holds the first two methods; Node.js's own module the third.
389
+ if (!platformKey(privateKey, alg)) throw new Refusal('bad-field', `privateKey: must be a private key for ${alg}: ${alg === SLH ? 'a Node.js KeyObject' : 'a Web Crypto CryptoKey'}.`);
390
+ let key;
391
+ try {
392
+ key = structuredClone(publicJwk);
393
+ } catch {
394
+ throw new Refusal('bad-key', 'key: must be a public key, as plain data.');
395
+ }
396
+ checkKey(key, [alg], 'key');
397
+ return Object.freeze({ key: Object.freeze(key), sign: (data) => sign(alg, privateKey, data) });
398
+ }
399
+
125
400
  /**
126
401
  * Make a new key set for a recorder, which signs seals: an Ed25519 key, an
127
402
  * ML-DSA-87 key and an SLH-DSA-SHA2-256s key. It needs Node.js: no browser
package/src/standing.js CHANGED
@@ -122,7 +122,7 @@ export function assembleCancellation(prepared, assertion) {
122
122
  * @param {string} fields.cancellation the fingerprint of the cancellation
123
123
  * @param {string} [fields.pass] for a helper agent: the fingerprint of the pass it acts under
124
124
  * @param {number|Date} [fields.when] when the cancellation was handed over; defaults to now
125
- * @param {CryptoKey[]} privateKeys the agent's private keys: Ed25519, then ML-DSA-87
125
+ * @param {any[]} privateKeys the agent's private keys, or signers: Ed25519, then ML-DSA-87
126
126
  * @returns {Promise<{record: object, fingerprint: string}>}
127
127
  */
128
128
  export async function writeAcknowledgement(fields, privateKeys) {
@@ -148,7 +148,7 @@ export async function writeAcknowledgement(fields, privateKeys) {
148
148
  * @param {string} fields.validFrom
149
149
  * @param {string} fields.validUntil
150
150
  * @param {number|Date} [fields.when] defaults to now
151
- * @param {CryptoKey[]} privateKeys the organisation's private keys: Ed25519, then ML-DSA-87
151
+ * @param {any[]} privateKeys the organisation's private keys, or signers: Ed25519, then ML-DSA-87. A signer must hold the keys "by" names
152
152
  * @returns {Promise<{record: object, fingerprint: string}>}
153
153
  */
154
154
  export async function writeVouching(fields, privateKeys) {
@@ -162,7 +162,7 @@ export async function writeVouching(fields, privateKeys) {
162
162
  when: formatTime(fields.when ?? Date.now()),
163
163
  };
164
164
  validateVouchingContent(content);
165
- return signWithKeySet('vouching', content, privateKeys);
165
+ return signWithKeySet('vouching', content, privateKeys, content.by.keys);
166
166
  }
167
167
 
168
168
  /**
@@ -171,7 +171,7 @@ export async function writeVouching(fields, privateKeys) {
171
171
  * @param {object} fields
172
172
  * @param {string} fields.vouching the fingerprint of the vouching record
173
173
  * @param {number|Date} [fields.when] defaults to now
174
- * @param {CryptoKey[]} privateKeys the private keys of the organisation that made the vouching record
174
+ * @param {any[]} privateKeys the private keys of the organisation that made the vouching record, or signers
175
175
  * @returns {Promise<{record: object, fingerprint: string}>}
176
176
  */
177
177
  export async function writeWithdrawal(fields, privateKeys) {
package/src/stub.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // The Stub (the receipt for one action) and the countersignature.
2
2
  // Format description, sections 5 and 6.
3
3
 
4
+ import { DOCUMENT_NAMES, REPEATABLE_DOCUMENT_NAMES, isUnknownReservedDocument } from './documents.js';
4
5
  import { fingerprint, formatTime, randomId, toBase64url } from './encoding.js';
5
6
  import * as f from './fields.js';
6
7
  import { encodeContent, parseRecord, protectedHeaders, signingInput, withinSize, KINDS } from './jws.js';
7
8
  import { KEY_SET_METHODS } from './keys.js';
8
- import { sign } from './signatures.js';
9
+ import { signingKeys } from './signatures.js';
9
10
 
10
11
  /**
11
12
  * Confirm every member of a stub's content (format description, section 5.1).
@@ -48,9 +49,17 @@ export function validateRequest(c) {
48
49
  if (Object.hasOwn(c, 'with')) f.shortName(c.with, 'with');
49
50
  if (Object.hasOwn(c, 'details')) {
50
51
  f.list(c.details, 1, 32, 'details');
52
+ const once = new Set();
51
53
  c.details.forEach((d, i) => {
52
54
  f.members(d, ['name', 'sha256'], [], `details[${i}]`);
53
55
  f.label(d.name, `details[${i}].name`);
56
+ if (isUnknownReservedDocument(d.name)) throw f.fail(`details[${i}].name`, 'names that begin "provared." are reserved, and this one is not on the list of document names.');
57
+ if (Object.hasOwn(DOCUMENT_NAMES, d.name) && !REPEATABLE_DOCUMENT_NAMES.includes(d.name)) {
58
+ if (once.has(d.name)) {
59
+ throw f.fail(`details[${i}].name`, 'this reserved name is named twice. Only provared.trace-id, provared.request-id and provared.event-id may be named more than once.');
60
+ }
61
+ once.add(d.name);
62
+ }
54
63
  f.fingerprintText(d.sha256, `details[${i}].sha256`);
55
64
  });
56
65
  }
@@ -71,12 +80,16 @@ export function validateCountersignatureContent(c) {
71
80
 
72
81
  // Sign content with a key set: two signatures side by side, Ed25519 then
73
82
  // ML-DSA-87, each over its own signing input (format description, 3.9).
74
- export async function signWithKeySet(kind, content, privateKeys) {
83
+ // Each item of "privateKeys" is a key or a signer (signatures.js);
84
+ // "expected" is the key set the record must be signed with, where the
85
+ // writer knows it, and a signer that holds another key is refused.
86
+ export async function signWithKeySet(kind, content, privateKeys, expected) {
75
87
  const { contentBytes, payloadB64 } = encodeContent(content);
76
88
  const headers = protectedHeaders(kind);
89
+ const signers = await signingKeys(privateKeys, KEY_SET_METHODS, expected);
77
90
  const signatures = [];
78
91
  for (let i = 0; i < KEY_SET_METHODS.length; i++) {
79
- const signature = await sign(KEY_SET_METHODS[i], privateKeys[i], signingInput(headers[i], payloadB64));
92
+ const signature = await signers[i](signingInput(headers[i], payloadB64));
80
93
  signatures.push({ protected: headers[i], signature: toBase64url(signature) });
81
94
  }
82
95
  return { record: withinSize({ payload: payloadB64, signatures }), fingerprint: await fingerprint(contentBytes) };
@@ -96,7 +109,7 @@ export async function signWithKeySet(kind, content, privateKeys) {
96
109
  * @param {string} [fields.terms] the fingerprint of the service's terms for agents that the agent relied on
97
110
  * @param {string} [fields.pass] for a helper agent: the fingerprint of the pass it acts under; "after" is then the stub before it under that pass
98
111
  * @param {number|Date} [fields.when] defaults to now
99
- * @param {CryptoKey[]} privateKeys the agent's private keys: Ed25519, then ML-DSA-87
112
+ * @param {any[]} privateKeys the agent's private keys, or signers (signatures.js): Ed25519, then ML-DSA-87
100
113
  * @returns {Promise<{record: object, fingerprint: string, seq: number}>}
101
114
  */
102
115
  export async function writeStub(fields, privateKeys) {
@@ -128,7 +141,7 @@ export async function writeStub(fields, privateKeys) {
128
141
  * reads the stub's fingerprint; it does not judge the stub.
129
142
  *
130
143
  * @param {object} stubRecord the stub as the agent sent it
131
- * @param {CryptoKey[]} privateKeys the service's private keys: Ed25519, then ML-DSA-87
144
+ * @param {any[]} privateKeys the service's private keys, or signers: Ed25519, then ML-DSA-87
132
145
  * @param {number|Date} [when] defaults to now
133
146
  * @returns {Promise<object>} the countersignature record
134
147
  */
package/src/types.d.ts CHANGED
@@ -50,6 +50,21 @@ export type KeySet = [Ed25519Key, MlDsaKey];
50
50
  export type SealKeySet = [Ed25519Key, MlDsaKey, SlhDsaKey];
51
51
  export type PublicKey = PasskeyKey | MlDsaKey | SlhDsaKey;
52
52
 
53
+ /**
54
+ * A signer: a key held elsewhere (a key service, a hardware module) that signs on request. Wherever
55
+ * "privateKeys" is taken, each item may be a key or a signer. A signer's key must be of the method of
56
+ * its place, and, where the writer knows the key the record must be signed with, that key. Every
57
+ * signature it gives is checked against its key before it is used.
58
+ */
59
+ export interface Signer {
60
+ /** The public key, in one of the shapes of the format description, section 3.8. */
61
+ key: Ed25519Key | MlDsaKey | SlhDsaKey;
62
+ /** The signature over exactly these bytes: 64 bytes for Ed25519, 4,627 for ML-DSA-87, 29,792 for SLH-DSA-SHA2-256s. */
63
+ sign(data: Uint8Array): Promise<Uint8Array | ArrayBuffer>;
64
+ }
65
+ /** A private key of the platform (a Web Crypto CryptoKey), or a signer. */
66
+ export type SigningKey = CryptoKey | Signer;
67
+
53
68
  // --- what records hold ---
54
69
 
55
70
  export interface Amount {
@@ -451,8 +466,20 @@ export interface Recorder {
451
466
  act<T>(
452
467
  request: Request,
453
468
  perform: () => Promise<T>,
454
- more?: { when?: number | Date; terms?: Fingerprint; approval?: { record: SignedRecord }; countersign?: (stub: SignedRecord) => Promise<SignedRecord | null> },
455
- ): Promise<{ done: boolean; answer: BeforeAnswer; result?: T; stub?: WrittenStub }>;
469
+ more?: {
470
+ when?: number | Date;
471
+ terms?: Fingerprint;
472
+ approval?: { record: SignedRecord };
473
+ countersign?: (stub: SignedRecord) => Promise<SignedRecord | null>;
474
+ /**
475
+ * Up to four further documents, from what the action gave back. The stub is then signed after the action,
476
+ * in the same turn, and keeps the time the action began. The request may then name at most 28 documents,
477
+ * and no approval may be given. If it fails, the stub is written without them, and "detailsAfter" in the
478
+ * answer says why.
479
+ */
480
+ detailsAfter?: (result: T) => DocumentRef[] | null | undefined | Promise<DocumentRef[] | null | undefined>;
481
+ },
482
+ ): Promise<{ done: boolean; answer: BeforeAnswer; result?: T; stub?: WrittenStub; detailsAfter?: { added: number; problem: Finding | null } }>;
456
483
  book(): string;
457
484
  /**
458
485
  * What the record shows as it stands: the answer a whole check of the book gives, with the cancellations
@@ -481,7 +508,8 @@ export type WrittenStub = Written & { seq: number; line: string; countersignatur
481
508
  export function openRecorder(o: {
482
509
  book: string;
483
510
  slip: Fingerprint;
484
- privateKeys: CryptoKey[];
511
+ /** The agent's private keys, or signers: Ed25519, then ML-DSA-87. A signer must hold the keys the slip, or the pass, names. */
512
+ privateKeys: SigningKey[];
485
513
  issuerKeys: string[];
486
514
  pass?: Fingerprint;
487
515
  options?: CheckOptions;
@@ -489,6 +517,8 @@ export function openRecorder(o: {
489
517
  now?: () => number;
490
518
  /** How long the other side is given to countersign, in milliseconds. Defaults to 30,000. */
491
519
  countersignWithin?: number;
520
+ /** How long a signer is given for each signature, in milliseconds. Defaults to 30,000. After that nothing is written. */
521
+ signWithin?: number;
492
522
  }): Promise<Recorder>;
493
523
 
494
524
  /** One of an agent's tools, as it is described to recordTools. */
@@ -564,21 +594,21 @@ export function writeStub(
564
594
  when?: number | Date;
565
595
  id?: Base64url;
566
596
  },
567
- privateKeys: CryptoKey[],
597
+ privateKeys: SigningKey[],
568
598
  ): Promise<Written & { seq: number }>;
569
599
  /** An agent hands part of its permission to a helper agent. */
570
600
  export function writePass(
571
601
  fields: { slip: Fingerprint; from?: Fingerprint; to: Party; actions: string[]; limits?: Limit[]; validFrom: Time; validUntil: Time; when?: number | Date; id?: Base64url },
572
- privateKeys: CryptoKey[],
602
+ privateKeys: SigningKey[],
573
603
  ): Promise<Written>;
574
- export function countersign(stub: SignedRecord, privateKeys: CryptoKey[], when?: number | Date): Promise<SignedRecord>;
604
+ export function countersign(stub: SignedRecord, privateKeys: SigningKey[], when?: number | Date): Promise<SignedRecord>;
575
605
  export function writeRefusal(
576
606
  fields: { slip: Fingerprint; by: Party; action: string; amount?: Amount; reason: string; when?: number | Date; id?: Base64url },
577
- privateKeys: CryptoKey[],
607
+ privateKeys: SigningKey[],
578
608
  ): Promise<Written>;
579
609
  export function writeTerms(
580
610
  fields: { by: Party; accepts: string[]; never?: string[]; validFrom: Time; validUntil: Time; id?: Base64url },
581
- privateKeys: CryptoKey[],
611
+ privateKeys: SigningKey[],
582
612
  ): Promise<Written>;
583
613
  export type VouchedFor =
584
614
  | { kind: 'person'; key: PasskeyKey; name: string }
@@ -586,15 +616,19 @@ export type VouchedFor =
586
616
  | { kind: 'recorder'; keys: SealKeySet; name: string };
587
617
  export function writeVouching(
588
618
  fields: { by: Party; for: VouchedFor; validFrom: Time; validUntil: Time; when?: number | Date; id?: Base64url },
589
- privateKeys: CryptoKey[],
619
+ privateKeys: SigningKey[],
590
620
  ): Promise<Written>;
591
- export function writeWithdrawal(fields: { vouching: Fingerprint; when?: number | Date; id?: Base64url }, privateKeys: CryptoKey[]): Promise<Written>;
621
+ export function writeWithdrawal(fields: { vouching: Fingerprint; when?: number | Date; id?: Base64url }, privateKeys: SigningKey[]): Promise<Written>;
592
622
  /** The agent's side states that it was handed the person's cancellation of a slip, and when. */
593
623
  export function writeAcknowledgement(
594
624
  fields: { slip: Fingerprint; cancellation: Fingerprint; pass?: Fingerprint; when?: number | Date; id?: Base64url },
595
- privateKeys: CryptoKey[],
625
+ privateKeys: SigningKey[],
596
626
  ): Promise<Written>;
597
- /** It needs Node.js: no browser has the seal's third signing method. */
627
+ /**
628
+ * It needs Node.js: no browser has the seal's third signing method. "privateKeys": the recorder's three keys (two
629
+ * Web Crypto keys, and a Node.js KeyObject or what Node.js reads as one: PEM text, DER bytes with their format, or a
630
+ * JSON Web Key with its format), or signers, each holding the key "by" names in its place.
631
+ */
598
632
  export function writeSeal(
599
633
  book: string,
600
634
  fields: { by: { keys: SealKeySet; name: string }; previous?: Fingerprint; when?: number | Date; id?: Base64url },
@@ -612,6 +646,8 @@ export function makeShow(text: string, indexes: number[], options?: { seal?: num
612
646
 
613
647
  export function generateKeySet(): Promise<{ keys: KeySet; privateKeys: CryptoKey[] }>;
614
648
  export function generateSealKeySet(): Promise<{ keys: SealKeySet; privateKeys: unknown[] }>;
649
+ /** A private key held in memory, wrapped as a signer: a CryptoKey, or for SLH-DSA-SHA2-256s a Node.js KeyObject. */
650
+ export function signerFromKey(alg: 'Ed25519' | 'ML-DSA-87' | 'SLH-DSA-SHA2-256s', privateKey: unknown, key: Ed25519Key | MlDsaKey | SlhDsaKey): Signer;
615
651
  export function fingerprint(contentBytes: Uint8Array): Promise<Fingerprint>;
616
652
  export function sha256(bytes: Uint8Array): Promise<Uint8Array>;
617
653
  export function formatTime(when: number | Date): Time;
@@ -623,6 +659,16 @@ export function utf8(text: string): Uint8Array;
623
659
  // --- the shared list ---
624
660
 
625
661
  export const RESERVED_PREFIX: 'provared.';
662
+ /** Document names that begin with this are reserved: only those in DOCUMENT_NAMES may be used. */
663
+ export const RESERVED_DOCUMENT_PREFIX: 'provared.';
664
+ /** The reserved document names, and the exact text whose SHA-256 each one gives (format description, section 27). */
665
+ export const DOCUMENT_NAMES: Readonly<Record<string, string>>;
666
+ /** For "provared.http-request": the method and the address exactly as sent, and the body's bytes where there is a body. */
667
+ export function httpRequestFingerprint(r: { method: string; url: string; body?: Uint8Array }): Promise<Fingerprint>;
668
+ /** For "provared.http-response": the status code, and the body's bytes where there is a body. */
669
+ export function httpResponseFingerprint(r: { status: number; body?: Uint8Array }): Promise<Fingerprint>;
670
+ /** For "provared.trace-id", "provared.request-id", "provared.event-id" and "provared.error": SHA-256 of the text's UTF-8 bytes, exactly as given. */
671
+ export function identifierFingerprint(id: string): Promise<Fingerprint>;
626
672
  export const ACTION_KINDS: Record<string, string>;
627
673
  export const SHARED_ACTIONS: Record<string, [kind: string, meaning: string]>;
628
674
  export const CONDUCT_RULES: Record<string, string>;