@zanii/custody-verdicts 0.1.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/LICENSE +159 -0
- package/README.md +204 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1189 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/index.ts +1357 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
95
|
+
Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work; and
|
|
103
|
+
|
|
104
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
105
|
+
distribution, then any Derivative Works that You distribute must
|
|
106
|
+
include a readable copy of the attribution notices contained
|
|
107
|
+
within such NOTICE file.
|
|
108
|
+
|
|
109
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
110
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
111
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
112
|
+
this License, without any additional terms or conditions.
|
|
113
|
+
|
|
114
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
115
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
116
|
+
except as required for reasonable and customary use in describing the
|
|
117
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
118
|
+
|
|
119
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed
|
|
120
|
+
to in writing, Licensor provides the Work (and each Contributor
|
|
121
|
+
provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES
|
|
122
|
+
OR CONDITIONS OF ANY KIND, either express or implied, including,
|
|
123
|
+
without limitation, any warranties or conditions of TITLE,
|
|
124
|
+
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
|
125
|
+
|
|
126
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
127
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
128
|
+
unless required by applicable law (such as deliberate and grossly
|
|
129
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
130
|
+
liable to You for damages, including any direct, indirect, special,
|
|
131
|
+
incidental, or consequential damages of any character arising as a
|
|
132
|
+
result of this License or out of the use or inability to use the
|
|
133
|
+
Work.
|
|
134
|
+
|
|
135
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
136
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
137
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
138
|
+
or other liability obligations and/or rights consistent with this
|
|
139
|
+
License. However, in accepting such obligations, You may act only
|
|
140
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
141
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
142
|
+
defend, and hold each Contributor harmless for any liability
|
|
143
|
+
incurred by, or claims asserted against, such Contributor.
|
|
144
|
+
|
|
145
|
+
END OF TERMS AND CONDITIONS
|
|
146
|
+
|
|
147
|
+
Copyright 2026 Zanii
|
|
148
|
+
|
|
149
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
150
|
+
you may not use this file except in compliance with the License.
|
|
151
|
+
You may obtain a copy of the License at
|
|
152
|
+
|
|
153
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
154
|
+
|
|
155
|
+
Unless required by applicable law or agreed to in writing, software
|
|
156
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
157
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
158
|
+
See the License for the specific language governing permissions and
|
|
159
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# @zanii/custody-verdicts
|
|
2
|
+
|
|
3
|
+
**The one verifier.** Zanii Custody's hard rule is *one verifier, two surfaces*: the
|
|
4
|
+
subject page (zanii-id) and `@zanii/cli` import this same package and call the same
|
|
5
|
+
function. Never two implementations of the verdict logic: two drift, and the drift
|
|
6
|
+
always favours the vendor.
|
|
7
|
+
|
|
8
|
+
Verdicts are **computed, never stored**. `computeVerdicts` derives eight verdict rows
|
|
9
|
+
at view time, in the reader's browser or the auditor's terminal, from the evidence
|
|
10
|
+
itself: a `SubjectHistory` from `@zanii/subject` whose receipts were already verified
|
|
11
|
+
offline (signature + delegation + scope), plus an optional evidence bag.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { computeVerdicts, computeAnchorLag, TRAINING_MANIFEST_TAG } from '@zanii/custody-verdicts';
|
|
15
|
+
|
|
16
|
+
const rows = await computeVerdicts(history); // history = await fetchSubjectHistory(tag)
|
|
17
|
+
const lag = computeAnchorLag(history.sth, new Date()); // seconds; null when unknown
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`TRAINING_MANIFEST_TAG` (`"zanii:training-manifests"`) is the well-known ledger tag
|
|
21
|
+
under which declared training manifests are published. The page and the CLI fetch that
|
|
22
|
+
slice to build the row-4 training evidence bag (`ExtraEvidence.training`).
|
|
23
|
+
|
|
24
|
+
`computeVerdicts` is async for one reason: rows 3 and 6 check claims through injected
|
|
25
|
+
verifiers (`@zanii/attest`'s quote verifier, `@zanii/residency`'s region verifier)
|
|
26
|
+
that may be async. Still pure: no network, no clock.
|
|
27
|
+
|
|
28
|
+
## The eight rows
|
|
29
|
+
|
|
30
|
+
Always in this order; each `{ id, label, state, detail }`, with `detail` the
|
|
31
|
+
plain-language working the page renders verbatim:
|
|
32
|
+
|
|
33
|
+
| id | label | `proven` when | `failed` when | otherwise |
|
|
34
|
+
|---|---|---|---|---|
|
|
35
|
+
| `purpose` | Used only for its declared purpose | history verified AND every embedded payload is a well-formed custody touch (`verifyTouch`), retention shred (`verifyKeyDestruction`), or one of the rows 6-8 evidence payloads (structurally checked) | any offline check failed or any payload malformed | empty slice → `cannot determine` |
|
|
36
|
+
| `self_sovereign` | You can check this yourself | every receipt verified offline AND the `sth` verifies | any offline check fails; bad sth | empty slice / no sth → `cannot determine` |
|
|
37
|
+
| `human_read` | Nobody read it | a SIGNED coverage attestation (sig + trusted signer when pinned + `attestationHash` binding + enclave quote via injected verifier) proves all egress paths receipt-gated for the whole window | any receipt shows a human actor; the attestation is unsigned, badly signed, from an unrecognized signer, unbound, malformed, or its quote is disproven | `not covered`: "No coverage attestation for this window"; no quote verifier injected → `cannot determine` |
|
|
38
|
+
| `trained_on` | Never used to train a model | `@zanii/train`'s `verifyAbsence` passes for this subject's tag against a declared manifest anchored BEFORE the run attestation | the proof does not verify, names another ref, or the manifest is malformed | no declared run → `not covered`: "No declared training runs to check against"; wrong anchor order / missing times → `cannot determine` |
|
|
39
|
+
| `copy_kept` | No copy was kept | every object touched in the slice has a shred receipt bound to its ref (payload `ref` = the touch's ref) that passes its own offline check AND `verifyKeyDestruction`, and any image evidence matches | a presented shred receipt fails either check; image evidence mismatches or is incomplete | none bound to a touched object's ref → `not covered`: "No key-destruction record on file."; some objects covered, some not → `cannot determine` |
|
|
40
|
+
| `residency` | Data stayed in its declared region | EVERY custody touch has a verified in-set claim (a claim with a `ref` binds every touch of that object; a ref-less claim covers the touches at its own timestamp), every region signal authenticates (injected `verifyRegion`), and `detectCrossings` finds no exit | a verified claim's region is outside the allow-set (a border crossing), or a claim is malformed, invalid, or rides on an unverified receipt | no claims → `not covered`: "No residency attestations on record" (self-declared claims are not attestations and say so); empty slice, any touch without a claim, no `jurisdiction` config, or no injected `verifyRegion` → `cannot determine` |
|
|
41
|
+
| `subprocessor` | Only declared third parties touched it | every custody-touch junction and recorded transfer resolves to a declared party under a named agreement (`verifyTransfers` against the reader-pinned list, plus a junction/transfer divergence check) | any `undeclared` receiver, any transfer without an agreement id, any malformed record, or a junction naming a destination with no matching declared transfer | none recorded → `not covered`: "No onward transfers declared or recorded"; destinations on record but no reader-provided `declaredSubprocessors` → `cannot determine` |
|
|
42
|
+
| `minimization` | Only what was needed was taken | every recorded access proof verifies (`verifyMinimization`) against its committed minimum field set, and every commitment record recomputes to its recorded commitment (`verifyFieldSet`) | any `excess` access (a field outside the declared minimum), a widened-after-commitment set, or a malformed record | no commitments → `not covered`: "No minimum-field commitments on record"; empty slice, commitments without access proofs, access proofs without commitments, or an access proof against an unknown commitment → `cannot determine` |
|
|
43
|
+
|
|
44
|
+
Four states: `proven`, `not covered`, `failed`, `cannot determine`. The last three are
|
|
45
|
+
as first-class as the first: a missing proof is not a clean result, and nothing here
|
|
46
|
+
ever renders "never" out of a database column.
|
|
47
|
+
|
|
48
|
+
On an **empty slice** (zero receipts) rows 1-2 answer `cannot determine` and rows 3-5
|
|
49
|
+
stay grey regardless of evidence: there is nothing to cover. Row 3 with a coverage
|
|
50
|
+
attestation over an empty slice answers `cannot determine` (coverage over nothing
|
|
51
|
+
proves nothing about this subject's data); row 4 answers `not covered`; row 5's
|
|
52
|
+
evidence lives in the slice, so an empty slice has none. Rows 6 and 8 answer
|
|
53
|
+
`cannot determine` on an empty slice (there is no window to cover); row 7's evidence
|
|
54
|
+
lives in the slice, so an empty slice answers `not covered`.
|
|
55
|
+
|
|
56
|
+
## Evidence shapes
|
|
57
|
+
|
|
58
|
+
`ExtraEvidence` (`{ coverage?, training?, destructionImage?, jurisdiction?,
|
|
59
|
+
declaredSubprocessors? }`) carries the evidence and reader-side trust roots that do
|
|
60
|
+
not live in the slice; everything is checked offline, here, in this one place,
|
|
61
|
+
against the real producers' shapes:
|
|
62
|
+
|
|
63
|
+
- **`training`** (row 4) is `{ manifest, anchoredAt, runAttestedAt, absenceProof }`:
|
|
64
|
+
`@zanii/train`'s `TrainingManifest` and `AbsenceProof` (sorted-Merkle over
|
|
65
|
+
covering-gap pair leaves, reusing core's RFC 6962 inclusion proofs). The manifest
|
|
66
|
+
anchor must precede the run attestation, or the ordering cannot be trusted.
|
|
67
|
+
- **`destructionImage`** (row 5, optional) is `{ attestation, publishedImageHash }`: a
|
|
68
|
+
claimed destruction-image attestation plus the image hash the operator published.
|
|
69
|
+
What is checked here is **bag-internal consistency only**, via `@zanii/attest`'s
|
|
70
|
+
`attestationHash` convention (the published image hash is `attestationHash` of the
|
|
71
|
+
published attestation record): a mismatch is `failed`, a match appends the
|
|
72
|
+
attested-image line to the proven detail. A match does NOT by itself prove the
|
|
73
|
+
destruction ran under that image: nothing yet binds the shred receipt to the
|
|
74
|
+
attestation, and the attestation's quote is not verified on this path. The full
|
|
75
|
+
binding ships with the enclave phase. The destruction record itself needs no bag
|
|
76
|
+
entry: it is the `@zanii/retention` shred receipt carried in the subject's slice
|
|
77
|
+
(signature-covered embedded payload, target `data.retention.shred`), bound to the
|
|
78
|
+
subject by slice membership and to the OBJECT by ref: a shred counts only for an
|
|
79
|
+
object whose custody touches carry the same payload `ref`, and `proven` requires a
|
|
80
|
+
verified bound shred for every object the slice touches (partial coverage answers
|
|
81
|
+
`cannot determine`; a shred bound only to unrelated refs answers `not covered`).
|
|
82
|
+
Its salted `subject_commitment` can later be proven to name the subject by
|
|
83
|
+
revealing the salt.
|
|
84
|
+
- **`coverage`** (row 3) is a signed coverage attestation
|
|
85
|
+
(`{ v: 1, type: 'coverage.attestation', allPathsGated, windowStart, windowEnd,
|
|
86
|
+
imageHash, attestation, signer, sig, trustedSigners? }`): every egress path
|
|
87
|
+
receipt-gated (the `@zanii/broker` invariant) for a window, bound to an attested
|
|
88
|
+
image hash, signed by the platform key. Verified fail-closed: signature and (when
|
|
89
|
+
pinned) trusted signer, then the `attestationHash` binding, then
|
|
90
|
+
`verifyAttestation` with the caller's injected quote verifier
|
|
91
|
+
(pass it as `computeVerdicts(history, evidence, { verifyQuote })`). Without an
|
|
92
|
+
injected verifier the quote stays unknown and the row can only reach
|
|
93
|
+
`cannot determine`, never `proven`.
|
|
94
|
+
- **`jurisdiction`** (row 6) is `string[]`: the declared jurisdiction as an allow-set
|
|
95
|
+
of region codes (`@zanii/residency`'s `allowedRegions`). Reader-side configuration,
|
|
96
|
+
never producer-supplied (same trust rule as `coverage.trustedSigners`).
|
|
97
|
+
- **`declaredSubprocessors`** (row 7) is `string[]`: the declared subprocessor list
|
|
98
|
+
that onward transfers and custody junctions are checked against. Reader-side
|
|
99
|
+
configuration, never producer-supplied.
|
|
100
|
+
|
|
101
|
+
## Row 6: "Data stayed in its declared region"
|
|
102
|
+
|
|
103
|
+
Mechanism: residency claim payloads in the slice (`@zanii/residency`'s
|
|
104
|
+
`buildResidencyClaim` shape, embedded signature-covered exactly like the row-5 shred
|
|
105
|
+
payloads) are checked with `verifyResidency` against the reader-pinned
|
|
106
|
+
`jurisdiction` allow-set and the caller's injected `verifyRegion` (pass it as
|
|
107
|
+
`computeVerdicts(history, evidence, { verifyRegion })`), which authenticates the
|
|
108
|
+
region signal (a cloud provider identity document, a TEE geo signal) and returns the
|
|
109
|
+
trusted region. `detectCrossings` then runs over the verified touch sequence: any
|
|
110
|
+
exit from the allow-set is a border crossing and a red, never silent.
|
|
111
|
+
|
|
112
|
+
`proven` requires per-touch coverage, never a window span: EVERY custody touch in the
|
|
113
|
+
slice must be covered by a verified in-set claim. A claim carrying a `ref` binds to
|
|
114
|
+
every touch of that object; a ref-less claim is time-scoped and covers the touches
|
|
115
|
+
made at its own timestamp. Every region signal must authenticate inside the
|
|
116
|
+
allow-set, no claim may be malformed or unverified, and no crossings may be
|
|
117
|
+
detected. A claim whose region source is self-declared (the operator asserted its
|
|
118
|
+
own region) is not an attestation: it is not counted as evidence, and when every
|
|
119
|
+
claim is self-declared the row answers `not covered` and says so.
|
|
120
|
+
|
|
121
|
+
The limit: the proof is only as strong as the injected geo trust root. A proven row
|
|
122
|
+
means "signals your verifier trusts placed every touch inside your declared
|
|
123
|
+
jurisdiction"; it cannot prove residency your geo source cannot. A touch with no
|
|
124
|
+
claim answers `cannot determine`, never `proven`.
|
|
125
|
+
|
|
126
|
+
## Row 7: "Only declared third parties touched it"
|
|
127
|
+
|
|
128
|
+
Mechanism: transfer records in the slice (`@zanii/subprocessor`'s `buildTransfer`
|
|
129
|
+
shape) are checked with `verifyTransfers` against the reader-pinned
|
|
130
|
+
`declaredSubprocessors` list, and every custody-touch junction
|
|
131
|
+
(`@zanii/data-custody`'s `junction` field) must name a destination with a matching
|
|
132
|
+
declared transfer: a divergence between the custody trail and the transfer record is
|
|
133
|
+
a red, never silent.
|
|
134
|
+
|
|
135
|
+
`proven` requires: every recorded transfer's receiver is declared, every transfer
|
|
136
|
+
carries an agreement id (stricter than the producer, which requires an id only for
|
|
137
|
+
dpa/scc), no record is malformed, and every junction resolves to a declared party
|
|
138
|
+
under a named agreement.
|
|
139
|
+
|
|
140
|
+
The limit: this proves the transfers that were recorded. An off-record leak (a
|
|
141
|
+
processor that received plaintext and emitted no transfer receipt) cannot be caught
|
|
142
|
+
here; that residual is the enclave problem, unchanged. And an agreement id is a
|
|
143
|
+
reference, not a guarantee the counterparty honored it. Senders are not checked:
|
|
144
|
+
`verifyTransfers` runs without a controller, so an undeclared `from` is
|
|
145
|
+
indistinguishable from the origin and goes unchecked.
|
|
146
|
+
|
|
147
|
+
## Row 8: "Only what was needed was taken"
|
|
148
|
+
|
|
149
|
+
Mechanism: field-set commitment records in the slice (`@zanii/minimization`'s
|
|
150
|
+
`commitFieldSet` output carried as a `minimization.commit` payload) are recomputed
|
|
151
|
+
with `verifyFieldSet` (a mismatch means the declared minimum was widened after the
|
|
152
|
+
commitment was recorded), and every access proof (`buildAccessProof` shape) is
|
|
153
|
+
checked with `verifyMinimization` against its committed field set: any accessed
|
|
154
|
+
field outside the declared minimum is a flagged excess, a red.
|
|
155
|
+
|
|
156
|
+
`proven` requires: every commitment record recomputes to its recorded commitment and
|
|
157
|
+
every recorded access stayed inside its committed minimum. Commitments without
|
|
158
|
+
access proofs, access proofs without commitments, or an access proof whose
|
|
159
|
+
commitment is not on record answer `cannot determine`: an unchecked access is
|
|
160
|
+
unknown, not clean.
|
|
161
|
+
|
|
162
|
+
The limit: this proves declared-vs-accessed: that the agent stayed inside the
|
|
163
|
+
minimum that was committed. It does not prove the minimum was well-chosen (a
|
|
164
|
+
judgement, not a cryptographic fact), and it proves the recorded accesses: an
|
|
165
|
+
off-record read is the enclave problem, unchanged.
|
|
166
|
+
|
|
167
|
+
## Trust roots: who may attest coverage
|
|
168
|
+
|
|
169
|
+
The same reader-side rule now guards three trust roots: `coverage.trustedSigners`
|
|
170
|
+
(row 3), `jurisdiction` (row 6), and `declaredSubprocessors` (row 7). All three are
|
|
171
|
+
reader-side configuration, merged into the evidence by the subject page or the CLI
|
|
172
|
+
from its OWN deployment config, never from producer-supplied data: an attacker who
|
|
173
|
+
could name their own signer, their own jurisdiction, or their own subprocessor list
|
|
174
|
+
would attest anything.
|
|
175
|
+
|
|
176
|
+
A coverage attestation is only as good as the key that signs it. The producer side
|
|
177
|
+
(enclave phase) signs with the platform's coverage-signing key; the reader side, the
|
|
178
|
+
subject page and the CLI, pins the platform's coverage-signing DID(s) and merges them
|
|
179
|
+
into the evidence as `coverage.trustedSigners` before calling `computeVerdicts`. Two
|
|
180
|
+
rules make this a real trust root rather than a formality:
|
|
181
|
+
|
|
182
|
+
- **The list is reader-side configuration.** It must never come from
|
|
183
|
+
producer-supplied data: an attacker who could name their own key trusted would
|
|
184
|
+
attest anything. For the same reason `trustedSigners` is NOT part of the signed
|
|
185
|
+
record; it is excluded from the signature preimage and merged in after signing.
|
|
186
|
+
- **An unknown signer is never `proven`.** When `trustedSigners` is present, a
|
|
187
|
+
coverage attestation signed by any other DID is `failed` ("coverage attestation
|
|
188
|
+
from an unrecognized signer"); an empty list trusts no one. When it is absent (day
|
|
189
|
+
one), any well-formed did:key signer authenticates, and the remaining checks
|
|
190
|
+
decide. Deployments should always pin once the enclave phase ships.
|
|
191
|
+
|
|
192
|
+
**Row 3's producer side ships with the enclave phase.** `@zanii/attest` +
|
|
193
|
+
`@zanii/broker` produce signed coverage attestations there. Until then the evidence
|
|
194
|
+
does not exist, the row answers `not covered`, and `proven` is unreachable: that is
|
|
195
|
+
the design, not a gap. A "nobody read it" claim without hardware attestation would be
|
|
196
|
+
our word, not evidence.
|
|
197
|
+
|
|
198
|
+
## Guarantees
|
|
199
|
+
|
|
200
|
+
- Pure: no network, no clock (`computeAnchorLag` takes `now` from the caller). Async
|
|
201
|
+
only for the injected verifiers (enclave quote, region signal).
|
|
202
|
+
- Never throws: malformed input yields honest `cannot determine` / `not covered` /
|
|
203
|
+
`failed` rows with reasons (verification paths return results; creation paths throw).
|
|
204
|
+
- No verdict is ever stored or served; the ledger serves raw evidence only.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { type SignedTreeHead } from '@zanii/core';
|
|
2
|
+
import { type AbsenceProof, type TrainingManifest } from '@zanii/train';
|
|
3
|
+
import { type Attestation, type QuoteVerifier } from '@zanii/attest';
|
|
4
|
+
import { type RegionVerifier } from '@zanii/residency';
|
|
5
|
+
import type { SubjectHistory } from '@zanii/subject';
|
|
6
|
+
export type { AbsenceProof, TrainingManifest } from '@zanii/train';
|
|
7
|
+
export type { Attestation, QuoteVerifier } from '@zanii/attest';
|
|
8
|
+
export type { RegionVerifier, ResidencyClaim } from '@zanii/residency';
|
|
9
|
+
export type { Agreement, Transfer } from '@zanii/subprocessor';
|
|
10
|
+
export type { AccessProof } from '@zanii/minimization';
|
|
11
|
+
/**
|
|
12
|
+
* The well-known ledger tag under which declared training manifests are published.
|
|
13
|
+
* The subject page and the CLI fetch that slice to build the row-4 training evidence
|
|
14
|
+
* bag (`ExtraEvidence.training`).
|
|
15
|
+
*/
|
|
16
|
+
export declare const TRAINING_MANIFEST_TAG = "zanii:training-manifests";
|
|
17
|
+
/** The four verdict states. The last three are as loud as the first. */
|
|
18
|
+
export type VerdictState = 'proven' | 'not covered' | 'failed' | 'cannot determine';
|
|
19
|
+
export type VerdictId = 'purpose' | 'self_sovereign' | 'human_read' | 'trained_on' | 'copy_kept' | 'residency' | 'subprocessor' | 'minimization';
|
|
20
|
+
export interface VerdictRow {
|
|
21
|
+
id: VerdictId;
|
|
22
|
+
label: string;
|
|
23
|
+
state: VerdictState;
|
|
24
|
+
/** Plain-language working: which receipts were computed over, why this state. Rendered verbatim. */
|
|
25
|
+
detail: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Coverage attestation for the "nobody read it" line: a SIGNED record asserting that
|
|
29
|
+
* every egress path to the data was receipt-gated (the `@zanii/broker` invariant) for
|
|
30
|
+
* the whole window, bound to the attested image whose code enforced it. The signature
|
|
31
|
+
* covers every field but `sig`; `imageHash` must equal `attestationHash(attestation)`
|
|
32
|
+
* (`@zanii/attest`'s convention), and the embedded attestation is verified through
|
|
33
|
+
* `verifyAttestation` with the caller's injected quote verifier. Anything unsigned or
|
|
34
|
+
* malformed is never `proven`. The producer side ships with the enclave phase
|
|
35
|
+
* (`@zanii/attest` + `@zanii/broker`); until then this evidence does not exist and the
|
|
36
|
+
* row answers `not covered`, by design.
|
|
37
|
+
*/
|
|
38
|
+
export interface CoverageEvidence {
|
|
39
|
+
v: 1;
|
|
40
|
+
type: 'coverage.attestation';
|
|
41
|
+
/** Every egress path was receipt-gated; partial coverage is no coverage. */
|
|
42
|
+
allPathsGated: boolean;
|
|
43
|
+
/** ISO timestamp: start of the gated window. */
|
|
44
|
+
windowStart: string;
|
|
45
|
+
/** ISO timestamp: end of the gated window. */
|
|
46
|
+
windowEnd: string;
|
|
47
|
+
/** `attestationHash` of the embedded attestation: the image the claim is bound to. */
|
|
48
|
+
imageHash: string;
|
|
49
|
+
/** The full attestation record (`@zanii/attest`'s `Attestation`). */
|
|
50
|
+
attestation: Attestation;
|
|
51
|
+
/** did:key of the platform key that signed this attestation. */
|
|
52
|
+
signer: string;
|
|
53
|
+
/** `ed25519:<hex>` over the canonical record minus `sig`. */
|
|
54
|
+
sig: string;
|
|
55
|
+
/**
|
|
56
|
+
* Optional trust root: the deployment's pinned coverage-signing DIDs. The reader
|
|
57
|
+
* (subject page / CLI) sets this from its OWN configuration, never from
|
|
58
|
+
* producer-supplied data; otherwise an attacker could attest with their own key and
|
|
59
|
+
* name it trusted. Not part of the signed record (excluded from the signature
|
|
60
|
+
* preimage). When present, a signer not in the list is `failed`; an empty list
|
|
61
|
+
* trusts no one. When absent, any well-formed did:key signer is accepted.
|
|
62
|
+
*/
|
|
63
|
+
trustedSigners?: string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A declared, anchored training run plus `@zanii/train`'s absence proof. Ordering is
|
|
67
|
+
* the whole claim: the manifest anchor must PRECEDE the run attestation. A manifest
|
|
68
|
+
* anchored afterwards could have been written to exclude the subject.
|
|
69
|
+
*/
|
|
70
|
+
export interface TrainingEvidence {
|
|
71
|
+
/** The run's sorted-Merkle training manifest (`@zanii/train`'s `TrainingManifest`). */
|
|
72
|
+
manifest: TrainingManifest;
|
|
73
|
+
/** ISO timestamp when the manifest root was anchored. */
|
|
74
|
+
anchoredAt: string;
|
|
75
|
+
/** ISO timestamp of the run attestation. */
|
|
76
|
+
runAttestedAt: string;
|
|
77
|
+
/** `@zanii/train`'s `AbsenceProof` for this subject's tag, verified against the manifest. */
|
|
78
|
+
absenceProof: AbsenceProof;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Optional image evidence for the "no copy was kept" line: a CLAIMED destruction-image
|
|
82
|
+
* attestation plus the image hash the operator published for it. What is checked here
|
|
83
|
+
* is bag-internal consistency only, via `@zanii/attest`'s `attestationHash` convention
|
|
84
|
+
* (the published image hash is `attestationHash` of the published attestation record):
|
|
85
|
+
* a mismatch is `failed`. A match does NOT by itself prove the destruction ran under
|
|
86
|
+
* that image: nothing yet binds the shred receipt to the attestation, and the
|
|
87
|
+
* attestation's quote is not verified on this path. Full binding ships with the
|
|
88
|
+
* enclave phase.
|
|
89
|
+
*/
|
|
90
|
+
export interface DestructionImageEvidence {
|
|
91
|
+
/** The attestation (`@zanii/attest`'s `Attestation`) claimed for the destruction image. */
|
|
92
|
+
attestation: Attestation;
|
|
93
|
+
/** The published image hash: `attestationHash` of the published attestation record. */
|
|
94
|
+
publishedImageHash: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Optional evidence bag for rows 3–5. Row 5 needs no bag entry for the destruction
|
|
98
|
+
* record itself: the shred receipts are read from the subject's slice, each already
|
|
99
|
+
* signature-covered and offline-verified. `destructionImage` only checks a claimed
|
|
100
|
+
* destruction-image attestation against the published image hash.
|
|
101
|
+
*/
|
|
102
|
+
export interface ExtraEvidence {
|
|
103
|
+
coverage?: CoverageEvidence;
|
|
104
|
+
training?: TrainingEvidence;
|
|
105
|
+
destructionImage?: DestructionImageEvidence;
|
|
106
|
+
/**
|
|
107
|
+
* Row 6 trust root: the declared jurisdiction as an allow-set of region codes
|
|
108
|
+
* (`@zanii/residency`'s `allowedRegions`, e.g. `['AE', 'me-central-1']`). READER
|
|
109
|
+
* configuration (deployment-side), never producer-supplied: the same trust rule as
|
|
110
|
+
* `coverage.trustedSigners`, or a producer could declare the whole world in bounds.
|
|
111
|
+
* When absent, residency claims can reach `cannot determine`, never `proven`. An
|
|
112
|
+
* empty array allows no region: every verified claim is a crossing.
|
|
113
|
+
*/
|
|
114
|
+
jurisdiction?: string[];
|
|
115
|
+
/**
|
|
116
|
+
* Row 7 trust root: the declared subprocessor list (the parties onward transfers
|
|
117
|
+
* may name). READER configuration, never producer-supplied, same trust rule as
|
|
118
|
+
* `coverage.trustedSigners`. When absent, transfers or junctions on record reach
|
|
119
|
+
* `cannot determine`, never `proven`. An empty array declares no one.
|
|
120
|
+
*/
|
|
121
|
+
declaredSubprocessors?: string[];
|
|
122
|
+
}
|
|
123
|
+
/** Caller-injected verifiers: the enclave quote verifier for row 3, the region
|
|
124
|
+
* signal verifier for row 6. */
|
|
125
|
+
export interface VerdictOptions {
|
|
126
|
+
/**
|
|
127
|
+
* Platform-specific enclave quote verifier (`@zanii/attest`'s `QuoteVerifier`).
|
|
128
|
+
* Without it a signed coverage attestation can reach `cannot determine`, never
|
|
129
|
+
* `proven`: the quote stays unknown, and unknown is not verified.
|
|
130
|
+
*/
|
|
131
|
+
verifyQuote?: QuoteVerifier;
|
|
132
|
+
/**
|
|
133
|
+
* Region signal verifier (`@zanii/residency`'s `RegionVerifier`): authenticates a
|
|
134
|
+
* claim's region attestation (a cloud provider identity document, a TEE geo
|
|
135
|
+
* signal) and returns the trusted region. Without it residency claims can reach
|
|
136
|
+
* `cannot determine`, never `proven`.
|
|
137
|
+
*/
|
|
138
|
+
verifyRegion?: RegionVerifier;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Compute the eight verdict rows from an offline-verified subject history. Pure: no
|
|
142
|
+
* network, no clock. Async only because row 3's coverage attestation is checked
|
|
143
|
+
* through `@zanii/attest`'s `verifyAttestation` and row 6's residency claims through
|
|
144
|
+
* `@zanii/residency`'s `verifyResidency`, whose injected verifiers may be async;
|
|
145
|
+
* pass them via `options.verifyQuote` / `options.verifyRegion`. Never throws:
|
|
146
|
+
* malformed input yields honest `cannot determine` / `not covered` / `failed` rows
|
|
147
|
+
* with reasons. Rows are always returned in this fixed order: purpose,
|
|
148
|
+
* self_sovereign, human_read, trained_on, copy_kept, residency, subprocessor,
|
|
149
|
+
* minimization.
|
|
150
|
+
*/
|
|
151
|
+
export declare function computeVerdicts(history: SubjectHistory, evidence?: ExtraEvidence, options?: VerdictOptions): Promise<VerdictRow[]>;
|
|
152
|
+
/**
|
|
153
|
+
* Seconds between the signed tree head's timestamp and `now` (the caller supplies
|
|
154
|
+
* `now`; this package has no clock). The page shows anchor lag: four minutes vs
|
|
155
|
+
* four hours is a property the reader is entitled to see. Null when unknown (no sth,
|
|
156
|
+
* no timestamp, unparseable). Negative means the sth is newer than the caller's
|
|
157
|
+
* clock: possible skew, shown as-is.
|
|
158
|
+
*/
|
|
159
|
+
export declare function computeAnchorLag(sth: Record<string, unknown> | SignedTreeHead | undefined | null, now: Date | string | number): number | null;
|
|
160
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|