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/CHANGELOG.md +340 -0
- package/README.md +26 -8
- package/bin/provared-check.mjs +3 -1
- package/docs/guide.md +195 -10
- package/docs/threat-model.md +89 -7
- package/package.json +1 -1
- package/page/index.html +1 -1
- package/page/style.css +3 -0
- package/spec/provared-format.md +153 -8
- package/src/dev.d.ts +1 -1
- package/src/dev.js +1 -1
- package/src/documents.js +149 -0
- package/src/guard.js +74 -6
- package/src/index.js +8 -3
- package/src/pass.js +1 -1
- package/src/recorder.js +149 -13
- package/src/seal.js +4 -3
- package/src/service.js +4 -4
- package/src/signatures.js +283 -1
- package/src/standing.js +4 -4
- package/src/stub.js +18 -5
- package/src/types.d.ts +60 -12
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.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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`
|
|
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
|
-
- **
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
package/docs/threat-model.md
CHANGED
|
@@ -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,
|
|
62
|
-
record as intact. An entry none of whose
|
|
63
|
-
check is not treated as evidence: what it
|
|
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.
|
|
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.**
|
|
494
|
-
|
|
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
|
+
"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; }
|
package/spec/provared-format.md
CHANGED
|
@@ -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
|
|
230
|
-
record as intact. An entry none
|
|
231
|
-
|
|
232
|
-
and it is not compared with
|
|
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,
|
|
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.
|
|
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]
|