@atrib/verify 0.1.2 → 0.2.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/README.md +7 -5
- package/dist/verify-record.d.ts +202 -11
- package/dist/verify-record.d.ts.map +1 -1
- package/dist/verify-record.js +190 -10
- package/dist/verify-record.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ This method operates on `RecommendationDocument` shapes (settlement-recommendati
|
|
|
73
73
|
|
|
74
74
|
### `verifyRecord(record, options): Promise<RecordVerificationResult>`
|
|
75
75
|
|
|
76
|
-
Per-record verification. Verifies a single signed record's Ed25519 signature and surfaces per-record annotations defined by spec sections [§1.2.5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#125-informed_by) ([D041](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d041-informed_by-linking-primitive-and-informed_by-edge-type)), [§1.2.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#126-provenance_token) ([D044](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d044-provenance_token-field-for-cross-session-causal-anchoring)), and [§8.4](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#84-coarsened-timing-posture) ([D045](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d045-privacy-postures-normative-spec-section)).
|
|
76
|
+
Per-record verification. Verifies a single signed record's Ed25519 signature and surfaces per-record annotations defined by spec sections [§1.2.5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#125-informed_by) ([D041](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d041-informed_by-linking-primitive-and-informed_by-edge-type)), [§1.2.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#126-provenance_token) ([D044](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d044-provenance_token-field-for-cross-session-causal-anchoring)), [§6.7](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#67-capability-declarations) ([D051](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d051-capability-scoped-records-via-directory-published-envelopes)), and [§8.4](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#84-coarsened-timing-posture) ([D045](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d045-privacy-postures-normative-spec-section)).
|
|
77
77
|
|
|
78
78
|
```typescript
|
|
79
79
|
import { verifyRecord } from '@atrib/verify'
|
|
@@ -81,6 +81,7 @@ import { verifyRecord } from '@atrib/verify'
|
|
|
81
81
|
const result = await verifyRecord(record, {
|
|
82
82
|
upstreamCandidate, // optional, for provenance_token resolution
|
|
83
83
|
informedByCandidates: [], // optional, for informed_by[] resolution
|
|
84
|
+
identityClaim, // optional, for capability_check (caller does directory lookup)
|
|
84
85
|
})
|
|
85
86
|
// result: {
|
|
86
87
|
// valid: boolean
|
|
@@ -88,6 +89,7 @@ const result = await verifyRecord(record, {
|
|
|
88
89
|
// posture: { timestamp_granularity, timestamp_consistent, timestamp_granularity_explicit }
|
|
89
90
|
// provenance?: { token, upstream_record_hash, upstream_resolved }
|
|
90
91
|
// informed_by_resolution?: { resolved: string[], dangling: string[] }
|
|
92
|
+
// capability_check?: { envelope, in_envelope, mismatches, unresolvable }
|
|
91
93
|
// warnings: string[]
|
|
92
94
|
// }
|
|
93
95
|
```
|
|
@@ -96,14 +98,14 @@ const result = await verifyRecord(record, {
|
|
|
96
98
|
|
|
97
99
|
- `provenance`: `{ token, upstream_record_hash, upstream_resolved }` per session-genesis record carrying `provenance_token` ([D044](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d044-provenance_token-field-for-cross-session-causal-anchoring) / [§1.2.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#126-provenance_token)). The 16-byte token truncation is irreversible: `upstream_record_hash` populates only when the caller supplies a candidate whose canonical-form SHA-256[:16] matches the token.
|
|
98
100
|
- `informed_by_resolution`: `{ resolved: string[], dangling: string[] }` per record carrying `informed_by` ([D041](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d041-informed_by-linking-primitive-and-informed_by-edge-type) / [§1.2.5](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#125-informed_by)). Dangling references are flagged but do not fail verification: they signal "the verifier has not seen upstream context," not "the record is invalid."
|
|
99
|
-
- `posture`: `{ timestamp_granularity, timestamp_consistent, timestamp_granularity_explicit }` ([D045](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d045-privacy-postures-normative-spec-section) / [§8.4](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#84-coarsened-timing-posture)). Always populated. Surfaces the declared timing granularity, whether the timestamp value structurally matches the spec's trailing-zero invariant
|
|
101
|
+
- `posture`: `{ timestamp_granularity, timestamp_consistent, timestamp_granularity_explicit, args_commitment_form, result_commitment_form, tool_name_form }` ([D045](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d045-privacy-postures-normative-spec-section) / [D061](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d061-add-tool_name-args_hash-result_hash-fields-to-§121) / [§8.2](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#82-opaque-name-posture) / [§8.3](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#83-salted-commitment-posture) / [§8.4](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#84-coarsened-timing-posture)). Always populated. Surfaces (a) the declared timing granularity, whether the timestamp value structurally matches the spec's trailing-zero invariant, and whether the field was explicitly set vs defaulted; (b) the structurally-detected `args_hash` / `result_hash` commitment scheme: `'salted-sha256'` when `args_salt` / `result_salt` is present, `'plain-sha256'` otherwise (the `'hmac-sha256'` variant from §8.3 is signaled out-of-band and is not structurally detectable); and (c) the §8.2 `tool_name_form`: `'hashed'` when `tool_name` matches `^sha256:[0-9a-f]{64}$`, `'plain'` for any other present value, `null` when the field is absent. Per D061 the §8.2 verbatim-vs-opaque distinction is NOT structurally detectable — both surface as `'plain'`.
|
|
102
|
+
- `capability_check`: `{ envelope, in_envelope, mismatches, unresolvable }` ([D051](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d051-capability-scoped-records-via-directory-published-envelopes) / [§6.7](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#67-capability-declarations)). Populated only when the caller passes a resolved `identityClaim` in options. Checks the record's `event_type` against the envelope's `event_types` allowlist and the record's `timestamp` against `expires_at`. `tool_names` (against tool_call records), `max_amount`, and `counterparties` (against transaction records) flag `unresolvable: true` because the constraints depend on data not yet on the standard record shape (`tool_name`) or out-of-band protocol events (payment amount + counterparty). Per [§6.7.3](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#673-out-of-envelope-records) out-of-envelope is a signal, not invalidation: mismatches do not flip `valid` to false. The caller is responsible for fetching the active envelope at the record's timestamp via `@atrib/directory`'s `lookup()` (or a cached equivalent); `@atrib/verify` intentionally has no `@atrib/directory` dependency.
|
|
103
|
+
- `cross_attestation`: `{ signers_count, signers_valid, missing }` ([D052](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d052-cross-attestation-requirement-for-transaction-records) / [§1.7.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#176-cross-attestation-requirement-for-transaction-records)). Populated only on transaction records (`event_type = transaction`). Each entry in `signers[]` is verified against the cross-attestation canonical bytes (JCS form with `signers: []` and the top-level `signature` field omitted, per §1.7.6). `missing: true` when fewer than 2 signers verify — atrib's normative minimum. Per §1.7.6 missing is a SIGNAL not invalidation: `valid` stays true if the underlying signature path holds. Legacy single-signer transaction records (no `signers[]` array, only top-level `signature`) surface as `signers_count: 0, missing: true` so consumers can flag them while accepting the cryptographic validity.
|
|
100
104
|
|
|
101
105
|
**Pending per-record annotations** (tracked as a Pending decision in [DECISIONS.md P005](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#p005-reconcile-atribverify-readme-per-record-annotations-with-actual-code-surface)):
|
|
102
106
|
|
|
103
|
-
- `capability_check` ([D051](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d051-capability-scoped-records-via-directory-published-envelopes) / [§6.7](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#67-capability-declarations)): requires `@atrib/directory` integration to look up the signing key's capability envelope.
|
|
104
|
-
- `cross_attestation` ([D052](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d052-cross-attestation-requirement-for-transaction-records) / [§1.7.6](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#176-cross-attestation-requirement-for-transaction-records)): requires a `signers[]` field on `AtribRecord` plus a transaction-record canonicalization variant in `@atrib/mcp`.
|
|
105
107
|
- `cross_log_proof_count` / `cross_log_threshold_met` / `cross_log_equivocation_detected` ([D050](https://github.com/creatornader/atrib/blob/main/DECISIONS.md#d050-cross-log-replication-for-equivocation-defense) / [§2.11](https://github.com/creatornader/atrib/blob/main/atrib-spec.md#211-cross-log-replication)): requires multi-log proof-bundle parsing and trusted-log-set config.
|
|
106
|
-
|
|
108
|
+
(Note: `tool_name_form`, `args_commitment_form`, and `result_commitment_form` per §8.2/§8.3 are all now implemented under `posture` above. D061 added `tool_name`, `args_hash`, and `result_hash` to the §1.2.1 canonical record schema, completing the structural inputs.)
|
|
107
109
|
|
|
108
110
|
Each pending annotation is its own ADR scope when external consumers need it.
|
|
109
111
|
|
package/dist/verify-record.d.ts
CHANGED
|
@@ -9,13 +9,19 @@
|
|
|
9
9
|
* Implemented annotations (this file):
|
|
10
10
|
* - provenance: { token, upstream_record_hash, upstream_resolved } (D044 / §1.2.6)
|
|
11
11
|
* - informed_by_resolution: { resolved: string[], dangling: string[] } (D041 / §1.2.5, §3.2.4)
|
|
12
|
-
* - posture: { timestamp_granularity, timestamp_consistent }
|
|
12
|
+
* - posture: { timestamp_granularity, timestamp_consistent, ...} (D045 / §8.2 / §8.3 / §8.4)
|
|
13
|
+
* - capability_check: { envelope, in_envelope, mismatches, unresolvable } (D051 / §6.7)
|
|
13
14
|
*
|
|
14
15
|
* Pending annotations (tracked in DECISIONS.md P005):
|
|
15
|
-
* - capability_check: D051 / §6.7 (needs @atrib/directory integration)
|
|
16
16
|
* - cross_attestation: D052 / §1.7.6 (needs `signers[]` type addition + transaction-record signing variant in @atrib/mcp)
|
|
17
17
|
* - cross_log_*: D050 / §2.11 (needs multi-log proof-bundle infrastructure)
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Design note on dependencies. The capability_check annotation does NOT
|
|
20
|
+
* fetch the identity claim itself; the caller does the @atrib/directory
|
|
21
|
+
* lookup and passes the resolved claim into VerifyRecordOptions. This
|
|
22
|
+
* keeps @atrib/verify lean (no WASM-bridge dep) and lets the caller
|
|
23
|
+
* decide lookup strategy (cached vs live, batch vs per-record). Same
|
|
24
|
+
* pattern as `upstreamCandidate` and `informedByCandidates`.
|
|
19
25
|
*/
|
|
20
26
|
import { base64urlDecode, type AtribRecord } from '@atrib/mcp';
|
|
21
27
|
/**
|
|
@@ -48,18 +54,108 @@ export interface InformedByAnnotation {
|
|
|
48
54
|
dangling: string[];
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
57
|
+
* Capability-envelope shape from spec §6.7.1. All sub-fields are optional;
|
|
58
|
+
* a claim with an empty envelope (`{}`) declares no scope. The verifier
|
|
59
|
+
* reads this off the resolved IdentityClaim's `capabilities` field.
|
|
60
|
+
*/
|
|
61
|
+
export interface CapabilityEnvelope {
|
|
62
|
+
tool_names?: string[];
|
|
63
|
+
event_types?: string[];
|
|
64
|
+
max_amount?: {
|
|
65
|
+
currency: string;
|
|
66
|
+
value: number;
|
|
67
|
+
};
|
|
68
|
+
counterparties?: string[];
|
|
69
|
+
expires_at?: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Capability-check surfacing for a record whose signer published an
|
|
73
|
+
* IdentityClaim with a `capabilities` field (D051 / spec §6.7).
|
|
74
|
+
*
|
|
75
|
+
* Per §6.7.3: out-of-envelope is a SIGNAL, not invalidation. Records
|
|
76
|
+
* outside the envelope remain cryptographically valid; the signature
|
|
77
|
+
* verifies, log inclusion verifies. Consumers decide policy.
|
|
78
|
+
*/
|
|
79
|
+
export interface CapabilityCheckAnnotation {
|
|
80
|
+
/** The active envelope at the record's timestamp, or null if none. */
|
|
81
|
+
envelope: CapabilityEnvelope | null;
|
|
82
|
+
/**
|
|
83
|
+
* True iff every present envelope field accommodates the record. False
|
|
84
|
+
* when any present constraint excludes the record's content. True when
|
|
85
|
+
* envelope is null (no constraint = trivially in-envelope).
|
|
86
|
+
*/
|
|
87
|
+
in_envelope: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Specific constraints the record violates, listed for debugging.
|
|
90
|
+
* Each entry is a short string like 'event_type not in allowlist' or
|
|
91
|
+
* 'expires_at exceeded'. Empty when in_envelope is true.
|
|
92
|
+
*/
|
|
93
|
+
mismatches: string[];
|
|
94
|
+
/**
|
|
95
|
+
* True iff one or more checks could not be resolved out-of-band
|
|
96
|
+
* (e.g., a transaction record's amount + counterparty are not
|
|
97
|
+
* derivable without the protocol-specific payment event). Per §6.7.2
|
|
98
|
+
* the verifier MUST flag this rather than passing or failing silently.
|
|
99
|
+
*/
|
|
100
|
+
unresolvable: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Cross-attestation surfacing for a transaction record per spec §1.7.6
|
|
104
|
+
* (D052). atrib's normative minimum is two verified signers per
|
|
105
|
+
* transaction record (typically agent + counterparty). Records below
|
|
106
|
+
* the minimum get `missing: true`; verifiers MUST flag this rather
|
|
107
|
+
* than silently passing or failing.
|
|
108
|
+
*
|
|
109
|
+
* The annotation populates ONLY for records with
|
|
110
|
+
* `event_type = https://atrib.dev/v1/types/transaction`. Other event
|
|
111
|
+
* types continue to use the standard single-signer signature path.
|
|
112
|
+
*/
|
|
113
|
+
export interface CrossAttestationAnnotation {
|
|
114
|
+
/**
|
|
115
|
+
* Total signers attached to the record (regardless of validity).
|
|
116
|
+
* `0` for legacy single-signer transaction records that carry only the
|
|
117
|
+
* top-level `signature` field instead of the `signers[]` array.
|
|
118
|
+
*/
|
|
119
|
+
signers_count: number;
|
|
120
|
+
/**
|
|
121
|
+
* Number of signers whose Ed25519 signature successfully verifies
|
|
122
|
+
* against the cross-attestation canonical bytes (JCS form with
|
|
123
|
+
* `signers: []` and `signature` omitted, per §1.7.6).
|
|
124
|
+
*/
|
|
125
|
+
signers_valid: number;
|
|
126
|
+
/**
|
|
127
|
+
* `true` iff `signers_valid < 2` (atrib's normative minimum). Per
|
|
128
|
+
* §1.7.6 verifiers MUST flag this; consumers decide whether to accept
|
|
129
|
+
* the record. Like §6.7's `in_envelope: false`, this is a SIGNAL not
|
|
130
|
+
* an invalidation: the underlying record may still be cryptographically
|
|
131
|
+
* valid via the legacy top-level signature.
|
|
132
|
+
*/
|
|
133
|
+
missing: boolean;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Posture surfacing for a record. Exposes three §8 postures: timing
|
|
137
|
+
* (§8.4), args/result commitment (§8.3), and tool-name disclosure (§8.2).
|
|
54
138
|
*
|
|
55
139
|
* `timestamp_granularity` is the declared coarsening level (or 'ms' by
|
|
56
140
|
* default when absent). `timestamp_consistent` is true iff the timestamp
|
|
57
141
|
* value matches the granularity's trailing-zero invariant per spec §8.4
|
|
58
|
-
* (e.g., 'min' requires `timestamp % 60000 == 0`).
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
142
|
+
* (e.g., 'min' requires `timestamp % 60000 == 0`).
|
|
143
|
+
*
|
|
144
|
+
* `args_commitment_form` and `result_commitment_form` are detected
|
|
145
|
+
* structurally per spec §8.3. Presence of `args_salt` indicates the
|
|
146
|
+
* salted-sha256 scheme; absence indicates the default plain-sha256
|
|
147
|
+
* scheme. The hmac-sha256 variant is structurally indistinguishable to
|
|
148
|
+
* non-key-holders (the issuer signals it out-of-band per §8.3, not via
|
|
149
|
+
* record fields), so we surface only the two structurally-detectable
|
|
150
|
+
* forms.
|
|
151
|
+
*
|
|
152
|
+
* `tool_name_form` is the §8.2 / D061 form of the optional `tool_name`
|
|
153
|
+
* field. The §8.2 verbatim-vs-opaque distinction is NOT structurally
|
|
154
|
+
* detectable (`book_flight` matches the opaque regex), so per D061 we
|
|
155
|
+
* surface only `'hashed' | 'plain' | null`: hashed when the value
|
|
156
|
+
* matches `^sha256:[0-9a-f]{64}$`, plain for any other present value,
|
|
157
|
+
* null when the field is absent. Consumers wanting verbatim-vs-opaque
|
|
158
|
+
* MUST use out-of-band metadata (e.g., a name registry).
|
|
63
159
|
*/
|
|
64
160
|
export interface PostureAnnotation {
|
|
65
161
|
timestamp_granularity: 'ms' | 's' | 'min' | 'h' | 'd';
|
|
@@ -74,6 +170,25 @@ export interface PostureAnnotation {
|
|
|
74
170
|
* Surfaced separately because absence affects JCS canonical form per §1.3.
|
|
75
171
|
*/
|
|
76
172
|
timestamp_granularity_explicit: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Detected commitment scheme for `args_hash`. 'salted-sha256' iff
|
|
175
|
+
* `args_salt` is present on the record; 'plain-sha256' otherwise. The
|
|
176
|
+
* 'hmac-sha256' variant from §8.3 is not structurally detectable.
|
|
177
|
+
*/
|
|
178
|
+
args_commitment_form: 'plain-sha256' | 'salted-sha256';
|
|
179
|
+
/**
|
|
180
|
+
* Detected commitment scheme for `result_hash`. Same semantics as
|
|
181
|
+
* `args_commitment_form` but driven by `result_salt`.
|
|
182
|
+
*/
|
|
183
|
+
result_commitment_form: 'plain-sha256' | 'salted-sha256';
|
|
184
|
+
/**
|
|
185
|
+
* Detected form of the optional `tool_name` field per §8.2 / D061:
|
|
186
|
+
* - 'hashed' when value matches `^sha256:[0-9a-f]{64}$` (unambiguous)
|
|
187
|
+
* - 'plain' for any other present value (verbatim and opaque-label
|
|
188
|
+
* forms are not structurally distinguishable; both surface as plain)
|
|
189
|
+
* - null when the field is absent (the §8.1 default posture)
|
|
190
|
+
*/
|
|
191
|
+
tool_name_form: 'hashed' | 'plain' | null;
|
|
77
192
|
}
|
|
78
193
|
export interface RecordVerificationResult {
|
|
79
194
|
/** True iff signatureOk AND no fatal warnings. */
|
|
@@ -103,8 +218,34 @@ export interface RecordVerificationResult {
|
|
|
103
218
|
* granularity and whether the timestamp value structurally matches it.
|
|
104
219
|
*/
|
|
105
220
|
posture: PostureAnnotation;
|
|
221
|
+
/**
|
|
222
|
+
* Capability-check annotation. Populated only when the caller passes
|
|
223
|
+
* an `identityClaim` in options. When the claim has no `capabilities`
|
|
224
|
+
* field (or an empty envelope), in_envelope is true and mismatches is
|
|
225
|
+
* empty (no constraint = trivially in-envelope per §6.7.1).
|
|
226
|
+
*/
|
|
227
|
+
capability_check?: CapabilityCheckAnnotation;
|
|
228
|
+
/**
|
|
229
|
+
* Cross-attestation annotation per §1.7.6 (D052). Populated ONLY for
|
|
230
|
+
* transaction records (`event_type = https://atrib.dev/v1/types/transaction`).
|
|
231
|
+
* Surfaces signers_count, signers_valid, and missing (true when fewer
|
|
232
|
+
* than 2 signers verified). Per §1.7.6 verifiers MUST flag missing
|
|
233
|
+
* cross-attestation; consumers decide policy.
|
|
234
|
+
*/
|
|
235
|
+
cross_attestation?: CrossAttestationAnnotation;
|
|
106
236
|
warnings: string[];
|
|
107
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Resolved identity claim for capability-check input. This is the SHAPE
|
|
240
|
+
* @atrib/verify needs from a directory lookup, NOT the @atrib/directory
|
|
241
|
+
* type itself (avoids a hard package dep). Callers pass either:
|
|
242
|
+
* - the @atrib/directory `IdentityClaim` (structurally compatible), or
|
|
243
|
+
* - a hand-constructed object with this shape (e.g., from a cache)
|
|
244
|
+
*/
|
|
245
|
+
export interface ResolvedIdentityClaim {
|
|
246
|
+
creator_key: string;
|
|
247
|
+
capabilities?: CapabilityEnvelope;
|
|
248
|
+
}
|
|
108
249
|
export interface VerifyRecordOptions {
|
|
109
250
|
/**
|
|
110
251
|
* Candidate upstream record for provenance_token resolution. If
|
|
@@ -123,6 +264,19 @@ export interface VerifyRecordOptions {
|
|
|
123
264
|
* not invalidating.
|
|
124
265
|
*/
|
|
125
266
|
informedByCandidates?: AtribRecord[];
|
|
267
|
+
/**
|
|
268
|
+
* Resolved identity claim for the record's `creator_key`, used for
|
|
269
|
+
* capability_check (D051 / §6.7). When supplied, verifyRecord populates
|
|
270
|
+
* `capability_check` on the result. The claim should be the active
|
|
271
|
+
* envelope at the record's timestamp per §6.7.2 step 1; the caller
|
|
272
|
+
* is responsible for picking the right historical version when the
|
|
273
|
+
* envelope has rotated. When omitted, capability_check is not surfaced.
|
|
274
|
+
*
|
|
275
|
+
* Caller is responsible for fetching this via @atrib/directory's
|
|
276
|
+
* lookup() or a cached equivalent. @atrib/verify intentionally does
|
|
277
|
+
* NOT depend on @atrib/directory.
|
|
278
|
+
*/
|
|
279
|
+
identityClaim?: ResolvedIdentityClaim;
|
|
126
280
|
}
|
|
127
281
|
/**
|
|
128
282
|
* Verify one signed record.
|
|
@@ -135,6 +289,41 @@ export interface VerifyRecordOptions {
|
|
|
135
289
|
export declare function verifyRecord(record: AtribRecord, options?: VerifyRecordOptions): Promise<RecordVerificationResult>;
|
|
136
290
|
declare function resolveProvenance(token: string, upstreamCandidate: AtribRecord | undefined): ProvenanceAnnotation;
|
|
137
291
|
declare function resolveInformedBy(entries: string[], candidates: AtribRecord[], warnings: string[]): InformedByAnnotation;
|
|
292
|
+
/**
|
|
293
|
+
* Capability-check derivation per spec §6.7.2.
|
|
294
|
+
*
|
|
295
|
+
* The signer's claim may declare a capability envelope. We check the
|
|
296
|
+
* record against each present sub-field of the envelope and report any
|
|
297
|
+
* mismatches. The caller is responsible for supplying the ACTIVE envelope
|
|
298
|
+
* at the record's timestamp (i.e., picking the right historical version
|
|
299
|
+
* when capabilities have rotated per §6.7.4).
|
|
300
|
+
*
|
|
301
|
+
* Per §6.7.3 mismatches are SIGNALS, not invalidation. We don't push
|
|
302
|
+
* mismatches into `warnings` (which would set `valid: false`); they go
|
|
303
|
+
* into the structured `mismatches[]` field for consumer policy to weigh.
|
|
304
|
+
*
|
|
305
|
+
* For transaction records with `max_amount` or `counterparties`
|
|
306
|
+
* constraints, we set `unresolvable: true`. The protocol-specific
|
|
307
|
+
* transaction event isn't accessible to @atrib/verify; the caller would
|
|
308
|
+
* need to provide the resolved amount + counterparty out-of-band, which
|
|
309
|
+
* is a future-API extension if a real consumer needs it.
|
|
310
|
+
*/
|
|
311
|
+
declare function resolveCapabilityCheck(record: AtribRecord, claim: ResolvedIdentityClaim, _warnings: string[]): CapabilityCheckAnnotation;
|
|
312
|
+
/**
|
|
313
|
+
* Cross-attestation derivation per spec §1.7.6 (D052).
|
|
314
|
+
*
|
|
315
|
+
* For each entry in `record.signers`, verify the Ed25519 signature
|
|
316
|
+
* against the cross-attestation canonical bytes (JCS form with
|
|
317
|
+
* `signers: []` and the top-level `signature` field omitted). Count
|
|
318
|
+
* valid signatures; flag `missing: true` when fewer than 2 verify.
|
|
319
|
+
*
|
|
320
|
+
* Per §1.7.6 mismatches are SIGNALS, not invalidation: missing
|
|
321
|
+
* cross-attestation does NOT push to `warnings[]` (which would flip
|
|
322
|
+
* `valid` to false). The legacy top-level `signature` (already
|
|
323
|
+
* verified above by `verifyRecordSignature`) keeps the record
|
|
324
|
+
* cryptographically valid; cross_attestation is a policy signal.
|
|
325
|
+
*/
|
|
326
|
+
declare function resolveCrossAttestation(record: AtribRecord): Promise<CrossAttestationAnnotation>;
|
|
138
327
|
declare function detectPosture(record: AtribRecord, warnings: string[]): PostureAnnotation;
|
|
139
328
|
export declare const __test_only__: {
|
|
140
329
|
PROVENANCE_TOKEN_PATTERN: RegExp;
|
|
@@ -142,6 +331,8 @@ export declare const __test_only__: {
|
|
|
142
331
|
GRANULARITY_MULTIPLIER: Record<"s" | "ms" | "min" | "h" | "d", number>;
|
|
143
332
|
resolveProvenance: typeof resolveProvenance;
|
|
144
333
|
resolveInformedBy: typeof resolveInformedBy;
|
|
334
|
+
resolveCapabilityCheck: typeof resolveCapabilityCheck;
|
|
335
|
+
resolveCrossAttestation: typeof resolveCrossAttestation;
|
|
145
336
|
detectPosture: typeof detectPosture;
|
|
146
337
|
base64urlDecode: typeof base64urlDecode;
|
|
147
338
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-record.d.ts","sourceRoot":"","sources":["../src/verify-record.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"verify-record.d.ts","sourceRoot":"","sources":["../src/verify-record.ts"],"names":[],"mappings":"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"}
|
package/dist/verify-record.js
CHANGED
|
@@ -10,17 +10,29 @@
|
|
|
10
10
|
* Implemented annotations (this file):
|
|
11
11
|
* - provenance: { token, upstream_record_hash, upstream_resolved } (D044 / §1.2.6)
|
|
12
12
|
* - informed_by_resolution: { resolved: string[], dangling: string[] } (D041 / §1.2.5, §3.2.4)
|
|
13
|
-
* - posture: { timestamp_granularity, timestamp_consistent }
|
|
13
|
+
* - posture: { timestamp_granularity, timestamp_consistent, ...} (D045 / §8.2 / §8.3 / §8.4)
|
|
14
|
+
* - capability_check: { envelope, in_envelope, mismatches, unresolvable } (D051 / §6.7)
|
|
14
15
|
*
|
|
15
16
|
* Pending annotations (tracked in DECISIONS.md P005):
|
|
16
|
-
* - capability_check: D051 / §6.7 (needs @atrib/directory integration)
|
|
17
17
|
* - cross_attestation: D052 / §1.7.6 (needs `signers[]` type addition + transaction-record signing variant in @atrib/mcp)
|
|
18
18
|
* - cross_log_*: D050 / §2.11 (needs multi-log proof-bundle infrastructure)
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* Design note on dependencies. The capability_check annotation does NOT
|
|
21
|
+
* fetch the identity claim itself; the caller does the @atrib/directory
|
|
22
|
+
* lookup and passes the resolved claim into VerifyRecordOptions. This
|
|
23
|
+
* keeps @atrib/verify lean (no WASM-bridge dep) and lets the caller
|
|
24
|
+
* decide lookup strategy (cached vs live, batch vs per-record). Same
|
|
25
|
+
* pattern as `upstreamCandidate` and `informedByCandidates`.
|
|
20
26
|
*/
|
|
21
|
-
import { base64urlDecode, base64urlEncode, canonicalRecord, hexEncode, sha256, verifyRecord as verifyRecordSignature, } from '@atrib/mcp';
|
|
27
|
+
import { base64urlDecode, base64urlEncode, canonicalCrossAttestationInput, canonicalRecord, hexEncode, sha256, verifyRecord as verifyRecordSignature, } from '@atrib/mcp';
|
|
28
|
+
import * as ed from '@noble/ed25519';
|
|
22
29
|
const PROVENANCE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
23
30
|
const SHA256_REF_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
31
|
+
// §8.2 hashed tool_name form per D061. The verbatim and opaque-label forms
|
|
32
|
+
// are NOT structurally distinguishable (the spec's verbatim example
|
|
33
|
+
// `book_flight` also matches the opaque regex), so this is the only
|
|
34
|
+
// regex-detectable form.
|
|
35
|
+
const TOOL_NAME_HASHED_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
24
36
|
const GRANULARITY_MULTIPLIER = {
|
|
25
37
|
ms: 1,
|
|
26
38
|
s: 1000,
|
|
@@ -38,14 +50,31 @@ const GRANULARITY_MULTIPLIER = {
|
|
|
38
50
|
*/
|
|
39
51
|
export async function verifyRecord(record, options = {}) {
|
|
40
52
|
const warnings = [];
|
|
53
|
+
// §1.2.1 + §1.7.6: the top-level `signature` field is OPTIONAL on
|
|
54
|
+
// transaction records that carry the `signers[]` array. Skip the
|
|
55
|
+
// legacy single-sig check when (a) the record is a transaction AND
|
|
56
|
+
// (b) signers[] is present and non-empty. The cross_attestation
|
|
57
|
+
// annotation below covers signature verification per signer.
|
|
58
|
+
const isTransaction = record.event_type === 'https://atrib.dev/v1/types/transaction';
|
|
59
|
+
const hasSignersArray = Array.isArray(record.signers) && record.signers.length > 0;
|
|
60
|
+
const skipLegacySignatureCheck = isTransaction && hasSignersArray;
|
|
41
61
|
let signatureOk = false;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
if (skipLegacySignatureCheck) {
|
|
63
|
+
// §1.7.6: signature validity for transaction records lives in
|
|
64
|
+
// cross_attestation, not signatureOk. We treat signatureOk as true
|
|
65
|
+
// here so consumers' valid-bit logic isn't broken; the actual
|
|
66
|
+
// multi-sig validity is exposed via cross_attestation.signers_valid.
|
|
67
|
+
signatureOk = true;
|
|
46
68
|
}
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
else {
|
|
70
|
+
try {
|
|
71
|
+
signatureOk = await verifyRecordSignature(record);
|
|
72
|
+
if (!signatureOk)
|
|
73
|
+
warnings.push('signature verification failed');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
warnings.push(`signature verification error: ${err.message}`);
|
|
77
|
+
}
|
|
49
78
|
}
|
|
50
79
|
const result = {
|
|
51
80
|
valid: false,
|
|
@@ -67,6 +96,17 @@ export async function verifyRecord(record, options = {}) {
|
|
|
67
96
|
if (record.informed_by && record.informed_by.length > 0) {
|
|
68
97
|
result.informed_by_resolution = resolveInformedBy(record.informed_by, options.informedByCandidates ?? [], warnings);
|
|
69
98
|
}
|
|
99
|
+
// capability_check (D051 / §6.7) — surface only when caller supplied a
|
|
100
|
+
// resolved identity claim. We do not look up the claim ourselves to
|
|
101
|
+
// avoid coupling @atrib/verify to @atrib/directory's WASM bridge.
|
|
102
|
+
if (options.identityClaim) {
|
|
103
|
+
result.capability_check = resolveCapabilityCheck(record, options.identityClaim, warnings);
|
|
104
|
+
}
|
|
105
|
+
// cross_attestation (D052 / §1.7.6) — surface only on transaction records.
|
|
106
|
+
// Other event types continue to use the standard single-signer path.
|
|
107
|
+
if (record.event_type === 'https://atrib.dev/v1/types/transaction') {
|
|
108
|
+
result.cross_attestation = await resolveCrossAttestation(record);
|
|
109
|
+
}
|
|
70
110
|
result.valid = signatureOk && warnings.length === 0;
|
|
71
111
|
return result;
|
|
72
112
|
}
|
|
@@ -106,6 +146,128 @@ function resolveInformedBy(entries, candidates, warnings) {
|
|
|
106
146
|
}
|
|
107
147
|
return { resolved, dangling };
|
|
108
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Capability-check derivation per spec §6.7.2.
|
|
151
|
+
*
|
|
152
|
+
* The signer's claim may declare a capability envelope. We check the
|
|
153
|
+
* record against each present sub-field of the envelope and report any
|
|
154
|
+
* mismatches. The caller is responsible for supplying the ACTIVE envelope
|
|
155
|
+
* at the record's timestamp (i.e., picking the right historical version
|
|
156
|
+
* when capabilities have rotated per §6.7.4).
|
|
157
|
+
*
|
|
158
|
+
* Per §6.7.3 mismatches are SIGNALS, not invalidation. We don't push
|
|
159
|
+
* mismatches into `warnings` (which would set `valid: false`); they go
|
|
160
|
+
* into the structured `mismatches[]` field for consumer policy to weigh.
|
|
161
|
+
*
|
|
162
|
+
* For transaction records with `max_amount` or `counterparties`
|
|
163
|
+
* constraints, we set `unresolvable: true`. The protocol-specific
|
|
164
|
+
* transaction event isn't accessible to @atrib/verify; the caller would
|
|
165
|
+
* need to provide the resolved amount + counterparty out-of-band, which
|
|
166
|
+
* is a future-API extension if a real consumer needs it.
|
|
167
|
+
*/
|
|
168
|
+
function resolveCapabilityCheck(record, claim, _warnings) {
|
|
169
|
+
const envelope = claim.capabilities ?? null;
|
|
170
|
+
// Empty envelope or absent capabilities field: no constraint declared.
|
|
171
|
+
// Per §6.7.1: "A claim with `capabilities: {}` declares no scope."
|
|
172
|
+
if (!envelope || Object.keys(envelope).length === 0) {
|
|
173
|
+
return { envelope: null, in_envelope: true, mismatches: [], unresolvable: false };
|
|
174
|
+
}
|
|
175
|
+
const mismatches = [];
|
|
176
|
+
let unresolvable = false;
|
|
177
|
+
// expires_at: envelope expired when the record's timestamp is after the cutoff.
|
|
178
|
+
// Per §6.7.2: expired envelope is "treated as having no constraint and flagged
|
|
179
|
+
// separately" — we add the mismatch but don't treat it as out-of-envelope on
|
|
180
|
+
// its own; other sub-fields still apply if present.
|
|
181
|
+
if (typeof envelope.expires_at === 'number' && record.timestamp > envelope.expires_at) {
|
|
182
|
+
mismatches.push(`envelope expired at ${envelope.expires_at}; record timestamp ${record.timestamp}`);
|
|
183
|
+
}
|
|
184
|
+
// event_types: the record's event_type URI must be in the allowlist.
|
|
185
|
+
if (Array.isArray(envelope.event_types) && envelope.event_types.length > 0) {
|
|
186
|
+
if (!envelope.event_types.includes(record.event_type)) {
|
|
187
|
+
mismatches.push(`event_type '${record.event_type}' not in allowlist`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// tool_names: per §6.7.2 step 2, only applies to tool_call records and
|
|
191
|
+
// requires the record's tool_name. The current AtribRecord shape does
|
|
192
|
+
// not expose tool_name (per §8.2 default posture: only content_id is
|
|
193
|
+
// present, which is a hash of serverUrl + toolName). Without tool_name
|
|
194
|
+
// we can't check this constraint. Mark unresolvable.
|
|
195
|
+
if (Array.isArray(envelope.tool_names) && envelope.tool_names.length > 0) {
|
|
196
|
+
if (record.event_type === 'https://atrib.dev/v1/types/tool_call') {
|
|
197
|
+
unresolvable = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// max_amount + counterparties: per §6.7.2 the verifier "MUST resolve
|
|
201
|
+
// the transaction amount and counterparty from the protocol-specific
|
|
202
|
+
// transaction event the record commits to". @atrib/verify doesn't
|
|
203
|
+
// have access to the payment-protocol event; flag as unresolvable.
|
|
204
|
+
// Future API extension: accept resolved amount + counterparty as
|
|
205
|
+
// VerifyRecordOptions inputs if a real consumer needs it.
|
|
206
|
+
if (record.event_type === 'https://atrib.dev/v1/types/transaction') {
|
|
207
|
+
if (envelope.max_amount || (Array.isArray(envelope.counterparties) && envelope.counterparties.length > 0)) {
|
|
208
|
+
unresolvable = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
envelope,
|
|
213
|
+
in_envelope: mismatches.length === 0,
|
|
214
|
+
mismatches,
|
|
215
|
+
unresolvable,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Cross-attestation derivation per spec §1.7.6 (D052).
|
|
220
|
+
*
|
|
221
|
+
* For each entry in `record.signers`, verify the Ed25519 signature
|
|
222
|
+
* against the cross-attestation canonical bytes (JCS form with
|
|
223
|
+
* `signers: []` and the top-level `signature` field omitted). Count
|
|
224
|
+
* valid signatures; flag `missing: true` when fewer than 2 verify.
|
|
225
|
+
*
|
|
226
|
+
* Per §1.7.6 mismatches are SIGNALS, not invalidation: missing
|
|
227
|
+
* cross-attestation does NOT push to `warnings[]` (which would flip
|
|
228
|
+
* `valid` to false). The legacy top-level `signature` (already
|
|
229
|
+
* verified above by `verifyRecordSignature`) keeps the record
|
|
230
|
+
* cryptographically valid; cross_attestation is a policy signal.
|
|
231
|
+
*/
|
|
232
|
+
async function resolveCrossAttestation(record) {
|
|
233
|
+
const signers = Array.isArray(record.signers) ? record.signers : [];
|
|
234
|
+
const signers_count = signers.length;
|
|
235
|
+
if (signers_count === 0) {
|
|
236
|
+
// Legacy single-signer transaction record. Per §1.7.6 normative
|
|
237
|
+
// minimum is 2; flag as missing.
|
|
238
|
+
return { signers_count: 0, signers_valid: 0, missing: true };
|
|
239
|
+
}
|
|
240
|
+
// All signers cover the same canonical bytes (§1.7.6).
|
|
241
|
+
let canonicalBytes;
|
|
242
|
+
try {
|
|
243
|
+
canonicalBytes = canonicalCrossAttestationInput(record);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Canonicalization shouldn't fail on a structurally-valid record;
|
|
247
|
+
// if it does, treat all signers as unverifiable.
|
|
248
|
+
return { signers_count, signers_valid: 0, missing: true };
|
|
249
|
+
}
|
|
250
|
+
let signers_valid = 0;
|
|
251
|
+
for (const entry of signers) {
|
|
252
|
+
if (typeof entry?.creator_key !== 'string' || typeof entry?.signature !== 'string')
|
|
253
|
+
continue;
|
|
254
|
+
try {
|
|
255
|
+
const pubKey = base64urlDecode(entry.creator_key);
|
|
256
|
+
const sig = base64urlDecode(entry.signature);
|
|
257
|
+
const ok = await ed.verifyAsync(sig, canonicalBytes, pubKey);
|
|
258
|
+
if (ok)
|
|
259
|
+
signers_valid++;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Malformed key/sig bytes: skip without counting.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
signers_count,
|
|
267
|
+
signers_valid,
|
|
268
|
+
missing: signers_valid < 2,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
109
271
|
function detectPosture(record, warnings) {
|
|
110
272
|
const declared = record.timestamp_granularity;
|
|
111
273
|
const explicit = typeof declared === 'string';
|
|
@@ -116,10 +278,26 @@ function detectPosture(record, warnings) {
|
|
|
116
278
|
if (!consistent) {
|
|
117
279
|
warnings.push(`timestamp_granularity declares '${granularity}' but timestamp ${record.timestamp} is not a multiple of ${multiplier}`);
|
|
118
280
|
}
|
|
281
|
+
// Spec §8.3 commitment-posture detection: presence of args_salt /
|
|
282
|
+
// result_salt indicates the salted-sha256 scheme; absence indicates the
|
|
283
|
+
// default plain-sha256 scheme. The hmac-sha256 variant from §8.3 is
|
|
284
|
+
// signaled out-of-band and is not structurally detectable.
|
|
285
|
+
const args_commitment_form = typeof record.args_salt === 'string' ? 'salted-sha256' : 'plain-sha256';
|
|
286
|
+
const result_commitment_form = typeof record.result_salt === 'string' ? 'salted-sha256' : 'plain-sha256';
|
|
287
|
+
// Spec §8.2 / D061 tool_name_form: hashed when the value matches the
|
|
288
|
+
// `sha256:<hex>` form (unambiguous), plain otherwise (verbatim vs opaque
|
|
289
|
+
// not structurally distinguishable), null when the field is absent.
|
|
290
|
+
let tool_name_form = null;
|
|
291
|
+
if (typeof record.tool_name === 'string') {
|
|
292
|
+
tool_name_form = TOOL_NAME_HASHED_PATTERN.test(record.tool_name) ? 'hashed' : 'plain';
|
|
293
|
+
}
|
|
119
294
|
return {
|
|
120
295
|
timestamp_granularity: granularity,
|
|
121
296
|
timestamp_consistent: consistent,
|
|
122
297
|
timestamp_granularity_explicit: explicit,
|
|
298
|
+
args_commitment_form,
|
|
299
|
+
result_commitment_form,
|
|
300
|
+
tool_name_form,
|
|
123
301
|
};
|
|
124
302
|
}
|
|
125
303
|
// Re-exported defensively so consumers that want stricter validation can
|
|
@@ -130,6 +308,8 @@ export const __test_only__ = {
|
|
|
130
308
|
GRANULARITY_MULTIPLIER,
|
|
131
309
|
resolveProvenance,
|
|
132
310
|
resolveInformedBy,
|
|
311
|
+
resolveCapabilityCheck,
|
|
312
|
+
resolveCrossAttestation,
|
|
133
313
|
detectPosture,
|
|
134
314
|
base64urlDecode,
|
|
135
315
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-record.js","sourceRoot":"","sources":["../src/verify-record.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC
|
|
1
|
+
{"version":3,"file":"verify-record.js","sourceRoot":"","sources":["../src/verify-record.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atrib/verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@noble/ed25519": "^2.3.0",
|
|
31
31
|
"@noble/hashes": "^2.2.0",
|
|
32
32
|
"canonicalize": "^2.1.0",
|
|
33
|
-
"@atrib/mcp": "0.
|
|
33
|
+
"@atrib/mcp": "0.2.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^25.6.0",
|