provared 0.3.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/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.3.0, a draft.** Published as it is, by one author in their
9
+ **Version 0.5.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
 
@@ -149,11 +149,11 @@ in a browser straight from the disk; it needs no server. The file allows
149
149
  itself no request of any kind: its content security policy lets nothing
150
150
  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
- three browser builds this repository is tested with.) The file for
153
- version 0.3.0 is attached to the release on GitHub; it is the same
154
- file, byte for byte, as for version 0.2.0, because nothing the page
155
- holds changed. Its SHA-256 fingerprint is
156
- `7e142fa119458ce00c5fdd6e1cafc75cb3bb4672624157a0a0ef4624f92812ae`.
152
+ three browser builds this repository is tested with, and on 13 October
153
+ 2026 in Chrome 155, Edge 155 and Brave 155.) The file for
154
+ version 0.5.0 is attached to the release on GitHub. Its SHA-256
155
+ fingerprint is
156
+ `cec1b8d5e6e7a11c00478c6e83d9f9b5963340ec656657ea0ac84f32e8e52bda`.
157
157
 
158
158
  **Run the demonstration in a browser.**
159
159
 
@@ -388,7 +388,8 @@ const answer = await checkBefore(
388
388
  { issuerKeys: ['the thumbprint of the passkey you trust'] },
389
389
  );
390
390
 
391
- answer.allowed; // true only if the record so far is sound, fully checked, and the action is inside the slip
391
+ answer.allowed; // true only if the record so far is sound, fully checked (but for a seal's third signature, below), and the action is inside the slip
392
+ answer.notChecked; // present only where the device went on without checking a seal's third signature (SLH-DSA)
392
393
  answer.breaches; // why not, in the checker's own codes
393
394
  answer.problems; // why the question could not be answered with a yes
394
395
  answer.needs; // what the slip asks to go with it: 'approval', 'countersignature'
@@ -550,6 +551,77 @@ not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
550
551
  though the tool has run and its stub is in the book. `recordTools`
551
552
  names no agent software, and works with any function.
552
553
 
554
+ ### Binding a stub to the platform's own record
555
+
556
+ Where the other side will not countersign, a stub can still be matched
557
+ with the other system's own log of the call: name, among its documents,
558
+ the fingerprint of an identifier that both records hold. Document names
559
+ that begin `provared.` are reserved for this (format description,
560
+ section 27); any other such name is refused.
561
+
562
+ | Name | The fingerprint of | Made with |
563
+ |---|---|---|
564
+ | `provared.arguments` | The call's arguments, in the one form of RFC 8785 | `argumentsFingerprint` |
565
+ | `provared.http-request` | The request: its method, its address exactly as sent, and its body where it has one; no header | `httpRequestFingerprint` |
566
+ | `provared.http-response` | The answer: its status code, and its body where it has one; no header | `httpResponseFingerprint` |
567
+ | `provared.trace-id` | The trace-id of the W3C Trace Context `traceparent` header the agent's side sent (its form is not checked) | `identifierFingerprint` |
568
+ | `provared.request-id` | An identifier the agent's side chose and sent with the call | `identifierFingerprint` |
569
+ | `provared.event-id` | An identifier the platform gave for the call | `identifierFingerprint` |
570
+ | `provared.error` | The error text the agent's software was given | `identifierFingerprint` |
571
+
572
+ `DOCUMENT_NAMES` lists them, each with the exact text it stands for. An
573
+ HTTP request or answer is written as the signature base of RFC 9421
574
+ (nothing is signed with it): the address is taken exactly as it was
575
+ sent, never rewritten, so give it in the form that was sent. A body is
576
+ the bytes RFC 9530 digests: after any content coding (such as gzip),
577
+ before any transfer coding; a body of `null` is no body. The three
578
+ identifiers may be named more than once in a stub; every other reserved
579
+ name once at most. A trace-id is fingerprinted as given: its form (32
580
+ lower-case hexadecimal characters in the W3C standard) is not checked.
581
+
582
+ An identifier that the platform gives only in its answer cannot be in a
583
+ stub signed before the action. `act` takes `detailsAfter` for it:
584
+
585
+ ```js
586
+ import { identifierFingerprint } from 'provared';
587
+
588
+ const outcome = await recorder.act(
589
+ { action: 'provared.data.change', with: 'tickets' },
590
+ async () => updateTheTicket(), // gives the platform's answer
591
+ { detailsAfter: async (answer) => [{ name: 'provared.event-id', sha256: await identifierFingerprint(answer.requestId) }] },
592
+ );
593
+ outcome.detailsAfter; // { added: 1, problem: null }
594
+ ```
595
+
596
+ The stub is still written and checked with the whole book before the
597
+ action, so it is known to fit. After the action it is signed again with
598
+ up to four further documents, within the same call, so that no other
599
+ call comes between; it keeps the time the action began. If
600
+ `detailsAfter` fails, gives no answer in the time the other side is
601
+ given to countersign (`countersignWithin`), gives more than four
602
+ documents, or gives one that would be refused, the stub is written
603
+ without them, and `outcome.detailsAfter.problem` says why: an action
604
+ that was taken always gets its stub. The request may then name at most
605
+ 28 documents of its own, and no approval may be given, because an
606
+ approval covers exactly the documents the person saw. `detailsAfter`
607
+ must not call the writer.
608
+
609
+ A matching identifier shows that the two records speak of one call, not
610
+ that the call did what either says: the platform's identifier is the
611
+ platform's word. It binds the two only where the platform writes the
612
+ same identifier into its own log; which one a platform keeps is for you
613
+ to find out, so record both one you sent and one you were given where
614
+ you can. Only an identifier picks out one call: a fingerprint of a
615
+ request, an answer, the arguments or an error shows only that the
616
+ content matches, since identical requests, and identical answers (many a
617
+ `200` with the body `{}`), share one fingerprint. A platform that logs
618
+ the decoded body of a compressed message will not match the body's
619
+ fingerprint. An identifier that is short or easy to guess can be guessed
620
+ from its fingerprint.
621
+
622
+ `recordTools` still names the arguments `arguments` by default; a tool
623
+ can name them `provared.arguments` through its own `details` function.
624
+
553
625
  ## How signing works
