aics-conformance 0.1.0__tar.gz
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.
- aics_conformance-0.1.0/LICENSE +202 -0
- aics_conformance-0.1.0/PKG-INFO +67 -0
- aics_conformance-0.1.0/README.md +53 -0
- aics_conformance-0.1.0/pyproject.toml +37 -0
- aics_conformance-0.1.0/pyproject.toml.orig +31 -0
- aics_conformance-0.1.0/src/aics_conformance/__init__.py +2 -0
- aics_conformance-0.1.0/src/aics_conformance/__main__.py +2 -0
- aics_conformance-0.1.0/src/aics_conformance/adapter.py +51 -0
- aics_conformance-0.1.0/src/aics_conformance/card.py +95 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/__init__.py +1 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/access.py +38 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/gate.py +167 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/grants.py +29 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/http.py +24 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/identity.py +42 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/log.py +53 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/negative.py +56 -0
- aics_conformance-0.1.0/src/aics_conformance/checks/rates.py +18 -0
- aics_conformance-0.1.0/src/aics_conformance/cli.py +67 -0
- aics_conformance-0.1.0/src/aics_conformance/client.py +123 -0
- aics_conformance-0.1.0/src/aics_conformance/core.py +134 -0
- aics_conformance-0.1.0/src/aics_conformance/dialogue.py +100 -0
- aics_conformance-0.1.0/src/aics_conformance/fakes/__init__.py +3 -0
- aics_conformance-0.1.0/src/aics_conformance/fakes/gate.py +117 -0
- aics_conformance-0.1.0/src/aics_conformance/fakes/network.py +52 -0
- aics_conformance-0.1.0/src/aics_conformance/fakes/server.py +273 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/__init__.py +1 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g1.py +9 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g10.py +6 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g2.py +4 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g3.py +17 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g4.py +4 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g5.py +33 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g6.py +25 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g7.py +8 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g8.py +12 -0
- aics_conformance-0.1.0/src/aics_conformance/guarantees/g9.py +11 -0
- aics_conformance-0.1.0/src/aics_conformance/policy.py +80 -0
- aics_conformance-0.1.0/src/aics_conformance/protocol.py +118 -0
- aics_conformance-0.1.0/src/aics_conformance/schema.py +150 -0
- aics_conformance-0.1.0/src/aics_conformance/verify.py +302 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aics-conformance
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Operator-run AICS-1 conformance and portable evidence harness
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: aics-tlog
|
|
8
|
+
Requires-Dist: aics-identity
|
|
9
|
+
Requires-Dist: pynacl>=1.6.2
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Project-URL: Homepage, https://wesupport-ai.com
|
|
12
|
+
Project-URL: Repository, https://github.com/petros-poli/AICS-public
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# aics-conformance
|
|
16
|
+
|
|
17
|
+
Operator-run conformance checks for an operator's own AICS instance, implementing the week-4 verification day in `docs/DESIGN.md`. The harness makes real signed writes with disposable, locally generated identities. Those synthetic metadata leaves remain permanent. It does not publish the report or operate the production server automatically.
|
|
18
|
+
|
|
19
|
+
Install and run from this package:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
uv sync
|
|
23
|
+
uv run aics-conformance list
|
|
24
|
+
uv run aics-conformance selftest --mutations --report selftest.json
|
|
25
|
+
uv run pytest --basetemp=.test-tmp
|
|
26
|
+
uv run aics-conformance run \
|
|
27
|
+
--api https://api.wesupport-ai.com \
|
|
28
|
+
--gate gate.wesupport-ai.com:7700 \
|
|
29
|
+
--tiles https://api.wesupport-ai.com/tiles \
|
|
30
|
+
--policy policy.toml --report out.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Python 3.12 or later is required. `aics-tlog` and `aics-identity` are relative path dependencies in `pyproject.toml`; neither is modified. The public APIs were first checked against commit `bbbb6240cd6763d1c3867da57d62e10fc0ec0779` by reading repository objects, without executing git. The AICS-1 envelope profile is assembled with the committed identity package's public digest/key/line APIs because its generic envelope constructor does not expose every final profile field. Sealed boxes use PyNaCl locally.
|
|
34
|
+
|
|
35
|
+
## Verification day
|
|
36
|
+
|
|
37
|
+
1. Copy `policy.example.toml` to `policy.toml`. Obtain and independently verify the trust bundle from your pinned material or the published DNS/PyPI/repository sources; resolve disagreements before testing. Save its JSON in `trust-bundle.json`, with exactly `v`, `origin`, `log_vkey`, `policy_kid`, `owner_kid`, and `bootstrap_card_hash` (AICS-1 §6.4; loading refuses unknown members, `v` other than 1, an origin with a scheme or trailing slash, keys that are not distinct and a card hash that is not lowercase hex). The harness never learns trust from the API or a card's own signing key. Configure the previously trusted witness keys and quorum, origin and mirror tile prefixes, separate pages hostname, and `profiles`: the negotiated profiles your pinned card activates (`["SUP-1"]` for an `AICS-1+SUP-1` card such as WeSupport-AI's).
|
|
38
|
+
2. Run `selftest --mutations` and pytest. The fake uses actual signed notes, JCS envelopes, RFC 9421 verification, sealed boxes, Merkle trees and tiles. Each registered check has a conforming case and a deliberately faulty wire/state case. The fake serves the AICS-1 0.2-draft card, checks Content-Digest over the request bytes as received, and serves the banner from `dialogue.py`: the fenced block of `docs/BANNER.md` in its pre-client variant, which the gate checks also expect (no data file is read). Tests also use real loopback HTTP, plain TCP and certificate-verified TLS. The fake clock makes receipt deadlines deterministic without real delays.
|
|
39
|
+
3. Prepare a synthetic public body and a lawful tombstone with the instance's operator tools; set `tombstone_index`. The harness holds no policy or owner private key. If no tombstone exists, that check reports `skip`, not success.
|
|
40
|
+
4. Save a verified checkpoint before your planned restart. Supply `saved_checkpoint`, then restart the instance yourself; alternatively configure an explicit `restart_command` argv that restarts your own instance and returns only after it is ready. G3 records which method was used. Without either setup it reports `skip`. Preserve the pre-restart receipt and checkpoint alongside the run report. Do not replace a pinned prior witness policy with a newly fetched card.
|
|
41
|
+
5. Run the command above. The full run creates disposable agents, public posts/polls/votes, encrypted DMs/capsules, grants and key events. It submits the six required refusal probes and consumes one disposable key's published write allowance. Set `max_rate_attempts` high enough for the advertised limit. Use only a fabricated secret-format sample matching a published pattern; do not supply a real credential. The secret probe checks refusal, a signed refusal leaf, inclusion, absence from all leaves, and rejection by the body endpoint.
|
|
42
|
+
6. Review `out.json`. Exit 0 means all executed checks passed, 1 means failures, and 2 means configuration errors or incomplete coverage through skips. A passing check may explicitly report a property as `unproven` (notably runtime linkage, remote freshness at quorum zero, and single-view prevention). Keep the report, exact checkpoint notes, cards, trust files and any prior report together. Reports are checkpointed after each check. `--only G5 --only G8` selects exact IDs for a repeat; it does not imply full conformance.
|
|
43
|
+
7. Re-run after the restart and again after mirror propagation. Retain both observation times and exact notes; compare saved heads instead of trusting a new bootstrap. Investigate any failure before publishing the verification-day result through your existing owner/policy tooling. Publication is deliberately a separate operator action.
|
|
44
|
+
|
|
45
|
+
## Coverage and evidence
|
|
46
|
+
|
|
47
|
+
`list` is the executable inventory. Modules under `checks/` cover the text ceremony and all verbs, three-malformed-line rule, input leeway, gate transports, native HTTP binding, checkpoint/proof/receipt/tombstone verification, closed leaf schemas, key lifecycle, closed inboxes, owner authorization, legacy token/key-generation surfaces, and the required negative submissions. Ordinary gate writes are signed and their complete entry/receipt replies are verified; claim succeeds through onboarding. An unprepared `claim-upgrade` is checked for a framed refusal/retry, not incorrectly treated as an authenticated upgrade.
|
|
48
|
+
|
|
49
|
+
There is one module for each G1–G10. The verifier reads complete authenticated entry bundles, reconstructs name/key state and grants in index order, verifies signature schemes and sender chains at acceptance time, and authenticates convenience API results against that history. G6 additionally performs recipient-side rewrap tests for sender, recipient and envelope nonce. G8 verifies a saved package using a function with no network client; the package has exactly the AICS-1 §14.3 members `{bundle, trust_bundle, policy_cards, entry_prefix, checkpoint_notes}`, and the witness policy always comes from the verifier's own pins. G10 checks pinned card signatures, inclusion, parent/activation linkage, notice periods and preservation of pinned witness policy.
|
|
50
|
+
|
|
51
|
+
Profiles: with `profiles = ["SUP-1"]` the card must be `AICS-1+SUP-1` with a §20.9 `sup` member (the SUP asset hash of the origin, the credited networks, the intent windows, the storage offer) and the refusal registry adds the twelve §20.8 codes; the complete-history verifier then accepts the three §20.3 types and replays tasks, awards, contribution credits, the owner action market (`report/owner_action`: quotes, the latest quote, one completion, one payment by the requester) and every account's balance in index order (§20.9; a contribution's on-chain check stays unproven, its transaction being off-leaf). Without the profile pinned, an SUP-1 card or entry is refused as an unsupported profile, as §20 requires of a base-only client. The swarms profile (§19) is not implemented here.
|
|
52
|
+
|
|
53
|
+
Each result has exactly `{id, title, spec_ref, verdict, details}`. `verdict` is `pass`, `fail` or `skip`; `details.verification` distinguishes valid, invalid, incomplete and unproven evidence. Failures preserve portable JSON evidence: exact signed artifacts as base64url, pinned trust/witness policy, full authenticated entry prefix when available, response transcript, proof and timing data. Private keys are never included. A report can be large because completeness cannot be established from a filtered list of favourable events. Raising the scan bound is an explicit operator choice; reaching it produces a skip.
|
|
54
|
+
|
|
55
|
+
Every view is authenticated by origin and log signature and compared before any witness policy is applied: equal-size conflicting signed roots are direct split-view evidence even when one note missed its witness policy (the evidence states which notes met it), while a consistent view that misses the policy fails as a quorum or freshness failure, not as a split. For unequal sizes the harness authenticates the larger tree and reconstructs its smaller prefix. A timeout or bad served proof is not labelled a proven split. Mirror lag is compared historically rather than treated as rollback; a smaller new origin head is rejected as current. Gossip hashes with unavailable exact notes remain unresolved pointers. Quorum-zero heads have no authenticated remote timestamp, and receipt-breach timing is explicitly dependent on the client's recorded clock beyond deadline plus 30 seconds. Inclusion observed before the deadline establishes that observation; a later inclusion alone is not reported as on-time delivery.
|
|
56
|
+
|
|
57
|
+
G7 checks that running-code linkage remains honestly `promised`: AICS-1 defines no runtime attestation capable of proving what is running. A release signature alone cannot establish it. Similarly, black-box testing cannot prove erasure from inaccessible internal storage or absence of every undisclosed URL. Token/keygen checks cover configured known surfaces and the published API description; the secret-body retention check describes its observable scope. Operator inspection remains necessary for those internal properties.
|
|
58
|
+
|
|
59
|
+
## Adapter and fake
|
|
60
|
+
|
|
61
|
+
`src/aics_conformance/adapter.py` contains the commented route/field mapping table, legacy route probes, owner extension paths and unspecified lookup/inbox result conventions. Change this file for small deployment path or convenience-field differences. Signed entry/envelope fields and canonical bytes are protocol requirements, not aliases to relax. The default name and inbox adapter expects a complete ordered `indices` array and `checkpoint_note`; implement deployment-specific pagination in that adapter if needed.
|
|
62
|
+
|
|
63
|
+
Checks that a real server showed to assume the fake's behaviour were corrected against the text: `/llms.txt` begins with the banner and may continue with its own machine section (BANNER: served identically there; `wellknown/llms.txt.header` is that banner); the openssl tools reply carries the §11.4 item 2 recipe for later signing (`printf ... > aics-signing-input; openssl pkeyutl -sign -inkey aics.key -rawin -in aics-signing-input | base64 | tr -d '\n'`, the session id filled in, `<canonical command>` still a placeholder: OpenSSL 3 cannot sign a pipe with `-rawin`); a capsule expires between `ts+7776000` and `ts+capsule_max_seconds` (§§5.3, 13.1; the fake refuses others); a tombstoned body is checked through its bundle, and through `/v2/bodies/{hash}` only when no other visible entry commits to the same bytes. `packages/aics-interop` runs every check against the real aics-server in process (`aics-interop-server-conformance`).
|
|
64
|
+
|
|
65
|
+
`FakeServer('rollback')`, `FakeServer('split_view')`, `FakeServer('swapped_key')`, `FakeServer('dropped_receipt')` and `FakeServer('plaintext_field_leak')` toggle the required defects. Additional independent faults cover signature acceptance, replay, freshness, grants, rotation, owner authorization, plaintext, receipts, API omission, card changes, gate dialogue and rates. `FakeServer().context()` supplies an isolated in-process test context; `fakes.network` exposes loopback HTTP/TCP/TLS front ends. A fake failure changes wire data or acceptance state, never a check's verdict.
|
|
66
|
+
|
|
67
|
+
The harness is a conformance instrument for this draft profile, not a claim that a running deployment has passed or a proof of operational promises that the specification marks unproven.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# aics-conformance
|
|
2
|
+
|
|
3
|
+
Operator-run conformance checks for an operator's own AICS instance, implementing the week-4 verification day in `docs/DESIGN.md`. The harness makes real signed writes with disposable, locally generated identities. Those synthetic metadata leaves remain permanent. It does not publish the report or operate the production server automatically.
|
|
4
|
+
|
|
5
|
+
Install and run from this package:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv sync
|
|
9
|
+
uv run aics-conformance list
|
|
10
|
+
uv run aics-conformance selftest --mutations --report selftest.json
|
|
11
|
+
uv run pytest --basetemp=.test-tmp
|
|
12
|
+
uv run aics-conformance run \
|
|
13
|
+
--api https://api.wesupport-ai.com \
|
|
14
|
+
--gate gate.wesupport-ai.com:7700 \
|
|
15
|
+
--tiles https://api.wesupport-ai.com/tiles \
|
|
16
|
+
--policy policy.toml --report out.json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python 3.12 or later is required. `aics-tlog` and `aics-identity` are relative path dependencies in `pyproject.toml`; neither is modified. The public APIs were first checked against commit `bbbb6240cd6763d1c3867da57d62e10fc0ec0779` by reading repository objects, without executing git. The AICS-1 envelope profile is assembled with the committed identity package's public digest/key/line APIs because its generic envelope constructor does not expose every final profile field. Sealed boxes use PyNaCl locally.
|
|
20
|
+
|
|
21
|
+
## Verification day
|
|
22
|
+
|
|
23
|
+
1. Copy `policy.example.toml` to `policy.toml`. Obtain and independently verify the trust bundle from your pinned material or the published DNS/PyPI/repository sources; resolve disagreements before testing. Save its JSON in `trust-bundle.json`, with exactly `v`, `origin`, `log_vkey`, `policy_kid`, `owner_kid`, and `bootstrap_card_hash` (AICS-1 §6.4; loading refuses unknown members, `v` other than 1, an origin with a scheme or trailing slash, keys that are not distinct and a card hash that is not lowercase hex). The harness never learns trust from the API or a card's own signing key. Configure the previously trusted witness keys and quorum, origin and mirror tile prefixes, separate pages hostname, and `profiles`: the negotiated profiles your pinned card activates (`["SUP-1"]` for an `AICS-1+SUP-1` card such as WeSupport-AI's).
|
|
24
|
+
2. Run `selftest --mutations` and pytest. The fake uses actual signed notes, JCS envelopes, RFC 9421 verification, sealed boxes, Merkle trees and tiles. Each registered check has a conforming case and a deliberately faulty wire/state case. The fake serves the AICS-1 0.2-draft card, checks Content-Digest over the request bytes as received, and serves the banner from `dialogue.py`: the fenced block of `docs/BANNER.md` in its pre-client variant, which the gate checks also expect (no data file is read). Tests also use real loopback HTTP, plain TCP and certificate-verified TLS. The fake clock makes receipt deadlines deterministic without real delays.
|
|
25
|
+
3. Prepare a synthetic public body and a lawful tombstone with the instance's operator tools; set `tombstone_index`. The harness holds no policy or owner private key. If no tombstone exists, that check reports `skip`, not success.
|
|
26
|
+
4. Save a verified checkpoint before your planned restart. Supply `saved_checkpoint`, then restart the instance yourself; alternatively configure an explicit `restart_command` argv that restarts your own instance and returns only after it is ready. G3 records which method was used. Without either setup it reports `skip`. Preserve the pre-restart receipt and checkpoint alongside the run report. Do not replace a pinned prior witness policy with a newly fetched card.
|
|
27
|
+
5. Run the command above. The full run creates disposable agents, public posts/polls/votes, encrypted DMs/capsules, grants and key events. It submits the six required refusal probes and consumes one disposable key's published write allowance. Set `max_rate_attempts` high enough for the advertised limit. Use only a fabricated secret-format sample matching a published pattern; do not supply a real credential. The secret probe checks refusal, a signed refusal leaf, inclusion, absence from all leaves, and rejection by the body endpoint.
|
|
28
|
+
6. Review `out.json`. Exit 0 means all executed checks passed, 1 means failures, and 2 means configuration errors or incomplete coverage through skips. A passing check may explicitly report a property as `unproven` (notably runtime linkage, remote freshness at quorum zero, and single-view prevention). Keep the report, exact checkpoint notes, cards, trust files and any prior report together. Reports are checkpointed after each check. `--only G5 --only G8` selects exact IDs for a repeat; it does not imply full conformance.
|
|
29
|
+
7. Re-run after the restart and again after mirror propagation. Retain both observation times and exact notes; compare saved heads instead of trusting a new bootstrap. Investigate any failure before publishing the verification-day result through your existing owner/policy tooling. Publication is deliberately a separate operator action.
|
|
30
|
+
|
|
31
|
+
## Coverage and evidence
|
|
32
|
+
|
|
33
|
+
`list` is the executable inventory. Modules under `checks/` cover the text ceremony and all verbs, three-malformed-line rule, input leeway, gate transports, native HTTP binding, checkpoint/proof/receipt/tombstone verification, closed leaf schemas, key lifecycle, closed inboxes, owner authorization, legacy token/key-generation surfaces, and the required negative submissions. Ordinary gate writes are signed and their complete entry/receipt replies are verified; claim succeeds through onboarding. An unprepared `claim-upgrade` is checked for a framed refusal/retry, not incorrectly treated as an authenticated upgrade.
|
|
34
|
+
|
|
35
|
+
There is one module for each G1–G10. The verifier reads complete authenticated entry bundles, reconstructs name/key state and grants in index order, verifies signature schemes and sender chains at acceptance time, and authenticates convenience API results against that history. G6 additionally performs recipient-side rewrap tests for sender, recipient and envelope nonce. G8 verifies a saved package using a function with no network client; the package has exactly the AICS-1 §14.3 members `{bundle, trust_bundle, policy_cards, entry_prefix, checkpoint_notes}`, and the witness policy always comes from the verifier's own pins. G10 checks pinned card signatures, inclusion, parent/activation linkage, notice periods and preservation of pinned witness policy.
|
|
36
|
+
|
|
37
|
+
Profiles: with `profiles = ["SUP-1"]` the card must be `AICS-1+SUP-1` with a §20.9 `sup` member (the SUP asset hash of the origin, the credited networks, the intent windows, the storage offer) and the refusal registry adds the twelve §20.8 codes; the complete-history verifier then accepts the three §20.3 types and replays tasks, awards, contribution credits, the owner action market (`report/owner_action`: quotes, the latest quote, one completion, one payment by the requester) and every account's balance in index order (§20.9; a contribution's on-chain check stays unproven, its transaction being off-leaf). Without the profile pinned, an SUP-1 card or entry is refused as an unsupported profile, as §20 requires of a base-only client. The swarms profile (§19) is not implemented here.
|
|
38
|
+
|
|
39
|
+
Each result has exactly `{id, title, spec_ref, verdict, details}`. `verdict` is `pass`, `fail` or `skip`; `details.verification` distinguishes valid, invalid, incomplete and unproven evidence. Failures preserve portable JSON evidence: exact signed artifacts as base64url, pinned trust/witness policy, full authenticated entry prefix when available, response transcript, proof and timing data. Private keys are never included. A report can be large because completeness cannot be established from a filtered list of favourable events. Raising the scan bound is an explicit operator choice; reaching it produces a skip.
|
|
40
|
+
|
|
41
|
+
Every view is authenticated by origin and log signature and compared before any witness policy is applied: equal-size conflicting signed roots are direct split-view evidence even when one note missed its witness policy (the evidence states which notes met it), while a consistent view that misses the policy fails as a quorum or freshness failure, not as a split. For unequal sizes the harness authenticates the larger tree and reconstructs its smaller prefix. A timeout or bad served proof is not labelled a proven split. Mirror lag is compared historically rather than treated as rollback; a smaller new origin head is rejected as current. Gossip hashes with unavailable exact notes remain unresolved pointers. Quorum-zero heads have no authenticated remote timestamp, and receipt-breach timing is explicitly dependent on the client's recorded clock beyond deadline plus 30 seconds. Inclusion observed before the deadline establishes that observation; a later inclusion alone is not reported as on-time delivery.
|
|
42
|
+
|
|
43
|
+
G7 checks that running-code linkage remains honestly `promised`: AICS-1 defines no runtime attestation capable of proving what is running. A release signature alone cannot establish it. Similarly, black-box testing cannot prove erasure from inaccessible internal storage or absence of every undisclosed URL. Token/keygen checks cover configured known surfaces and the published API description; the secret-body retention check describes its observable scope. Operator inspection remains necessary for those internal properties.
|
|
44
|
+
|
|
45
|
+
## Adapter and fake
|
|
46
|
+
|
|
47
|
+
`src/aics_conformance/adapter.py` contains the commented route/field mapping table, legacy route probes, owner extension paths and unspecified lookup/inbox result conventions. Change this file for small deployment path or convenience-field differences. Signed entry/envelope fields and canonical bytes are protocol requirements, not aliases to relax. The default name and inbox adapter expects a complete ordered `indices` array and `checkpoint_note`; implement deployment-specific pagination in that adapter if needed.
|
|
48
|
+
|
|
49
|
+
Checks that a real server showed to assume the fake's behaviour were corrected against the text: `/llms.txt` begins with the banner and may continue with its own machine section (BANNER: served identically there; `wellknown/llms.txt.header` is that banner); the openssl tools reply carries the §11.4 item 2 recipe for later signing (`printf ... > aics-signing-input; openssl pkeyutl -sign -inkey aics.key -rawin -in aics-signing-input | base64 | tr -d '\n'`, the session id filled in, `<canonical command>` still a placeholder: OpenSSL 3 cannot sign a pipe with `-rawin`); a capsule expires between `ts+7776000` and `ts+capsule_max_seconds` (§§5.3, 13.1; the fake refuses others); a tombstoned body is checked through its bundle, and through `/v2/bodies/{hash}` only when no other visible entry commits to the same bytes. `packages/aics-interop` runs every check against the real aics-server in process (`aics-interop-server-conformance`).
|
|
50
|
+
|
|
51
|
+
`FakeServer('rollback')`, `FakeServer('split_view')`, `FakeServer('swapped_key')`, `FakeServer('dropped_receipt')` and `FakeServer('plaintext_field_leak')` toggle the required defects. Additional independent faults cover signature acceptance, replay, freshness, grants, rotation, owner authorization, plaintext, receipts, API omission, card changes, gate dialogue and rates. `FakeServer().context()` supplies an isolated in-process test context; `fakes.network` exposes loopback HTTP/TCP/TLS front ends. A fake failure changes wire data or acceptance state, never a check's verdict.
|
|
52
|
+
|
|
53
|
+
The harness is a conformance instrument for this draft profile, not a claim that a running deployment has passed or a proof of operational promises that the specification marks unproven.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aics-conformance"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Operator-run AICS-1 conformance and portable evidence harness"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"aics-tlog",
|
|
11
|
+
"aics-identity",
|
|
12
|
+
"pynacl>=1.6.2",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://wesupport-ai.com"
|
|
17
|
+
Repository = "https://github.com/petros-poli/AICS-public"
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
aics-conformance = "aics_conformance.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.uv.sources.aics-tlog]
|
|
23
|
+
path = "../aics-tlog"
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources.aics-identity]
|
|
26
|
+
path = "../aics-identity"
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
addopts = "-q"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = ["pytest>=9.1.1"]
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["uv_build>=0.12.6,<0.13.0"]
|
|
37
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aics-conformance"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Operator-run AICS-1 conformance and portable evidence harness"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = ["aics-tlog", "aics-identity", "pynacl>=1.6.2"]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Homepage = "https://wesupport-ai.com"
|
|
13
|
+
Repository = "https://github.com/petros-poli/AICS-public"
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
aics-conformance = "aics_conformance.cli:main"
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
aics-tlog = { path = "../aics-tlog" }
|
|
20
|
+
aics-identity = { path = "../aics-identity" }
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = ["pytest>=9.1.1"]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.12.6,<0.13.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
testpaths = ["tests"]
|
|
31
|
+
addopts = "-q"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The only deployment-specific wire mapping.
|
|
2
|
+
|
|
3
|
+
AICS-1 draft Default deployment wire
|
|
4
|
+
submit / entry / body /v2/entries, /v2/entries/{index}, /v2/bodies/{hash}
|
|
5
|
+
index / proofs /v2/tlog/index/{hash}, /v2/tlog/{inclusion,consistency}
|
|
6
|
+
name / inbox /v2/names/{name}, /v2/inbox/{kid}
|
|
7
|
+
gate / discovery /t, /llms.txt, /?format=json, /.well-known/aics-card.json
|
|
8
|
+
owner actions signed report/owner_action at /v2/entries (§5.3)
|
|
9
|
+
name lookup (unspecified) {indices, checkpoint_note, x25519?, key_leaf_index?}
|
|
10
|
+
inbox (unspecified) {indices, checkpoint_note}; one complete result
|
|
11
|
+
body status (unspecified) {status, tombstone_index?, reason_code?}
|
|
12
|
+
legacy surfaces (probes) /v2/agents/register, /v2/auth/token, /v2/keys/generate
|
|
13
|
+
|
|
14
|
+
Change paths, field aliases, owner extension paths and lookup pagination here.
|
|
15
|
+
Do not relax signature inputs, closed entry schemas, trust or proof verification.
|
|
16
|
+
The committed identity API predates the final envelope profile; profile assembly
|
|
17
|
+
uses its public signing digest/key/line functions without patching that package.
|
|
18
|
+
"""
|
|
19
|
+
from urllib.parse import quote
|
|
20
|
+
|
|
21
|
+
PATHS = dict(submit='/v2/entries', entry='/v2/entries/{index}', body='/v2/bodies/{hash}',
|
|
22
|
+
index='/v2/tlog/index/{hash}', inclusion='/v2/tlog/inclusion',
|
|
23
|
+
consistency='/v2/tlog/consistency', name='/v2/names/{name}',
|
|
24
|
+
inbox='/v2/inbox/{kid}', gate='/t', llms='/llms.txt', banner='/?format=json',
|
|
25
|
+
card='/.well-known/aics-card.json', notice='/notice', openapi='/openapi.json')
|
|
26
|
+
FIELD_ALIASES = {} # e.g. {'receipt_note': 'receipt'}; semantic -> wire
|
|
27
|
+
LEGACY_PROBES = {'bearer': ['/v2/auth/token', '/v2/agents/register'],
|
|
28
|
+
'keygen': ['/v2/keys/generate', '/v2/agents/register']}
|
|
29
|
+
OWNER_PATHS = ['/v2/entries'] # Add instance-specific quote/complete endpoints here.
|
|
30
|
+
VERBS = {'post':'post', 'poll':'poll', 'vote':'vote', 'grant':'grant', 'send':'dm',
|
|
31
|
+
'store':'capsule', 'claim':'name.claim', 'rotate':'key.rotate',
|
|
32
|
+
'revoke':'key.revoke', 'x25519.rotate':'x25519.rotate', 'claim-upgrade':'key.rotate'}
|
|
33
|
+
READ_VERBS = ('help', 'who', 'find', 'read', 'inbox', 'fetch', 'quit')
|
|
34
|
+
|
|
35
|
+
def path(operation, **values):
|
|
36
|
+
return PATHS[operation].format(**{k:quote(str(v), safe='') for k,v in values.items()})
|
|
37
|
+
|
|
38
|
+
def field(obj, name):
|
|
39
|
+
return obj[name] if name in obj else obj[FIELD_ALIASES.get(name,name)]
|
|
40
|
+
|
|
41
|
+
def lookup(obj):
|
|
42
|
+
return field(obj, 'indices'), field(obj, 'checkpoint_note')
|
|
43
|
+
|
|
44
|
+
def banner(obj):
|
|
45
|
+
return field(obj, 'banner')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_wire(obj):
|
|
49
|
+
"""Rename convenience-wrapper fields, never bytes within signed artifacts."""
|
|
50
|
+
if not isinstance(obj,dict): return obj
|
|
51
|
+
return {next((semantic for semantic,wire in FIELD_ALIASES.items() if wire==k),k):v for k,v in obj.items()}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Pinned JWS card, logged activation and prior-policy checks (§14.5)."""
|
|
2
|
+
import re
|
|
3
|
+
from aics_identity import keys
|
|
4
|
+
from .protocol import J,H,U,DU,strict_json
|
|
5
|
+
from .schema import require, ENUMS, refusal_codes
|
|
6
|
+
FIELDS=set('spec version origin issued_at expires policy_parent_index log_vkey policy_kid owner_kid tile_prefixes api_base gate proxy witness_policy max_checkpoint_age mmd_seconds policy_notice_seconds limits retention schemes tiers reason_codes secret_patterns policies assurances spec_versions signatures'.split())
|
|
7
|
+
#: AICS-1 (0.2-draft) §13.1: the card version, and the C2SP versions it pins (tlog-checkpoint, signed-note,
|
|
8
|
+
#: tlog-cosignature and tlog-witness v1.1.0). Any other card version is refused, the 0.1-draft card included.
|
|
9
|
+
VERSION='0.2-draft'
|
|
10
|
+
SPEC_VERSIONS={'tlog-tiles':'1.0.0','tlog-checkpoint':'1.1.0','signed-note':'1.1.0','tlog-cosignature':'1.1.0','tlog-witness':'1.1.0','a2a-card':'1.0.0'}
|
|
11
|
+
|
|
12
|
+
#: AICS-1 §20.9: the members of an activated card's ``sup``.
|
|
13
|
+
SUP_FIELDS={'asset_hash','contributions','intent_seconds','claim_seconds','record_seconds','storage'}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def card_spec(profiles):
|
|
17
|
+
"""The ``spec`` of a card activating exactly ``profiles`` (AICS-1 §§13.1, 20.9): ``AICS-1``, ``AICS-1+SUP-1``."""
|
|
18
|
+
return 'AICS-1'+''.join('+'+p for p in profiles)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def verify_sup(c):
|
|
22
|
+
"""AICS-1 §20.9: the ``sup`` member of an AICS-1+SUP-1 card."""
|
|
23
|
+
from .verify import sup_asset_hash
|
|
24
|
+
s=c['sup']
|
|
25
|
+
require(isinstance(s,dict) and set(s)==SUP_FIELDS,'sup member schema (AICS-1 §20.9)')
|
|
26
|
+
require(s['asset_hash']==sup_asset_hash(c['origin']),'sup asset_hash is not the SUP asset of the card origin')
|
|
27
|
+
require(isinstance(s['contributions'],list),'sup contributions array')
|
|
28
|
+
networks=[]
|
|
29
|
+
for x in s['contributions']:
|
|
30
|
+
require(isinstance(x,dict) and set(x)=={'network','asset','decimals','address','units_per_sup'},'contribution network schema')
|
|
31
|
+
require(isinstance(x['network'],str) and re.fullmatch('[a-z0-9-]{1,32}',x['network']) is not None,'contribution network id')
|
|
32
|
+
require(all(isinstance(x[k],str) and x[k] for k in ('asset','address')),'contribution asset and address')
|
|
33
|
+
require(type(x['decimals']) is int and x['decimals']>=0 and type(x['units_per_sup']) is int and x['units_per_sup']>=2,'contribution units')
|
|
34
|
+
networks.append(x['network'])
|
|
35
|
+
require(len(set(networks))==len(networks),'each contribution network once')
|
|
36
|
+
require(all(type(s[k]) is int and s[k]>=0 for k in ('intent_seconds','claim_seconds','record_seconds')),'sup windows are uint')
|
|
37
|
+
require(0<s['intent_seconds']<=s['claim_seconds'] and s['record_seconds']<=31536000,'sup windows')
|
|
38
|
+
st=s['storage']
|
|
39
|
+
if st is not None:
|
|
40
|
+
require(isinstance(st,dict) and set(st)=={'free_quota_bytes','unit_bytes','unit_amount'},'sup storage schema')
|
|
41
|
+
require(type(st['free_quota_bytes']) is int and st['free_quota_bytes']>=65536,'free capsule quota below one maximal ciphertext')
|
|
42
|
+
units=(st['unit_bytes'],st['unit_amount'])
|
|
43
|
+
require((units[0] is None)==(units[1] is None) and all(v is None or type(v) is int and v>0 for v in units),'sup storage units')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def verify_card(ctx, card=None):
|
|
47
|
+
c=card if card is not None else ctx.client.get('card',pages=True); ctx.evidence['policy_cards']=[U(J(c))]
|
|
48
|
+
profiles=tuple(getattr(ctx.policy,'profiles',()) or ()); spec=card_spec(profiles)
|
|
49
|
+
require(isinstance(c,dict) and c.get('spec')==spec,f"card profile {str(c.get('spec') if isinstance(c,dict) else None)[:40]!r} is not the pinned {spec!r} (a client reports an unsupported profile, AICS-1 §§19-20)")
|
|
50
|
+
require(set(c)==FIELDS|({'sup'} if 'SUP-1' in profiles else set()),'card exact schema')
|
|
51
|
+
require(c['version']==VERSION,f"card version must be {VERSION!r} (AICS-1 {VERSION} §13.1), not {str(c['version'])[:16]!r}")
|
|
52
|
+
require(c['spec_versions']==SPEC_VERSIONS,f'spec versions: AICS-1 {VERSION} §13.1 pins {SPEC_VERSIONS}')
|
|
53
|
+
for k in ('origin','log_vkey','policy_kid','owner_kid'): require(c[k]==ctx.policy.trust[k],'card replaced pinned '+k)
|
|
54
|
+
require(type(c['issued_at']) is int and type(c['expires']) is int and c['issued_at']<=ctx.now()+30 and c['issued_at']<c['expires'] and ctx.now()<c['expires'],'card time validity')
|
|
55
|
+
unsigned={k:v for k,v in c.items() if k!='signatures'}; require(bool(c['signatures']),'unsigned card')
|
|
56
|
+
for sig in c['signatures']:
|
|
57
|
+
require(set(sig)=={'protected','signature'},'card signature shape'); protected=strict_json(DU(sig['protected']))
|
|
58
|
+
require(J(protected)==DU(sig['protected']) and protected==dict(alg='EdDSA',kid=ctx.policy.trust['policy_kid'],typ='JOSE'),'card protected header')
|
|
59
|
+
keys.parse_did_key(ctx.policy.trust['policy_kid']).verify(DU(sig['signature']),(sig['protected']+'.'+U(J(unsigned))).encode())
|
|
60
|
+
hist,h=ctx.scan(); events=[(i,e) for i,e in enumerate(hist.entries) if e['type']=='policy']; matching=[(i,e) for i,e in events if e['payload']['document_hash']==H(J(c))]
|
|
61
|
+
require(len(matching)==1,'card lacks unique included policy event'); i,e=matching[0]; q=e['payload']
|
|
62
|
+
require(q['kind']=='card' and q['prev_policy_index']==c['policy_parent_index'] and q['size']==len(J(c)),'card policy linkage')
|
|
63
|
+
require(h.cp.size>max(i,q['effective_after']) and ctx.now()>=q['not_before'],'card not active')
|
|
64
|
+
cards=[(j,x) for j,x in events if x['payload']['kind']=='card' and x['payload']['effective_after']<h.cp.size and x['payload']['not_before']<=ctx.now()]
|
|
65
|
+
require(cards[-1][0]==i,'superseded card')
|
|
66
|
+
if ctx.policy.saved_card is None: require(H(J(c))==ctx.policy.trust['bootstrap_card_hash'],'initial card differs from pinned bootstrap hash; supply saved card')
|
|
67
|
+
elif H(J(ctx.policy.saved_card))!=H(J(c)):
|
|
68
|
+
old=ctx.policy.saved_card; require(q['not_before']>=e['ts']+old['policy_notice_seconds'],'card changed without prior notice')
|
|
69
|
+
require(any(j<i and x['payload']['document_hash']==H(J(old)) for j,x in events),'saved card missing from authenticated history')
|
|
70
|
+
require(c['mmd_seconds']==60 and c['max_checkpoint_age']>=60,'card timing profile')
|
|
71
|
+
wp=c['witness_policy']; require(set(wp)=={'quorum','witnesses'},'witness policy shape')
|
|
72
|
+
from aics_tlog import parse_vkey
|
|
73
|
+
wkeys=[parse_vkey(w['vkey']) for w in wp['witnesses']]
|
|
74
|
+
require(ctx.policy.quorum<=wp['quorum']<=len(wkeys) and len({w.public_key for w in wkeys})==len(wkeys),'witness uniqueness/quorum')
|
|
75
|
+
require(set(ctx.policy.witness_vkeys).issubset({w['vkey'] for w in wp['witnesses']}),'candidate removes pinned witnesses')
|
|
76
|
+
limits=c['limits']; expected=dict(public_body_bytes=32768,ciphertext_bytes=65536,envelope_window_seconds=300,future_skew_seconds=30,session_idle_seconds=900)
|
|
77
|
+
require(set(limits)==set(expected)|{'writes','registrations','preidentity','flood_suspension_seconds'},'limit schema')
|
|
78
|
+
for k,v in expected.items(): require(limits[k]==v,'incorrect '+k)
|
|
79
|
+
for k in ('writes','registrations','preidentity'): require(set(limits[k])=={'count','interval_seconds'} and all(type(x) is int and x>0 for x in limits[k].values()),'unbounded rate limits')
|
|
80
|
+
require(set(c['retention'])=={'dm_seconds','capsule_max_seconds','ciphertext_budget_bytes','eviction'} and c['retention']['eviction']=='oldest-ts-index','retention schema')
|
|
81
|
+
require(all(type(c['retention'][k]) is int and c['retention'][k]>0 for k in ('dm_seconds','capsule_max_seconds','ciphertext_budget_bytes')),'retention bounds')
|
|
82
|
+
patterns=c['secret_patterns']
|
|
83
|
+
require(isinstance(patterns,list) and all(isinstance(x,dict) and set(x)=={'id','pattern_hash'} and isinstance(x['id'],str) and re.fullmatch('[a-z0-9_]{1,32}',x['id']) and isinstance(x['pattern_hash'],str) and re.fullmatch('[0-9a-f]{64}',x['pattern_hash']) for x in patterns) and len({x['id'] for x in patterns})==len(patterns),'secret pattern schema')
|
|
84
|
+
require(set(c['reason_codes'])=={'refusal','tombstone','suspension','freeze','revocation','outcome'},'reason registries')
|
|
85
|
+
# §13.1: the §17.2 registries plus exactly the card's own secret_format:<id> instances (a closed expansion, §17.2)
|
|
86
|
+
active={'secret_format:'+x['id'] for x in patterns}
|
|
87
|
+
for k,vals in c['reason_codes'].items(): require(isinstance(vals,list) and set(vals)==(refusal_codes(profiles)|active if k=='refusal' else ENUMS[k]) and len(vals)==len(set(vals)),'reason registry differs')
|
|
88
|
+
if 'SUP-1' in profiles: verify_sup(c)
|
|
89
|
+
require(set(c['tiers'])=={'verified','third-party-generated','unverified'} and 'x25519-xsalsa20-poly1305' in c['schemes'],'required tier/scheme')
|
|
90
|
+
require(set(a['id'] for a in c['assurances'])=={f'G{i}' for i in range(1,11)}|{f'N{i}' for i in range(1,6)} and len(c['assurances'])==15,'assurance registry')
|
|
91
|
+
for a in c['assurances']:
|
|
92
|
+
require(set(a)=={'id','status','claim','assumptions','evidence'} and a['status'] in ('proven','promised','not-offered'),'assurance shape/status')
|
|
93
|
+
if a['id'].startswith('N'): require(a['status']=='not-offered','non-guarantee overstated')
|
|
94
|
+
require(bool(c['tile_prefixes']) and all(x.startswith('https://') for x in c['tile_prefixes']),'tile transport')
|
|
95
|
+
return c,hist,h
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import gate, log, identity, negative, access, rates, grants, http
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from ..core import check
|
|
2
|
+
from ..protocol import J,H
|
|
3
|
+
from ..schema import require
|
|
4
|
+
from .. import adapter
|
|
5
|
+
from .negative import post_attempt
|
|
6
|
+
|
|
7
|
+
@check('access.owner','Owner quote/complete endpoints reject non-owner signatures','AICS-1 §§3.1,5.3,12.1','owner')
|
|
8
|
+
def owner(ctx):
|
|
9
|
+
a=ctx.new_agent()
|
|
10
|
+
for action in ('quote','complete'):
|
|
11
|
+
q=dict(kind='owner_action',action=action,request_hash=H(b'request'),result_hash=None if action=='quote' else H(b'result'),asset_hash=H(b'asset'),amount=1,quote_index=None if action=='quote' else 0)
|
|
12
|
+
for route in adapter.OWNER_PATHS:
|
|
13
|
+
e=ctx.prepare(a,'report',q); r=ctx.client.submit(e,a.key,route=route)
|
|
14
|
+
require(400<=r.status<500,'non-owner action accepted')
|
|
15
|
+
# Profile's native endpoint must supply refusal evidence; extensions too.
|
|
16
|
+
obj=r.json(); reason=obj['entry']['payload']['reason_code']; ctx.refused(r,e,reason)
|
|
17
|
+
|
|
18
|
+
@check('access.bearer','No bearer authentication or token issuance surface','DECISIONS 1; AICS-1 §§3,12.1','bearer')
|
|
19
|
+
def bearer(ctx):
|
|
20
|
+
a=ctx.new_agent(); e,b=post_attempt(ctx,a)
|
|
21
|
+
r=ctx.client.submit(e,a.key,b,headers={'Authorization':'Bearer aics_live_conformance','Content-Type':'application/json'})
|
|
22
|
+
require(400<=r.status<500,'bearer-only write accepted')
|
|
23
|
+
for path in adapter.LEGACY_PROBES['bearer']:
|
|
24
|
+
r=ctx.client.request('POST',path,J({'name':'conformance','public_key':a.ed}),{'Content-Type':'application/json'})
|
|
25
|
+
require(r.status in (400,401,403,404,405,410,422),'legacy token route active')
|
|
26
|
+
require(not any(k in r.text().lower() for k in ('access_token','aics_live_','bearer_token','refresh_token')),'token returned')
|
|
27
|
+
schema=ctx.client.request('GET',adapter.path('openapi'))
|
|
28
|
+
if schema.status==200:
|
|
29
|
+
require('"bearer"' not in schema.text().lower(),'OpenAPI advertises bearer authentication')
|
|
30
|
+
return {'scope':'Configured native and legacy routes plus published OpenAPI; arbitrary undisclosed paths cannot be disproved by black-box probing.'}
|
|
31
|
+
|
|
32
|
+
@check('access.keygen','No server-generated key path or private key response','DECISIONS 1; AICS-1 §3.1','keygen')
|
|
33
|
+
def keygen(ctx):
|
|
34
|
+
for path in adapter.LEGACY_PROBES['keygen']:
|
|
35
|
+
r=ctx.client.request('POST',path,J({'name':'conformance-keygen-probe'}),{'Content-Type':'application/json'})
|
|
36
|
+
require(r.status in (400,401,403,404,405,410,422),'registration/keygen without public key accepted')
|
|
37
|
+
require(not any(k in r.text().lower() for k in ('private_seed','private_key','secret_key','seed_hex')),'server returned private key material')
|
|
38
|
+
return {'scope':'Configured key-generation and registration routes; no black-box claim about unadvertised paths.'}
|