@typedstandards/verify-core 0.6.0 → 0.7.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 +69 -0
- package/README.md +22 -11
- package/dist/verify.d.ts +44 -3
- package/dist/verify.js +14 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog — @typedstandards/verify-core
|
|
2
|
+
|
|
3
|
+
Factual record of what changed per published version. Check numbers (#1–#15)
|
|
4
|
+
refer to the Typed Standards specification §9.2 verification sequence. Issue
|
|
5
|
+
references are to `npstorey/civic-ai-tools-website` (#119 is the offline-crypto
|
|
6
|
+
hardening arc; #116 is the standalone-verifier arc this package was extracted
|
|
7
|
+
in).
|
|
8
|
+
|
|
9
|
+
## 0.7.0 — 2026-06-16
|
|
10
|
+
|
|
11
|
+
- #1 envelope integrity is now TRI-STATE. `VerifyResult` gains an
|
|
12
|
+
`envelopeIntegrity: { status: 'verified' | 'altered' | 'unavailable'; reason? }`
|
|
13
|
+
field. A null `package` (content not fetched) previously surfaced only as
|
|
14
|
+
`hashMatch: false`, which a consumer could not distinguish from real tampering;
|
|
15
|
+
it now resolves to `status: 'unavailable'`, distinct from the `altered`
|
|
16
|
+
(bytes-present, hash-mismatch) case. `VerifyInput` gains an optional
|
|
17
|
+
`contentUnavailableReason: 'private' | 'unfetchable'` so the caller can say WHY
|
|
18
|
+
the content is absent — `private` (the commitment redacted the location for a
|
|
19
|
+
sealed/committed record, integrity N/A) vs. `unfetchable` (a present location
|
|
20
|
+
whose bytes could not be retrieved). Fixes the verifier false-alarming
|
|
21
|
+
"Contents changed since signing" on a perfectly valid content-private package
|
|
22
|
+
(npstorey/typedstandards#21). Additive and back-compatible: `hashMatch` is
|
|
23
|
+
unchanged (still `false` in both the `altered` and `unavailable` cases), so
|
|
24
|
+
existing consumers reading only `hashMatch` are unaffected.
|
|
25
|
+
|
|
26
|
+
## 0.6.1 — 2026-06-10
|
|
27
|
+
|
|
28
|
+
- Documentation only; no code changes. Corrects the README's stale "Check
|
|
29
|
+
depth (v1)" section — checks #7/#8/#10 have run at full cryptographic depth
|
|
30
|
+
since 0.2.0–0.6.0, not presence/hash-parity/state depth as previously
|
|
31
|
+
described. Adds this changelog.
|
|
32
|
+
|
|
33
|
+
## 0.6.0 — 2026-06-07
|
|
34
|
+
|
|
35
|
+
- #7 hardening: strict RFC 5280 certificate-chain validation (#119 P4).
|
|
36
|
+
- Shared `parseInclusionProof` guard for #8 consumers (#119 P4).
|
|
37
|
+
|
|
38
|
+
## 0.5.0 — 2026-06-07
|
|
39
|
+
|
|
40
|
+
- #10 at full depth: independent verification of the signed lifecycle
|
|
41
|
+
attestation chain, including reachability from the package node, replacing
|
|
42
|
+
host-reported state (#119 P3).
|
|
43
|
+
|
|
44
|
+
## 0.4.0 — 2026-06-07
|
|
45
|
+
|
|
46
|
+
- #7 deepened: TSA certificate-chain verification to a pinned FreeTSA root;
|
|
47
|
+
token `genTime` surfaced to check #5 (#119 P2b).
|
|
48
|
+
|
|
49
|
+
## 0.3.1 — 2026-06-07
|
|
50
|
+
|
|
51
|
+
- Fix: accept high-S ECDSA signatures (`lowS: false`) from third-party
|
|
52
|
+
signers.
|
|
53
|
+
|
|
54
|
+
## 0.3.0 — 2026-06-07
|
|
55
|
+
|
|
56
|
+
- #7 at cryptographic depth: RFC 3161 token parsing, message-imprint match,
|
|
57
|
+
and TSA signature verification, FreeTSA profile (#119 P2a).
|
|
58
|
+
|
|
59
|
+
## 0.2.0 — 2026-06-06
|
|
60
|
+
|
|
61
|
+
- #8 at cryptographic depth: Rekor Merkle inclusion-proof verification from
|
|
62
|
+
the carried proof, against Rekor's pinned public key; carried-offline
|
|
63
|
+
integration test (#119 P1).
|
|
64
|
+
|
|
65
|
+
## 0.1.0 — 2026-06-05
|
|
66
|
+
|
|
67
|
+
- Initial release (#116 WS2/WS3): the spec §9.2 check suite, browser-safe (no
|
|
68
|
+
Node built-ins). #7 at presence depth, #8 at hash-parity depth, #10 at state
|
|
69
|
+
depth; all other checks fully client-side.
|
package/README.md
CHANGED
|
@@ -66,17 +66,28 @@ The barrel (`.`) also exports the individual check functions
|
|
|
66
66
|
`computeContentHashSha256`), and every status vocabulary type — so a consumer can
|
|
67
67
|
drive the checks one-at-a-time and render the math as it resolves.
|
|
68
68
|
|
|
69
|
-
## Check depth (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
## Check depth (v0.6.x)
|
|
70
|
+
|
|
71
|
+
Every check in the spec §9.2 sequence runs fully client-side, including the three
|
|
72
|
+
that shipped at reduced depth in v0.1:
|
|
73
|
+
|
|
74
|
+
- **#7 RFC 3161 timestamp** — full cryptographic verification: token parsing,
|
|
75
|
+
message-imprint match, TSA signature, and certificate-chain validation with
|
|
76
|
+
strict RFC 5280 checks to a pinned root (FreeTSA EC-P384 leaf → RSA-4096
|
|
77
|
+
root).
|
|
78
|
+
- **#8 Rekor transparency log** — cryptographic Merkle inclusion-proof
|
|
79
|
+
verification from the proof carried with the package, against Rekor's pinned
|
|
80
|
+
public key; no live call to Rekor required.
|
|
81
|
+
- **#10 lifecycle** — independent verification of the signed lifecycle
|
|
82
|
+
attestation chain, including reachability from the package node, instead of
|
|
83
|
+
trusting a host-reported state.
|
|
84
|
+
|
|
85
|
+
Verification is offline-first: from a self-contained commitment bundle the full
|
|
86
|
+
sequence runs with zero network access (demonstrated by a hermetic,
|
|
87
|
+
network-blocked test in this monorepo). Trust anchors (the TSA certificate
|
|
88
|
+
chain and the Rekor log public key) ship pinned in the source with capture
|
|
89
|
+
dates and rotation notes. See `CHANGELOG.md` for how each check reached full
|
|
90
|
+
depth across 0.2.0–0.6.0.
|
|
80
91
|
|
|
81
92
|
## License
|
|
82
93
|
|
package/dist/verify.d.ts
CHANGED
|
@@ -11,6 +11,34 @@ export interface VerifySignatureEnvelope {
|
|
|
11
11
|
algorithm?: string;
|
|
12
12
|
kid?: string;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Why a package's content was unavailable for envelope recomputation (#1).
|
|
16
|
+
* - `private` — the commitment OMITTED the content location: a creator-only /
|
|
17
|
+
* unlisted record whose bytes are private by design. Integrity is N/A, never a
|
|
18
|
+
* failure; the public commitment (signature, key, timestamp, transparency log)
|
|
19
|
+
* is still fully verifiable.
|
|
20
|
+
* - `unfetchable` — a location WAS present but its bytes could not be retrieved
|
|
21
|
+
* (404 / network). An availability problem, surfaced as unconfirmed rather than
|
|
22
|
+
* altered.
|
|
23
|
+
*/
|
|
24
|
+
export type ContentUnavailableReason = 'private' | 'unfetchable';
|
|
25
|
+
/**
|
|
26
|
+
* Tri-state envelope integrity (#1). Replaces the boolean `hashMatch` lens, which
|
|
27
|
+
* collapsed "content unavailable" onto `false` — so a private (content-redacted)
|
|
28
|
+
* record read as tampered.
|
|
29
|
+
* - `verified` — bytes present; the recomputed hash matches the signed hash.
|
|
30
|
+
* - `altered` — bytes present; the hash MISMATCHES. Real tampering.
|
|
31
|
+
* - `unavailable` — no bytes to recompute from; `reason` says whether the content
|
|
32
|
+
* was private by design or merely unfetchable.
|
|
33
|
+
* `hashMatch` is retained UNCHANGED for back-compat (it is `false` in BOTH the
|
|
34
|
+
* `altered` and `unavailable` cases); a consumer that must not conflate tampering
|
|
35
|
+
* with unavailability reads THIS field.
|
|
36
|
+
*/
|
|
37
|
+
export interface EnvelopeIntegrityResult {
|
|
38
|
+
status: 'verified' | 'altered' | 'unavailable';
|
|
39
|
+
/** Present only when `status === 'unavailable'`. */
|
|
40
|
+
reason?: ContentUnavailableReason;
|
|
41
|
+
}
|
|
14
42
|
/** Lifecycle STATE as carried by the WS1 sidecar (`CommitmentLifecycle`). */
|
|
15
43
|
export interface CommitmentLifecycleState {
|
|
16
44
|
status: 'active' | 'withdrawn';
|
|
@@ -24,9 +52,11 @@ export interface CommitmentLifecycleState {
|
|
|
24
52
|
* the package JSON the sidecar's `packageUrl` resolves to.
|
|
25
53
|
*/
|
|
26
54
|
export interface VerifyInput {
|
|
27
|
-
/** The canonical package JSON (the signed envelope). Null
|
|
28
|
-
*
|
|
29
|
-
* report null.
|
|
55
|
+
/** The canonical package JSON (the signed envelope). Null when the content is
|
|
56
|
+
* unavailable — its bytes were not fetched, so envelope integrity (#1) cannot be
|
|
57
|
+
* recomputed and the package-derived checks report null. `contentUnavailableReason`
|
|
58
|
+
* says WHY it is null (private-by-design vs. unfetchable); the two read very
|
|
59
|
+
* differently and must not be conflated with tampering. */
|
|
30
60
|
package: Record<string, unknown> | null;
|
|
31
61
|
/** The claimed envelope hash (`sidecar.packageHash` / `basePackageHash`). */
|
|
32
62
|
packageHash: string;
|
|
@@ -55,6 +85,13 @@ export interface VerifyInput {
|
|
|
55
85
|
/** Legacy external SHA-256 for the pre-v0.1 content-hash relabel. Defaults to
|
|
56
86
|
* `packageHash` (they coincide for pre-v0.1 packages). */
|
|
57
87
|
legacyExternalHash?: string;
|
|
88
|
+
/** When `package` is null, why: `private` (the commitment redacted the content
|
|
89
|
+
* location — a sealed/committed record) or `unfetchable` (a location was present
|
|
90
|
+
* but its bytes could not be retrieved). Drives the `unavailable` reason on
|
|
91
|
+
* `envelopeIntegrity`. Ignored when `package` is non-null. Absent ⇒ the
|
|
92
|
+
* conservative `unfetchable` (a null package with no stated reason is treated as
|
|
93
|
+
* an unconfirmed availability problem, never silently as a by-design redaction). */
|
|
94
|
+
contentUnavailableReason?: ContentUnavailableReason;
|
|
58
95
|
}
|
|
59
96
|
export interface VerifyDeps {
|
|
60
97
|
/** Parsed trust registry (server: loaded; browser: fetched from the sidecar's
|
|
@@ -72,6 +109,10 @@ export interface VerifyDeps {
|
|
|
72
109
|
* can surface them directly. */
|
|
73
110
|
export interface VerifyResult {
|
|
74
111
|
hashMatch: boolean;
|
|
112
|
+
/** Tri-state envelope integrity (#1) — the corrected lens consumers SHOULD render
|
|
113
|
+
* instead of branching on `hashMatch`, which cannot express "content unavailable,
|
|
114
|
+
* so integrity is N/A" and reads it as tampering. See `EnvelopeIntegrityResult`. */
|
|
115
|
+
envelopeIntegrity: EnvelopeIntegrityResult;
|
|
75
116
|
recomputedHash: string | null;
|
|
76
117
|
/** = recomputedHash (check #13 nodeId). */
|
|
77
118
|
nodeId: string | null;
|
package/dist/verify.js
CHANGED
|
@@ -38,12 +38,24 @@ import { resolveLifecycleFromLegacyColumns, } from "./lifecycle.js";
|
|
|
38
38
|
*/
|
|
39
39
|
export async function verifyEvidence(input, deps) {
|
|
40
40
|
const { package: pkg, packageHash } = input;
|
|
41
|
-
// Step 1 — recompute the envelope hash (checks #1 + #13).
|
|
41
|
+
// Step 1 — recompute the envelope hash (checks #1 + #13). Tri-state: a present
|
|
42
|
+
// package recomputes to `verified` / `altered`; a null package is `unavailable`
|
|
43
|
+
// (NOT `altered`) — its content was never fetched, so there is nothing to compare,
|
|
44
|
+
// and `contentUnavailableReason` distinguishes a by-design redaction (private)
|
|
45
|
+
// from a fetch failure (unfetchable). `hashMatch` stays a boolean for back-compat.
|
|
42
46
|
let hashMatch = false;
|
|
43
47
|
let recomputedHash = null;
|
|
48
|
+
let envelopeIntegrity;
|
|
44
49
|
if (pkg) {
|
|
45
50
|
recomputedHash = recomputePackageHash(pkg);
|
|
46
51
|
hashMatch = recomputedHash === packageHash;
|
|
52
|
+
envelopeIntegrity = { status: hashMatch ? 'verified' : 'altered' };
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
envelopeIntegrity = {
|
|
56
|
+
status: 'unavailable',
|
|
57
|
+
reason: input.contentUnavailableReason ?? 'unfetchable',
|
|
58
|
+
};
|
|
47
59
|
}
|
|
48
60
|
// Step 2 — signature (check #2), dispatching on the stored algorithm (#111).
|
|
49
61
|
let signatureValid = null;
|
|
@@ -144,6 +156,7 @@ export async function verifyEvidence(input, deps) {
|
|
|
144
156
|
});
|
|
145
157
|
return {
|
|
146
158
|
hashMatch,
|
|
159
|
+
envelopeIntegrity,
|
|
147
160
|
recomputedHash,
|
|
148
161
|
nodeId: recomputedHash,
|
|
149
162
|
signatureValid,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typedstandards/verify-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Portable, browser-safe verification core for Typed Standards evidence 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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
22
|
"README.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|