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.
- package/CHANGELOG.md +1062 -0
- package/LICENSE +202 -0
- package/LICENSE-CC-BY-4.0.txt +396 -0
- package/README.md +689 -0
- package/bin/provared-check.mjs +565 -0
- package/bin/provared-headers.mjs +110 -0
- package/docs/threat-model.md +997 -0
- package/net/headers.js +403 -0
- package/package.json +57 -0
- package/page/check-page.js +162 -0
- package/page/index.html +47 -0
- package/page/render.js +530 -0
- package/page/style.css +150 -0
- package/samples/office-supplies.expected.json +82 -0
- package/samples/office-supplies.jsonl +10 -0
- package/samples/office-supplies.show.json +59 -0
- package/spec/provared-format.md +1672 -0
- package/src/actions.js +90 -0
- package/src/approval.js +73 -0
- package/src/blockstamp.js +198 -0
- package/src/book.js +2232 -0
- package/src/check.d.ts +21 -0
- package/src/check.js +15 -0
- package/src/compare.js +276 -0
- package/src/cover.js +198 -0
- package/src/der.js +192 -0
- package/src/encoding.js +286 -0
- package/src/fields.js +94 -0
- package/src/guard.js +171 -0
- package/src/headers.js +380 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +37 -0
- package/src/jws.js +235 -0
- package/src/keys.js +149 -0
- package/src/pass.js +67 -0
- package/src/passkey-browser.d.ts +20 -0
- package/src/passkey-browser.js +171 -0
- package/src/recorder.js +1115 -0
- package/src/seal.js +133 -0
- package/src/service.js +124 -0
- package/src/signatures.js +147 -0
- package/src/slip.js +401 -0
- package/src/standing.js +181 -0
- package/src/stub.js +140 -0
- package/src/timestamp.js +351 -0
- package/src/tools.js +192 -0
- package/src/tree.js +143 -0
- package/src/types.d.ts +628 -0
- package/src/webauthn.js +119 -0
package/src/stub.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The Stub (the receipt for one action) and the countersignature.
|
|
2
|
+
// Format description, sections 5 and 6.
|
|
3
|
+
|
|
4
|
+
import { fingerprint, formatTime, randomId, toBase64url } from './encoding.js';
|
|
5
|
+
import * as f from './fields.js';
|
|
6
|
+
import { encodeContent, parseRecord, protectedHeaders, signingInput, withinSize, KINDS } from './jws.js';
|
|
7
|
+
import { KEY_SET_METHODS } from './keys.js';
|
|
8
|
+
import { sign } from './signatures.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Confirm every member of a stub's content (format description, section 5.1).
|
|
12
|
+
* @param {any} c
|
|
13
|
+
*/
|
|
14
|
+
export function validateStubContent(c) {
|
|
15
|
+
f.members(c, ['action', 'id', 'seq', 'slip', 'type', 'when'], ['amount', 'approval', 'details', 'pass', 'previous', 'terms', 'with'], 'stub');
|
|
16
|
+
f.id(c.id, 'id');
|
|
17
|
+
f.fingerprintText(c.slip, 'slip');
|
|
18
|
+
f.wholeNumber(c.seq, 'seq');
|
|
19
|
+
if (c.seq === 0) {
|
|
20
|
+
if (Object.hasOwn(c, 'previous')) throw f.fail('previous', 'the first stub under a slip names no stub before it.');
|
|
21
|
+
} else {
|
|
22
|
+
if (!Object.hasOwn(c, 'previous')) throw f.fail('previous', 'is missing.');
|
|
23
|
+
f.fingerprintText(c.previous, 'previous');
|
|
24
|
+
}
|
|
25
|
+
validateRequest(c);
|
|
26
|
+
if (Object.hasOwn(c, 'approval')) f.fingerprintText(c.approval, 'approval');
|
|
27
|
+
if (Object.hasOwn(c, 'pass')) f.fingerprintText(c.pass, 'pass');
|
|
28
|
+
if (Object.hasOwn(c, 'terms')) {
|
|
29
|
+
f.fingerprintText(c.terms, 'terms');
|
|
30
|
+
if (!Object.hasOwn(c, 'with')) throw f.fail('terms', 'a stub that relies on the terms of a service must name the service.');
|
|
31
|
+
}
|
|
32
|
+
f.time(c.when, 'when');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Confirm the members that say what was done, or what is asked for: the
|
|
37
|
+
* action, and where present the amount, the service and the documents. A
|
|
38
|
+
* stub, an approval and a refusal share them.
|
|
39
|
+
* @param {any} c
|
|
40
|
+
*/
|
|
41
|
+
export function validateRequest(c) {
|
|
42
|
+
f.actionName(c.action, 'action');
|
|
43
|
+
if (Object.hasOwn(c, 'amount')) {
|
|
44
|
+
f.members(c.amount, ['unit', 'value'], [], 'amount');
|
|
45
|
+
f.unit(c.amount.unit, 'amount.unit');
|
|
46
|
+
f.wholeNumber(c.amount.value, 'amount.value');
|
|
47
|
+
}
|
|
48
|
+
if (Object.hasOwn(c, 'with')) f.shortName(c.with, 'with');
|
|
49
|
+
if (Object.hasOwn(c, 'details')) {
|
|
50
|
+
f.list(c.details, 1, 32, 'details');
|
|
51
|
+
c.details.forEach((d, i) => {
|
|
52
|
+
f.members(d, ['name', 'sha256'], [], `details[${i}]`);
|
|
53
|
+
f.label(d.name, `details[${i}].name`);
|
|
54
|
+
f.fingerprintText(d.sha256, `details[${i}].sha256`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Confirm every member of a countersignature's content (format description,
|
|
61
|
+
* section 6).
|
|
62
|
+
* @param {any} c
|
|
63
|
+
*/
|
|
64
|
+
export function validateCountersignatureContent(c) {
|
|
65
|
+
f.members(c, ['stub', 'type', 'when'], [], 'countersignature');
|
|
66
|
+
f.fingerprintText(c.stub, 'stub');
|
|
67
|
+
f.time(c.when, 'when');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- writing ---
|
|
71
|
+
|
|
72
|
+
// Sign content with a key set: two signatures side by side, Ed25519 then
|
|
73
|
+
// ML-DSA-87, each over its own signing input (format description, 3.9).
|
|
74
|
+
export async function signWithKeySet(kind, content, privateKeys) {
|
|
75
|
+
const { contentBytes, payloadB64 } = encodeContent(content);
|
|
76
|
+
const headers = protectedHeaders(kind);
|
|
77
|
+
const signatures = [];
|
|
78
|
+
for (let i = 0; i < KEY_SET_METHODS.length; i++) {
|
|
79
|
+
const signature = await sign(KEY_SET_METHODS[i], privateKeys[i], signingInput(headers[i], payloadB64));
|
|
80
|
+
signatures.push({ protected: headers[i], signature: toBase64url(signature) });
|
|
81
|
+
}
|
|
82
|
+
return { record: withinSize({ payload: payloadB64, signatures }), fingerprint: await fingerprint(contentBytes) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Write and sign a stub.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} fields
|
|
89
|
+
* @param {string} fields.slip the fingerprint of the slip relied on
|
|
90
|
+
* @param {{seq: number, fingerprint: string}|null} fields.after the stub before this one under the same slip, or null for the first
|
|
91
|
+
* @param {string} fields.action
|
|
92
|
+
* @param {{unit: string, value: number}} [fields.amount]
|
|
93
|
+
* @param {string} [fields.with] the id of the service, as the slip names it
|
|
94
|
+
* @param {{name: string, sha256: string}[]} [fields.details] fingerprints of documents, never the documents
|
|
95
|
+
* @param {string} [fields.approval] the fingerprint of the person's approval of this one action
|
|
96
|
+
* @param {string} [fields.terms] the fingerprint of the service's terms for agents that the agent relied on
|
|
97
|
+
* @param {string} [fields.pass] for a helper agent: the fingerprint of the pass it acts under; "after" is then the stub before it under that pass
|
|
98
|
+
* @param {number|Date} [fields.when] defaults to now
|
|
99
|
+
* @param {CryptoKey[]} privateKeys the agent's private keys: Ed25519, then ML-DSA-87
|
|
100
|
+
* @returns {Promise<{record: object, fingerprint: string, seq: number}>}
|
|
101
|
+
*/
|
|
102
|
+
export async function writeStub(fields, privateKeys) {
|
|
103
|
+
const seq = fields.after ? fields.after.seq + 1 : 0;
|
|
104
|
+
const content = {
|
|
105
|
+
type: KINDS.stub.type,
|
|
106
|
+
id: fields.id ?? randomId(),
|
|
107
|
+
slip: fields.slip,
|
|
108
|
+
seq,
|
|
109
|
+
action: fields.action,
|
|
110
|
+
when: formatTime(fields.when ?? Date.now()),
|
|
111
|
+
};
|
|
112
|
+
if (fields.after) content.previous = fields.after.fingerprint;
|
|
113
|
+
if (fields.amount) content.amount = fields.amount;
|
|
114
|
+
if (fields.with) content.with = fields.with;
|
|
115
|
+
if (fields.details && fields.details.length) content.details = fields.details;
|
|
116
|
+
if (fields.approval) content.approval = fields.approval;
|
|
117
|
+
if (fields.terms) content.terms = fields.terms;
|
|
118
|
+
if (fields.pass) content.pass = fields.pass;
|
|
119
|
+
validateStubContent(content);
|
|
120
|
+
const signed = await signWithKeySet('stub', content, privateKeys);
|
|
121
|
+
return { ...signed, seq };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Countersign a stub: the service's statement "this happened with me".
|
|
126
|
+
*
|
|
127
|
+
* The service should check the stub before it signs. This function only
|
|
128
|
+
* reads the stub's fingerprint; it does not judge the stub.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} stubRecord the stub as the agent sent it
|
|
131
|
+
* @param {CryptoKey[]} privateKeys the service's private keys: Ed25519, then ML-DSA-87
|
|
132
|
+
* @param {number|Date} [when] defaults to now
|
|
133
|
+
* @returns {Promise<object>} the countersignature record
|
|
134
|
+
*/
|
|
135
|
+
export async function countersign(stubRecord, privateKeys, when) {
|
|
136
|
+
const stub = await parseRecord(stubRecord, 'stub');
|
|
137
|
+
const content = { type: KINDS.countersignature.type, stub: stub.fingerprint, when: formatTime(when ?? Date.now()) };
|
|
138
|
+
validateCountersignatureContent(content);
|
|
139
|
+
return (await signWithKeySet('countersignature', content, privateKeys)).record;
|
|
140
|
+
}
|
package/src/timestamp.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Checking an outside time-stamp: a signed statement by a time-stamp
|
|
2
|
+
// service that some fingerprint existed at some time.
|
|
3
|
+
//
|
|
4
|
+
// The standard is RFC 3161 (the time-stamp), carried in the signed-data
|
|
5
|
+
// layout of RFC 5652, signed with a key named by an X.509 certificate
|
|
6
|
+
// (RFC 5280). Format description, section 21.
|
|
7
|
+
//
|
|
8
|
+
// The checker is told which services it trusts, by the fingerprint of each
|
|
9
|
+
// service's certificate. It follows no chain of certificates and asks no
|
|
10
|
+
// outside party: the person checking decides whom to trust.
|
|
11
|
+
|
|
12
|
+
import { TAG, badStamp, children, contentOf, ecdsaToRaw, expect, hexOf, readElement, timeOf, validateDer, wholeOf } from './der.js';
|
|
13
|
+
import { Refusal, equalBytes, formatTime, sha256, toBase64url } from './encoding.js';
|
|
14
|
+
|
|
15
|
+
/** The largest time-stamp, in bytes. */
|
|
16
|
+
export const MAX_STAMP_BYTES = 12288;
|
|
17
|
+
|
|
18
|
+
/** The loosest a time-stamp may state its own time and still count, in seconds. */
|
|
19
|
+
export const MAX_ACCURACY_SECONDS = 300;
|
|
20
|
+
|
|
21
|
+
// Object identifiers, as the hexadecimal of their DER content.
|
|
22
|
+
const OID = {
|
|
23
|
+
signedData: '2a864886f70d010702',
|
|
24
|
+
tstInfo: '2a864886f70d0109100104',
|
|
25
|
+
contentType: '2a864886f70d010903',
|
|
26
|
+
messageDigest: '2a864886f70d010904',
|
|
27
|
+
signingCertificate: '2a864886f70d010910020c',
|
|
28
|
+
signingCertificateV2: '2a864886f70d010910022f',
|
|
29
|
+
sha1: '2b0e03021a',
|
|
30
|
+
sha256: '608648016503040201',
|
|
31
|
+
sha384: '608648016503040202',
|
|
32
|
+
sha512: '608648016503040203',
|
|
33
|
+
rsaEncryption: '2a864886f70d010101',
|
|
34
|
+
sha256WithRsa: '2a864886f70d01010b',
|
|
35
|
+
sha384WithRsa: '2a864886f70d01010c',
|
|
36
|
+
sha512WithRsa: '2a864886f70d01010d',
|
|
37
|
+
ecdsaWithSha256: '2a8648ce3d040302',
|
|
38
|
+
ecdsaWithSha384: '2a8648ce3d040303',
|
|
39
|
+
ecdsaWithSha512: '2a8648ce3d040304',
|
|
40
|
+
ecPublicKey: '2a8648ce3d0201',
|
|
41
|
+
p256: '2a8648ce3d030107',
|
|
42
|
+
p384: '2b81040022',
|
|
43
|
+
ed25519: '2b6570',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const HASHES = { [OID.sha256]: 'SHA-256', [OID.sha384]: 'SHA-384', [OID.sha512]: 'SHA-512' };
|
|
47
|
+
const RSA_WITH = { [OID.sha256WithRsa]: 'SHA-256', [OID.sha384WithRsa]: 'SHA-384', [OID.sha512WithRsa]: 'SHA-512' };
|
|
48
|
+
const ECDSA_WITH = { [OID.ecdsaWithSha256]: 'SHA-256', [OID.ecdsaWithSha384]: 'SHA-384', [OID.ecdsaWithSha512]: 'SHA-512' };
|
|
49
|
+
|
|
50
|
+
const invalid = (why) => new Refusal('stamp-invalid', why);
|
|
51
|
+
|
|
52
|
+
// An "AlgorithmIdentifier": a sequence that begins with an object identifier.
|
|
53
|
+
function algorithmOf(bytes, element, what) {
|
|
54
|
+
expect(element, TAG.SEQUENCE, what);
|
|
55
|
+
const parts = children(bytes, element, 2);
|
|
56
|
+
return { oid: hexOf(bytes, expect(parts[0], TAG.OID, what)), parameters: parts[1] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function digest(name, bytes) {
|
|
60
|
+
return new Uint8Array(await globalThis.crypto.subtle.digest(name, bytes));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The statement the service signed (RFC 3161 section 2.4.2, "TSTInfo").
|
|
64
|
+
function readStatement(bytes) {
|
|
65
|
+
const outer = expect(readElement(bytes, 0), TAG.SEQUENCE, 'the statement');
|
|
66
|
+
if (outer.end !== bytes.length) throw badStamp('the statement has bytes left over.');
|
|
67
|
+
validateDer(bytes, outer);
|
|
68
|
+
const parts = children(bytes, outer, 10);
|
|
69
|
+
const version = expect(parts[0], TAG.INTEGER, 'the version of the statement');
|
|
70
|
+
if (hexOf(bytes, version) !== '01') throw badStamp('the statement is of a version other than 1.');
|
|
71
|
+
expect(parts[1], TAG.OID, 'the policy');
|
|
72
|
+
const imprint = children(bytes, expect(parts[2], TAG.SEQUENCE, 'the fingerprint that was stamped'), 2);
|
|
73
|
+
const hash = algorithmOf(bytes, imprint[0], 'the fingerprint method').oid;
|
|
74
|
+
const stamped = contentOf(bytes, expect(imprint[1], TAG.OCTET_STRING, 'the fingerprint that was stamped'));
|
|
75
|
+
expect(parts[3], TAG.INTEGER, 'the serial number');
|
|
76
|
+
const time = timeOf(bytes, expect(parts[4], TAG.GENERALIZED_TIME, 'the time'));
|
|
77
|
+
|
|
78
|
+
// What may follow, each at most once and in this order: how exact the
|
|
79
|
+
// time is, whether stamps are strictly ordered, a number used once, the
|
|
80
|
+
// service's name, and extensions.
|
|
81
|
+
let i = 5;
|
|
82
|
+
let accuracySeconds = 0;
|
|
83
|
+
if (parts[i] && parts[i].tag === TAG.SEQUENCE) {
|
|
84
|
+
for (const a of children(bytes, parts[i], 3)) {
|
|
85
|
+
if (a.tag === TAG.INTEGER) {
|
|
86
|
+
const digits = contentOf(bytes, a);
|
|
87
|
+
if (digits.length > 4 || digits[0] & 0x80) throw badStamp('the statement says how exact its time is in a way that cannot be read.');
|
|
88
|
+
accuracySeconds = digits.reduce((n, b) => n * 256 + b, 0);
|
|
89
|
+
} else if (a.tag !== 0x80 && a.tag !== 0x81) {
|
|
90
|
+
throw badStamp('the statement says how exact its time is in a way that cannot be read.');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
if (parts[i] && parts[i].tag === TAG.BOOLEAN) i++;
|
|
96
|
+
if (parts[i] && parts[i].tag === TAG.INTEGER) i++;
|
|
97
|
+
if (parts[i] && parts[i].tag === TAG.CONTEXT_0) i++;
|
|
98
|
+
let critical = false;
|
|
99
|
+
if (parts[i] && parts[i].tag === TAG.CONTEXT_1) {
|
|
100
|
+
for (const extension of children(bytes, parts[i], 16)) {
|
|
101
|
+
const fields = children(bytes, expect(extension, TAG.SEQUENCE, 'an extension'), 3);
|
|
102
|
+
if (fields.some((x) => x.tag === TAG.BOOLEAN && bytes[x.start] === 0xff)) critical = true;
|
|
103
|
+
}
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
if (i !== parts.length) throw badStamp('the statement holds parts that are not in the order its standard sets out.');
|
|
107
|
+
return { hash, stamped, time, accuracySeconds, critical };
|
|
108
|
+
}
|
|
109
|
+
|
|
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');
|
|
118
|
+
let modulus = contentOf(inner, n);
|
|
119
|
+
if (modulus[0] === 0) modulus = modulus.subarray(1);
|
|
120
|
+
const bits = modulus.length === 0 ? 0 : (modulus.length - 1) * 8 + (32 - Math.clz32(modulus[0]));
|
|
121
|
+
let exponent = contentOf(inner, e);
|
|
122
|
+
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.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// What is needed from a certificate (RFC 5280 section 4.1): when it is in
|
|
129
|
+
// force, and its public key.
|
|
130
|
+
function readCertificate(bytes, element) {
|
|
131
|
+
const whole = wholeOf(bytes, expect(element, TAG.SEQUENCE, 'a certificate'));
|
|
132
|
+
const tbs = expect(children(bytes, element, 3)[0], TAG.SEQUENCE, 'the body of a certificate');
|
|
133
|
+
const parts = children(bytes, tbs, 12);
|
|
134
|
+
// The version, in [0], is absent only in the oldest kind of certificate.
|
|
135
|
+
let i = parts[0] && parts[0].tag === TAG.CONTEXT_0 ? 1 : 0;
|
|
136
|
+
expect(parts[i++], TAG.INTEGER, 'the serial number of a certificate');
|
|
137
|
+
expect(parts[i++], TAG.SEQUENCE, 'the signing method of a certificate');
|
|
138
|
+
expect(parts[i++], TAG.SEQUENCE, 'the issuer of a certificate');
|
|
139
|
+
const validity = children(bytes, expect(parts[i++], TAG.SEQUENCE, 'the dates of a certificate'), 2);
|
|
140
|
+
if (validity.length !== 2) throw badStamp('a certificate does not hold two dates.');
|
|
141
|
+
expect(parts[i++], TAG.SEQUENCE, 'the subject of a certificate');
|
|
142
|
+
const keyInfo = expect(parts[i], TAG.SEQUENCE, 'the public key of a certificate');
|
|
143
|
+
const keyParts = children(bytes, keyInfo, 2);
|
|
144
|
+
const keyAlgorithm = algorithmOf(bytes, keyParts[0], 'the kind of public key');
|
|
145
|
+
return {
|
|
146
|
+
whole,
|
|
147
|
+
notBefore: timeOf(bytes, validity[0]),
|
|
148
|
+
notAfter: timeOf(bytes, validity[1]),
|
|
149
|
+
spki: wholeOf(bytes, keyInfo),
|
|
150
|
+
keyOid: keyAlgorithm.oid,
|
|
151
|
+
keyBits: expect(keyParts[1], TAG.BIT_STRING, 'the public key of a certificate'),
|
|
152
|
+
curveOid: keyAlgorithm.parameters && keyAlgorithm.parameters.tag === TAG.OID ? hexOf(bytes, keyAlgorithm.parameters) : null,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The signed attribute that says which certificate signed (RFC 3161 section
|
|
157
|
+
// 2.4.2; RFC 2634 and RFC 5035): the fingerprint method and the fingerprint
|
|
158
|
+
// of the certificate.
|
|
159
|
+
function signingCertificateOf(bytes, value, v2) {
|
|
160
|
+
const certs = children(bytes, expect(children(bytes, expect(value, TAG.SEQUENCE, 'the signing certificate attribute'), 2)[0], TAG.SEQUENCE, 'the signing certificate attribute'), 8);
|
|
161
|
+
const first = children(bytes, expect(certs[0], TAG.SEQUENCE, 'the signing certificate attribute'), 3);
|
|
162
|
+
let at = 0;
|
|
163
|
+
let hash = v2 ? 'SHA-256' : 'SHA-1';
|
|
164
|
+
if (v2 && first[0] && first[0].tag === TAG.SEQUENCE) {
|
|
165
|
+
hash = HASHES[algorithmOf(bytes, first[0], 'the signing certificate attribute').oid];
|
|
166
|
+
if (!hash) throw invalid('A time-stamp names its certificate with a fingerprint method that is not accepted.');
|
|
167
|
+
at = 1;
|
|
168
|
+
}
|
|
169
|
+
return { hash, value: contentOf(bytes, expect(first[at], TAG.OCTET_STRING, 'the signing certificate attribute')) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check the service's signature with the public key of its certificate.
|
|
173
|
+
// Returns "valid", "invalid", or "unavailable" where this device has no
|
|
174
|
+
// built-in support for the method. The fingerprint method is the one the
|
|
175
|
+
// signer names (RFC 5652 section 5.4); a signing method that names another
|
|
176
|
+
// is refused.
|
|
177
|
+
async function verifyWith(certificate, bytes, signatureOid, digestName, signature, signed, without) {
|
|
178
|
+
const subtle = globalThis.crypto.subtle;
|
|
179
|
+
let importParams;
|
|
180
|
+
let verifyParams;
|
|
181
|
+
let method;
|
|
182
|
+
let raw = signature;
|
|
183
|
+
if (certificate.keyOid === OID.rsaEncryption) {
|
|
184
|
+
if (signatureOid !== OID.rsaEncryption && RSA_WITH[signatureOid] !== digestName) return 'invalid';
|
|
185
|
+
checkRsaKey(bytes, certificate.keyBits);
|
|
186
|
+
method = 'RSA';
|
|
187
|
+
importParams = { name: 'RSASSA-PKCS1-v1_5', hash: digestName };
|
|
188
|
+
verifyParams = { name: 'RSASSA-PKCS1-v1_5' };
|
|
189
|
+
} else if (certificate.keyOid === OID.ecPublicKey) {
|
|
190
|
+
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';
|
|
192
|
+
method = 'ECDSA';
|
|
193
|
+
importParams = { name: 'ECDSA', namedCurve: curve[0] };
|
|
194
|
+
verifyParams = { name: 'ECDSA', hash: digestName };
|
|
195
|
+
try {
|
|
196
|
+
raw = ecdsaToRaw(signature, curve[1]);
|
|
197
|
+
} catch {
|
|
198
|
+
return 'invalid';
|
|
199
|
+
}
|
|
200
|
+
} else if (certificate.keyOid === OID.ed25519 && signatureOid === OID.ed25519) {
|
|
201
|
+
method = 'Ed25519';
|
|
202
|
+
importParams = { name: 'Ed25519' };
|
|
203
|
+
verifyParams = { name: 'Ed25519' };
|
|
204
|
+
} else {
|
|
205
|
+
return 'invalid';
|
|
206
|
+
}
|
|
207
|
+
if (without.includes(method)) return 'unavailable';
|
|
208
|
+
let key;
|
|
209
|
+
try {
|
|
210
|
+
key = await subtle.importKey('spki', certificate.spki, importParams, false, ['verify']);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// A device with no Ed25519 says so when the key is loaded.
|
|
213
|
+
return method === 'Ed25519' && e && e.name === 'NotSupportedError' ? 'unavailable' : 'invalid';
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
return (await subtle.verify(verifyParams, key, raw, signed)) ? 'valid' : 'invalid';
|
|
217
|
+
} catch {
|
|
218
|
+
return 'invalid';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check one time-stamp.
|
|
224
|
+
*
|
|
225
|
+
* @param {Uint8Array} token the time-stamp, as the service returned it (the "TimeStampToken"); it is not changed
|
|
226
|
+
* @param {Uint8Array} fingerprintBytes the 32 bytes that must have been stamped
|
|
227
|
+
* @param {object} [options]
|
|
228
|
+
* @param {string[]} [options.trusted] the certificate fingerprints (SHA-256, base64url) of the services the checker trusts
|
|
229
|
+
* @param {string[]} [options.without] methods ("RSA", "ECDSA", "Ed25519") to treat as not built into this device
|
|
230
|
+
* @returns {Promise<{state: 'valid'|'untrusted'|'unavailable', when: string|null, time: number, authority: string|null}>}
|
|
231
|
+
* "valid": signed by a service the checker trusts. "untrusted": signed
|
|
232
|
+
* soundly, by a service the checker did not name. "unavailable": this
|
|
233
|
+
* device cannot check the service's signing method; no time is given.
|
|
234
|
+
* "authority" is the fingerprint of the certificate that signed. "when"
|
|
235
|
+
* is the stated time, to the second, rounded down.
|
|
236
|
+
* @throws {Refusal} "stamp-bad-data", "stamp-wrong-data" or "stamp-invalid"
|
|
237
|
+
*/
|
|
238
|
+
export async function checkStamp(token, fingerprintBytes, options) {
|
|
239
|
+
if (options === null || typeof options !== 'object') options = {};
|
|
240
|
+
const without = Array.isArray(options.without) ? options.without : [];
|
|
241
|
+
if (!(token instanceof Uint8Array) || token.length === 0 || token.length > MAX_STAMP_BYTES) {
|
|
242
|
+
throw badStamp('it is empty, or larger than 12,288 bytes.');
|
|
243
|
+
}
|
|
244
|
+
if (!(fingerprintBytes instanceof Uint8Array)) throw badStamp('there is no fingerprint to compare it with.');
|
|
245
|
+
|
|
246
|
+
// The outer wrapper, and the signed data inside it (RFC 5652 sections 3
|
|
247
|
+
// and 5.1). Every part is read, and must be strict DER.
|
|
248
|
+
const wrapper = expect(readElement(token, 0), TAG.SEQUENCE, 'the wrapper');
|
|
249
|
+
if (wrapper.end !== token.length) throw badStamp('it has bytes left over.');
|
|
250
|
+
validateDer(token, wrapper);
|
|
251
|
+
const outer = children(token, wrapper, 2);
|
|
252
|
+
if (outer.length !== 2 || hexOf(token, expect(outer[0], TAG.OID, 'the kind of content')) !== OID.signedData) throw badStamp('it is not signed data.');
|
|
253
|
+
const inner = children(token, expect(outer[1], TAG.CONTEXT_0, 'the signed data'), 1);
|
|
254
|
+
const parts = children(token, expect(inner[0], TAG.SEQUENCE, 'the signed data'), 6);
|
|
255
|
+
if (hexOf(token, expect(parts[0], TAG.INTEGER, 'the version')) !== '03') throw badStamp('the signed data is of a version other than 3.');
|
|
256
|
+
for (const method of children(token, expect(parts[1], TAG.SET, 'the list of fingerprint methods'), 8)) algorithmOf(token, method, 'a fingerprint method');
|
|
257
|
+
const enclosed = children(token, expect(parts[2], TAG.SEQUENCE, 'the enclosed content'), 2);
|
|
258
|
+
if (enclosed.length !== 2 || hexOf(token, expect(enclosed[0], TAG.OID, 'the kind of enclosed content')) !== OID.tstInfo) {
|
|
259
|
+
throw badStamp('what it encloses is not a time-stamp statement.');
|
|
260
|
+
}
|
|
261
|
+
const statementHolder = children(token, expect(enclosed[1], TAG.CONTEXT_0, 'the statement'), 1);
|
|
262
|
+
const statementBytes = contentOf(token, expect(statementHolder[0], TAG.OCTET_STRING, 'the statement'));
|
|
263
|
+
const statement = readStatement(statementBytes);
|
|
264
|
+
|
|
265
|
+
// The certificates, a list of withdrawn certificates (not used), and the one signer. Nothing else.
|
|
266
|
+
let next = 3;
|
|
267
|
+
const certificates = [];
|
|
268
|
+
if (parts[next] && parts[next].tag === TAG.CONTEXT_0) {
|
|
269
|
+
for (const c of children(token, parts[next], 8)) {
|
|
270
|
+
// Other kinds of certificate may sit here; only the ordinary kind is read.
|
|
271
|
+
if (c.tag === TAG.SEQUENCE) certificates.push(readCertificate(token, c));
|
|
272
|
+
}
|
|
273
|
+
next++;
|
|
274
|
+
}
|
|
275
|
+
if (parts[next] && parts[next].tag === TAG.CONTEXT_1) next++;
|
|
276
|
+
if (next !== parts.length - 1) throw badStamp('the signed data holds parts that are not in the order its standard sets out.');
|
|
277
|
+
const signers = children(token, expect(parts[next], TAG.SET, 'the signer'), 1);
|
|
278
|
+
const signer = children(token, expect(signers[0], TAG.SEQUENCE, 'the signer'), 7);
|
|
279
|
+
// Version 1 names the certificate by its issuer and number; version 3 by its key.
|
|
280
|
+
const signerVersion = hexOf(token, expect(signer[0], TAG.INTEGER, 'the version of the signer'));
|
|
281
|
+
if (!signer[1] || !((signerVersion === '01' && signer[1].tag === TAG.SEQUENCE) || (signerVersion === '03' && signer[1].tag === 0x80))) {
|
|
282
|
+
throw badStamp('the signer is not named as its standard sets out.');
|
|
283
|
+
}
|
|
284
|
+
const digestName = HASHES[algorithmOf(token, signer[2], 'the signer\'s fingerprint method').oid];
|
|
285
|
+
const attributes = expect(signer[3], TAG.CONTEXT_0, 'the signed attributes');
|
|
286
|
+
const signatureOid = algorithmOf(token, signer[4], 'the signing method').oid;
|
|
287
|
+
const signature = contentOf(token, expect(signer[5], TAG.OCTET_STRING, 'the signature'));
|
|
288
|
+
if (signer.length > 7 || (signer.length === 7 && signer[6].tag !== TAG.CONTEXT_1)) throw badStamp('the signer holds parts that are not in the order its standard sets out.');
|
|
289
|
+
|
|
290
|
+
// What was stamped must be the fingerprint the checker holds.
|
|
291
|
+
if (statement.hash !== OID.sha256 || !equalBytes(statement.stamped, fingerprintBytes)) {
|
|
292
|
+
throw new Refusal('stamp-wrong-data', 'A time-stamp was made for something else.');
|
|
293
|
+
}
|
|
294
|
+
if (statement.critical) throw invalid('A time-stamp carries an extension that it says must be understood, and this checker does not understand it.');
|
|
295
|
+
if (statement.accuracySeconds > MAX_ACCURACY_SECONDS) throw invalid('A time-stamp states its time too loosely: to no better than more than 300 seconds.');
|
|
296
|
+
|
|
297
|
+
// The signed attributes must name the statement (its kind and its
|
|
298
|
+
// fingerprint, RFC 5652 section 5.4) and the certificate that signed
|
|
299
|
+
// (RFC 3161 section 2.4.2).
|
|
300
|
+
if (!digestName) throw invalid('A time-stamp uses a fingerprint method that is not accepted.');
|
|
301
|
+
let kindNamed = false;
|
|
302
|
+
let digestNamed = false;
|
|
303
|
+
let named = null;
|
|
304
|
+
for (const attribute of children(token, attributes, 16)) {
|
|
305
|
+
const pair = children(token, expect(attribute, TAG.SEQUENCE, 'an attribute'), 2);
|
|
306
|
+
const name = hexOf(token, expect(pair[0], TAG.OID, 'the name of an attribute'));
|
|
307
|
+
const values = children(token, expect(pair[1], TAG.SET, 'the value of an attribute'), 4);
|
|
308
|
+
if (name === OID.contentType) {
|
|
309
|
+
if (kindNamed || values.length !== 1 || values[0].tag !== TAG.OID || hexOf(token, values[0]) !== OID.tstInfo) {
|
|
310
|
+
throw invalid('A time-stamp\'s signature does not cover a time-stamp statement.');
|
|
311
|
+
}
|
|
312
|
+
kindNamed = true;
|
|
313
|
+
} else if (name === OID.messageDigest) {
|
|
314
|
+
if (digestNamed || values.length !== 1 || values[0].tag !== TAG.OCTET_STRING || !equalBytes(contentOf(token, values[0]), await digest(digestName, statementBytes))) {
|
|
315
|
+
throw invalid('A time-stamp\'s signature does not cover its statement.');
|
|
316
|
+
}
|
|
317
|
+
digestNamed = true;
|
|
318
|
+
} else if (name === OID.signingCertificateV2 || name === OID.signingCertificate) {
|
|
319
|
+
if (values.length !== 1) throw invalid('A time-stamp does not say, in one way, which certificate signed it.');
|
|
320
|
+
// Where both forms are present, the newer one is used.
|
|
321
|
+
if (named === null || name === OID.signingCertificateV2) named = signingCertificateOf(token, values[0], name === OID.signingCertificateV2);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!kindNamed || !digestNamed) throw invalid('A time-stamp\'s signature does not cover its statement.');
|
|
325
|
+
if (named === null) throw invalid('A time-stamp does not say which certificate signed it.');
|
|
326
|
+
|
|
327
|
+
// The one certificate the signed attributes name. A copy with the same
|
|
328
|
+
// key and other contents has another fingerprint, and is not it.
|
|
329
|
+
let certificate = null;
|
|
330
|
+
for (const c of certificates) {
|
|
331
|
+
if (equalBytes(await digest(named.hash, c.whole), named.value)) {
|
|
332
|
+
certificate = c;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!certificate) throw invalid(certificates.length ? 'A time-stamp does not carry the certificate it says signed it.' : 'A time-stamp carries no certificate to check it with.');
|
|
337
|
+
|
|
338
|
+
// The signature covers the attributes, written as a set (RFC 5652 section
|
|
339
|
+
// 5.4). The bytes are copied, so that the caller's time-stamp is not changed.
|
|
340
|
+
const signed = new Uint8Array(wholeOf(token, attributes));
|
|
341
|
+
signed[0] = TAG.SET;
|
|
342
|
+
const state = await verifyWith(certificate, token, signatureOid, digestName, signature, signed, without);
|
|
343
|
+
if (state === 'unavailable') return { state: 'unavailable', when: null, time: NaN, authority: null };
|
|
344
|
+
if (state !== 'valid') throw invalid('A time-stamp\'s signature does not fit the certificate it names.');
|
|
345
|
+
if (statement.time < certificate.notBefore || statement.time > certificate.notAfter) {
|
|
346
|
+
throw invalid('A time-stamp is dated outside the time its service\'s certificate was in force.');
|
|
347
|
+
}
|
|
348
|
+
const authority = toBase64url(await sha256(certificate.whole));
|
|
349
|
+
const trusted = Array.isArray(options.trusted) && options.trusted.includes(authority);
|
|
350
|
+
return { state: trusted ? 'valid' : 'untrusted', when: formatTime(statement.time), time: statement.time, authority };
|
|
351
|
+
}
|