@typedstandards/verify-core 0.8.1 → 0.10.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 +76 -1
- package/README.md +11 -3
- package/dist/blob-ref.js +1 -1
- package/dist/canonicalization.d.ts +7 -0
- package/dist/canonicalization.js +15 -0
- package/dist/checks.d.ts +70 -4
- package/dist/checks.js +160 -6
- package/dist/did-key.d.ts +28 -0
- package/dist/did-key.js +74 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/lifecycle.js +1 -1
- package/dist/profiles.js +6 -0
- package/dist/self-certifying.d.ts +46 -0
- package/dist/self-certifying.js +89 -0
- package/dist/trust-registry.d.ts +20 -4
- package/dist/trust-registry.js +16 -0
- package/dist/types.d.ts +1 -1
- package/dist/verify.d.ts +28 -5
- package/dist/verify.js +50 -8
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,86 @@
|
|
|
1
1
|
# Changelog — @typedstandards/verify-core
|
|
2
2
|
|
|
3
|
-
Factual record of what changed per published version. Check numbers (#1–#
|
|
3
|
+
Factual record of what changed per published version. Check numbers (#1–#16)
|
|
4
4
|
refer to the Typed Standards specification §9.2 verification sequence. Issue
|
|
5
5
|
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.10.0 — 2026-09-22
|
|
10
|
+
|
|
11
|
+
Specification v0.1.9 (hub ADR-0029 and ADR-0030, anchored at `typedstandards#77`): a Producer Profile
|
|
12
|
+
for deterministic tool output, a raw-bytes content rule, a content-profile check, and a self-certifying
|
|
13
|
+
signer. **A minor bump** — new exports, and every type-level union below gains members, so a consumer
|
|
14
|
+
with an exhaustive `switch` or `Record` over one of them needs the new cases.
|
|
15
|
+
|
|
16
|
+
- **The `scripted-recomputation` Producer Profile (ADR-0029).** `PROFILE_CAPTURE_VOCAB` gains
|
|
17
|
+
`scripted-recomputation: ['script-run', 'tool-emitted']`, a second stand-in entry under hub Q32, and
|
|
18
|
+
`CaptureMethod` gains both values. Check #15 now resolves a `scripted-recomputation/<subtype>`
|
|
19
|
+
package's capture method instead of reporting `producerProfile_bundle_unresolved`. The two signed
|
|
20
|
+
ADR-0028 eval-run packages read `ok`.
|
|
21
|
+
- **`raw-bytes/v1` (`RAW_BYTES_CANONICALIZATION`).** A third content-canonicalization rule, in
|
|
22
|
+
`KNOWN_CANONICALIZATION_RULES`: `contentHash` fingerprints the exact bytes of `output`.
|
|
23
|
+
- With inline `output`, check #4 hashes its UTF-8 bytes.
|
|
24
|
+
- With a BlobRef `output`, check #4 hashes the file's bytes obtained through the injected fetcher
|
|
25
|
+
(`verifyContentHashWithFetch`; `verifyRecord` runs it):
|
|
26
|
+
- `content_hash_mismatch` when they differ, or when `contentHash.sha256` differs from the hex of
|
|
27
|
+
`output.ref`;
|
|
28
|
+
- the new status `content_bytes_unavailable` when the bytes cannot be obtained. It is never `ok`
|
|
29
|
+
without hashing.
|
|
30
|
+
- `verifyContentHash` stays synchronous and I/O-free, with an optional `outputBytes` argument.
|
|
31
|
+
- **Check #16, the content-profile label (`checkContentProfile`, `CONTENT_PROFILE_STATUSES`,
|
|
32
|
+
`KNOWN_CONTENT_PROFILES`).** Reads `metadata.contentProfile` and reports one of:
|
|
33
|
+
- `ok`;
|
|
34
|
+
- `contentProfile_absent` (read as `"default"`);
|
|
35
|
+
- `contentProfile_unknown`;
|
|
36
|
+
- `contentProfile_inconsistent` (compared with `producerProfile` only when both are present).
|
|
37
|
+
|
|
38
|
+
`VerifyResult` gains `contentProfile`.
|
|
39
|
+
- **The self-certifying signer (ADR-0030).**
|
|
40
|
+
- **The identifier.** `deriveKeyDerivedIdentifier` derives `did:key:z…` from an Ed25519 SPKI
|
|
41
|
+
`publicKey`. `isKeyDerivedIdentifier` and `KEY_DERIVED_IDENTIFIER_PREFIX` recognize one.
|
|
42
|
+
- **Key trust.** `KEY_TRUST_STATUSES` gains `self_certified` (`verified: false`) for a
|
|
43
|
+
`pseudonymous` signer whose identifier matches its signing key.
|
|
44
|
+
- **Check #14.** It gains `key_derived_match` and `key_derived_mismatch` (fatal; the result carries
|
|
45
|
+
`claimed` and `derived`), and never reports `ok` under a key-derived identifier.
|
|
46
|
+
`checkSignerIdentity` takes an optional `publicKey`.
|
|
47
|
+
- **Registry provenance.** `VerifyDeps.registryProvenance` (`TRUST_REGISTRY_PROVENANCES`:
|
|
48
|
+
`'declared-url' | 'bundle'`; absent is treated as `'bundle'`) states where the caller's registry
|
|
49
|
+
came from.
|
|
50
|
+
- For a key-derived identifier, only a `'declared-url'` registry can raise the status.
|
|
51
|
+
- A bundle-carried registry contributes only `revoked` and `deprecated_invalid`.
|
|
52
|
+
- This holds at every tier, and under a mismatch too.
|
|
53
|
+
- **Base-58.** The encoder is written in place; there is no new runtime dependency.
|
|
54
|
+
- **Signers whose identifier is not key-derived are unchanged.** Already-signed packages verify
|
|
55
|
+
exactly as before, apart from checks #15 and #16, which now resolve a `scripted-recomputation`
|
|
56
|
+
profile and report a content profile.
|
|
57
|
+
|
|
58
|
+
## 0.9.0 — 2026-08-19
|
|
59
|
+
|
|
60
|
+
Vocabulary settlement (spec v0.1.5 Appendix J; registry Q50/Q66, ADR-0025,
|
|
61
|
+
anchored at `civic-ai-tools#160`). "Evidence" is retired from the artifact and
|
|
62
|
+
infrastructure brand role and retained only for the epistemic
|
|
63
|
+
Question/Evidence/Claim role. **A minor bump** — a new export, no removals, no
|
|
64
|
+
behavior change.
|
|
65
|
+
|
|
66
|
+
- **`verifyRecord` is the canonical §9.2 entry point.** `verifyEvidence` remains
|
|
67
|
+
exported as a **deprecated alias**, and it is the same function object
|
|
68
|
+
(`verifyEvidence === verifyRecord`), not a wrapper — every existing caller
|
|
69
|
+
keeps working unchanged and gets byte-identical results. The alias is removed
|
|
70
|
+
no earlier than the next MAJOR version. Migration class `alias-and-deprecate`
|
|
71
|
+
(Appendix J).
|
|
72
|
+
- Package `description` and one `keywords` entry now say "record packages"
|
|
73
|
+
rather than "evidence packages"; the README's usage example imports
|
|
74
|
+
`verifyRecord` and states the alias explicitly. Present-tense references to
|
|
75
|
+
the entry point in published JSDoc (`VerifyInput`, `lifecycle.ts`) name
|
|
76
|
+
`verifyRecord`; past-tense historical narration of what the function did at
|
|
77
|
+
earlier versions is left as written, since it remains accurate.
|
|
78
|
+
- New regression guard in `package-exports.test.ts`: both names must resolve
|
|
79
|
+
and must be the same function object, so the alias cannot silently become a
|
|
80
|
+
second implementation or disappear.
|
|
81
|
+
- No wire, algorithm, check-depth, or result-shape change. Already-signed
|
|
82
|
+
packages are byte-identical and verify exactly as before.
|
|
83
|
+
|
|
9
84
|
## 0.8.1 — 2026-08-17
|
|
10
85
|
|
|
11
86
|
- Removed the `prepare` (install-time build) script (typedstandards#39).
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @typedstandards/verify-core
|
|
2
2
|
|
|
3
|
-
The portable, **browser-safe** verification core for [Typed Standards][ts]
|
|
3
|
+
The portable, **browser-safe** verification core for [Typed Standards][ts] record
|
|
4
4
|
packages — the spec §9.2 check suite, factored so it runs **identically** on a
|
|
5
5
|
server (e.g. civicaitools.org's verify route) and in the browser (the
|
|
6
6
|
typedstandards.org client-side verifier). One implementation, so a tampered
|
|
@@ -45,7 +45,7 @@ edge, and in Node.
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
47
|
```ts
|
|
48
|
-
import {
|
|
48
|
+
import { verifyRecord, type VerifyInput, type TrustRegistry } from '@typedstandards/verify-core';
|
|
49
49
|
|
|
50
50
|
const input: VerifyInput = {
|
|
51
51
|
package: pkg, // the parsed package JSON
|
|
@@ -56,7 +56,7 @@ const input: VerifyInput = {
|
|
|
56
56
|
lifecycle, // optional sidecar lifecycle state
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
const result = await
|
|
59
|
+
const result = await verifyRecord(input, {
|
|
60
60
|
registry, // a TrustRegistry (fetched from the package's trustRegistryUrl)
|
|
61
61
|
fetch: globalThis.fetch,
|
|
62
62
|
});
|
|
@@ -64,6 +64,14 @@ const result = await verifyEvidence(input, {
|
|
|
64
64
|
// contentHash, typeResolution, signerIdentity, lifecycle, ...
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
> **`verifyEvidence` is the same function.** The 2026-08-19 vocabulary settlement
|
|
68
|
+
> renamed the entry point to `verifyRecord` (spec Appendix J) — "evidence" is
|
|
69
|
+
> retired from the artifact/infrastructure brand role and kept only for the
|
|
70
|
+
> epistemic Question/Evidence/Claim role. `verifyEvidence` is still exported as a
|
|
71
|
+
> **deprecated alias of this exact function object**, so existing code keeps
|
|
72
|
+
> working unchanged and returns identical results. The alias is removed no
|
|
73
|
+
> earlier than the next MAJOR version; new code should use `verifyRecord`.
|
|
74
|
+
|
|
67
75
|
The barrel (`.`) also exports the individual check functions
|
|
68
76
|
(`recomputePackageHash`, `verifySignature`, `verifyKeyTrust`, `verifyContentHash`,
|
|
69
77
|
…), the canonicalization primitives (`computeEnvelopeHash`,
|
package/dist/blob-ref.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Content-addressable blob references for
|
|
1
|
+
// Content-addressable blob references for record package fields (spec §9.2
|
|
2
2
|
// check #9) — browser-safe.
|
|
3
3
|
//
|
|
4
4
|
// Large fields (full output text, trace JSON, composed skill guidance) can be
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare const LEGACY_JSON_CANONICALIZATION = "https://typedstandards.org/canonicalization/legacy-json/v1";
|
|
2
2
|
export declare const DATHERE_AG_JUPYTER_CANONICALIZATION = "https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1";
|
|
3
|
+
/** hub ADR-0029 §4: the SHA-256 of the bytes of the package's `output`, with no
|
|
4
|
+
* normalization — the digest `sha256sum` prints for the file. */
|
|
5
|
+
export declare const RAW_BYTES_CANONICALIZATION = "https://typedstandards.org/canonicalization/raw-bytes/v1";
|
|
3
6
|
/** Canonicalization-rule URIs this implementation knows how to apply. An
|
|
4
7
|
* unknown URI fails verify check #3 (`unknown_canonicalization_rule`). */
|
|
5
8
|
export declare const KNOWN_CANONICALIZATION_RULES: readonly string[];
|
|
@@ -43,5 +46,9 @@ export declare function computeEnvelopeHash(pkg: Record<string, unknown>): strin
|
|
|
43
46
|
* (and the sig envelope, which lives off-package here) omitted.
|
|
44
47
|
* - dathere-ag-jupyter/v1 → the executed notebook object
|
|
45
48
|
* (`extensions["org.civicaitools.notebook"]` and its rendered outputs).
|
|
49
|
+
* - raw-bytes/v1 → the UTF-8 bytes of an inline string `output`.
|
|
50
|
+
* An `output` that is not a string (a BlobRef, whose bytes live outside the
|
|
51
|
+
* package) throws here: those bytes have to be obtained first, which is what
|
|
52
|
+
* check #4's fetching path (`verifyContentHashWithFetch`) does.
|
|
46
53
|
*/
|
|
47
54
|
export declare function computeContentHashSha256(pkg: Record<string, unknown>, rule: string): string;
|
package/dist/canonicalization.js
CHANGED
|
@@ -17,11 +17,15 @@ import { sha256Hex } from "./primitives.js";
|
|
|
17
17
|
// targets — verifiers resolve them via the local registry below.
|
|
18
18
|
export const LEGACY_JSON_CANONICALIZATION = 'https://typedstandards.org/canonicalization/legacy-json/v1';
|
|
19
19
|
export const DATHERE_AG_JUPYTER_CANONICALIZATION = 'https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1';
|
|
20
|
+
/** hub ADR-0029 §4: the SHA-256 of the bytes of the package's `output`, with no
|
|
21
|
+
* normalization — the digest `sha256sum` prints for the file. */
|
|
22
|
+
export const RAW_BYTES_CANONICALIZATION = 'https://typedstandards.org/canonicalization/raw-bytes/v1';
|
|
20
23
|
/** Canonicalization-rule URIs this implementation knows how to apply. An
|
|
21
24
|
* unknown URI fails verify check #3 (`unknown_canonicalization_rule`). */
|
|
22
25
|
export const KNOWN_CANONICALIZATION_RULES = [
|
|
23
26
|
LEGACY_JSON_CANONICALIZATION,
|
|
24
27
|
DATHERE_AG_JUPYTER_CANONICALIZATION,
|
|
28
|
+
RAW_BYTES_CANONICALIZATION,
|
|
25
29
|
];
|
|
26
30
|
/** Notebook extension whose object the dathere-ag-jupyter/v1 rule fingerprints
|
|
27
31
|
* (spec §8.2, §8.7.2). */
|
|
@@ -83,8 +87,19 @@ export function computeEnvelopeHash(pkg) {
|
|
|
83
87
|
* (and the sig envelope, which lives off-package here) omitted.
|
|
84
88
|
* - dathere-ag-jupyter/v1 → the executed notebook object
|
|
85
89
|
* (`extensions["org.civicaitools.notebook"]` and its rendered outputs).
|
|
90
|
+
* - raw-bytes/v1 → the UTF-8 bytes of an inline string `output`.
|
|
91
|
+
* An `output` that is not a string (a BlobRef, whose bytes live outside the
|
|
92
|
+
* package) throws here: those bytes have to be obtained first, which is what
|
|
93
|
+
* check #4's fetching path (`verifyContentHashWithFetch`) does.
|
|
86
94
|
*/
|
|
87
95
|
export function computeContentHashSha256(pkg, rule) {
|
|
96
|
+
if (rule === RAW_BYTES_CANONICALIZATION) {
|
|
97
|
+
const output = pkg['output'];
|
|
98
|
+
if (typeof output !== 'string') {
|
|
99
|
+
throw new Error('raw-bytes/v1 content hash over a package alone requires an inline string output');
|
|
100
|
+
}
|
|
101
|
+
return sha256Hex(output);
|
|
102
|
+
}
|
|
88
103
|
if (rule === DATHERE_AG_JUPYTER_CANONICALIZATION) {
|
|
89
104
|
const extensions = pkg['extensions'];
|
|
90
105
|
const notebook = extensions?.[NOTEBOOK_EXTENSION_KEY];
|
package/dist/checks.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface ContentCanonicalizationResolution {
|
|
|
25
25
|
* legacy-json/v1).
|
|
26
26
|
*/
|
|
27
27
|
export declare function resolveContentCanonicalization(pkg: Record<string, unknown>): ContentCanonicalizationResolution;
|
|
28
|
-
export declare const CONTENT_HASH_STATUSES: readonly ["ok", "content_hash_mismatch", "contentHash_no_supported_algorithm", "unresolved_rule", "legacy_relabeled"];
|
|
28
|
+
export declare const CONTENT_HASH_STATUSES: readonly ["ok", "content_hash_mismatch", "contentHash_no_supported_algorithm", "unresolved_rule", "legacy_relabeled", "content_bytes_unavailable"];
|
|
29
29
|
export type ContentHashStatus = (typeof CONTENT_HASH_STATUSES)[number];
|
|
30
30
|
export interface ContentHashCheck {
|
|
31
31
|
status: ContentHashStatus;
|
|
@@ -55,8 +55,53 @@ export interface ContentHashCheck {
|
|
|
55
55
|
* single-SHA-256 is RELABELED as `{ sha256: <hex> }` per §8.2 rather than
|
|
56
56
|
* recomputed; its integrity is established by check #1, so check #4 reports
|
|
57
57
|
* `legacy_relabeled`.
|
|
58
|
+
*
|
|
59
|
+
* raw-bytes/v1 with a BlobRef `output` (hub ADR-0029 §4): the fingerprinted
|
|
60
|
+
* bytes live outside the package. `outputBytes` are those bytes when the caller
|
|
61
|
+
* holds them.
|
|
62
|
+
* - `contentHash.sha256` differs from the hex of `output.ref` →
|
|
63
|
+
* `content_hash_mismatch`, decided from the package alone.
|
|
64
|
+
* - no `outputBytes` → `content_bytes_unavailable`: the bytes were not
|
|
65
|
+
* checked. Never `ok` without hashing.
|
|
66
|
+
* - otherwise the SHA-256 of `outputBytes` is compared with
|
|
67
|
+
* `contentHash.sha256` → `ok` / `content_hash_mismatch`.
|
|
68
|
+
* This function does no I/O; `verifyContentHashWithFetch` obtains the bytes
|
|
69
|
+
* through the injected fetcher and passes them here.
|
|
70
|
+
*/
|
|
71
|
+
export declare function verifyContentHash(pkg: Record<string, unknown>, resolution: ContentCanonicalizationResolution, legacyExternalHash?: string, outputBytes?: Uint8Array): ContentHashCheck;
|
|
72
|
+
/**
|
|
73
|
+
* Check #4 with the raw-bytes/v1 BlobRef path wired to the injected fetcher
|
|
74
|
+
* (hub ADR-0029 §4). Identical to `verifyContentHash` for every other rule and
|
|
75
|
+
* shape. For a raw-bytes/v1 package whose `output` is a BlobRef, it obtains the
|
|
76
|
+
* file's bytes through `options.fetch` (an offline verifier supplies a local
|
|
77
|
+
* copy through that fetcher) and hashes them; bytes it cannot obtain leave the
|
|
78
|
+
* status at `content_bytes_unavailable`.
|
|
58
79
|
*/
|
|
59
|
-
export declare function
|
|
80
|
+
export declare function verifyContentHashWithFetch(pkg: Record<string, unknown>, resolution: ContentCanonicalizationResolution, legacyExternalHash?: string, options?: BlobFetchOptions): Promise<ContentHashCheck>;
|
|
81
|
+
/** The known `metadata.contentProfile` values (spec §8.1.2). */
|
|
82
|
+
export declare const KNOWN_CONTENT_PROFILES: readonly string[];
|
|
83
|
+
export declare const CONTENT_PROFILE_STATUSES: readonly ["ok", "contentProfile_absent", "contentProfile_unknown", "contentProfile_inconsistent"];
|
|
84
|
+
export type ContentProfileStatus = (typeof CONTENT_PROFILE_STATUSES)[number];
|
|
85
|
+
export interface ContentProfileCheck {
|
|
86
|
+
status: ContentProfileStatus;
|
|
87
|
+
/** `metadata.contentProfile` as carried, when it is a string. */
|
|
88
|
+
contentProfile?: string;
|
|
89
|
+
/** `producerProfile` as carried, when present. */
|
|
90
|
+
producerProfile?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check #16 — `metadata.contentProfile` (hub ADR-0029 §5).
|
|
94
|
+
* - key absent → `contentProfile_absent` (read as `"default"`, spec §8.1.2).
|
|
95
|
+
* - present and neither `"default"` nor `"datHere"` → `contentProfile_unknown`
|
|
96
|
+
* (reported, not passed; consistency is not judged).
|
|
97
|
+
* - present, known, `producerProfile` present, and the ADR-0006 §2 invariant
|
|
98
|
+
* fails (`contentProfile === "datHere"` iff `producerProfile` starts with
|
|
99
|
+
* `ai-assisted-analysis/datHere`) → `contentProfile_inconsistent`.
|
|
100
|
+
* - otherwise → `ok`.
|
|
101
|
+
* The invariant is compared only when both fields are present. No status is an
|
|
102
|
+
* integrity failure: both labels are signature-covered.
|
|
103
|
+
*/
|
|
104
|
+
export declare function checkContentProfile(pkg: Record<string, unknown>): ContentProfileCheck;
|
|
60
105
|
export declare const TYPE_RESOLUTION_STATUSES: readonly ["ok", "implicit", "unknown_type"];
|
|
61
106
|
export type TypeResolutionStatus = (typeof TYPE_RESOLUTION_STATUSES)[number];
|
|
62
107
|
export interface TypeResolution {
|
|
@@ -70,7 +115,7 @@ export interface TypeResolution {
|
|
|
70
115
|
* and renders as such — it does NOT fail verification.
|
|
71
116
|
*/
|
|
72
117
|
export declare function resolvePackageType(pkg: Record<string, unknown>): TypeResolution;
|
|
73
|
-
export declare const SIGNER_IDENTITY_CHECK_STATUSES: readonly ["ok", "signer_identity_mismatch", "no_signer", "no_registry_identity"];
|
|
118
|
+
export declare const SIGNER_IDENTITY_CHECK_STATUSES: readonly ["ok", "signer_identity_mismatch", "no_signer", "no_registry_identity", "key_derived_match", "key_derived_mismatch"];
|
|
74
119
|
export type SignerIdentityCheckStatus = (typeof SIGNER_IDENTITY_CHECK_STATUSES)[number];
|
|
75
120
|
export interface SignerIdentityCheck {
|
|
76
121
|
status: SignerIdentityCheckStatus;
|
|
@@ -78,6 +123,10 @@ export interface SignerIdentityCheck {
|
|
|
78
123
|
claimed?: string;
|
|
79
124
|
/** The identifier the registry records for the signing `kid`, when present. */
|
|
80
125
|
registered?: string;
|
|
126
|
+
/** The identifier derived from the envelope's `publicKey`, under a
|
|
127
|
+
* key-derived `signer.identifier` (hub ADR-0030 §2). Absent on a
|
|
128
|
+
* `key_derived_mismatch` whose key could not be decoded. */
|
|
129
|
+
derived?: string;
|
|
81
130
|
}
|
|
82
131
|
/**
|
|
83
132
|
* Check #14 — `signer.identifier` ↔ trust-registry `signerIdentity` cross-check
|
|
@@ -86,8 +135,25 @@ export interface SignerIdentityCheck {
|
|
|
86
135
|
* verifier derives the signer from the registry and skips the cross-check. A
|
|
87
136
|
* registry entry without a `signerIdentity` (legacy registry) yields
|
|
88
137
|
* `no_registry_identity`.
|
|
138
|
+
*
|
|
139
|
+
* A key-derived `signer.identifier` (one beginning `did:key:`, hub ADR-0030 §3)
|
|
140
|
+
* is checked against the envelope's `publicKey` instead, whatever the
|
|
141
|
+
* `bindingTier` and whatever the registry's source:
|
|
142
|
+
* - the identifier derived from `publicKey` differs → `key_derived_mismatch`,
|
|
143
|
+
* fatal, carrying `claimed` and `derived`;
|
|
144
|
+
* - it matches, and a registry entry for the `kid` records a different
|
|
145
|
+
* identifier → `signer_identity_mismatch`, fatal (a contradiction only
|
|
146
|
+
* lowers, so any registry may report it);
|
|
147
|
+
* - it matches otherwise → `key_derived_match`.
|
|
148
|
+
* `ok` ("matches the registry") is never reported under a key-derived
|
|
149
|
+
* identifier. Without `publicKey` the derivation cannot run; the registry
|
|
150
|
+
* cross-check runs as for any signer, except that its `ok` reads
|
|
151
|
+
* `no_registry_identity`, since nothing established the identifier.
|
|
89
152
|
*/
|
|
90
|
-
export declare function checkSignerIdentity(pkg: Record<string, unknown>, kid: string | undefined, registry: TrustRegistry | undefined
|
|
153
|
+
export declare function checkSignerIdentity(pkg: Record<string, unknown>, kid: string | undefined, registry: TrustRegistry | undefined,
|
|
154
|
+
/** The signature envelope's base64 SPKI `publicKey`. Read only when the
|
|
155
|
+
* signer's identifier is key-derived. */
|
|
156
|
+
publicKey?: string): SignerIdentityCheck;
|
|
91
157
|
export declare const CAPTURE_METHOD_VOCAB_STATUSES: readonly ["ok", "captureMethod_unknown", "producerProfile_bundle_unresolved", "no_capture_method"];
|
|
92
158
|
export type CaptureMethodVocabStatus = (typeof CAPTURE_METHOD_VOCAB_STATUSES)[number];
|
|
93
159
|
export interface CaptureMethodVocabCheck {
|
package/dist/checks.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// Content-canonicalization, content-hash, and typed-standards envelope checks
|
|
2
|
-
// (spec §9.2 checks #3, #4, #12, #13, #14, #15) + the package-level
|
|
3
|
-
// walker (#9) — browser-safe.
|
|
2
|
+
// (spec §9.2 checks #3, #4, #12, #13, #14, #15, #16) + the package-level
|
|
3
|
+
// blob-ref walker (#9) — browser-safe.
|
|
4
4
|
//
|
|
5
5
|
// Factored verbatim from the server `verify.ts` (WS2); these functions were
|
|
6
6
|
// already pure JS over the package object. The only environmental change is the
|
|
7
7
|
// blob-ref walker now threads an injected fetcher. Each check degrades gracefully
|
|
8
8
|
// for pre-v0.1 packages that omit the corresponding field.
|
|
9
|
-
import { computeEnvelopeHash, computeContentHashSha256, isMultihashContentHash, KNOWN_CANONICALIZATION_RULES, LEGACY_JSON_CANONICALIZATION, DATHERE_AG_JUPYTER_CANONICALIZATION, } from "./canonicalization.js";
|
|
9
|
+
import { computeEnvelopeHash, computeContentHashSha256, isMultihashContentHash, KNOWN_CANONICALIZATION_RULES, LEGACY_JSON_CANONICALIZATION, DATHERE_AG_JUPYTER_CANONICALIZATION, RAW_BYTES_CANONICALIZATION, } from "./canonicalization.js";
|
|
10
|
+
import { sha256Hex } from "./primitives.js";
|
|
10
11
|
import { captureVocabForProfile, resolveProfileType } from "./profiles.js";
|
|
11
|
-
import { isBlobRef, verifyBlobRef, } from "./blob-ref.js";
|
|
12
|
+
import { isBlobRef, parseBlobRef, verifyBlobRef, } from "./blob-ref.js";
|
|
13
|
+
import { isKeyDerivedIdentifier } from "./did-key.js";
|
|
14
|
+
import { compareKeyDerivedIdentifier } from "./self-certifying.js";
|
|
12
15
|
/**
|
|
13
16
|
* Recompute a package's envelope hash, routed by the §8.2 detection rule: v0.1
|
|
14
17
|
* packages (multihash `contentHash` present) hash via RFC 8785 JCS; pre-v0.1
|
|
@@ -65,6 +68,7 @@ export const CONTENT_HASH_STATUSES = [
|
|
|
65
68
|
'contentHash_no_supported_algorithm',
|
|
66
69
|
'unresolved_rule',
|
|
67
70
|
'legacy_relabeled',
|
|
71
|
+
'content_bytes_unavailable',
|
|
68
72
|
];
|
|
69
73
|
/** Multihash algorithms this verifier can recompute today (spec §8.2 lists
|
|
70
74
|
* sha256 required + sha3-256 / blake3 as registered alternates). */
|
|
@@ -86,8 +90,20 @@ const SUPPORTED_CONTENT_HASH_ALGORITHMS = ['sha256'];
|
|
|
86
90
|
* single-SHA-256 is RELABELED as `{ sha256: <hex> }` per §8.2 rather than
|
|
87
91
|
* recomputed; its integrity is established by check #1, so check #4 reports
|
|
88
92
|
* `legacy_relabeled`.
|
|
93
|
+
*
|
|
94
|
+
* raw-bytes/v1 with a BlobRef `output` (hub ADR-0029 §4): the fingerprinted
|
|
95
|
+
* bytes live outside the package. `outputBytes` are those bytes when the caller
|
|
96
|
+
* holds them.
|
|
97
|
+
* - `contentHash.sha256` differs from the hex of `output.ref` →
|
|
98
|
+
* `content_hash_mismatch`, decided from the package alone.
|
|
99
|
+
* - no `outputBytes` → `content_bytes_unavailable`: the bytes were not
|
|
100
|
+
* checked. Never `ok` without hashing.
|
|
101
|
+
* - otherwise the SHA-256 of `outputBytes` is compared with
|
|
102
|
+
* `contentHash.sha256` → `ok` / `content_hash_mismatch`.
|
|
103
|
+
* This function does no I/O; `verifyContentHashWithFetch` obtains the bytes
|
|
104
|
+
* through the injected fetcher and passes them here.
|
|
89
105
|
*/
|
|
90
|
-
export function verifyContentHash(pkg, resolution, legacyExternalHash) {
|
|
106
|
+
export function verifyContentHash(pkg, resolution, legacyExternalHash, outputBytes) {
|
|
91
107
|
const contentHash = pkg['contentHash'];
|
|
92
108
|
if (!isMultihashContentHash(contentHash)) {
|
|
93
109
|
return {
|
|
@@ -107,6 +123,21 @@ export function verifyContentHash(pkg, resolution, legacyExternalHash) {
|
|
|
107
123
|
contentHash,
|
|
108
124
|
};
|
|
109
125
|
}
|
|
126
|
+
const output = pkg['output'];
|
|
127
|
+
if (resolution.rule === RAW_BYTES_CANONICALIZATION && isBlobRef(output)) {
|
|
128
|
+
// sha256 is the only supported algorithm, so `checkable` is ['sha256'] here.
|
|
129
|
+
const signed = contentHash['sha256'];
|
|
130
|
+
if (signed !== parseBlobRef(output.ref).hash) {
|
|
131
|
+
// No file can hash to two different signed digests.
|
|
132
|
+
return { status: 'content_hash_mismatch', algorithms, contentHash };
|
|
133
|
+
}
|
|
134
|
+
if (outputBytes === undefined) {
|
|
135
|
+
return { status: 'content_bytes_unavailable', algorithms, contentHash };
|
|
136
|
+
}
|
|
137
|
+
return sha256Hex(outputBytes) === signed
|
|
138
|
+
? { status: 'ok', algorithms, matched: 'sha256', contentHash }
|
|
139
|
+
: { status: 'content_hash_mismatch', algorithms, contentHash };
|
|
140
|
+
}
|
|
110
141
|
for (const algo of checkable) {
|
|
111
142
|
let recomputed;
|
|
112
143
|
try {
|
|
@@ -130,6 +161,88 @@ export function verifyContentHash(pkg, resolution, legacyExternalHash) {
|
|
|
130
161
|
}
|
|
131
162
|
return { status: 'content_hash_mismatch', algorithms, contentHash };
|
|
132
163
|
}
|
|
164
|
+
/** Fetch a BlobRef's bytes through the injected fetcher (defaulting to
|
|
165
|
+
* `globalThis.fetch`, the same default check #9 uses). `undefined` on any
|
|
166
|
+
* failure. */
|
|
167
|
+
async function fetchBlobRefBytes(ref, options) {
|
|
168
|
+
try {
|
|
169
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
170
|
+
const response = await fetcher(ref.url, {
|
|
171
|
+
signal: options.signal ?? AbortSignal.timeout(15_000),
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok)
|
|
174
|
+
return undefined;
|
|
175
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check #4 with the raw-bytes/v1 BlobRef path wired to the injected fetcher
|
|
183
|
+
* (hub ADR-0029 §4). Identical to `verifyContentHash` for every other rule and
|
|
184
|
+
* shape. For a raw-bytes/v1 package whose `output` is a BlobRef, it obtains the
|
|
185
|
+
* file's bytes through `options.fetch` (an offline verifier supplies a local
|
|
186
|
+
* copy through that fetcher) and hashes them; bytes it cannot obtain leave the
|
|
187
|
+
* status at `content_bytes_unavailable`.
|
|
188
|
+
*/
|
|
189
|
+
export async function verifyContentHashWithFetch(pkg, resolution, legacyExternalHash, options = {}) {
|
|
190
|
+
const first = verifyContentHash(pkg, resolution, legacyExternalHash);
|
|
191
|
+
const output = pkg['output'];
|
|
192
|
+
if (first.status !== 'content_bytes_unavailable' || !isBlobRef(output)) {
|
|
193
|
+
return first;
|
|
194
|
+
}
|
|
195
|
+
const bytes = await fetchBlobRefBytes(output, options);
|
|
196
|
+
if (bytes === undefined)
|
|
197
|
+
return first;
|
|
198
|
+
return verifyContentHash(pkg, resolution, legacyExternalHash, bytes);
|
|
199
|
+
}
|
|
200
|
+
// --- Content-profile check (spec §9.2 check #16; hub ADR-0029 §5) ---
|
|
201
|
+
/** The known `metadata.contentProfile` values (spec §8.1.2). */
|
|
202
|
+
export const KNOWN_CONTENT_PROFILES = ['default', 'datHere'];
|
|
203
|
+
export const CONTENT_PROFILE_STATUSES = [
|
|
204
|
+
'ok',
|
|
205
|
+
'contentProfile_absent',
|
|
206
|
+
'contentProfile_unknown',
|
|
207
|
+
'contentProfile_inconsistent',
|
|
208
|
+
];
|
|
209
|
+
/**
|
|
210
|
+
* Check #16 — `metadata.contentProfile` (hub ADR-0029 §5).
|
|
211
|
+
* - key absent → `contentProfile_absent` (read as `"default"`, spec §8.1.2).
|
|
212
|
+
* - present and neither `"default"` nor `"datHere"` → `contentProfile_unknown`
|
|
213
|
+
* (reported, not passed; consistency is not judged).
|
|
214
|
+
* - present, known, `producerProfile` present, and the ADR-0006 §2 invariant
|
|
215
|
+
* fails (`contentProfile === "datHere"` iff `producerProfile` starts with
|
|
216
|
+
* `ai-assisted-analysis/datHere`) → `contentProfile_inconsistent`.
|
|
217
|
+
* - otherwise → `ok`.
|
|
218
|
+
* The invariant is compared only when both fields are present. No status is an
|
|
219
|
+
* integrity failure: both labels are signature-covered.
|
|
220
|
+
*/
|
|
221
|
+
export function checkContentProfile(pkg) {
|
|
222
|
+
const metadata = pkg['metadata'];
|
|
223
|
+
const raw = typeof metadata === 'object' && metadata !== null && !Array.isArray(metadata)
|
|
224
|
+
? metadata['contentProfile']
|
|
225
|
+
: undefined;
|
|
226
|
+
const producerProfile = typeof pkg['producerProfile'] === 'string' ? pkg['producerProfile'] : undefined;
|
|
227
|
+
const base = {
|
|
228
|
+
...(typeof raw === 'string' ? { contentProfile: raw } : {}),
|
|
229
|
+
...(producerProfile !== undefined ? { producerProfile } : {}),
|
|
230
|
+
};
|
|
231
|
+
if (raw === undefined) {
|
|
232
|
+
return { status: 'contentProfile_absent', ...base };
|
|
233
|
+
}
|
|
234
|
+
if (typeof raw !== 'string' || !KNOWN_CONTENT_PROFILES.includes(raw)) {
|
|
235
|
+
return { status: 'contentProfile_unknown', ...base };
|
|
236
|
+
}
|
|
237
|
+
if (producerProfile !== undefined) {
|
|
238
|
+
const isDatHere = raw === 'datHere';
|
|
239
|
+
const profileIsDatHere = producerProfile.startsWith('ai-assisted-analysis/datHere');
|
|
240
|
+
if (isDatHere !== profileIsDatHere) {
|
|
241
|
+
return { status: 'contentProfile_inconsistent', ...base };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { status: 'ok', ...base };
|
|
245
|
+
}
|
|
133
246
|
// --- Typed-standards envelope checks (spec §9.2 checks #12, #14, #15) ---
|
|
134
247
|
// v0.1 ratified node type URIs (spec §8.12.1). Recognized so check #12 resolves
|
|
135
248
|
// them; only `content/analysis/v1` (+ the lifecycle sub-types) are operationalized
|
|
@@ -174,6 +287,8 @@ export const SIGNER_IDENTITY_CHECK_STATUSES = [
|
|
|
174
287
|
'signer_identity_mismatch',
|
|
175
288
|
'no_signer',
|
|
176
289
|
'no_registry_identity',
|
|
290
|
+
'key_derived_match',
|
|
291
|
+
'key_derived_mismatch',
|
|
177
292
|
];
|
|
178
293
|
/**
|
|
179
294
|
* Check #14 — `signer.identifier` ↔ trust-registry `signerIdentity` cross-check
|
|
@@ -182,14 +297,53 @@ export const SIGNER_IDENTITY_CHECK_STATUSES = [
|
|
|
182
297
|
* verifier derives the signer from the registry and skips the cross-check. A
|
|
183
298
|
* registry entry without a `signerIdentity` (legacy registry) yields
|
|
184
299
|
* `no_registry_identity`.
|
|
300
|
+
*
|
|
301
|
+
* A key-derived `signer.identifier` (one beginning `did:key:`, hub ADR-0030 §3)
|
|
302
|
+
* is checked against the envelope's `publicKey` instead, whatever the
|
|
303
|
+
* `bindingTier` and whatever the registry's source:
|
|
304
|
+
* - the identifier derived from `publicKey` differs → `key_derived_mismatch`,
|
|
305
|
+
* fatal, carrying `claimed` and `derived`;
|
|
306
|
+
* - it matches, and a registry entry for the `kid` records a different
|
|
307
|
+
* identifier → `signer_identity_mismatch`, fatal (a contradiction only
|
|
308
|
+
* lowers, so any registry may report it);
|
|
309
|
+
* - it matches otherwise → `key_derived_match`.
|
|
310
|
+
* `ok` ("matches the registry") is never reported under a key-derived
|
|
311
|
+
* identifier. Without `publicKey` the derivation cannot run; the registry
|
|
312
|
+
* cross-check runs as for any signer, except that its `ok` reads
|
|
313
|
+
* `no_registry_identity`, since nothing established the identifier.
|
|
185
314
|
*/
|
|
186
|
-
export function checkSignerIdentity(pkg, kid, registry
|
|
315
|
+
export function checkSignerIdentity(pkg, kid, registry,
|
|
316
|
+
/** The signature envelope's base64 SPKI `publicKey`. Read only when the
|
|
317
|
+
* signer's identifier is key-derived. */
|
|
318
|
+
publicKey) {
|
|
187
319
|
const signer = pkg['signer'];
|
|
188
320
|
if (!signer || typeof signer !== 'object' || typeof signer.identifier !== 'string') {
|
|
189
321
|
return { status: 'no_signer' };
|
|
190
322
|
}
|
|
191
323
|
const entry = kid && registry ? registry.keys.find((k) => k.kid === kid) : undefined;
|
|
192
324
|
const registered = entry?.signerIdentity?.identifier;
|
|
325
|
+
if (isKeyDerivedIdentifier(signer.identifier)) {
|
|
326
|
+
const comparison = compareKeyDerivedIdentifier(signer, publicKey);
|
|
327
|
+
if (comparison && !comparison.match) {
|
|
328
|
+
return {
|
|
329
|
+
status: 'key_derived_mismatch',
|
|
330
|
+
claimed: comparison.claimed,
|
|
331
|
+
...(comparison.derived !== undefined ? { derived: comparison.derived } : {}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (registered && registered !== signer.identifier) {
|
|
335
|
+
return { status: 'signer_identity_mismatch', claimed: signer.identifier, registered };
|
|
336
|
+
}
|
|
337
|
+
if (comparison) {
|
|
338
|
+
return {
|
|
339
|
+
status: 'key_derived_match',
|
|
340
|
+
claimed: signer.identifier,
|
|
341
|
+
derived: comparison.derived,
|
|
342
|
+
...(registered ? { registered } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
return { status: 'no_registry_identity', claimed: signer.identifier };
|
|
346
|
+
}
|
|
193
347
|
if (!registered) {
|
|
194
348
|
return { status: 'no_registry_identity', claimed: signer.identifier };
|
|
195
349
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base-58 encoding, Bitcoin alphabet: big-endian base conversion, one leading
|
|
3
|
+
* `1` per leading zero byte (draft-msporny-base58-03). Not exported from the
|
|
4
|
+
* package entry point; the derivation below is its one shipped caller.
|
|
5
|
+
*/
|
|
6
|
+
export declare function base58btcEncode(bytes: Uint8Array): string;
|
|
7
|
+
/** The prefix that makes a `signer.identifier` key-derived (ADR-0030 §3). */
|
|
8
|
+
export declare const KEY_DERIVED_IDENTIFIER_PREFIX = "did:key:";
|
|
9
|
+
/**
|
|
10
|
+
* Derive the key-derived identifier from a base64 SPKI DER Ed25519 public key
|
|
11
|
+
* (the signature envelope's `publicKey`), per ADR-0030 §2:
|
|
12
|
+
* 1. base64-decode to 44 bytes whose first 12 are the Ed25519 SPKI prefix;
|
|
13
|
+
* 2. take the trailing 32 bytes (the raw RFC 8032 public key);
|
|
14
|
+
* 3. prepend `ed 01`;
|
|
15
|
+
* 4. base58btc-encode;
|
|
16
|
+
* 5. prefix `did:key:z`.
|
|
17
|
+
* Steps 1-2 are `extractRawPublicKey`, which throws on a malformed key; so
|
|
18
|
+
* does this function.
|
|
19
|
+
*/
|
|
20
|
+
export declare function deriveKeyDerivedIdentifier(publicKeyB64Der: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a `signer.identifier` is key-derived: it begins `did:key:`
|
|
23
|
+
* (ADR-0030 §3). The prefix alone decides — a `did:key:u…` spelling, or a
|
|
24
|
+
* `did:key:z…` for another key type, is key-derived and can never equal an
|
|
25
|
+
* Ed25519 derivation, so it is a fatal mismatch. `bindingTier` does not enter
|
|
26
|
+
* this decision.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isKeyDerivedIdentifier(identifier: unknown): identifier is string;
|
package/dist/did-key.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The key-derived signer identifier (hub ADR-0030 §2, §3) — browser-safe.
|
|
2
|
+
//
|
|
3
|
+
// A signer with no domain names itself by its public key: `did:key` in its
|
|
4
|
+
// base58btc (`z`) form for an Ed25519 key (The did:key Method v0.9, W3C CCG
|
|
5
|
+
// draft; multicodec `ed25519-pub` 0xed; multibase `z`). A verifier recomputes
|
|
6
|
+
// the identifier from the envelope's `publicKey` and compares it with the
|
|
7
|
+
// package's `signer.identifier` byte for byte. It never decodes the claimed
|
|
8
|
+
// string, so this module has an encoder and no decoder.
|
|
9
|
+
//
|
|
10
|
+
// The base-58 encoder is written in place (ADR-0030 §6): no runtime
|
|
11
|
+
// dependency, no `Buffer`, no Node built-in.
|
|
12
|
+
import { extractRawPublicKey } from "./signature.js";
|
|
13
|
+
/** The Bitcoin base-58 alphabet (draft-msporny-base58-03). */
|
|
14
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
15
|
+
/**
|
|
16
|
+
* Base-58 encoding, Bitcoin alphabet: big-endian base conversion, one leading
|
|
17
|
+
* `1` per leading zero byte (draft-msporny-base58-03). Not exported from the
|
|
18
|
+
* package entry point; the derivation below is its one shipped caller.
|
|
19
|
+
*/
|
|
20
|
+
export function base58btcEncode(bytes) {
|
|
21
|
+
let zeros = 0;
|
|
22
|
+
while (zeros < bytes.length && bytes[zeros] === 0)
|
|
23
|
+
zeros++;
|
|
24
|
+
// Base-58 digits, least significant first.
|
|
25
|
+
const digits = [];
|
|
26
|
+
for (let i = zeros; i < bytes.length; i++) {
|
|
27
|
+
let carry = bytes[i];
|
|
28
|
+
for (let j = 0; j < digits.length; j++) {
|
|
29
|
+
carry += digits[j] * 256;
|
|
30
|
+
digits[j] = carry % 58;
|
|
31
|
+
carry = Math.floor(carry / 58);
|
|
32
|
+
}
|
|
33
|
+
while (carry > 0) {
|
|
34
|
+
digits.push(carry % 58);
|
|
35
|
+
carry = Math.floor(carry / 58);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
let out = '1'.repeat(zeros);
|
|
39
|
+
for (let k = digits.length - 1; k >= 0; k--)
|
|
40
|
+
out += BASE58_ALPHABET[digits[k]];
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** The prefix that makes a `signer.identifier` key-derived (ADR-0030 §3). */
|
|
44
|
+
export const KEY_DERIVED_IDENTIFIER_PREFIX = 'did:key:';
|
|
45
|
+
/** The unsigned-varint encoding of multicodec `ed25519-pub` (0xed). */
|
|
46
|
+
const ED25519_PUB_MULTICODEC = Uint8Array.from([0xed, 0x01]);
|
|
47
|
+
/**
|
|
48
|
+
* Derive the key-derived identifier from a base64 SPKI DER Ed25519 public key
|
|
49
|
+
* (the signature envelope's `publicKey`), per ADR-0030 §2:
|
|
50
|
+
* 1. base64-decode to 44 bytes whose first 12 are the Ed25519 SPKI prefix;
|
|
51
|
+
* 2. take the trailing 32 bytes (the raw RFC 8032 public key);
|
|
52
|
+
* 3. prepend `ed 01`;
|
|
53
|
+
* 4. base58btc-encode;
|
|
54
|
+
* 5. prefix `did:key:z`.
|
|
55
|
+
* Steps 1-2 are `extractRawPublicKey`, which throws on a malformed key; so
|
|
56
|
+
* does this function.
|
|
57
|
+
*/
|
|
58
|
+
export function deriveKeyDerivedIdentifier(publicKeyB64Der) {
|
|
59
|
+
const raw = extractRawPublicKey(publicKeyB64Der);
|
|
60
|
+
const mc = new Uint8Array(ED25519_PUB_MULTICODEC.length + raw.length);
|
|
61
|
+
mc.set(ED25519_PUB_MULTICODEC, 0);
|
|
62
|
+
mc.set(raw, ED25519_PUB_MULTICODEC.length);
|
|
63
|
+
return `${KEY_DERIVED_IDENTIFIER_PREFIX}z${base58btcEncode(mc)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a `signer.identifier` is key-derived: it begins `did:key:`
|
|
67
|
+
* (ADR-0030 §3). The prefix alone decides — a `did:key:u…` spelling, or a
|
|
68
|
+
* `did:key:z…` for another key type, is key-derived and can never equal an
|
|
69
|
+
* Ed25519 derivation, so it is a fatal mismatch. `bindingTier` does not enter
|
|
70
|
+
* this decision.
|
|
71
|
+
*/
|
|
72
|
+
export function isKeyDerivedIdentifier(identifier) {
|
|
73
|
+
return typeof identifier === 'string' && identifier.startsWith(KEY_DERIVED_IDENTIFIER_PREFIX);
|
|
74
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './attestation.ts';
|
|
|
6
6
|
export * from './blob-ref.ts';
|
|
7
7
|
export * from './signature.ts';
|
|
8
8
|
export * from './trust-registry.ts';
|
|
9
|
+
export { deriveKeyDerivedIdentifier, isKeyDerivedIdentifier, KEY_DERIVED_IDENTIFIER_PREFIX, } from './did-key.ts';
|
|
9
10
|
export * from './checks.ts';
|
|
10
11
|
export * from './lifecycle.ts';
|
|
11
12
|
export * from './rekor.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @typedstandards/verify-core (in-repo) — the portable, browser-safe
|
|
1
|
+
// @typedstandards/verify-core (in-repo) — the portable, browser-safe record
|
|
2
2
|
// verification core (civic-ai-tools-website#116 WS2).
|
|
3
3
|
//
|
|
4
4
|
// Pure functions over provided data: no `node:crypto` / `fs` / `path` / `process`
|
|
@@ -20,6 +20,7 @@ export * from "./attestation.js";
|
|
|
20
20
|
export * from "./blob-ref.js";
|
|
21
21
|
export * from "./signature.js";
|
|
22
22
|
export * from "./trust-registry.js";
|
|
23
|
+
export { deriveKeyDerivedIdentifier, isKeyDerivedIdentifier, KEY_DERIVED_IDENTIFIER_PREFIX, } from "./did-key.js";
|
|
23
24
|
export * from "./checks.js";
|
|
24
25
|
export * from "./lifecycle.js";
|
|
25
26
|
export * from "./rekor.js";
|
package/dist/lifecycle.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// verification logic (factored from the server `verify.ts`). The DB + blob fetch
|
|
12
12
|
// orchestration that assembles the chain stays in the server `lifecycle.ts`,
|
|
13
13
|
// which imports these. WS2's portable orchestrator (`verify.ts` →
|
|
14
|
-
// `
|
|
14
|
+
// `verifyRecord`) consumes lifecycle at STATE depth (the sidecar's
|
|
15
15
|
// status/withdrawnAt/reason) rather than re-running the chain — verifying the
|
|
16
16
|
// signed attestation chain independently in the browser is civic-ai-tools-website#119.
|
|
17
17
|
import { computeEnvelopeHash } from "./canonicalization.js";
|
package/dist/profiles.js
CHANGED
|
@@ -10,6 +10,12 @@ export const PROFILE_CAPTURE_VOCAB = {
|
|
|
10
10
|
'claude-code-jsonl-readback',
|
|
11
11
|
'claude-code-self-report',
|
|
12
12
|
],
|
|
13
|
+
// Status note, 2026-09-21 (hub ADR-0029 §2, Q32 still open): a second
|
|
14
|
+
// stand-in entry. A program computed the content from inputs pinned by hash;
|
|
15
|
+
// `script-run` = a packaging program read files already on disk, `tool-emitted`
|
|
16
|
+
// = the computing program wrote the package itself. The guidance-bundle routing
|
|
17
|
+
// convention that replaces this table is not settled by this entry.
|
|
18
|
+
'scripted-recomputation': ['script-run', 'tool-emitted'],
|
|
13
19
|
};
|
|
14
20
|
/**
|
|
15
21
|
* Resolve a package's Producer Profile *type* (the segment before the first `/`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { KeyTrustResult, TrustRegistryProvenance } from './trust-registry.ts';
|
|
2
|
+
/** The outcome of comparing a key-derived `signer.identifier` with the
|
|
3
|
+
* identifier derived from the envelope's `publicKey` (ADR-0030 §2). */
|
|
4
|
+
export interface KeyDerivedComparison {
|
|
5
|
+
claimed: string;
|
|
6
|
+
/** Absent when the derivation failed (a malformed key). */
|
|
7
|
+
derived?: string;
|
|
8
|
+
match: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Compare the package's `signer.identifier` with the identifier derived from
|
|
12
|
+
* the envelope's `publicKey`. Returns `null` when the self-certifying check
|
|
13
|
+
* does not run: no signer, an identifier that is not key-derived, or no
|
|
14
|
+
* envelope key. A failed derivation is a mismatch with no `derived` value.
|
|
15
|
+
*/
|
|
16
|
+
export declare function compareKeyDerivedIdentifier(signer: unknown, publicKey: string | undefined): KeyDerivedComparison | null;
|
|
17
|
+
/**
|
|
18
|
+
* G2-B for a key-derived identifier, whether or not it matched its envelope
|
|
19
|
+
* key (ADR-0030 §4 rule 3 as amended; spec v0.1.9 §9.4): only a registry
|
|
20
|
+
* fetched from the declared `trustRegistryUrl` can raise the status. Returns
|
|
21
|
+
* `base` unless it is a raising verdict (`active`, `deprecated_valid`) from a
|
|
22
|
+
* bundle-carried or provenance-less registry; that verdict is ignored and the
|
|
23
|
+
* status is what the envelope alone yields with no registry supplied
|
|
24
|
+
* (`registry_unavailable` with a `kid`, `legacy_embedded` without),
|
|
25
|
+
* `verified: false`. Lowering and unlisted verdicts pass through.
|
|
26
|
+
*/
|
|
27
|
+
export declare function applyKeyDerivedRegistryRule(base: KeyTrustResult, provenance: TrustRegistryProvenance | undefined): KeyTrustResult;
|
|
28
|
+
/**
|
|
29
|
+
* Apply ADR-0030 §4 rules 2-3, as amended at gate G2, to the registry-path
|
|
30
|
+
* verdict `base`, for a package whose key-derived identifier MATCHED its
|
|
31
|
+
* envelope key. (A mismatch takes `applyKeyDerivedRegistryRule` alone.)
|
|
32
|
+
*
|
|
33
|
+
* For a key-derived identifier, at any `bindingTier`:
|
|
34
|
+
* - A registry from the declared `trustRegistryUrl` that lists the
|
|
35
|
+
* envelope's `(kid, publicKey)` decides: its verdict stands.
|
|
36
|
+
* - Any other registry (bundle-carried, or no stated provenance)
|
|
37
|
+
* contributes only a lowering verdict (`revoked`, `deprecated_invalid`).
|
|
38
|
+
* A raising one (`active`, `deprecated_valid`) is ignored.
|
|
39
|
+
* - With no registry listing the key, or a raising verdict ignored, the
|
|
40
|
+
* status is `self_certified` at `pseudonymous`; at any other tier it is
|
|
41
|
+
* what the envelope alone yields with no registry supplied
|
|
42
|
+
* (`registry_unavailable` with a `kid`, `legacy_embedded` without) — never
|
|
43
|
+
* `active`, never `verified: true`. `self_certified` is reserved for the
|
|
44
|
+
* `pseudonymous` rung (§3).
|
|
45
|
+
*/
|
|
46
|
+
export declare function applySelfCertifiedKeyTrust(base: KeyTrustResult, bindingTier: unknown, provenance: TrustRegistryProvenance | undefined): KeyTrustResult;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The self-certifying signer's key-trust rule (hub ADR-0030 §3, §4) —
|
|
2
|
+
// browser-safe. Internal to verify-core: `verifyRecord` applies it; the
|
|
3
|
+
// package entry point does not re-export it.
|
|
4
|
+
import { deriveKeyDerivedIdentifier, isKeyDerivedIdentifier } from "./did-key.js";
|
|
5
|
+
/** The one `bindingTier` under which a derived match is `self_certified`. */
|
|
6
|
+
const SELF_CERTIFYING_TIER = 'pseudonymous';
|
|
7
|
+
/** Verdicts that say the key must not be trusted for this signature. A
|
|
8
|
+
* bundle-carried registry may report these about a self-certified signer. */
|
|
9
|
+
const LOWERING_STATUSES = new Set(['revoked', 'deprecated_invalid']);
|
|
10
|
+
/** Verdicts that mean no registry listed the envelope's `(kid, publicKey)`. */
|
|
11
|
+
const UNLISTED_STATUSES = new Set([
|
|
12
|
+
'registry_unavailable',
|
|
13
|
+
'unknown_key',
|
|
14
|
+
'legacy_embedded',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Compare the package's `signer.identifier` with the identifier derived from
|
|
18
|
+
* the envelope's `publicKey`. Returns `null` when the self-certifying check
|
|
19
|
+
* does not run: no signer, an identifier that is not key-derived, or no
|
|
20
|
+
* envelope key. A failed derivation is a mismatch with no `derived` value.
|
|
21
|
+
*/
|
|
22
|
+
export function compareKeyDerivedIdentifier(signer, publicKey) {
|
|
23
|
+
if (!signer || typeof signer !== 'object')
|
|
24
|
+
return null;
|
|
25
|
+
const claimed = signer.identifier;
|
|
26
|
+
if (!isKeyDerivedIdentifier(claimed) || !publicKey)
|
|
27
|
+
return null;
|
|
28
|
+
let derived;
|
|
29
|
+
try {
|
|
30
|
+
derived = deriveKeyDerivedIdentifier(publicKey);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { claimed, match: false };
|
|
34
|
+
}
|
|
35
|
+
return { claimed, derived, match: derived === claimed };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* G2-B for a key-derived identifier, whether or not it matched its envelope
|
|
39
|
+
* key (ADR-0030 §4 rule 3 as amended; spec v0.1.9 §9.4): only a registry
|
|
40
|
+
* fetched from the declared `trustRegistryUrl` can raise the status. Returns
|
|
41
|
+
* `base` unless it is a raising verdict (`active`, `deprecated_valid`) from a
|
|
42
|
+
* bundle-carried or provenance-less registry; that verdict is ignored and the
|
|
43
|
+
* status is what the envelope alone yields with no registry supplied
|
|
44
|
+
* (`registry_unavailable` with a `kid`, `legacy_embedded` without),
|
|
45
|
+
* `verified: false`. Lowering and unlisted verdicts pass through.
|
|
46
|
+
*/
|
|
47
|
+
export function applyKeyDerivedRegistryRule(base, provenance) {
|
|
48
|
+
if (provenance === 'declared-url')
|
|
49
|
+
return base;
|
|
50
|
+
if (UNLISTED_STATUSES.has(base.status) || LOWERING_STATUSES.has(base.status))
|
|
51
|
+
return base;
|
|
52
|
+
return base.kid !== undefined
|
|
53
|
+
? { status: 'registry_unavailable', verified: false, kid: base.kid }
|
|
54
|
+
: { status: 'legacy_embedded', verified: false };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Apply ADR-0030 §4 rules 2-3, as amended at gate G2, to the registry-path
|
|
58
|
+
* verdict `base`, for a package whose key-derived identifier MATCHED its
|
|
59
|
+
* envelope key. (A mismatch takes `applyKeyDerivedRegistryRule` alone.)
|
|
60
|
+
*
|
|
61
|
+
* For a key-derived identifier, at any `bindingTier`:
|
|
62
|
+
* - A registry from the declared `trustRegistryUrl` that lists the
|
|
63
|
+
* envelope's `(kid, publicKey)` decides: its verdict stands.
|
|
64
|
+
* - Any other registry (bundle-carried, or no stated provenance)
|
|
65
|
+
* contributes only a lowering verdict (`revoked`, `deprecated_invalid`).
|
|
66
|
+
* A raising one (`active`, `deprecated_valid`) is ignored.
|
|
67
|
+
* - With no registry listing the key, or a raising verdict ignored, the
|
|
68
|
+
* status is `self_certified` at `pseudonymous`; at any other tier it is
|
|
69
|
+
* what the envelope alone yields with no registry supplied
|
|
70
|
+
* (`registry_unavailable` with a `kid`, `legacy_embedded` without) — never
|
|
71
|
+
* `active`, never `verified: true`. `self_certified` is reserved for the
|
|
72
|
+
* `pseudonymous` rung (§3).
|
|
73
|
+
*/
|
|
74
|
+
export function applySelfCertifiedKeyTrust(base, bindingTier, provenance) {
|
|
75
|
+
const listed = !UNLISTED_STATUSES.has(base.status);
|
|
76
|
+
if (listed && (provenance === 'declared-url' || LOWERING_STATUSES.has(base.status))) {
|
|
77
|
+
return base;
|
|
78
|
+
}
|
|
79
|
+
if (bindingTier !== SELF_CERTIFYING_TIER) {
|
|
80
|
+
// Unlisted: the registry path's own verdict is unchanged. Listed with a
|
|
81
|
+
// raising verdict from a registry that may not raise: the envelope alone.
|
|
82
|
+
return applyKeyDerivedRegistryRule(base, provenance);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
status: 'self_certified',
|
|
86
|
+
verified: false,
|
|
87
|
+
...(base.kid !== undefined ? { kid: base.kid } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
package/dist/trust-registry.d.ts
CHANGED
|
@@ -16,14 +16,30 @@ export interface TrustRegistryKey {
|
|
|
16
16
|
export interface TrustRegistry {
|
|
17
17
|
keys: TrustRegistryKey[];
|
|
18
18
|
}
|
|
19
|
-
export declare const KEY_TRUST_STATUSES: readonly ["active", "deprecated_valid", "deprecated_invalid", "revoked", "unknown_key", "registry_unavailable", "legacy_embedded"];
|
|
19
|
+
export declare const KEY_TRUST_STATUSES: readonly ["active", "deprecated_valid", "deprecated_invalid", "revoked", "unknown_key", "registry_unavailable", "legacy_embedded", "self_certified"];
|
|
20
20
|
export type KeyTrustStatus = (typeof KEY_TRUST_STATUSES)[number];
|
|
21
|
+
/**
|
|
22
|
+
* Where a supplied registry came from (hub ADR-0030 §4 rule 3). This module
|
|
23
|
+
* still has no opinion on HOW a registry is obtained; the caller states it,
|
|
24
|
+
* because for a signer with a key-derived identifier the answer decides what
|
|
25
|
+
* the registry may do:
|
|
26
|
+
* - `declared-url` — fetched from the view's declared `trustRegistryUrl` (or
|
|
27
|
+
* `trustRegistryUrlLegacy`). Its verdict about the key stands.
|
|
28
|
+
* - `bundle` — carried in the bundle (the `?inline=1` form's
|
|
29
|
+
* `trustRegistry`), or obtained any other way. It may lower a
|
|
30
|
+
* self-certified signer's status (`revoked`, `deprecated_invalid`) and
|
|
31
|
+
* never raise it.
|
|
32
|
+
* A registry supplied with no stated provenance is treated as `bundle`.
|
|
33
|
+
* Signers whose identifier is not key-derived are unaffected by provenance.
|
|
34
|
+
*/
|
|
35
|
+
export declare const TRUST_REGISTRY_PROVENANCES: readonly ["declared-url", "bundle"];
|
|
36
|
+
export type TrustRegistryProvenance = (typeof TRUST_REGISTRY_PROVENANCES)[number];
|
|
21
37
|
export interface KeyTrustResult {
|
|
22
38
|
status: KeyTrustStatus;
|
|
23
39
|
/** `true` iff the status is `active` or `deprecated_valid`. Legacy-embedded
|
|
24
|
-
* signatures are intentionally surfaced as
|
|
25
|
-
* trust registry
|
|
26
|
-
* rather than failed. */
|
|
40
|
+
* and self-certified signatures are intentionally surfaced as
|
|
41
|
+
* `verified: false` because no trust registry vouches for them — the UI
|
|
42
|
+
* renders them as neutral rather than failed. */
|
|
27
43
|
verified: boolean;
|
|
28
44
|
/** The registry `kid` when available. Omitted for `legacy_embedded` /
|
|
29
45
|
* pre-registry packages because the signature has no kid to report. */
|
package/dist/trust-registry.js
CHANGED
|
@@ -22,7 +22,23 @@ export const KEY_TRUST_STATUSES = [
|
|
|
22
22
|
'unknown_key', // (kid, publicKey) pair not found in registry
|
|
23
23
|
'registry_unavailable', // registry could not be loaded
|
|
24
24
|
'legacy_embedded', // signed with an embedded key, no registry kid stored
|
|
25
|
+
'self_certified', // key-derived identifier matches the key; no registry vouches (hub ADR-0030 §3)
|
|
25
26
|
];
|
|
27
|
+
/**
|
|
28
|
+
* Where a supplied registry came from (hub ADR-0030 §4 rule 3). This module
|
|
29
|
+
* still has no opinion on HOW a registry is obtained; the caller states it,
|
|
30
|
+
* because for a signer with a key-derived identifier the answer decides what
|
|
31
|
+
* the registry may do:
|
|
32
|
+
* - `declared-url` — fetched from the view's declared `trustRegistryUrl` (or
|
|
33
|
+
* `trustRegistryUrlLegacy`). Its verdict about the key stands.
|
|
34
|
+
* - `bundle` — carried in the bundle (the `?inline=1` form's
|
|
35
|
+
* `trustRegistry`), or obtained any other way. It may lower a
|
|
36
|
+
* self-certified signer's status (`revoked`, `deprecated_invalid`) and
|
|
37
|
+
* never raise it.
|
|
38
|
+
* A registry supplied with no stated provenance is treated as `bundle`.
|
|
39
|
+
* Signers whose identifier is not key-derived are unaffected by provenance.
|
|
40
|
+
*/
|
|
41
|
+
export const TRUST_REGISTRY_PROVENANCES = ['declared-url', 'bundle'];
|
|
26
42
|
/**
|
|
27
43
|
* Build a `KeyTrustResult` for a package whose signature predates the trust
|
|
28
44
|
* registry — i.e. has a valid public key but no `kid`. We accept that the
|
package/dist/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface SignerIdentity {
|
|
|
16
16
|
* the per-profile vocabulary table in `profiles.ts` is the single source that
|
|
17
17
|
* enumerates these.
|
|
18
18
|
*/
|
|
19
|
-
export type CaptureMethod = 'chat-flow-stream' | 'claude-code-jsonl-readback' | 'claude-code-self-report';
|
|
19
|
+
export type CaptureMethod = 'chat-flow-stream' | 'claude-code-jsonl-readback' | 'claude-code-self-report' | 'script-run' | 'tool-emitted';
|
|
20
20
|
/**
|
|
21
21
|
* The subset of the WHATWG `fetch` signature verify-core depends on. Network
|
|
22
22
|
* I/O is INJECTED rather than imported so the same code runs on the server (its
|
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
|
-
import { type KeyTrustResult, type TrustRegistry } from './trust-registry.ts';
|
|
4
|
-
import { type ContentCanonicalizationResolution, type ContentHashCheck, type TypeResolution, type SignerIdentityCheck, type CaptureMethodVocabCheck, type BlobRefVerification } from './checks.ts';
|
|
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';
|
|
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`). */
|
|
@@ -48,7 +48,7 @@ export interface CommitmentLifecycleState {
|
|
|
48
48
|
reinstatedReason?: string;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* Everything `
|
|
51
|
+
* Everything `verifyRecord` needs, aligned to the WS1 commitment sidecar +
|
|
52
52
|
* the package JSON the sidecar's `packageUrl` resolves to.
|
|
53
53
|
*/
|
|
54
54
|
export interface VerifyInput {
|
|
@@ -97,7 +97,13 @@ export interface VerifyDeps {
|
|
|
97
97
|
/** Parsed trust registry (server: loaded; browser: fetched from the sidecar's
|
|
98
98
|
* `trustRegistryUrl`). */
|
|
99
99
|
registry: TrustRegistry | undefined;
|
|
100
|
-
/**
|
|
100
|
+
/** Where `registry` came from (hub ADR-0030 §4 rule 3): `declared-url` when
|
|
101
|
+
* it was fetched from the view's declared `trustRegistryUrl`, `bundle` when
|
|
102
|
+
* it was carried in the bundle. Read only for a signer whose identifier is
|
|
103
|
+
* key-derived; absent is treated as `bundle`. */
|
|
104
|
+
registryProvenance?: TrustRegistryProvenance;
|
|
105
|
+
/** Injected fetcher for #8 / #9, and for #4 under raw-bytes/v1 when `output`
|
|
106
|
+
* is a BlobRef. Defaults to `globalThis.fetch`. */
|
|
101
107
|
fetch?: FetchLike;
|
|
102
108
|
/** Optional server-deeper lifecycle resolution (the signed attestation chain).
|
|
103
109
|
* When provided it is surfaced verbatim, preserving the server route's current
|
|
@@ -144,11 +150,28 @@ export interface VerifyResult {
|
|
|
144
150
|
typeResolution: TypeResolution | null;
|
|
145
151
|
signerIdentity: SignerIdentityCheck | null;
|
|
146
152
|
captureMethodVocab: CaptureMethodVocabCheck | null;
|
|
153
|
+
/** Check #16 — `metadata.contentProfile` (hub ADR-0029 §5). */
|
|
154
|
+
contentProfile: ContentProfileCheck | null;
|
|
147
155
|
lifecycle: LifecycleResolution;
|
|
148
156
|
}
|
|
149
157
|
/**
|
|
150
158
|
* Run the §9.2 check suite. The step order mirrors the server route exactly
|
|
151
159
|
* (notably: Rekor before key-trust, because a deprecated-key trust decision is
|
|
152
160
|
* time-bounded by the Rekor `integratedTime`).
|
|
161
|
+
*
|
|
162
|
+
* Named `verifyEvidence` before the 2026-08-19 vocabulary settlement; that
|
|
163
|
+
* name remains exported as a deprecated alias of this exact function.
|
|
153
164
|
*/
|
|
154
|
-
export declare function
|
|
165
|
+
export declare function verifyRecord(input: VerifyInput, deps: VerifyDeps): Promise<VerifyResult>;
|
|
166
|
+
/**
|
|
167
|
+
* @deprecated Renamed to {@link verifyRecord} in the 2026-08-19 vocabulary
|
|
168
|
+
* settlement — "evidence" is retired from the artifact/infrastructure brand
|
|
169
|
+
* role and retained only for the epistemic Question/Evidence/Claim role (spec
|
|
170
|
+
* §6.3, Appendix J; migration class `alias-and-deprecate`).
|
|
171
|
+
*
|
|
172
|
+
* This is the SAME function object, not a wrapper: `verifyEvidence ===
|
|
173
|
+
* verifyRecord`. Existing callers keep working unchanged and get identical
|
|
174
|
+
* results; the alias is removed no earlier than this package's next MAJOR
|
|
175
|
+
* version. New code should import `verifyRecord`.
|
|
176
|
+
*/
|
|
177
|
+
export declare const verifyEvidence: typeof verifyRecord;
|
package/dist/verify.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Portable verification orchestrator (spec §9.2) — browser-safe.
|
|
2
2
|
//
|
|
3
|
-
// `
|
|
3
|
+
// `verifyRecord` runs the §9.2 check suite over an already-resolved package +
|
|
4
4
|
// its proofs, returning one structured verdict. It is the SINGLE orchestration
|
|
5
5
|
// both consumers run: the civicaitools.org server route (which injects its
|
|
6
6
|
// `fetch`, its loaded trust registry, and — to preserve its current output — its
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
// test pins this.
|
|
12
12
|
//
|
|
13
13
|
// `VerifyInput` is shaped to align field-for-field with the WS1 commitment
|
|
14
|
-
// sidecar (`buildCommitmentView`) so WS3 can wire sidecar →
|
|
14
|
+
// sidecar (`buildCommitmentView`) so WS3 can wire sidecar → verifyRecord with
|
|
15
15
|
// no glue. The server route adapts its DB row to the same shape.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
17
|
+
// The entry point was named `verifyEvidence` before the 2026-08-19 vocabulary
|
|
18
|
+
// settlement (spec Appendix J, migration class `alias-and-deprecate`). That
|
|
19
|
+
// name is still exported, still the same function object, and still works;
|
|
20
|
+
// see the deprecated alias at the bottom of this file.
|
|
21
|
+
//
|
|
22
|
+
// Check depth: fully client-side for #1/#2/#3/#4/#5/#6/#9/#12/#13/#14/#15/#16; #7 (RFC
|
|
18
23
|
// 3161) is PRESENCE (`hasTimestamp`) AND, when a token is present, cryptographic TSA
|
|
19
24
|
// verification against the pinned FreeTSA anchor (civic-ai-tools-website#119 P2a —
|
|
20
25
|
// ECDSA-P384 over the signed TSTInfo, message-imprint match, genTime-in-validity);
|
|
@@ -29,14 +34,18 @@ import { verifyRekorEntry } from "./rekor.js";
|
|
|
29
34
|
import { verifyRekorInclusion, } from "./rekor-inclusion.js";
|
|
30
35
|
import { verifyRfc3161Timestamp } from "./rfc3161.js";
|
|
31
36
|
import { verifyKeyTrust, legacyEmbeddedKeyTrust, } from "./trust-registry.js";
|
|
32
|
-
import {
|
|
37
|
+
import { applyKeyDerivedRegistryRule, applySelfCertifiedKeyTrust, compareKeyDerivedIdentifier, } from "./self-certifying.js";
|
|
38
|
+
import { resolveContentCanonicalization, verifyContentHashWithFetch, resolvePackageType, checkSignerIdentity, checkCaptureMethodVocab, checkContentProfile, verifyPackageBlobRefs, } from "./checks.js";
|
|
33
39
|
import { resolveLifecycleFromLegacyColumns, } from "./lifecycle.js";
|
|
34
40
|
/**
|
|
35
41
|
* Run the §9.2 check suite. The step order mirrors the server route exactly
|
|
36
42
|
* (notably: Rekor before key-trust, because a deprecated-key trust decision is
|
|
37
43
|
* time-bounded by the Rekor `integratedTime`).
|
|
44
|
+
*
|
|
45
|
+
* Named `verifyEvidence` before the 2026-08-19 vocabulary settlement; that
|
|
46
|
+
* name remains exported as a deprecated alias of this exact function.
|
|
38
47
|
*/
|
|
39
|
-
export async function
|
|
48
|
+
export async function verifyRecord(input, deps) {
|
|
40
49
|
const { package: pkg, packageHash } = input;
|
|
41
50
|
// Step 1 — recompute the envelope hash (checks #1 + #13). Tri-state: a present
|
|
42
51
|
// package recomputes to `verified` / `altered`; a null package is `unavailable`
|
|
@@ -132,18 +141,38 @@ export async function verifyEvidence(input, deps) {
|
|
|
132
141
|
else if (sigPublicKey) {
|
|
133
142
|
keyTrust = legacyEmbeddedKeyTrust();
|
|
134
143
|
}
|
|
135
|
-
// Step
|
|
144
|
+
// Step 4b — the self-certifying signer (hub ADR-0030 §3-§4, rule 3 as
|
|
145
|
+
// amended at G2; spec v0.1.9 §9.4). Runs only when the package is present,
|
|
146
|
+
// its `signer.identifier` is key-derived, and the envelope carries a
|
|
147
|
+
// `publicKey`. For a key-derived identifier, match or mismatch, only a
|
|
148
|
+
// registry fetched from the declared URL can raise the status; a
|
|
149
|
+
// bundle-carried or provenance-less one contributes only lowering verdicts.
|
|
150
|
+
// On a match, rules 2-3 then decide the status (`self_certified` at
|
|
151
|
+
// `pseudonymous`); on a mismatch (fatal at check #14) the lowering-only rule
|
|
152
|
+
// applies to the registry-path verdict. A signer whose identifier is not
|
|
153
|
+
// key-derived is untouched.
|
|
154
|
+
const pkgSigner = pkg ? pkg['signer'] : undefined;
|
|
155
|
+
const keyDerived = compareKeyDerivedIdentifier(pkgSigner, sigPublicKey);
|
|
156
|
+
if (keyTrust && keyDerived) {
|
|
157
|
+
keyTrust = keyDerived.match
|
|
158
|
+
? applySelfCertifiedKeyTrust(keyTrust, pkgSigner.bindingTier, deps.registryProvenance)
|
|
159
|
+
: applyKeyDerivedRegistryRule(keyTrust, deps.registryProvenance);
|
|
160
|
+
}
|
|
161
|
+
// 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.
|
|
136
163
|
let contentCanonicalization = null;
|
|
137
164
|
let contentHashCheck = null;
|
|
138
165
|
let typeResolution = null;
|
|
139
166
|
let signerIdentity = null;
|
|
140
167
|
let captureMethodVocab = null;
|
|
168
|
+
let contentProfile = null;
|
|
141
169
|
if (pkg) {
|
|
142
170
|
contentCanonicalization = resolveContentCanonicalization(pkg);
|
|
143
|
-
contentHashCheck =
|
|
171
|
+
contentHashCheck = await verifyContentHashWithFetch(pkg, contentCanonicalization, input.legacyExternalHash ?? packageHash, { fetch: deps.fetch });
|
|
144
172
|
typeResolution = resolvePackageType(pkg);
|
|
145
|
-
signerIdentity = checkSignerIdentity(pkg, sigKid, deps.registry);
|
|
173
|
+
signerIdentity = checkSignerIdentity(pkg, sigKid, deps.registry, sigPublicKey);
|
|
146
174
|
captureMethodVocab = checkCaptureMethodVocab(pkg);
|
|
175
|
+
contentProfile = checkContentProfile(pkg);
|
|
147
176
|
}
|
|
148
177
|
// Step 6 — lifecycle (check #10). Server-deeper resolution wins when supplied;
|
|
149
178
|
// otherwise derive STATE from the sidecar's lifecycle (the portable depth).
|
|
@@ -177,6 +206,19 @@ export async function verifyEvidence(input, deps) {
|
|
|
177
206
|
typeResolution,
|
|
178
207
|
signerIdentity,
|
|
179
208
|
captureMethodVocab,
|
|
209
|
+
contentProfile,
|
|
180
210
|
lifecycle,
|
|
181
211
|
};
|
|
182
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* @deprecated Renamed to {@link verifyRecord} in the 2026-08-19 vocabulary
|
|
215
|
+
* settlement — "evidence" is retired from the artifact/infrastructure brand
|
|
216
|
+
* role and retained only for the epistemic Question/Evidence/Claim role (spec
|
|
217
|
+
* §6.3, Appendix J; migration class `alias-and-deprecate`).
|
|
218
|
+
*
|
|
219
|
+
* This is the SAME function object, not a wrapper: `verifyEvidence ===
|
|
220
|
+
* verifyRecord`. Existing callers keep working unchanged and get identical
|
|
221
|
+
* results; the alias is removed no earlier than this package's next MAJOR
|
|
222
|
+
* version. New code should import `verifyRecord`.
|
|
223
|
+
*/
|
|
224
|
+
export const verifyEvidence = verifyRecord;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typedstandards/verify-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Portable, browser-safe verification core for Typed Standards
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|
|
7
7
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.json",
|
|
28
28
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
29
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
30
30
|
"test": "node --test --experimental-strip-types \"src/**/*.test.ts\"",
|
|
31
31
|
"prepublishOnly": "npm run clean && npm run build"
|
|
32
32
|
},
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"canonicalize": "^3.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
+
"@types/node": "^20",
|
|
39
40
|
"typescript": "^5.9.3"
|
|
40
41
|
},
|
|
41
42
|
"engines": {
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
},
|
|
56
57
|
"keywords": [
|
|
57
58
|
"typed-standards",
|
|
58
|
-
"
|
|
59
|
+
"record-package",
|
|
59
60
|
"verification",
|
|
60
61
|
"provenance",
|
|
61
62
|
"ed25519",
|