554
626
 
555
627
  - **The person** signs a slip with a passkey. A passkey signs a short
@@ -562,9 +634,118 @@ names no agent software, and works with any function.
562
634
  offers a quantum-safe passkey yet.
563
635
  - **Whoever keeps the book** signs each seal with three keys: the same
564
636
  two, and SLH-DSA-SHA2-256s, which rests only on fingerprint functions.
565
- - **Browsers are only now adding ML-DSA, and have no SLH-DSA.** Where a
566
- browser lacks a method, the checking page says that those signatures
567
- were not checked, and does not show a pass. Node.js checks all of them.
637
+ - **The browsers tried have ML-DSA; none has SLH-DSA.** Tried on the
638
+ author's computer on 13 October 2026: Chrome 155, Edge 155 and
639
+ Brave 155 each offer Ed25519 and ML-DSA (all three strengths) to a
640
+ page, and check real signatures made with them. None of the three
641
+ offers SLH-DSA. So in these browsers the checking page checks every
642
+ signature of a slip, a stub, a countersignature and the other records,
643
+ but not a seal's third signature: a book with a seal reads "not
644
+ complete" there, and the time-stamp beside the seal is not counted.
645
+ Where a browser lacks a method, the checking page says that those
646
+ signatures were not checked, and does not show a pass. Node.js checks
647
+ all of them.
648
+
649
+ ### Keys held elsewhere: signers
650
+
651
+ The keys of an agent, a service, an organisation or a recorder need not
652
+ sit in the memory of the program that writes the records. Wherever a
653
+ writer takes `privateKeys` (`openRecorder`, `writeStub`, `countersign`,
654
+ `writeRefusal`, `writeTerms`, `writePass`, `writeAcknowledgement`,
655
+ `writeVouching`, `writeWithdrawal` and `writeSeal`), each item may be a
656
+ key, as before, or a **signer**: an object with two members.
657
+
658
+ - `key`: the public key, as a JSON Web Key in one of the shapes the
659
+ format allows (format description, section 3.8).
660
+ - `sign(data)`: a promise of the signature over exactly those bytes, as a
661
+ `Uint8Array` or an `ArrayBuffer`.
662
+
663
+ A signer lets a key live in a key service or a hardware module. Keys and
664
+ signers may be mixed: an agent may keep its ML-DSA-87 key in a key
665
+ service and its Ed25519 key elsewhere. `signerFromKey(alg, privateKey,
666
+ key)` wraps a key held in memory as a signer, so that both take one path.
667
+
668
+ ```js
669
+ import { openRecorder, signerFromKey } from 'provared';
670
+
671
+ // "keyService.sign" stands for the service's own call. It must sign the
672
+ // bytes themselves, not a fingerprint of them.
673
+ const held = {
674
+ key: agentKeys[1], // the agent's ML-DSA-87 public key, as the slip names it
675
+ sign: (data) => keyService.sign('agent-ml-dsa-87', data),
676
+ };
677
+ const recorder = await openRecorder({
678
+ book, slip, issuerKeys,
679
+ privateKeys: [signerFromKey('Ed25519', ed25519PrivateKey, agentKeys[0]), held],
680
+ signWithin: 10000, // milliseconds for each signature; 30,000 by default
681
+ });
682
+ ```
683
+
684
+ A record made through a signer is the same record as one made with the
685
+ key itself: the same content, the same labels and the same Ed25519
686
+ signature. ML-DSA-87 and SLH-DSA signatures are made afresh each time,
687
+ with a key or through a signer, so those differ between any two
688
+ signings, as they always did.
689
+
690
+ A signer is not taken on trust:
691
+
692
+ - Its two members are read once. Its key must be exactly a key of the
693
+ method of its place in the list, as plain data.
694
+ - A signer for a method this device does not have built in is refused
695
+ before any signer is asked to sign, since its signatures could not be
696
+ checked here.
697
+ - Where the writer knows the key the record must be signed with, the
698
+ signer's key must be that key: the agent's keys as the slip names them,
699
+ or a helper's as its pass names them, in the stub writer; the keys
700
+ `by` names in `writeSeal`, `writeRefusal`, `writeTerms` and
701
+ `writeVouching`. A signer for another key is refused (`bad-key`)
702
+ before any signer is asked to sign. This catches the most likely
703
+ fault of a key held elsewhere: a mistyped key name in its settings.
704
+ The stub writer compares when it is opened, with the slip, or the
705
+ pass it acts under, as its book holds it. Opened with a pass that its
706
+ book does not yet hold, it compares nothing then, and refuses each
707
+ action (`pass-missing`), as it always did.
708
+ - `writeStub`, `countersign`, `writePass`, `writeAcknowledgement` and
709
+ `writeWithdrawal`, used by themselves, cannot know which key the
710
+ record must be signed with. There a signer for another key gives a
711
+ record that fails its own check, as a wrong key in memory always did.
712
+ - Every signature it gives is checked against its key before it is
713
+ used. A signature of the wrong length, one that does not check
714
+ (another key used, or other bytes signed, such as a fingerprint of
715
+ them), something other than bytes, or a `sign` that throws: no record
716
+ is made (`signature-invalid`), nothing is written to a book, and the
717
+ error names the place in the list, the method and the key's
718
+ thumbprint.
719
+ - In the stub writer, a signer is given `signWithin` milliseconds for
720
+ each signature (30,000 by default). After that the action is not
721
+ taken, nothing is written, the writer goes on to its next call, and a
722
+ late answer is not used. A signer that calls the writer is refused at
723
+ once, as an action that does so is. The other writers set no time of
724
+ their own: a signer that never answers leaves the call waiting, so give
725
+ the signer's own connection a time limit.
726
+
727
+ Where no record is made, signers earlier in the list may already have
728
+ signed, and a signer that ran out of time may still sign later. So a key
729
+ service's own log may hold signatures that match no record.
730
+
731
+ A key given as before is used exactly as before: a private Web Crypto
732
+ `CryptoKey` of the method of its place and, in a seal's SLH-DSA place, a
733
+ private Node.js `KeyObject`, or what Node.js reads as one (PEM text, DER
734
+ bytes with their format, or a JSON Web Key with its format). Anything
735
+ else that is not a signer (a public key, a key of another method or
736
+ kind, a value that only looks like a key) is refused (`bad-field`).
737
+
738
+ What a signer does not change:
739
+
740
+ - **The library cannot know how a key held elsewhere is kept**, who else
741
+ can ask for signatures with it, or whether the service keeps it safe.
742
+ It checks only that each signature fits the key the signer names.
743
+ - **A record does not say where a key was kept.** A record signed
744
+ through a key service reads the same as one signed with a key in
745
+ memory, and a key set split between two places reads as one agent.
746
+ - **Each signature costs one more check.** Measured on one computer on
747
+ 15 October 2026: about 0.3 thousandths of a second for Ed25519, 0.6
748
+ for ML-DSA-87 and 2 for SLH-DSA-SHA2-256s.
568
749
 
