provared 0.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +1062 -0
  2. package/LICENSE +202 -0
  3. package/LICENSE-CC-BY-4.0.txt +396 -0
  4. package/README.md +689 -0
  5. package/bin/provared-check.mjs +565 -0
  6. package/bin/provared-headers.mjs +110 -0
  7. package/docs/threat-model.md +997 -0
  8. package/net/headers.js +403 -0
  9. package/package.json +57 -0
  10. package/page/check-page.js +162 -0
  11. package/page/index.html +47 -0
  12. package/page/render.js +530 -0
  13. package/page/style.css +150 -0
  14. package/samples/office-supplies.expected.json +82 -0
  15. package/samples/office-supplies.jsonl +10 -0
  16. package/samples/office-supplies.show.json +59 -0
  17. package/spec/provared-format.md +1672 -0
  18. package/src/actions.js +90 -0
  19. package/src/approval.js +73 -0
  20. package/src/blockstamp.js +198 -0
  21. package/src/book.js +2232 -0
  22. package/src/check.d.ts +21 -0
  23. package/src/check.js +15 -0
  24. package/src/compare.js +276 -0
  25. package/src/cover.js +198 -0
  26. package/src/der.js +192 -0
  27. package/src/encoding.js +286 -0
  28. package/src/fields.js +94 -0
  29. package/src/guard.js +171 -0
  30. package/src/headers.js +380 -0
  31. package/src/index.d.ts +2 -0
  32. package/src/index.js +37 -0
  33. package/src/jws.js +235 -0
  34. package/src/keys.js +149 -0
  35. package/src/pass.js +67 -0
  36. package/src/passkey-browser.d.ts +20 -0
  37. package/src/passkey-browser.js +171 -0
  38. package/src/recorder.js +1115 -0
  39. package/src/seal.js +133 -0
  40. package/src/service.js +124 -0
  41. package/src/signatures.js +147 -0
  42. package/src/slip.js +401 -0
  43. package/src/standing.js +181 -0
  44. package/src/stub.js +140 -0
  45. package/src/timestamp.js +351 -0
  46. package/src/tools.js +192 -0
  47. package/src/tree.js +143 -0
  48. package/src/types.d.ts +628 -0
  49. package/src/webauthn.js +119 -0
