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/CHANGELOG.md +291 -2
- package/README.md +116 -645
- package/bin/provared-check.mjs +36 -1
- package/docs/guide.md +736 -0
- package/docs/threat-model.md +12 -0
- package/package.json +25 -2
- package/spec/provared-format.md +33 -4
- package/src/compare.js +10 -7
- package/src/dev.d.ts +54 -0
- package/src/dev.js +123 -0
- package/src/encoding.js +9 -1
- package/src/keys.js +24 -1
- package/src/recorder.js +32 -4
- package/src/signatures.js +5 -1
- package/src/slip.js +24 -11
- package/src/timestamp.js +59 -16
- package/src/tools.js +7 -1
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
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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.');
|