569
750
  ## Using the library
570
751
 
@@ -697,6 +878,10 @@ prepared, then `assembleSlip`; keep the disclosures it prepared.
697
878
  - A name in a record is a label. Only keys are checked. A vouching
698
879
  record shows which organisation stood behind a name, not how well it
699
880
  checked.
881
+ - An identifier of another system's, named in a stub, shows that the
882
+ two records speak of one call, not that the call did what either
883
+ says. A fingerprint of a request, an answer, the arguments or an error
884
+ shows only that the content matches, not that it was the same call.
700
885
 
701
886
  And about time:
702
887
 
@@ -58,10 +58,10 @@ keys. People decide what it means.
58
58
  under a seal, whose three signatures include two quantum-safe ones.
59
59
  The outside time-stamps themselves are of today's kind.
60
60
  - **A signature that cannot be checked is not a pass.** Where a device
61
- has no built-in ML-DSA, the checker says so, and never reports the
62
- record as intact. An entry none of whose signatures the device could
63
- check is not treated as evidence: what it says is not shown, and it is
64
- not compared with its slip.
61
+ has no built-in ML-DSA, or no SLH-DSA for a seal, the checker says
62
+ so, and never reports the record as intact. An entry none of whose
63
+ signatures the device could check is not treated as evidence: what it
64
+ says is not shown, and it is not compared with its slip.
65
65
  - **A record does not show that an action was wise, lawful or wanted.**