package/src/actions.js ADDED
@@ -0,0 +1,90 @@
1
+ // The shared list of action names, the kinds of action, and the rules of
2
+ // conduct a slip can state. Format description, section 16.
3
+ //
4
+ // This is a first draft, written before real agents have used it. Anyone may
5
+ // go on using action names of their own; names that begin "provared." are
6
+ // reserved for this list.
7
+
8
+ /** Every action name that begins with this is reserved for the shared list. */
9
+ export const RESERVED_PREFIX = 'provared.';
10
+
11
+ /** The kinds of action. Each shared action belongs to exactly one. */
12
+ export const ACTION_KINDS = {
13
+ reads: 'Looks at data without changing it.',
14
+ changes: 'Creates or changes data in a way that can be put back.',
15
+ destroys: 'Removes data or a resource in a way that may not be put back.',
16
+ sends: 'Sends a message or data to a person, to the public or to another system.',
17
+ commits: 'Binds the person to something: an order, a booking, an agreement, an account.',
18
+ grants: 'Gives someone or something access or permission.',
19
+ runs: 'Runs a program or a command.',
20
+ enters: 'Signs in to a system or an account.',
21
+ };
22
+
23
+ /** The shared actions: name, kind, and what the name means. */
24
+ export const SHARED_ACTIONS = {
25
+ 'provared.data.read': ['reads', 'Read stored data or a file.'],
26
+ 'provared.data.search': ['reads', 'Search stored data.'],
27
+ 'provared.data.create': ['changes', 'Create a new item of data or a new file.'],
28
+ 'provared.data.change': ['changes', 'Change an item of data or a file that exists.'],
29
+ 'provared.data.delete': ['destroys', 'Delete data or a file.'],
30
+ 'provared.data.export': ['sends', 'Copy data out of the system that keeps it.'],
31
+ 'provared.message.read': ['reads', 'Read messages.'],
32
+ 'provared.message.draft': ['changes', 'Write a message without sending it.'],
33
+ 'provared.message.send': ['sends', 'Send a message to a person.'],
34
+ 'provared.post.publish': ['sends', 'Publish something where the public can see it.'],
35
+ 'provared.calendar.read': ['reads', 'Read a calendar.'],
36
+ 'provared.calendar.change': ['changes', 'Add, move or remove an entry in a calendar.'],
37
+ 'provared.booking.make': ['commits', 'Book a place, a time or a service.'],
38
+ 'provared.booking.cancel': ['commits', 'Cancel a booking.'],
39
+ 'provared.order.place': ['commits', 'Place an order for goods or services.'],
40
+ 'provared.order.cancel': ['commits', 'Cancel an order.'],
41
+ 'provared.terms.accept': ['commits', 'Accept terms, or sign an agreement.'],
42
+ 'provared.form.submit': ['commits', 'Submit a form or an application.'],
43
+ 'provared.account.create': ['commits', 'Open an account.'],
44
+ 'provared.account.sign-in': ['enters', 'Sign in to an account or a system.'],
45
+ 'provared.access.grant': ['grants', 'Give a person or a program access.'],
46
+ 'provared.access.remove': ['changes', 'Take access away from a person or a program.'],
47
+ 'provared.key.create': ['grants', 'Make a key or a password that gives access.'],
48
+ 'provared.code.read': ['reads', 'Read the source of a program.'],
49
+ 'provared.code.change': ['changes', 'Change the source of a program.'],
50
+ 'provared.code.run': ['runs', 'Run a program or a command.'],
51
+ 'provared.code.release': ['runs', 'Put a program into live use.'],
52
+ 'provared.system.configure': ['changes', 'Change the settings of a system.'],
53
+ 'provared.web.read': ['reads', 'Read a public web page.'],
54
+ 'provared.agent.instruct': ['sends', 'Hand a task to another agent.'],
55
+ };
56
+
57
+ /**
58
+ * The rules of conduct a slip can state. They are the person's signed
59
+ * instruction. No check of a record can show that an agent kept them.
60
+ */
61
+ export const CONDUCT_RULES = {
62
+ impersonate: 'Never claim to be a human being, or to be anyone other than the agent of the person who signed the slip.',
63
+ bypass: 'Never go around an access control, a block or a refusal.',
64
+ escalate: 'Never obtain or use access that it was not given.',
65
+ conceal: 'Never hide, alter or leave out its own records.',
66
+ deceive: 'Never state what it has reason to believe is false, and never make up a record or a result.',
67
+ harass: 'Never threaten, pressure or attack a person.',
68
+ disclose: 'Never pass information that is not public to anyone the slip does not name.',
69
+ 'continue-after-stop': 'Never carry on after being told to stop.',
70
+ };
71
+
72
+ /**
73
+ * The kind of a shared action, or null for a name that is not on the shared
74
+ * list (a name of the writer's own).
75
+ * @param {string} name
76
+ * @returns {string|null}
77
+ */
78
+ export function actionKind(name) {
79
+ return typeof name === 'string' && Object.hasOwn(SHARED_ACTIONS, name) ? SHARED_ACTIONS[name][0] : null;
80
+ }
81
+
82
+ /** Whether a name may stand in a "never" list: a kind of action, or a rule of conduct. */
83
+ export function isNeverName(name) {
84
+ return typeof name === 'string' && (Object.hasOwn(ACTION_KINDS, name) || Object.hasOwn(CONDUCT_RULES, name));
85
+ }
86
+
87
+ /** Whether an action name is reserved and not on the shared list. */
88
+ export function isUnknownReserved(name) {
89
+ return typeof name === 'string' && name.startsWith(RESERVED_PREFIX) && !Object.hasOwn(SHARED_ACTIONS, name);
90
+ }
@@ -0,0 +1,73 @@
1
+ // The approval: the person's own yes to one action, signed with the same
2
+ // passkey as the slip. Format description, section 17.
3
+
4
+ import { canonicalJson, fingerprint, formatTime, randomId } from './encoding.js';
5
+ import * as f from './fields.js';
6
+ import { KINDS } from './jws.js';
7
+ import { assembleSlip, preparePasskeyRecord } from './slip.js';
8
+ import { validateRequest } from './stub.js';
9
+
10
+ /**
11
+ * Confirm every member of an approval's content.
12
+ * @param {any} c
13
+ */
14
+ export function validateApprovalContent(c) {
15
+ f.members(c, ['action', 'id', 'slip', 'type', 'when'], ['amount', 'details', 'with'], 'approval');
16
+ f.id(c.id, 'id');
17
+ f.fingerprintText(c.slip, 'slip');
18
+ validateRequest(c);
19
+ f.time(c.when, 'when');
20
+ }
21
+
22
+ /**
23
+ * What an approval and a stub must agree on: the action, and the amount, the
24
+ * service and the documents, each present in both or in neither.
25
+ * @param {any} c the content of an approval or of a stub
26
+ * @returns {string}
27
+ */
28
+ export function requestOf(c) {
29
+ const request = { action: c.action };
30
+ for (const name of ['amount', 'details', 'with']) if (Object.hasOwn(c, name)) request[name] = c[name];
31
+ return canonicalJson(request);
32
+ }
33
+
34
+ /**
35
+ * Build the content of an approval and the challenge the person's passkey
36
+ * must sign.
37
+ *
38
+ * @param {object} fields
39
+ * @param {string} fields.slip the fingerprint of the slip
40
+ * @param {string} fields.action
41
+ * @param {{unit: string, value: number}} [fields.amount]
42
+ * @param {string} [fields.with]
43
+ * @param {{name: string, sha256: string}[]} [fields.details]
44
+ * @param {number|Date} [fields.when] defaults to now
45
+ * @returns {Promise<{payloadB64: string, protectedB64: string, challenge: Uint8Array, fingerprint: string}>}
46
+ * "fingerprint" is what the agent's stub must name as its approval
47
+ */
48
+ export async function prepareApproval(fields) {
49
+ const content = {
50
+ type: KINDS.approval.type,
51
+ id: fields.id ?? randomId(),
52
+ slip: fields.slip,
53
+ action: fields.action,
54
+ when: formatTime(fields.when ?? Date.now()),
55
+ };
56
+ if (fields.amount) content.amount = fields.amount;
57
+ if (fields.with) content.with = fields.with;
58
+ if (fields.details && fields.details.length) content.details = fields.details;
59
+ validateApprovalContent(content);
60
+ const prepared = await preparePasskeyRecord('approval', content);
61
+ return { ...prepared, fingerprint: await fingerprint(prepared.contentBytes) };
62
+ }
63
+
64
+ /**
65
+ * Put the passkey's three values with the prepared content: the signed
66
+ * approval. The values are stored exactly as the passkey returned them.
67
+ * @param {{payloadB64: string, protectedB64: string}} prepared from prepareApproval
68
+ * @param {{authenticatorData: Uint8Array, clientDataJSON: Uint8Array, signature: Uint8Array}} assertion
69
+ * @returns {object} the approval record
70
+ */
71
+ export function assembleApproval(prepared, assertion) {
72
+ return assembleSlip(prepared, assertion);
73
+ }
@@ -0,0 +1,198 @@
1
+ // A block time-stamp: proof that a fingerprint was written into a block of
2
+ // a public blockchain. The chain is used purely as a clock. No coin, no
3
+ // wallet, no account and no payment is involved, in making a proof or in
4
+ // checking one.
5
+ //
6
+ // The proof is a file of the open format "OpenTimestamps", version 1, as
7
+ // that format's reference reader accepts it, read strictly here. It is a
8
+ // list of steps that lead from the fingerprint to the Merkle root in the
9
+ // header of a block of the Bitcoin blockchain: join bytes before, join
10
+ // bytes after, take SHA-256. The block's own 80-byte header comes with the
11
+ // proof.
12
+ //
13
+ // Nothing here rests on a signature. It rests on SHA-256 (FIPS 180-4), and
14
+ // on the person checking having named the block as one they trust to be
15
+ // part of the chain.
16
+ //
17
+ // Format description, section 21.4. Runs unchanged in a browser.
18
+
19
+ import { Refusal, concatBytes, equalBytes, formatTime, sha256, toBase64url, utf8 } from './encoding.js';
20
+
21
+ /** The largest proof accepted, in bytes: the same as for a time-stamp from a service. */
22
+ export const MAX_PROOF_BYTES = 12288;
23
+ /** The length of a block header, in bytes. */
24
+ export const BLOCK_HEADER_BYTES = 80;
25
+ /**
26
+ * How far the time a block states may be from the true time: two hours.
27
+ * A block's time is set by whoever made the block, within about that much.
28
+ */
29
+ export const BLOCK_TIME_ALLOWANCE_MS = 7200 * 1000;
30
+
31
+ // The limits of the format's own reference reader.
32
+ const MAX_VALUE_BYTES = 4096;
33
+ const MAX_ATTESTATION_BYTES = 8192;
34
+ const MAX_DEPTH = 255;
35
+
36
+ // How a proof file begins, and the mark of "this value is the Merkle root
37
+ // of a block of the Bitcoin blockchain".
38
+ const MAGIC = concatBytes(new Uint8Array([0x00]), utf8('OpenTimestamps'), new Uint8Array([0x00, 0x00]), utf8('Proof'), new Uint8Array([0x00, 0xbf, 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94]));
39
+ const BLOCK_MARK = new Uint8Array([0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01]);
40
+
41
+ const STEP = { SHA256: 0x08, JOIN_AFTER: 0xf0, JOIN_BEFORE: 0xf1, ATTESTATION: 0x00, FORK: 0xff };
42
+
43
+ function bad(why) {
44
+ return new Refusal('stamp-bad-data', `A block time-stamp is not laid out as its format sets out: ${why}`);
45
+ }
46
+
47
+ function reader(bytes) {
48
+ let at = 0;
49
+ return {
50
+ byte() {
51
+ if (at >= bytes.length) throw bad('it is cut short.');
52
+ return bytes[at++];
53
+ },
54
+ take(length) {
55
+ if (at + length > bytes.length) throw bad('it is cut short.');
56
+ at += length;
57
+ return bytes.subarray(at - length, at);
58
+ },
59
+ done() {
60
+ return at === bytes.length;
61
+ },
62
+ };
63
+ }
64
+
65
+ // A whole number as the format writes it: seven bits to a byte, lowest
66
+ // first, the top bit saying that another byte follows. Only the shortest
67
+ // form is accepted, so that one number has one form.
68
+ function number(r, most) {
69
+ let value = 0;
70
+ for (let i = 0; i < 5; i++) {
71
+ const b = r.byte();
72
+ value += (b & 0x7f) * 2 ** (7 * i);
73
+ if (!(b & 0x80)) {
74
+ if (i > 0 && b === 0) throw bad('a number is not written in its shortest form.');
75
+ if (value > most) throw bad('a number is larger than the format allows.');
76
+ return value;
77
+ }
78
+ }
79
+ throw bad('a number is longer than the format allows.');
80
+ }
81
+
82
+ // One thing that follows from a value: a statement about it, or a step
83
+ // that makes a new value, with everything that follows from that.
84
+ async function readItem(r, tag, value, depth, found) {
85
+ if (tag === STEP.ATTESTATION) {
86
+ const mark = r.take(8);
87
+ const payload = r.take(number(r, MAX_ATTESTATION_BYTES));
88
+ // Statements of other kinds (a proof not yet complete, another chain) say nothing here.
89
+ if (!equalBytes(mark, BLOCK_MARK)) return;
90
+ const p = reader(payload);
91
+ const height = number(p, 2 ** 35 - 1);
92
+ if (!p.done()) throw bad('the statement about a block holds more than the block\'s number.');
93
+ if (value.length === 32) found.push({ root: value, height });
94
+ return;
95
+ }
96
+ let next;
97
+ if (tag === STEP.SHA256) {
98
+ next = await sha256(value);
99
+ } else if (tag === STEP.JOIN_AFTER || tag === STEP.JOIN_BEFORE) {
100
+ const joined = r.take(number(r, MAX_VALUE_BYTES));
101
+ if (joined.length === 0) throw bad('a step joins nothing.');
102
+ if (value.length + joined.length > MAX_VALUE_BYTES) throw bad('a step makes a value longer than 4,096 bytes.');
103
+ next = tag === STEP.JOIN_AFTER ? concatBytes(value, joined) : concatBytes(joined, value);
104
+ } else {
105
+ throw bad('it uses a step other than "join before", "join after" and SHA-256.');
106
+ }
107
+ await readFrom(r, next, depth + 1, found);
108
+ }
109
+
110
+ // Everything that follows from a value. Each thing but the last is marked
111
+ // as one of several.
112
+ async function readFrom(r, value, depth, found) {
113
+ if (depth > MAX_DEPTH) throw bad('its steps go deeper than the format allows.');
114
+ let tag = r.byte();
115
+ while (tag === STEP.FORK) {
116
+ await readItem(r, r.byte(), value, depth, found);
117
+ tag = r.byte();
118
+ }
119
+ await readItem(r, tag, value, depth, found);
120
+ }
121
+
122
+ const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
123
+
124
+ /**
125
+ * The fingerprint of a block, as it is written everywhere: SHA-256 taken
126
+ * twice over the header, the bytes in reverse order, in hex.
127
+ * @param {Uint8Array} header the 80-byte block header
128
+ * @returns {Promise<string>} 64 hex characters
129
+ */
130
+ export async function blockFingerprint(header) {
131
+ return hex((await sha256(await sha256(header))).reverse());
132
+ }
133
+
134
+ /**
135
+ * Put a proof and the header of its block in the form they have beside a
136
+ * seal or a cancellation, in the list "stamps".
137
+ * @param {Uint8Array} proof the proof file, complete: it must lead to a block
138
+ * @param {Uint8Array} header the 80-byte header of that block
139
+ * @returns {{block: string, proof: string}}
140
+ */
141
+ export function blockStampItem(proof, header) {
142
+ if (!(proof instanceof Uint8Array) || proof.length === 0 || proof.length > MAX_PROOF_BYTES) throw bad('it must be at most 12,288 bytes.');
143
+ if (!(header instanceof Uint8Array) || header.length !== BLOCK_HEADER_BYTES) throw bad('the block header that comes with it must be 80 bytes.');
144
+ return { block: toBase64url(header), proof: toBase64url(proof) };
145
+ }
146
+
147
+ /**
148
+ * Check one block time-stamp.
149
+ *
150
+ * @param {Uint8Array} proof the proof file; it is not changed
151
+ * @param {Uint8Array} header the 80-byte header of the block the proof leads to
152
+ * @param {Uint8Array} fingerprintBytes the 32 bytes that must have been stamped
153
+ * @param {string[]} [trusted] the fingerprints (64 hex characters) of the blocks the checker trusts to be part of the chain
154
+ * @returns {Promise<{kind: 'block', state: 'valid'|'untrusted', when: string, time: number, earliest: number, authority: string, height: number}>}
155
+ * "valid": the proof leads to a block the checker named. "untrusted": it
156
+ * leads to the block that comes with it, which the checker did not name;
157
+ * never treat this as a time. "when" is the time the block states. A
158
+ * block's time is only right to within about two hours, so there are two
159
+ * times in milliseconds. "time" is when the record existed by: the
160
+ * stated time and two hours. "earliest" is the earliest the proof could
161
+ * have been made: the stated time less two hours. Which of the two is
162
+ * the safe one depends on what is asked (see book.js). "authority" is
163
+ * the block's fingerprint. "height" is the block's number as the proof
164
+ * states it; nothing checks it.
165
+ * @throws {Refusal} "stamp-bad-data", "stamp-wrong-data" or "stamp-invalid"
166
+ */
167
+ export async function checkBlockStamp(proof, header, fingerprintBytes, trusted) {
168
+ if (!(proof instanceof Uint8Array) || proof.length > MAX_PROOF_BYTES) throw bad('it must be at most 12,288 bytes.');
169
+ if (!(header instanceof Uint8Array) || header.length !== BLOCK_HEADER_BYTES) throw bad('the block header that comes with it must be 80 bytes.');
170
+ const r = reader(proof);
171
+ if (!equalBytes(r.take(MAGIC.length), MAGIC)) throw bad('it does not begin as a proof file does.');
172
+ if (r.byte() !== 1) throw bad('it is not version 1 of the format.');
173
+ if (r.byte() !== STEP.SHA256) throw bad('its first fingerprint is not SHA-256.');
174
+ const stamped = r.take(32);
175
+ const found = [];
176
+ await readFrom(r, stamped, 0, found);
177
+ if (!r.done()) throw bad('there are bytes after its end.');
178
+
179
+ if (!(fingerprintBytes instanceof Uint8Array) || !equalBytes(stamped, fingerprintBytes)) {
180
+ throw new Refusal('stamp-wrong-data', 'A block time-stamp was made for something other than this record.');
181
+ }
182
+ // The Merkle root is bytes 36 to 67 of a block header, and the time bytes 68 to 71, lowest first.
183
+ const root = header.subarray(36, 68);
184
+ const match = found.find((f) => equalBytes(f.root, root));
185
+ if (!match) throw new Refusal('stamp-invalid', 'A block time-stamp does not lead to the block that comes with it.');
186
+ const stated = (header[68] + header[69] * 2 ** 8 + header[70] * 2 ** 16 + header[71] * 2 ** 24) * 1000;
187
+ const fingerprint = await blockFingerprint(header);
188
+ const named = Array.isArray(trusted) && trusted.some((t) => typeof t === 'string' && t.toLowerCase() === fingerprint);
189
+ return {
190
+ kind: 'block',
191
+ state: named ? 'valid' : 'untrusted',
192
+ when: formatTime(stated),
193
+ time: stated + BLOCK_TIME_ALLOWANCE_MS,
194
+ earliest: stated - BLOCK_TIME_ALLOWANCE_MS,
195
+ authority: fingerprint,
196
+ height: match.height,
197
+ };
198
+ }