legion-foundation 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.
Files changed (44) hide show
  1. legion_foundation-0.1.0/LICENSE +201 -0
  2. legion_foundation-0.1.0/NOTICE +23 -0
  3. legion_foundation-0.1.0/PKG-INFO +399 -0
  4. legion_foundation-0.1.0/README.md +374 -0
  5. legion_foundation-0.1.0/hatch_build.py +416 -0
  6. legion_foundation-0.1.0/pyproject.toml +77 -0
  7. legion_foundation-0.1.0/src/legion_foundation/__init__.py +88 -0
  8. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/input/arrays.json +8 -0
  9. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/input/french.json +6 -0
  10. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/input/structures.json +8 -0
  11. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/input/unicode.json +3 -0
  12. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/input/values.json +5 -0
  13. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/input/weird.json +11 -0
  14. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/output/arrays.json +1 -0
  15. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/output/french.json +1 -0
  16. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/output/structures.json +1 -0
  17. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/output/unicode.json +1 -0
  18. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/output/values.json +1 -0
  19. legion_foundation-0.1.0/src/legion_foundation/_corpus/jcs-vectors/output/weird.json +1 -0
  20. legion_foundation-0.1.0/src/legion_foundation/_corpus/receipt/chain.vectors.json +1119 -0
  21. legion_foundation-0.1.0/src/legion_foundation/_corpus/syscall/authorize.vectors.json +3912 -0
  22. legion_foundation-0.1.0/src/legion_foundation/_corpus/syscall/constants.json +7 -0
  23. legion_foundation-0.1.0/src/legion_foundation/_corpus/syscall/mint.vectors.json +328 -0
  24. legion_foundation-0.1.0/src/legion_foundation/_corpus/trust/weight.vectors.json +1460 -0
  25. legion_foundation-0.1.0/src/legion_foundation/_corpus/verdict/canonicalization.vectors.json +131910 -0
  26. legion_foundation-0.1.0/src/legion_foundation/_corpus/verdict/contentkey.vectors.json +93 -0
  27. legion_foundation-0.1.0/src/legion_foundation/_corpus/verdict/ed25519-smallorder-blocklist.json +21 -0
  28. legion_foundation-0.1.0/src/legion_foundation/_corpus/verdict/goodstanding.vectors.json +971 -0
  29. legion_foundation-0.1.0/src/legion_foundation/_corpus/verdict/negative.vectors.json +228 -0
  30. legion_foundation-0.1.0/src/legion_foundation/_corpus/verdict/signing.vectors.json +360 -0
  31. legion_foundation-0.1.0/src/legion_foundation/conformance.py +2168 -0
  32. legion_foundation-0.1.0/src/legion_foundation/contentkey.py +129 -0
  33. legion_foundation-0.1.0/src/legion_foundation/corpus.sha256 +23 -0
  34. legion_foundation-0.1.0/src/legion_foundation/crypto.py +168 -0
  35. legion_foundation-0.1.0/src/legion_foundation/didkey.py +89 -0
  36. legion_foundation-0.1.0/src/legion_foundation/ed25519strict.py +130 -0
  37. legion_foundation-0.1.0/src/legion_foundation/jcs.py +123 -0
  38. legion_foundation-0.1.0/src/legion_foundation/py.typed +0 -0
  39. legion_foundation-0.1.0/src/legion_foundation/receipt.py +284 -0
  40. legion_foundation-0.1.0/src/legion_foundation/syscall.py +1095 -0
  41. legion_foundation-0.1.0/src/legion_foundation/trust.py +380 -0
  42. legion_foundation-0.1.0/src/legion_foundation/verdict.py +431 -0
  43. legion_foundation-0.1.0/tests/conformance.py +120 -0
  44. legion_foundation-0.1.0/tests/tamper_bundled_digest.py +233 -0
