provared 0.6.0 → 0.7.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 +514 -0
- package/README.md +13 -1
- package/bin/provared-check.mjs +20 -9
- package/docs/guide.md +60 -11
- package/docs/threat-model.md +71 -26
- package/net/headers.js +1 -0
- package/package.json +1 -1
- package/page/check-page.js +8 -1
- package/page/index.html +2 -0
- package/page/render.js +27 -2
- package/spec/provared-format.md +86 -16
- package/src/book.js +6 -2
- package/src/check.d.ts +1 -0
- package/src/check.js +1 -1
- package/src/denial.js +1 -1
- package/src/der.js +1 -0
- package/src/documents.js +8 -3
- package/src/signatures.js +6 -1
- package/src/slip.js +72 -5
- package/src/types.d.ts +15 -2
package/bin/provared-check.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// provared-check: check a book or a Show from the command line.
|
|
3
3
|
//
|
|
4
|
-
// provared-check <file> [--issuer <thumbprint>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
|
|
4
|
+
// provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]... [--stamp-service <fingerprint>]...
|
|
5
5
|
// [--block <fingerprint>]... [--voucher <fingerprint>]... [--disclosures <file>] [--cancellation <file>]...
|
|
6
6
|
// [--seal <file>]... [--witness <fingerprint>]... [--root <top fingerprint>] [--size <entries>] [--json]
|
|
7
7
|
//
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { readFileSync } from 'node:fs';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import { DENIAL_SOURCES, MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, limitWords, neverWords } from '../src/check.js';
|
|
23
|
+
import { DENIAL_SOURCES, MIN_BLOCKS_AFTER, REFUSAL_REASONS, blocksInChain, checkBook, checkHeaderChain, checkShow, conditionWords, isPageAddress, limitWords, neverWords } from '../src/check.js';
|
|
24
24
|
|
|
25
25
|
// Node.js marks its ML-DSA and SLH-DSA as experimental and says so on every
|
|
26
26
|
// run. The checker does not repeat that warning; any other warning is shown.
|
|
@@ -29,16 +29,19 @@ process.on('warning', (w) => {
|
|
|
29
29
|
if (w.name !== 'ExperimentalWarning') console.error(`${w.name}: ${w.message}`);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--
|
|
33
|
-
[--
|
|
34
|
-
[--
|
|
35
|
-
[--headers <file>] [--json]
|
|
32
|
+
const USAGE = `Usage: provared-check <file> [--issuer <thumbprint>]... [--origin <address>]... [--sealer <fingerprint>]...
|
|
33
|
+
[--stamp-service <fingerprint>]... [--block <fingerprint>]... [--voucher <fingerprint>]...
|
|
34
|
+
[--disclosures <file>] [--cancellation <file>]... [--seal <file>]... [--witness <fingerprint>]...
|
|
35
|
+
[--root <top fingerprint>] [--size <entries>] [--headers <file>] [--json]
|
|
36
36
|
provared-check --sample [--json]
|
|
37
37
|
|
|
38
38
|
<file> a book (one entry to a line) or a Show (one JSON object)
|
|
39
39
|
--sample check the sample record that comes with the library, naming the passkey, the recorder and the
|
|
40
40
|
time-stamp service it was made with as trusted (they are stand-ins, and the people are invented)
|
|
41
41
|
--issuer the thumbprint of an issuer key you already trust; may be repeated
|
|
42
|
+
--origin the address of the page at which the person signs permissions, such as https://sign.example.org
|
|
43
|
+
(https://, a website name in lower case, a port only if it is needed, and nothing more); a
|
|
44
|
+
permission signed at any other address is refused; may be repeated
|
|
42
45
|
--sealer the fingerprint of the key set of a recorder you expect to have sealed the book; may be repeated
|
|
43
46
|
--stamp-service the fingerprint of the certificate of a time-stamp service you trust; may be repeated
|
|
44
47
|
--block the fingerprint (64 hex characters) of a block of a public blockchain that you trust to be part of the chain;
|
|
@@ -68,14 +71,14 @@ const FINGERPRINT = /^[A-Za-z0-9_-]{43}$/;
|
|
|
68
71
|
const BLOCK = /^[0-9a-f]{64}$/;
|
|
69
72
|
|
|
70
73
|
// Options that may be repeated, and those that may be given once.
|
|
71
|
-
const LISTS = { '--issuer': 'issuerKeys', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--witness': 'witnesses', '--cancellation': 'cancellations', '--seal': 'seals' };
|
|
74
|
+
const LISTS = { '--issuer': 'issuerKeys', '--origin': 'issuerOrigins', '--sealer': 'sealKeys', '--stamp-service': 'stampServices', '--block': 'blocks', '--voucher': 'vouchers', '--witness': 'witnesses', '--cancellation': 'cancellations', '--seal': 'seals' };
|
|
72
75
|
const ONCE = { '--disclosures': 'disclosures', '--root': 'expectedRoot', '--size': 'expectedSize', '--headers': 'headers' };
|
|
73
76
|
|
|
74
77
|
// The arguments, or the reason they cannot be used. A value that is not a
|
|
75
78
|
// fingerprint is refused, never set aside: a typing error must not quietly
|
|
76
79
|
// leave a time-stamp or a name uncounted.
|
|
77
80
|
function parseArguments(argv) {
|
|
78
|
-
const out = { file: null, sample: false, issuerKeys: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], witnesses: [], cancellations: [], seals: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
|
|
81
|
+
const out = { file: null, sample: false, issuerKeys: [], issuerOrigins: [], sealKeys: [], stampServices: [], blocks: [], vouchers: [], witnesses: [], cancellations: [], seals: [], disclosures: undefined, expectedRoot: undefined, expectedSize: undefined, json: false };
|
|
79
82
|
for (let i = 0; i < argv.length; i++) {
|
|
80
83
|
const a = argv[i];
|
|
81
84
|
if (a === '--help' || a === '-h') return { help: true };
|
|
@@ -93,6 +96,9 @@ function parseArguments(argv) {
|
|
|
93
96
|
return { error: `The value of ${a} is not a fingerprint. A fingerprint is 43 letters, digits, hyphens or underscores, with no spaces.` };
|
|
94
97
|
}
|
|
95
98
|
if (a === '--block' && !BLOCK.test(value)) return { error: 'The value of --block is not the fingerprint of a block: 64 characters, the digits 0 to 9 and the letters a to f.' };
|
|
99
|
+
if (a === '--origin' && !isPageAddress(value)) {
|
|
100
|
+
return { error: 'The value of --origin is not a page address in its plain form: https://, a website name in lower case, a port only if it is needed, and nothing more, such as https://sign.example.org.' };
|
|
101
|
+
}
|
|
96
102
|
if (a === '--size' && !/^\d{1,6}$/.test(value)) return { error: 'The value of --size is not a number of entries.' };
|
|
97
103
|
if (Object.hasOwn(LISTS, a)) out[LISTS[a]].push(value);
|
|
98
104
|
else out[ONCE[a]] = a === '--size' ? Number(value) : value;
|
|
@@ -101,7 +107,7 @@ function parseArguments(argv) {
|
|
|
101
107
|
}
|
|
102
108
|
if (out.sample) {
|
|
103
109
|
if (out.file !== null) return { error: '--sample names the sample record itself: give no file with it.' };
|
|
104
|
-
for (const name of ['issuerKeys', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'cancellations', 'seals']) {
|
|
110
|
+
for (const name of ['issuerKeys', 'issuerOrigins', 'sealKeys', 'stampServices', 'blocks', 'vouchers', 'witnesses', 'cancellations', 'seals']) {
|
|
105
111
|
if (out[name].length) return { error: '--sample names what to trust itself: give no other option with it but --json.' };
|
|
106
112
|
}
|
|
107
113
|
if (out.disclosures !== undefined || out.expectedRoot !== undefined || out.expectedSize !== undefined || out.headers !== undefined) {
|
|
@@ -503,6 +509,10 @@ function print(result, isShow) {
|
|
|
503
509
|
console.log(` Time-stamped in a way you trust? ${stamped}`);
|
|
504
510
|
if (s.counts.seals || args.witnesses.length) console.log(` Witnessed by a witness you trust? ${witnessedWords(s)}${isShow && s.witnessed ? ' (single pages: this says nothing about totals over the whole book)' : ''}`);
|
|
505
511
|
console.log(` Checked against keys you named? ${trustWords(s)}`);
|
|
512
|
+
// Shown only where the person named an address: a permission signed
|
|
513
|
+
// elsewhere is a problem, reported above, so here every one was signed
|
|
514
|
+
// at an address they named.
|
|
515
|
+
if (args.issuerOrigins.length) console.log(` Signed at an address you named? yes: every permission was signed at ${[...new Set(args.issuerOrigins)].join(' or ')}`);
|
|
506
516
|
if (args.seals.length) console.log(` Seals held elsewhere compared? ${heldWords(result)}`);
|
|
507
517
|
}
|
|
508
518
|
console.log(` Slips ${s.counts.slips}, stubs ${s.counts.stubs} (countersigned ${s.counts.countersigned}, one-sided ${s.counts.oneSided})${s.counts.seals ? `, seals ${s.counts.seals}` : ''}`);
|
|
@@ -608,6 +618,7 @@ try {
|
|
|
608
618
|
|
|
609
619
|
const options = {};
|
|
610
620
|
if (args.issuerKeys.length) options.issuerKeys = args.issuerKeys;
|
|
621
|
+
if (args.issuerOrigins.length) options.issuerOrigins = args.issuerOrigins;
|
|
611
622
|
if (args.sealKeys.length) options.sealKeys = args.sealKeys;
|
|
612
623
|
if (args.stampServices.length) options.stampServices = args.stampServices;
|
|
613
624
|
if (args.blocks.length) options.blocks = args.blocks;
|
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.7.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
|
|
|
@@ -33,14 +33,16 @@ An Internet-Draft is a working document, not a standard.
|
|
|
33
33
|
| `src/` | The library: the checker, the writers for every kind of record, the stub writer beside an agent and the connector for an agent's tools. No outside code |
|
|
34
34
|
| `bin/provared-check.mjs` | The checker for the command line |
|
|
35
35
|
| `bin/provared-headers.mjs`, `net/` | A command that fetches the chain of Bitcoin block headers from nodes you name, so that a block time-stamp can be checked with no node of your own. The only code here that makes a network request. Experimental |
|
|
36
|
-
| `page/` | The checking page and the demonstration page |
|
|
36
|
+
| `page/` | The checking page, the signing page and the demonstration page |
|
|
37
37
|
| `demo/` | The demonstration: an invented office agent works under a permission and goes outside it four times |
|
|
38
38
|
| `samples/` | A sample record, written once, that the tests keep checking |
|
|
39
|
-
| `tools/` |
|
|
39
|
+
| `tools/` | Small tools: one wrote the sample record, one writes the checking page as a single file, one writes the shared test files, one writes the record page at prova.red/records/, and one writes the files of the signing page's own site, sign.prova.red |
|
|
40
40
|
| `test/` | The tests |
|
|
41
41
|
| `test-vectors/` | Shared test files: records and values, each with the answer this library gives. Another implementation of the format can be tested against them |
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
42
|
+
| `integrations/claude-code/` | The recorder beside Claude Code, `npm install -g provared-claude-code`, also a Claude Code plugin. See its own [README](https://github.com/provared/provared/blob/main/integrations/claude-code/README.md) |
|
|
43
|
+
| `mcp-executor/` | An executor for MCP tool servers, `npm install -g provared-mcp-executor`: it holds the tool servers' credentials, checks each tool call against the permission, and countersigns each receipt with keys of its own. See its own [README](https://github.com/provared/provared/blob/main/mcp-executor/README.md) |
|
|
44
|
+
| `python/` | The Python version, `pip install provared`, which gives the same answers. See its own [README](https://github.com/provared/provared/blob/main/python/README.md), and its examples with three agent frameworks in `python/examples/`, and small packages for two of them in `python/integrations/` |
|
|
45
|
+
| `site/` | The pages at [prova.red](https://prova.red/): what a record proves, with the checker's answer for the sample record, and a worked example. Published with GitHub Pages by `.github/workflows/pages.yml`, which also serves the checking page at `prova.red/check/` and as one file. The signing page is served from a repository of its own at `sign.prova.red`, so that the passkeys made there belong to a name that serves nothing else. The pages load nothing from anywhere and keep nothing about a reader |
|
|
44
46
|
|
|
45
47
|
The npm package holds `src/`, `bin/`, `net/`, `spec/`, `docs/`,
|
|
46
48
|
`samples/` and the checking page. The tests, the shared test files, the
|
|
@@ -105,7 +107,7 @@ const passkey = await developmentPasskey();
|
|
|
105
107
|
fills in the issuer (its key, page address and marked name) and the
|
|
106
108
|
agent's keys; by default the slip runs from a minute ago for one day,
|
|
107
109
|
holds no limits and names no other party, and its purpose says that it
|
|
108
|
-
is for development. [examples/first-record.mjs](
|
|
110
|
+
is for development. [examples/first-record.mjs](https://github.com/provared/provared/blob/main/examples/first-record.mjs)
|
|
109
111
|
is a whole first record in one file. In Python the same is
|
|
110
112
|
`provared.dev` (`development_passkey`, `development_slip`,
|
|
111
113
|
`development_recorder`), with the writer as `.writer`.
|
|
@@ -151,9 +153,9 @@ be loaded from anywhere. The tool prints the file's SHA-256 fingerprint,
|
|
|
151
153
|
so that a copy can be compared with it. (Tried on 8 October 2026 in the
|
|
152
154
|
three browser builds this repository is tested with, and on 13 October
|
|
153
155
|
2026 in Chrome 155, Edge 155 and Brave 155.) The file for
|
|
154
|
-
version 0.
|
|
156
|
+
version 0.7.0 is attached to the release on GitHub. Its SHA-256
|
|
155
157
|
fingerprint is
|
|
156
|
-
`
|
|
158
|
+
`c7b3d5fe1b9168628fc91308c7f952b970c05b3abeb908a132b8b2b78aaab1b5`.
|
|
157
159
|
|
|
158
160
|
**Run the demonstration in a browser.**
|
|
159
161
|
|
|
@@ -761,6 +763,16 @@ not a promise. The numbers `0` and `-0` share a fingerprint, as RFC
|
|
|
761
763
|
though the tool has run and its stub is in the book. `recordTools`
|
|
762
764
|
names no agent software, and works with any function.
|
|
763
765
|
|
|
766
|
+
[examples/frameworks/](https://github.com/provared/provared/tree/main/examples/frameworks) in the repository shows
|
|
767
|
+
`recordTools` beside a JavaScript agent framework, the Vercel AI SDK
|
|
768
|
+
(`ai`, version 7). The SDK's own tool calls the recorded tool from its
|
|
769
|
+
`execute` function, and the SDK hands the words of `NotTaken` back to the
|
|
770
|
+
model as a failed tool call, so the agent is told why a call was not run.
|
|
771
|
+
It runs with a scripted stand-in model and sends nothing anywhere; it has
|
|
772
|
+
its own `package.json`, with exact versions, and is not part of the npm
|
|
773
|
+
package. The Python examples are in
|
|
774
|
+
[python/examples/](https://github.com/provared/provared/tree/main/python/examples) in the repository.
|
|
775
|
+
|
|
764
776
|
### Denials: the calls the agent's side refused
|
|
765
777
|
|
|
766
778
|
A book shows what the agent did, not how often it was stopped. Opened
|
|
@@ -832,6 +844,9 @@ section 27); any other such name is refused.
|
|
|
832
844
|
| `provared.request-id` | An identifier the agent's side chose and sent with the call | `identifierFingerprint` |
|
|
833
845
|
| `provared.event-id` | An identifier the platform gave for the call | `identifierFingerprint` |
|
|
834
846
|
| `provared.error` | The error text the agent's software was given | `identifierFingerprint` |
|
|
847
|
+
| `provared.tool-name` | The name of the tool, exactly as the agent's software called it (not the slip's action name) | `identifierFingerprint` |
|
|
848
|
+
| `provared.tool-call-id` | The identifier the model gave the tool call, exactly as received | `identifierFingerprint` |
|
|
849
|
+
| `provared.tool-result` | The tool's result, in the one form of RFC 8785; never the result itself | `argumentsFingerprint` (the same one form) |
|
|
835
850
|
|
|
836
851
|
`DOCUMENT_NAMES` lists them, each with the exact text it stands for. An
|
|
837
852
|
HTTP request or answer is written as the signature base of RFC 9421
|
|
@@ -885,13 +900,42 @@ from its fingerprint.
|
|
|
885
900
|
|
|
886
901
|
`recordTools` names the arguments `provared.arguments` by default (before
|
|
887
902
|
version 0.6.0, `arguments`; every checker reads both); a tool can name
|
|
888
|
-
them otherwise through its own `details` function.
|
|
903
|
+
them otherwise through its own `details` function. It does not name the
|
|
904
|
+
tool's name or its result: a tool's `details` function can add
|
|
905
|
+
`provared.tool-name`, and `provared.tool-result` is added after the
|
|
906
|
+
action, through `detailsAfter` of `act`. The MCP executor
|
|
907
|
+
(`mcp-executor/`) names all three where it has them. Each of the three
|
|
908
|
+
may be named once in a stub; a checker of version 0.5.0 or 0.6.0 refuses
|
|
909
|
+
a stub that names any of them, and one of 0.4.0 or earlier reads them as
|
|
910
|
+
labels.
|
|
911
|
+
|
|
912
|
+
To show each receipt on the trace span of its call in an observability
|
|
913
|
+
tool, use the OpenTelemetry bridge in `integrations/opentelemetry/` (not
|
|
914
|
+
yet published); a span is a pointer to the receipt, not evidence. It puts
|
|
915
|
+
`provared.trace-id` into a receipt only for a tool that you say sends the
|
|
916
|
+
trace out with its call, in a `traceparent` header, as this section
|
|
917
|
+
defines the name; it cannot see whether the tool really does. Where a
|
|
918
|
+
tool gives the model's identifier for the call (`callId`), the receipt
|
|
919
|
+
names it as `provared.tool-call-id`.
|
|
889
920
|
|
|
890
921
|
## How signing works
|
|
891
922
|
|
|
892
923
|
- **The person** signs a slip with a passkey. A passkey signs a short
|
|
893
924
|
value handed to it; that value is the SHA-256 fingerprint of the slip's
|
|
894
925
|
exact signed bytes, so the signature covers the whole slip.
|
|
926
|
+
- **The signing page is served at an address of its own**,
|
|
927
|
+
[sign.prova.red](https://sign.prova.red/), which serves nothing else
|
|
928
|
+
(its files are copied from `page/` by `tools/make-sign-site.mjs` into
|
|
929
|
+
the public repository `provared/sign`). A passkey can be asked to sign
|
|
930
|
+
by any page under the website name it was made for, and what it signs
|
|
931
|
+
is a fingerprint the person cannot read on the device's prompt, so a
|
|
932
|
+
passkey made for a name that serves other pages could be made to sign
|
|
933
|
+
a permission by one of them. A passkey made at sign.prova.red can be
|
|
934
|
+
asked only there. Whoever checks a record can name the address the
|
|
935
|
+
person signs at (`issuerOrigins`, or `--origin`, or the box on the
|
|
936
|
+
checking page): a permission signed under the same passkey anywhere
|
|
937
|
+
else is then refused (`issuer-origin-not-expected`). The list of
|
|
938
|
+
threats, section 3, says what this closes and what it does not.
|
|
895
939
|
- **The agent and the service** each sign with two keys at once: Ed25519,
|
|
896
940
|
which is today's kind, and ML-DSA-87, which is quantum-safe. A record
|
|
897
941
|
counts only if both signatures check. A forger must break both.
|
|
@@ -1087,6 +1131,10 @@ wrong. Never show it as fact: the checking page and the command-line
|
|
|
1087
1131
|
checker do not.
|
|
1088
1132
|
|
|
1089
1133
|
Options: `issuerKeys` (the thumbprints of issuer keys you trust),
|
|
1134
|
+
`issuerOrigins` (the addresses of the pages at which the person signs
|
|
1135
|
+
permissions, such as `https://sign.example.org`, each in the one form
|
|
1136
|
+
the format allows; a permission signed anywhere else is refused with
|
|
1137
|
+
`issuer-origin-not-expected`; see "How signing works"),
|
|
1090
1138
|
`sealKeys` (the fingerprints of the key sets of the recorders you
|
|
1091
1139
|
expect), `stampServices` (the fingerprints of the certificates of the
|
|
1092
1140
|
time-stamp services you trust), `blocks` (the fingerprints of the
|
|
@@ -1110,8 +1158,9 @@ where the record has a seal, the recorder you expect), 1 (no problem
|
|
|
1110
1158
|
found, but the agent is outside its slip, not every signature could be
|
|
1111
1159
|
checked, or you did not name whom you trust) or 2 (a problem was found,
|
|
1112
1160
|
or what you gave on the command line could not be used). A value that
|
|
1113
|
-
is not a fingerprint
|
|
1114
|
-
|
|
1161
|
+
is not a fingerprint, or an address given with `--origin` that is not a
|
|
1162
|
+
page address in its plain form, is refused, never set aside. The
|
|
1163
|
+
checking page gives its green answer on the same terms.
|
|
1115
1164
|
|
|
1116
1165
|
Writing records: `prepareSlip` and `assembleSlip`, `prepareApproval` and
|
|
1117
1166
|
`assembleApproval` (the person), `writeStub` (the agent), `countersign`,
|
package/docs/threat-model.md
CHANGED
|
@@ -91,20 +91,47 @@ follows the checking steps of the Web Authentication standard.
|
|
|
91
91
|
section 6b. Slips should still be short-lived.
|
|
92
92
|
- Who the issuer is. The slip proves which key signed, not whose key it
|
|
93
93
|
is.
|
|
94
|
-
- Against a page that has the passkey sign a record dressed as a sign-in
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
the
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
94
|
+
- Against a page that has the passkey sign a record dressed as a sign-in,
|
|
95
|
+
where the passkey's website name (its `rpId`) serves anything but the
|
|
96
|
+
signing page. A passkey signs only a challenge, and the challenge for a
|
|
97
|
+
slip, an approval or a cancellation is the fingerprint of the record,
|
|
98
|
+
which the person cannot read on the device's prompt. Any page at an
|
|
99
|
+
origin under the slip's `rpId` (the website's own operator, a script
|
|
100
|
+
running on one of its pages, or the operator of any subdomain) can hand
|
|
101
|
+
the device the fingerprint of a record it wrote as an ordinary sign-in
|
|
102
|
+
challenge: the client data of a sign-in and of a record have the same
|
|
103
|
+
shape, and the person sees a sign-in prompt. No device and no page need
|
|
104
|
+
be broken into. The checker compares the origin and the rpId with what
|
|
105
|
+
the slip itself says; and, where the reader names the address they
|
|
106
|
+
expect the person to sign at (`issuerOrigins`, `--origin`, or the box
|
|
107
|
+
on the checking page; format description, section 4.4), with that
|
|
108
|
+
address too, refusing a slip signed anywhere else
|
|
109
|
+
(`issuer-origin-not-expected`). What each measure does:
|
|
110
|
+
- A passkey made for a name that serves only the signing page closes
|
|
111
|
+
this for that passkey: the device lets it sign only for pages under
|
|
112
|
+
that name, so no other page can have it sign anything. Provared's own
|
|
113
|
+
signing page is served at sign.prova.red, a name that serves nothing
|
|
114
|
+
else.
|
|
115
|
+
- A reader who names the address is protected by naming alone against
|
|
116
|
+
every other address under the `rpId`, whatever name the passkey was
|
|
117
|
+
made for: the client data the passkey signs must say that address, so
|
|
118
|
+
a record signed at any other address under the `rpId` is refused. It
|
|
119
|
+
does not protect against another page served at the named address
|
|
120
|
+
itself.
|
|
121
|
+
- A page that shows the record before it asks for the signature helps
|
|
122
|
+
only against a page that asks openly for a record to be signed. It
|
|
123
|
+
does nothing against a prompt dressed as a sign-in, which shows no
|
|
124
|
+
record.
|
|
125
|
+
- Naming an address protects only the reader who names it. It does not
|
|
126
|
+
stop the passkey signing records at other pages, which readers who
|
|
127
|
+
name no address, or name another one, accept.
|
|
128
|
+
|
|
129
|
+
What stays open: a reader who names no address is protected only by
|
|
130
|
+
which passkey they trust; whoever serves the named address (the host of
|
|
131
|
+
the site, or whoever holds its domain name) can serve another page
|
|
132
|
+
there, and a reader's naming does not tell that page from the real one;
|
|
133
|
+
and a passkey made for a name that serves other pages stays exposed to
|
|
134
|
+
every reader who does not name the address.
|
|
108
135
|
|
|
109
136
|
## 4. The Stub and the countersignature
|
|
110
137
|
|
|
@@ -144,8 +171,11 @@ platform gave the call (`provared.event-id`), of one the agent's side
|
|
|
144
171
|
chose and sent (`provared.trace-id`, `provared.request-id`), of part
|
|
145
172
|
of the request and the answer (`provared.http-request`: the method, the
|
|
146
173
|
address and the body; `provared.http-response`: the status and the
|
|
147
|
-
body; no header is covered),
|
|
148
|
-
(`provared.error`)
|
|
174
|
+
body; no header is covered), of the error the agent was given
|
|
175
|
+
(`provared.error`), of the name of the tool that was called
|
|
176
|
+
(`provared.tool-name`), of the identifier the model gave the tool call
|
|
177
|
+
(`provared.tool-call-id`) or of the tool's result (`provared.tool-result`,
|
|
178
|
+
in the one form of RFC 8785, as the arguments are).
|
|
149
179
|
|
|
150
180
|
- *What it shows.* Someone who holds the platform's own log can work the
|
|
151
181
|
fingerprint out again and match the two records, without either
|
|
@@ -158,7 +188,11 @@ body; no header is covered), or of the error the agent was given
|
|
|
158
188
|
share one fingerprint, so a match with one of them does not show that
|
|
159
189
|
it was the same call. Only `provared.trace-id`, `provared.request-id`
|
|
160
190
|
and `provared.event-id` are meant to pick out one call, and only they
|
|
161
|
-
may be named more than once in a stub.
|
|
191
|
+
may be named more than once in a stub. `provared.tool-call-id` picks
|
|
192
|
+
out the model's request for the call, which the agent's software may
|
|
193
|
+
carry out more than once (a retry): two stubs then name one identifier.
|
|
194
|
+
A tool's name and a tool's result are content: every call of one tool
|
|
195
|
+
shares its name's fingerprint, and identical results share one.
|
|
162
196
|
- *The body's bytes* are what RFC 9530 digests: the content after any
|
|
163
197
|
content coding (such as gzip) and before any transfer coding. A
|
|
164
198
|
platform that logs the decoded body of a message sent compressed will
|
|
@@ -172,8 +206,13 @@ body; no header is covered), or of the error the agent was given
|
|
|
172
206
|
at all: which one a platform writes into its log differs from one
|
|
173
207
|
platform to another, and is for whoever relies on it to find out. That
|
|
174
208
|
the fingerprint was made from the text the name says: a checker cannot
|
|
175
|
-
tell; only someone who holds that text can. An identifier
|
|
176
|
-
short or easy to guess gives no privacy
|
|
209
|
+
tell; only someone who holds that text can. An identifier, a tool's
|
|
210
|
+
name or a result that is short or easy to guess gives no privacy
|
|
211
|
+
through its fingerprint: a tool's name is found by trying the
|
|
212
|
+
framework's or the server's tool names, and a short result (`"ok"`,
|
|
213
|
+
`{}`) by trying the likely values. The record holds fingerprints and
|
|
214
|
+
not the result itself so that the record stays small and fixed, not to
|
|
215
|
+
hide the result.
|
|
177
216
|
- *Against A1 and A3* (the agent's own side). The binding is written by
|
|
178
217
|
the agent's side, so a dishonest one can leave it out, or name a
|
|
179
218
|
wrong identifier; only the match with the platform's log, made later,
|
|
@@ -186,10 +225,14 @@ body; no header is covered), or of the error the agent was given
|
|
|
186
225
|
match: whoever writes the stub must give the address as sent.
|
|
187
226
|
- *A reserved name not on the list* is refused (`bad-field`), so that a
|
|
188
227
|
name that may be given a meaning later cannot be used now with another;
|
|
189
|
-
so is one of the
|
|
228
|
+
so is one of the seven once-only names named twice (two `provared.arguments`
|
|
190
229
|
fingerprints in one stub would leave unclear which was the call's). A
|
|
191
230
|
book written before this rule that holds such a name no longer passes
|
|
192
|
-
its check.
|
|
231
|
+
its check. The three names added after version 0.6.0 (`provared.tool-name`,
|
|
232
|
+
`provared.tool-call-id`, `provared.tool-result`) are refused by a
|
|
233
|
+
checker of version 0.5.0 or 0.6.0, which knows the first seven only,
|
|
234
|
+
and read as labels by one of 0.4.0 or earlier: a book that names them
|
|
235
|
+
checks in full only with a later checker.
|
|
193
236
|
|
|
194
237
|
## 4a. Limits, conditions, approvals and the other side's records
|
|
195
238
|
|
|
@@ -1161,11 +1204,12 @@ it show a pass for a record that does not check.
|
|
|
1161
1204
|
It asks for every header from the first block onwards, so a node does
|
|
1162
1205
|
not learn which blocks matter to the person.
|
|
1163
1206
|
- What the person hands the checking page beside a record (their own
|
|
1164
|
-
copies of cancellations, disclosures for covered fields, the blocks
|
|
1165
|
-
|
|
1166
|
-
A file that is not JSON, or a line that
|
|
1167
|
-
|
|
1168
|
-
|
|
1207
|
+
copies of cancellations, disclosures for covered fields, the blocks,
|
|
1208
|
+
the fingerprints and the signing page's address they trust) is read in
|
|
1209
|
+
the browser and sent nowhere. A file that is not JSON, or a line that
|
|
1210
|
+
is not a fingerprint or a page address in its plain form, stops the
|
|
1211
|
+
check and says so: it is never set aside silently. The sample record
|
|
1212
|
+
is checked without the files handed over.
|
|
1169
1213
|
- The checker depends on no outside code. It uses only the cryptography
|
|
1170
1214
|
built into the browser or Node.js.
|
|
1171
1215
|
- **The checking page as one file** (`tools/make-single-page.mjs`) holds
|
|
@@ -1241,6 +1285,7 @@ fails if the two differ.
|
|
|
1241
1285
|
| `passkey-user-not-verified` | A passkey signature where the device did not confirm the person |
|
|
1242
1286
|
| `passkey-bad-data` | Passkey values that are cut short or malformed |
|
|
1243
1287
|
| `issuer-not-expected` | A slip signed by a key other than the ones the checker was told to expect |
|
|
1288
|
+
| `issuer-origin-not-expected` | A slip signed at a page address other than the ones the checker was told to expect (section 3) |
|
|
1244
1289
|
| `slip-missing` | A stub whose slip is not earlier in the book |
|
|
1245
1290
|
| `slip-unusable` | A stub whose slip failed its own check |
|
|
1246
1291
|
| `chain-broken` | A stub that is missing, moved, repeated or changed |
|
package/net/headers.js
CHANGED
|
@@ -232,6 +232,7 @@ export function apart(a, b) {
|
|
|
232
232
|
*/
|
|
233
233
|
export function nodeAddress(text) {
|
|
234
234
|
const line = String(text).split('#')[0].trim();
|
|
235
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- no repetition inside repetition can match the same text two ways (labels split by a full stop the labels cannot match), so matching takes time in proportion to the input.
|
|
235
236
|
const m = line.match(/^\[([0-9a-fA-F:.]+)\](?::(\d{1,5}))?$/) ?? line.match(/^([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*)\.?(?::(\d{1,5}))?$/);
|
|
236
237
|
if (!m) return null;
|
|
237
238
|
const host = m[1].toLowerCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "provared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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/check-page.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// request at all.
|
|
7
7
|
|
|
8
8
|
import { checkBook, checkShow } from '../src/check.js';
|
|
9
|
-
import { readBlocks, readFingerprints, readHanded, renderResult, shown } from './render.js';
|
|
9
|
+
import { readBlocks, readFingerprints, readHanded, readOrigins, renderResult, shown } from './render.js';
|
|
10
10
|
|
|
11
11
|
const status = document.getElementById('status');
|
|
12
12
|
const result = document.getElementById('result');
|
|
@@ -37,6 +37,13 @@ function trusted() {
|
|
|
37
37
|
throw new Error('A line in the box for blocks is not the fingerprint of a block. It is 64 characters: the digits 0 to 9 and the letters a to f. Correct it, or empty the box.');
|
|
38
38
|
}
|
|
39
39
|
if (blocks.values.length) options.blocks = blocks.values;
|
|
40
|
+
// The addresses at which the person expects the permissions to have been
|
|
41
|
+
// signed: each in the one form the format allows, or the check stops.
|
|
42
|
+
const origins = readOrigins(document.getElementById('trust-origins').value);
|
|
43
|
+
if (origins.bad.length) {
|
|
44
|
+
throw new Error('A line in the box for the address of the signing page is not a page address in its plain form: https://, a website name in lower case, a port only if it is needed, and nothing more, such as https://sign.example.org. Correct it, or empty the box.');
|
|
45
|
+
}
|
|
46
|
+
if (origins.values.length) options.issuerOrigins = origins.values;
|
|
40
47
|
return options;
|
|
41
48
|
}
|
|
42
49
|
|
package/page/index.html
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
<p class="quiet">A check says more when you tell it which keys and services you expect. Put one fingerprint on each line. Use fingerprints you obtained yourself, from the people concerned.</p>
|
|
21
21
|
<label for="trust-issuer">The person's passkey (its thumbprint)</label>
|
|
22
22
|
<textarea id="trust-issuer" rows="2" spellcheck="false"></textarea>
|
|
23
|
+
<label for="trust-origins">The address of the page at which the person signs permissions (such as https://sign.example.org; a permission signed at any other address is refused)</label>
|
|
24
|
+
<textarea id="trust-origins" rows="2" spellcheck="false"></textarea>
|
|
23
25
|
<label for="trust-sealer">Whoever keeps and seals the book (the fingerprint of its key set)</label>
|
|
24
26
|
<textarea id="trust-sealer" rows="2" spellcheck="false"></textarea>
|
|
25
27
|
<label for="trust-stamps">Time-stamp services (the fingerprint of each service's certificate)</label>
|
package/page/render.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// is kept inside its own box, so that a record cannot hide, reorder or
|
|
8
8
|
// draw over the words around it.
|
|
9
9
|
|
|
10
|
-
import { DENIAL_SOURCES, REFUSAL_REASONS, conditionWords, limitWords, neverWords } from '../src/check.js';
|
|
10
|
+
import { DENIAL_SOURCES, REFUSAL_REASONS, conditionWords, isPageAddress, limitWords, neverWords } from '../src/check.js';
|
|
11
11
|
|
|
12
12
|
// The same holds for characters that cannot be seen (zero-width characters,
|
|
13
13
|
// soft hyphens, line separators, fillers), with which two different labels
|
|
@@ -54,6 +54,20 @@ export function readBlocks(text) {
|
|
|
54
54
|
return { values: lines.filter((line) => /^[0-9a-fA-F]{64}$/.test(line)), bad: lines.filter((line) => !/^[0-9a-fA-F]{64}$/.test(line)) };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Read the box of page addresses, one to a line: the addresses at which the
|
|
59
|
+
* person expects the permissions to have been signed, each in the one form
|
|
60
|
+
* the format allows (https://, a website name in lower case, a port only if
|
|
61
|
+
* it is needed, and nothing more). As with the other boxes, a line that is
|
|
62
|
+
* not one is never dropped silently.
|
|
63
|
+
* @param {string} text
|
|
64
|
+
* @returns {{values: string[], bad: string[]}}
|
|
65
|
+
*/
|
|
66
|
+
export function readOrigins(text) {
|
|
67
|
+
const lines = String(text).split(/\s+/).filter((line) => line !== '');
|
|
68
|
+
return { values: lines.filter((line) => isPageAddress(line)), bad: lines.filter((line) => !isPageAddress(line)) };
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
/**
|
|
58
72
|
* @param {unknown} text
|
|
59
73
|
* @returns {string} the text, safe to place among other words
|
|
@@ -82,6 +96,17 @@ export function el(tag, className, text) {
|
|
|
82
96
|
|
|
83
97
|
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
84
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The heading of the card of the top fingerprint. It names the Seal only
|
|
101
|
+
* where the record holds one: a book, or the pages of a Show, with no seal
|
|
102
|
+
* has a top fingerprint and no seal.
|
|
103
|
+
* @param {{summary?: {counts?: {seals?: number}}}} result the checker's answer
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function topFingerprintHeading(result) {
|
|
107
|
+
return (result?.summary?.counts?.seals ?? 0) > 0 ? 'The top fingerprint (the Seal)' : 'The top fingerprint';
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
/**
|
|
86
111
|
* A time from a record, in words. The checker has already confirmed its
|
|
87
112
|
* form; anything else is shown as it is.
|
|
@@ -625,7 +650,7 @@ export function renderResult(target, result, { isShow = false, checkedBy = 'Chec
|
|
|
625
650
|
|
|
626
651
|
if (result.root) {
|
|
627
652
|
const seal = el('div', 'card seal');
|
|
628
|
-
seal.append(el('h3', 'kind',
|
|
653
|
+
seal.append(el('h3', 'kind', topFingerprintHeading(result)));
|
|
629
654
|
const list = el('dl');
|
|
630
655
|
row(list, 'Top fingerprint', result.root, true);
|
|
631
656
|
row(list, 'Entries in the book', String(result.size));
|