provared 0.4.0 → 0.6.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/CHANGELOG.md +1083 -0
- package/README.md +6 -3
- package/bin/provared-check.mjs +115 -11
- package/docs/guide.md +464 -15
- package/docs/threat-model.md +315 -9
- package/package.json +2 -2
- package/page/check-page.js +11 -32
- package/page/index.html +4 -0
- package/page/render.js +157 -9
- package/spec/provared-format.md +809 -23
- package/src/book.js +539 -18
- package/src/check.d.ts +3 -0
- package/src/check.js +2 -0
- package/src/denial.js +123 -0
- package/src/der.js +55 -6
- package/src/dev.d.ts +1 -1
- package/src/dev.js +1 -1
- package/src/documents.js +149 -0
- package/src/guard.js +13 -2
- package/src/index.js +18 -3
- package/src/jws.js +8 -0
- package/src/keys.js +48 -1
- package/src/pass.js +1 -1
- package/src/recorder.js +267 -15
- package/src/seal.js +36 -7
- package/src/sealer.js +812 -0
- package/src/service.js +4 -4
- package/src/signatures.js +281 -5
- package/src/standing.js +4 -4
- package/src/stub.js +18 -5
- package/src/timestamp.js +55 -16
- package/src/tools.js +30 -13
- package/src/tree.js +139 -7
- package/src/types.d.ts +314 -20
- package/src/witness.js +63 -0
- package/src/witnessing.js +362 -0
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 {
|
|
@@ -140,9 +155,10 @@ export type BookEntry =
|
|
|
140
155
|
| { stub: SignedRecord; countersignature?: SignedRecord; approval?: SignedRecord }
|
|
141
156
|
| { refusal: SignedRecord }
|
|
142
157
|
| { terms: SignedRecord }
|
|
143
|
-
| { seal: SignedRecord; stamps?: StampItem[] }
|
|
158
|
+
| { seal: SignedRecord; stamps?: StampItem[]; witnesses?: SignedRecord[] }
|
|
144
159
|
| { cancellation: SignedRecord; stamps?: StampItem[] }
|
|
145
160
|
| { acknowledgement: SignedRecord }
|
|
161
|
+
| { denial: SignedRecord }
|
|
146
162
|
| { vouching: SignedRecord }
|
|
147
163
|
| { withdrawal: SignedRecord }
|
|
148
164
|
| { pass: SignedRecord };
|
|
@@ -153,7 +169,7 @@ export interface Show {
|
|
|
153
169
|
root: Fingerprint;
|
|
154
170
|
/** "disclosures" reveal covered fields of the record on that page. */
|
|
155
171
|
pages: { index: number; entry: string; path: Fingerprint[]; disclosures?: string[] }[];
|
|
156
|
-
seal?: { seal: SignedRecord; stamps?: StampItem[] };
|
|
172
|
+
seal?: { seal: SignedRecord; stamps?: StampItem[]; witnesses?: SignedRecord[] };
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
// --- what the checker answers ---
|
|
@@ -184,6 +200,28 @@ export interface Stamp {
|
|
|
184
200
|
/** For a block: its number, as the proof states it. Nothing checks it. */
|
|
185
201
|
height?: number;
|
|
186
202
|
}
|
|
203
|
+
/** A witness statement beside a seal, as the checker found it (format description, section 29). */
|
|
204
|
+
export interface WitnessCheck {
|
|
205
|
+
/** The statement's own fingerprint. */
|
|
206
|
+
fingerprint: Fingerprint | null;
|
|
207
|
+
/** The fingerprint of the witness's key set: how the person names a witness they trust. */
|
|
208
|
+
witness: Fingerprint | null;
|
|
209
|
+
/** The witness's name, a label. */
|
|
210
|
+
name: string | null;
|
|
211
|
+
/** When the witness says it signed: its own word. */
|
|
212
|
+
when: Time | null;
|
|
213
|
+
/** "valid": it passed its check and both its signatures were checked here. "unavailable": this device could not check them both. */
|
|
214
|
+
state: 'valid' | 'unavailable' | 'invalid';
|
|
215
|
+
/** Whether its witness is one the person named. */
|
|
216
|
+
trusted: boolean;
|
|
217
|
+
/**
|
|
218
|
+
* Whether it counts: valid, trusted, its seal passed its own check with its first two signatures checked here, the book
|
|
219
|
+
* starts with a permission signed by a passkey you named, you named the recorders you expect, and every permission the
|
|
220
|
+
* seal covers names the first line's passkey and agent (a Show must hold every page the seal covers). Section 29.2.
|
|
221
|
+
*/
|
|
222
|
+
counted: boolean;
|
|
223
|
+
signatures: KeySignature[];
|
|
224
|
+
}
|
|
187
225
|
/** A time-stamp beside a seal or a cancellation: a service's answer, or a proof with the header of its block. */
|
|
188
226
|
export type StampItem = Base64url | { block: Base64url; proof: Base64url };
|
|
189
227
|
/** Who vouches for a name, if anyone does. */
|
|
@@ -191,7 +229,7 @@ export type Vouched = { counted: boolean; by: string; keys: Fingerprint } | null
|
|
|
191
229
|
|
|
192
230
|
export interface CheckedEntry {
|
|
193
231
|
index: number;
|
|
194
|
-
kind: 'slip' | 'stub' | 'refusal' | 'terms' | 'seal' | 'cancellation' | 'acknowledgement' | 'vouching' | 'withdrawal' | 'pass' | 'unreadable';
|
|
232
|
+
kind: 'slip' | 'stub' | 'refusal' | 'terms' | 'seal' | 'cancellation' | 'acknowledgement' | 'vouching' | 'withdrawal' | 'pass' | 'denial' | 'unreadable';
|
|
195
233
|
fingerprint: Fingerprint | null;
|
|
196
234
|
/** How much of the entry's own signing this device confirmed. */
|
|
197
235
|
verified: 'all' | 'some' | 'none';
|
|
@@ -242,6 +280,8 @@ export interface CheckedEntry {
|
|
|
242
280
|
service?: string | null;
|
|
243
281
|
sealer?: Fingerprint;
|
|
244
282
|
stamps?: Stamp[];
|
|
283
|
+
/** A seal: the witness statements on its line. */
|
|
284
|
+
witnesses?: WitnessCheck[];
|
|
245
285
|
counted?: boolean;
|
|
246
286
|
}
|
|
247
287
|
|
|
@@ -266,11 +306,18 @@ export interface Summary {
|
|
|
266
306
|
seals: number;
|
|
267
307
|
cancellations: number;
|
|
268
308
|
acknowledgements: number;
|
|
309
|
+
denials: number;
|
|
269
310
|
vouchings: number;
|
|
270
311
|
passes: number;
|
|
271
312
|
};
|
|
272
313
|
/** How far the last time-stamp from a trusted service reaches. */
|
|
273
314
|
sealed: { entries: number; when: Time; seal: number; by: 'service' | 'block' } | null;
|
|
315
|
+
/**
|
|
316
|
+
* How far a witness the person named reaches: the latest seal for which a statement counts, the entries it covers,
|
|
317
|
+
* the witnesses whose statements count for it, and every seal for which one counts. Null where none does, or where a
|
|
318
|
+
* problem was found. It excuses nothing.
|
|
319
|
+
*/
|
|
320
|
+
witnessed: { seal: number; entries: number; by: Fingerprint[]; seals: number[] } | null;
|
|
274
321
|
/** What no check can show, in plain sentences. */
|
|
275
322
|
limits: string[];
|
|
276
323
|
}
|
|
@@ -318,6 +365,46 @@ export interface HeldAcknowledgement {
|
|
|
318
365
|
inBook: number | null;
|
|
319
366
|
}
|
|
320
367
|
|
|
368
|
+
/** A seal held elsewhere, as it is handed over: the entry it is in the book. */
|
|
369
|
+
export interface HandedSeal {
|
|
370
|
+
seal: SignedRecord;
|
|
371
|
+
stamps?: StampItem[];
|
|
372
|
+
/** Witness statements for the seal, 1 to 4, none from one witness twice: those beside it in the book, or that came later. */
|
|
373
|
+
witnesses?: SignedRecord[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* A seal held elsewhere (by the person, an auditor, a copy kept apart), handed over beside the book. The book
|
|
378
|
+
* must hold the same seal at the place it states: a book that ends before that place was cut short
|
|
379
|
+
* ("book-cut-short"), and one that holds another line there was changed ("seal-not-in-book").
|
|
380
|
+
*/
|
|
381
|
+
export interface HeldSeal {
|
|
382
|
+
kind: 'seal';
|
|
383
|
+
fingerprint: Fingerprint | null;
|
|
384
|
+
/** The fingerprint of the key set that signed it. */
|
|
385
|
+
sealer: Fingerprint | null;
|
|
386
|
+
/** The entry of the book at which it stands: the number of entries it covers. Null where it could not be read. */
|
|
387
|
+
place: number | null;
|
|
388
|
+
/** That entry, where the book holds the same seal there; null otherwise, or where it was not used. */
|
|
389
|
+
inBook: number | null;
|
|
390
|
+
/**
|
|
391
|
+
* Whether it was compared with the book: it passed its check, its recorder is one you named, this device checked its
|
|
392
|
+
* first two signatures, and it gives the book's own number (section 21.6). One that was not compared gives no pass.
|
|
393
|
+
*/
|
|
394
|
+
used: boolean;
|
|
395
|
+
verified: 'all' | 'some' | 'none';
|
|
396
|
+
problems: Finding[];
|
|
397
|
+
notes: string[];
|
|
398
|
+
signatures: KeySignature[];
|
|
399
|
+
/** Its own time-stamps: checked and shown, not used for the entries of the book. */
|
|
400
|
+
stamps: Stamp[];
|
|
401
|
+
/** The witness statements handed over with it. One that counts, for a seal the book holds at its place, counts for that seal. */
|
|
402
|
+
witnesses: WitnessCheck[];
|
|
403
|
+
content?: any;
|
|
404
|
+
/** The earliest time a time-stamp from a trusted service gives it, where all three of its signatures were checked. */
|
|
405
|
+
stampedAt?: Time;
|
|
406
|
+
}
|
|
407
|
+
|
|
321
408
|
export interface CheckResult {
|
|
322
409
|
format: string;
|
|
323
410
|
size: number;
|
|
@@ -325,8 +412,8 @@ export interface CheckResult {
|
|
|
325
412
|
problems: Finding[];
|
|
326
413
|
notes: string[];
|
|
327
414
|
entries: CheckedEntry[];
|
|
328
|
-
/** What was handed over beside the book, and what came of it. */
|
|
329
|
-
held: HeldCancellation[];
|
|
415
|
+
/** What was handed over beside the book, and what came of it: the cancellations first, then the seals. */
|
|
416
|
+
held: (HeldCancellation | HeldSeal)[];
|
|
330
417
|
summary: Summary;
|
|
331
418
|
}
|
|
332
419
|
|
|
@@ -341,6 +428,8 @@ export interface CheckOptions {
|
|
|
341
428
|
blocks?: string[];
|
|
342
429
|
/** The key set fingerprints of the organisations whose vouching the checker trusts. */
|
|
343
430
|
vouchers?: Fingerprint[];
|
|
431
|
+
/** The key set fingerprints of the witnesses the checker trusts (format description, section 29). */
|
|
432
|
+
witnesses?: Fingerprint[];
|
|
344
433
|
/** For covered fields: the disclosures handed over, by the fingerprint of the slip they belong to. */
|
|
345
434
|
disclosures?: Record<Fingerprint, string[]>;
|
|
346
435
|
/**
|
|
@@ -349,6 +438,11 @@ export interface CheckOptions {
|
|
|
349
438
|
* out, or holds them with a later time-stamp.
|
|
350
439
|
*/
|
|
351
440
|
cancellations?: HandedCancellation[];
|
|
441
|
+
/**
|
|
442
|
+
* Seals held elsewhere, each as the entry it is in the book. At most 64. The book must hold each at the
|
|
443
|
+
* place it states. Each counts only where its recorder is one named in "sealKeys".
|
|
444
|
+
*/
|
|
445
|
+
seals?: HandedSeal[];
|
|
352
446
|
expectedRoot?: Fingerprint;
|
|
353
447
|
expectedSize?: number;
|
|
354
448
|
/** Signing methods to treat as not built into this device. It can only withhold a pass. */
|
|
@@ -391,6 +485,14 @@ export function checkSlip(record: SignedRecord, options?: Pick<CheckOptions, 'is
|
|
|
391
485
|
export function methodAvailable(alg: string): Promise<boolean>;
|
|
392
486
|
export function thumbprint(key: PublicKey): Promise<string>;
|
|
393
487
|
export function keySetFingerprint(keys: KeySet | SealKeySet): Promise<Fingerprint>;
|
|
488
|
+
/**
|
|
489
|
+
* Check a consistency proof (RFC 9162, section 2.1.4.2): that the tree of m leaves whose root is rootM is the first
|
|
490
|
+
* part of the tree of n leaves whose root is rootN. For m equal to n the proof is empty and the roots the same; for
|
|
491
|
+
* m of 0 the proof is empty and rootM the root of the empty tree. Never throws for a bad proof.
|
|
492
|
+
*/
|
|
493
|
+
export function verifyConsistency(m: number, n: number, rootM: Uint8Array, rootN: Uint8Array, proof: Uint8Array[]): Promise<boolean>;
|
|
494
|
+
/** Check an audit path (RFC 9162, section 2.1.3.2). */
|
|
495
|
+
export function verifyInclusion(index: number, size: number, leaf: Uint8Array, path: Uint8Array[], root: Uint8Array): Promise<boolean>;
|
|
394
496
|
/** The fingerprint of a block, as it is written everywhere: 64 hex characters. */
|
|
395
497
|
export function blockFingerprint(header: Uint8Array): Promise<string>;
|
|
396
498
|
/** A chain of Bitcoin block headers, checked on this device from the chain's first block. */
|
|
@@ -418,6 +520,8 @@ export function conditionWords(condition: Condition): string;
|
|
|
418
520
|
export function neverWords(name: string): string;
|
|
419
521
|
export const LIMITS: string[];
|
|
420
522
|
export const REFUSAL_REASONS: Record<string, string>;
|
|
523
|
+
/** Who said no, in a denial: "check", "person" or "rule", each with its meaning. */
|
|
524
|
+
export const DENIAL_SOURCES: Record<'check' | 'person' | 'rule', string>;
|
|
421
525
|
|
|
422
526
|
export interface Proposal extends Request {
|
|
423
527
|
slip: Fingerprint;
|
|
@@ -451,8 +555,30 @@ export interface Recorder {
|
|
|
451
555
|
act<T>(
|
|
452
556
|
request: Request,
|
|
453
557
|
perform: () => Promise<T>,
|
|
454
|
-
more?: {
|
|
455
|
-
|
|
558
|
+
more?: {
|
|
559
|
+
when?: number | Date;
|
|
560
|
+
terms?: Fingerprint;
|
|
561
|
+
approval?: { record: SignedRecord };
|
|
562
|
+
countersign?: (stub: SignedRecord) => Promise<SignedRecord | null>;
|
|
563
|
+
/**
|
|
564
|
+
* Up to four further documents, from what the action gave back. The stub is then signed after the action,
|
|
565
|
+
* in the same turn, and keeps the time the action began. The request may then name at most 28 documents,
|
|
566
|
+
* and no approval may be given. If it fails, the stub is written without them, and "detailsAfter" in the
|
|
567
|
+
* answer says why.
|
|
568
|
+
*/
|
|
569
|
+
detailsAfter?: (result: T) => DocumentRef[] | null | undefined | Promise<DocumentRef[] | null | undefined>;
|
|
570
|
+
/** False writes no denial for this call, where the writer was opened with "denials: true". */
|
|
571
|
+
denial?: boolean;
|
|
572
|
+
},
|
|
573
|
+
): Promise<{ done: boolean; answer: BeforeAnswer; result?: T; stub?: WrittenStub; detailsAfter?: { added: number; problem: Finding | null }; denial?: DenialAnswer }>;
|
|
574
|
+
/**
|
|
575
|
+
* Writes a denial for an action that was not taken where "act" wrote none: the person said no, a rule of the
|
|
576
|
+
* software around the agent said no, or ("check", with the codes of that answer) the check before acting said no
|
|
577
|
+
* on a call for which "act" was asked to write none. A writer opened without "denials: true" writes none.
|
|
578
|
+
*/
|
|
579
|
+
deny(request: Request, more: { source: 'check' | 'person' | 'rule'; codes?: string[]; when?: number | Date }): Promise<DenialAnswer>;
|
|
580
|
+
/** Whether this writer writes denials, as it was opened. */
|
|
581
|
+
readonly denials: boolean;
|
|
456
582
|
book(): string;
|
|
457
583
|
/**
|
|
458
584
|
* What the record shows as it stands: the answer a whole check of the book gives, with the cancellations
|
|
@@ -476,12 +602,18 @@ export interface Recorder {
|
|
|
476
602
|
*/
|
|
477
603
|
add(entry: BookEntry): Promise<{ acknowledgement?: SignedRecord }>;
|
|
478
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* What the stub writer did about a denial. "notWritten" counts the denials not written because of the hourly
|
|
607
|
+
* limit since the writer was opened.
|
|
608
|
+
*/
|
|
609
|
+
export type DenialAnswer = { written: true; fingerprint: Fingerprint; line: string; notWritten: number } | { written: false; problem: Finding; notWritten: number };
|
|
479
610
|
/** A stub the stub writer wrote. A countersignature that did not check is left out, and "problem" says why. */
|
|
480
611
|
export type WrittenStub = Written & { seq: number; line: string; countersignature: { accepted: boolean; problem: Finding | null } };
|
|
481
612
|
export function openRecorder(o: {
|
|
482
613
|
book: string;
|
|
483
614
|
slip: Fingerprint;
|
|
484
|
-
|
|
615
|
+
/** The agent's private keys, or signers: Ed25519, then ML-DSA-87. A signer must hold the keys the slip, or the pass, names. */
|
|
616
|
+
privateKeys: SigningKey[];
|
|
485
617
|
issuerKeys: string[];
|
|
486
618
|
pass?: Fingerprint;
|
|
487
619
|
options?: CheckOptions;
|
|
@@ -489,6 +621,12 @@ export function openRecorder(o: {
|
|
|
489
621
|
now?: () => number;
|
|
490
622
|
/** How long the other side is given to countersign, in milliseconds. Defaults to 30,000. */
|
|
491
623
|
countersignWithin?: number;
|
|
624
|
+
/** How long a signer is given for each signature, in milliseconds. Defaults to 30,000. After that nothing is written. */
|
|
625
|
+
signWithin?: number;
|
|
626
|
+
/** True to write a denial whenever "act" does not take an action, and to let "deny" write one. Defaults to false. */
|
|
627
|
+
denials?: boolean;
|
|
628
|
+
/** The most denials written in any hour, by the clock: 1 to 100,000. Defaults to 600. */
|
|
629
|
+
denialsPerHour?: number;
|
|
492
630
|
}): Promise<Recorder>;
|
|
493
631
|
|
|
494
632
|
/** One of an agent's tools, as it is described to recordTools. */
|
|
@@ -500,7 +638,7 @@ export interface RecordedTool<A = any, R = any> {
|
|
|
500
638
|
/** The id of the service the tool deals with, as the slip names it. */
|
|
501
639
|
with?: string | ((args: A) => string | undefined);
|
|
502
640
|
amount?: Amount | ((args: A) => Amount | undefined);
|
|
503
|
-
/** The documents to name in the stub, or false for none. By default: "arguments", the fingerprint of the arguments. */
|
|
641
|
+
/** The documents to name in the stub, or false for none. By default: "provared.arguments", the fingerprint of the arguments. */
|
|
504
642
|
details?: false | ((args: A) => DocumentRef[] | Promise<DocumentRef[]>);
|
|
505
643
|
/** Asks the other side to countersign the stub. */
|
|
506
644
|
countersign?: (stub: SignedRecord, args: A) => Promise<SignedRecord | null>;
|
|
@@ -509,9 +647,11 @@ export interface RecordedTool<A = any, R = any> {
|
|
|
509
647
|
}
|
|
510
648
|
/** A call through a recorded tool that was not run. "answer" is what the check before acting said. */
|
|
511
649
|
export class NotTaken extends Error {
|
|
512
|
-
constructor(tool: string, answer: BeforeAnswer);
|
|
650
|
+
constructor(tool: string, answer: BeforeAnswer, denial?: DenialAnswer);
|
|
513
651
|
tool: string;
|
|
514
652
|
answer: BeforeAnswer;
|
|
653
|
+
/** Where the stub writer writes denials: what it did about the denial for this call. */
|
|
654
|
+
denial?: DenialAnswer;
|
|
515
655
|
}
|
|
516
656
|
/**
|
|
517
657
|
* Puts an agent's tools behind the stub writer: each call asks first, runs the tool only if the slip allows
|
|
@@ -564,21 +704,21 @@ export function writeStub(
|
|
|
564
704
|
when?: number | Date;
|
|
565
705
|
id?: Base64url;
|
|
566
706
|
},
|
|
567
|
-
privateKeys:
|
|
707
|
+
privateKeys: SigningKey[],
|
|
568
708
|
): Promise<Written & { seq: number }>;
|
|
569
709
|
/** An agent hands part of its permission to a helper agent. */
|
|
570
710
|
export function writePass(
|
|
571
711
|
fields: { slip: Fingerprint; from?: Fingerprint; to: Party; actions: string[]; limits?: Limit[]; validFrom: Time; validUntil: Time; when?: number | Date; id?: Base64url },
|
|
572
|
-
privateKeys:
|
|
712
|
+
privateKeys: SigningKey[],
|
|
573
713
|
): Promise<Written>;
|
|
574
|
-
export function countersign(stub: SignedRecord, privateKeys:
|
|
714
|
+
export function countersign(stub: SignedRecord, privateKeys: SigningKey[], when?: number | Date): Promise<SignedRecord>;
|
|
575
715
|
export function writeRefusal(
|
|
576
716
|
fields: { slip: Fingerprint; by: Party; action: string; amount?: Amount; reason: string; when?: number | Date; id?: Base64url },
|
|
577
|
-
privateKeys:
|
|
717
|
+
privateKeys: SigningKey[],
|
|
578
718
|
): Promise<Written>;
|
|
579
719
|
export function writeTerms(
|
|
580
720
|
fields: { by: Party; accepts: string[]; never?: string[]; validFrom: Time; validUntil: Time; id?: Base64url },
|
|
581
|
-
privateKeys:
|
|
721
|
+
privateKeys: SigningKey[],
|
|
582
722
|
): Promise<Written>;
|
|
583
723
|
export type VouchedFor =
|
|
584
724
|
| { kind: 'person'; key: PasskeyKey; name: string }
|
|
@@ -586,21 +726,163 @@ export type VouchedFor =
|
|
|
586
726
|
| { kind: 'recorder'; keys: SealKeySet; name: string };
|
|
587
727
|
export function writeVouching(
|
|
588
728
|
fields: { by: Party; for: VouchedFor; validFrom: Time; validUntil: Time; when?: number | Date; id?: Base64url },
|
|
589
|
-
privateKeys:
|
|
729
|
+
privateKeys: SigningKey[],
|
|
590
730
|
): Promise<Written>;
|
|
591
|
-
export function writeWithdrawal(fields: { vouching: Fingerprint; when?: number | Date; id?: Base64url }, privateKeys:
|
|
731
|
+
export function writeWithdrawal(fields: { vouching: Fingerprint; when?: number | Date; id?: Base64url }, privateKeys: SigningKey[]): Promise<Written>;
|
|
592
732
|
/** The agent's side states that it was handed the person's cancellation of a slip, and when. */
|
|
593
733
|
export function writeAcknowledgement(
|
|
594
734
|
fields: { slip: Fingerprint; cancellation: Fingerprint; pass?: Fingerprint; when?: number | Date; id?: Base64url },
|
|
595
|
-
privateKeys:
|
|
735
|
+
privateKeys: SigningKey[],
|
|
736
|
+
): Promise<Written>;
|
|
737
|
+
/** The format's own list of finding codes, from which a denial by the check before acting names its reasons. */
|
|
738
|
+
export const FINDING_CODES: readonly string[];
|
|
739
|
+
/** The reasons of a refusal by the check before acting, as a denial names them: 1 to 16 codes, each once. */
|
|
740
|
+
export function denialCodes(answer: Pick<BeforeAnswer, 'problems' | 'breaches'>): string[];
|
|
741
|
+
/** The agent's side states that it did not take an action that was asked for, and why. */
|
|
742
|
+
export function writeDenial(
|
|
743
|
+
fields: Request & { slip: Fingerprint; pass?: Fingerprint; source: 'check' | 'person' | 'rule'; codes?: string[]; when?: number | Date; id?: Base64url },
|
|
744
|
+
privateKeys: SigningKey[],
|
|
596
745
|
): Promise<Written>;
|
|
597
|
-
/**
|
|
746
|
+
/**
|
|
747
|
+
* It needs Node.js: no browser has the seal's third signing method. "privateKeys": the recorder's three keys (two
|
|
748
|
+
* Web Crypto keys, and a Node.js KeyObject or what Node.js reads as one: PEM text, DER bytes with their format, or a
|
|
749
|
+
* JSON Web Key with its format), or signers, each holding the key "by" names in its place.
|
|
750
|
+
*/
|
|
598
751
|
export function writeSeal(
|
|
599
752
|
book: string,
|
|
600
|
-
|
|
753
|
+
/** "book": the book's number among the books of one permission, 2 or more; 1 or none for the first (section 21.6). */
|
|
754
|
+
fields: { by: { keys: SealKeySet; name: string }; previous?: Fingerprint; book?: number; when?: number | Date; id?: Base64url },
|
|
601
755
|
privateKeys: unknown[],
|
|
602
756
|
): Promise<Written & { fingerprintBytes: Uint8Array }>;
|
|
603
757
|
|
|
758
|
+
/** The most witness statements beside one seal, or handed over with one seal held elsewhere. */
|
|
759
|
+
export const MAX_WITNESSES: 4;
|
|
760
|
+
/** A witness signs for a seal. This only writes the statement; it does not check the seal (see openWitness). */
|
|
761
|
+
export function writeWitness(
|
|
762
|
+
fields: { seal: Fingerprint; size: number; root: Fingerprint; by: { keys: KeySet; name: string }; when?: number | Date; id?: Base64url },
|
|
763
|
+
privateKeys: SigningKey[],
|
|
764
|
+
): Promise<Written>;
|
|
765
|
+
/** The consistency proof from the tree of the first m leaves to the tree of all of them (RFC 9162, section 2.1.4.1). Empty for m of 0 or m equal to the number of leaves. */
|
|
766
|
+
export function consistencyProof(leaves: Uint8Array[], m: number): Promise<Uint8Array[]>;
|
|
767
|
+
/** The audit path of one leaf (RFC 9162, section 2.1.3). */
|
|
768
|
+
export function inclusionPath(leaves: Uint8Array[], index: number): Promise<Uint8Array[]>;
|
|
769
|
+
/** How a witness keeps sequences of books apart: the recorder's key set, with the passkey and the agent's key set that the permission on the book's first line names. */
|
|
770
|
+
export function bookIdentity(keys: SealKeySet, firstLine: Uint8Array): Promise<Fingerprint>;
|
|
771
|
+
/**
|
|
772
|
+
* What a witness is sent with a seal: the consistency proof from the seal it holds for the book ("from", its number
|
|
773
|
+
* of entries), or, where it holds none or the seal is the first of the next book, the audit path of the book's first line in
|
|
774
|
+
* the new seal's tree.
|
|
775
|
+
*/
|
|
776
|
+
export function witnessProof(book: string, size: number, from?: number | null): Promise<Uint8Array[]>;
|
|
777
|
+
/** A witness's state, as it hands it back: to be saved before a statement is given out. */
|
|
778
|
+
export interface WitnessState {
|
|
779
|
+
type: 'provared.witness-state.v0';
|
|
780
|
+
/** The fingerprint of the witness's key set. */
|
|
781
|
+
by: Fingerprint;
|
|
782
|
+
/** For each recorder, passkey (its thumbprint) and agent (the fingerprint of its key set): the latest seal this witness signed for, and its statement. */
|
|
783
|
+
books: { passkey: Fingerprint; agent: Fingerprint; seal: SignedRecord; statement: SignedRecord }[];
|
|
784
|
+
}
|
|
785
|
+
export type WitnessAnswer<T> = T | { refused: Finding };
|
|
786
|
+
export interface Witness {
|
|
787
|
+
/** Sign for a seal if it extends the latest seal of its book this witness holds. The state handed back holds the new seal: save it first. */
|
|
788
|
+
witness(entry: { seal: SignedRecord; stamps?: StampItem[]; witnesses?: SignedRecord[] }, proof: Uint8Array[], firstLine: Uint8Array): Promise<WitnessAnswer<{ statement: SignedRecord; state: WitnessState }>>;
|
|
789
|
+
/** Take as the latest seal of its book a seal this witness signed for before, from a copy of what it gave out. It signs nothing. */
|
|
790
|
+
update(entry: { seal: SignedRecord; witnesses: SignedRecord[]; stamps?: StampItem[] }, proof: Uint8Array[], firstLine: Uint8Array): Promise<WitnessAnswer<{ state: WitnessState }>>;
|
|
791
|
+
/** Opened from a saved state, the witness signs nothing until it is told it has been brought up to date. */
|
|
792
|
+
ready(): void;
|
|
793
|
+
latest(book: Fingerprint): { seal: SignedRecord; statement: SignedRecord } | null;
|
|
794
|
+
state(): WitnessState;
|
|
795
|
+
}
|
|
796
|
+
/** A witness needs SLH-DSA, so it needs Node.js. */
|
|
797
|
+
export function openWitness(o: {
|
|
798
|
+
by: { keys: KeySet; name: string };
|
|
799
|
+
signers: SigningKey[];
|
|
800
|
+
state?: WitnessState;
|
|
801
|
+
clock?: () => number;
|
|
802
|
+
withoutMethods?: string[];
|
|
803
|
+
}): Promise<Witness>;
|
|
804
|
+
|
|
805
|
+
/** The leaf fingerprint of one line of a book, without its line feed: SHA-256 of 0x00 and the line (RFC 6962, section 2.1). What a recorder sends a central sealer. */
|
|
806
|
+
export function leafFingerprint(line: string): Promise<Uint8Array>;
|
|
807
|
+
/** Which book a sealer call is about: the leaf fingerprint of its first line (a first book), or that with the book's number (section 21.6). */
|
|
808
|
+
export type SealerBook = Fingerprint | { first: Fingerprint; number?: number };
|
|
809
|
+
/** A central sealer's state, as it hands it back: to be saved before the answer is given out. At most 1,000 books. */
|
|
810
|
+
export interface SealerState {
|
|
811
|
+
type: 'provared.sealer-state.v0';
|
|
812
|
+
/** The fingerprint of the sealer's key set. */
|
|
813
|
+
by: Fingerprint;
|
|
814
|
+
/**
|
|
815
|
+
* For each book: its first line's leaf fingerprint and number, the recorder it belongs to (absent for the sealer's own calls),
|
|
816
|
+
* the book it follows, whether a later book follows it, and its latest seal with the tree's parts at that seal. A book is kept at
|
|
817
|
+
* its latest seal, or with none at its first line: lines that no seal covers are asked for again after a start.
|
|
818
|
+
*/
|
|
819
|
+
books: {
|
|
820
|
+
first: Fingerprint;
|
|
821
|
+
number: number;
|
|
822
|
+
recorder?: string;
|
|
823
|
+
follows?: Fingerprint;
|
|
824
|
+
closed: boolean;
|
|
825
|
+
/** The latest seal and its time-stamps (at most four); witness statements are never kept. */
|
|
826
|
+
latest?: { entry: { seal: SignedRecord; stamps?: StampItem[] }; parts: Base64url[] };
|
|
827
|
+
}[];
|
|
828
|
+
}
|
|
829
|
+
export type SealerAnswer<T> = T | { refused: Finding; size?: number };
|
|
830
|
+
/** The calls of a central sealer, made as one recorder (forRecorder) or as none (the sealer's own). */
|
|
831
|
+
export interface SealerCalls {
|
|
832
|
+
/**
|
|
833
|
+
* Take in the leaf fingerprints of new lines of one book, in order, from place "at" (the number of lines it holds). A later book
|
|
834
|
+
* names the book it follows; a book whose first line the sealer holds starts only as the next book after the latest with that
|
|
835
|
+
* first line. "line" is the book's first line itself: where any recorder has a list of agents, a book is started only with it,
|
|
836
|
+
* and it must be a permission for an agent of the caller's own. Where the call started a book, the answer holds the state: save it first.
|
|
837
|
+
*/
|
|
838
|
+
extend(book: SealerBook, at: number, leaves: Uint8Array[], more?: { follows?: Fingerprint; line?: string }): Promise<SealerAnswer<{ size: number; state?: SealerState }>>;
|
|
839
|
+
/** Sign a seal over the tree this sealer holds; "previous" is the seal the new seal must name, as the book shows it. The state handed back holds the new seal: save it first. */
|
|
840
|
+
seal(
|
|
841
|
+
book: SealerBook,
|
|
842
|
+
more: { previous: Fingerprint | null; when?: number | Date; id?: Base64url },
|
|
843
|
+
): Promise<SealerAnswer<{ entry: { seal: SignedRecord; stamps?: StampItem[] }; line: string; fingerprint: Fingerprint; fingerprintBytes: Uint8Array; state: SealerState; stampProblem?: Finding }>>;
|
|
844
|
+
/**
|
|
845
|
+
* Bring the sealer up to date for a book from a later seal it signed, with the leaf fingerprints from its own latest seal to that one.
|
|
846
|
+
* Where it is the first seal of a later book, the book before is closed; where the sealer holds that book at an earlier seal than the
|
|
847
|
+
* one named, it is refused until that book is brought up to date. A book it does not hold is started here under the same rules
|
|
848
|
+
* as with extend, "line" included. It signs nothing. It keeps the seal, whose three signatures it checks, and its time-stamps
|
|
849
|
+
* (at most four), read for their form only: no time-stamp's signature is checked. It keeps no witness statements; an entry longer
|
|
850
|
+
* than a line of a book may be is refused (too-large). The entry is read once, and only that copy is used.
|
|
851
|
+
*/
|
|
852
|
+
update(
|
|
853
|
+
book: SealerBook,
|
|
854
|
+
entry: { seal: SignedRecord; stamps?: StampItem[]; witnesses?: SignedRecord[] },
|
|
855
|
+
leaves: Uint8Array[],
|
|
856
|
+
more?: { line?: string },
|
|
857
|
+
): Promise<SealerAnswer<{ size: number; state: SealerState }>>;
|
|
858
|
+
latest(book: SealerBook): { entry: { seal: SignedRecord; stamps?: StampItem[] }; line: string; fingerprint: Fingerprint } | null;
|
|
859
|
+
held(book: SealerBook): { first: Fingerprint; number: number; follows: Fingerprint | null; size: number; closed: boolean; latest: Fingerprint | null; sealed: number } | null;
|
|
860
|
+
}
|
|
861
|
+
export interface Sealer extends SealerCalls {
|
|
862
|
+
state(): SealerState;
|
|
863
|
+
/**
|
|
864
|
+
* The same calls for one named recorder (a label): its books, and every book after them, are its own. "rules": the fingerprints
|
|
865
|
+
* of the key sets of the agents whose permissions it may start books with (1 to 64; an agent belongs to one recorder), and the
|
|
866
|
+
* most books it may hold (1 to 1,000). A recorder's rules are fixed by the first call that gives them.
|
|
867
|
+
*/
|
|
868
|
+
forRecorder(recorder: string, rules?: { agents?: Fingerprint[]; books?: number }): SealerCalls;
|
|
869
|
+
/**
|
|
870
|
+
* Take off the books that hold no seal (nothing was signed for them): every recorder's, or one recorder's (null for the sealer's
|
|
871
|
+
* own calls). The state handed back is to be saved.
|
|
872
|
+
*/
|
|
873
|
+
releaseUnsealed(o?: { recorder?: string | null }): Promise<{ released: { first: Fingerprint; number: number; recorder: string | null }[]; state: SealerState }>;
|
|
874
|
+
}
|
|
875
|
+
/** A central sealer: it seals books it is sent only the leaf fingerprints of. It checks every seal it signs, so it needs Node.js. */
|
|
876
|
+
export function openSealer(o: {
|
|
877
|
+
by: { keys: SealKeySet; name: string };
|
|
878
|
+
signers: unknown[];
|
|
879
|
+
/** Ask a time-stamp service for a stamp over a seal's fingerprint: an item of "stamps", or null. */
|
|
880
|
+
stamp?: (fingerprintBytes: Uint8Array) => Promise<StampItem | null> | StampItem | null;
|
|
881
|
+
state?: SealerState;
|
|
882
|
+
clock?: () => number;
|
|
883
|
+
signWithin?: number;
|
|
884
|
+
}): Promise<Sealer>;
|
|
885
|
+
|
|
604
886
|
/** A proof from a public blockchain and the 80-byte header of its block, in the form they have in "stamps". */
|
|
605
887
|
export function blockStampItem(proof: Uint8Array, header: Uint8Array): { block: Base64url; proof: Base64url };
|
|
606
888
|
|
|
@@ -612,6 +894,8 @@ export function makeShow(text: string, indexes: number[], options?: { seal?: num
|
|
|
612
894
|
|
|
613
895
|
export function generateKeySet(): Promise<{ keys: KeySet; privateKeys: CryptoKey[] }>;
|
|
614
896
|
export function generateSealKeySet(): Promise<{ keys: SealKeySet; privateKeys: unknown[] }>;
|
|
897
|
+
/** A private key held in memory, wrapped as a signer: a CryptoKey, or for SLH-DSA-SHA2-256s a Node.js KeyObject. */
|
|
898
|
+
export function signerFromKey(alg: 'Ed25519' | 'ML-DSA-87' | 'SLH-DSA-SHA2-256s', privateKey: unknown, key: Ed25519Key | MlDsaKey | SlhDsaKey): Signer;
|
|
615
899
|
export function fingerprint(contentBytes: Uint8Array): Promise<Fingerprint>;
|
|
616
900
|
export function sha256(bytes: Uint8Array): Promise<Uint8Array>;
|
|
617
901
|
export function formatTime(when: number | Date): Time;
|
|
@@ -623,6 +907,16 @@ export function utf8(text: string): Uint8Array;
|
|
|
623
907
|
// --- the shared list ---
|
|
624
908
|
|
|
625
909
|
export const RESERVED_PREFIX: 'provared.';
|
|
910
|
+
/** Document names that begin with this are reserved: only those in DOCUMENT_NAMES may be used. */
|
|
911
|
+
export const RESERVED_DOCUMENT_PREFIX: 'provared.';
|
|
912
|
+
/** The reserved document names, and the exact text whose SHA-256 each one gives (format description, section 27). */
|
|
913
|
+
export const DOCUMENT_NAMES: Readonly<Record<string, string>>;
|
|
914
|
+
/** For "provared.http-request": the method and the address exactly as sent, and the body's bytes where there is a body. */
|
|
915
|
+
export function httpRequestFingerprint(r: { method: string; url: string; body?: Uint8Array }): Promise<Fingerprint>;
|
|
916
|
+
/** For "provared.http-response": the status code, and the body's bytes where there is a body. */
|
|
917
|
+
export function httpResponseFingerprint(r: { status: number; body?: Uint8Array }): Promise<Fingerprint>;
|
|
918
|
+
/** 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. */
|
|
919
|
+
export function identifierFingerprint(id: string): Promise<Fingerprint>;
|
|
626
920
|
export const ACTION_KINDS: Record<string, string>;
|
|
627
921
|
export const SHARED_ACTIONS: Record<string, [kind: string, meaning: string]>;
|
|
628
922
|
export const CONDUCT_RULES: Record<string, string>;
|
package/src/witness.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The witness statement: a party with a key set of its own (a witness)
|
|
2
|
+
// states that it saw a seal, that the seal extends every earlier seal of
|
|
3
|
+
// the same book it signed for, and that it will sign for no seal of that
|
|
4
|
+
// book that does not extend this one. It is signed with the witness's two
|
|
5
|
+
// keys. A statement sits beside its seal in the seal's book entry, as
|
|
6
|
+
// time-stamps do, or is handed to a checker beside a seal held elsewhere.
|
|
7
|
+
// Format description, section 29.
|
|
8
|
+
|
|
9
|
+
import { formatTime, randomId } from './encoding.js';
|
|
10
|
+
import * as f from './fields.js';
|
|
11
|
+
import { KINDS } from './jws.js';
|
|
12
|
+
import { checkKeySet } from './keys.js';
|
|
13
|
+
import { signWithKeySet } from './stub.js';
|
|
14
|
+
|
|
15
|
+
/** The most witness statements one seal entry may hold, or that may be handed over with one seal. */
|
|
16
|
+
export const MAX_WITNESSES = 4;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Confirm every member of a witness statement's content.
|
|
20
|
+
* @param {any} c
|
|
21
|
+
*/
|
|
22
|
+
export function validateWitnessContent(c) {
|
|
23
|
+
f.members(c, ['by', 'id', 'root', 'seal', 'size', 'type', 'when'], [], 'witness');
|
|
24
|
+
f.id(c.id, 'id');
|
|
25
|
+
f.fingerprintText(c.seal, 'seal');
|
|
26
|
+
f.wholeNumber(c.size, 'size');
|
|
27
|
+
if (c.size < 1) throw f.fail('size', 'must be 1 or more.');
|
|
28
|
+
f.fingerprintText(c.root, 'root');
|
|
29
|
+
f.members(c.by, ['keys', 'name'], [], 'by');
|
|
30
|
+
f.label(c.by.name, 'by.name');
|
|
31
|
+
checkKeySet(c.by.keys, 'by.keys');
|
|
32
|
+
f.time(c.when, 'when');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write and sign a witness statement for a seal. This function only writes
|
|
37
|
+
* the statement: it does not check the seal. A witness should sign only as
|
|
38
|
+
* the format description, section 29.3, sets out; openWitness
|
|
39
|
+
* (witnessing.js) does.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} fields
|
|
42
|
+
* @param {string} fields.seal the seal's fingerprint
|
|
43
|
+
* @param {number} fields.size the seal's "size", repeated
|
|
44
|
+
* @param {string} fields.root the seal's "root", repeated
|
|
45
|
+
* @param {{keys: object[], name: string}} fields.by the witness's public key set and its name (a label)
|
|
46
|
+
* @param {number|Date} [fields.when] defaults to now
|
|
47
|
+
* @param {any[]} privateKeys the witness's private keys, or signers (signatures.js): Ed25519, then ML-DSA-87. A
|
|
48
|
+
* signer must hold the key "by" names in its place
|
|
49
|
+
* @returns {Promise<{record: object, fingerprint: string}>}
|
|
50
|
+
*/
|
|
51
|
+
export async function writeWitness(fields, privateKeys) {
|
|
52
|
+
const content = {
|
|
53
|
+
type: KINDS.witness.type,
|
|
54
|
+
id: fields.id ?? randomId(),
|
|
55
|
+
seal: fields.seal,
|
|
56
|
+
size: fields.size,
|
|
57
|
+
root: fields.root,
|
|
58
|
+
by: fields.by,
|
|
59
|
+
when: formatTime(fields.when ?? Date.now()),
|
|
60
|
+
};
|
|
61
|
+
validateWitnessContent(content);
|
|
62
|
+
return signWithKeySet('witness', content, privateKeys, content.by.keys);
|
|
63
|
+
}
|