Metadata-Version: 2.5
Name: legion-foundation
Version: 0.1.0
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.
Author: Copia Wealth Studios Inc. (LEGION-OS), Michael J. Sikorsky
Maintainer-email: LEGION-OS Security <security@copiawealthstudios.com>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: canonicalization,capabilities,did-key,ed25519,es256,jcs,rfc8785,ucan
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: cryptography>=42
Requires-Dist: rfc8785>=0.1.4
Description-Content-Type: text/markdown

# legion-foundation

The **verdict spine** in Python — an *independent* implementation of a frozen,
cross-language signing contract, with **no dependency on the TypeScript
reference binding**. It exists to prove the underlying wire format is
genuinely polyglot: this binding reproduces, byte-for-byte, the same pinned
canonical bytes and verifies the same pinned signatures that nine other
independent language bindings do, against one shared conformance corpus. The
corpus is the contract; if a binding can't reproduce a vector, it fails — the
tape is never edited to make a binding pass.

**Provenance boundary.** In-source `ADR-NNNN` / `PB-NNNN` markers scattered
through this codebase's comments refer to entries in the parent project's
private architectural decision record — they are not resolvable outside that
repository. The same applies to citations embedded inside the bundled
conformance corpus itself: several vector files carry a `spec` or `note` field
that names an internal document (`SIGNING-syscall.md`, `ADR-0012`, `ADR-0027`,
`ADR-0035`, ...) or contains internal review shorthand such as the literal
string `Generated — never hand-edit (drift-gated by pnpm corpus:drift)`. None
of these are resolvable outside the private repository either. The normative,
versioned contract for the wire formats this package implements is a document
called `SIGNING.md`, which currently also lives in that private repository and
is **not** distributed with this package. So: this README and the bundled
conformance corpus are, today, the complete publicly available description of
the wire format. Where a comment or a corpus `spec`/`note` field cites
`SIGNING.md §N` or another private document, treat the corpus vectors
themselves as the operative specification — they are the thing every binding
is actually gated against, independent of whether the cited document is
reachable.

Every signed message in this family follows the same construction:

```
signed_bytes = utf8("<DOMAIN>\n") || JCS(projection)
```

RFC 8785 JCS (JSON Canonicalization Scheme) over a null-stripped projection of
the message, prefixed with an off-wire domain-separation tag that never
appears in the message itself. This package implements several subsystems built
on that one mechanism, each with its own distinct DOMAIN tag — so a signature
for one can never validate as another:

- **verdict** — DOMAIN `legion.os/v/1`: `utf8("legion.os/v/1\n") || JCS(projection)`
- **syscall** — DOMAIN `legion.os/syscall/v/1`: `utf8("legion.os/syscall/v/1\n") || JCS(projection)`
- **trust** — DOMAIN `legion.os/trust/v/1`: `utf8("legion.os/trust/v/1\n") || JCS(projection)`

**The exported `DOMAIN` / `DOMAIN_SYSCALL` / `DOMAIN_TRUST` constants already
carry the trailing newline** — measured: `DOMAIN == "legion.os/v/1\n"`,
`DOMAIN_SYSCALL == "legion.os/syscall/v/1\n"`, `DOMAIN_TRUST ==
"legion.os/trust/v/1\n"`. The formula above is written with an explicit `\n`
only to show where the newline lives conceptually. In code, construct the
signed bytes as:

```python
signed_input = DOMAIN.encode("utf-8") + jcs(projection)   # NOT DOMAIN.encode() + b"\n" + jcs(projection)
```

Appending a separate `\n` double-newlines the signed bytes and every signature
fails to verify.

## What it implements

The verdict spine is checked by two ordered gates, referred to below as
**GATE 1** and **GATE 2** — GATE 1 (`verify`) establishes authenticity and
well-formedness; GATE 2 (`certify_close`) then checks standing (expiry and
revocation) on an already-verified envelope. Both are `async def` functions —
see the calling convention under **`VerifyResult` / `CertifyResult` are
unions, not classes** below.