@@ -0,0 +1,201 @@
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,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Copia Wealth Studios Inc. (LEGION-OS) and Michael J. Sikorsky
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,23 @@
1
+ legion-foundation
2
+ Copyright 2026 Copia Wealth Studios Inc. (LEGION-OS) and Michael J. Sikorsky
3
+
4
+ This product is licensed under the Apache License, Version 2.0 (see LICENSE).
5
+
6
+ The Apache-2.0 patent grant (§3) is intentional: adopters of the verdict spine
7
+ receive a patent license to the mechanism, so it is safe to build on. The family
8
+ monetizes the data and product layers, not by holding the primitive's patents
9
+ over the people who adopt it — "open mechanism, private data."
10
+
11
+ Third-party content
12
+ --------------------
13
+ This distribution bundles a conformance corpus at
14
+ `legion_foundation/_corpus/`. Twelve of its files, under
15
+ `legion_foundation/_corpus/jcs-vectors/`, are the official RFC 8785 (JSON
16
+ Canonicalization Scheme) test vectors from the `cyberphone/json-canonicalization`
17
+ project (Copyright 2018 Anders Rundgren, the RFC's author), licensed under the
18
+ Apache License, Version 2.0. They are redistributed here unmodified, as an
19
+ independent third-party conformance oracle this package's JCS implementation is
20
+ checked against.
21
+
22
+ All other files under `legion_foundation/_corpus/` (the `verdict/`, `syscall/`,
23
+ `trust/`, and `receipt/` vector files) are original to this project.
@@ -0,0 +1,399 @@
1
+ Metadata-Version: 2.5
2
+ Name: legion-foundation
3
+ Version: 0.1.0
4
+ Summary: The verdict spine in Python: RFC 8785 canonicalization, EdDSA/ES256/HS256 signing with kid-binds-alg, two-gate verify + certify_close, and UCAN-style capabilities. Gated by a language-neutral conformance corpus.
5
+ Author: Copia Wealth Studios Inc. (LEGION-OS), Michael J. Sikorsky
6
+ Maintainer-email: LEGION-OS Security <security@copiawealthstudios.com>
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Keywords: canonicalization,capabilities,did-key,ed25519,es256,jcs,rfc8785,ucan
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: cryptography>=42
23
+ Requires-Dist: rfc8785>=0.1.4
24
+ Description-Content-Type: text/markdown
25
+
26
+ # legion-foundation
27
+
28
+ The **verdict spine** in Python — an *independent* implementation of a frozen,
29
+ cross-language signing contract, with **no dependency on the TypeScript
30
+ reference binding**. It exists to prove the underlying wire format is
31
+ genuinely polyglot: this binding reproduces, byte-for-byte, the same pinned
32
+ canonical bytes and verifies the same pinned signatures that nine other
33
+ independent language bindings do, against one shared conformance corpus. The
34
+ corpus is the contract; if a binding can't reproduce a vector, it fails — the
35
+ tape is never edited to make a binding pass.
36
+
37
+ **Provenance boundary.** In-source `ADR-NNNN` / `PB-NNNN` markers scattered
38
+ through this codebase's comments refer to entries in the parent project's
39
+ private architectural decision record — they are not resolvable outside that
40
+ repository. The same applies to citations embedded inside the bundled
41
+ conformance corpus itself: several vector files carry a `spec` or `note` field
42
+ that names an internal document (`SIGNING-syscall.md`, `ADR-0012`, `ADR-0027`,
43
+ `ADR-0035`, ...) or contains internal review shorthand such as the literal
44
+ string `Generated — never hand-edit (drift-gated by pnpm corpus:drift)`. None
45
+ of these are resolvable outside the private repository either. The normative,
46
+ versioned contract for the wire formats this package implements is a document
47
+ called `SIGNING.md`, which currently also lives in that private repository and
48
+ is **not** distributed with this package. So: this README and the bundled
49
+ conformance corpus are, today, the complete publicly available description of
50
+ the wire format. Where a comment or a corpus `spec`/`note` field cites
51
+ `SIGNING.md §N` or another private document, treat the corpus vectors
52
+ themselves as the operative specification — they are the thing every binding
53
+ is actually gated against, independent of whether the cited document is
54
+ reachable.
55
+
56
+ Every signed message in this family follows the same construction:
57
+
58
+ ```
59
+ signed_bytes = utf8("<DOMAIN>\n") || JCS(projection)
60
+ ```
61
+
62
+ RFC 8785 JCS (JSON Canonicalization Scheme) over a null-stripped projection of
63
+ the message, prefixed with an off-wire domain-separation tag that never
64
+ appears in the message itself. This package implements several subsystems built
65
+ on that one mechanism, each with its own distinct DOMAIN tag — so a signature
66
+ for one can never validate as another:
67
+
68
+ - **verdict** — DOMAIN `legion.os/v/1`: `utf8("legion.os/v/1\n") || JCS(projection)`
69
+ - **syscall** — DOMAIN `legion.os/syscall/v/1`: `utf8("legion.os/syscall/v/1\n") || JCS(projection)`
70
+ - **trust** — DOMAIN `legion.os/trust/v/1`: `utf8("legion.os/trust/v/1\n") || JCS(projection)`
71
+
72
+ **The exported `DOMAIN` / `DOMAIN_SYSCALL` / `DOMAIN_TRUST` constants already
73
+ carry the trailing newline** — measured: `DOMAIN == "legion.os/v/1\n"`,
74
+ `DOMAIN_SYSCALL == "legion.os/syscall/v/1\n"`, `DOMAIN_TRUST ==
75
+ "legion.os/trust/v/1\n"`. The formula above is written with an explicit `\n`
76
+ only to show where the newline lives conceptually. In code, construct the
77
+ signed bytes as:
78
+
79
+ ```python
80
+ signed_input = DOMAIN.encode("utf-8") + jcs(projection) # NOT DOMAIN.encode() + b"\n" + jcs(projection)
81
+ ```
82
+
83
+ Appending a separate `\n` double-newlines the signed bytes and every signature
84
+ fails to verify.
85
+
86
+ ## What it implements
87
+
88
+ The verdict spine is checked by two ordered gates, referred to below as
89
+ **GATE 1** and **GATE 2** — GATE 1 (`verify`) establishes authenticity and
90
+ well-formedness; GATE 2 (`certify_close`) then checks standing (expiry and
91
+ revocation) on an already-verified envelope. Both are `async def` functions —
92
+ see the calling convention under **`VerifyResult` / `CertifyResult` are
93
+ unions, not classes** below.
94
+
95
+ - **RFC 8785 JCS** (`jcs.py`) via the [`rfc8785`](https://pypi.org/project/rfc8785/)
96
+ package (maintained by Trail of Bits, itself adapted from the RFC author's
97
+ own reference implementation). Cross-checked in the conformance test against
98
+ the official cyberphone vectors (the independent oracle) **and** the pinned
99
+ canonical hex in the committed corpus.
100
+ - **GATE 1 — `verify(envelope, resolve_key)`** (`verdict.py`): kid-binds-alg
101
+ (the wire `alg` is never used to select the algorithm; the keystore's bound
102
+ alg is, and a mismatch is rejected), integrity over
103
+ `utf8("legion.os/v/1\n") || JCS(null-stripped projection)`, body_type↔body
104
+ disjointness, and a canonical-number guard. Returns the **canonical
105
+ projection**, not the raw input.
106
+ - **GATE 2 — `certify_close(verified, ...)`** (`verdict.py`): expiry +
107
+ `status_ref` → active/revoked/suspended/unknown (fail-closed),
108
+ `require_status`, expiry-checked-before-status.
109
+ - **Crypto** (`crypto.py`) via [`cryptography`](https://pypi.org/project/cryptography/):
110
+ Ed25519 (default), HMAC-SHA-256 (constant-time compare, single trust domain),
111
+ and ECDSA P-256/SHA-256.
112
+ - **`content_key(input)`** (`contentkey.py`): the Legion-defined DERIVED id,
113
+ `"sha256:" + base64url-UNPADDED(SHA-256(JCS(NFC-deep(input))))`. `nfc_deep`
114
+ recursively NFC-normalizes every string — object **keys and values** — via the
115
+ stdlib `unicodedata.normalize("NFC", …)` (native, Python's own UCD — not ICU),
116
+ then the existing `jcs` sorts the already-normalized keys. **Additive** — the
117
+ sign/verify path is untouched, so a composed/decomposed pair shares a
118
+ `content_key` but keeps **different** signed bytes (audit fidelity). An NFC key
119
+ collision **raises** (never merges); scope is **encoding only** (`"100"` !=
120
+ `100`, dates stay distinct). Apps get normalized identity for free and must
121
+ never hand-normalize. Gated against the project's committed conformance
122
+ vectors.
123
+
124
+ ### The ES256 cross-language detail (the whole point)
125
+
126
+ SIGNING.md §4.1 fixes the on-wire ES256 signature as **raw `r‖s`, 64 bytes
127
+ (IEEE P1363)** — *not* ASN.1 DER. Python's `cryptography` ECDSA `verify`
128
+ expects DER, so this binding:
129
+
130
+ 1. **rejects** any ES256 signature that is not exactly 64 bytes — a DER-encoded
131
+ signature presented on the wire (~70 bytes) is rejected (the
132
+ `es256_der_catcher` vector, `must_verify=false`); and
133
+ 2. converts the 64-byte `r‖s` to DER via `encode_dss_signature` before handing
134
+ it to `cryptography`.
135
+
136
+ A binding that accepts DER on the wire is non-conformant.
137
+
138
+ ## Import paths
139
+
140
+ `legion_foundation`'s top-level package re-exports the verdict-spine surface
141
+ directly:
142
+
143
+ ```python
144
+ from legion_foundation import (
145
+ jcs, canonicalize, CanonicalizationLimitError,
146
+ MAX_CANON_DEPTH, MAX_CANON_NODES, MAX_CANON_SCALAR,
147
+ content_key, nfc, nfc_deep,
148
+ verify_bytes, sign_bytes, MintRefusedError,
149
+ ED25519_SMALLORDER_BLOCKLIST, ed25519_strict_profile_fails, es256_is_high_s,
150
+ b64u_decode, b64u_encode,
151
+ verify, certify_close,
152
+ VerifyResult, CertifyResult, VerifyingKey,
153
+ NonCanonicalNumber, assert_canonical_numbers,
154
+ well_formed_error, signed_view, is_alg, is_tier, is_credential, is_verdict,
155
+ ALLOWED_ALGS, BODY_TYPE_CREDENTIAL, BODY_TYPE_VERDICT, DOMAIN,
156
+ )
157
+ ```
158
+
159
+ The `syscall`, `trust`, `receipt`, and `didkey` modules are **not** re-exported
160
+ at the top level — import them by their submodule path:
161
+
162
+ ```python
163
+ from legion_foundation.syscall import authorize, mint_delegation, attenuate
164
+ ```
165
+
166
+ ### `VerifyResult` / `CertifyResult` are unions, not classes
167
+
168
+ Both are `Union` type aliases over a `Literal`-tagged success/failure pair. That
169
+ tagging is what makes the canonical idiom narrow correctly — after `if r.ok:`, a
170
+ type checker knows `r.envelope` is a `dict` rather than `dict | None`.
171
+
172
+ **`verify` and `certify_close` are both `async def` coroutines** — calling
173
+ either returns a coroutine object, not a `VerifyResult`/`CertifyResult`, until
174
+ it is `await`ed. `await` is only legal inside an `async def` function, so the
175
+ canonical idiom is:
176
+
177
+ ```python
178
+ import asyncio
179
+
180
+ async def check(envelope, resolve_key):
181
+ r = await verify(envelope, resolve_key)
182
+ if r.ok:
183
+ issuer = r.envelope["issuer"] # narrows; no assert or cast needed
184
+ else:
185
+ log(r.reason)
186
+
187
+ asyncio.run(check(envelope, resolve_key))
188
+ ```
189
+
190
+ Calling `r = verify(envelope, resolve_key)` without `await` binds `r` to an
191
+ un-awaited coroutine: `r.ok` raises `AttributeError` (coroutines have no `.ok`
192
+ attribute), and Python additionally emits a `RuntimeWarning: coroutine … was
193
+ never awaited` because the verification never actually ran.
194
+
195
+ The practical consequence: `isinstance(r, VerifyResult)` works, but the alias is
196
+ **not callable** — `VerifyResult(...)` raises `TypeError`. Construct or match the
197
+ variants instead, which are exported for exactly that purpose:
198
+
199
+ ```python
200
+ from legion_foundation import VerifyOk, VerifyFail, CertifyOk, CertifyFail
201
+ ```
202
+
203
+ This package ships a PEP 561 `py.typed` marker, so these shapes are authoritative
204
+ to `mypy` and `pyright`.
205
+
206
+ ## Run the conformance gate
207
+
208
+ Requires **Python >= 3.11**. Runtime dependencies are `rfc8785` (RFC 8785 JCS)
209
+ and `cryptography` (Ed25519 / ECDSA P-256 / HMAC-SHA-256) — both installed
210
+ automatically by `pip`.
211
+
212
+ ```bash
213
+ pip install legion-foundation
214
+ python -m legion_foundation.conformance # prints N/N; exits non-zero on any failure
215
+ ```
216
+
217
+ That command is the one that actually works for a PyPI consumer: the
218
+ distribution bundles a pinned 23-file slice of the conformance corpus
219
+ (`jcs-vectors/`, `verdict/`, `syscall/`, `trust/`, `receipt/`) as package data,
220
+ so the gate can run against an **installed** copy with no repo checkout in
221
+ sight. It mirrors the TS binding's three angles against the bundled tape: (1)
222
+ JCS == official vectors == pinned hex, (2) `signed_input` reconstruction +
223
+ crypto-primitive verify/reject per `must_verify`, (3) the real `verify()` gate
224
+ agrees with `must_verify` (incl. the DER-catcher and the null-omission lenient
225
+ input), plus `certify_close` asserted by name.
226
+
227
+ Also run `python -m legion_foundation.conformance --self-test` to check the
228
+ gate's own failure mode rather than its happy path: it copies a real corpus
229
+ tree into a temp directory, mutates it three ways (deletes a whitelisted file,
230
+ truncates a vector array, points at an empty directory), and asserts that
231
+ *each* mutation makes the gate fail non-zero. A green `259/259` from a gate
232
+ that would also pass against a silently truncated tape is not evidence of
233
+ anything; `--self-test` is the check that it would not.
234
+
235
+ **Contributors working from a monorepo checkout do NOT run the same command
236
+ against the same tape.** `python -m legion_foundation.conformance` alone reads
237
+ whichever corpus resolved into the **installed** package at build time
238
+ (`mode: bundled`) — measured, in a clean venv from the repo root: `mode:
239
+ bundled`. The command that actually prefers the live, freshly regenerated
240
+ `corpus/` tree is the in-repo shim:
241
+
242
+ ```bash
243
+ python3 packages/foundation-py/tests/conformance.py
244
+ ```
245
+
246
+ The shim prepends the repo's own `src/` tree to `PYTHONPATH` and relaunches the
247
+ gate in a fresh subprocess. That is the whole mechanism: the module the
248
+ subprocess imports **is** the repo's copy, so its own `__file__`-anchored
249
+ resolution — marker-anchored on `legion.manifest.json`, walking up from
250
+ `__file__`, never from `cwd` — naturally lands on `mode: repo` against the live
251
+ tree. Repo source against the repo tape, with no flag and no hand-off.
252
+
253
+ There is deliberately **no** command-line option to point the gate at an
254
+ arbitrary corpus. An earlier revision had one; it was removed rather than
255
+ hardened, because a flag that redirects the tape is indistinguishable from the
256
+ attack it enables — it let an installed copy verify an unpinned tree while
257
+ skipping the bundled digest check. Resolution is now a property of where the
258
+ imported module lives, which a caller cannot forge by passing an argument.
259
+
260
+ A bare `python -m legion_foundation.conformance` therefore resolves `mode: repo`
261
+ whenever the module it imports lives under a checkout (via `PYTHONPATH`, or an
262
+ editable install), and `mode: bundled` otherwise — which is the case for an
263
+ ordinary `pip install legion-foundation`.
264
+
265
+ Every run — through the shim or bare — prints a line naming which tape it
266
+ actually tested, so you never have to guess:
267
+
268
+ ```
269
+ conformance gate resolution -> mode: bundled, root: <path>
270
+ ```
271
+
272
+ or `mode: repo` when the module resolved a live checkout. Check that line before
273
+ trusting a green result to mean "the tape in this checkout," not "whatever was
274
+ staged into the wheel."
275
+
276
+ One caveat worth knowing, since this package hardens against it internally: a
277
+ bare `python -m <module>` puts the current working directory on `sys.path`, so
278
+ running the gate from a directory you do not control could import something
279
+ other than the installed package. That is ordinary CPython `-m` behaviour rather
280
+ than anything specific to this package, and the two subprocess relaunches this
281
+ package performs internally both set `PYTHONSAFEPATH=1` against it. If you are
282
+ running the gate somewhere untrusted, do the same — `PYTHONSAFEPATH=1 python -m
283
+ legion_foundation.conformance`.
284
+
285
+ **What a green run proves, and what it does not.** A passing run proves that
286
+ *this artifact* — this interpreter, this build of `cryptography`, this build
287
+ of OpenSSL — reproduces the pinned bytes and pinned signatures on *your*
288
+ machine. That is genuine environment-drift detection: it did not exist before
289
+ this package shipped its own corpus slice, and no other check available to a
290
+ PyPI consumer offers it. It does **not** prove cross-binding conformance —
291
+ that the Go, Rust, Zig, Swift, Java, Haskell, Elixir, C, and TypeScript
292
+ bindings agree with this one is established by a ten-job CI pipeline a
293
+ consumer of this package cannot see or rerun; running this gate is not a
294
+ substitute for that pipeline, and no claim to the contrary should be inferred
295
+ from a green result here.
296
+
297
+ **A green run also does not prove the artifact you installed is authentic.**
298
+ The gate ships *inside* the same distribution it tests: anyone who can replace
299
+ the contents of this package on your machine (a compromised mirror, a tampered
300
+ wheel, a supply-chain substitution) can replace the gate along with it, so a
301
+ self-test can never catch tampering to the artifact carrying it. That is a
302
+ property of every self-test, not something specific to this design. Verifying
303
+ that the bits you installed are the bits actually published requires
304
+ publish-side attestation — Sigstore / PEP 740 provenance — which this release
305
+ does not carry.
306
+
307
+ **The bundled corpus is deliberately NOT all non-forgeable, and only the
308
+ ES256 vectors are.** The JCS vectors are the **official third-party cyberphone
309
+ RFC 8785 test suite** — an oracle external to this project, not authored by
310
+ it, and untouched. The ES256 signing vectors publish a **public key with no
311
+ matching private key** bundled anywhere in this distribution, so verifying
312
+ those pinned signatures cannot be faked by regenerating both sides of the
313
+ check from a private key you also control. The EdDSA and HS256 portions of the
314
+ tape are different: the corpus deliberately publishes the raw Ed25519
315
+ `seed_hex` and the HS256 `secret_hex` used to mint those vectors — both are
316
+ byte-counting sequences (`000102...1f`), oracle material with no purpose
317
+ beyond reproducing this exact tape, securing nothing, and identical across
318
+ every language binding. If you run a secret scanner over your installed
319
+ `site-packages`, it **will** flag these as apparent private keys/secrets — they
320
+ are intentional, public, non-sensitive test fixtures, not a leaked credential.
321
+
322
+ ## `syscall` — capabilities
323
+
324
+ `legion_foundation.syscall` is the Python binding for the second subsystem
325
+ (import it explicitly — see **Import paths** above). It **reuses this
326
+ package's mechanism** — `jcs` (RFC 8785), `crypto.verify_bytes` (Ed25519 via
327
+ `cryptography`), and `crypto.b64u_decode`/`b64u_encode` — and adds only the
328
+ syscall-specific parts: a **distinct** DOMAIN tag `legion.os/syscall/v/1` (so a
329
+ verdict signature can never verify as a syscall one — pinned by a cross-domain
330
+ test), and `did:key` Ed25519 principals (base58btc decoded by a
331
+ **dependency-free ~12-line decoder**, then strip the `0xed01` multicodec).
332
+
333
+ It also carries the UCAN `pol` evaluator — `==`/`!=`/`<`/`<=`/`>`/`>=`/`like`/
334
+ `not`/`and`/`or`/`all`/`any` over dotted+index selectors; empty policy = permit;
335
+ an unresolvable selector evaluates to `false` rather than raising; the
336
+ evaluator is finite by construction.
337
+
338
+ The **`authorize`** gate is the syscall analogue of `certify_close`, and is
339
+ fail-closed in a fixed order: gate 0 size/depth → gate 1 nonce-freshness
340
+ *first* (replay exits before any crypto) → verify → walk the proof chain →
341
+ root anchor. `authorize`'s exact refusal reasons are gated against the
342
+ project's committed conformance vectors.
343
+
344
+ **One caller-visible failure mode is not a return value.** If the opt-in
345
+ `require_root_pol` argument is present and is not a `Mapping` of selector →
346
+ JSON scalar, `authorize` raises `TypeError` — before gate 0, so ahead of every
347
+ refusal path. That is deliberate: it is caller-side API misuse, not folded
348
+ into a token refusal, because the `AuthorizeResult.reason` strings are
349
+ corpus-pinned and reporting a misconfiguration as a request refusal would send
350
+ whoever debugs it to inspect an innocent request instead. Pass `None` for "no
351
+ pin." The argument is snapshotted at entry, so mutating the mapping later in
352
+ the call — from an injected `resolve_prf`, say — cannot void the pin.
353
+
354
+ ## `trust` — the recursion
355
+
356
+ `legion_foundation.trust` is the third subsystem's binding (import it
357
+ explicitly by submodule path). It answers a different question than `verdict`
358
+ or `syscall`: not "is this envelope authentic" but "how much should a verifier
359
+ be trusted, given its measured track record." DOMAIN `legion.os/trust/v/1`,
360
+ distinct from the other two. `weight_from_record` turns a track record (true
361
+ positives, false positives, false negatives, and the ground-truth base rate)
362
+ into a calibrated integer-PPM weight — deliberately **float-free**, ≈0 at cold
363
+ start (no graded verdicts yet), and ≈0 for a "fabricator" — a verifier whose
364
+ measured precision is at or below the base rate, i.e. no better than guessing
365
+ the prior and contributing no skill above chance. `mint_weight_verdict` signs
366
+ the resulting weight as a spine CREDENTIAL over `utf8(DOMAIN_TRUST) ||
367
+ JCS(projection)` — reusing the same verdict-spine shape and signing mechanism,
368
+ with only the DOMAIN tag distinct — and structurally refuses to mint a weight
369
+ for a SEMANTIC verifier (one with no answer key to measure against), since
370
+ there is no track record to calibrate against in that case.
371
+
372
+ ## `receipt` — the closing leg
373
+
374
+ `legion_foundation.receipt` (import it explicitly by submodule path) wires up
375
+ the third leg of the UCAN triad — delegation → invocation → receipt — but
376
+ **mints no DOMAIN tag and no new wire primitive of its own.** It is pure
377
+ orchestration over the existing spine: an execution record is an ordinary
378
+ `verdict` **credential** (the executor's claim: "I ran invocation X, status S,
379
+ output digest D"), and a receipt is an ordinary `verdict` **verdict** envelope
380
+ — an independent party's signed appraisal *of* that execution record. The one
381
+ invariant `receipt` adds is that the receipt's signer must not be the
382
+ execution record's signer (verifier != executor); a receipt signed by the same
383
+ key that signed the execution record would merely notarize the executor's own
384
+ claim rather than independently confirm it, so `legion_foundation.receipt`
385
+ refuses to build one (`ProposerIsVerifierError`) and re-checks the same
386
+ condition at verify time.
387
+
388
+ ## Security
389
+
390
+ This package implements a cryptographic signing and authorization system. The
391
+ source repository is private and has no public issue tracker, so please do
392
+ not open a public issue for a suspected vulnerability. Instead, report it
393
+ directly to:
394
+
395
+ security@copiawealthstudios.com
396
+
397
+ ## License
398
+
399
+ Apache-2.0. This distribution includes the `LICENSE` and `NOTICE` files.