@typedstandards/verify-core 0.6.1 → 0.8.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 +25 -0
- package/dist/signature.d.ts +11 -0
- package/dist/signature.js +11 -6
- package/dist/verify.d.ts +44 -3
- package/dist/verify.js +14 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,31 @@ 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.8.0 — 2026-08-01
|
|
10
|
+
|
|
11
|
+
- New export: `ED25519_SPKI_PREFIX` (`signature.ts`) — the fixed 12-byte
|
|
12
|
+
Ed25519 SPKI DER prefix this module asserts in `extractRawPublicKey`.
|
|
13
|
+
Previously duplicated locally by `@typedstandards/produce-core`'s
|
|
14
|
+
`derivePublicKeySpki`, which now imports it (typedstandards#36). No behavior
|
|
15
|
+
change; the bytes are identical.
|
|
16
|
+
|
|
17
|
+
## 0.7.0 — 2026-06-16
|
|
18
|
+
|
|
19
|
+
- #1 envelope integrity is now TRI-STATE. `VerifyResult` gains an
|
|
20
|
+
`envelopeIntegrity: { status: 'verified' | 'altered' | 'unavailable'; reason? }`
|
|
21
|
+
field. A null `package` (content not fetched) previously surfaced only as
|
|
22
|
+
`hashMatch: false`, which a consumer could not distinguish from real tampering;
|
|
23
|
+
it now resolves to `status: 'unavailable'`, distinct from the `altered`
|
|
24
|
+
(bytes-present, hash-mismatch) case. `VerifyInput` gains an optional
|
|
25
|
+
`contentUnavailableReason: 'private' | 'unfetchable'` so the caller can say WHY
|
|
26
|
+
the content is absent — `private` (the commitment redacted the location for a
|
|
27
|
+
sealed/committed record, integrity N/A) vs. `unfetchable` (a present location
|
|
28
|
+
whose bytes could not be retrieved). Fixes the verifier false-alarming
|
|
29
|
+
"Contents changed since signing" on a perfectly valid content-private package
|
|
30
|
+
(npstorey/typedstandards#21). Additive and back-compatible: `hashMatch` is
|
|
31
|
+
unchanged (still `false` in both the `altered` and `unavailable` cases), so
|
|
32
|
+
existing consumers reading only `hashMatch` are unaffected.
|
|
33
|
+
|
|
9
34
|
## 0.6.1 — 2026-06-10
|
|
10
35
|
|
|
11
36
|
- Documentation only; no code changes. Corrects the README's stale "Check
|
package/dist/signature.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed 12-byte prefix of an Ed25519 public key in SPKI DER (SEQUENCE →
|
|
3
|
+
* AlgorithmIdentifier{ OID 1.3.101.112 } → BIT STRING, 0 unused bits); the
|
|
4
|
+
* full 44-byte structure is this prefix followed by the 32-byte raw key.
|
|
5
|
+
* Asserting the prefix + length before slicing the tail makes a malformed key
|
|
6
|
+
* throw rather than yield 32 arbitrary bytes — matching the old JWK path,
|
|
7
|
+
* which threw on a bad key. Exported (typedstandards#36) so producers
|
|
8
|
+
* assembling SPKI DER — `@typedstandards/produce-core`'s
|
|
9
|
+
* `derivePublicKeySpki` — build from the same bytes this module asserts.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ED25519_SPKI_PREFIX: Uint8Array<ArrayBuffer>;
|
|
1
12
|
/**
|
|
2
13
|
* Extract the raw 32-byte Ed25519 public key from a base64 SPKI DER encoding.
|
|
3
14
|
* Browser-safe replacement for Node's `crypto.createPublicKey(...).export({jwk})`:
|
package/dist/signature.js
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
// reused as-is, INCLUDING the #111 algorithm dispatch.
|
|
9
9
|
import { ed25519, ed25519ph } from '@noble/curves/ed25519.js';
|
|
10
10
|
import { base64ToBytes, sha512Hex, utf8ToBytes } from "./primitives.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The fixed 12-byte prefix of an Ed25519 public key in SPKI DER (SEQUENCE →
|
|
13
|
+
* AlgorithmIdentifier{ OID 1.3.101.112 } → BIT STRING, 0 unused bits); the
|
|
14
|
+
* full 44-byte structure is this prefix followed by the 32-byte raw key.
|
|
15
|
+
* Asserting the prefix + length before slicing the tail makes a malformed key
|
|
16
|
+
* throw rather than yield 32 arbitrary bytes — matching the old JWK path,
|
|
17
|
+
* which threw on a bad key. Exported (typedstandards#36) so producers
|
|
18
|
+
* assembling SPKI DER — `@typedstandards/produce-core`'s
|
|
19
|
+
* `derivePublicKeySpki` — build from the same bytes this module asserts.
|
|
20
|
+
*/
|
|
21
|
+
export const ED25519_SPKI_PREFIX = Uint8Array.from([
|
|
17
22
|
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
|
|
18
23
|
]);
|
|
19
24
|
const ED25519_SPKI_LENGTH = 44;
|
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.8.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",
|