66
66
  - **A badly written slip is faithfully recorded as a badly written slip.**
67
67
 
@@ -123,6 +123,60 @@ fingerprint.
123
123
  - The documents themselves. A stub holds fingerprints of documents, not
124
124
  the documents. Whoever keeps the documents must keep them.
125
125
 
126
+ **A stub bound to the other system's own record of the call.** Where the
127
+ other side will not countersign, a stub may name, under a reserved name
128
+ (format description, section 27), the fingerprint of the identifier the
129
+ platform gave the call (`provared.event-id`), of one the agent's side
130
+ chose and sent (`provared.trace-id`, `provared.request-id`), of part
131
+ of the request and the answer (`provared.http-request`: the method, the
132
+ address and the body; `provared.http-response`: the status and the
133
+ body; no header is covered), or of the error the agent was given
134
+ (`provared.error`).
135
+
136
+ - *What it shows.* Someone who holds the platform's own log can work the
137
+ fingerprint out again and match the two records, without either
138
+ side's help. For an identifier, the two speak of one call. An agent's
139
+ side that later claims another identifier for the call, or a platform
140
+ that later shows another, is caught by the mismatch.
141
+ - *Only an identifier picks out one call.* A request, an answer, the
142
+ arguments or an error shows only that the content matches: identical
143
+ requests, and identical answers (many a `200` with the body `{}`),
144
+ share one fingerprint, so a match with one of them does not show that
145
+ it was the same call. Only `provared.trace-id`, `provared.request-id`
146
+ and `provared.event-id` are meant to pick out one call, and only they
147
+ may be named more than once in a stub.
148
+ - *The body's bytes* are what RFC 9530 digests: the content after any
149
+ content coding (such as gzip) and before any transfer coding. A
150
+ platform that logs the decoded body of a message sent compressed will
151
+ not match.
152
+ - *A trace-id's form is not checked.* The W3C standard writes it as 32
153
+ lower-case hexadecimal characters, but any text is fingerprinted as
154
+ given.
155
+ - *What it does not show.* That the call did what either record says:
156
+ the platform's identifier is the platform's word, and the stub is
157
+ still the agent's side's word. That the platform keeps the identifier
158
+ at all: which one a platform writes into its log differs from one
159
+ platform to another, and is for whoever relies on it to find out. That
160
+ the fingerprint was made from the text the name says: a checker cannot
161
+ tell; only someone who holds that text can. An identifier that is
162
+ short or easy to guess gives no privacy through its fingerprint.
163
+ - *Against A1 and A3* (the agent's own side). The binding is written by
164
+ the agent's side, so a dishonest one can leave it out, or name a
165
+ wrong identifier; only the match with the platform's log, made later,
166
+ catches the second. It adds nothing against an agent that writes no
167
+ stub.
168
+ - *Two spellings of one address.* An HTTP address is taken exactly as it
169
+ was sent and never rewritten, so that two programs fingerprint the
170
+ same request the same way. A program that records a rewritten address
171
+ (`%2f` for `%2F`, a host in capitals) makes a fingerprint that will not
172
+ match: whoever writes the stub must give the address as sent.
173
+ - *A reserved name not on the list* is refused (`bad-field`), so that a
174
+ name that may be given a meaning later cannot be used now with another;
175
+ so is one of the four once-only names named twice (two `provared.arguments`
176
+ fingerprints in one stub would leave unclear which was the call's). A
177
+ book written before this rule that holds such a name no longer passes
178
+ its check.
179
+
126
180
  ## 4a. Limits, conditions, approvals and the other side's records
127
181
 
128
182
  **Protects.** That what the person allowed is stated exactly, and that a
@@ -235,6 +289,25 @@ disagree about. Totals are added as whole numbers of any size.
235
289
  of the arguments fixes what was asked for without holding it; where
236
290
  the arguments are few and easy to guess, the fingerprint can be
237
291
  guessed.
292
+ - **Documents named after the action** (`detailsAfter` in the stub
293
+ writer's `act`). The stub is first written and checked with the whole
294
+ book before the action, as always; after the action, it is signed
295
+ again with up to four further documents, still within the same call,
296
+ so that no other call comes between, and keeps the time the action
297
+ began. The room for them is kept beforehand: with today's limits the
298
+ largest stub, with 32 documents of the longest names, and a
299
+ countersignature still fits a line, and the writer checks this again
300
+ for each such call. If what `detailsAfter` gives cannot go in (it
301
+ fails, gives no answer within the time the other side is given to
302
+ countersign, gives more than four, or gives a document that would be
303
+ refused), the stub made before the action is written without them,
304
+ and the answer says why: an action that was taken always gets its
305
+ stub. What it gives is copied when it is read, so a later change to
306
+ it changes nothing. A call to the writer from inside it is refused at
307
+ once. It cannot be used with an approval, because an approval covers
308
+ exactly the documents the person saw. The stub is signed twice for
309
+ such an action, and only one of the two is written: a signer kept
310
+ elsewhere is asked once more.
238
311
  - **The check before acting refuses more in one case.** An action that
239
312
  would add to a period which a stub dated after it already overfills is
240
313
  refused, though a checker would report nothing new for it.
@@ -450,7 +523,12 @@ within 5 minutes for a time-stamp from a service, and to within 2 hours
450
523
  and 5 minutes for a block time-stamp). A6 cannot make a
451
524
  seal: it needs the recorder's three keys. A8 must break Ed25519, ML-DSA-87
452
525
  and SLH-DSA together to forge a seal; the third rests only on fingerprint
453
- functions. Nobody can move a time-stamp to another seal: it is made over
526
+ functions. One exception, stated: the check before acting on a device
527
+ without SLH-DSA (the Python version, every browser) goes on after a seal
528
+ whose first two signatures check there, so on that device a seal rests on
529
+ Ed25519 and ML-DSA-87 alone, and a third signature of the right length
530
+ that would not check is not seen. A device with SLH-DSA, such as the
531
+ command-line checker, sees it. Nobody can move a time-stamp to another seal: it is made over
454
532
  the seal's fingerprint.
455
533
 
456
534
  A block time-stamp is a second witness of the time, which rests on no
@@ -490,8 +568,12 @@ person checking gives.
490
568
  does not read what a certificate says its key is for. It checks that
491
569
  the stated time lies within the certificate's dates, and leaves the
492
570
  rest to the person who chose to trust it.
493
- - **A browser.** No browser offers SLH-DSA or ML-DSA to a page yet. A
494
- checker there reports a seal as not fully checked, never as a pass.
571
+ - **A browser.** Tried on the author's computer on 13 October 2026:
572
+ Chrome 155, Edge 155 and Brave 155 offer ML-DSA to a page; none of the
573
+ three offers SLH-DSA. So a checker in a browser
574
+ reports a seal as not fully checked, never as a pass, and counts no
575
+ time-stamp beside it. In a browser without ML-DSA, every record signed
576
+ with ML-DSA is reported as not fully checked too.
495
577
  - **A few bytes of a time-stamp's wrapper** are not covered by the
496
578
  service's signature (the list of fingerprint methods, and the lines
497
579
  that name the certificate by its issuer). Changing them changes nothing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "provared",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Proof of what an AI agent was allowed to do, and what it then did. A passkey-signed permission with limits a machine can check, a signed receipt for each action, and a free checker. Evidence, not a verdict.",
5
5
  "author": "Pavel Izmaylov",
6
6
  "license": "Apache-2.0 AND CC-BY-4.0",
package/page/index.html CHANGED
@@ -8,7 +8,7 @@
8
8
  </head>
9
9
  <body>
10
10
  <main>
11
- <h1>Check a record</h1>
11
+ <h1><svg class="mark" viewBox="0 0 64 64" width="32" height="32" aria-hidden="true"><circle cx="32" cy="32" r="32" fill="#8f000f"/><path d="M20 15 H37 L45 23 V49 H20 Z" fill="#ffffff"/><path d="M37 15 V23 H45" fill="none" stroke="#8f000f" stroke-width="2.5" stroke-linejoin="round"/><path d="M25 29 H40 M25 35 H36" stroke="#8f000f" stroke-width="2.5" stroke-linecap="round"/><path d="M24.5 43 C27 39, 29 46, 31.5 42.5 C34 39, 36 46, 40.5 41.5" fill="none" stroke="#8f000f" stroke-width="3" stroke-linecap="round"/></svg>Check a record</h1>
12
12
  <p class="motto">Provared, draft version 0. Evidence, not a verdict.</p>
13
13
  <p>Choose a record file. It is checked here, in this browser. Nothing is sent anywhere.</p>
14
14
  <p>
package/page/style.css CHANGED
@@ -148,3 +148,6 @@ details.more summary { cursor: pointer; font-weight: 600; }
148
148
  details.more label { display: block; margin-top: 0.7rem; }
149
149
  details.more input[type="file"] { display: block; max-width: 100%; margin-top: 0.3rem; color: var(--text); }
150
150
  details.more textarea { width: 100%; box-sizing: border-box; font-family: ui-monospace, monospace; font-size: 0.85rem; color: var(--text); background: var(--page); border: 1px solid var(--line); border-radius: 0.3rem; }
151
+
152
+ /* The mark: a permission slip with a signature, beside a heading or the site's name. */
153
+ .mark { vertical-align: -0.2em; margin-right: 0.4em; }
@@ -226,10 +226,22 @@ Because the slip names both keys, taking the quantum-safe signature off a
226
226
  stub does not leave a valid stub.
227
227
 
228
228
  A checker on a device with no built-in ML-DSA MUST report that the
229
- quantum-safe signature could not be checked, and MUST NOT report the
230
- record as intact. An entry none of whose signatures the device could
231
- check MUST NOT be treated as evidence: what it says is not shown as fact,
232
- and it is not compared with its slip.
229
+ quantum-safe signature could not be checked (unless it is of the wrong
230
+ length, below), and MUST NOT report the record as intact. An entry none
231
+ of whose signatures the device could check MUST NOT be treated as
232
+ evidence: what it says is not shown as fact, and it is not compared with
233
+ its slip.
234
+
235
+ **A signature of the wrong length.** A signature whose length is not its
236
+ method's (64 bytes for Ed25519, 4,627 for ML-DSA-87, 29,792 for
237
+ SLH-DSA-SHA2-256s) can never check. A checker MUST report it as invalid,
238
+ whether or not the device has the method: it is a problem with the
239
+ record, not a signature left unchecked. This holds for every signature of
240
+ a record, the passkey's Ed25519 signature on a slip among them, and so
241
+ for the check of a whole book or of a Show as for the check before acting
242
+ (section 20). It does not reach the signature inside a time-stamp from a
243
+ service (section 21.2): a device without that time-stamp's method does
244
+ not check it, whatever its length, and does not count it.
233
245
 
