@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 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 (v1)
70
-
71
- Fully client-side: #1 envelope hash, #2 signature, #3 canonicalization, #4 content
72
- hash, #5 key trust, #6 kid consistency, #9 blob refs, #12 type, #13 nodeId, #14
73
- signer identity, #15 captureMethod vocab. **Presence** only for #7 (RFC 3161).
74
- **Hash-parity** only for #8 (Rekor). **State** depth for #10 (lifecycle).
75
-
76
- The deeper offline crypto — TSA-chain verification, Rekor Merkle-inclusion,
77
- independent lifecycle-chain verification, and the authoritative bundle-mode test —
78
- is a fast-follow (#119), not in this release. The UI built on this should be
79
- honest about what is presence vs. full-crypto.
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 mirrors the server's
28
- * "blob unavailable" path: integrity fails and the package-derived checks
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.6.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": {