@typedstandards/verify-core 0.9.0 → 0.11.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 +123 -1
- package/dist/canonicalization.d.ts +7 -0
- package/dist/canonicalization.js +15 -0
- package/dist/checks.d.ts +111 -4
- package/dist/checks.js +224 -9
- 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 +1 -0
- package/dist/profiles.js +6 -0
- package/dist/rfc3161.d.ts +33 -4
- package/dist/rfc3161.js +83 -24
- 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 +15 -3
- package/dist/verify.js +78 -6
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,133 @@
|
|
|
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.11.0 — 2026-09-22
|
|
10
|
+
|
|
11
|
+
The verifier follow-ons (anchored at `typedstandards#98`): specification check #6, the ratified
|
|
12
|
+
`attestation/revises/v1` (specification v0.1.10), one fetch per referenced file, and a timestamp result
|
|
13
|
+
that tells a token that does not verify from one whose only fault is its chain. **A minor bump** — new
|
|
14
|
+
exports, `VerifyResult` gains a field, and `Rfc3161FailReason` gains a member, so a consumer that
|
|
15
|
+
builds a `VerifyResult` literal (a test double, say), or has an exhaustive `switch` or `Record` over
|
|
16
|
+
the reasons, needs the new cases.
|
|
17
|
+
|
|
18
|
+
- **Check #6, `metadata.signingKeyId` consistency (`checkSigningKeyIdConsistency`,
|
|
19
|
+
`SIGNING_KEY_ID_CONSISTENCY_STATUSES`; typedstandards#88).** Specification §9.2 check #6 was
|
|
20
|
+
implemented nowhere. It compares the signature envelope's `kid` with the package's
|
|
21
|
+
`metadata.signingKeyId` and reports one of:
|
|
22
|
+
- `ok` — both present and equal;
|
|
23
|
+
- `signingKeyId_mismatch` — both present and different; the one failing status (§8.3.1: the field
|
|
24
|
+
MUST equal the envelope `kid`). The result carries `kid` and `signingKeyId`;
|
|
25
|
+
- `kid_absent` — the envelope carries no `kid`; legitimate under a key-derived identifier, and on
|
|
26
|
+
legacy packages, which carry neither field;
|
|
27
|
+
- `signingKeyId_absent` — a `kid` that the signed bytes do not name, so the check cannot confirm
|
|
28
|
+
it.
|
|
29
|
+
|
|
30
|
+
`signer.identifier` is not compared: under a key-derived identifier the `kid` SHOULD be the
|
|
31
|
+
identifier, which is not a condition of this check. `VerifyResult` gains `signingKeyIdConsistency`,
|
|
32
|
+
null when there is no package, no signature envelope, or a malformed one. The captured and
|
|
33
|
+
published packages measured for this change (the three captured commitment bundles, the
|
|
34
|
+
self-certified interop fixture, the two ADR-0028 eval-run packages, and two published example
|
|
35
|
+
bundles under a key-derived identifier) read `ok`, except the legacy capture, which carries neither
|
|
36
|
+
field and reads `kid_absent`.
|
|
37
|
+
- **Check #12 registers `attestation/revises/v1` (typedstandards#96).** The ratified set is sixteen
|
|
38
|
+
attestation sub-types plus `content/analysis/v1`; a node of that type read `unknown_type` and now
|
|
39
|
+
reads `ok`.
|
|
40
|
+
- **One fetch per BlobRef URL per `verifyRecord` (typedstandards#90).** Under `raw-bytes/v1` with a
|
|
41
|
+
BlobRef `output`, checks #9 and #4 each fetched the file. They now share one request per URL within a
|
|
42
|
+
`verifyRecord` call, and report exactly what they reported before, including when the fetch fails and
|
|
43
|
+
when the bytes do not match. No public signature changes, and no request is added.
|
|
44
|
+
- **Check #7 evaluates the TSA signature when the chain does not reach a pinned root
|
|
45
|
+
(typedstandards#94).** `verifyRfc3161Timestamp` used to return at the chain step, so for a token
|
|
46
|
+
whose chain did not reach a pinned anchor `signatureValid` read `null` whether the signature was
|
|
47
|
+
genuine or not. It now reads `true` or `false` there as well. For a consumer:
|
|
48
|
+
- `signatureValid` is `null` when an earlier step returned first (`parse_error`,
|
|
49
|
+
`unexpected_algorithm`, `imprint_mismatch`, `no_message_digest`, `content_not_bound`,
|
|
50
|
+
`no_signing_cert`, `eku_not_timestamping`), or when the signing cert's key is not a P-384 key.
|
|
51
|
+
`true` means the signature verifies under the embedded signing cert's key; that key is trusted
|
|
52
|
+
only when `chainVerified` is `true`. `false` means a P-384 key under which the signature does
|
|
53
|
+
not verify, including a malformed signature encoding;
|
|
54
|
+
- `reason` reports the token's own faults ahead of the chain's: `genTime_outside_validity`, then
|
|
55
|
+
`unexpected_algorithm` for a signing-cert key that is not P-384, then `signature_invalid`, then
|
|
56
|
+
the chain reason. A token with an unpinned chain and a signature that does not verify used to
|
|
57
|
+
read `untrusted_root` (or another chain reason) and now reads `signature_invalid`. A signing-cert
|
|
58
|
+
key that is not P-384 used to read the chain reason, or `signature_invalid` under a pinned chain,
|
|
59
|
+
and now reads `unexpected_algorithm`. `untrusted_root`, `chain_incomplete`,
|
|
60
|
+
`chain_signature_invalid` and `chain_outside_validity` are now reported only for a token whose
|
|
61
|
+
TSA signature verifies and whose `genTime` is within the signing cert's validity;
|
|
62
|
+
- **new reason `chain_outside_validity`:** an intermediate or root certificate not valid at
|
|
63
|
+
`genTime`, a chain fault. `genTime_outside_validity` now means only the signing cert. A token
|
|
64
|
+
whose signing cert is valid at `genTime` and whose intermediate or root is not used to read
|
|
65
|
+
`genTime_outside_validity` and now reads `chain_outside_validity`;
|
|
66
|
+
- **the message imprint is compared before the SignerInfo algorithms:** a token that does not bind
|
|
67
|
+
this package's hash reads `imprint_mismatch` whatever algorithms it uses. One that did so with a
|
|
68
|
+
digest or signature algorithm outside the checked set used to read `unexpected_algorithm`, and
|
|
69
|
+
`imprintMatches` was `null` for any `unexpected_algorithm` result from that check; it is now
|
|
70
|
+
`true` there;
|
|
71
|
+
- each reason now names one kind of fault, so a consumer can classify a token by `reason` alone;
|
|
72
|
+
- `verified` is unchanged: `true` only for a fully verified token chained to a pinned anchor. No
|
|
73
|
+
request is added.
|
|
74
|
+
- **`KNOWN_TYPE_URIS` is exported.** The type URIs check #12 resolves `ok` (`content/analysis/v1` and
|
|
75
|
+
the sixteen attestation sub-types), frozen, so a check outside this package can compare them with the
|
|
76
|
+
specification's list.
|
|
77
|
+
- **`RFC3161_FAIL_REASONS` is exported.** The thirteen reasons `verifyRfc3161Timestamp` can return,
|
|
78
|
+
as a frozen array; `Rfc3161FailReason` is now derived from it. It gains one member,
|
|
79
|
+
`chain_outside_validity`, so a consumer with an exhaustive `switch` or `Record` over it needs the
|
|
80
|
+
new case.
|
|
81
|
+
|
|
82
|
+
## 0.10.0 — 2026-09-22
|
|
83
|
+
|
|
84
|
+
Specification v0.1.9 (hub ADR-0029 and ADR-0030, anchored at `typedstandards#77`): a Producer Profile
|
|
85
|
+
for deterministic tool output, a raw-bytes content rule, a content-profile check, and a self-certifying
|
|
86
|
+
signer. **A minor bump** — new exports, and every type-level union below gains members, so a consumer
|
|
87
|
+
with an exhaustive `switch` or `Record` over one of them needs the new cases.
|
|
88
|
+
|
|
89
|
+
- **The `scripted-recomputation` Producer Profile (ADR-0029).** `PROFILE_CAPTURE_VOCAB` gains
|
|
90
|
+
`scripted-recomputation: ['script-run', 'tool-emitted']`, a second stand-in entry under hub Q32, and
|
|
91
|
+
`CaptureMethod` gains both values. Check #15 now resolves a `scripted-recomputation/<subtype>`
|
|
92
|
+
package's capture method instead of reporting `producerProfile_bundle_unresolved`. The two signed
|
|
93
|
+
ADR-0028 eval-run packages read `ok`.
|
|
94
|
+
- **`raw-bytes/v1` (`RAW_BYTES_CANONICALIZATION`).** A third content-canonicalization rule, in
|
|
95
|
+
`KNOWN_CANONICALIZATION_RULES`: `contentHash` fingerprints the exact bytes of `output`.
|
|
96
|
+
- With inline `output`, check #4 hashes its UTF-8 bytes.
|
|
97
|
+
- With a BlobRef `output`, check #4 hashes the file's bytes obtained through the injected fetcher
|
|
98
|
+
(`verifyContentHashWithFetch`; `verifyRecord` runs it):
|
|
99
|
+
- `content_hash_mismatch` when they differ, or when `contentHash.sha256` differs from the hex of
|
|
100
|
+
`output.ref`;
|
|
101
|
+
- the new status `content_bytes_unavailable` when the bytes cannot be obtained. It is never `ok`
|
|
102
|
+
without hashing.
|
|
103
|
+
- `verifyContentHash` stays synchronous and I/O-free, with an optional `outputBytes` argument.
|
|
104
|
+
- **Check #16, the content-profile label (`checkContentProfile`, `CONTENT_PROFILE_STATUSES`,
|
|
105
|
+
`KNOWN_CONTENT_PROFILES`).** Reads `metadata.contentProfile` and reports one of:
|
|
106
|
+
- `ok`;
|
|
107
|
+
- `contentProfile_absent` (read as `"default"`);
|
|
108
|
+
- `contentProfile_unknown`;
|
|
109
|
+
- `contentProfile_inconsistent` (compared with `producerProfile` only when both are present).
|
|
110
|
+
|
|
111
|
+
`VerifyResult` gains `contentProfile`.
|
|
112
|
+
- **The self-certifying signer (ADR-0030).**
|
|
113
|
+
- **The identifier.** `deriveKeyDerivedIdentifier` derives `did:key:z…` from an Ed25519 SPKI
|
|
114
|
+
`publicKey`. `isKeyDerivedIdentifier` and `KEY_DERIVED_IDENTIFIER_PREFIX` recognize one.
|
|
115
|
+
- **Key trust.** `KEY_TRUST_STATUSES` gains `self_certified` (`verified: false`) for a
|
|
116
|
+
`pseudonymous` signer whose identifier matches its signing key.
|
|
117
|
+
- **Check #14.** It gains `key_derived_match` and `key_derived_mismatch` (fatal; the result carries
|
|
118
|
+
`claimed` and `derived`), and never reports `ok` under a key-derived identifier.
|
|
119
|
+
`checkSignerIdentity` takes an optional `publicKey`.
|
|
120
|
+
- **Registry provenance.** `VerifyDeps.registryProvenance` (`TRUST_REGISTRY_PROVENANCES`:
|
|
121
|
+
`'declared-url' | 'bundle'`; absent is treated as `'bundle'`) states where the caller's registry
|
|
122
|
+
came from.
|
|
123
|
+
- For a key-derived identifier, only a `'declared-url'` registry can raise the status.
|
|
124
|
+
- A bundle-carried registry contributes only `revoked` and `deprecated_invalid`.
|
|
125
|
+
- This holds at every tier, and under a mismatch too.
|
|
126
|
+
- **Base-58.** The encoder is written in place; there is no new runtime dependency.
|
|
127
|
+
- **Signers whose identifier is not key-derived are unchanged.** Already-signed packages verify
|
|
128
|
+
exactly as before, apart from checks #15 and #16, which now resolve a `scripted-recomputation`
|
|
129
|
+
profile and report a content profile.
|
|
130
|
+
|
|
9
131
|
## 0.9.0 — 2026-08-19
|
|
10
132
|
|
|
11
133
|
Vocabulary settlement (spec v0.1.5 Appendix J; registry Q50/Q66, ADR-0025,
|
|
@@ -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,54 @@ 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`.
|
|
79
|
+
*/
|
|
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.
|
|
58
103
|
*/
|
|
59
|
-
export declare function
|
|
104
|
+
export declare function checkContentProfile(pkg: Record<string, unknown>): ContentProfileCheck;
|
|
105
|
+
export declare const KNOWN_TYPE_URIS: readonly string[];
|
|
60
106
|
export declare const TYPE_RESOLUTION_STATUSES: readonly ["ok", "implicit", "unknown_type"];
|
|
61
107
|
export type TypeResolutionStatus = (typeof TYPE_RESOLUTION_STATUSES)[number];
|
|
62
108
|
export interface TypeResolution {
|
|
@@ -70,7 +116,47 @@ export interface TypeResolution {
|
|
|
70
116
|
* and renders as such — it does NOT fail verification.
|
|
71
117
|
*/
|
|
72
118
|
export declare function resolvePackageType(pkg: Record<string, unknown>): TypeResolution;
|
|
73
|
-
export declare const
|
|
119
|
+
export declare const SIGNING_KEY_ID_CONSISTENCY_STATUSES: readonly ["ok", "signingKeyId_mismatch", "kid_absent", "signingKeyId_absent"];
|
|
120
|
+
export type SigningKeyIdConsistencyStatus = (typeof SIGNING_KEY_ID_CONSISTENCY_STATUSES)[number];
|
|
121
|
+
export interface SigningKeyIdConsistencyCheck {
|
|
122
|
+
status: SigningKeyIdConsistencyStatus;
|
|
123
|
+
/** On `signingKeyId_mismatch`: the signature envelope's `kid`. */
|
|
124
|
+
kid?: string;
|
|
125
|
+
/** On `signingKeyId_mismatch`: `metadata.signingKeyId` as carried, when it is
|
|
126
|
+
* a string. */
|
|
127
|
+
signingKeyId?: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check #6 — the signature envelope's `kid` against `metadata.signingKeyId`
|
|
131
|
+
* (spec §9.2 check #6, §8.3.1). The package's `signingKeyId` is inside the
|
|
132
|
+
* signed bytes and the envelope's `kid` is not, so a `kid` swapped on the
|
|
133
|
+
* envelope after signing leaves the signature valid; this comparison is what
|
|
134
|
+
* detects it.
|
|
135
|
+
* - both present and equal → `ok`.
|
|
136
|
+
* - both present and different → `signingKeyId_mismatch`, the one failing
|
|
137
|
+
* status: §8.3.1 says `metadata.signingKeyId` MUST equal the envelope's
|
|
138
|
+
* `kid`, and the spec reads a mismatch as envelope-vs-canonical drift. The
|
|
139
|
+
* comparison is exact (no case or whitespace folding), and a present value
|
|
140
|
+
* that is not a string is a mismatch.
|
|
141
|
+
* - the envelope carries no `kid` → `kid_absent`: there is nothing to compare.
|
|
142
|
+
* Under a key-derived `signer.identifier` (§8.5.1) `kid` is optional, and
|
|
143
|
+
* legacy packages carry neither field; for a registry signer, check #5
|
|
144
|
+
* carries the consequence of a missing `kid`.
|
|
145
|
+
* - the envelope carries a `kid` and the package no `metadata.signingKeyId`
|
|
146
|
+
* → `signingKeyId_absent`: the `kid` is bound by nothing signed, so the
|
|
147
|
+
* check cannot confirm it. The package does not meet §8.3.1, which requires
|
|
148
|
+
* the field; that is not evidence the envelope was changed.
|
|
149
|
+
* `undefined`, `null` and `''` count as absent on either side. Only a mismatch
|
|
150
|
+
* carries the two values, which are then the finding; otherwise they are
|
|
151
|
+
* `VerifyResult.kid` and the package's own field.
|
|
152
|
+
*
|
|
153
|
+
* `signer.identifier` is not read. Under a key-derived identifier the `kid`
|
|
154
|
+
* SHOULD be the identifier string (§8.3.1); that is not a condition of this
|
|
155
|
+
* check, and for a registry signer `kid` and `signer.identifier` differ by
|
|
156
|
+
* design. The identity cross-check is check #14's.
|
|
157
|
+
*/
|
|
158
|
+
export declare function checkSigningKeyIdConsistency(pkg: Record<string, unknown>, kid: string | undefined): SigningKeyIdConsistencyCheck;
|
|
159
|
+
export declare const SIGNER_IDENTITY_CHECK_STATUSES: readonly ["ok", "signer_identity_mismatch", "no_signer", "no_registry_identity", "key_derived_match", "key_derived_mismatch"];
|
|
74
160
|
export type SignerIdentityCheckStatus = (typeof SIGNER_IDENTITY_CHECK_STATUSES)[number];
|
|
75
161
|
export interface SignerIdentityCheck {
|
|
76
162
|
status: SignerIdentityCheckStatus;
|
|
@@ -78,6 +164,10 @@ export interface SignerIdentityCheck {
|
|
|
78
164
|
claimed?: string;
|
|
79
165
|
/** The identifier the registry records for the signing `kid`, when present. */
|
|
80
166
|
registered?: string;
|
|
167
|
+
/** The identifier derived from the envelope's `publicKey`, under a
|
|
168
|
+
* key-derived `signer.identifier` (hub ADR-0030 §2). Absent on a
|
|
169
|
+
* `key_derived_mismatch` whose key could not be decoded. */
|
|
170
|
+
derived?: string;
|
|
81
171
|
}
|
|
82
172
|
/**
|
|
83
173
|
* Check #14 — `signer.identifier` ↔ trust-registry `signerIdentity` cross-check
|
|
@@ -86,8 +176,25 @@ export interface SignerIdentityCheck {
|
|
|
86
176
|
* verifier derives the signer from the registry and skips the cross-check. A
|
|
87
177
|
* registry entry without a `signerIdentity` (legacy registry) yields
|
|
88
178
|
* `no_registry_identity`.
|
|
179
|
+
*
|
|
180
|
+
* A key-derived `signer.identifier` (one beginning `did:key:`, hub ADR-0030 §3)
|
|
181
|
+
* is checked against the envelope's `publicKey` instead, whatever the
|
|
182
|
+
* `bindingTier` and whatever the registry's source:
|
|
183
|
+
* - the identifier derived from `publicKey` differs → `key_derived_mismatch`,
|
|
184
|
+
* fatal, carrying `claimed` and `derived`;
|
|
185
|
+
* - it matches, and a registry entry for the `kid` records a different
|
|
186
|
+
* identifier → `signer_identity_mismatch`, fatal (a contradiction only
|
|
187
|
+
* lowers, so any registry may report it);
|
|
188
|
+
* - it matches otherwise → `key_derived_match`.
|
|
189
|
+
* `ok` ("matches the registry") is never reported under a key-derived
|
|
190
|
+
* identifier. Without `publicKey` the derivation cannot run; the registry
|
|
191
|
+
* cross-check runs as for any signer, except that its `ok` reads
|
|
192
|
+
* `no_registry_identity`, since nothing established the identifier.
|
|
89
193
|
*/
|
|
90
|
-
export declare function checkSignerIdentity(pkg: Record<string, unknown>, kid: string | undefined, registry: TrustRegistry | undefined
|
|
194
|
+
export declare function checkSignerIdentity(pkg: Record<string, unknown>, kid: string | undefined, registry: TrustRegistry | undefined,
|
|
195
|
+
/** The signature envelope's base64 SPKI `publicKey`. Read only when the
|
|
196
|
+
* signer's identifier is key-derived. */
|
|
197
|
+
publicKey?: string): SignerIdentityCheck;
|
|
91
198
|
export declare const CAPTURE_METHOD_VOCAB_STATUSES: readonly ["ok", "captureMethod_unknown", "producerProfile_bundle_unresolved", "no_capture_method"];
|
|
92
199
|
export type CaptureMethodVocabStatus = (typeof CAPTURE_METHOD_VOCAB_STATUSES)[number];
|
|
93
200
|
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, #6, #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,16 +161,102 @@ 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
|
|
136
249
|
// today, but the full ratified set is registered so conformant packages don't
|
|
137
|
-
// render as `unknown_type`.
|
|
138
|
-
|
|
250
|
+
// render as `unknown_type`. The sixteen attestation sub-types are in the order
|
|
251
|
+
// of the §8.12.1 table; `type-resolution.test.ts` holds that table's copy.
|
|
252
|
+
// Exported and frozen so a check outside this package can compare the registered
|
|
253
|
+
// set with the specification's list without being able to change it.
|
|
254
|
+
export const KNOWN_TYPE_URIS = Object.freeze([
|
|
139
255
|
'content/analysis/v1',
|
|
140
256
|
'attestation/withdraws/v1',
|
|
141
257
|
'attestation/reinstates/v1',
|
|
142
258
|
'attestation/supersedes/v1',
|
|
259
|
+
'attestation/revises/v1',
|
|
143
260
|
'attestation/publishes/v1',
|
|
144
261
|
'attestation/locatedAt/v1',
|
|
145
262
|
'attestation/corroborates/v1',
|
|
@@ -152,7 +269,7 @@ const KNOWN_TYPE_URIS = [
|
|
|
152
269
|
'attestation/certifies/v1',
|
|
153
270
|
'attestation/evaluates/v1',
|
|
154
271
|
'attestation/conforms/v1',
|
|
155
|
-
];
|
|
272
|
+
]);
|
|
156
273
|
export const TYPE_RESOLUTION_STATUSES = ['ok', 'implicit', 'unknown_type'];
|
|
157
274
|
/**
|
|
158
275
|
* Check #12 — `type` resolution. An absent field resolves to the implicit
|
|
@@ -169,11 +286,70 @@ export function resolvePackageType(pkg) {
|
|
|
169
286
|
}
|
|
170
287
|
return { status: 'unknown_type', type: raw };
|
|
171
288
|
}
|
|
289
|
+
// --- Envelope kid ↔ metadata.signingKeyId (spec §9.2 check #6) ---
|
|
290
|
+
export const SIGNING_KEY_ID_CONSISTENCY_STATUSES = [
|
|
291
|
+
'ok',
|
|
292
|
+
'signingKeyId_mismatch',
|
|
293
|
+
'kid_absent',
|
|
294
|
+
'signingKeyId_absent',
|
|
295
|
+
];
|
|
296
|
+
const isAbsent = (v) => v === undefined || v === null || v === '';
|
|
297
|
+
/**
|
|
298
|
+
* Check #6 — the signature envelope's `kid` against `metadata.signingKeyId`
|
|
299
|
+
* (spec §9.2 check #6, §8.3.1). The package's `signingKeyId` is inside the
|
|
300
|
+
* signed bytes and the envelope's `kid` is not, so a `kid` swapped on the
|
|
301
|
+
* envelope after signing leaves the signature valid; this comparison is what
|
|
302
|
+
* detects it.
|
|
303
|
+
* - both present and equal → `ok`.
|
|
304
|
+
* - both present and different → `signingKeyId_mismatch`, the one failing
|
|
305
|
+
* status: §8.3.1 says `metadata.signingKeyId` MUST equal the envelope's
|
|
306
|
+
* `kid`, and the spec reads a mismatch as envelope-vs-canonical drift. The
|
|
307
|
+
* comparison is exact (no case or whitespace folding), and a present value
|
|
308
|
+
* that is not a string is a mismatch.
|
|
309
|
+
* - the envelope carries no `kid` → `kid_absent`: there is nothing to compare.
|
|
310
|
+
* Under a key-derived `signer.identifier` (§8.5.1) `kid` is optional, and
|
|
311
|
+
* legacy packages carry neither field; for a registry signer, check #5
|
|
312
|
+
* carries the consequence of a missing `kid`.
|
|
313
|
+
* - the envelope carries a `kid` and the package no `metadata.signingKeyId`
|
|
314
|
+
* → `signingKeyId_absent`: the `kid` is bound by nothing signed, so the
|
|
315
|
+
* check cannot confirm it. The package does not meet §8.3.1, which requires
|
|
316
|
+
* the field; that is not evidence the envelope was changed.
|
|
317
|
+
* `undefined`, `null` and `''` count as absent on either side. Only a mismatch
|
|
318
|
+
* carries the two values, which are then the finding; otherwise they are
|
|
319
|
+
* `VerifyResult.kid` and the package's own field.
|
|
320
|
+
*
|
|
321
|
+
* `signer.identifier` is not read. Under a key-derived identifier the `kid`
|
|
322
|
+
* SHOULD be the identifier string (§8.3.1); that is not a condition of this
|
|
323
|
+
* check, and for a registry signer `kid` and `signer.identifier` differ by
|
|
324
|
+
* design. The identity cross-check is check #14's.
|
|
325
|
+
*/
|
|
326
|
+
export function checkSigningKeyIdConsistency(pkg, kid) {
|
|
327
|
+
const metadata = pkg['metadata'];
|
|
328
|
+
const signingKeyId = typeof metadata === 'object' && metadata !== null && !Array.isArray(metadata)
|
|
329
|
+
? metadata['signingKeyId']
|
|
330
|
+
: undefined;
|
|
331
|
+
if (kid === undefined || isAbsent(kid)) {
|
|
332
|
+
return { status: 'kid_absent' };
|
|
333
|
+
}
|
|
334
|
+
if (isAbsent(signingKeyId)) {
|
|
335
|
+
return { status: 'signingKeyId_absent' };
|
|
336
|
+
}
|
|
337
|
+
if (signingKeyId === kid) {
|
|
338
|
+
return { status: 'ok' };
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
status: 'signingKeyId_mismatch',
|
|
342
|
+
kid,
|
|
343
|
+
...(typeof signingKeyId === 'string' ? { signingKeyId } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
172
346
|
export const SIGNER_IDENTITY_CHECK_STATUSES = [
|
|
173
347
|
'ok',
|
|
174
348
|
'signer_identity_mismatch',
|
|
175
349
|
'no_signer',
|
|
176
350
|
'no_registry_identity',
|
|
351
|
+
'key_derived_match',
|
|
352
|
+
'key_derived_mismatch',
|
|
177
353
|
];
|
|
178
354
|
/**
|
|
179
355
|
* Check #14 — `signer.identifier` ↔ trust-registry `signerIdentity` cross-check
|
|
@@ -182,14 +358,53 @@ export const SIGNER_IDENTITY_CHECK_STATUSES = [
|
|
|
182
358
|
* verifier derives the signer from the registry and skips the cross-check. A
|
|
183
359
|
* registry entry without a `signerIdentity` (legacy registry) yields
|
|
184
360
|
* `no_registry_identity`.
|
|
361
|
+
*
|
|
362
|
+
* A key-derived `signer.identifier` (one beginning `did:key:`, hub ADR-0030 §3)
|
|
363
|
+
* is checked against the envelope's `publicKey` instead, whatever the
|
|
364
|
+
* `bindingTier` and whatever the registry's source:
|
|
365
|
+
* - the identifier derived from `publicKey` differs → `key_derived_mismatch`,
|
|
366
|
+
* fatal, carrying `claimed` and `derived`;
|
|
367
|
+
* - it matches, and a registry entry for the `kid` records a different
|
|
368
|
+
* identifier → `signer_identity_mismatch`, fatal (a contradiction only
|
|
369
|
+
* lowers, so any registry may report it);
|
|
370
|
+
* - it matches otherwise → `key_derived_match`.
|
|
371
|
+
* `ok` ("matches the registry") is never reported under a key-derived
|
|
372
|
+
* identifier. Without `publicKey` the derivation cannot run; the registry
|
|
373
|
+
* cross-check runs as for any signer, except that its `ok` reads
|
|
374
|
+
* `no_registry_identity`, since nothing established the identifier.
|
|
185
375
|
*/
|
|
186
|
-
export function checkSignerIdentity(pkg, kid, registry
|
|
376
|
+
export function checkSignerIdentity(pkg, kid, registry,
|
|
377
|
+
/** The signature envelope's base64 SPKI `publicKey`. Read only when the
|
|
378
|
+
* signer's identifier is key-derived. */
|
|
379
|
+
publicKey) {
|
|
187
380
|
const signer = pkg['signer'];
|
|
188
381
|
if (!signer || typeof signer !== 'object' || typeof signer.identifier !== 'string') {
|
|
189
382
|
return { status: 'no_signer' };
|
|
190
383
|
}
|
|
191
384
|
const entry = kid && registry ? registry.keys.find((k) => k.kid === kid) : undefined;
|
|
192
385
|
const registered = entry?.signerIdentity?.identifier;
|
|
386
|
+
if (isKeyDerivedIdentifier(signer.identifier)) {
|
|
387
|
+
const comparison = compareKeyDerivedIdentifier(signer, publicKey);
|
|
388
|
+
if (comparison && !comparison.match) {
|
|
389
|
+
return {
|
|
390
|
+
status: 'key_derived_mismatch',
|
|
391
|
+
claimed: comparison.claimed,
|
|
392
|
+
...(comparison.derived !== undefined ? { derived: comparison.derived } : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (registered && registered !== signer.identifier) {
|
|
396
|
+
return { status: 'signer_identity_mismatch', claimed: signer.identifier, registered };
|
|
397
|
+
}
|
|
398
|
+
if (comparison) {
|
|
399
|
+
return {
|
|
400
|
+
status: 'key_derived_match',
|
|
401
|
+
claimed: signer.identifier,
|
|
402
|
+
derived: comparison.derived,
|
|
403
|
+
...(registered ? { registered } : {}),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return { status: 'no_registry_identity', claimed: signer.identifier };
|
|
407
|
+
}
|
|
193
408
|
if (!registered) {
|
|
194
409
|
return { status: 'no_registry_identity', claimed: signer.identifier };
|
|
195
410
|
}
|