@typedstandards/verify-core 0.10.0 → 0.12.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 +101 -0
- package/dist/checks.d.ts +41 -0
- package/dist/checks.js +65 -4
- package/dist/rfc3161.d.ts +35 -4
- package/dist/rfc3161.js +89 -25
- package/dist/verify.d.ts +5 -1
- package/dist/verify.js +54 -4
- package/dist/x509.d.ts +3 -2
- package/dist/x509.js +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,107 @@ references are to `npstorey/civic-ai-tools-website` (#119 is the offline-crypto
|
|
|
6
6
|
hardening arc; #116 is the standalone-verifier arc this package was extracted
|
|
7
7
|
in).
|
|
8
8
|
|
|
9
|
+
## 0.12.0 — 2026-09-24
|
|
10
|
+
|
|
11
|
+
The certificate-chain split (typedstandards#100, shipped in #106). **A minor bump** — `ChainFailReason`
|
|
12
|
+
and `Rfc3161FailReason` each gain a member, so a consumer with an exhaustive `switch` or `Record` over
|
|
13
|
+
either needs the new case.
|
|
14
|
+
|
|
15
|
+
- **A chain link signed with an algorithm the validator does not implement is no longer reported as an
|
|
16
|
+
invalid signature.** `verifyCertChainToAnchor` checks each link's signature algorithm at the step
|
|
17
|
+
where it verifies the link's signature:
|
|
18
|
+
- an algorithm it does not implement (anything but RSASSA-PKCS1-v1_5 with SHA-256, SHA-384 or
|
|
19
|
+
SHA-512; an ECDSA-signed intermediate, say) reads the new reason `link_algorithm_unsupported`. It
|
|
20
|
+
used to read `link_signature_invalid`;
|
|
21
|
+
- an RSA link whose signature does not verify keeps `link_signature_invalid`;
|
|
22
|
+
- no other reason changes order, and `verifyCertSignatureRsa` is unchanged.
|
|
23
|
+
- **`verifyRfc3161Timestamp` reports that link as `chain_algorithm_unsupported`**, a new reason, where
|
|
24
|
+
it used to report `chain_signature_invalid`. `RFC3161_FAIL_REASONS` holds fourteen codes.
|
|
25
|
+
`chain_signature_invalid` now means only a link whose signature does not verify: a fault in the
|
|
26
|
+
token, not a limit of the verifier. `verified` is unchanged, and no request is added.
|
|
27
|
+
- **On typedstandards.org** (the site's verifier, live since #106; not part of this package's API):
|
|
28
|
+
- **a timestamp whose chain has a link with an invalid signature (`chain_signature_invalid`) now
|
|
29
|
+
fails the package, where it was a caveat.** `chain_algorithm_unsupported` is a caveat, as the
|
|
30
|
+
verifier's other policy limits are, and the #7 row reads amber for every caveat-class reason
|
|
31
|
+
(typedstandards#104);
|
|
32
|
+
- **bundle mode requests nothing.** A fully offline run gives `verifyRecord` a fetcher that sends no
|
|
33
|
+
request, so a file the package references is not fetched and reads "not checked offline", and no
|
|
34
|
+
transparency-log lookup is sent (typedstandards#105). A consumer that wants the same passes such a
|
|
35
|
+
fetcher as `VerifyDeps.fetch`.
|
|
36
|
+
|
|
37
|
+
## 0.11.0 — 2026-09-22
|
|
38
|
+
|
|
39
|
+
The verifier follow-ons (anchored at `typedstandards#98`): specification check #6, the ratified
|
|
40
|
+
`attestation/revises/v1` (specification v0.1.10), one fetch per referenced file, and a timestamp result
|
|
41
|
+
that tells a token that does not verify from one whose only fault is its chain. **A minor bump** — new
|
|
42
|
+
exports, `VerifyResult` gains a field, and `Rfc3161FailReason` gains a member, so a consumer that
|
|
43
|
+
builds a `VerifyResult` literal (a test double, say), or has an exhaustive `switch` or `Record` over
|
|
44
|
+
the reasons, needs the new cases.
|
|
45
|
+
|
|
46
|
+
- **Check #6, `metadata.signingKeyId` consistency (`checkSigningKeyIdConsistency`,
|
|
47
|
+
`SIGNING_KEY_ID_CONSISTENCY_STATUSES`; typedstandards#88).** Specification §9.2 check #6 was
|
|
48
|
+
implemented nowhere. It compares the signature envelope's `kid` with the package's
|
|
49
|
+
`metadata.signingKeyId` and reports one of:
|
|
50
|
+
- `ok` — both present and equal;
|
|
51
|
+
- `signingKeyId_mismatch` — both present and different; the one failing status (§8.3.1: the field
|
|
52
|
+
MUST equal the envelope `kid`). The result carries `kid` and `signingKeyId`;
|
|
53
|
+
- `kid_absent` — the envelope carries no `kid`; legitimate under a key-derived identifier, and on
|
|
54
|
+
legacy packages, which carry neither field;
|
|
55
|
+
- `signingKeyId_absent` — a `kid` that the signed bytes do not name, so the check cannot confirm
|
|
56
|
+
it.
|
|
57
|
+
|
|
58
|
+
`signer.identifier` is not compared: under a key-derived identifier the `kid` SHOULD be the
|
|
59
|
+
identifier, which is not a condition of this check. `VerifyResult` gains `signingKeyIdConsistency`,
|
|
60
|
+
null when there is no package, no signature envelope, or a malformed one. The captured and
|
|
61
|
+
published packages measured for this change (the three captured commitment bundles, the
|
|
62
|
+
self-certified interop fixture, the two ADR-0028 eval-run packages, and two published example
|
|
63
|
+
bundles under a key-derived identifier) read `ok`, except the legacy capture, which carries neither
|
|
64
|
+
field and reads `kid_absent`.
|
|
65
|
+
- **Check #12 registers `attestation/revises/v1` (typedstandards#96).** The ratified set is sixteen
|
|
66
|
+
attestation sub-types plus `content/analysis/v1`; a node of that type read `unknown_type` and now
|
|
67
|
+
reads `ok`.
|
|
68
|
+
- **One fetch per BlobRef URL per `verifyRecord` (typedstandards#90).** Under `raw-bytes/v1` with a
|
|
69
|
+
BlobRef `output`, checks #9 and #4 each fetched the file. They now share one request per URL within a
|
|
70
|
+
`verifyRecord` call, and report exactly what they reported before, including when the fetch fails and
|
|
71
|
+
when the bytes do not match. No public signature changes, and no request is added.
|
|
72
|
+
- **Check #7 evaluates the TSA signature when the chain does not reach a pinned root
|
|
73
|
+
(typedstandards#94).** `verifyRfc3161Timestamp` used to return at the chain step, so for a token
|
|
74
|
+
whose chain did not reach a pinned anchor `signatureValid` read `null` whether the signature was
|
|
75
|
+
genuine or not. It now reads `true` or `false` there as well. For a consumer:
|
|
76
|
+
- `signatureValid` is `null` when an earlier step returned first (`parse_error`,
|
|
77
|
+
`unexpected_algorithm`, `imprint_mismatch`, `no_message_digest`, `content_not_bound`,
|
|
78
|
+
`no_signing_cert`, `eku_not_timestamping`), or when the signing cert's key is not a P-384 key.
|
|
79
|
+
`true` means the signature verifies under the embedded signing cert's key; that key is trusted
|
|
80
|
+
only when `chainVerified` is `true`. `false` means a P-384 key under which the signature does
|
|
81
|
+
not verify, including a malformed signature encoding;
|
|
82
|
+
- `reason` reports the token's own faults ahead of the chain's: `genTime_outside_validity`, then
|
|
83
|
+
`unexpected_algorithm` for a signing-cert key that is not P-384, then `signature_invalid`, then
|
|
84
|
+
the chain reason. A token with an unpinned chain and a signature that does not verify used to
|
|
85
|
+
read `untrusted_root` (or another chain reason) and now reads `signature_invalid`. A signing-cert
|
|
86
|
+
key that is not P-384 used to read the chain reason, or `signature_invalid` under a pinned chain,
|
|
87
|
+
and now reads `unexpected_algorithm`. `untrusted_root`, `chain_incomplete`,
|
|
88
|
+
`chain_signature_invalid` and `chain_outside_validity` are now reported only for a token whose
|
|
89
|
+
TSA signature verifies and whose `genTime` is within the signing cert's validity;
|
|
90
|
+
- **new reason `chain_outside_validity`:** an intermediate or root certificate not valid at
|
|
91
|
+
`genTime`, a chain fault. `genTime_outside_validity` now means only the signing cert. A token
|
|
92
|
+
whose signing cert is valid at `genTime` and whose intermediate or root is not used to read
|
|
93
|
+
`genTime_outside_validity` and now reads `chain_outside_validity`;
|
|
94
|
+
- **the message imprint is compared before the SignerInfo algorithms:** a token that does not bind
|
|
95
|
+
this package's hash reads `imprint_mismatch` whatever algorithms it uses. One that did so with a
|
|
96
|
+
digest or signature algorithm outside the checked set used to read `unexpected_algorithm`, and
|
|
97
|
+
`imprintMatches` was `null` for any `unexpected_algorithm` result from that check; it is now
|
|
98
|
+
`true` there;
|
|
99
|
+
- each reason now names one kind of fault, so a consumer can classify a token by `reason` alone;
|
|
100
|
+
- `verified` is unchanged: `true` only for a fully verified token chained to a pinned anchor. No
|
|
101
|
+
request is added.
|
|
102
|
+
- **`KNOWN_TYPE_URIS` is exported.** The type URIs check #12 resolves `ok` (`content/analysis/v1` and
|
|
103
|
+
the sixteen attestation sub-types), frozen, so a check outside this package can compare them with the
|
|
104
|
+
specification's list.
|
|
105
|
+
- **`RFC3161_FAIL_REASONS` is exported.** The thirteen reasons `verifyRfc3161Timestamp` can return,
|
|
106
|
+
as a frozen array; `Rfc3161FailReason` is now derived from it. It gains one member,
|
|
107
|
+
`chain_outside_validity`, so a consumer with an exhaustive `switch` or `Record` over it needs the
|
|
108
|
+
new case.
|
|
109
|
+
|
|
9
110
|
## 0.10.0 — 2026-09-22
|
|
10
111
|
|
|
11
112
|
Specification v0.1.9 (hub ADR-0029 and ADR-0030, anchored at `typedstandards#77`): a Producer Profile
|
package/dist/checks.d.ts
CHANGED
|
@@ -102,6 +102,7 @@ export interface ContentProfileCheck {
|
|
|
102
102
|
* integrity failure: both labels are signature-covered.
|
|
103
103
|
*/
|
|
104
104
|
export declare function checkContentProfile(pkg: Record<string, unknown>): ContentProfileCheck;
|
|
105
|
+
export declare const KNOWN_TYPE_URIS: readonly string[];
|
|
105
106
|
export declare const TYPE_RESOLUTION_STATUSES: readonly ["ok", "implicit", "unknown_type"];
|
|
106
107
|
export type TypeResolutionStatus = (typeof TYPE_RESOLUTION_STATUSES)[number];
|
|
107
108
|
export interface TypeResolution {
|
|
@@ -115,6 +116,46 @@ export interface TypeResolution {
|
|
|
115
116
|
* and renders as such — it does NOT fail verification.
|
|
116
117
|
*/
|
|
117
118
|
export declare function resolvePackageType(pkg: Record<string, unknown>): TypeResolution;
|
|
119
|
+
export declare const SIGNING_KEY_ID_CONSISTENCY_STATUSES: readonly ["ok", "signingKeyId_mismatch", "kid_absent", "signingKeyId_absent"];
|
|
120
|
+
export type SigningKeyIdConsistencyStatus = (typeof SIGNING_KEY_ID_CONSISTENCY_STATUSES)[number];
|
|
121
|
+
export interface SigningKeyIdConsistencyCheck {
|
|
122
|
+
status: SigningKeyIdConsistencyStatus;
|
|
123
|
+
/** On `signingKeyId_mismatch`: the signature envelope's `kid`. */
|
|
124
|
+
kid?: string;
|
|
125
|
+
/** On `signingKeyId_mismatch`: `metadata.signingKeyId` as carried, when it is
|
|
126
|
+
* a string. */
|
|
127
|
+
signingKeyId?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check #6 — the signature envelope's `kid` against `metadata.signingKeyId`
|
|
131
|
+
* (spec §9.2 check #6, §8.3.1). The package's `signingKeyId` is inside the
|
|
132
|
+
* signed bytes and the envelope's `kid` is not, so a `kid` swapped on the
|
|
133
|
+
* envelope after signing leaves the signature valid; this comparison is what
|
|
134
|
+
* detects it.
|
|
135
|
+
* - both present and equal → `ok`.
|
|
136
|
+
* - both present and different → `signingKeyId_mismatch`, the one failing
|
|
137
|
+
* status: §8.3.1 says `metadata.signingKeyId` MUST equal the envelope's
|
|
138
|
+
* `kid`, and the spec reads a mismatch as envelope-vs-canonical drift. The
|
|
139
|
+
* comparison is exact (no case or whitespace folding), and a present value
|
|
140
|
+
* that is not a string is a mismatch.
|
|
141
|
+
* - the envelope carries no `kid` → `kid_absent`: there is nothing to compare.
|
|
142
|
+
* Under a key-derived `signer.identifier` (§8.5.1) `kid` is optional, and
|
|
143
|
+
* legacy packages carry neither field; for a registry signer, check #5
|
|
144
|
+
* carries the consequence of a missing `kid`.
|
|
145
|
+
* - the envelope carries a `kid` and the package no `metadata.signingKeyId`
|
|
146
|
+
* → `signingKeyId_absent`: the `kid` is bound by nothing signed, so the
|
|
147
|
+
* check cannot confirm it. The package does not meet §8.3.1, which requires
|
|
148
|
+
* the field; that is not evidence the envelope was changed.
|
|
149
|
+
* `undefined`, `null` and `''` count as absent on either side. Only a mismatch
|
|
150
|
+
* carries the two values, which are then the finding; otherwise they are
|
|
151
|
+
* `VerifyResult.kid` and the package's own field.
|
|
152
|
+
*
|
|
153
|
+
* `signer.identifier` is not read. Under a key-derived identifier the `kid`
|
|
154
|
+
* SHOULD be the identifier string (§8.3.1); that is not a condition of this
|
|
155
|
+
* check, and for a registry signer `kid` and `signer.identifier` differ by
|
|
156
|
+
* design. The identity cross-check is check #14's.
|
|
157
|
+
*/
|
|
158
|
+
export declare function checkSigningKeyIdConsistency(pkg: Record<string, unknown>, kid: string | undefined): SigningKeyIdConsistencyCheck;
|
|
118
159
|
export declare const SIGNER_IDENTITY_CHECK_STATUSES: readonly ["ok", "signer_identity_mismatch", "no_signer", "no_registry_identity", "key_derived_match", "key_derived_mismatch"];
|
|
119
160
|
export type SignerIdentityCheckStatus = (typeof SIGNER_IDENTITY_CHECK_STATUSES)[number];
|
|
120
161
|
export interface SignerIdentityCheck {
|
package/dist/checks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Content-canonicalization, content-hash, and typed-standards envelope checks
|
|
2
|
-
// (spec §9.2 checks #3, #4, #12, #13, #14, #15, #16) + the package-level
|
|
2
|
+
// (spec §9.2 checks #3, #4, #6, #12, #13, #14, #15, #16) + the package-level
|
|
3
3
|
// blob-ref walker (#9) — browser-safe.
|
|
4
4
|
//
|
|
5
5
|
// Factored verbatim from the server `verify.ts` (WS2); these functions were
|
|
@@ -247,12 +247,16 @@ export function checkContentProfile(pkg) {
|
|
|
247
247
|
// v0.1 ratified node type URIs (spec §8.12.1). Recognized so check #12 resolves
|
|
248
248
|
// them; only `content/analysis/v1` (+ the lifecycle sub-types) are operationalized
|
|
249
249
|
// today, but the full ratified set is registered so conformant packages don't
|
|
250
|
-
// render as `unknown_type`.
|
|
251
|
-
|
|
250
|
+
// render as `unknown_type`. The sixteen attestation sub-types are in the order
|
|
251
|
+
// of the §8.12.1 table; `type-resolution.test.ts` holds that table's copy.
|
|
252
|
+
// Exported and frozen so a check outside this package can compare the registered
|
|
253
|
+
// set with the specification's list without being able to change it.
|
|
254
|
+
export const KNOWN_TYPE_URIS = Object.freeze([
|
|
252
255
|
'content/analysis/v1',
|
|
253
256
|
'attestation/withdraws/v1',
|
|
254
257
|
'attestation/reinstates/v1',
|
|
255
258
|
'attestation/supersedes/v1',
|
|
259
|
+
'attestation/revises/v1',
|
|
256
260
|
'attestation/publishes/v1',
|
|
257
261
|
'attestation/locatedAt/v1',
|
|
258
262
|
'attestation/corroborates/v1',
|
|
@@ -265,7 +269,7 @@ const KNOWN_TYPE_URIS = [
|
|
|
265
269
|
'attestation/certifies/v1',
|
|
266
270
|
'attestation/evaluates/v1',
|
|
267
271
|
'attestation/conforms/v1',
|
|
268
|
-
];
|
|
272
|
+
]);
|
|
269
273
|
export const TYPE_RESOLUTION_STATUSES = ['ok', 'implicit', 'unknown_type'];
|
|
270
274
|
/**
|
|
271
275
|
* Check #12 — `type` resolution. An absent field resolves to the implicit
|
|
@@ -282,6 +286,63 @@ export function resolvePackageType(pkg) {
|
|
|
282
286
|
}
|
|
283
287
|
return { status: 'unknown_type', type: raw };
|
|
284
288
|
}
|
|
289
|
+
// --- Envelope kid ↔ metadata.signingKeyId (spec §9.2 check #6) ---
|
|
290
|
+
export const SIGNING_KEY_ID_CONSISTENCY_STATUSES = [
|
|
291
|
+
'ok',
|
|
292
|
+
'signingKeyId_mismatch',
|
|
293
|
+
'kid_absent',
|
|
294
|
+
'signingKeyId_absent',
|
|
295
|
+
];
|
|
296
|
+
const isAbsent = (v) => v === undefined || v === null || v === '';
|
|
297
|
+
/**
|
|
298
|
+
* Check #6 — the signature envelope's `kid` against `metadata.signingKeyId`
|
|
299
|
+
* (spec §9.2 check #6, §8.3.1). The package's `signingKeyId` is inside the
|
|
300
|
+
* signed bytes and the envelope's `kid` is not, so a `kid` swapped on the
|
|
301
|
+
* envelope after signing leaves the signature valid; this comparison is what
|
|
302
|
+
* detects it.
|
|
303
|
+
* - both present and equal → `ok`.
|
|
304
|
+
* - both present and different → `signingKeyId_mismatch`, the one failing
|
|
305
|
+
* status: §8.3.1 says `metadata.signingKeyId` MUST equal the envelope's
|
|
306
|
+
* `kid`, and the spec reads a mismatch as envelope-vs-canonical drift. The
|
|
307
|
+
* comparison is exact (no case or whitespace folding), and a present value
|
|
308
|
+
* that is not a string is a mismatch.
|
|
309
|
+
* - the envelope carries no `kid` → `kid_absent`: there is nothing to compare.
|
|
310
|
+
* Under a key-derived `signer.identifier` (§8.5.1) `kid` is optional, and
|
|
311
|
+
* legacy packages carry neither field; for a registry signer, check #5
|
|
312
|
+
* carries the consequence of a missing `kid`.
|
|
313
|
+
* - the envelope carries a `kid` and the package no `metadata.signingKeyId`
|
|
314
|
+
* → `signingKeyId_absent`: the `kid` is bound by nothing signed, so the
|
|
315
|
+
* check cannot confirm it. The package does not meet §8.3.1, which requires
|
|
316
|
+
* the field; that is not evidence the envelope was changed.
|
|
317
|
+
* `undefined`, `null` and `''` count as absent on either side. Only a mismatch
|
|
318
|
+
* carries the two values, which are then the finding; otherwise they are
|
|
319
|
+
* `VerifyResult.kid` and the package's own field.
|
|
320
|
+
*
|
|
321
|
+
* `signer.identifier` is not read. Under a key-derived identifier the `kid`
|
|
322
|
+
* SHOULD be the identifier string (§8.3.1); that is not a condition of this
|
|
323
|
+
* check, and for a registry signer `kid` and `signer.identifier` differ by
|
|
324
|
+
* design. The identity cross-check is check #14's.
|
|
325
|
+
*/
|
|
326
|
+
export function checkSigningKeyIdConsistency(pkg, kid) {
|
|
327
|
+
const metadata = pkg['metadata'];
|
|
328
|
+
const signingKeyId = typeof metadata === 'object' && metadata !== null && !Array.isArray(metadata)
|
|
329
|
+
? metadata['signingKeyId']
|
|
330
|
+
: undefined;
|
|
331
|
+
if (kid === undefined || isAbsent(kid)) {
|
|
332
|
+
return { status: 'kid_absent' };
|
|
333
|
+
}
|
|
334
|
+
if (isAbsent(signingKeyId)) {
|
|
335
|
+
return { status: 'signingKeyId_absent' };
|
|
336
|
+
}
|
|
337
|
+
if (signingKeyId === kid) {
|
|
338
|
+
return { status: 'ok' };
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
status: 'signingKeyId_mismatch',
|
|
342
|
+
kid,
|
|
343
|
+
...(typeof signingKeyId === 'string' ? { signingKeyId } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
285
346
|
export const SIGNER_IDENTITY_CHECK_STATUSES = [
|
|
286
347
|
'ok',
|
|
287
348
|
'signer_identity_mismatch',
|
package/dist/rfc3161.d.ts
CHANGED
|
@@ -10,7 +10,8 @@ export interface TsaRootAnchor {
|
|
|
10
10
|
rootKeyDer: string;
|
|
11
11
|
}
|
|
12
12
|
export declare const FREETSA_ROOT_ANCHORS: readonly TsaRootAnchor[];
|
|
13
|
-
export
|
|
13
|
+
export declare const RFC3161_FAIL_REASONS: readonly ["parse_error", "unexpected_algorithm", "no_message_digest", "content_not_bound", "imprint_mismatch", "no_signing_cert", "eku_not_timestamping", "genTime_outside_validity", "chain_incomplete", "chain_signature_invalid", "chain_algorithm_unsupported", "chain_outside_validity", "untrusted_root", "signature_invalid"];
|
|
14
|
+
export type Rfc3161FailReason = (typeof RFC3161_FAIL_REASONS)[number];
|
|
14
15
|
export interface Rfc3161VerifyResult {
|
|
15
16
|
/** All checks passed: a chain-trusted TSA signed this package hash at genTime. */
|
|
16
17
|
verified: boolean;
|
|
@@ -22,18 +23,48 @@ export interface Rfc3161VerifyResult {
|
|
|
22
23
|
ekuTimestamping: boolean | null;
|
|
23
24
|
/** genTime within the signing cert's validity window. */
|
|
24
25
|
withinValidity: boolean | null;
|
|
25
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The TSA ECDSA-P384 signature verifies under the embedded signing cert's key.
|
|
28
|
+
* Evaluated whenever the token parses, uses the checked algorithms, binds this
|
|
29
|
+
* package and carries a timestamping signing cert with a P-384 key — whether or not
|
|
30
|
+
* that cert chains to a pinned root (`chainVerified`), which is what makes the key
|
|
31
|
+
* trusted. `null` when an earlier step returned first, or when the signing cert's
|
|
32
|
+
* key is not a P-384 key (`unexpected_algorithm`): a key this verifier cannot
|
|
33
|
+
* evaluate is not an invalid signature. `false` covers a P-384 key under which the
|
|
34
|
+
* signature does not verify, including a malformed signature encoding.
|
|
35
|
+
*/
|
|
26
36
|
signatureValid: boolean | null;
|
|
27
37
|
genTime?: number;
|
|
28
38
|
/** The matched root anchor name. */
|
|
29
39
|
tsa?: string;
|
|
40
|
+
/**
|
|
41
|
+
* The first failing step, in this order: `parse_error`; `imprint_mismatch` (the
|
|
42
|
+
* TSTInfo imprint is not SHA-256 of this package's hash; compared before the
|
|
43
|
+
* SignerInfo algorithms); `unexpected_algorithm` (SignerInfo digest or signature
|
|
44
|
+
* algorithm outside the checked set); `no_message_digest`, `content_not_bound`;
|
|
45
|
+
* `no_signing_cert`, `eku_not_timestamping`. After the signing-cert step the token's
|
|
46
|
+
* own faults come first: `genTime_outside_validity` (the signing cert only), then
|
|
47
|
+
* `unexpected_algorithm` (a signing-cert key that is not P-384), then
|
|
48
|
+
* `signature_invalid`, then the chain's reason — `untrusted_root`,
|
|
49
|
+
* `chain_incomplete`, `chain_signature_invalid` (a link whose RSA signature does not
|
|
50
|
+
* verify), `chain_algorithm_unsupported` (a link signed with an algorithm the chain
|
|
51
|
+
* validator does not implement), or `chain_outside_validity` (an intermediate or
|
|
52
|
+
* root not valid at genTime). So a chain reason is reported only for
|
|
53
|
+
* a token whose TSA signature verifies and whose genTime is within the signing cert's
|
|
54
|
+
* validity, and each reason names one kind of fault.
|
|
55
|
+
*/
|
|
30
56
|
reason?: Rfc3161FailReason;
|
|
31
57
|
}
|
|
32
58
|
/**
|
|
33
59
|
* Verify an RFC 3161 token (base64) attests `expectedHashHex` (the package SHA-256),
|
|
34
60
|
* chaining the token's embedded signing cert to a pinned TSA root. Async (the cert
|
|
35
61
|
* chain uses WebCrypto RSA). A parse failure / untrusted chain is reported, not
|
|
36
|
-
* thrown
|
|
37
|
-
*
|
|
62
|
+
* thrown. The message imprint is compared first, before the SignerInfo algorithms.
|
|
63
|
+
* The TSA signature is evaluated even when the chain does not reach a pinned
|
|
64
|
+
* root, and a fault of the token itself (`genTime_outside_validity`,
|
|
65
|
+
* `unexpected_algorithm` for a signing-cert key that is not P-384,
|
|
66
|
+
* `signature_invalid`) is reported ahead of a chain fault. `parse_error`, and
|
|
67
|
+
* `unexpected_algorithm` for the SignerInfo's digest or signature algorithm, still
|
|
68
|
+
* return before any signature is evaluated.
|
|
38
69
|
*/
|
|
39
70
|
export declare function verifyRfc3161Timestamp(tokenB64: string, expectedHashHex: string, anchors?: readonly TsaRootAnchor[]): Promise<Rfc3161VerifyResult>;
|
package/dist/rfc3161.js
CHANGED
|
@@ -7,11 +7,21 @@
|
|
|
7
7
|
// 2. signedAttrs.messageDigest == SHA-512(TSTInfo) — the signature is BOUND to it;
|
|
8
8
|
// 3. the token's EMBEDDED signing cert chains to the PINNED FreeTSA RSA-4096 root
|
|
9
9
|
// (each link's signature verified; CA + validity-vs-genTime checked) — so the
|
|
10
|
-
// signer is trusted WITHOUT pinning the (rotatable) signing key
|
|
10
|
+
// signer is trusted WITHOUT pinning the (rotatable) signing key. An intermediate
|
|
11
|
+
// or root not valid at genTime is a chain fault, `chain_outside_validity`;
|
|
11
12
|
// 4. the signing cert carries EKU id-kp-timeStamping and its validity covers
|
|
12
13
|
// genTime;
|
|
13
14
|
// 5. the TSA's ECDSA-P384 signature over SHA-512(signedAttrs) verifies under THAT
|
|
14
|
-
//
|
|
15
|
+
// leaf's key. The signature is evaluated whether or not step 3 reaches a pinned
|
|
16
|
+
// root (typedstandards#94), so a token that does not verify reads differently
|
|
17
|
+
// from one whose only fault is its chain; `verified` still requires all five. A
|
|
18
|
+
// leaf key that is not a P-384 key is outside the set this verifier checks: it
|
|
19
|
+
// reads `unexpected_algorithm`, not `signature_invalid`.
|
|
20
|
+
//
|
|
21
|
+
// Step 1 runs before the SignerInfo digest/signature algorithms are checked: it reads
|
|
22
|
+
// only the TSTInfo, so a token that does not bind this package reads `imprint_mismatch`
|
|
23
|
+
// whatever algorithms it uses. Each reason names one kind of fault, so a consumer can
|
|
24
|
+
// classify a token by `reason` alone.
|
|
15
25
|
//
|
|
16
26
|
// Crypto: the leaf TSA signature is `@noble` ECDSA-P384 (the cert key); the cert
|
|
17
27
|
// chain is RSASSA-PKCS1-v1.5/SHA-512 via WebCrypto (`globalThis.crypto.subtle`, a
|
|
@@ -41,6 +51,26 @@ export const FREETSA_ROOT_ANCHORS = [
|
|
|
41
51
|
rootKeyDer: 'MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6qlmQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlelb+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsmq4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7VDYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzdEcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3SciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XTFNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYBBF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837gYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVqM2ITKentbCMCAwEAAQ==',
|
|
42
52
|
},
|
|
43
53
|
];
|
|
54
|
+
// Source-of-truth array (not just a type), as BLOB_REF_VERIFY_REASONS in blob-ref.ts,
|
|
55
|
+
// so a consumer can enumerate every reason `verifyRfc3161Timestamp` can return at
|
|
56
|
+
// runtime and fail when a new one arrives unclassified. Frozen: reading it cannot
|
|
57
|
+
// change it.
|
|
58
|
+
export const RFC3161_FAIL_REASONS = Object.freeze([
|
|
59
|
+
'parse_error',
|
|
60
|
+
'unexpected_algorithm',
|
|
61
|
+
'no_message_digest',
|
|
62
|
+
'content_not_bound',
|
|
63
|
+
'imprint_mismatch',
|
|
64
|
+
'no_signing_cert',
|
|
65
|
+
'eku_not_timestamping',
|
|
66
|
+
'genTime_outside_validity',
|
|
67
|
+
'chain_incomplete',
|
|
68
|
+
'chain_signature_invalid',
|
|
69
|
+
'chain_algorithm_unsupported',
|
|
70
|
+
'chain_outside_validity',
|
|
71
|
+
'untrusted_root',
|
|
72
|
+
'signature_invalid',
|
|
73
|
+
]);
|
|
44
74
|
// EC P-384 SPKI DER: 23-byte prefix + 97-byte uncompressed point (0x04 ‖ X ‖ Y).
|
|
45
75
|
const EC_P384_SPKI_PREFIX = Uint8Array.from([
|
|
46
76
|
0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01,
|
|
@@ -123,16 +153,23 @@ function parseTstInfo(tst) {
|
|
|
123
153
|
}
|
|
124
154
|
// Map the X.509 chain verifier's precise reason onto this module's vocabulary. The
|
|
125
155
|
// strict-RFC structural/policy rejections (#119 P4) collapse to `chain_incomplete`
|
|
126
|
-
// (the chain could not be trusted)
|
|
127
|
-
// `chain_signature_invalid
|
|
156
|
+
// (the chain could not be trusted), only a real RSA link failure surfaces as
|
|
157
|
+
// `chain_signature_invalid`, and a link signed with an algorithm the validator does
|
|
158
|
+
// not implement is `chain_algorithm_unsupported` (#100); the chain layer keeps the
|
|
159
|
+
// fine-grained reason. The
|
|
160
|
+
// chain's validity failure is `chain_outside_validity`: `verifyRfc3161Timestamp`
|
|
161
|
+
// reports a signing cert not valid at genTime before the chain's reason, so here it
|
|
162
|
+
// can only be an intermediate or the root (typedstandards#94).
|
|
128
163
|
function toRfc3161ChainReason(reason) {
|
|
129
164
|
switch (reason) {
|
|
130
165
|
case 'genTime_outside_validity':
|
|
131
|
-
return '
|
|
166
|
+
return 'chain_outside_validity';
|
|
132
167
|
case 'untrusted_root':
|
|
133
168
|
return 'untrusted_root';
|
|
134
169
|
case 'link_signature_invalid':
|
|
135
170
|
return 'chain_signature_invalid';
|
|
171
|
+
case 'link_algorithm_unsupported':
|
|
172
|
+
return 'chain_algorithm_unsupported';
|
|
136
173
|
case 'unsupported_critical_extension':
|
|
137
174
|
case 'algorithm_mismatch':
|
|
138
175
|
case 'issuer_not_found':
|
|
@@ -161,8 +198,13 @@ function fail(base, reason) {
|
|
|
161
198
|
* Verify an RFC 3161 token (base64) attests `expectedHashHex` (the package SHA-256),
|
|
162
199
|
* chaining the token's embedded signing cert to a pinned TSA root. Async (the cert
|
|
163
200
|
* chain uses WebCrypto RSA). A parse failure / untrusted chain is reported, not
|
|
164
|
-
* thrown
|
|
165
|
-
*
|
|
201
|
+
* thrown. The message imprint is compared first, before the SignerInfo algorithms.
|
|
202
|
+
* The TSA signature is evaluated even when the chain does not reach a pinned
|
|
203
|
+
* root, and a fault of the token itself (`genTime_outside_validity`,
|
|
204
|
+
* `unexpected_algorithm` for a signing-cert key that is not P-384,
|
|
205
|
+
* `signature_invalid`) is reported ahead of a chain fault. `parse_error`, and
|
|
206
|
+
* `unexpected_algorithm` for the SignerInfo's digest or signature algorithm, still
|
|
207
|
+
* return before any signature is evaluated.
|
|
166
208
|
*/
|
|
167
209
|
export async function verifyRfc3161Timestamp(tokenB64, expectedHashHex, anchors = FREETSA_ROOT_ANCHORS) {
|
|
168
210
|
let buf;
|
|
@@ -197,13 +239,15 @@ export async function verifyRfc3161Timestamp(tokenB64, expectedHashHex, anchors
|
|
|
197
239
|
signatureValid: null,
|
|
198
240
|
genTime: tst.genTime,
|
|
199
241
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
242
|
+
// The imprint reads only the TSTInfo, not the SignerInfo algorithms, so it runs
|
|
243
|
+
// first: a token that does not bind this package fails whatever it is signed with.
|
|
203
244
|
base.imprintMatches =
|
|
204
245
|
tst.hashAlgOid === OID_SHA256 && bytesToHex(tst.hashedMessage) === expectedHashHex.toLowerCase();
|
|
205
246
|
if (!base.imprintMatches)
|
|
206
247
|
return fail(base, 'imprint_mismatch');
|
|
248
|
+
if (token.digestAlgOid !== OID_SHA512 || token.signatureAlgOid !== OID_ECDSA_SHA512) {
|
|
249
|
+
return fail(base, 'unexpected_algorithm');
|
|
250
|
+
}
|
|
207
251
|
if (!mdAttr)
|
|
208
252
|
return fail(base, 'no_message_digest');
|
|
209
253
|
base.contentBound = bytesEqual(mdAttr, sha512(token.tstInfo));
|
|
@@ -219,27 +263,47 @@ export async function verifyRfc3161Timestamp(tokenB64, expectedHashHex, anchors
|
|
|
219
263
|
base.withinValidity = tst.genTime >= leaf.notBefore && tst.genTime <= leaf.notAfter;
|
|
220
264
|
const chain = await validateChainToRoot(token.certs, leaf, anchors, tst.genTime);
|
|
221
265
|
base.chainVerified = chain.ok;
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
base.tsa = chain.tsa;
|
|
225
|
-
if (!base.withinValidity)
|
|
226
|
-
return fail(base, 'genTime_outside_validity');
|
|
266
|
+
if (chain.ok)
|
|
267
|
+
base.tsa = chain.tsa;
|
|
227
268
|
// The TSA signature: ECDSA-P384 over SHA-512(signedAttrs re-tagged [0]→SET),
|
|
228
|
-
// under the
|
|
229
|
-
|
|
230
|
-
|
|
269
|
+
// under the leaf's key. lowS:false — a TSA emits high-S (#119 fix). Evaluated
|
|
270
|
+
// whatever the chain read (#94): the key is trusted only if `chain.ok`, but a
|
|
271
|
+
// signature that does not verify under it is a fault of the token itself.
|
|
272
|
+
//
|
|
273
|
+
// Two separate steps. A leaf key that is not a P-384 SPKI is one this verifier
|
|
274
|
+
// cannot evaluate: `signatureValid` stays null and the token reads
|
|
275
|
+
// `unexpected_algorithm`. Only a P-384 key under which the signature does not
|
|
276
|
+
// verify (a malformed signature encoding included) is `signature_invalid`.
|
|
277
|
+
let point = null;
|
|
231
278
|
try {
|
|
232
|
-
|
|
233
|
-
base.signatureValid = p384.verify(token.signature, sha512(signedBytes), point, {
|
|
234
|
-
format: 'der',
|
|
235
|
-
prehash: false,
|
|
236
|
-
lowS: false,
|
|
237
|
-
});
|
|
279
|
+
point = extractP384Point(leaf.spkiDer);
|
|
238
280
|
}
|
|
239
281
|
catch {
|
|
240
|
-
|
|
282
|
+
point = null;
|
|
283
|
+
}
|
|
284
|
+
if (point) {
|
|
285
|
+
const signedBytes = Uint8Array.from(rawTlv(buf, token.signedAttrs));
|
|
286
|
+
signedBytes[0] = 0x31;
|
|
287
|
+
try {
|
|
288
|
+
base.signatureValid = p384.verify(token.signature, sha512(signedBytes), point, {
|
|
289
|
+
format: 'der',
|
|
290
|
+
prehash: false,
|
|
291
|
+
lowS: false,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
base.signatureValid = false;
|
|
296
|
+
}
|
|
241
297
|
}
|
|
298
|
+
// The token's own faults first, then the chain's: a chain reason is reported only
|
|
299
|
+
// for a token whose signature verifies and whose genTime is within validity.
|
|
300
|
+
if (!base.withinValidity)
|
|
301
|
+
return fail(base, 'genTime_outside_validity');
|
|
302
|
+
if (!point)
|
|
303
|
+
return fail(base, 'unexpected_algorithm');
|
|
242
304
|
if (!base.signatureValid)
|
|
243
305
|
return fail(base, 'signature_invalid');
|
|
306
|
+
if (!chain.ok)
|
|
307
|
+
return fail(base, chain.reason ?? 'untrusted_root');
|
|
244
308
|
return { ...base, verified: true };
|
|
245
309
|
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type RekorInclusionProof, type RekorInclusionResult } from './rekor-inclusion.ts';
|
|
2
2
|
import { type Rfc3161VerifyResult } from './rfc3161.ts';
|
|
3
3
|
import { type KeyTrustResult, type TrustRegistry, type TrustRegistryProvenance } from './trust-registry.ts';
|
|
4
|
-
import { type ContentCanonicalizationResolution, type ContentHashCheck, type TypeResolution, type SignerIdentityCheck, type CaptureMethodVocabCheck, type ContentProfileCheck, type BlobRefVerification } from './checks.ts';
|
|
4
|
+
import { type ContentCanonicalizationResolution, type ContentHashCheck, type TypeResolution, type SignerIdentityCheck, type SigningKeyIdConsistencyCheck, type CaptureMethodVocabCheck, type ContentProfileCheck, type BlobRefVerification } from './checks.ts';
|
|
5
5
|
import { type LifecycleResolution } from './lifecycle.ts';
|
|
6
6
|
import type { FetchLike } from './types.ts';
|
|
7
7
|
/** The parsed signature envelope (`buildCommitmentView`'s `signature`). */
|
|
@@ -149,6 +149,10 @@ export interface VerifyResult {
|
|
|
149
149
|
contentHash: ContentHashCheck | null;
|
|
150
150
|
typeResolution: TypeResolution | null;
|
|
151
151
|
signerIdentity: SignerIdentityCheck | null;
|
|
152
|
+
/** Check #6 — the envelope `kid` against `metadata.signingKeyId`. Null when
|
|
153
|
+
* the package is absent, no signature envelope was supplied, or the
|
|
154
|
+
* signature was malformed: there is no envelope `kid` to read. */
|
|
155
|
+
signingKeyIdConsistency: SigningKeyIdConsistencyCheck | null;
|
|
152
156
|
captureMethodVocab: CaptureMethodVocabCheck | null;
|
|
153
157
|
/** Check #16 — `metadata.contentProfile` (hub ADR-0029 §5). */
|
|
154
158
|
contentProfile: ContentProfileCheck | null;
|
package/dist/verify.js
CHANGED
|
@@ -35,8 +35,50 @@ import { verifyRekorInclusion, } from "./rekor-inclusion.js";
|
|
|
35
35
|
import { verifyRfc3161Timestamp } from "./rfc3161.js";
|
|
36
36
|
import { verifyKeyTrust, legacyEmbeddedKeyTrust, } from "./trust-registry.js";
|
|
37
37
|
import { applyKeyDerivedRegistryRule, applySelfCertifiedKeyTrust, compareKeyDerivedIdentifier, } from "./self-certifying.js";
|
|
38
|
-
import { resolveContentCanonicalization, verifyContentHashWithFetch, resolvePackageType, checkSignerIdentity, checkCaptureMethodVocab, checkContentProfile, verifyPackageBlobRefs, } from "./checks.js";
|
|
38
|
+
import { resolveContentCanonicalization, verifyContentHashWithFetch, resolvePackageType, checkSignerIdentity, checkSigningKeyIdConsistency, checkCaptureMethodVocab, checkContentProfile, verifyPackageBlobRefs, } from "./checks.js";
|
|
39
39
|
import { resolveLifecycleFromLegacyColumns, } from "./lifecycle.js";
|
|
40
|
+
/**
|
|
41
|
+
* A fetcher that sends at most one request per URL and replays that request's
|
|
42
|
+
* outcome to every later call for the same URL. `verifyRecord` makes one per
|
|
43
|
+
* call and hands it to checks #9 and #4, which would otherwise each fetch a
|
|
44
|
+
* raw-bytes/v1 BlobRef `output` (#90); the scope is one `verifyRecord` call.
|
|
45
|
+
*
|
|
46
|
+
* The outcome replays as it happened, so neither check reads anything
|
|
47
|
+
* differently: a fetcher that threw throws again, a non-ok response is
|
|
48
|
+
* non-ok again, and a body that could not be read fails again. The body is
|
|
49
|
+
* read once, on first demand, and every caller receives the same
|
|
50
|
+
* `ArrayBuffer`; the two checks only read it. The first call's `init` (its
|
|
51
|
+
* abort signal) governs the one request. The underlying fetcher is resolved
|
|
52
|
+
* when the request is sent (`globalThis.fetch` when none is injected), as the
|
|
53
|
+
* checks resolve it.
|
|
54
|
+
*/
|
|
55
|
+
function oneFetchPerUrl(fetcher) {
|
|
56
|
+
const requests = new Map();
|
|
57
|
+
return async (url, init) => {
|
|
58
|
+
let request = requests.get(url);
|
|
59
|
+
if (request === undefined) {
|
|
60
|
+
const send = fetcher ?? globalThis.fetch;
|
|
61
|
+
request = Promise.resolve()
|
|
62
|
+
.then(() => send(url, init))
|
|
63
|
+
.then((response) => ({ response }));
|
|
64
|
+
requests.set(url, request);
|
|
65
|
+
}
|
|
66
|
+
const held = await request;
|
|
67
|
+
const body = () => {
|
|
68
|
+
if (held.body === undefined)
|
|
69
|
+
held.body = held.response.arrayBuffer();
|
|
70
|
+
return held.body;
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
ok: held.response.ok,
|
|
74
|
+
status: held.response.status,
|
|
75
|
+
headers: held.response.headers,
|
|
76
|
+
arrayBuffer: () => body(),
|
|
77
|
+
text: async () => new TextDecoder().decode(await body()),
|
|
78
|
+
json: async () => JSON.parse(new TextDecoder().decode(await body())),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
40
82
|
/**
|
|
41
83
|
* Run the §9.2 check suite. The step order mirrors the server route exactly
|
|
42
84
|
* (notably: Rekor before key-trust, because a deprecated-key trust decision is
|
|
@@ -113,11 +155,14 @@ export async function verifyRecord(input, deps) {
|
|
|
113
155
|
if (input.rfc3161Timestamp && packageHash) {
|
|
114
156
|
rfc3161 = await verifyRfc3161Timestamp(input.rfc3161Timestamp, packageHash);
|
|
115
157
|
}
|
|
158
|
+
// Checks #9 and #4 share one fetch per BlobRef URL: under raw-bytes/v1 with a
|
|
159
|
+
// BlobRef `output`, both read the same file (#90).
|
|
160
|
+
const blobFetch = oneFetchPerUrl(deps.fetch);
|
|
116
161
|
// Step 3b — blob references embedded in the package (check #9).
|
|
117
162
|
let blobRefs = [];
|
|
118
163
|
let blobRefsVerified = null;
|
|
119
164
|
if (pkg) {
|
|
120
|
-
blobRefs = await verifyPackageBlobRefs(pkg, { fetch:
|
|
165
|
+
blobRefs = await verifyPackageBlobRefs(pkg, { fetch: blobFetch });
|
|
121
166
|
if (blobRefs.length > 0) {
|
|
122
167
|
blobRefsVerified = blobRefs.every((r) => r.ok);
|
|
123
168
|
}
|
|
@@ -159,18 +204,22 @@ export async function verifyRecord(input, deps) {
|
|
|
159
204
|
: applyKeyDerivedRegistryRule(keyTrust, deps.registryProvenance);
|
|
160
205
|
}
|
|
161
206
|
// Step 5 — canonicalization, content-hash, and envelope checks
|
|
162
|
-
// (#3/#4/#12/#14/#15/#16). #4 fetches only for a raw-bytes/v1 BlobRef output.
|
|
207
|
+
// (#3/#4/#6/#12/#14/#15/#16). #4 fetches only for a raw-bytes/v1 BlobRef output.
|
|
163
208
|
let contentCanonicalization = null;
|
|
164
209
|
let contentHashCheck = null;
|
|
165
210
|
let typeResolution = null;
|
|
166
211
|
let signerIdentity = null;
|
|
212
|
+
let signingKeyIdConsistency = null;
|
|
167
213
|
let captureMethodVocab = null;
|
|
168
214
|
let contentProfile = null;
|
|
169
215
|
if (pkg) {
|
|
170
216
|
contentCanonicalization = resolveContentCanonicalization(pkg);
|
|
171
|
-
contentHashCheck = await verifyContentHashWithFetch(pkg, contentCanonicalization, input.legacyExternalHash ?? packageHash, { fetch:
|
|
217
|
+
contentHashCheck = await verifyContentHashWithFetch(pkg, contentCanonicalization, input.legacyExternalHash ?? packageHash, { fetch: blobFetch });
|
|
172
218
|
typeResolution = resolvePackageType(pkg);
|
|
173
219
|
signerIdentity = checkSignerIdentity(pkg, sigKid, deps.registry, sigPublicKey);
|
|
220
|
+
if (input.signature && !input.signatureMalformed) {
|
|
221
|
+
signingKeyIdConsistency = checkSigningKeyIdConsistency(pkg, input.signature.kid);
|
|
222
|
+
}
|
|
174
223
|
captureMethodVocab = checkCaptureMethodVocab(pkg);
|
|
175
224
|
contentProfile = checkContentProfile(pkg);
|
|
176
225
|
}
|
|
@@ -205,6 +254,7 @@ export async function verifyRecord(input, deps) {
|
|
|
205
254
|
contentHash: contentHashCheck,
|
|
206
255
|
typeResolution,
|
|
207
256
|
signerIdentity,
|
|
257
|
+
signingKeyIdConsistency,
|
|
208
258
|
captureMethodVocab,
|
|
209
259
|
contentProfile,
|
|
210
260
|
lifecycle,
|
package/dist/x509.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ export declare function bytesEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
|
40
40
|
* Verify `cert.signature` over `cert.tbsBytes` using `issuerSpkiDer` (RSA), via
|
|
41
41
|
* WebCrypto (`globalThis.crypto.subtle` — a browser/Node global, not a `node:`
|
|
42
42
|
* import, so verify-core stays browser-safe). Returns false for an unsupported
|
|
43
|
-
* algorithm or any verification error
|
|
43
|
+
* algorithm or any verification error; `verifyCertChainToAnchor` tells the two
|
|
44
|
+
* apart before it calls this.
|
|
44
45
|
*/
|
|
45
46
|
export declare function verifyCertSignatureRsa(cert: X509Cert, issuerSpkiDer: Uint8Array): Promise<boolean>;
|
|
46
47
|
/** A pinned trust anchor: a root whose public key is trusted by value. */
|
|
@@ -49,7 +50,7 @@ export interface ChainAnchor {
|
|
|
49
50
|
/** Root public key, base64 SPKI DER. */
|
|
50
51
|
rootKeyDer: string;
|
|
51
52
|
}
|
|
52
|
-
export type ChainFailReason = 'unsupported_critical_extension' | 'algorithm_mismatch' | 'genTime_outside_validity' | 'issuer_not_found' | 'issuer_not_ca' | 'path_len_exceeded' | 'link_signature_invalid' | 'untrusted_root';
|
|
53
|
+
export type ChainFailReason = 'unsupported_critical_extension' | 'algorithm_mismatch' | 'genTime_outside_validity' | 'issuer_not_found' | 'issuer_not_ca' | 'path_len_exceeded' | 'link_signature_invalid' | 'link_algorithm_unsupported' | 'untrusted_root';
|
|
53
54
|
export interface ChainResult {
|
|
54
55
|
/** The path links to a pinned anchor and every strict check passed. */
|
|
55
56
|
ok: boolean;
|
package/dist/x509.js
CHANGED
|
@@ -148,11 +148,18 @@ const RSA_SIG_HASH = {
|
|
|
148
148
|
'1.2.840.113549.1.1.12': 'SHA-384',
|
|
149
149
|
'1.2.840.113549.1.1.13': 'SHA-512',
|
|
150
150
|
};
|
|
151
|
+
/** Whether `oid` is a certificate signature algorithm `verifyCertSignatureRsa`
|
|
152
|
+
* implements. The chain validator reports a link signed with any other algorithm as
|
|
153
|
+
* `link_algorithm_unsupported`, not as a signature that does not verify (#100). */
|
|
154
|
+
function isImplementedCertSignatureAlgorithm(oid) {
|
|
155
|
+
return Object.prototype.hasOwnProperty.call(RSA_SIG_HASH, oid);
|
|
156
|
+
}
|
|
151
157
|
/**
|
|
152
158
|
* Verify `cert.signature` over `cert.tbsBytes` using `issuerSpkiDer` (RSA), via
|
|
153
159
|
* WebCrypto (`globalThis.crypto.subtle` — a browser/Node global, not a `node:`
|
|
154
160
|
* import, so verify-core stays browser-safe). Returns false for an unsupported
|
|
155
|
-
* algorithm or any verification error
|
|
161
|
+
* algorithm or any verification error; `verifyCertChainToAnchor` tells the two
|
|
162
|
+
* apart before it calls this.
|
|
156
163
|
*/
|
|
157
164
|
export async function verifyCertSignatureRsa(cert, issuerSpkiDer) {
|
|
158
165
|
const hash = RSA_SIG_HASH[cert.sigAlgOid];
|
|
@@ -207,6 +214,11 @@ export async function verifyCertChainToAnchor(certs, leaf, anchors, atTime) {
|
|
|
207
214
|
if (issuer.pathLen !== null && depth > issuer.pathLen) {
|
|
208
215
|
return { ok: false, reason: 'path_len_exceeded' }; // (c)
|
|
209
216
|
}
|
|
217
|
+
// The algorithm is checked where the signature is, so the reasons before it read
|
|
218
|
+
// as they did; a link this validator cannot check is not one that fails (#100).
|
|
219
|
+
if (!isImplementedCertSignatureAlgorithm(current.sigAlgOid)) {
|
|
220
|
+
return { ok: false, reason: 'link_algorithm_unsupported' };
|
|
221
|
+
}
|
|
210
222
|
if (!(await verifyCertSignatureRsa(current, issuer.spkiDer))) {
|
|
211
223
|
return { ok: false, reason: 'link_signature_invalid' };
|
|
212
224
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typedstandards/verify-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Portable, browser-safe verification core for Typed Standards record packages — the §9.2 check suite, single-sourced so it verifies identically on a server and in the browser.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nathan Storey",
|