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/docs/guide.md
CHANGED
|
@@ -6,7 +6,7 @@ Everything about the format, the library, the checker, the stub writer beside an
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
**Version 0.
|
|
9
|
+
**Version 0.6.0, a draft.** Published as it is, by one author in their
|
|
10
10
|
own time; fixes as time allows. The format may still change, so do not
|
|
11
11
|
rely on a draft record staying checkable.
|
|
12
12
|
|
|
@@ -21,7 +21,7 @@ An Internet-Draft is a working document, not a standard.
|
|
|
21
21
|
|---|---|---|
|
|
22
22
|
| **Slip** | A short permission a person signs with a passkey: which agent, which actions, what limits and conditions, with whom, from when until when, and why | Built |
|
|
23
23
|
| **Stub** | A signed receipt for each action, pointing back to its slip, countersigned by the other side where possible, and approved by the person where the slip asks for it | Built |
|
|
24
|
-
| **Seal** | The stubs chained together and gathered under one top fingerprint, signed by whoever keeps the book
|
|
24
|
+
| **Seal** | The stubs chained together and gathered under one top fingerprint, signed by whoever keeps the book, time-stamped by an outside service, and signed for by witnesses | Built, with the part of a witness that decides and signs. A public place where seals are kept is not |
|
|
25
25
|
| **Show** | One page shown to a checker with proof that it is in the book, and when the book was sealed, with the names it does not need kept covered | Built. Only a slip's names and purpose can be covered so far |
|
|
26
26
|
|
|
27
27
|
## What is here
|
|
@@ -151,9 +151,9 @@ be loaded from anywhere. The tool prints the file's SHA-256 fingerprint,
|
|
|
151
151
|
so that a copy can be compared with it. (Tried on 8 October 2026 in the
|
|
152
152
|
three browser builds this repository is tested with, and on 13 October
|
|
153
153
|
2026 in Chrome 155, Edge 155 and Brave 155.) The file for
|
|
154
|
-
version 0.
|
|
154
|
+
version 0.6.0 is attached to the release on GitHub. Its SHA-256
|
|
155
155
|
fingerprint is
|
|
156
|
-
`
|
|
156
|
+
`ae48c425616d70849aa8d43b1d71ef7ee56736da3ba6a00eaa4375c428e60a25`.
|
|
157
157
|
|
|
158
158
|
**Run the demonstration in a browser.**
|
|
159
159
|
|
|
@@ -168,8 +168,9 @@ is invented: nothing is bought, sent or deleted. The checking
|
|
|
168
168
|
page on its own is at `http://localhost:8787/check`. It takes a record
|
|
169
169
|
file, the fingerprints you already trust (keys, time-stamp services,
|
|
170
170
|
blocks), and what you were handed with the record: the person's own
|
|
171
|
-
copies of cancellations,
|
|
172
|
-
read in the browser and
|
|
171
|
+
copies of cancellations, seals of the book held elsewhere, and
|
|
172
|
+
disclosures for covered fields. All of it is read in the browser and
|
|
173
|
+
sent nowhere.
|
|
173
174
|
|
|
174
175
|
## What a slip can say
|
|
175
176
|
|
|
@@ -236,6 +237,214 @@ The fingerprints for the sample are in
|
|
|
236
237
|
`samples/office-supplies.expected.json`. The sample's time-stamp service
|
|
237
238
|
is made up, as its passkey is.
|
|
238
239
|
|
|
240
|
+
### Seals held elsewhere
|
|
241
|
+
|
|
242
|
+
Whoever keeps the book can cut off its end, with the last seal, and what
|
|
243
|
+
is left still checks. Anyone who keeps a copy of a seal elsewhere (the
|
|
244
|
+
person, an auditor, a second copy out of the recorder's reach) can hand
|
|
245
|
+
it to the checker beside the book, as the entry it is in the book:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
provared-check record.jsonl --seal seal.json --sealer <fingerprint> --issuer <thumbprint>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
In the library: `seals`, up to 64. A seal at entry n covers the n
|
|
252
|
+
entries before it, so the book must hold that very seal at entry n. A
|
|
253
|
+
book that ends before it is reported as cut short (`book-cut-short`); a
|
|
254
|
+
book that holds another line there, as changed (`seal-not-in-book`).
|
|
255
|
+
Both are problems with the book. A seal handed over is checked as any
|
|
256
|
+
seal is, and counts only where you named its recorder (`--sealer`, or
|
|
257
|
+
`sealKeys`); one that does not pass its own check is a problem with the
|
|
258
|
+
whole check. On a device without SLH-DSA (the Python version, and
|
|
259
|
+
browsers that have ML-DSA, such as Edge and Brave 155 when tested) it
|
|
260
|
+
counts on its Ed25519 and ML-DSA-87 signatures, and says so; on a device
|
|
261
|
+
without ML-DSA it is not used. The seal is compared with the book by its
|
|
262
|
+
content: the same content with other signatures is the same seal, and
|
|
263
|
+
the answer says that the signatures differ. Nothing in a seal names its
|
|
264
|
+
book, so hand over only seals of the book being checked: a seal of
|
|
265
|
+
another book by the same recorder (for example the book it kept before
|
|
266
|
+
it started a new one) is reported as `seal-not-in-book` or
|
|
267
|
+
`book-cut-short`. Without `--sealer`, the seals handed over are not
|
|
268
|
+
compared with the book, and the command-line checker says so on a line
|
|
269
|
+
of its own and gives no pass.
|
|
270
|
+
Its own time-stamps are shown but are not used for the entries of the
|
|
271
|
+
book. The answer lists each one in `held`, after any cancellations. A
|
|
272
|
+
Show does not use one. What it does not catch: a book cut back after the
|
|
273
|
+
latest seal anyone holds, or a book never shown to anyone who holds one.
|
|
274
|
+
|
|
275
|
+
### Witnesses
|
|
276
|
+
|
|
277
|
+
A witness is a party, with a key set of its own, that keeps for each
|
|
278
|
+
book the latest seal it has signed for, and signs a short statement for
|
|
279
|
+
a new seal only where the new seal extends that one (format
|
|
280
|
+
description, section 29). The recorder sends it the seal and a
|
|
281
|
+
consistency proof (RFC 9162, section 2.1.4); the statement goes beside
|
|
282
|
+
the seal on its line, in `witnesses`, up to four, or is handed to the
|
|
283
|
+
checker later with a copy of the seal (`--seal` with a file of
|
|
284
|
+
`{"seal": ..., "witnesses": [...]}`). Name the witnesses you trust by
|
|
285
|
+
the fingerprints of their key sets:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
provared-check record.jsonl --witness <fingerprint> --sealer <fingerprint> --issuer <thumbprint>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
In the library: `witnesses`. The answer's `summary.witnessed` gives the
|
|
292
|
+
latest seal for which a statement from a witness you trust counts, the
|
|
293
|
+
entries it covers, those witnesses, and every seal witnessed; the
|
|
294
|
+
command-line checker prints it as "Witnessed by a witness you trust?".
|
|
295
|
+
A statement that does not check, or that speaks of another seal, is a
|
|
296
|
+
problem (`witness-invalid`). A statement counts only where you also
|
|
297
|
+
named the passkey you trust (`--issuer`) and the recorder you expect
|
|
298
|
+
(`--sealer`), the book starts with a permission signed by that passkey,
|
|
299
|
+
and every permission before the seal is from that passkey for the same
|
|
300
|
+
agent (a Show must hold every page the seal covers): a witness knows a book only by the passkey and the agent of
|
|
301
|
+
the permission it starts from and its recorder's keys, and the recorder
|
|
302
|
+
chooses the first line and its keys. Being witnessed excuses nothing
|
|
303
|
+
and is not needed for a pass. What it adds: a second history of the
|
|
304
|
+
entries before a witnessed seal, sealed by the same recorder and
|
|
305
|
+
starting from any permission that passkey gave that agent (a renewal
|
|
306
|
+
included), shown to someone else, would need that witness to sign for it
|
|
307
|
+
too. What it does not: a history sealed with other keys, even keys you
|
|
308
|
+
also named, or one that starts from a permission for another agent;
|
|
309
|
+
anything never shown to the witness; or the time. To see whether the end
|
|
310
|
+
of the book is missing, ask the witness for the latest seal it holds for
|
|
311
|
+
this recorder, passkey and agent, and hand that seal over beside the book
|
|
312
|
+
it is of. One recorder key set keeps one sequence for each person and
|
|
313
|
+
agent: books kept side by side for one agent need recorder keys of
|
|
314
|
+
their own.
|
|
315
|
+
|
|
316
|
+
A recorder may keep the record of one permission in several books: a
|
|
317
|
+
seal says which in `book` (2, 3 and so on; the first book has no
|
|
318
|
+
number), and the first seal of a later book names the last seal of the
|
|
319
|
+
book before. `writeSeal` takes `book`. A witness treats every book of one
|
|
320
|
+
recorder, person and agent as one sequence: it signs for a new book only
|
|
321
|
+
where its first seal starts from the latest seal it holds, and never
|
|
322
|
+
again for the book before. A checker of one book does not see the book
|
|
323
|
+
before; handed that book's last seal, it says whether this book follows
|
|
324
|
+
on from it (format description, section 21.6). A seal handed over that
|
|
325
|
+
gives another book number than the book is not compared with it, and
|
|
326
|
+
gives no pass: the command-line checker says "Seals held elsewhere
|
|
327
|
+
compared? NO" and does not end with code 0, even for an honest book.
|
|
328
|
+
To check book n − 1 against the seal book n starts from, check book
|
|
329
|
+
n − 1 with that seal handed over beside it.
|
|
330
|
+
|
|
331
|
+
The library holds a witness: `openWitness({ by, signers, state })`, with
|
|
332
|
+
`witness(entry, proof, firstLine)`, which signs only for a seal whose
|
|
333
|
+
three signatures it checked and that extends the seal it holds, and
|
|
334
|
+
hands back its state with each statement, to be saved before the
|
|
335
|
+
statement is given out; `update`, `ready`, `latest` and `state`.
|
|
336
|
+
`witnessProof(book, size, from)` makes the proof to send it (`from`:
|
|
337
|
+
the size of the seal the witness holds of the same book, or none for a
|
|
338
|
+
first seal and for the first seal of the next book), and `bookIdentity`
|
|
339
|
+
names a sequence as a witness knows it: by the recorder's keys with the
|
|
340
|
+
passkey and the agent of the permission on the book's first line. Whoever can change a witness's
|
|
341
|
+
state can make it forget a sequence: keep it as safely as its keys. A
|
|
342
|
+
witness needs SLH-DSA, so it needs Node.js; the Python version has none.
|
|
343
|
+
|
|
344
|
+
### A central sealer
|
|
345
|
+
|
|
346
|
+
The recorder's three keys need not sit on the agent's computer. A
|
|
347
|
+
central sealer, run by a team other than the one that runs the agent,
|
|
348
|
+
holds them and seals books it never sees: the recorder sends it the
|
|
349
|
+
leaf fingerprint of each new line (`leafFingerprint(line)`: the SHA-256
|
|
350
|
+
of the byte 0x00 and the line), and asks for a seal; the sealer builds
|
|
351
|
+
the same tree as the checker, signs a seal over its own number of
|
|
352
|
+
entries and top fingerprint, and hands back the seal's entry, which the
|
|
353
|
+
recorder adds to its book with the stub writer's `add` (format
|
|
354
|
+
description, section 21.7). A sealer sent a wrong fingerprint signs a
|
|
355
|
+
seal the book refuses (`seal-mismatch`).
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
import { leafFingerprint, openSealer, toBase64url } from 'provared';
|
|
359
|
+
|
|
360
|
+
const sealer = await openSealer({ by: { keys, name: 'Our central sealer' }, signers, state });
|
|
361
|
+
const first = toBase64url(await leafFingerprint(lines[0])); // a book is known by its first line
|
|
362
|
+
await sealer.extend(first, 0, await Promise.all(lines.map(leafFingerprint)));
|
|
363
|
+
const made = await sealer.seal(first, { previous: null }); // the seal the book's latest seal names, or null
|
|
364
|
+
// save made.state first, then hand made.entry to the book
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`extend(book, at, leaves)` takes lines only at the place it holds, so a
|
|
368
|
+
line is never taken twice. `seal(book, { previous })` signs only where
|
|
369
|
+
`previous` is the latest seal it holds of that book: a sealer whose
|
|
370
|
+
state is behind the book signs nothing until it is brought up to date
|
|
371
|
+
with `update(book, entry, leaves)` (the book's latest seal, and the leaf
|
|
372
|
+
fingerprints since the latest seal it holds); a book that lost a seal
|
|
373
|
+
the sealer gave takes it back with `latest(book)`. A later book is named
|
|
374
|
+
`{ first, number }`, and its first `extend` names the book it follows
|
|
375
|
+
(`{ follows }`); its first seal names the last seal of that book, which
|
|
376
|
+
is then closed, and so is a book brought up to date from such a first
|
|
377
|
+
seal (bring books up to date in their order: a later book's first seal
|
|
378
|
+
is refused, `not-up-to-date`, while the sealer holds the book before at
|
|
379
|
+
an earlier seal than the one it names). Every book with one first line
|
|
380
|
+
is one chain: a book whose first line the sealer holds starts only as
|
|
381
|
+
the next book after the latest with that first line (`not-next-book`). A sealer that serves several
|
|
382
|
+
recorders gives each its own calls, `sealer.forRecorder(name)`: a book,
|
|
383
|
+
and every book after it, belongs to the recorder that started it, and
|
|
384
|
+
another recorder's calls about it are refused (`other-recorder`) or
|
|
385
|
+
answered as if the sealer did not hold it. Recorders that do not trust
|
|
386
|
+
each other are given rules, `sealer.forRecorder(name, { agents, books })`:
|
|
387
|
+
the fingerprints of the key sets of the agents whose permissions each may
|
|
388
|
+
start books with (an agent belongs to one recorder), and the most books
|
|
389
|
+
each may hold. Then every book is started with its first line itself
|
|
390
|
+
(`extend(book, 0, leaves, { line })`, or `update(book, entry, leaves, {
|
|
391
|
+
line })` for a book brought in), which must be a permission for one of
|
|
392
|
+
the recorder's own agents (`agent-not-allowed`, or `other-recorder` for
|
|
393
|
+
another recorder's agent); so no recorder can start a book whose first
|
|
394
|
+
line is a permission for another recorder's agent. `releaseUnsealed()` takes off the
|
|
395
|
+
books that hold no seal, for which nothing was signed. Save the state
|
|
396
|
+
handed back with a seal, with a book started (`extend`) and with
|
|
397
|
+
`update` before the answer is given out, and the one `releaseUnsealed`
|
|
398
|
+
hands back. Of a seal entry brought in with `update`, the sealer keeps
|
|
399
|
+
the seal, whose three signatures it checks, and its time-stamps, read
|
|
400
|
+
for their form only: it checks no time-stamp's signature, so one that
|
|
401
|
+
`latest` gives back is not vouched for by the sealer. A book is
|
|
402
|
+
kept in the state at its latest seal, or with none at its first line:
|
|
403
|
+
started again, the sealer asks for the lines since. It keeps at most
|
|
404
|
+
1,000 books, about 52 KB of state for each sealed book. It checks every
|
|
405
|
+
seal it signs, so it needs Node.js; the Python version has
|
|
406
|
+
`leaf_fingerprint` only.
|
|
407
|
+
|
|
408
|
+
What a central sealer adds, and what still needs a witness. With its
|
|
409
|
+
state kept, it never signs two different histories of one first line.
|
|
410
|
+
With rules, it starts a recorder's books only from permissions for that
|
|
411
|
+
recorder's own agents, and so learns each first line. It sees only
|
|
412
|
+
fingerprints after the first line, so it cannot tell that a book which
|
|
413
|
+
starts from another first line (another permission for the same agent,
|
|
414
|
+
or, with no rules, any record the agent's side can sign) repeats lines
|
|
415
|
+
of a book it has sealed. A sealer that loses its state, or is put back
|
|
416
|
+
to an older copy, can be made to sign a second history, and two states
|
|
417
|
+
kept with one key set are two sealers. A witness is the check for these
|
|
418
|
+
(format description, sections 21.7 and 29).
|
|
419
|
+
|
|
420
|
+
One case a witness does not settle. A recorder, through its own calls
|
|
421
|
+
alone, can start a book from a permission for one of its own agents and
|
|
422
|
+
copy into it the permissions and stubs of another recorder's agent,
|
|
423
|
+
leaving out any it chooses (a stub over a limit, say); the sealer seals
|
|
424
|
+
it. Where one person's passkey signed the permissions for both agents,
|
|
425
|
+
a reader who names that passkey and the sealer's key set finds the book
|
|
426
|
+
intact and within its slips. A witness statement on its seal is not
|
|
427
|
+
counted (the seal covers a permission for an agent other than the first
|
|
428
|
+
line's), so the seal reads as not witnessed, but nothing reports why.
|
|
429
|
+
What shows it: a seal of the other recorder's own book, held elsewhere
|
|
430
|
+
and handed to the checker with this book (`seals`, `--seal`), gives
|
|
431
|
+
`book-cut-short` or `seal-not-in-book`; or, where the two books'
|
|
432
|
+
numbers differ, it cannot be compared, and the checker says so and
|
|
433
|
+
gives no pass. So readers of books from a
|
|
434
|
+
sealer shared by recorders that do not trust each other should hold
|
|
435
|
+
each recorder's own seals elsewhere, or the organisation should run a
|
|
436
|
+
sealer, with its own key set, for each recorder (format description,
|
|
437
|
+
section 21.7).
|
|
438
|
+
|
|
439
|
+
The folder `sealer/` of the repository holds `provared-sealer`, a small
|
|
440
|
+
program around it: it listens on 127.0.0.1, takes questions only from the
|
|
441
|
+
recorders it was given (each proves that it holds its token, which never
|
|
442
|
+
travels), names each recorder with its agents and its number of books,
|
|
443
|
+
keeps one state for all of them, keeps its keys in a file for its owner
|
|
444
|
+
alone or reaches them through signer modules named in its settings, and
|
|
445
|
+
makes sure the state is on the disk before it answers a question that
|
|
446
|
+
changed it. See its README.
|
|
447
|
+
|
|
239
448
|
### Knowing that a block is part of the chain
|
|
240
449
|
|
|
241
450
|
**Experimental.** `provared-headers` is new in version 0.1.0 and may
|
|
@@ -533,7 +742,8 @@ A call that the slip does not allow throws `NotTaken`, with the answer
|
|
|
533
742
|
of the check before acting, and the tool is not run. Where the slip asks
|
|
534
743
|
for the person's own approval, a tool's `approve` function is asked for
|
|
535
744
|
it once. By default the stub of a call names one document,
|
|
536
|
-
`arguments
|
|
745
|
+
`provared.arguments` (the reserved name of section 27 of the format
|
|
746
|
+
description): the fingerprint of the call's arguments, written in one
|
|
537
747
|
form (RFC 8785). So the record fixes exactly what was asked for without
|
|
538
748
|
holding it; `argumentsFingerprint` works the same fingerprint out again.
|
|
539
749
|
The arguments must be JSON: a list with a gap in it, or with a named
|
|
@@ -551,6 +761,132 @@ not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
|
|
|
551
761
|
though the tool has run and its stub is in the book. `recordTools`
|
|
552
762
|
names no agent software, and works with any function.
|
|
553
763
|
|
|
764
|
+
### Denials: the calls the agent's side refused
|
|
765
|
+
|
|
766
|
+
A book shows what the agent did, not how often it was stopped. Opened
|
|
767
|
+
with `denials: true`, the stub writer also writes a **denial** whenever
|
|
768
|
+
`act` does not take an action: the agent's side's own signed statement
|
|
769
|
+
that it did not take it, and why (format description, section 28).
|
|
770
|
+
|
|
771
|
+
```js
|
|
772
|
+
const recorder = await openRecorder({ book, slip, privateKeys, issuerKeys, denials: true });
|
|
773
|
+
|
|
774
|
+
const outcome = await recorder.act({ action: 'provared.data.delete' }, async () => removeTheFiles());
|
|
775
|
+
outcome.done; // false: the slip does not allow it
|
|
776
|
+
outcome.denial.written; // true: the book now holds a denial, by "check", with the reason "action-not-allowed"
|
|
777
|
+
|
|
778
|
+
// A refusal that came from elsewhere: the person said no, or a rule of the software around the agent did.
|
|
779
|
+
await recorder.deny({ action: 'provared.order.place', amount: { unit: 'GBP', value: 900 } }, { source: 'person' });
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
A denial names what was asked for (its action, amount and service) and
|
|
783
|
+
who said no: `check` (the check before acting, with the codes it gave),
|
|
784
|
+
`person` or `rule`. Its documents are fingerprints only; arguments are
|
|
785
|
+
never written into it. `more.denial: false` asks `act` to write none
|
|
786
|
+
for one call. A writer writes at most `denialsPerHour` (600 by default)
|
|
787
|
+
in any hour by its clock. Each denial it writes, or is handed through
|
|
788
|
+
`add`, counts at the clock's time when it joins the book, whatever date
|
|
789
|
+
it states; those of its own chain that the book already held when it
|
|
790
|
+
was opened count at the dates they state. Past that it writes none, and
|
|
791
|
+
each answer's `notWritten` says how many it left out since it was
|
|
792
|
+
opened. A writer opened again on a book counts the book's denials at
|
|
793
|
+
their stated dates, so a caller who dates denials back and then opens
|
|
794
|
+
the writer again can get a second burst (5 and then 5 more within 3,302
|
|
795
|
+
seconds, at a limit of 5). At 600 an hour, an agent that is refused again and again still
|
|
796
|
+
fills a book's 100,000 entries in about a week: the limit slows that,
|
|
797
|
+
it does not stop it. A stub is never kept out by that limit, and
|
|
798
|
+
nothing that goes wrong in writing a denial makes `act` or `deny` fail:
|
|
799
|
+
the answer says what was done. `deny` does throw where the call itself
|
|
800
|
+
is malformed: a request that is not an object, or that cannot be copied
|
|
801
|
+
as plain data, a `when` that is neither
|
|
802
|
+
a number nor a date, `codes` that cannot be copied, or a call from
|
|
803
|
+
inside an action, which is refused as every call from there is. It
|
|
804
|
+
writes nothing for a writer opened without `denials: true`, and takes a
|
|
805
|
+
`when` within 300 seconds of the clock. It records what its caller
|
|
806
|
+
states: it does not check that the action was not taken, so a caller
|
|
807
|
+
can write a denial for a request that also has a stub.
|
|
808
|
+
|
|
809
|
+
`recordTools` passes it on: with a writer that writes denials, a call
|
|
810
|
+
that is not run leaves one, and `NotTaken` holds what was done
|
|
811
|
+
(`denial`). Where a tool can ask the person for approval, no denial is
|
|
812
|
+
written until it is known that the call will not run: so one call
|
|
813
|
+
leaves a stub or a denial, never both. A checker shows each denial as
|
|
814
|
+
the agent's side's own word, counts it (`summary.counts.denials`), and
|
|
815
|
+
never counts it against the agent. An agent can simply not write one,
|
|
816
|
+
so a book without denials shows nothing about refusals.
|
|
817
|
+
|
|
818
|
+
### Binding a stub to the platform's own record
|
|
819
|
+
|
|
820
|
+
Where the other side will not countersign, a stub can still be matched
|
|
821
|
+
with the other system's own log of the call: name, among its documents,
|
|
822
|
+
the fingerprint of an identifier that both records hold. Document names
|
|
823
|
+
that begin `provared.` are reserved for this (format description,
|
|
824
|
+
section 27); any other such name is refused.
|
|
825
|
+
|
|
826
|
+
| Name | The fingerprint of | Made with |
|
|
827
|
+
|---|---|---|
|
|
828
|
+
| `provared.arguments` | The call's arguments, in the one form of RFC 8785 | `argumentsFingerprint` |
|
|
829
|
+
| `provared.http-request` | The request: its method, its address exactly as sent, and its body where it has one; no header | `httpRequestFingerprint` |
|
|
830
|
+
| `provared.http-response` | The answer: its status code, and its body where it has one; no header | `httpResponseFingerprint` |
|
|
831
|
+
| `provared.trace-id` | The trace-id of the W3C Trace Context `traceparent` header the agent's side sent (its form is not checked) | `identifierFingerprint` |
|
|
832
|
+
| `provared.request-id` | An identifier the agent's side chose and sent with the call | `identifierFingerprint` |
|
|
833
|
+
| `provared.event-id` | An identifier the platform gave for the call | `identifierFingerprint` |
|
|
834
|
+
| `provared.error` | The error text the agent's software was given | `identifierFingerprint` |
|
|
835
|
+
|
|
836
|
+
`DOCUMENT_NAMES` lists them, each with the exact text it stands for. An
|
|
837
|
+
HTTP request or answer is written as the signature base of RFC 9421
|
|
838
|
+
(nothing is signed with it): the address is taken exactly as it was
|
|
839
|
+
sent, never rewritten, so give it in the form that was sent. A body is
|
|
840
|
+
the bytes RFC 9530 digests: after any content coding (such as gzip),
|
|
841
|
+
before any transfer coding; a body of `null` is no body. The three
|
|
842
|
+
identifiers may be named more than once in a stub; every other reserved
|
|
843
|
+
name once at most. A trace-id is fingerprinted as given: its form (32
|
|
844
|
+
lower-case hexadecimal characters in the W3C standard) is not checked.
|
|
845
|
+
|
|
846
|
+
An identifier that the platform gives only in its answer cannot be in a
|
|
847
|
+
stub signed before the action. `act` takes `detailsAfter` for it:
|
|
848
|
+
|
|
849
|
+
```js
|
|
850
|
+
import { identifierFingerprint } from 'provared';
|
|
851
|
+
|
|
852
|
+
const outcome = await recorder.act(
|
|
853
|
+
{ action: 'provared.data.change', with: 'tickets' },
|
|
854
|
+
async () => updateTheTicket(), // gives the platform's answer
|
|
855
|
+
{ detailsAfter: async (answer) => [{ name: 'provared.event-id', sha256: await identifierFingerprint(answer.requestId) }] },
|
|
856
|
+
);
|
|
857
|
+
outcome.detailsAfter; // { added: 1, problem: null }
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
The stub is still written and checked with the whole book before the
|
|
861
|
+
action, so it is known to fit. After the action it is signed again with
|
|
862
|
+
up to four further documents, within the same call, so that no other
|
|
863
|
+
call comes between; it keeps the time the action began. If
|
|
864
|
+
`detailsAfter` fails, gives no answer in the time the other side is
|
|
865
|
+
given to countersign (`countersignWithin`), gives more than four
|
|
866
|
+
documents, or gives one that would be refused, the stub is written
|
|
867
|
+
without them, and `outcome.detailsAfter.problem` says why: an action
|
|
868
|
+
that was taken always gets its stub. The request may then name at most
|
|
869
|
+
28 documents of its own, and no approval may be given, because an
|
|
870
|
+
approval covers exactly the documents the person saw. `detailsAfter`
|
|
871
|
+
must not call the writer.
|
|
872
|
+
|
|
873
|
+
A matching identifier shows that the two records speak of one call, not
|
|
874
|
+
that the call did what either says: the platform's identifier is the
|
|
875
|
+
platform's word. It binds the two only where the platform writes the
|
|
876
|
+
same identifier into its own log; which one a platform keeps is for you
|
|
877
|
+
to find out, so record both one you sent and one you were given where
|
|
878
|
+
you can. Only an identifier picks out one call: a fingerprint of a
|
|
879
|
+
request, an answer, the arguments or an error shows only that the
|
|
880
|
+
content matches, since identical requests, and identical answers (many a
|
|
881
|
+
`200` with the body `{}`), share one fingerprint. A platform that logs
|
|
882
|
+
the decoded body of a compressed message will not match the body's
|
|
883
|
+
fingerprint. An identifier that is short or easy to guess can be guessed
|
|
884
|
+
from its fingerprint.
|
|
885
|
+
|
|
886
|
+
`recordTools` names the arguments `provared.arguments` by default (before
|
|
887
|
+
version 0.6.0, `arguments`; every checker reads both); a tool can name
|
|
888
|
+
them otherwise through its own `details` function.
|
|
889
|
+
|
|
554
890
|
## How signing works
|
|
555
891
|
|
|
556
892
|
- **The person** signs a slip with a passkey. A passkey signs a short
|
|
@@ -575,6 +911,107 @@ names no agent software, and works with any function.
|
|
|
575
911
|
signatures were not checked, and does not show a pass. Node.js checks
|
|
576
912
|
all of them.
|
|
577
913
|
|
|
914
|
+
### Keys held elsewhere: signers
|
|
915
|
+
|
|
916
|
+
The keys of an agent, a service, an organisation or a recorder need not
|
|
917
|
+
sit in the memory of the program that writes the records. Wherever a
|
|
918
|
+
writer takes `privateKeys` (`openRecorder`, `writeStub`, `countersign`,
|
|
919
|
+
`writeRefusal`, `writeTerms`, `writePass`, `writeAcknowledgement`,
|
|
920
|
+
`writeVouching`, `writeWithdrawal` and `writeSeal`), each item may be a
|
|
921
|
+
key, as before, or a **signer**: an object with two members.
|
|
922
|
+
|
|
923
|
+
- `key`: the public key, as a JSON Web Key in one of the shapes the
|
|
924
|
+
format allows (format description, section 3.8).
|
|
925
|
+
- `sign(data)`: a promise of the signature over exactly those bytes, as a
|
|
926
|
+
`Uint8Array` or an `ArrayBuffer`.
|
|
927
|
+
|
|
928
|
+
A signer lets a key live in a key service or a hardware module. Keys and
|
|
929
|
+
signers may be mixed: an agent may keep its ML-DSA-87 key in a key
|
|
930
|
+
service and its Ed25519 key elsewhere. `signerFromKey(alg, privateKey,
|
|
931
|
+
key)` wraps a key held in memory as a signer, so that both take one path.
|
|
932
|
+
|
|
933
|
+
```js
|
|
934
|
+
import { openRecorder, signerFromKey } from 'provared';
|
|
935
|
+
|
|
936
|
+
// "keyService.sign" stands for the service's own call. It must sign the
|
|
937
|
+
// bytes themselves, not a fingerprint of them.
|
|
938
|
+
const held = {
|
|
939
|
+
key: agentKeys[1], // the agent's ML-DSA-87 public key, as the slip names it
|
|
940
|
+
sign: (data) => keyService.sign('agent-ml-dsa-87', data),
|
|
941
|
+
};
|
|
942
|
+
const recorder = await openRecorder({
|
|
943
|
+
book, slip, issuerKeys,
|
|
944
|
+
privateKeys: [signerFromKey('Ed25519', ed25519PrivateKey, agentKeys[0]), held],
|
|
945
|
+
signWithin: 10000, // milliseconds for each signature; 30,000 by default
|
|
946
|
+
});
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
A record made through a signer is the same record as one made with the
|
|
950
|
+
key itself: the same content, the same labels and the same Ed25519
|
|
951
|
+
signature. ML-DSA-87 and SLH-DSA signatures are made afresh each time,
|
|
952
|
+
with a key or through a signer, so those differ between any two
|
|
953
|
+
signings, as they always did.
|
|
954
|
+
|
|
955
|
+
A signer is not taken on trust:
|
|
956
|
+
|
|
957
|
+
- Its two members are read once. Its key must be exactly a key of the
|
|
958
|
+
method of its place in the list, as plain data.
|
|
959
|
+
- A signer for a method this device does not have built in is refused
|
|
960
|
+
before any signer is asked to sign, since its signatures could not be
|
|
961
|
+
checked here.
|
|
962
|
+
- Where the writer knows the key the record must be signed with, the
|
|
963
|
+
signer's key must be that key: the agent's keys as the slip names them,
|
|
964
|
+
or a helper's as its pass names them, in the stub writer; the keys
|
|
965
|
+
`by` names in `writeSeal`, `writeRefusal`, `writeTerms` and
|
|
966
|
+
`writeVouching`. A signer for another key is refused (`bad-key`)
|
|
967
|
+
before any signer is asked to sign. This catches the most likely
|
|
968
|
+
fault of a key held elsewhere: a mistyped key name in its settings.
|
|
969
|
+
The stub writer compares when it is opened, with the slip, or the
|
|
970
|
+
pass it acts under, as its book holds it. Opened with a pass that its
|
|
971
|
+
book does not yet hold, it compares nothing then, and refuses each
|
|
972
|
+
action (`pass-missing`), as it always did.
|
|
973
|
+
- `writeStub`, `countersign`, `writePass`, `writeAcknowledgement` and
|
|
974
|
+
`writeWithdrawal`, used by themselves, cannot know which key the
|
|
975
|
+
record must be signed with. There a signer for another key gives a
|
|
976
|
+
record that fails its own check, as a wrong key in memory always did.
|
|
977
|
+
- Every signature it gives is checked against its key before it is
|
|
978
|
+
used. A signature of the wrong length, one that does not check
|
|
979
|
+
(another key used, or other bytes signed, such as a fingerprint of
|
|
980
|
+
them), something other than bytes, or a `sign` that throws: no record
|
|
981
|
+
is made (`signature-invalid`), nothing is written to a book, and the
|
|
982
|
+
error names the place in the list, the method and the key's
|
|
983
|
+
thumbprint.
|
|
984
|
+
- In the stub writer, a signer is given `signWithin` milliseconds for
|
|
985
|
+
each signature (30,000 by default). After that the action is not
|
|
986
|
+
taken, nothing is written, the writer goes on to its next call, and a
|
|
987
|
+
late answer is not used. A signer that calls the writer is refused at
|
|
988
|
+
once, as an action that does so is. The other writers set no time of
|
|
989
|
+
their own: a signer that never answers leaves the call waiting, so give
|
|
990
|
+
the signer's own connection a time limit.
|
|
991
|
+
|
|
992
|
+
Where no record is made, signers earlier in the list may already have
|
|
993
|
+
signed, and a signer that ran out of time may still sign later. So a key
|
|
994
|
+
service's own log may hold signatures that match no record.
|
|
995
|
+
|
|
996
|
+
A key given as before is used exactly as before: a private Web Crypto
|
|
997
|
+
`CryptoKey` of the method of its place and, in a seal's SLH-DSA place, a
|
|
998
|
+
private Node.js `KeyObject`, or what Node.js reads as one (PEM text, DER
|
|
999
|
+
bytes with their format, or a JSON Web Key with its format). Anything
|
|
1000
|
+
else that is not a signer (a public key, a key of another method or
|
|
1001
|
+
kind, a value that only looks like a key) is refused (`bad-field`).
|
|
1002
|
+
|
|
1003
|
+
What a signer does not change:
|
|
1004
|
+
|
|
1005
|
+
- **The library cannot know how a key held elsewhere is kept**, who else
|
|
1006
|
+
can ask for signatures with it, or whether the service keeps it safe.
|
|
1007
|
+
It checks only that each signature fits the key the signer names.
|
|
1008
|
+
- **A record does not say where a key was kept.** A record signed
|
|
1009
|
+
through a key service reads the same as one signed with a key in
|
|
1010
|
+
memory, and a key set split between two places reads as one agent.
|
|
1011
|
+
- **Each signature costs one more check.** Measured on one computer on
|
|
1012
|
+
15 October 2026: about 0.3 thousandths of a second for Ed25519, 0.6
|
|
1013
|
+
for ML-DSA-87 and 2 for SLH-DSA-SHA2-256s.
|
|
1014
|
+
|
|
578
1015
|
## Using the library
|
|
579
1016
|
|
|
580
1017
|
```js
|
|
@@ -626,15 +1063,15 @@ answer with a named reason. It keeps two things apart:
|
|
|
626
1063
|
|---|---|
|
|
627
1064
|
| `size`, `root` | How many entries the book holds, and its top fingerprint |
|
|
628
1065
|
| `problems` | Problems with the book as a whole |
|
|
629
|
-
| `entries` | One for each line: `index`, `kind` (`slip`, `stub`, `refusal`, `terms`, `seal`, `cancellation`, `acknowledgement`, `vouching`, `withdrawal`, `pass` or `unreadable`), `fingerprint`, `verified` (`all`, `some` or `none` of its signatures confirmed here), `problems`, `breaches`, `notes`, the signature states, and `content`. Where a time-stamp you trust on a later seal covers the entry: `existedBy`, the time by which it is shown to have existed. On a seal or a cancellation that passed its check: `stampedAt`, the time its own time-stamps give it |
|
|
630
|
-
| `held` | One for each cancellation handed over beside the book: whether it passed its check and was used, whether the book holds it (`inBook`), the time its time-stamp gives, and the acknowledgements handed over with it (`acknowledgements`: who acknowledged, and when by their own word) |
|
|
1066
|
+
| `entries` | One for each line: `index`, `kind` (`slip`, `stub`, `refusal`, `terms`, `seal`, `cancellation`, `acknowledgement`, `vouching`, `withdrawal`, `pass`, `denial` or `unreadable`), `fingerprint`, `verified` (`all`, `some` or `none` of its signatures confirmed here), `problems`, `breaches`, `notes`, the signature states, and `content`. Where a time-stamp you trust on a later seal covers the entry: `existedBy`, the time by which it is shown to have existed. On a seal or a cancellation that passed its check: `stampedAt`, the time its own time-stamps give it |
|
|
1067
|
+
| `held` | One for each cancellation handed over beside the book (`kind` `cancellation`): whether it passed its check and was used, whether the book holds it (`inBook`), the time its time-stamp gives, and the acknowledgements handed over with it (`acknowledgements`: who acknowledged, and when by their own word). Then one for each seal held elsewhere (`kind` `seal`): whether it passed its check and was used, the entry it stands at (`place`), whether the book holds it there (`inBook`), its recorder's key set (`sealer`), its signatures and time-stamps |
|
|
631
1068
|
| `summary.problemFound` | A problem was found somewhere in the record |
|
|
632
1069
|
| `summary.fullyChecked` | Every signature was checked on this device |
|
|
633
1070
|
| `summary.intact` | No problem was found **and** every signature was checked. The only answer that is a pass |
|
|
634
1071
|
| `summary.methodsMissing` | The signing methods this device lacks, if any, for example `ML-DSA-87` |
|
|
635
1072
|
| `summary.withinSlips` | Every stub was compared with its slip, and none is outside it |
|
|
636
1073
|
| `summary.firstBreach` | The first entry that shows the agent outside its slip, if any |
|
|
637
|
-
| `summary.counts` | Slips, stubs, countersigned stubs (the countersignature confirmed on this device), one-sided stubs (no countersignature; a stub that was not checked, because its slip failed, is neither), stubs the person approved, refusals, sets of terms, and the other kinds of entry |
|
|
1074
|
+
| `summary.counts` | Slips, stubs, countersigned stubs (the countersignature confirmed on this device), one-sided stubs (no countersignature; a stub that was not checked, because its slip failed, is neither), stubs the person approved, refusals, sets of terms, denials (the agent's side's own word, never counted against it), and the other kinds of entry |
|
|
638
1075
|
| `summary.sealed` | How far the last time-stamp you trust reaches: how many entries, the time they existed by, and whether a service or a block (`by`) gave it. Otherwise nothing |
|
|
639
1076
|
| `summary.limits` | What no check can show, in plain sentences |
|
|
640
1077
|
|
|
@@ -654,9 +1091,11 @@ Options: `issuerKeys` (the thumbprints of issuer keys you trust),
|
|
|
654
1091
|
expect), `stampServices` (the fingerprints of the certificates of the
|
|
655
1092
|
time-stamp services you trust), `blocks` (the fingerprints of the
|
|
656
1093
|
blocks of a public blockchain you trust), `vouchers` (the fingerprints of the key
|
|
657
|
-
sets of the organisations whose vouching you trust), `
|
|
1094
|
+
sets of the organisations whose vouching you trust), `witnesses` (the
|
|
1095
|
+
fingerprints of the key sets of the witnesses you trust), `disclosures` (the
|
|
658
1096
|
disclosures of covered fields that you were handed), `cancellations`
|
|
659
|
-
(the person's own copies of cancellations), `
|
|
1097
|
+
(the person's own copies of cancellations), `seals` (seals of the book
|
|
1098
|
+
held elsewhere), `expectedRoot` and
|
|
660
1099
|
`expectedSize` (a top fingerprint and a number of entries you already
|
|
661
1100
|
hold a copy of). Give
|
|
662
1101
|
them whenever you have them.
|
|
@@ -680,7 +1119,8 @@ Writing records: `prepareSlip` and `assembleSlip`, `prepareApproval` and
|
|
|
680
1119
|
`writeRefusal` and `writeTerms` (the service), `writeVouching` and
|
|
681
1120
|
`writeWithdrawal` (an organisation), `writePass` (an agent, to a helper
|
|
682
1121
|
agent), `writeSeal` (whoever keeps
|
|
683
|
-
the book; it needs Node.js), `
|
|
1122
|
+
the book; it needs Node.js), `writeWitness` and `openWitness` (a
|
|
1123
|
+
witness; `openWitness` needs Node.js), `consistencyProof`, `writeBook` and `makeShow`, all from
|
|
684
1124
|
`provared`. The library asks no time-stamp service itself: you send
|
|
685
1125
|
the seal's fingerprint to the service you choose and put its answer
|
|
686
1126
|
beside the seal. In a browser, `createPasskey`, `signSlipWithPasskey`,
|
|
@@ -706,6 +1146,10 @@ prepared, then `assembleSlip`; keep the disclosures it prepared.
|
|
|
706
1146
|
- A name in a record is a label. Only keys are checked. A vouching
|
|
707
1147
|
record shows which organisation stood behind a name, not how well it
|
|
708
1148
|
checked.
|
|
1149
|
+
- An identifier of another system's, named in a stub, shows that the
|
|
1150
|
+
two records speak of one call, not that the call did what either
|
|
1151
|
+
says. A fingerprint of a request, an answer, the arguments or an error
|
|
1152
|
+
shows only that the content matches, not that it was the same call.
|
|
709
1153
|
|
|
710
1154
|
And about time:
|
|
711
1155
|
|
|
@@ -717,7 +1161,10 @@ And about time:
|
|
|
717
1161
|
the agent outside its slip;
|
|
718
1162
|
- the last seal, with everything after it, can itself be removed. Only a
|
|
719
1163
|
copy of it, or of its top fingerprint and number of entries, held by
|
|
720
|
-
someone else shows that
|
|
1164
|
+
someone else shows that: hand such a copy of a seal to the checker
|
|
1165
|
+
(`--seal`, or `seals`), such as the latest seal a witness holds;
|
|
1166
|
+
- a witness statement is the witness's own word for when it saw the
|
|
1167
|
+
seal, and is not used as a time-stamp;
|
|
721
1168
|
- time-stamp services sign with methods of today's kind, which are not
|
|
722
1169
|
quantum-safe.
|
|
723
1170
|
|
|
@@ -730,7 +1177,9 @@ connects to anything: to the Bitcoin nodes you name, for block headers.
|
|
|
730
1177
|
|
|
731
1178
|
## Not built yet
|
|
732
1179
|
|
|
733
|
-
A public place where seals are kept
|
|
1180
|
+
A public place where seals are kept, which gathers the seals of many
|
|
1181
|
+
books; a witness that runs as a service (the library holds the part that
|
|
1182
|
+
decides and signs); covering fields other than a slip's
|
|
734
1183
|
names and purpose; connectors for particular agent software (the
|
|
735
1184
|
general one, `recordTools`, is built); a compact binary form; the
|
|
736
1185
|
recorder service. The checking page cannot yet take a chain of block
|