234
246
  A recorder is known by a key set of exactly three keys: Ed25519,
235
247
  ML-DSA-87, then SLH-DSA-SHA2-256s (FIPS 205, the plain method with an
@@ -238,8 +250,8 @@ order, and counts as signed only if all three check. The third rests only
238
250
  on fingerprint functions, so it would stand even if the mathematics
239
251
  behind ML-DSA were broken. When this draft was written, no browser
240
252
  offered it to a page, and Node.js offered it outside the standard
241
- interface: a checker in a browser reports it as not checked, which is
242
- never a pass.
253
+ interface: a checker in a browser reports it as not checked (or, if it is
254
+ of the wrong length, as invalid), which is never a pass.
243
255
 
244
256
  The issuer's passkey signature on a slip is of today's kind only. No
245
257
  device offers a quantum-safe passkey.
@@ -428,7 +440,10 @@ checker keeps no state.
428
440
  | `pass` | For a helper agent: the pass it acts under. Optional | The fingerprint of the pass (section 25) |
429
441
  | `when` | When it was done | A time |
430
442
 
431
- A stub holds fingerprints of documents, never the documents.
443
+ A stub holds fingerprints of documents, never the documents. A document
444
+ `name` is a label chosen by whoever wrote the stub, except that names
445
+ that begin `provared.` are reserved (section 27). The same label MAY
446
+ appear more than once; section 27 says which reserved names may.
432
447
 
433
448
  ### 5.2 Signing and checking
434
449
 
@@ -646,6 +661,11 @@ the seal's own date (section 21.1, rule 4).
646
661
  that the organisation checked well.
647
662
  - The fingerprints of an agent's software in a slip are a statement of
648
663
  what the person approved, not proof of what ran.
664
+ - A reserved identifier that binds a stub to another system's own
665
+ record of the call (section 27) shows that the two records speak of
666
+ one call, not that the call did what either says. A matching
667
+ fingerprint of a request, an answer, arguments or an error shows only
668
+ that the content matches, not that it was the same call.
649
669
 
650
670
  ## 14. Standards used
651
671
 
@@ -667,6 +687,8 @@ the seal's own date (section 21.1, rule 4).
667
687
  | RFC 3161; RFC 5652; RFC 5280; ITU-T X.690 | Time-stamps: the statement, its signed wrapper, the certificate, and the byte layout (DER) |
668
688
  | The OpenTimestamps proof file, version 1 (an open format; not the work of a standards body) | Block time-stamps: the steps from a fingerprint to the Merkle root of a block |
669
689
  | RFC 9901 (Selective Disclosure for JSON Web Tokens) | Covered fields |
690
+ | RFC 9421 (HTTP Message Signatures), section 2.5, only its way of writing a message as text; RFC 9530 (Digest Fields); RFC 9110, section 5.6.2 | The texts that the reserved document names `provared.http-request` and `provared.http-response` stand for (section 27). Nothing is signed with RFC 9421 |
691
+ | W3C Trace Context (a W3C Recommendation) | The trace-id that `provared.trace-id` stands for (section 27) |
670
692
 
671
693
  ## 15. Choices that go beyond the standards
672
694
 
@@ -723,6 +745,12 @@ is frozen.
723
745
  pass.
724
746
  18. The acknowledgement of section 26 and its label
725
747
  `vnd.provared.acknowledgement.v0+json`.
748
+ 19. The reserved document names of section 27, and the exact texts their
749
+ fingerprints are made from: an HTTP address taken exactly as it was
750
+ sent, never rewritten; a body that is present but empty given a
751
+ Content-Digest, and an absent body none; no header covered; SHA-256
752
+ only; only the three identifiers may be named more than once; the
753
+ form of a trace-id not checked.
726
754
 
727
755
  ## 16. The shared list: actions, kinds of action, rules of conduct
728
756
 
@@ -918,7 +946,18 @@ holds too much. A checker reports nothing new for it, because the
918
946
  finding stands with the later stub. The answer MUST be that nothing is allowed
919
947
  (`record-not-sound`) if the book so far has a problem, if any signature
920
948
  in it could not be checked on that device, or if the check was not told
921
- which issuer keys it trusts. An action dated before the last stub of
949
+ which issuer keys it trusts. One exception is allowed: where every
950
+ signature that could not be checked is the third signature of a seal
951
+ (section 21.1) whose first two signatures were checked on that device,
952
+ and nothing else was left unchecked, the check MAY go on, and its answer
953
+ MUST then name the method it did not check. A seal widens no permission;
954
+ a device without that method would otherwise be refused everything once
955
+ the book holds a seal. On such a device a seal rests on its first two
956
+ signatures only: a third signature of the right length that would not
957
+ check is not seen there. A third signature of the wrong length is not
958
+ left unchecked: as section 3.9 sets out, it is invalid on every device,
959
+ so the seal is a problem there and nothing is allowed. An action dated
960
+ before the last stub of
922
961
  its own chain (the agent's, or the helper's under its pass; section 7)
923
962
  is refused (`time-went-backwards`).
924
963
 
@@ -1695,6 +1734,112 @@ section 22. Nothing obliges an agent's side to acknowledge: a
1695
1734
  cancellation with no acknowledgement shows nothing about whether the
1696
1735
  agent's side was told.
1697
1736
 
1737
+ ## 27. Reserved document names
1738
+
1739
+ Where the other side of an action will not countersign, the next best
1740
+ binding is the other system's own identifier for the call, named in the
1741
+ stub by its fingerprint. An investigator who holds that system's own log
1742
+ can then work out the fingerprint again and match the two records,
1743
+ without either side's help. Only an identifier is meant to pick out one
1744
+ call. A stub may also name fingerprints of some of what was sent and
1745
+ received (the arguments; an HTTP request's method, address and body; an
1746
+ answer's status and body); those show that the content matches, not
1747
+ that it was the same call.
1748
+
1749
+ So document names (section 5.1) that begin `provared.` are reserved. A
1750
+ document name that begins `provared.` and is not in this list MUST be
1751
+ refused (`bad-field`), in a stub, an approval (section 17) and the
1752
+ check before acting (section 20). The comparison is exact: a name in
1753
+ other letters, such as `Provared.invoice`, is a label like any other.
1754
+ `provared.trace-id`, `provared.request-id` and `provared.event-id` MAY
1755
+ each appear more than once in one stub (identifiers from two systems,
1756
+ say); every other name in the list MUST appear at most once, and a
1757
+ record that names one of them twice MUST be refused (`bad-field`), in
1758
+ the same places. The `sha256` of each is the SHA-256, in base64url, of
1759
+ exactly this text, in UTF-8:
1760
+
1761
+ | Name | The text whose SHA-256 is given |
1762
+ |---|---|
1763
+ | `provared.arguments` | The call's arguments, written in the one form of RFC 8785 |
1764
+ | `provared.http-request` | The request, written as below, over `"@method"`, `"@target-uri"` and, where there is a body, `"content-digest"` |
1765
+ | `provared.http-response` | The answer, written as below, over `"@status"` and, where there is a body, `"content-digest"` |
1766
+ | `provared.trace-id` | The trace-id of the W3C Trace Context `traceparent` header that the agent's side sent, exactly as sent. That standard writes it as 32 lower-case hexadecimal characters; this format does not check the form, and a checker cannot |
1767
+ | `provared.request-id` | An identifier that the agent's side chose and sent with the call, exactly as sent |
1768
+ | `provared.event-id` | An identifier that the platform gave for the call, exactly as it gave it |
1769
+ | `provared.error` | The error text that the agent's software was given, exactly as given |
1770
+
1771
+ An identifier or an error text MUST be one character or more, and
1772
+ well-formed Unicode.
1773
+
1774
+ **How a request or an answer is written.** As the signature base of RFC
1775
+ 9421 (HTTP Message Signatures), section 2.5, with no signature
1776
+ parameters. RFC 9421 is used only for this exact way of writing a
1777
+ message as text; nothing is signed with it. For each component, in the
1778
+ order given: a line `"<component>": <value>` and a line feed (0x0A);
1779
+ then the line `"@signature-params": (` with the components, each in
1780
+ double quotation marks and separated by one space, and `)`, with no line
1781
+ feed after it. The values:
1782
+
1783
+ - `"@method"`: the method exactly as sent, a token of RFC 9110, section
1784
+ 5.6.2. Its letters are not changed, as RFC 9421, section 2.2.1, sets
1785
+ out.
1786
+ - `"@target-uri"`: the absolute address exactly as sent. It MUST begin
1787
+ `http://` or `https://`, hold only the visible characters of ASCII
1788
+ (0x21 to 0x7E), and hold no fragment (`#`). It is never rewritten:
1789
+ two spellings of one address (`%2F` and `%2f`, a host in capitals, a
1790
+ port written or left out) give two fingerprints. Whoever writes the
1791
+ stub MUST give the address in the form that was sent.
1792
+ - `"@status"`: the status code of the answer, a whole number from 100 to
1793
+ 599, written in digits.
1794
+ - `"content-digest"`: present only where the message has a body; a body
1795
+ that is present and empty has one. The value is the field value of RFC
1796
+ 9530 with SHA-256 only: `sha-256=:`, the SHA-256 of the body's bytes,
1797
+ in base64 with padding (RFC 4648, section 4), and `:`. The body's
1798
+ bytes are what RFC 9530 digests: the content after any content coding
1799
+ (such as gzip) and before any transfer coding. A system that keeps
1800
+ the decoded body, where the body was sent compressed, will not match.
1801
+
1802
+ No header is covered, and nothing else of the message: two messages
1803
+ that differ only in their headers give one fingerprint.
1804
+
1805
+ For example, the request `POST https://example.com/foo?param=Value&Pet=dog`
1806
+ with the body `{"hello": "world"}` is written as these four lines:
1807
+
1808
+ ```
1809
+ "@method": POST
1810
+ "@target-uri": https://example.com/foo?param=Value&Pet=dog
1811
+ "content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
1812
+ "@signature-params": ("@method" "@target-uri" "content-digest")
1813
+ ```
1814
+
1815
+ **What a binding shows, and what it does not.**
1816
+
1817
+ - An identifier binds the two records only where the other system writes
1818
+ the same identifier into its own log. Which one a system keeps differs
1819
+ from one system to another, and is for whoever relies on it to find
1820
+ out. Recording both kinds, one the agent's side chose and sent
1821
+ (`provared.trace-id`, `provared.request-id`) and one the platform gave
1822
+ back (`provared.event-id`), gives the better chance of a match.
1823
+ - The platform's identifier is the platform's word. A matching
1824
+ identifier shows that the two records speak of one call, not that the
1825
+ call did what either says.
1826
+ - Only `provared.trace-id`, `provared.request-id` and `provared.event-id`
1827
+ are meant to pick out one call. `provared.arguments`,
1828
+ `provared.http-request`, `provared.http-response` and `provared.error`
1829
+ show only that the content matches: identical requests, and identical
1830
+ answers (many a `200` with the body `{}`), share one fingerprint, so a
1831
+ match with one of them does not show that it was the same call.
1832
+ - A fingerprint of an identifier, or of arguments, that is short or easy
1833
+ to guess can be guessed: it hides nothing from someone who can try
1834
+ the likely values.
1835
+ - A checker cannot see whether a fingerprint was made from the text this
1836
+ section names: only someone who holds that text can work it out again.
1837
+ - Records written before this section was added may use names that
1838
+ begin `provared.` that are not in the list, or name one of the four
1839
+ once-only names twice; such records are refused, so a book that holds
1840
+ one no longer passes its check. A checker that does not know this
1841
+ section reads a reserved name as a label.
1842
+
1698
1843
  ---
1699
1844
 
1700
1845
  Copyright 2026 Pavel Izmaylov. This document is licensed under the
package/src/dev.d.ts CHANGED
@@ -50,5 +50,5 @@ export function developmentSlip(fields: DevelopmentSlipFields): Promise<Developm
50
50
  /** The stub writer opened under a development slip, in one call. */
51
51
  export function developmentRecorder(
52
52
  fields: DevelopmentSlipFields,
53
- more?: { options?: CheckOptions; now?: () => number; countersignWithin?: number },
53
+ more?: { options?: CheckOptions; now?: () => number; countersignWithin?: number; signWithin?: number },
54
54
  ): Promise<Omit<DevelopmentSlip, 'book'> & { recorder: Recorder }>;
package/src/dev.js CHANGED
@@ -110,7 +110,7 @@ export async function developmentSlip(fields = {}) {
110
110
  * The stub writer, opened under a development slip, in one call: the
111
111
  * shortest way to a first record. The same as developmentSlip followed by
112
112
  * openRecorder; "more" is passed to openRecorder (options, now,
113
- * countersignWithin).
113
+ * countersignWithin, signWithin).
114
114
  *
115
115
  * @param {object} fields the members of the slip; "actions" is required
116
116
  * @param {object} [more]