@bsv/dpp-core 0.3.0-beta.1
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/LICENSE +58 -0
- package/README.md +95 -0
- package/dist/acceptance.d.ts +100 -0
- package/dist/acceptance.d.ts.map +1 -0
- package/dist/acceptance.js +202 -0
- package/dist/acceptance.js.map +1 -0
- package/dist/anchor.d.ts +66 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +145 -0
- package/dist/anchor.js.map +1 -0
- package/dist/attestation.d.ts +40 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +111 -0
- package/dist/attestation.js.map +1 -0
- package/dist/blob.d.ts +11 -0
- package/dist/blob.d.ts.map +1 -0
- package/dist/blob.js +16 -0
- package/dist/blob.js.map +1 -0
- package/dist/canonical.d.ts +32 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +54 -0
- package/dist/canonical.js.map +1 -0
- package/dist/canonicalJson.d.ts +22 -0
- package/dist/canonicalJson.d.ts.map +1 -0
- package/dist/canonicalJson.js +60 -0
- package/dist/canonicalJson.js.map +1 -0
- package/dist/codec.d.ts +45 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +459 -0
- package/dist/codec.js.map +1 -0
- package/dist/constants.d.ts +150 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +179 -0
- package/dist/constants.js.map +1 -0
- package/dist/did.d.ts +37 -0
- package/dist/did.d.ts.map +1 -0
- package/dist/did.js +129 -0
- package/dist/did.js.map +1 -0
- package/dist/evidence.d.ts +224 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +716 -0
- package/dist/evidence.js.map +1 -0
- package/dist/evidencePackage.d.ts +114 -0
- package/dist/evidencePackage.d.ts.map +1 -0
- package/dist/evidencePackage.js +164 -0
- package/dist/evidencePackage.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/owner.d.ts +135 -0
- package/dist/owner.d.ts.map +1 -0
- package/dist/owner.js +181 -0
- package/dist/owner.js.map +1 -0
- package/dist/publisherPolicy.d.ts +70 -0
- package/dist/publisherPolicy.d.ts.map +1 -0
- package/dist/publisherPolicy.js +186 -0
- package/dist/publisherPolicy.js.map +1 -0
- package/dist/signatures.d.ts +84 -0
- package/dist/signatures.d.ts.map +1 -0
- package/dist/signatures.js +137 -0
- package/dist/signatures.js.map +1 -0
- package/dist/transition.d.ts +29 -0
- package/dist/transition.d.ts.map +1 -0
- package/dist/transition.js +117 -0
- package/dist/transition.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/verifyChain.d.ts +152 -0
- package/dist/verifyChain.d.ts.map +1 -0
- package/dist/verifyChain.js +314 -0
- package/dist/verifyChain.js.map +1 -0
- package/package.json +46 -0
- package/schemas/capabilities.schema.json +129 -0
- package/schemas/epcis-import.schema.json +212 -0
- package/schemas/evidence-export.schema.json +67 -0
- package/schemas/evidence-package.schema.json +307 -0
- package/schemas/native-evidence-extension.schema.json +66 -0
- package/schemas/paginated-history.schema.json +32 -0
- package/schemas/passport-projection.schema.json +151 -0
- package/schemas/passport-source.schema.json +370 -0
- package/schemas/profile-evidence.schema.json +706 -0
- package/schemas/publisher-policy.schema.json +173 -0
- package/schemas/verification-report.schema.json +397 -0
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { Hash, LockingScript, Utils } from '@bsv/sdk';
|
|
2
|
+
import { inspectAttestationAnchor } from './anchor.js';
|
|
3
|
+
import { LIFECYCLE_MEDIA_TYPE, LIFECYCLE_REPRESENTATION, lifecycleClaimDigest, verifyLifecycleClaim, } from './attestation.js';
|
|
4
|
+
import { didKeyFromIdentityKey } from './did.js';
|
|
5
|
+
import { inspectChain } from './verifyChain.js';
|
|
6
|
+
import { verifyPolicyChain } from './publisherPolicy.js';
|
|
7
|
+
import { bindAcceptanceToState, inspectManagedAcceptance } from './acceptance.js';
|
|
8
|
+
/**
|
|
9
|
+
* The one verification contract (`spec/verification.md`): sixteen named checks,
|
|
10
|
+
* four answers each, an independently expected subject and an observation of
|
|
11
|
+
* the latest state that is never a proof. `verifyChain` stays the verifier of
|
|
12
|
+
* supplied history; this is the report every surface produces from it and
|
|
13
|
+
* from the attestation, anchor and credential rails beside it.
|
|
14
|
+
*
|
|
15
|
+
* The core is dependency-light, so the pieces that need a proof suite, a
|
|
16
|
+
* status list or an authority registry arrive through the policy as
|
|
17
|
+
* functions. A caller that supplies none gets `unknown` on those checks, with
|
|
18
|
+
* a reason that says why, which is the contract's whole point: nothing is
|
|
19
|
+
* passed because it was not looked at.
|
|
20
|
+
*/
|
|
21
|
+
export const REPORT_VERSION = '1';
|
|
22
|
+
export const EVIDENCE_CHECK_NAMES = [
|
|
23
|
+
'recordEncoding',
|
|
24
|
+
'actorSignatures',
|
|
25
|
+
'publisherSignatures',
|
|
26
|
+
'linkage',
|
|
27
|
+
'inclusion',
|
|
28
|
+
'nativeAttestationSignature',
|
|
29
|
+
'anchorSignature',
|
|
30
|
+
'anchorKeyDerivation',
|
|
31
|
+
'anchorDigestAndMetadataBinding',
|
|
32
|
+
'externalCredentialProof',
|
|
33
|
+
'subjectBinding',
|
|
34
|
+
'issuerAuthority',
|
|
35
|
+
'schema',
|
|
36
|
+
'credentialTime',
|
|
37
|
+
'credentialStatus',
|
|
38
|
+
'evidenceAvailability',
|
|
39
|
+
];
|
|
40
|
+
const outpointRef = (o) => `${o.txid}:${o.outputIndex}`;
|
|
41
|
+
const digestRef = (hex) => `sha256:${hex}`;
|
|
42
|
+
const sameOutpoint = (a, b) => a.txid === b.txid && a.outputIndex === b.outputIndex;
|
|
43
|
+
function toBytes(value) {
|
|
44
|
+
return typeof value === 'string' ? Utils.toArray(value, 'utf8') : value;
|
|
45
|
+
}
|
|
46
|
+
function check(name, status, reasonCode, evidenceRefs = [], extra = {}) {
|
|
47
|
+
return { name, status, ...(reasonCode == null ? {} : { reasonCode }), evidenceRefs, ...extra };
|
|
48
|
+
}
|
|
49
|
+
/** Combine several partial answers into one: a failure outranks an unknown, an unknown outranks a pass. */
|
|
50
|
+
function combine(name, parts, empty) {
|
|
51
|
+
if (parts.length === 0)
|
|
52
|
+
return empty;
|
|
53
|
+
const refs = [...new Set(parts.flatMap((p) => p.evidenceRefs ?? []))];
|
|
54
|
+
const details = parts.map((p) => p.detail).filter((d) => d !== undefined);
|
|
55
|
+
const pick = (status) => parts.find((p) => p.status === status);
|
|
56
|
+
const worst = pick('fail') ?? pick('unknown') ?? (parts.every((p) => p.status === 'not-applicable') ? pick('not-applicable') : undefined);
|
|
57
|
+
if (worst != null) {
|
|
58
|
+
return check(name, worst.status, worst.reasonCode ?? 'x-unspecified', refs, details.length > 0 ? { detail: details } : {});
|
|
59
|
+
}
|
|
60
|
+
return check(name, 'pass', undefined, refs, details.length > 0 ? { detail: details } : {});
|
|
61
|
+
}
|
|
62
|
+
function tokenChecks(txs, inspection, policy) {
|
|
63
|
+
const names = ['recordEncoding', 'actorSignatures', 'publisherSignatures', 'linkage', 'inclusion'];
|
|
64
|
+
const publisherSelected = (policy.publisherKeys ?? []).length > 0 || policy.publisherPolicy != null;
|
|
65
|
+
if (txs == null || txs.length === 0 || inspection == null) {
|
|
66
|
+
return {
|
|
67
|
+
tip: null,
|
|
68
|
+
checks: names.map((n) => n === 'publisherSignatures' && !publisherSelected
|
|
69
|
+
? check(n, 'not-applicable', 'publisher-not-selected')
|
|
70
|
+
: check(n, 'unknown', 'no-evidence')),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const states = inspection.states;
|
|
74
|
+
const refs = states.map((s) => outpointRef({ txid: s.txid, outputIndex: s.outputIndex }));
|
|
75
|
+
const perState = states.map((s) => ({
|
|
76
|
+
index: s.index,
|
|
77
|
+
txid: s.txid,
|
|
78
|
+
version: s.version,
|
|
79
|
+
op: s.op,
|
|
80
|
+
userSignatureValid: s.userSignatureValid,
|
|
81
|
+
serverSignatureValid: s.serverSignatureValid,
|
|
82
|
+
linkageValid: s.linkageValid,
|
|
83
|
+
ownerConsentValid: s.ownerConsentValid,
|
|
84
|
+
controlValid: s.controlValid,
|
|
85
|
+
spv: s.spv,
|
|
86
|
+
...(s.spvReason == null ? {} : { spvReason: s.spvReason }),
|
|
87
|
+
}));
|
|
88
|
+
const failure = inspection.failure;
|
|
89
|
+
// The inspection evaluates every check on a state before stopping at it, so
|
|
90
|
+
// the states nobody looked at are the ones after the failing index; a
|
|
91
|
+
// transaction that yielded no state was looked at and could not be checked.
|
|
92
|
+
const inspectedCount = failure?.kind === 'encoding' ? failure.index + 1 : states.length;
|
|
93
|
+
const uninspected = inspectedCount < txs.length;
|
|
94
|
+
/** A check that held on every inspected state: pass when every supplied state was inspected, otherwise unknown. */
|
|
95
|
+
const heldOrNotInspected = (name) => failure?.kind === 'encoding'
|
|
96
|
+
? check(name, 'unknown', 'decode-failed', refs, { detail: perState, scope: { stateIndex: failure.index } })
|
|
97
|
+
: uninspected
|
|
98
|
+
? check(name, 'unknown', 'not-inspected', refs, { detail: perState, scope: failure == null ? {} : { stateIndex: failure.index } })
|
|
99
|
+
: check(name, 'pass', undefined, refs, { detail: perState });
|
|
100
|
+
const encoding = failure?.kind === 'encoding'
|
|
101
|
+
? check('recordEncoding', 'fail', 'decode-failed', [...refs, failure.txid], { scope: { stateIndex: failure.index }, detail: failure.message })
|
|
102
|
+
: heldOrNotInspected('recordEncoding');
|
|
103
|
+
const actor = failure?.kind === 'userSignature'
|
|
104
|
+
? check('actorSignatures', 'fail', 'signature-invalid', refs, { scope: { stateIndex: failure.index }, detail: perState })
|
|
105
|
+
: heldOrNotInspected('actorSignatures');
|
|
106
|
+
const publisher = !publisherSelected
|
|
107
|
+
? check('publisherSignatures', 'not-applicable', 'publisher-not-selected', [])
|
|
108
|
+
: failure?.kind === 'serverSignature'
|
|
109
|
+
? check('publisherSignatures', 'fail', states[failure.index]?.publisherOutsideWindow ? 'publisher-not-authorised' : 'signature-invalid', refs, { scope: { stateIndex: failure.index }, detail: perState })
|
|
110
|
+
: heldOrNotInspected('publisherSignatures');
|
|
111
|
+
const linkage = failure?.kind === 'linkage'
|
|
112
|
+
? check('linkage', 'fail', linkageReasonCode(failure.message), refs, { scope: { stateIndex: failure.index }, detail: failure.message })
|
|
113
|
+
: failure?.kind === 'consent'
|
|
114
|
+
? check('linkage', 'fail', failure.message.includes('authorisation_commitment') ? 'acceptance-commitment-absent' : 'consent-not-proven', refs, { scope: { stateIndex: failure.index }, detail: failure.message })
|
|
115
|
+
: heldOrNotInspected('linkage');
|
|
116
|
+
let inclusion;
|
|
117
|
+
if (failure?.kind === 'inclusion') {
|
|
118
|
+
inclusion = check('inclusion', 'fail', 'proof-refuted', refs, { scope: { stateIndex: failure.index }, detail: failure.message });
|
|
119
|
+
}
|
|
120
|
+
else if (failure?.kind === 'encoding') {
|
|
121
|
+
inclusion = check('inclusion', 'unknown', 'decode-failed', refs, { scope: { stateIndex: failure.index }, detail: perState });
|
|
122
|
+
}
|
|
123
|
+
else if (uninspected) {
|
|
124
|
+
inclusion = check('inclusion', 'unknown', 'not-inspected', refs, { detail: perState });
|
|
125
|
+
}
|
|
126
|
+
else if (states.some((s) => s.spvReason === 'header-source-unavailable')) {
|
|
127
|
+
inclusion = check('inclusion', 'unknown', 'header-source-unavailable', refs, { detail: inspection.spvUnavailable ?? perState });
|
|
128
|
+
}
|
|
129
|
+
else if (states.some((s) => s.spvReason === 'no-proof')) {
|
|
130
|
+
inclusion = check('inclusion', 'unknown', 'proof-absent', refs, { detail: perState });
|
|
131
|
+
}
|
|
132
|
+
else if (states.some((s) => s.spvReason === 'header-check-disabled')) {
|
|
133
|
+
inclusion = check('inclusion', 'unknown', 'not-selected', refs, { detail: perState });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
inclusion = check('inclusion', 'pass', undefined, refs, { detail: perState });
|
|
137
|
+
}
|
|
138
|
+
const last = states.at(-1);
|
|
139
|
+
return {
|
|
140
|
+
inspection,
|
|
141
|
+
tip: last == null ? null : { txid: last.txid, outputIndex: last.outputIndex },
|
|
142
|
+
genesis: states[0],
|
|
143
|
+
checks: [encoding, actor, publisher, linkage, inclusion],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Which shared reason code a linkage failure carries. The message is the
|
|
148
|
+
* verifier's own sentence (`transition.ts`, `owner.ts`); the code is the
|
|
149
|
+
* contract's word for the rule it names, so a consumer branches on the code
|
|
150
|
+
* and a person reads the sentence.
|
|
151
|
+
*/
|
|
152
|
+
function linkageReasonCode(message) {
|
|
153
|
+
if (message.includes('retired'))
|
|
154
|
+
return 'lineage-retired';
|
|
155
|
+
if (message.includes('control_linkage') || message.includes('not the controller'))
|
|
156
|
+
return 'control-not-proven';
|
|
157
|
+
if (message.includes('version 1') || message.includes('version 2') || message.includes('upgrade transition'))
|
|
158
|
+
return 'version-transition-invalid';
|
|
159
|
+
return 'link-broken';
|
|
160
|
+
}
|
|
161
|
+
function inspectNativeClaims(claims) {
|
|
162
|
+
const findings = { claims: [], invalid: [] };
|
|
163
|
+
for (const [index, value] of (claims ?? []).entries()) {
|
|
164
|
+
// Subject binding is its own check; the signature check must not fail a
|
|
165
|
+
// claim for naming another product, so no expectation is passed here.
|
|
166
|
+
const result = verifyLifecycleClaim(value);
|
|
167
|
+
if (result.signature !== 'verified') {
|
|
168
|
+
findings.invalid.push({ index, reason: result.reason ?? 'native claim signature does not verify' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const claim = value;
|
|
172
|
+
const digest = lifecycleClaimDigest(claim);
|
|
173
|
+
findings.claims.push({ claim, digest, issuerKey: result.issuerKey, ref: digestRef(digest) });
|
|
174
|
+
}
|
|
175
|
+
return findings;
|
|
176
|
+
}
|
|
177
|
+
function inspectAnchors(anchors) {
|
|
178
|
+
return (anchors ?? []).map((anchor, index) => {
|
|
179
|
+
// A script that is not even hex is evidence that fails, not an exception:
|
|
180
|
+
// the finding carries the reason and the report stays a report.
|
|
181
|
+
let script;
|
|
182
|
+
let parseFailure;
|
|
183
|
+
try {
|
|
184
|
+
script = typeof anchor.lockingScript === 'string' ? LockingScript.fromHex(anchor.lockingScript) : anchor.lockingScript;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
parseFailure = 'lockingScript is not hex';
|
|
188
|
+
}
|
|
189
|
+
const inspection = script == null
|
|
190
|
+
? { metadata: undefined, keyDerivationValid: null, signatureValid: null, failure: parseFailure }
|
|
191
|
+
: inspectAttestationAnchor(script);
|
|
192
|
+
const ref = anchor.txid != null && anchor.outputIndex != null ? outpointRef({ txid: anchor.txid, outputIndex: anchor.outputIndex }) : `anchor:${index}`;
|
|
193
|
+
return {
|
|
194
|
+
index,
|
|
195
|
+
ref,
|
|
196
|
+
metadata: inspection.metadata,
|
|
197
|
+
keyDerivationValid: inspection.keyDerivationValid,
|
|
198
|
+
signatureValid: inspection.signatureValid,
|
|
199
|
+
failure: inspection.failure,
|
|
200
|
+
...(anchor.securedBytes == null ? {} : { securedBytes: toBytes(anchor.securedBytes) }),
|
|
201
|
+
};
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function observe(policy, passportId, tip, alternativeTips, checkedAt) {
|
|
205
|
+
const observers = policy.observers ?? [];
|
|
206
|
+
return Promise.all(observers.map(async (observer) => {
|
|
207
|
+
try {
|
|
208
|
+
const answer = await observer.observe({ passportId, tip });
|
|
209
|
+
return {
|
|
210
|
+
id: observer.id,
|
|
211
|
+
kind: observer.kind,
|
|
212
|
+
observedAt: checkedAt,
|
|
213
|
+
result: answer.result,
|
|
214
|
+
...(answer.height == null ? {} : { height: answer.height }),
|
|
215
|
+
...(answer.blockHash == null ? {} : { blockHash: answer.blockHash }),
|
|
216
|
+
...(answer.spendingTxid == null ? {} : { spendingTxid: answer.spendingTxid }),
|
|
217
|
+
...(answer.detail == null ? {} : { detail: answer.detail }),
|
|
218
|
+
candidateOutpoints: answer.candidateOutpoints ?? [],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (cause) {
|
|
222
|
+
return {
|
|
223
|
+
id: observer.id,
|
|
224
|
+
kind: observer.kind,
|
|
225
|
+
observedAt: checkedAt,
|
|
226
|
+
result: 'unavailable',
|
|
227
|
+
detail: cause instanceof Error ? cause.message : String(cause),
|
|
228
|
+
candidateOutpoints: [],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
})).then((answers) => {
|
|
232
|
+
const candidates = [];
|
|
233
|
+
const add = (o) => {
|
|
234
|
+
if (!candidates.some((c) => sameOutpoint(c, o)))
|
|
235
|
+
candidates.push(o);
|
|
236
|
+
};
|
|
237
|
+
if (tip != null)
|
|
238
|
+
add(tip);
|
|
239
|
+
alternativeTips.forEach(add);
|
|
240
|
+
answers.forEach((a) => a.candidateOutpoints.forEach(add));
|
|
241
|
+
const answered = answers.filter((a) => a.result !== 'unavailable');
|
|
242
|
+
let latestState;
|
|
243
|
+
if (alternativeTips.length > 0 || answers.some((a) => a.result === 'conflicting'))
|
|
244
|
+
latestState = 'conflicting';
|
|
245
|
+
else if (answered.length === 0)
|
|
246
|
+
latestState = 'unknown';
|
|
247
|
+
else if (answered.some((a) => a.result === 'spent')) {
|
|
248
|
+
latestState = answered.every((a) => a.result === 'spent') ? 'superseded' : 'conflicting';
|
|
249
|
+
}
|
|
250
|
+
else if (answered.every((a) => a.result === 'unspent'))
|
|
251
|
+
latestState = 'observed';
|
|
252
|
+
else
|
|
253
|
+
latestState = 'conflicting';
|
|
254
|
+
return {
|
|
255
|
+
latestState,
|
|
256
|
+
sources: answers.map(({ candidateOutpoints: _c, ...source }) => source),
|
|
257
|
+
queryScope: observers.length === 0 ? 'no source asked' : `${observers.map((o) => `${o.kind} ${o.id}`).join(', ')} asked for ${passportId}`,
|
|
258
|
+
observedAt: checkedAt,
|
|
259
|
+
candidateOutpoints: candidates,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Evaluate the evidence for one passport and report it as `spec/verification.md`
|
|
265
|
+
* requires. Never throws for evidence that fails: every failure is a check with
|
|
266
|
+
* a reason. It throws only for a caller error, such as a malformed policy key.
|
|
267
|
+
*/
|
|
268
|
+
export async function verifyPassportEvidence(evidence, expected, policy = {}) {
|
|
269
|
+
const checkedAt = policy.checkedAt ?? new Date().toISOString();
|
|
270
|
+
const policyId = policy.policyId ?? 'none';
|
|
271
|
+
const limits = [
|
|
272
|
+
'Supplied-history validity does not establish current ownership, an authorised genesis or the absence of later states.',
|
|
273
|
+
'An anchor establishes its anchoring service\'s commitment to bytes, not the truth of the claim those bytes carry.',
|
|
274
|
+
];
|
|
275
|
+
// The publisher policy is verified before any key in it is trusted. A chain
|
|
276
|
+
// that does not hold selects the check (the caller asked for it) and leaves
|
|
277
|
+
// it unknown: no key is trusted, and none is called a stranger either.
|
|
278
|
+
const policyChain = policy.publisherPolicy == null ? undefined : verifyPolicyChain(policy.publisherPolicy.chain, policy.publisherPolicy.operatorIdentityKeys);
|
|
279
|
+
const verifiedPolicyChain = policyChain?.ok === true ? policy.publisherPolicy.chain : undefined;
|
|
280
|
+
const policyFailure = policyChain != null && !policyChain.ok ? policyChain.failure : undefined;
|
|
281
|
+
// The token rail, and the alternative histories that make a genesis ambiguous.
|
|
282
|
+
const txs = evidence.tokenHistory;
|
|
283
|
+
const chainOptions = {
|
|
284
|
+
chainTracker: policy.chainTracker,
|
|
285
|
+
publisherKeys: policy.publisherKeys,
|
|
286
|
+
publisherPolicy: verifiedPolicyChain,
|
|
287
|
+
ownerConsent: policy.ownerConsent,
|
|
288
|
+
...(policy.controlAuthorities == null ? {} : { controlAuthorities: policy.controlAuthorities }),
|
|
289
|
+
managedAcceptance: policy.managedAcceptance?.required === true,
|
|
290
|
+
};
|
|
291
|
+
const inspection = txs != null && txs.length > 0 ? await inspectChain(txs, chainOptions) : undefined;
|
|
292
|
+
const token = tokenChecks(txs, inspection, policy);
|
|
293
|
+
if (policyFailure != null) {
|
|
294
|
+
const at = token.checks.findIndex((c) => c.name === 'publisherSignatures');
|
|
295
|
+
if (at >= 0)
|
|
296
|
+
token.checks[at] = check('publisherSignatures', 'unknown', 'publisher-policy-invalid', [], { detail: `publisher policy version ${policyFailure.version}: ${policyFailure.reason}, ${policyFailure.detail}` });
|
|
297
|
+
}
|
|
298
|
+
const alternatives = [];
|
|
299
|
+
for (const history of evidence.alternativeHistories ?? []) {
|
|
300
|
+
if (history.length === 0)
|
|
301
|
+
continue;
|
|
302
|
+
const alt = await inspectChain(history, chainOptions);
|
|
303
|
+
const last = alt.states.at(-1);
|
|
304
|
+
alternatives.push({ inspection: alt, tip: last == null ? null : { txid: last.txid, outputIndex: last.outputIndex } });
|
|
305
|
+
}
|
|
306
|
+
const validAlternatives = alternatives.filter((a) => a.inspection.complete && a.inspection.states[0]?.state.passportId === expected.passportId);
|
|
307
|
+
// A valid alternative that shares this history's genesis is a fork, two
|
|
308
|
+
// successors of one state, which accepted-chain spending decides and this
|
|
309
|
+
// report only names; one with another genesis is a rival claim to the
|
|
310
|
+
// identifier, which an authority policy decides. The two are told apart
|
|
311
|
+
// because they mean different things and get different words.
|
|
312
|
+
const genesisTxid = token.genesis?.txid;
|
|
313
|
+
const forks = validAlternatives.filter((a) => genesisTxid != null && a.inspection.states[0]?.txid === genesisTxid);
|
|
314
|
+
const rivalGenesis = validAlternatives.filter((a) => !forks.includes(a));
|
|
315
|
+
// The attestation and anchor rails.
|
|
316
|
+
const native = inspectNativeClaims(evidence.nativeClaims);
|
|
317
|
+
const anchors = inspectAnchors(evidence.anchors);
|
|
318
|
+
const nativeRefs = native.claims.map((c) => c.ref);
|
|
319
|
+
const nativeAttestationSignature = (evidence.nativeClaims ?? []).length === 0
|
|
320
|
+
? check('nativeAttestationSignature', 'unknown', 'no-evidence')
|
|
321
|
+
: native.invalid.length > 0
|
|
322
|
+
? check('nativeAttestationSignature', 'fail', 'signature-invalid', nativeRefs, { detail: native.invalid })
|
|
323
|
+
: check('nativeAttestationSignature', 'pass', undefined, nativeRefs);
|
|
324
|
+
const anchorRefs = anchors.map((a) => a.ref);
|
|
325
|
+
const anchorSignature = anchors.length === 0
|
|
326
|
+
? check('anchorSignature', 'unknown', 'no-evidence')
|
|
327
|
+
: anchors.some((a) => a.metadata == null)
|
|
328
|
+
? check('anchorSignature', 'fail', 'decode-failed', anchorRefs, { detail: anchors.filter((a) => a.metadata == null).map((a) => ({ ref: a.ref, failure: a.failure })) })
|
|
329
|
+
: anchors.some((a) => a.signatureValid !== true)
|
|
330
|
+
? check('anchorSignature', 'fail', 'signature-invalid', anchorRefs, { detail: anchors.filter((a) => a.signatureValid !== true).map((a) => a.ref) })
|
|
331
|
+
: check('anchorSignature', 'pass', undefined, anchorRefs);
|
|
332
|
+
const anchorKeyDerivation = anchors.length === 0
|
|
333
|
+
? check('anchorKeyDerivation', 'unknown', 'no-evidence')
|
|
334
|
+
: anchors.some((a) => a.metadata == null)
|
|
335
|
+
? check('anchorKeyDerivation', 'unknown', 'decode-failed', anchorRefs)
|
|
336
|
+
: anchors.some((a) => a.keyDerivationValid !== true)
|
|
337
|
+
? check('anchorKeyDerivation', 'fail', 'key-derivation-mismatch', anchorRefs, { detail: anchors.filter((a) => a.keyDerivationValid !== true).map((a) => a.ref) })
|
|
338
|
+
: check('anchorKeyDerivation', 'pass', undefined, anchorRefs);
|
|
339
|
+
// Secured representations: the bytes an anchor commits to, from the anchor
|
|
340
|
+
// itself, from a supplied credential, or from a supplied native claim.
|
|
341
|
+
const externals = (evidence.externalCredentials ?? []).map((c, index) => {
|
|
342
|
+
const bytes = toBytes(c.bytes);
|
|
343
|
+
return { index, representation: c.representation, mediaType: c.mediaType, bytes, digest: Utils.toHex(Hash.sha256(bytes)) };
|
|
344
|
+
});
|
|
345
|
+
const bytesByDigest = new Map();
|
|
346
|
+
for (const ext of externals)
|
|
347
|
+
bytesByDigest.set(ext.digest, { bytes: ext.bytes, representation: ext.representation });
|
|
348
|
+
for (const anchor of anchors) {
|
|
349
|
+
if (anchor.securedBytes != null)
|
|
350
|
+
bytesByDigest.set(Utils.toHex(Hash.sha256(anchor.securedBytes)), { bytes: anchor.securedBytes });
|
|
351
|
+
}
|
|
352
|
+
// Credential verification runs once per distinct byte string, and its answers
|
|
353
|
+
// serve both the anchor binding and the credential checks.
|
|
354
|
+
const verifications = new Map();
|
|
355
|
+
const credentialInputs = new Map();
|
|
356
|
+
for (const ext of externals)
|
|
357
|
+
credentialInputs.set(ext.digest, { representation: ext.representation, mediaType: ext.mediaType, bytes: ext.bytes });
|
|
358
|
+
// An anchor that declares an external representation and arrives without
|
|
359
|
+
// its bytes is a missing input: the proof check is unknown for it, never
|
|
360
|
+
// not-applicable (spec/verification.md section 4).
|
|
361
|
+
const proofExtras = [];
|
|
362
|
+
for (const anchor of anchors) {
|
|
363
|
+
if (anchor.metadata == null)
|
|
364
|
+
continue;
|
|
365
|
+
if (anchor.metadata.representation === LIFECYCLE_REPRESENTATION)
|
|
366
|
+
continue;
|
|
367
|
+
if (anchor.securedBytes == null) {
|
|
368
|
+
proofExtras.push({ status: 'unknown', reasonCode: 'secured-bytes-absent', evidenceRefs: [anchor.ref, digestRef(anchor.metadata.digest)], detail: `anchor ${anchor.ref} declares ${anchor.metadata.representation} and its bytes were not supplied` });
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const digest = Utils.toHex(Hash.sha256(anchor.securedBytes));
|
|
372
|
+
if (!credentialInputs.has(digest))
|
|
373
|
+
credentialInputs.set(digest, { representation: anchor.metadata.representation, mediaType: anchor.metadata.mediaType, bytes: anchor.securedBytes });
|
|
374
|
+
}
|
|
375
|
+
if (policy.credentialVerifier != null) {
|
|
376
|
+
for (const [digest, input] of credentialInputs) {
|
|
377
|
+
try {
|
|
378
|
+
verifications.set(digest, await policy.credentialVerifier({ ...input, digest, checkedAt }));
|
|
379
|
+
}
|
|
380
|
+
catch (cause) {
|
|
381
|
+
const failed = { status: 'unknown', reasonCode: 'verifier-not-supplied', detail: cause instanceof Error ? cause.message : String(cause) };
|
|
382
|
+
verifications.set(digest, { proof: failed, schema: failed, time: failed, status: failed, authority: failed });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const bindingParts = [];
|
|
387
|
+
const unavailableRefs = [];
|
|
388
|
+
for (const anchor of anchors) {
|
|
389
|
+
const meta = anchor.metadata;
|
|
390
|
+
if (meta == null)
|
|
391
|
+
continue;
|
|
392
|
+
const ref = anchor.ref;
|
|
393
|
+
const scopeRefs = [ref, digestRef(meta.digest)];
|
|
394
|
+
if (meta.representation === LIFECYCLE_REPRESENTATION) {
|
|
395
|
+
const claim = native.claims.find((c) => c.digest === meta.digest);
|
|
396
|
+
const bytes = anchor.securedBytes;
|
|
397
|
+
if (claim == null && bytes == null) {
|
|
398
|
+
unavailableRefs.push(digestRef(meta.digest));
|
|
399
|
+
bindingParts.push({ status: 'unknown', reasonCode: 'secured-bytes-absent', evidenceRefs: scopeRefs });
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (claim == null && bytes != null) {
|
|
403
|
+
// Bytes supplied for a native anchor must themselves be a signed claim.
|
|
404
|
+
let parsed;
|
|
405
|
+
try {
|
|
406
|
+
parsed = JSON.parse(Utils.toUTF8(bytes));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
parsed = undefined;
|
|
410
|
+
}
|
|
411
|
+
const result = parsed === undefined ? undefined : verifyLifecycleClaim(parsed);
|
|
412
|
+
if (result?.signature !== 'verified') {
|
|
413
|
+
bindingParts.push({ status: 'fail', reasonCode: 'digest-mismatch', evidenceRefs: scopeRefs, detail: 'supplied bytes are not a verified native claim with this digest' });
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const digest = lifecycleClaimDigest(parsed);
|
|
417
|
+
if (digest !== meta.digest) {
|
|
418
|
+
bindingParts.push({ status: 'fail', reasonCode: 'digest-mismatch', evidenceRefs: [...scopeRefs, digestRef(digest)] });
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
native.claims.push({ claim: parsed, digest, issuerKey: result.issuerKey, ref: digestRef(digest) });
|
|
422
|
+
}
|
|
423
|
+
const bound = native.claims.find((c) => c.digest === meta.digest);
|
|
424
|
+
const mismatches = [];
|
|
425
|
+
if (meta.issuer !== bound.claim.issuer)
|
|
426
|
+
mismatches.push('issuer');
|
|
427
|
+
if (meta.subject !== bound.claim.passportId)
|
|
428
|
+
mismatches.push('subject');
|
|
429
|
+
if (meta.attestationType !== bound.claim.eventType)
|
|
430
|
+
mismatches.push('attestationType');
|
|
431
|
+
if (meta.mediaType !== LIFECYCLE_MEDIA_TYPE)
|
|
432
|
+
mismatches.push('mediaType');
|
|
433
|
+
bindingParts.push(mismatches.length === 0
|
|
434
|
+
? { status: 'pass', evidenceRefs: scopeRefs }
|
|
435
|
+
: { status: 'fail', reasonCode: 'metadata-mismatch', evidenceRefs: scopeRefs, detail: mismatches });
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const supplied = bytesByDigest.get(meta.digest);
|
|
439
|
+
const bytes = anchor.securedBytes ?? supplied?.bytes;
|
|
440
|
+
if (bytes == null || bytes.length === 0) {
|
|
441
|
+
unavailableRefs.push(digestRef(meta.digest));
|
|
442
|
+
bindingParts.push({ status: 'unknown', reasonCode: 'secured-bytes-absent', evidenceRefs: scopeRefs });
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const digest = Utils.toHex(Hash.sha256(bytes));
|
|
446
|
+
if (digest !== meta.digest) {
|
|
447
|
+
bindingParts.push({ status: 'fail', reasonCode: 'digest-mismatch', evidenceRefs: [...scopeRefs, digestRef(digest)] });
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const verified = verifications.get(digest);
|
|
451
|
+
if (verified == null) {
|
|
452
|
+
bindingParts.push({ status: 'unknown', reasonCode: policy.credentialVerifier == null ? 'representation-unsupported' : 'verifier-not-supplied', evidenceRefs: scopeRefs, detail: meta.representation });
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (verified.proof.status !== 'pass') {
|
|
456
|
+
bindingParts.push({ status: 'unknown', reasonCode: 'referenced-artefact-mismatch', evidenceRefs: scopeRefs, detail: 'the committed credential did not verify, so its metadata cannot be compared' });
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const mismatches = [];
|
|
460
|
+
if (verified.issuer != null && meta.issuer !== verified.issuer)
|
|
461
|
+
mismatches.push('issuer');
|
|
462
|
+
if (verified.subjects != null && !verified.subjects.includes(meta.subject))
|
|
463
|
+
mismatches.push('subject');
|
|
464
|
+
if (verified.attestationType != null && meta.attestationType !== verified.attestationType)
|
|
465
|
+
mismatches.push('attestationType');
|
|
466
|
+
if (verified.id != null && meta.attestationId !== verified.id)
|
|
467
|
+
mismatches.push('attestationId');
|
|
468
|
+
bindingParts.push(mismatches.length === 0
|
|
469
|
+
? { status: 'pass', evidenceRefs: scopeRefs }
|
|
470
|
+
: { status: 'fail', reasonCode: 'metadata-mismatch', evidenceRefs: scopeRefs, detail: mismatches });
|
|
471
|
+
}
|
|
472
|
+
const anchorDigestAndMetadataBinding = anchors.length === 0
|
|
473
|
+
? check('anchorDigestAndMetadataBinding', 'unknown', 'no-evidence')
|
|
474
|
+
: combine('anchorDigestAndMetadataBinding', bindingParts, check('anchorDigestAndMetadataBinding', 'unknown', 'decode-failed', anchorRefs));
|
|
475
|
+
// Credential checks: external credentials through the supplied verifier.
|
|
476
|
+
const credentialParts = (key) => [...credentialInputs.keys()].map((digest) => {
|
|
477
|
+
const v = verifications.get(digest);
|
|
478
|
+
if (v == null)
|
|
479
|
+
return { status: 'unknown', reasonCode: 'verifier-not-supplied', evidenceRefs: [digestRef(digest)] };
|
|
480
|
+
return { ...v[key], evidenceRefs: [digestRef(digest), ...(v[key].evidenceRefs ?? [])] };
|
|
481
|
+
});
|
|
482
|
+
const externalCredentialProof = credentialInputs.size === 0 && proofExtras.length === 0
|
|
483
|
+
? check('externalCredentialProof', 'not-applicable', 'no-external-credential')
|
|
484
|
+
: combine('externalCredentialProof', [...credentialParts('proof'), ...proofExtras], check('externalCredentialProof', 'unknown', 'verifier-not-supplied'));
|
|
485
|
+
// Subject binding, from an expectation the evidence did not supply.
|
|
486
|
+
const subjectRefs = [];
|
|
487
|
+
let subjectBinding;
|
|
488
|
+
if (expected.source === 'none') {
|
|
489
|
+
subjectBinding = check('subjectBinding', 'unknown', 'subject-not-independent');
|
|
490
|
+
limits.push('The subject was taken from the evidence itself, so no check can establish that the evidence concerns the passport the reader asked about.');
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const problems = [];
|
|
494
|
+
for (const s of token.inspection?.states ?? []) {
|
|
495
|
+
subjectRefs.push(outpointRef({ txid: s.txid, outputIndex: s.outputIndex }));
|
|
496
|
+
if (s.state.passportId !== expected.passportId)
|
|
497
|
+
problems.push({ reasonCode: 'subject-mismatch', detail: `state ${s.index} names ${s.state.passportId}` });
|
|
498
|
+
}
|
|
499
|
+
if (token.genesis != null) {
|
|
500
|
+
const genesisOutpoint = { txid: token.genesis.txid, outputIndex: token.genesis.outputIndex };
|
|
501
|
+
if (expected.expectedGenesisOutpoint != null && !sameOutpoint(expected.expectedGenesisOutpoint, genesisOutpoint)) {
|
|
502
|
+
problems.push({ reasonCode: 'genesis-mismatch', detail: `genesis is ${outpointRef(genesisOutpoint)}, expected ${outpointRef(expected.expectedGenesisOutpoint)}` });
|
|
503
|
+
}
|
|
504
|
+
if (expected.expectedIssuer != null) {
|
|
505
|
+
const actorKey = token.genesis.state.actorIdentityKey;
|
|
506
|
+
const names = [actorKey, didKeyFromIdentityKey(actorKey)];
|
|
507
|
+
if (!names.includes(expected.expectedIssuer))
|
|
508
|
+
problems.push({ reasonCode: 'issuer-mismatch', detail: `genesis actor is ${names[1]}` });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (expected.expectedStateOutpoint != null && token.tip != null && !sameOutpoint(expected.expectedStateOutpoint, token.tip)) {
|
|
512
|
+
problems.push({ reasonCode: 'state-mismatch', detail: `supplied tip is ${outpointRef(token.tip)}, expected ${outpointRef(expected.expectedStateOutpoint)}` });
|
|
513
|
+
}
|
|
514
|
+
for (const c of native.claims) {
|
|
515
|
+
subjectRefs.push(c.ref);
|
|
516
|
+
if (c.claim.passportId !== expected.passportId)
|
|
517
|
+
problems.push({ reasonCode: 'subject-mismatch', detail: `native claim ${c.ref} names ${c.claim.passportId}` });
|
|
518
|
+
if (expected.expectedIssuer != null && c.claim.issuer !== expected.expectedIssuer && c.claim.issuerKeyDid !== expected.expectedIssuer) {
|
|
519
|
+
problems.push({ reasonCode: 'issuer-mismatch', detail: `native claim ${c.ref} is issued by ${c.claim.issuer}` });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const a of anchors) {
|
|
523
|
+
if (a.metadata == null)
|
|
524
|
+
continue;
|
|
525
|
+
subjectRefs.push(a.ref);
|
|
526
|
+
const accepted = [expected.passportId, ...(expected.productIdentifier == null ? [] : [expected.productIdentifier])];
|
|
527
|
+
if (!accepted.includes(a.metadata.subject))
|
|
528
|
+
problems.push({ reasonCode: 'subject-mismatch', detail: `anchor ${a.ref} names ${a.metadata.subject}` });
|
|
529
|
+
}
|
|
530
|
+
for (const [digest, v] of verifications) {
|
|
531
|
+
subjectRefs.push(digestRef(digest));
|
|
532
|
+
const accepted = [expected.passportId, ...(expected.productIdentifier == null ? [] : [expected.productIdentifier])];
|
|
533
|
+
if (v.subjects != null && !v.subjects.some((s) => accepted.includes(s)))
|
|
534
|
+
problems.push({ reasonCode: 'subject-mismatch', detail: `credential ${digestRef(digest)} names ${v.subjects.join(', ')}` });
|
|
535
|
+
}
|
|
536
|
+
if (rivalGenesis.length > 0 && expected.expectedGenesisOutpoint == null) {
|
|
537
|
+
problems.push({ reasonCode: 'genesis-ambiguous', detail: `${rivalGenesis.length + 1} valid genesis records carry this passport identifier` });
|
|
538
|
+
}
|
|
539
|
+
if (subjectRefs.length === 0)
|
|
540
|
+
subjectBinding = check('subjectBinding', 'unknown', 'no-evidence');
|
|
541
|
+
else if (problems.length === 0)
|
|
542
|
+
subjectBinding = check('subjectBinding', 'pass', undefined, subjectRefs);
|
|
543
|
+
else {
|
|
544
|
+
const ambiguous = problems.every((p) => p.reasonCode === 'genesis-ambiguous');
|
|
545
|
+
subjectBinding = check('subjectBinding', ambiguous ? 'unknown' : 'fail', problems[0].reasonCode, subjectRefs, { detail: problems });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Managed acceptance (`spec/managed-custody.md`): every version 2 TRANSFER
|
|
549
|
+
// that commits to an acceptance record references an artefact. A supplied
|
|
550
|
+
// record that binds to its TRANSFER makes the reference available; one that
|
|
551
|
+
// does not is a mismatch; a commitment nobody supplied a record for is an
|
|
552
|
+
// artefact the verifier could not fetch, never a pass.
|
|
553
|
+
const acceptanceParts = [];
|
|
554
|
+
const acceptanceCustodianQuestions = [];
|
|
555
|
+
const committedTransfers = (token.inspection?.states ?? []).filter((s) => s.state.version === '2' && s.state.op === 'TRANSFER' && s.state.authorisationCommitment !== '');
|
|
556
|
+
const suppliedAcceptances = (evidence.acceptanceRecords ?? []).map((value, index) => ({ index, value, inspection: inspectManagedAcceptance(value) }));
|
|
557
|
+
for (const transfer of committedTransfers) {
|
|
558
|
+
const ref = `acceptance:sha256:${transfer.state.authorisationCommitment}`;
|
|
559
|
+
const stateRef = outpointRef({ txid: transfer.txid, outputIndex: transfer.outputIndex });
|
|
560
|
+
const supplied = suppliedAcceptances.find((a) => a.inspection.commitment === transfer.state.authorisationCommitment);
|
|
561
|
+
if (supplied == null) {
|
|
562
|
+
acceptanceParts.push({ status: 'unknown', reasonCode: 'referenced-artefact-unavailable', evidenceRefs: [stateRef, ref], detail: `the acceptance record ${stateRef} commits to was not supplied` });
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const failures = [...supplied.inspection.failures];
|
|
566
|
+
if (supplied.inspection.signatureValid === true)
|
|
567
|
+
failures.push(...bindAcceptanceToState(supplied.value, transfer.state));
|
|
568
|
+
if (failures.length > 0) {
|
|
569
|
+
acceptanceParts.push({ status: 'fail', reasonCode: 'referenced-artefact-mismatch', evidenceRefs: [stateRef, ref], detail: failures.map((f) => f.detail) });
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
acceptanceParts.push({ status: 'pass', evidenceRefs: [stateRef, ref] });
|
|
573
|
+
acceptanceCustodianQuestions.push({ role: 'acceptance-custodian', identity: supplied.value.custodian, at: supplied.value.acceptance.acceptedAt, ref });
|
|
574
|
+
}
|
|
575
|
+
for (const orphan of suppliedAcceptances) {
|
|
576
|
+
if (committedTransfers.some((t) => t.state.authorisationCommitment === orphan.inspection.commitment))
|
|
577
|
+
continue;
|
|
578
|
+
acceptanceParts.push({ status: 'fail', reasonCode: 'referenced-artefact-mismatch', evidenceRefs: [`acceptance:${orphan.index}`], detail: orphan.inspection.commitment == null ? orphan.inspection.failures.map((f) => f.detail) : 'no supplied TRANSFER commits to this acceptance record' });
|
|
579
|
+
}
|
|
580
|
+
if (committedTransfers.length > 0) {
|
|
581
|
+
limits.push('An acceptance commitment binds a transfer to acceptance evidence the custodian retained and signed; it is custody-dependent evidence, not a signature made with a key the recipient controls.');
|
|
582
|
+
}
|
|
583
|
+
// Authority, role by role, on the policy's lists or verifier.
|
|
584
|
+
let issuerAuthority;
|
|
585
|
+
const authorityRefs = [...subjectRefs];
|
|
586
|
+
if (policy.authority == null) {
|
|
587
|
+
issuerAuthority = check('issuerAuthority', 'unknown', 'policy-missing');
|
|
588
|
+
}
|
|
589
|
+
else if (!policy.authority.required) {
|
|
590
|
+
issuerAuthority = check('issuerAuthority', 'not-applicable', 'authority-not-required', [], { detail: policy.authority.reason });
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
const auth = policy.authority;
|
|
594
|
+
const questions = [];
|
|
595
|
+
if (token.genesis != null) {
|
|
596
|
+
questions.push({ role: 'genesis-issuer', identity: token.genesis.state.actorIdentityKey, at: token.genesis.state.timestamp, ref: outpointRef({ txid: token.genesis.txid, outputIndex: token.genesis.outputIndex }) });
|
|
597
|
+
}
|
|
598
|
+
for (const c of native.claims)
|
|
599
|
+
questions.push({ role: 'lifecycle-claimant', identity: c.claim.issuer, at: c.claim.timestamp, ref: c.ref });
|
|
600
|
+
for (const a of anchors)
|
|
601
|
+
if (a.metadata != null)
|
|
602
|
+
questions.push({ role: 'anchoring-service', identity: a.metadata.anchoredBy, ref: a.ref });
|
|
603
|
+
questions.push(...acceptanceCustodianQuestions);
|
|
604
|
+
const parts = [];
|
|
605
|
+
for (const q of questions) {
|
|
606
|
+
const list = q.role === 'genesis-issuer' ? auth.genesisIssuers : q.role === 'lifecycle-claimant' ? auth.claimIssuers : q.role === 'anchoring-service' ? auth.anchoringServices : auth.acceptanceCustodians;
|
|
607
|
+
if (list != null) {
|
|
608
|
+
const names = q.role === 'genesis-issuer' ? [q.identity, didKeyFromIdentityKey(q.identity)] : [q.identity];
|
|
609
|
+
parts.push(names.some((n) => list.includes(n)) ? { status: 'pass', evidenceRefs: [q.ref, policyId] } : { status: 'fail', reasonCode: 'authority-unconfirmed', evidenceRefs: [q.ref], detail: `${q.role} ${q.identity} is not permitted by ${policyId}` });
|
|
610
|
+
}
|
|
611
|
+
else if (auth.verify != null) {
|
|
612
|
+
try {
|
|
613
|
+
parts.push({ ...(await auth.verify(q)), evidenceRefs: [q.ref] });
|
|
614
|
+
}
|
|
615
|
+
catch (cause) {
|
|
616
|
+
parts.push({ status: 'unknown', reasonCode: 'authority-unavailable', evidenceRefs: [q.ref], detail: cause instanceof Error ? cause.message : String(cause) });
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
parts.push({ status: 'unknown', reasonCode: 'authority-unconfirmed', evidenceRefs: [q.ref], detail: `${policyId} names no authority for ${q.role}` });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
parts.push(...credentialParts('authority'));
|
|
624
|
+
issuerAuthority = combine('issuerAuthority', parts, check('issuerAuthority', 'unknown', 'no-evidence', authorityRefs));
|
|
625
|
+
}
|
|
626
|
+
// Schema, time and status, across native claims and external credentials.
|
|
627
|
+
const schemaParts = [];
|
|
628
|
+
for (const c of native.claims) {
|
|
629
|
+
if (policy.profileValidator == null)
|
|
630
|
+
schemaParts.push({ status: 'unknown', reasonCode: 'schema-unavailable', evidenceRefs: [c.ref], detail: `${c.claim.profile}@${c.claim.profile_version}` });
|
|
631
|
+
else {
|
|
632
|
+
try {
|
|
633
|
+
schemaParts.push({ ...(await policy.profileValidator(c.claim)), evidenceRefs: [c.ref] });
|
|
634
|
+
}
|
|
635
|
+
catch (cause) {
|
|
636
|
+
schemaParts.push({ status: 'unknown', reasonCode: 'schema-unavailable', evidenceRefs: [c.ref], detail: cause instanceof Error ? cause.message : String(cause) });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
schemaParts.push(...credentialParts('schema'));
|
|
641
|
+
const schema = combine('schema', schemaParts, check('schema', 'unknown', 'no-evidence'));
|
|
642
|
+
const timeParts = native.claims.map((c) => ({ status: 'pass', evidenceRefs: [c.ref], detail: c.claim.timestamp }));
|
|
643
|
+
timeParts.push(...credentialParts('time'));
|
|
644
|
+
const credentialTime = combine('credentialTime', timeParts, check('credentialTime', 'unknown', 'no-evidence'));
|
|
645
|
+
const statusParts = native.claims.map((c) => ({ status: 'not-applicable', reasonCode: 'format-defines-no-status', evidenceRefs: [c.ref] }));
|
|
646
|
+
statusParts.push(...credentialParts('status'));
|
|
647
|
+
const credentialStatus = combine('credentialStatus', statusParts, check('credentialStatus', 'unknown', 'no-evidence'));
|
|
648
|
+
// Availability of everything the evidence pointed at.
|
|
649
|
+
const availabilityParts = unavailableRefs.map((ref) => ({ status: 'unknown', reasonCode: 'referenced-artefact-unavailable', evidenceRefs: [ref] }));
|
|
650
|
+
availabilityParts.push(...acceptanceParts);
|
|
651
|
+
for (const [digest, v] of verifications)
|
|
652
|
+
if (v.availability != null)
|
|
653
|
+
availabilityParts.push({ ...v.availability, evidenceRefs: [digestRef(digest), ...(v.availability.evidenceRefs ?? [])] });
|
|
654
|
+
const referenced = [...(token.inspection?.states.map((s) => outpointRef({ txid: s.txid, outputIndex: s.outputIndex })) ?? []), ...anchorRefs, ...nativeRefs];
|
|
655
|
+
const evidenceAvailability = referenced.length === 0 && availabilityParts.length === 0
|
|
656
|
+
? check('evidenceAvailability', 'unknown', 'no-evidence')
|
|
657
|
+
: combine('evidenceAvailability', availabilityParts.length === 0 ? [{ status: 'pass', evidenceRefs: referenced }] : availabilityParts, check('evidenceAvailability', 'pass', undefined, referenced));
|
|
658
|
+
const observations = await observe(policy, expected.passportId, token.tip, validAlternatives.map((a) => a.tip).filter((t) => t != null), checkedAt);
|
|
659
|
+
if (observations.latestState !== 'unknown') {
|
|
660
|
+
limits.push('The latest-state observation is as fresh and as complete as the sources asked; it is not a proof that no later spend exists.');
|
|
661
|
+
}
|
|
662
|
+
if (forks.length > 0) {
|
|
663
|
+
limits.push('Two valid successors of one state were supplied; which continuation is recognised is decided by accepted-chain spending evidence, never by this report or by counting sources.');
|
|
664
|
+
}
|
|
665
|
+
if ((token.inspection?.states ?? []).some((s) => s.version === '1')) {
|
|
666
|
+
limits.push('Version 1 states sign an unframed preimage: their field boundaries are established by field validation and the chain rules, not by the signature alone, and a version 1 TRANSFER proves control only where the owner-signed transfer is selected.');
|
|
667
|
+
}
|
|
668
|
+
const checks = [
|
|
669
|
+
...token.checks,
|
|
670
|
+
nativeAttestationSignature,
|
|
671
|
+
anchorSignature,
|
|
672
|
+
anchorKeyDerivation,
|
|
673
|
+
anchorDigestAndMetadataBinding,
|
|
674
|
+
externalCredentialProof,
|
|
675
|
+
subjectBinding,
|
|
676
|
+
issuerAuthority,
|
|
677
|
+
schema,
|
|
678
|
+
credentialTime,
|
|
679
|
+
credentialStatus,
|
|
680
|
+
evidenceAvailability,
|
|
681
|
+
];
|
|
682
|
+
for (const c of checks) {
|
|
683
|
+
if (c.status === 'unknown')
|
|
684
|
+
limits.push(`${c.name} was not established: ${c.reasonCode ?? 'no reason recorded'}.`);
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
reportVersion: REPORT_VERSION,
|
|
688
|
+
checkedAt,
|
|
689
|
+
expectedSubject: expected,
|
|
690
|
+
suppliedTip: token.tip,
|
|
691
|
+
policyId,
|
|
692
|
+
checks,
|
|
693
|
+
observations,
|
|
694
|
+
limits,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/** The short label a surface may show beside a check; the technical name stays in the report. */
|
|
698
|
+
export const EVIDENCE_CHECK_LABELS = {
|
|
699
|
+
recordEncoding: 'Every entry is a well-formed record',
|
|
700
|
+
actorSignatures: 'Every entry was signed by whoever wrote it',
|
|
701
|
+
publisherSignatures: 'Every entry was countersigned by the named publisher',
|
|
702
|
+
linkage: 'No step is missing from the middle',
|
|
703
|
+
inclusion: 'Every entry is in a block',
|
|
704
|
+
nativeAttestationSignature: 'Every lifecycle claim was signed by its issuer',
|
|
705
|
+
anchorSignature: 'Every anchor was signed by its anchoring service',
|
|
706
|
+
anchorKeyDerivation: 'Every anchor is locked to the key its service derives',
|
|
707
|
+
anchorDigestAndMetadataBinding: 'Every anchor commits to the exact bytes and names what they say',
|
|
708
|
+
externalCredentialProof: 'Every credential verifies under its own proof suite',
|
|
709
|
+
subjectBinding: 'Everything concerns the passport that was asked about',
|
|
710
|
+
issuerAuthority: 'Every issuer held the authority the policy requires',
|
|
711
|
+
schema: 'Every payload matches its declared profile',
|
|
712
|
+
credentialTime: 'Every credential is within its validity period',
|
|
713
|
+
credentialStatus: 'No credential is revoked or suspended',
|
|
714
|
+
evidenceAvailability: 'Everything the evidence points at was available',
|
|
715
|
+
};
|
|
716
|
+
//# sourceMappingURL=evidence.js.map
|