provared 0.1.0 → 0.3.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/timestamp.js CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { TAG, badStamp, children, contentOf, ecdsaToRaw, expect, hexOf, readElement, timeOf, validateDer, wholeOf } from './der.js';
13
13
  import { Refusal, equalBytes, formatTime, sha256, toBase64url } from './encoding.js';
14
+ import { weakEd25519 } from './keys.js';
14
15
 
15
16
  /** The largest time-stamp, in bytes. */
16
17
  export const MAX_STAMP_BYTES = 12288;
@@ -107,21 +108,53 @@ function readStatement(bytes) {
107
108
  return { hash, stamped, time, accuracySeconds, critical };
108
109
  }
109
110
 
110
- // The size of an RSA key in a certificate: 2,048 to 8,192 bits, with a
111
- // small public number, so that checking a signature cannot be made slow.
112
- function checkRsaKey(bytes, keyBits) {
113
- const inner = bytes.subarray(keyBits.start + 1, keyBits.end);
114
- const key = expect(readElement(inner, 0), TAG.SEQUENCE, 'an RSA key');
115
- const [n, e] = children(inner, key, 2);
116
- expect(n, TAG.INTEGER, 'an RSA key');
117
- expect(e, TAG.INTEGER, 'an RSA key');
111
+ // The public key of a certificate, written exactly as its standard sets
112
+ // out, so that two checkers cannot disagree about whether it can be read:
113
+ // RSA as RFC 3279 section 2.3.1 sets out (the method's parameters empty,
114
+ // the key two positive whole numbers in strict DER, nothing after them);
115
+ // ECDSA as RFC 5480 section 2.2 (an uncompressed point of the named
116
+ // curve); Ed25519 as RFC 8410 section 4 (no parameters, 32 bytes). In each,
117
+ // the string of bits has no unused bits.
118
+ //
119
+ // An RSA key is 2,048 to 8,192 bits, with an odd public number from 3 to
120
+ // 2^32 - 1: a small number, so that checking a signature cannot be made
121
+ // slow, and never 1, under which anyone can make a signature that checks.
122
+ function checkKeyEncoding(bytes, certificate, method, size) {
123
+ const why = 'the public key of a certificate is not written as its standard sets out.';
124
+ const bits = certificate.keyBits;
125
+ if (bits.end - bits.start < 1 || bytes[bits.start] !== 0) throw badStamp(why);
126
+ const inner = bytes.subarray(bits.start + 1, bits.end);
127
+ const parameters = certificate.keyParameters;
128
+ if (method === 'ECDSA') {
129
+ if (inner.length !== 1 + 2 * size || inner[0] !== 0x04) throw badStamp(why);
130
+ return;
131
+ }
132
+ if (method === 'Ed25519') {
133
+ if (parameters !== null || inner.length !== 32) throw badStamp(why);
134
+ if (weakEd25519(inner)) throw invalid('A time-stamp was signed with an Ed25519 key under which anyone can sign, which is not accepted.');
135
+ return;
136
+ }
137
+ if (parameters === null || parameters.tag !== TAG.NULL) throw badStamp(why);
138
+ let n;
139
+ let e;
140
+ try {
141
+ const key = readElement(inner, 0);
142
+ if (key.tag !== TAG.SEQUENCE || key.end !== inner.length) throw new Error();
143
+ validateDer(inner, key);
144
+ const parts = children(inner, key, 3);
145
+ [n, e] = parts;
146
+ if (parts.length !== 2 || n.tag !== TAG.INTEGER || e.tag !== TAG.INTEGER || inner[n.start] & 0x80 || inner[e.start] & 0x80) throw new Error();
147
+ } catch {
148
+ throw badStamp(why);
149
+ }
118
150
  let modulus = contentOf(inner, n);
119
151
  if (modulus[0] === 0) modulus = modulus.subarray(1);
120
- const bits = modulus.length === 0 ? 0 : (modulus.length - 1) * 8 + (32 - Math.clz32(modulus[0]));
152
+ const length = modulus.length === 0 ? 0 : (modulus.length - 1) * 8 + (32 - Math.clz32(modulus[0]));
121
153
  let exponent = contentOf(inner, e);
122
154
  if (exponent[0] === 0) exponent = exponent.subarray(1);
123
- if (bits < 2048 || bits > 8192 || exponent.length === 0 || exponent.length > 4) {
124
- throw invalid('A time-stamp was signed with an RSA key of a size that is not accepted.');
155
+ const odd = exponent.length > 0 && (exponent[exponent.length - 1] & 1) === 1;
156
+ if (length < 2048 || length > 8192 || exponent.length === 0 || exponent.length > 4 || !odd || (exponent.length === 1 && exponent[0] < 3)) {
157
+ throw invalid('A time-stamp was signed with an RSA key whose size or public number is not accepted.');
125
158
  }
126
159
  }
127
160
 
@@ -148,6 +181,7 @@ function readCertificate(bytes, element) {
148
181
  notAfter: timeOf(bytes, validity[1]),
149
182
  spki: wholeOf(bytes, keyInfo),
150
183
  keyOid: keyAlgorithm.oid,
184
+ keyParameters: keyAlgorithm.parameters ?? null,
151
185
  keyBits: expect(keyParts[1], TAG.BIT_STRING, 'the public key of a certificate'),
152
186
  curveOid: keyAlgorithm.parameters && keyAlgorithm.parameters.tag === TAG.OID ? hexOf(bytes, keyAlgorithm.parameters) : null,
153
187
  };
@@ -169,11 +203,14 @@ function signingCertificateOf(bytes, value, v2) {
169
203
  return { hash, value: contentOf(bytes, expect(first[at], TAG.OCTET_STRING, 'the signing certificate attribute')) };
170
204
  }
171
205
 
206
+ const KIND_NOT_ACCEPTED = 'A time-stamp was signed with a kind of key that is not accepted: RSA, ECDSA on the curves P-256 and P-384, and Ed25519 are.';
207
+
172
208
  // Check the service's signature with the public key of its certificate.
173
209
  // Returns "valid", "invalid", or "unavailable" where this device has no
174
210
  // built-in support for the method. The fingerprint method is the one the
175
211
  // signer names (RFC 5652 section 5.4); a signing method that names another
176
- // is refused.
212
+ // is refused; a key of a kind that is not accepted is refused in words that
213
+ // say so.
177
214
  async function verifyWith(certificate, bytes, signatureOid, digestName, signature, signed, without) {
178
215
  const subtle = globalThis.crypto.subtle;
179
216
  let importParams;
@@ -182,13 +219,15 @@ async function verifyWith(certificate, bytes, signatureOid, digestName, signatur
182
219
  let raw = signature;
183
220
  if (certificate.keyOid === OID.rsaEncryption) {
184
221
  if (signatureOid !== OID.rsaEncryption && RSA_WITH[signatureOid] !== digestName) return 'invalid';
185
- checkRsaKey(bytes, certificate.keyBits);
222
+ checkKeyEncoding(bytes, certificate, 'RSA');
186
223
  method = 'RSA';
187
224
  importParams = { name: 'RSASSA-PKCS1-v1_5', hash: digestName };
188
225
  verifyParams = { name: 'RSASSA-PKCS1-v1_5' };
189
226
  } else if (certificate.keyOid === OID.ecPublicKey) {
190
227
  const curve = certificate.curveOid === OID.p256 ? ['P-256', 32] : certificate.curveOid === OID.p384 ? ['P-384', 48] : null;
191
- if (ECDSA_WITH[signatureOid] !== digestName || !curve) return 'invalid';
228
+ if (!curve) throw invalid(KIND_NOT_ACCEPTED);
229
+ if (ECDSA_WITH[signatureOid] !== digestName) return 'invalid';
230
+ checkKeyEncoding(bytes, certificate, 'ECDSA', curve[1]);
192
231
  method = 'ECDSA';
193
232
  importParams = { name: 'ECDSA', namedCurve: curve[0] };
194
233
  verifyParams = { name: 'ECDSA', hash: digestName };
@@ -197,12 +236,14 @@ async function verifyWith(certificate, bytes, signatureOid, digestName, signatur
197
236
  } catch {
198
237
  return 'invalid';
199
238
  }
200
- } else if (certificate.keyOid === OID.ed25519 && signatureOid === OID.ed25519) {
239
+ } else if (certificate.keyOid === OID.ed25519) {
240
+ if (signatureOid !== OID.ed25519) return 'invalid';
241
+ checkKeyEncoding(bytes, certificate, 'Ed25519');
201
242
  method = 'Ed25519';
202
243
  importParams = { name: 'Ed25519' };
203
244
  verifyParams = { name: 'Ed25519' };
204
245
  } else {
205
- return 'invalid';
246
+ throw invalid(KIND_NOT_ACCEPTED);
206
247
  }
207
248
  if (without.includes(method)) return 'unavailable';
208
249
  let key;
@@ -212,6 +253,8 @@ async function verifyWith(certificate, bytes, signatureOid, digestName, signatur
212
253
  // A device with no Ed25519 says so when the key is loaded.
213
254
  return method === 'Ed25519' && e && e.name === 'NotSupportedError' ? 'unavailable' : 'invalid';
214
255
  }
256
+ // An Ed25519 signature whose first half is a point of small order is refused, as everywhere (format description, section 3.8).
257
+ if (method === 'Ed25519' && (raw.length !== 64 || weakEd25519(raw.subarray(0, 32)))) return 'invalid';
215
258
  try {
216
259
  return (await subtle.verify(verifyParams, key, raw, signed)) ? 'valid' : 'invalid';
217
260
  } catch {
package/src/tools.js CHANGED
@@ -18,6 +18,11 @@ import * as f from './fields.js';
18
18
  /** How deep the arguments of a call may nest. */
19
19
  const MAX_ARGUMENT_DEPTH = 64;
20
20
 
21
+ // Calling a generator function does not run its body, so its stub would be
22
+ // written for an action not yet taken.
23
+ const GENERATORS = new Set(['[object GeneratorFunction]', '[object AsyncGeneratorFunction]']);
24
+ const NOT_A_GENERATOR = 'must not be a generator function: calling one does not run its body, so its stub would be written for an action not yet taken.';
25
+
21
26
  /**
22
27
  * A call through a recorded tool that was not run: the check before acting
23
28
  * did not allow it, or the record so far has a problem. "answer" is what
@@ -108,7 +113,7 @@ const needsOnlyApproval = (answer) => answer.problems.length === 0 && answer.bre
108
113
  * @param {object} recorder the stub writer (openRecorder)
109
114
  * @param {Record<string, object>} tools by name. For each tool:
110
115
  * - "action": the action name the slip uses for what this tool does;
111
- * - "run": the tool itself, a function of the arguments. It must not call the stub writer;
116
+ * - "run": the tool itself, a function of the arguments, not a generator function. It must not call the stub writer;
112
117
  * Each function is called by itself, without "this". "amount" and "with" must give their value at once, not a promise.
113
118
  * - "with" (optional): the id of the service the tool deals with, as the slip names it, or a function of the arguments that gives it;
114
119
  * - "amount" (optional): {unit, value}, or a function of the arguments that gives it;
@@ -143,6 +148,7 @@ export function recordTools(recorder, tools, options = {}) {
143
148
  // Each member is read once: what is checked is what is fixed.
144
149
  const fixed = { action: spec.action, run: spec.run, details: spec.details, countersign: spec.countersign, approve: spec.approve, amount: spec.amount, with: spec.with };
145
150
  if (typeof fixed.run !== 'function') throw f.fail(`tools.${name}.run`, 'must be a function.');
151
+ if (GENERATORS.has(Object.prototype.toString.call(fixed.run))) throw f.fail(`tools.${name}.run`, NOT_A_GENERATOR);
146
152
  f.actionName(fixed.action, `tools.${name}.action`);
147
153
  for (const member of ['countersign', 'approve']) {
148
154
  if (fixed[member] !== undefined && typeof fixed[member] !== 'function') throw f.fail(`tools.${name}.${member}`, 'must be a function.');