provared 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/check.d.ts CHANGED
@@ -8,6 +8,8 @@ export {
8
8
  methodAvailable,
9
9
  thumbprint,
10
10
  keySetFingerprint,
11
+ verifyConsistency,
12
+ verifyInclusion,
11
13
  blockFingerprint,
12
14
  checkHeaderChain,
13
15
  blocksInChain,
@@ -17,5 +19,6 @@ export {
17
19
  neverWords,
18
20
  LIMITS,
19
21
  REFUSAL_REASONS,
22
+ DENIAL_SOURCES,
20
23
  } from './types.js';
21
24
  export type * from './types.js';
package/src/check.js CHANGED
@@ -7,9 +7,11 @@
7
7
  export { checkBook, checkShow, openChecker, LIMITS } from './book.js';
8
8
  export { limitWords, conditionWords, neverWords } from './compare.js';
9
9
  export { REFUSAL_REASONS } from './service.js';
10
+ export { DENIAL_SOURCES } from './denial.js';
10
11
  export { checkSlip } from './slip.js';
11
12
  export { keySetFingerprint } from './seal.js';
12
13
  export { blockFingerprint } from './blockstamp.js';
13
14
  export { checkHeaderChain, blocksInChain, MIN_BLOCKS_AFTER } from './headers.js';
14
15
  export { thumbprint } from './keys.js';
15
16
  export { methodAvailable } from './signatures.js';
17
+ export { verifyConsistency, verifyInclusion } from './tree.js';
package/src/denial.js ADDED
@@ -0,0 +1,123 @@
1
+ // The denial: the agent's own side states that it did not take an action
2
+ // that was asked for, and why. It is signed with the agent's two keys, or
3
+ // with the helper agent's under its pass. It is the agent's side's own
4
+ // word, and an agent can simply not write one.
5
+ // Format description, section 28.
6
+
7
+ import { formatTime, randomId } from './encoding.js';
8
+ import * as f from './fields.js';
9
+ import { KINDS } from './jws.js';
10
+ import { signWithKeySet, validateRequest } from './stub.js';
11
+
12
+ /** Who said no. */
13
+ export const DENIAL_SOURCES = {
14
+ check: 'The check before acting (section 20) did not allow the action.',
15
+ person: 'The person refused, or gave no approval when asked.',
16
+ rule: 'A rule of the software around the agent, outside the slip.',
17
+ };
18
+
19
+ /**
20
+ * The format's own list of finding codes: every code a checker or the check
21
+ * before acting gives (format description, section 28; the same list is in
22
+ * the list of threats, section 8). A denial whose source is "check" names
23
+ * its reasons from this list.
24
+ */
25
+ export const FINDING_CODES = Object.freeze([
26
+ 'not-json', 'bad-envelope', 'bad-base64url', 'too-large', 'unknown-type', 'bad-signatures-layout', 'payload-not-canonical',
27
+ 'payload-type-mismatch', 'bad-field', 'bad-key', 'signature-invalid', 'passkey-wrong-type', 'passkey-challenge-mismatch',
28
+ 'passkey-origin-mismatch', 'passkey-rpid-mismatch', 'passkey-user-not-present', 'passkey-user-not-verified', 'passkey-bad-data',
29
+ 'issuer-not-expected', 'slip-missing', 'slip-unusable', 'chain-broken', 'time-went-backwards', 'duplicate-id', 'duplicate-slip',
30
+ 'countersignature-invalid', 'countersignature-wrong-stub', 'countersignature-not-possible', 'line-not-canonical', 'unknown-entry',
31
+ 'approval-mismatch', 'approval-reused', 'approval-invalid', 'approval-not-found', 'approval-dated-after-stub', 'terms-not-found',
32
+ 'terms-unusable', 'terms-mismatch', 'pass-missing', 'pass-mismatch', 'pass-too-deep', 'cover-invalid', 'cancellation-invalid',
33
+ 'cancellation-not-found', 'acknowledgement-mismatch', 'vouching-missing', 'seal-mismatch', 'seal-chain-broken', 'sealer-not-expected', 'book-cut-short', 'seal-not-in-book', 'witness-invalid',
34
+ 'dated-after-stamp', 'stamp-bad-data', 'stamp-wrong-data', 'stamp-invalid', 'headers-invalid', 'record-not-sound', 'proof-invalid',
35
+ 'root-mismatch', 'check-failed', 'action-not-allowed', 'party-not-allowed', 'outside-valid-time', 'amount-missing', 'over-limit',
36
+ 'over-each-limit', 'over-count-limit', 'over-period-limit', 'countersignature-missing', 'approval-missing', 'prohibited',
37
+ 'pass-not-allowed', 'pass-wider', 'after-cancellation', 'outside-terms',
38
+ ]);
39
+
40
+ /** The most finding codes one denial may name. */
41
+ export const MAX_DENIAL_CODES = 16;
42
+
43
+ /**
44
+ * The reasons of a refusal by the check before acting, as a denial names
45
+ * them: the codes of its problems, then of its findings, each once, in that
46
+ * order, from the format's own list, at most 16. Where none is on the list,
47
+ * "check-failed".
48
+ * @param {{problems: {code: string}[], breaches: {code: string}[]}} answer the answer of the check before acting
49
+ * @returns {string[]}
50
+ */
51
+ export function denialCodes(answer) {
52
+ const codes = [];
53
+ for (const p of [...answer.problems, ...answer.breaches]) {
54
+ if (p && typeof p.code === 'string' && FINDING_CODES.includes(p.code) && !codes.includes(p.code)) codes.push(p.code);
55
+ }
56
+ return codes.length ? codes.slice(0, MAX_DENIAL_CODES) : ['check-failed'];
57
+ }
58
+
59
+ /**
60
+ * Confirm every member of a denial's content.
61
+ * @param {any} c
62
+ */
63
+ export function validateDenialContent(c) {
64
+ f.members(c, ['action', 'id', 'slip', 'source', 'type', 'when'], ['amount', 'codes', 'details', 'pass', 'with'], 'denial');
65
+ f.id(c.id, 'id');
66
+ f.fingerprintText(c.slip, 'slip');
67
+ if (Object.hasOwn(c, 'pass')) f.fingerprintText(c.pass, 'pass');
68
+ // What was asked for is written down as it was asked, as in a refusal: a
69
+ // reserved name that is not on the shared list may be why it was refused.
70
+ f.shortName(c.action, 'action');
71
+ // The amount, the service and the documents as in a stub; the service
72
+ // need not be one the slip names.
73
+ validateRequest({ ...c, action: 'x' });
74
+ if (typeof c.source !== 'string' || !Object.hasOwn(DENIAL_SOURCES, c.source)) throw f.fail('source', 'must be "check", "person" or "rule".');
75
+ if (c.source === 'check') {
76
+ if (!Object.hasOwn(c, 'codes')) throw f.fail('codes', 'is missing: a denial by the check before acting names its reasons.');
77
+ f.list(c.codes, 1, MAX_DENIAL_CODES, 'codes');
78
+ c.codes.forEach((code, i) => {
79
+ if (typeof code !== 'string' || !FINDING_CODES.includes(code)) throw f.fail(`codes[${i}]`, 'must be a code from the format\'s own list of finding codes.');
80
+ });
81
+ if (new Set(c.codes).size !== c.codes.length) throw f.fail('codes', 'a code is repeated.');
82
+ } else if (Object.hasOwn(c, 'codes')) {
83
+ throw f.fail('codes', 'only a denial by the check before acting names codes.');
84
+ }
85
+ f.time(c.when, 'when');
86
+ }
87
+
88
+ /**
89
+ * Write and sign a denial: the agent's side states that it did not take an
90
+ * action that was asked for, and why. It is signed with the keys of the
91
+ * agent the slip names or, where the signer is a helper agent, with the
92
+ * keys the pass names.
93
+ *
94
+ * @param {object} fields
95
+ * @param {string} fields.slip the fingerprint of the slip the agent acts under
96
+ * @param {string} [fields.pass] for a helper agent: the fingerprint of the pass it acts under
97
+ * @param {string} fields.action what was asked for
98
+ * @param {{unit: string, value: number}} [fields.amount]
99
+ * @param {string} [fields.with] the service asked for, as asked; it need not be one the slip names
100
+ * @param {{name: string, sha256: string}[]} [fields.details] fingerprints of documents, never the documents
101
+ * @param {'check'|'person'|'rule'} fields.source who said no
102
+ * @param {string[]} [fields.codes] where the source is "check": 1 to 16 finding codes, from FINDING_CODES
103
+ * @param {number|Date} [fields.when] defaults to now
104
+ * @param {any[]} privateKeys the agent's private keys, or signers: Ed25519, then ML-DSA-87
105
+ * @returns {Promise<{record: object, fingerprint: string}>}
106
+ */
107
+ export async function writeDenial(fields, privateKeys) {
108
+ const content = {
109
+ type: KINDS.denial.type,
110
+ id: fields.id ?? randomId(),
111
+ slip: fields.slip,
112
+ action: fields.action,
113
+ source: fields.source,
114
+ when: formatTime(fields.when ?? Date.now()),
115
+ };
116
+ if (fields.pass) content.pass = fields.pass;
117
+ if (fields.amount) content.amount = fields.amount;
118
+ if (fields.with) content.with = fields.with;
119
+ if (fields.details && fields.details.length) content.details = fields.details;
120
+ if (fields.codes !== undefined && fields.codes !== null) content.codes = fields.codes;
121
+ validateDenialContent(content);
122
+ return signWithKeySet('denial', content, privateKeys);
123
+ }
package/src/der.js CHANGED
@@ -81,6 +81,30 @@ export function contentOf(bytes, element) {
81
81
  return bytes.subarray(element.start, element.end);
82
82
  }
83
83
 
84
+ /**
85
+ * Whether the items of a set are in the order DER requires (X.690, section
86
+ * 11.6): their encodings compared as strings of bytes, the shorter padded
87
+ * at its end with zero bytes. Two equal encodings may stand side by side.
88
+ * @param {Uint8Array} bytes
89
+ * @param {{from: number, end: number}[]} items the elements of the set, in the order they appear
90
+ */
91
+ export function setInOrder(bytes, items) {
92
+ for (let i = 1; i < items.length; i++) {
93
+ const a = items[i - 1];
94
+ const b = items[i];
95
+ const longest = Math.max(a.end - a.from, b.end - b.from);
96
+ for (let k = 0; k < longest; k++) {
97
+ const x = a.from + k < a.end ? bytes[a.from + k] : 0;
98
+ const y = b.from + k < b.end ? bytes[b.from + k] : 0;
99
+ if (x !== y) {
100
+ if (x > y) return false;
101
+ break;
102
+ }
103
+ }
104
+ }
105
+ return true;
106
+ }
107
+
84
108
  /** The whole element, tag and length included. */
85
109
  export function wholeOf(bytes, element) {
86
110
  return bytes.subarray(element.from, element.end);
@@ -147,25 +171,45 @@ export function ecdsaToRaw(der, size) {
147
171
  /**
148
172
  * Walk a whole element and everything inside it, and refuse anything that
149
173
  * is not strict DER: a constructed part whose items do not fill it exactly,
150
- * a whole number or an object identifier that is not in its shortest form,
151
- * a truth value that is not written in the one way DER allows. Parts this
152
- * checker does not otherwise read are still read here, so that two checkers
153
- * cannot disagree about whether a time-stamp is well formed.
174
+ * a string (of bits, bytes or characters) or a time written in parts
175
+ * (X.690, section 10.2), a set whose items are not in order (section 11.6;
176
+ * only where "holdSetOrder" is true), a whole number or an object identifier
177
+ * that is not in its shortest form, a truth value that is not written in the
178
+ * one way DER allows, a string of bits whose unused bits are not zero. Parts
179
+ * this checker does not otherwise read are still read here, so that two
180
+ * checkers cannot disagree about whether a time-stamp is well formed. What a
181
+ * part holds as bytes (the value of an octet string) is read as bytes only.
182
+ * A part under a tag that hides its kind (a context tag): written in parts,
183
+ * the parts inside it are read like any other, but the tagged part itself is
184
+ * neither refused as a string written in parts nor held to the order of a
185
+ * set, since its tag does not say which it is; written in one piece, its
186
+ * value is read as bytes only. The order of a set is held only where a
187
+ * standard requires DER; checkStamp says where, and passes false for the
188
+ * rest of a time-stamp.
154
189
  * @param {Uint8Array} bytes
155
190
  * @param {{tag: number, start: number, end: number}} element
191
+ * @param {boolean} [holdSetOrder=true] whether the items of a set must be in DER order
156
192
  */
157
- export function validateDer(bytes, element) {
193
+ export function validateDer(bytes, element, holdSetOrder = true) {
158
194
  let budget = 4000;
159
195
  const walk = (e, depth) => {
160
196
  if (--budget < 0 || depth > 24) throw badStamp('it holds too many parts, or parts nested too deeply.');
161
197
  const length = e.end - e.start;
162
198
  if (e.tag & 0x20) {
199
+ // Of the universal kinds, only a sequence and a set are written in
200
+ // parts; a string or a time is written in one piece (X.690, section 10.2).
201
+ if ((e.tag & 0xc0) === 0 && e.tag !== TAG.SEQUENCE && e.tag !== TAG.SET) {
202
+ throw badStamp('a string of bits, bytes or characters, or a time, is written in parts, which DER does not allow.');
203
+ }
204
+ const items = [];
163
205
  let at = e.start;
164
206
  while (at < e.end) {
165
207
  const child = readElement(bytes, at, e.end);
166
208
  walk(child, depth + 1);
209
+ items.push(child);
167
210
  at = child.end;
168
211
  }
212
+ if (holdSetOrder && e.tag === TAG.SET && !setInOrder(bytes, items)) throw badStamp('the items of a set are not in the order DER requires.');
169
213
  return;
170
214
  }
171
215
  const first = bytes[e.start];
@@ -185,7 +229,12 @@ export function validateDer(bytes, element) {
185
229
  } else if (e.tag === TAG.NULL) {
186
230
  if (length !== 0) throw badStamp('an empty value is not empty.');
187
231
  } else if (e.tag === TAG.BIT_STRING) {
188
- if (length === 0 || first > 7) throw badStamp('a string of bits is not written as DER allows.');
232
+ // The first byte counts the unused bits of the last byte, 0 to 7; a
233
+ // string of no bits has none; and the unused bits are zero (X.690,
234
+ // sections 8.6.2 and 11.2.1).
235
+ if (length === 0 || first > 7 || (length === 1 && first !== 0) || (length > 1 && (bytes[e.end - 1] & ((1 << first) - 1)) !== 0)) {
236
+ throw badStamp('a string of bits is not written as DER allows.');
237
+ }
189
238
  }
190
239
  };
191
240
  walk(element, 0);
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; signWithin?: number },
53
+ more?: { options?: CheckOptions; now?: () => number; countersignWithin?: number; signWithin?: number; denials?: boolean; denialsPerHour?: 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, signWithin).
113
+ * countersignWithin, signWithin, denials, denialsPerHour).
114
114
  *
115
115
  * @param {object} fields the members of the slip; "actions" is required
116
116
  * @param {object} [more]
package/src/guard.js CHANGED
@@ -98,6 +98,8 @@ function onlySealThirdUnchecked(result) {
98
98
  const third = sigs.length === 3 && by('Ed25519')?.state === 'valid' && by('ML-DSA-87')?.state === 'valid' && by(SEAL_THIRD)?.state === 'unavailable';
99
99
  if (third) excused++; else note(sigs);
100
100
  stamps(e.stamps ?? []);
101
+ // The witness statements beside it: each of their signatures counts, as for any record.
102
+ for (const w of e.witnesses ?? []) note(w.signatures ?? []);
101
103
  } else if (e.kind === 'slip') {
102
104
  if (!e.signature || e.signature.state === 'unavailable') missing.add(e.signature ? e.signature.alg : 'passkey');
103
105
  else if (e.signature.state === 'unchecked') skipped = true;
@@ -111,7 +113,7 @@ function onlySealThirdUnchecked(result) {
111
113
  if (!e.signature) skipped = true;
112
114
  else if (e.signature.state === 'unavailable') missing.add(e.signature.alg);
113
115
  stamps(e.stamps ?? []);
114
- } else if (['refusal', 'terms', 'vouching', 'withdrawal', 'pass', 'acknowledgement'].includes(e.kind)) {
116
+ } else if (['refusal', 'terms', 'vouching', 'withdrawal', 'pass', 'acknowledgement', 'denial'].includes(e.kind)) {
115
117
  note(e.signatures ?? []);
116
118
  } else {
117
119
  skipped = true;
@@ -120,6 +122,15 @@ function onlySealThirdUnchecked(result) {
120
122
  for (const h of result.held ?? []) {
121
123
  stamps(h.stamps ?? []);
122
124
  for (const a of h.acknowledgements ?? []) note(a.signatures ?? []);
125
+ // A seal held elsewhere, handed over beside the book: excused as a seal in the book is.
126
+ if (h.kind === 'seal') {
127
+ const sigs = h.signatures ?? [];
128
+ const by = (method) => sigs.find((x) => x.method === method);
129
+ const third = sigs.length === 3 && by('Ed25519')?.state === 'valid' && by('ML-DSA-87')?.state === 'valid' && by(SEAL_THIRD)?.state === 'unavailable';
130
+ if (third) excused++;
131
+ else for (const x of sigs) if (x.state === 'unavailable') missing.add(x.method);
132
+ for (const w of h.witnesses ?? []) note(w.signatures ?? []);
133
+ }
123
134
  }
124
135
  return !skipped && missing.size === 0 && excused > 0 ? [SEAL_THIRD] : null;
125
136
  }
package/src/index.js CHANGED
@@ -11,8 +11,13 @@
11
11
  // writeStub (the agent), countersign, writeRefusal, writeTerms
12
12
  // (the service), writeSeal (whoever keeps the book), blockStampItem
13
13
  // (a proof from a public blockchain, to put beside a seal),
14
+ // writeWitness (a witness signs for a seal), openWitness (a witness
15
+ // that signs only for a seal that extends the last one it holds),
16
+ // openSealer (a central sealer, which seals books it is sent only
17
+ // the leaf fingerprints of: leafFingerprint),
14
18
  // prepareCancellation + assembleCancellation (the person ends a slip),
15
19
  // writeAcknowledgement (the agent's side says it was handed a cancellation),
20
+ // writeDenial (the agent's side says it did not take an action, and why),
16
21
  // writeVouching, writeWithdrawal (an organisation vouches for a name),
17
22
  // writePass (an agent hands part of its permission to a helper agent),
18
23
  // writeBook, makeShow
@@ -33,8 +38,13 @@ export { writeStub, countersign } from './stub.js';
33
38
  export { writeRefusal, writeTerms } from './service.js';
34
39
  export { writePass } from './pass.js';
35
40
  export { writeSeal } from './seal.js';
41
+ export { writeWitness, MAX_WITNESSES } from './witness.js';
42
+ export { openWitness, bookIdentity, witnessProof } from './witnessing.js';
43
+ export { openSealer, leafFingerprint } from './sealer.js';
44
+ export { consistencyProof, inclusionPath } from './tree.js';
36
45
  export { blockStampItem } from './blockstamp.js';
37
46
  export { prepareCancellation, assembleCancellation, writeAcknowledgement, writeVouching, writeWithdrawal, VOUCHED_KINDS } from './standing.js';
47
+ export { writeDenial, denialCodes, FINDING_CODES } from './denial.js';
38
48
  export { entryLine, writeBook, makeShow } from './book.js';
39
49
  export { generateKeySet, generateSealKeySet, signerFromKey } from './signatures.js';
40
50
  export { ACTION_KINDS, SHARED_ACTIONS, CONDUCT_RULES, RESERVED_PREFIX, actionKind } from './actions.js';
package/src/jws.js CHANGED
@@ -66,6 +66,14 @@ export const KINDS = {
66
66
  type: 'provared.pass.v0',
67
67
  protected: ['{"typ":"vnd.provared.pass.v0+json","alg":"Ed25519"}', '{"typ":"vnd.provared.pass.v0+json","alg":"ML-DSA-87"}'],
68
68
  },
69
+ denial: {
70
+ type: 'provared.denial.v0',
71
+ protected: ['{"typ":"vnd.provared.denial.v0+json","alg":"Ed25519"}', '{"typ":"vnd.provared.denial.v0+json","alg":"ML-DSA-87"}'],
72
+ },
73
+ witness: {
74
+ type: 'provared.witness.v0',
75
+ protected: ['{"typ":"vnd.provared.witness.v0+json","alg":"Ed25519"}', '{"typ":"vnd.provared.witness.v0+json","alg":"ML-DSA-87"}'],
76
+ },
69
77
  seal: {
70
78
  type: 'provared.seal.v0',
71
79
  protected: [
package/src/keys.js CHANGED
@@ -46,6 +46,50 @@ export function weakEd25519(bytes) {
46
46
  return y >= P || SMALL_ORDER_Y.has(y);
47
47
  }
48
48
 
49
+ // The order L of the Ed25519 base point (RFC 8032, section 5.1). The number
50
+ // S of a signature must be below it (RFC 8032, section 5.1.7); libraries
51
+ // differ in whether they check, so a checker checks itself.
52
+ const L = 2n ** 252n + 27742317777372353535851937790883648493n;
53
+
54
+ /**
55
+ * Whether bytes are an Ed25519 signature that can never be valid, whatever
56
+ * the key: not 64 bytes; or its first half (the point R) is one under which
57
+ * anyone can sign or is written in more than one way; or its second half
58
+ * (the number S, little-endian) is not below L.
59
+ * @param {Uint8Array} signature
60
+ * @returns {boolean}
61
+ */
62
+ export function badEd25519Signature(signature) {
63
+ if (signature.length !== 64) return true;
64
+ if (weakEd25519(signature.subarray(0, 32))) return true;
65
+ let s = 0n;
66
+ for (let i = 63; i >= 32; i--) s = (s << 8n) | BigInt(signature[i]);
67
+ return s >= L;
68
+ }
69
+
70
+ // The curve P-256 (FIPS 186-5, section D.1.2.3): y^2 = x^3 - 3x + b over
71
+ // the whole numbers modulo p. A key whose two halves are not a point on it
72
+ // can check no signature, and libraries report it in different ways, so a
73
+ // checker refuses it itself.
74
+ const P256_P = 2n ** 256n - 2n ** 224n + 2n ** 192n + 2n ** 96n - 1n;
75
+ const P256_B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn;
76
+
77
+ /**
78
+ * Whether two halves of 32 bytes each (x and y, big-endian) are a point on
79
+ * the curve P-256, each below p.
80
+ * @param {Uint8Array} x
81
+ * @param {Uint8Array} y
82
+ * @returns {boolean}
83
+ */
84
+ export function onP256(x, y) {
85
+ let X = 0n;
86
+ let Y = 0n;
87
+ for (const b of x) X = (X << 8n) | BigInt(b);
88
+ for (const b of y) Y = (Y << 8n) | BigInt(b);
89
+ if (X >= P256_P || Y >= P256_P) return false;
90
+ return (Y * Y) % P256_P === (((X * X * X - 3n * X + P256_B) % P256_P) + P256_P) % P256_P;
91
+ }
92
+
49
93
  function bad(where, why) {
50
94
  return new Refusal('bad-key', `${where}: ${why}`);
51
95
  }
@@ -94,9 +138,12 @@ export function checkKey(jwk, allowed, where) {
94
138
  if (bytesOf(jwk, 'pub', where).length !== 64) throw bad(where, 'an SLH-DSA-SHA2-256s public key is 64 bytes.');
95
139
  } else if (alg === 'ES256') {
96
140
  if (jwk.kty !== 'EC' || jwk.crv !== 'P-256') throw bad(where, 'an ES256 key has kty EC and crv P-256.');
97
- if (bytesOf(jwk, 'x', where).length !== 32 || bytesOf(jwk, 'y', where).length !== 32) {
141
+ const x = bytesOf(jwk, 'x', where);
142
+ const y = bytesOf(jwk, 'y', where);
143
+ if (x.length !== 32 || y.length !== 32) {
98
144
  throw bad(where, 'the two halves of a P-256 public key are 32 bytes each.');
99
145
  }
146
+ if (!onP256(x, y)) throw bad(where, 'a P-256 public key must be a point on the curve, each half below the prime of the curve.');
100
147
  } else {
101
148
  if (jwk.kty !== 'RSA' || jwk.e !== 'AQAB') throw bad(where, 'an RS256 key has kty RSA and e AQAB.');
102
149
  const n = bytesOf(jwk, 'n', where);
package/src/recorder.js CHANGED
@@ -38,12 +38,18 @@ import { KEY_SET_METHODS } from './keys.js';
38
38
  import { CLOCK_ALLOWANCE_MS, MAX_STAMPS } from './seal.js';
39
39
  import { holdSigningKeys } from './signatures.js';
40
40
  import { writeAcknowledgement } from './standing.js';
41
+ import { denialCodes, writeDenial } from './denial.js';
41
42
  import { validateRequest, writeStub } from './stub.js';
42
43
 
43
44
  /** How long the other side is given to hand back its countersignature, unless the caller says otherwise: 30 seconds. */
44
45
  export const COUNTERSIGN_WITHIN_MS = 30000;
45
46
  /** How long a signer is given for each signature, unless the caller says otherwise: 30 seconds. */
46
47
  export const SIGN_WITHIN_MS = 30000;
48
+ /** The most denials a writer writes in any hour, unless the caller says otherwise: 600. */
49
+ export const DENIALS_PER_HOUR = 600;
50
+ // The most a caller may set it to: as many as a book may hold entries.
51
+ const MAX_DENIALS_PER_HOUR = 100000;
52
+ const HOUR_MS = 3600000;
47
53
  // The longest wait a timer can hold.
48
54
  const MAX_WAIT_MS = 2 ** 31 - 1;
49
55
  // "detailsAfter" in "act": the most documents it may add, and the most the
@@ -227,11 +233,29 @@ function plainHeld(copy) {
227
233
  * @param {number} [o.countersignWithin] how long the other side is given to countersign, in milliseconds; defaults to 30,000
228
234
  * @param {number} [o.signWithin] how long a signer is given for each signature, in milliseconds; defaults to 30,000.
229
235
  * After that the action is not taken and nothing is written; a late answer is not used
230
- * @returns {Promise<object>} the writer: before, record, act, book, check, cancellations, add
236
+ * @param {boolean} [o.denials] true to write a denial (format description, section 28) whenever "act" does not take an
237
+ * action, and to let "deny" write one; defaults to false, and then no denial is written
238
+ * @param {number} [o.denialsPerHour] the most denials written in any hour, by the clock; defaults to 600. Each
239
+ * denial this writer writes, or is handed through "add", counts at the clock's time when it joins the book, whatever
240
+ * date it states; those the book held when the writer was opened count at the dates they state. Past the limit, a
241
+ * denial is not written, and the answers count those not written
242
+ * @returns {Promise<object>} the writer: before, record, act, deny, book, check, cancellations, add
231
243
  * @throws {Refusal} "record-not-sound" if the book so far has a problem; "bad-field" or "bad-key" if an item of
232
244
  * privateKeys is neither a key nor a signer, or is a signer that holds another key than the slip, or the pass, names
233
245
  */
234
- export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass, options = {}, now = Date.now, countersignWithin = COUNTERSIGN_WITHIN_MS, signWithin = SIGN_WITHIN_MS }) {
246
+ export async function openRecorder({
247
+ book,
248
+ slip,
249
+ privateKeys,
250
+ issuerKeys,
251
+ pass,
252
+ options = {},
253
+ now = Date.now,
254
+ countersignWithin = COUNTERSIGN_WITHIN_MS,
255
+ signWithin = SIGN_WITHIN_MS,
256
+ denials = false,
257
+ denialsPerHour = DENIALS_PER_HOUR,
258
+ }) {
235
259
  if (typeof now !== 'function') throw new Refusal('bad-field', 'now: must be a function that gives the time in milliseconds.');
236
260
  // A clock that gives anything but a time is a clock that failed: no comparison with it would hold.
237
261
  const device = now;
@@ -246,6 +270,10 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
246
270
  if (!(Number.isFinite(signWithin) && signWithin >= 1 && signWithin <= MAX_WAIT_MS)) {
247
271
  throw new Refusal('bad-field', 'signWithin: must be a number of milliseconds from 1 to 2,147,483,647.');
248
272
  }
273
+ if (typeof denials !== 'boolean') throw new Refusal('bad-field', 'denials: must be true or false.');
274
+ if (!(Number.isInteger(denialsPerHour) && denialsPerHour >= 1 && denialsPerHour <= MAX_DENIALS_PER_HOUR)) {
275
+ throw new Refusal('bad-field', 'denialsPerHour: must be a whole number from 1 to 100,000.');
276
+ }
249
277
  // Whom the writer trusts is fixed when it is opened: read once, as a whole
250
278
  // check reads it, and copied. The passkeys it trusts are its own
251
279
  // "issuerKeys", whatever the options say, read through the same fixed
@@ -360,12 +388,23 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
360
388
  const { result } = await standing();
361
389
  if (result.summary.problemFound) throw notWritten('A problem was found in the record so far. It must be looked at before the agent acts again.');
362
390
  let after = lastStub(result.entries);
391
+ // The times of the denials of this writer's own chain, which the hourly
392
+ // limit counts: those the book held when the writer was opened, at the
393
+ // dates they state; and each one that joins the book since, at the
394
+ // clock's time when it joined, whatever date it states (a denial may be
395
+ // dated up to 300 seconds back, and counting it there would let more
396
+ // than the limit be written in an hour of the clock).
397
+ const ownDenial = (e) => e.kind === 'denial' && e.problems.length === 0 && e.slip === slip && (e.pass ?? null) === (pass ?? null);
398
+ const denialTimes = result.entries.filter(ownDenial).map((e) => parseTime(e.content.when));
399
+ // How many denials were not written because of the hourly limit, since the writer was opened.
400
+ let notWrittenDenials = 0;
363
401
  // What was given at opening: the first sound acknowledgement of this
364
402
  // writer's own agent that came with a cancellation is the one it goes on
365
403
  // handing back; a cancellation that the book does not hold waits to be
366
404
  // written.
405
+ // (Seals held elsewhere, handed over with the options, come after the cancellations, and are not these.)
367
406
  result.held.forEach((h, i) => {
368
- if (!h.used) return;
407
+ if (h.kind !== 'cancellation' || !h.used) return;
369
408
  // The person's cancellation of this writer's own slip, given when it was opened, stops it from the start.
370
409
  if (h.slip === slip) stopped = true;
371
410
  h.acknowledgements.forEach((a, j) => {
@@ -390,7 +429,7 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
390
429
  // holds without it, follows the cancellation into the book: it was still
391
430
  // waiting when the writer that signed it handed it back.
392
431
  result.held.forEach((h, i) => {
393
- if (h.used && h.slip === slip && h.inBook !== null && acknowledged.has(h.fingerprint) && !written.has(h.fingerprint)) waiting.set(h.fingerprint, given[i].cancellation);
432
+ if (h.kind === 'cancellation' && h.used && h.slip === slip && h.inBook !== null && acknowledged.has(h.fingerprint) && !written.has(h.fingerprint)) waiting.set(h.fingerprint, given[i].cancellation);
394
433
  });
395
434
  // The services the slip names, to check a countersignature before it goes into the book.
396
435
  const slipEntry = result.entries.find((e) => e.kind === 'slip' && e.fingerprint === slip);
@@ -446,6 +485,10 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
446
485
  detailsAfter: more.detailsAfter,
447
486
  hasApproval: more.approval !== undefined && more.approval !== null,
448
487
  approval: undefined,
488
+ // For "act": false writes no denial for this call. For "deny": who said no, and why.
489
+ denial: more.denial,
490
+ source: more.source,
491
+ codes: more.codes === undefined ? undefined : plain(more.codes, 'codes'),
449
492
  };
450
493
  if (call.hasApproval) {
451
494
  try {
@@ -615,6 +658,54 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
615
658
  return { fits: true, checked };
616
659
  };
617
660
 
661
+ // A denial: the agent's side states that it did not take an action, and
662
+ // why. It is written into the book only if the book still passes its check
663
+ // with it and it is not dated ahead of the clock; it is tried on a reader
664
+ // split off the book's own, as a stub is, because a denial changes nothing
665
+ // read before it. At most "denialsPerHour" are written in any hour, by
666
+ // the clock: past it, none is written and the writer counts it. Whatever
667
+ // happens, the answer says what was done; nothing is thrown, so a denial
668
+ // never stands in the way of what the writer does next. The request is the
669
+ // copy made when the call was made, and only its action, amount, service
670
+ // and documents are written: the documents are fingerprints, never the
671
+ // documents themselves.
672
+ const denialAnswer = (problem) => ({ written: false, problem, notWritten: notWrittenDenials });
673
+ const writeDenialLine = async (request, source, codes, when) => {
674
+ try {
675
+ const at = now();
676
+ const t = when ?? at;
677
+ if (!Number.isFinite(t)) return denialAnswer({ code: 'bad-field', message: 'when: does not give a time.' });
678
+ if (aheadOfClock(t) || behindClock(t)) {
679
+ return denialAnswer({ code: 'record-not-sound', message: 'The denial was not written: a denial is written when the action is refused, so its date must be within 300 seconds of the stub writer\'s clock.' });
680
+ }
681
+ const inHour = denialTimes.filter((d) => d > at - HOUR_MS).length;
682
+ if (inHour >= denialsPerHour) {
683
+ notWrittenDenials++;
684
+ return denialAnswer({
685
+ code: 'denials-over-limit',
686
+ message: `The denial was not written: this stub writer has written ${denialsPerHour} denials in the last hour, the most it may. Since it was opened, ${notWrittenDenials} were not written for that reason.`,
687
+ });
688
+ }
689
+ const fields = { slip, action: request.action, source, when: t };
690
+ for (const name of ['amount', 'with', 'details']) if (request[name] !== undefined) fields[name] = request[name];
691
+ if (pass) fields.pass = pass;
692
+ if (codes) fields.codes = codes;
693
+ const made = await writeDenial(fields, signing);
694
+ const line = entryLine({ denial: made.record });
695
+ if (utf8(line).length > MAX_LINE_BYTES) return denialAnswer({ code: 'too-large', message: 'The denial is longer than a line of a book may be.' });
696
+ const tried = forkCarried(carried);
697
+ await carryOn(tried, line + '\n');
698
+ const checked = (await standing(tried)).result;
699
+ if (checked.summary.problemFound) return denialAnswer({ code: 'record-not-sound', message: 'The denial was not written: with it, the book would not pass its check.' });
700
+ carried = promoteCarried(tried);
701
+ text += line + '\n';
702
+ denialTimes.push(at);
703
+ return { written: true, fingerprint: made.fingerprint, line, notWritten: notWrittenDenials };
704
+ } catch (e) {
705
+ return denialAnswer(problemFrom(e));
706
+ }
707
+ };
708
+
618
709
  // A cancellation held beside the book is written into the book as soon as
619
710
  // it can be: once the date it gives is no longer ahead of the clock. It
620
711
  // goes in as the entry it is, without the acknowledgements that came with
@@ -924,7 +1015,10 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
924
1015
  stop(problemFrom(e));
925
1016
  }
926
1017
  }
927
- if (!answer.allowed) return { done: false, answer };
1018
+ if (!answer.allowed) {
1019
+ if (!denials || call.denial === false) return { done: false, answer };
1020
+ return { done: false, answer, denial: await writeDenialLine(call.request, 'check', denialCodes(answer), undefined) };
1021
+ }
928
1022
  const outcome = await runInside(perform);
929
1023
  if (!hasLater) {
930
1024
  const stub = await commit(await countersigned(made, call), made, tried);
@@ -980,6 +1074,35 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
980
1074
  });
981
1075
  },
982
1076
 
1077
+ /**
1078
+ * Write a denial for an action that was not taken, where "act" did not
1079
+ * write one: the person refused, or gave no approval when asked
1080
+ * ("person"); a rule of the software around the agent, outside the slip,
1081
+ * said no ("rule"); or the check before acting said no, and the caller
1082
+ * asked "act" to write no denial for that call ("check", with the codes
1083
+ * of that answer: denialCodes gives them). Nothing is taken and no stub
1084
+ * is written. A writer opened without "denials: true" writes none.
1085
+ * @param {{action: string, amount?: object, with?: string, details?: object[]}} request what was asked for
1086
+ * @param {{source: 'check'|'person'|'rule', codes?: string[], when?: number|Date}} more "codes" only for "check";
1087
+ * "when" defaults to the clock, and must be within 300 seconds of it
1088
+ * @returns {Promise<{written: boolean, fingerprint?: string, line?: string, problem?: object, notWritten: number}>}
1089
+ * "notWritten": how many denials were not written because of the hourly limit since the writer was opened
1090
+ */
1091
+ deny(request, more = {}) {
1092
+ return calling(request, more, async (call) => {
1093
+ if (!denials) return denialAnswer({ code: 'bad-field', message: 'This stub writer was opened without "denials: true", so it writes no denial.' });
1094
+ if (call.source !== 'check' && call.source !== 'person' && call.source !== 'rule') return denialAnswer({ code: 'bad-field', message: 'source: must be "check", "person" or "rule".' });
1095
+ let at;
1096
+ try {
1097
+ at = now();
1098
+ } catch (e) {
1099
+ return denialAnswer(problemFrom(e));
1100
+ }
1101
+ await writeWaiting(at);
1102
+ return writeDenialLine(call.request, call.source, call.codes, call.when);
1103
+ });
1104
+ },
1105
+
983
1106
  /** The book as it now stands. */
984
1107
  book() {
985
1108
  return text;
@@ -1220,6 +1343,7 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
1220
1343
  // An added stub of this writer's own chain moves its place in the chain on.
1221
1344
  after = lastStub(whole.checked.entries);
1222
1345
  const last = whole.checked.entries.at(-1);
1346
+ if (ownDenial(last)) denialTimes.push(at);
1223
1347
  if (last.kind === 'cancellation' && last.slip === slip && last.verified === 'all') {
1224
1348
  stopped = true;
1225
1349
  return { acknowledgement: await acknowledge(last.fingerprint, JSON.parse(line).cancellation, true, at) };
@@ -1275,5 +1399,7 @@ export async function openRecorder({ book, slip, privateKeys, issuerKeys, pass,
1275
1399
  });
1276
1400
  },
1277
1401
  };
1402
+ // Whether this writer writes denials, as it was opened. It cannot be changed.
1403
+ Object.defineProperty(writer, 'denials', { value: denials, enumerable: true });
1278
1404
  return writer;
1279
1405
  }