- **RFC 8785 JCS** (`jcs.py`) via the [`rfc8785`](https://pypi.org/project/rfc8785/)
  package (maintained by Trail of Bits, itself adapted from the RFC author's
  own reference implementation). Cross-checked in the conformance test against
  the official cyberphone vectors (the independent oracle) **and** the pinned
  canonical hex in the committed corpus.
- **GATE 1 — `verify(envelope, resolve_key)`** (`verdict.py`): kid-binds-alg
  (the wire `alg` is never used to select the algorithm; the keystore's bound
  alg is, and a mismatch is rejected), integrity over
  `utf8("legion.os/v/1\n") || JCS(null-stripped projection)`, body_type↔body
  disjointness, and a canonical-number guard. Returns the **canonical
  projection**, not the raw input.
- **GATE 2 — `certify_close(verified, ...)`** (`verdict.py`): expiry +
  `status_ref` → active/revoked/suspended/unknown (fail-closed),
  `require_status`, expiry-checked-before-status.
- **Crypto** (`crypto.py`) via [`cryptography`](https://pypi.org/project/cryptography/):
  Ed25519 (default), HMAC-SHA-256 (constant-time compare, single trust domain),
  and ECDSA P-256/SHA-256.
- **`content_key(input)`** (`contentkey.py`): the Legion-defined DERIVED id,
  `"sha256:" + base64url-UNPADDED(SHA-256(JCS(NFC-deep(input))))`. `nfc_deep`
  recursively NFC-normalizes every string — object **keys and values** — via the
  stdlib `unicodedata.normalize("NFC", …)` (native, Python's own UCD — not ICU),
  then the existing `jcs` sorts the already-normalized keys. **Additive** — the
  sign/verify path is untouched, so a composed/decomposed pair shares a
  `content_key` but keeps **different** signed bytes (audit fidelity). An NFC key
  collision **raises** (never merges); scope is **encoding only** (`"100"` !=
  `100`, dates stay distinct). Apps get normalized identity for free and must
  never hand-normalize. Gated against the project's committed conformance
  vectors.

### The ES256 cross-language detail (the whole point)

SIGNING.md §4.1 fixes the on-wire ES256 signature as **raw `r‖s`, 64 bytes
(IEEE P1363)** — *not* ASN.1 DER. Python's `cryptography` ECDSA `verify`
expects DER, so this binding:

1. **rejects** any ES256 signature that is not exactly 64 bytes — a DER-encoded
   signature presented on the wire (~70 bytes) is rejected (the
   `es256_der_catcher` vector, `must_verify=false`); and
2. converts the 64-byte `r‖s` to DER via `encode_dss_signature` before handing
   it to `cryptography`.

A binding that accepts DER on the wire is non-conformant.

## Import paths

`legion_foundation`'s top-level package re-exports the verdict-spine surface
directly:

```python
from legion_foundation import (
    jcs, canonicalize, CanonicalizationLimitError,
    MAX_CANON_DEPTH, MAX_CANON_NODES, MAX_CANON_SCALAR,
    content_key, nfc, nfc_deep,
    verify_bytes, sign_bytes, MintRefusedError,
    ED25519_SMALLORDER_BLOCKLIST, ed25519_strict_profile_fails, es256_is_high_s,
    b64u_decode, b64u_encode,
    verify, certify_close,
    VerifyResult, CertifyResult, VerifyingKey,
    NonCanonicalNumber, assert_canonical_numbers,
    well_formed_error, signed_view, is_alg, is_tier, is_credential, is_verdict,
    ALLOWED_ALGS, BODY_TYPE_CREDENTIAL, BODY_TYPE_VERDICT, DOMAIN,
)
```

The `syscall`, `trust`, `receipt`, and `didkey` modules are **not** re-exported
at the top level — import them by their submodule path:

```python
from legion_foundation.syscall import authorize, mint_delegation, attenuate
```

### `VerifyResult` / `CertifyResult` are unions, not classes

Both are `Union` type aliases over a `Literal`-tagged success/failure pair. That
tagging is what makes the canonical idiom narrow correctly — after `if r.ok:`, a
type checker knows `r.envelope` is a `dict` rather than `dict | None`.

**`verify` and `certify_close` are both `async def` coroutines** — calling
either returns a coroutine object, not a `VerifyResult`/`CertifyResult`, until
it is `await`ed. `await` is only legal inside an `async def` function, so the
canonical idiom is:

```python
import asyncio

async def check(envelope, resolve_key):
    r = await verify(envelope, resolve_key)
    if r.ok:
        issuer = r.envelope["issuer"]   # narrows; no assert or cast needed
    else:
        log(r.reason)

asyncio.run(check(envelope, resolve_key))
```

Calling `r = verify(envelope, resolve_key)` without `await` binds `r` to an
un-awaited coroutine: `r.ok` raises `AttributeError` (coroutines have no `.ok`
attribute), and Python additionally emits a `RuntimeWarning: coroutine … was
never awaited` because the verification never actually ran.

The practical consequence: `isinstance(r, VerifyResult)` works, but the alias is
**not callable** — `VerifyResult(...)` raises `TypeError`. Construct or match the
variants instead, which are exported for exactly that purpose:

```python
from legion_foundation import VerifyOk, VerifyFail, CertifyOk, CertifyFail
```

This package ships a PEP 561 `py.typed` marker, so these shapes are authoritative
to `mypy` and `pyright`.

## Run the conformance gate

Requires **Python >= 3.11**. Runtime dependencies are `rfc8785` (RFC 8785 JCS)
and `cryptography` (Ed25519 / ECDSA P-256 / HMAC-SHA-256) — both installed
automatically by `pip`.

```bash
pip install legion-foundation
python -m legion_foundation.conformance   # prints N/N; exits non-zero on any failure
```

That command is the one that actually works for a PyPI consumer: the
distribution bundles a pinned 23-file slice of the conformance corpus
(`jcs-vectors/`, `verdict/`, `syscall/`, `trust/`, `receipt/`) as package data,
so the gate can run against an **installed** copy with no repo checkout in
sight. It mirrors the TS binding's three angles against the bundled tape: (1)
JCS == official vectors == pinned hex, (2) `signed_input` reconstruction +
crypto-primitive verify/reject per `must_verify`, (3) the real `verify()` gate
agrees with `must_verify` (incl. the DER-catcher and the null-omission lenient
input), plus `certify_close` asserted by name.

Also run `python -m legion_foundation.conformance --self-test` to check the
gate's own failure mode rather than its happy path: it copies a real corpus
tree into a temp directory, mutates it three ways (deletes a whitelisted file,
truncates a vector array, points at an empty directory), and asserts that
*each* mutation makes the gate fail non-zero. A green `259/259` from a gate
that would also pass against a silently truncated tape is not evidence of
anything; `--self-test` is the check that it would not.

**Contributors working from a monorepo checkout do NOT run the same command
against the same tape.** `python -m legion_foundation.conformance` alone reads
whichever corpus resolved into the **installed** package at build time
(`mode: bundled`) — measured, in a clean venv from the repo root: `mode:
bundled`. The command that actually prefers the live, freshly regenerated
`corpus/` tree is the in-repo shim:

```bash
python3 packages/foundation-py/tests/conformance.py
```

The shim prepends the repo's own `src/` tree to `PYTHONPATH` and relaunches the
gate in a fresh subprocess. That is the whole mechanism: the module the
subprocess imports **is** the repo's copy, so its own `__file__`-anchored
resolution — marker-anchored on `legion.manifest.json`, walking up from
`__file__`, never from `cwd` — naturally lands on `mode: repo` against the live
tree. Repo source against the repo tape, with no flag and no hand-off.

There is deliberately **no** command-line option to point the gate at an
arbitrary corpus. An earlier revision had one; it was removed rather than
hardened, because a flag that redirects the tape is indistinguishable from the
attack it enables — it let an installed copy verify an unpinned tree while
skipping the bundled digest check. Resolution is now a property of where the
imported module lives, which a caller cannot forge by passing an argument.

A bare `python -m legion_foundation.conformance` therefore resolves `mode: repo`
whenever the module it imports lives under a checkout (via `PYTHONPATH`, or an
editable install), and `mode: bundled` otherwise — which is the case for an
ordinary `pip install legion-foundation`.

Every run — through the shim or bare — prints a line naming which tape it
actually tested, so you never have to guess:

```
conformance gate resolution -> mode: bundled, root: <path>
```

or `mode: repo` when the module resolved a live checkout. Check that line before
trusting a green result to mean "the tape in this checkout," not "whatever was
staged into the wheel."

One caveat worth knowing, since this package hardens against it internally: a
bare `python -m <module>` puts the current working directory on `sys.path`, so
running the gate from a directory you do not control could import something
other than the installed package. That is ordinary CPython `-m` behaviour rather
than anything specific to this package, and the two subprocess relaunches this
package performs internally both set `PYTHONSAFEPATH=1` against it. If you are
running the gate somewhere untrusted, do the same — `PYTHONSAFEPATH=1 python -m
legion_foundation.conformance`.

**What a green run proves, and what it does not.** A passing run proves that
*this artifact* — this interpreter, this build of `cryptography`, this build
of OpenSSL — reproduces the pinned bytes and pinned signatures on *your*
machine. That is genuine environment-drift detection: it did not exist before
this package shipped its own corpus slice, and no other check available to a
PyPI consumer offers it. It does **not** prove cross-binding conformance —
that the Go, Rust, Zig, Swift, Java, Haskell, Elixir, C, and TypeScript
bindings agree with this one is established by a ten-job CI pipeline a
consumer of this package cannot see or rerun; running this gate is not a
substitute for that pipeline, and no claim to the contrary should be inferred
from a green result here.

**A green run also does not prove the artifact you installed is authentic.**
The gate ships *inside* the same distribution it tests: anyone who can replace
the contents of this package on your machine (a compromised mirror, a tampered
wheel, a supply-chain substitution) can replace the gate along with it, so a
self-test can never catch tampering to the artifact carrying it. That is a
property of every self-test, not something specific to this design. Verifying
that the bits you installed are the bits actually published requires
publish-side attestation — Sigstore / PEP 740 provenance — which this release
does not carry.

**The bundled corpus is deliberately NOT all non-forgeable, and only the
ES256 vectors are.** The JCS vectors are the **official third-party cyberphone
RFC 8785 test suite** — an oracle external to this project, not authored by
it, and untouched. The ES256 signing vectors publish a **public key with no
matching private key** bundled anywhere in this distribution, so verifying
those pinned signatures cannot be faked by regenerating both sides of the
check from a private key you also control. The EdDSA and HS256 portions of the
tape are different: the corpus deliberately publishes the raw Ed25519
`seed_hex` and the HS256 `secret_hex` used to mint those vectors — both are
byte-counting sequences (`000102...1f`), oracle material with no purpose
beyond reproducing this exact tape, securing nothing, and identical across
every language binding. If you run a secret scanner over your installed
`site-packages`, it **will** flag these as apparent private keys/secrets — they
are intentional, public, non-sensitive test fixtures, not a leaked credential.

## `syscall` — capabilities

`legion_foundation.syscall` is the Python binding for the second subsystem
(import it explicitly — see **Import paths** above). It **reuses this
package's mechanism** — `jcs` (RFC 8785), `crypto.verify_bytes` (Ed25519 via
`cryptography`), and `crypto.b64u_decode`/`b64u_encode` — and adds only the
syscall-specific parts: a **distinct** DOMAIN tag `legion.os/syscall/v/1` (so a
verdict signature can never verify as a syscall one — pinned by a cross-domain
test), and `did:key` Ed25519 principals (base58btc decoded by a
**dependency-free ~12-line decoder**, then strip the `0xed01` multicodec).

It also carries the UCAN `pol` evaluator — `==`/`!=`/`<`/`<=`/`>`/`>=`/`like`/
`not`/`and`/`or`/`all`/`any` over dotted+index selectors; empty policy = permit;
an unresolvable selector evaluates to `false` rather than raising; the
evaluator is finite by construction.

The **`authorize`** gate is the syscall analogue of `certify_close`, and is
fail-closed in a fixed order: gate 0 size/depth → gate 1 nonce-freshness
*first* (replay exits before any crypto) → verify → walk the proof chain →
root anchor. `authorize`'s exact refusal reasons are gated against the
project's committed conformance vectors.

**One caller-visible failure mode is not a return value.** If the opt-in
`require_root_pol` argument is present and is not a `Mapping` of selector →
JSON scalar, `authorize` raises `TypeError` — before gate 0, so ahead of every
refusal path. That is deliberate: it is caller-side API misuse, not folded
into a token refusal, because the `AuthorizeResult.reason` strings are
corpus-pinned and reporting a misconfiguration as a request refusal would send
whoever debugs it to inspect an innocent request instead. Pass `None` for "no
pin." The argument is snapshotted at entry, so mutating the mapping later in
the call — from an injected `resolve_prf`, say — cannot void the pin.

## `trust` — the recursion

`legion_foundation.trust` is the third subsystem's binding (import it
explicitly by submodule path). It answers a different question than `verdict`
or `syscall`: not "is this envelope authentic" but "how much should a verifier
be trusted, given its measured track record." DOMAIN `legion.os/trust/v/1`,
distinct from the other two. `weight_from_record` turns a track record (true
positives, false positives, false negatives, and the ground-truth base rate)
into a calibrated integer-PPM weight — deliberately **float-free**, ≈0 at cold
start (no graded verdicts yet), and ≈0 for a "fabricator" — a verifier whose
measured precision is at or below the base rate, i.e. no better than guessing
the prior and contributing no skill above chance. `mint_weight_verdict` signs
the resulting weight as a spine CREDENTIAL over `utf8(DOMAIN_TRUST) ||
JCS(projection)` — reusing the same verdict-spine shape and signing mechanism,
with only the DOMAIN tag distinct — and structurally refuses to mint a weight
for a SEMANTIC verifier (one with no answer key to measure against), since
there is no track record to calibrate against in that case.

## `receipt` — the closing leg

`legion_foundation.receipt` (import it explicitly by submodule path) wires up
the third leg of the UCAN triad — delegation → invocation → receipt — but
**mints no DOMAIN tag and no new wire primitive of its own.** It is pure
orchestration over the existing spine: an execution record is an ordinary
`verdict` **credential** (the executor's claim: "I ran invocation X, status S,
output digest D"), and a receipt is an ordinary `verdict` **verdict** envelope
— an independent party's signed appraisal *of* that execution record. The one
invariant `receipt` adds is that the receipt's signer must not be the
execution record's signer (verifier != executor); a receipt signed by the same
key that signed the execution record would merely notarize the executor's own
claim rather than independently confirm it, so `legion_foundation.receipt`
refuses to build one (`ProposerIsVerifierError`) and re-checks the same
condition at verify time.

## Security

This package implements a cryptographic signing and authorization system. The
source repository is private and has no public issue tracker, so please do
not open a public issue for a suspected vulnerability. Instead, report it
directly to:

security@copiawealthstudios.com

## License

Apache-2.0. This distribution includes the `LICENSE` and `NOTICE` files.
