Metadata-Version: 2.4
Name: trust-core
Version: 0.1.0
Summary: A small, language-neutral trust plane: Biscuit capability tokens + per-request Ed25519 proof-of-possession, with the issuer private key isolatable in its own signer process.
Author-email: Aryan Falahatpisheh <aryanfalahat@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/falahat/trust-core
Project-URL: Repository, https://github.com/falahat/trust-core.git
Keywords: biscuit,capability,authorization,ed25519,proof-of-possession,zero-trust,sidecar
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: pynacl>=1.5
Requires-Dist: biscuit-python>=0.4
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: flake8>=6.0; extra == "dev"
Requires-Dist: pylint>=3.0; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"

# trust-core

A small, language-neutral **trust plane**: Biscuit public-key capability tokens + a per-request
Ed25519 **proof-of-possession**, plus a length-prefixed wire so the issuer private key can be
isolated in its own **signer process**.

The engine holds **zero app vocabulary** — the policy (Datalog) and the request-facts are data
passed in — so different services share it unchanged and each supplies its own rights/scopes and
policy strings. No novel crypto: it composes the audited Rust `biscuit-auth` core (via
`biscuit-python`) and libsodium (PyNaCl).

## The model

Three tiers, assigned by the **containment rule** — *process isolation buys containment only for
a component that holds a secret*:

| Tier | Holds | Isolation | trust-core |
|---|---|---|---|
| **1. Signer** — mint + job-sign | issuer **private** key | its own process | `IssuerKey`, `mint_biscuit`, `sign_job` |
| **2. Verifier + authorizer** | issuer **public** key only | in-process library | `authenticate`, `check_policy`, `verify_job` |
| **3. App predicates** | app state | stays in the app | *(the consumer's adapter)* |

Only tier 1 holds a secret, so only it needs to leave. Tiers 2–3 are a library the app links.

## Layers

- `trust_core.keys` — `IssuerKey` (mint + job-sign) and `WorkerKey` (a holder's PoP key). One
  32-byte Ed25519 seed each; the issuer seed doubles as the Biscuit root and the job signer.
- `trust_core.proof` — `RequestParts` / `WorkerProof` / `make_proof` + the header/query parsers.
  The canonical string binds `method`, `path`, and `sha256(body)`, so a captured signature is
  valid only for the one request it was made for.
- `trust_core.engine` — `mint_biscuit` (bind a key + arbitrary Datalog facts + expiry),
  `authenticate` (issuer-sig → identity → revocation → freshness → proof-of-possession),
  `check_policy` (one authorizer decision), `sign_job` / `verify_job`.
- `trust_core.wire` — the signer sidecar's length-prefixed JSON framing (fail-closed: oversize
  cap, closed-connection, non-object body).
- `trust_core.errors` — the single fail-closed `TrustError`.

## Why the facts are parameterized

`mint_biscuit` binds fact **values** as bound Datalog parameters (never string interpolation),
so a value can never inject Datalog — a safer primitive than a raw `mint(datalog_string)` helper.

## Example

```python
from trust_core import IssuerKey, WorkerKey, RequestParts, make_proof, \
    mint_biscuit, authenticate, check_policy

issuer, worker = IssuerKey.generate(), WorkerKey.generate()

# Issuer mints a capability binding the worker's public key.
token = mint_biscuit(
    issuer, public_hex=worker.public_hex, holder_id="gpu-1",
    facts={"right": ["lease"], "lane": ["transcribe"]},
    revocation_id="rev-1", expires_at=now + 3600,
)

# Worker proves possession on each request; verifier authenticates then authorizes.
proof = make_proof(worker, token, RequestParts("POST", "/lease", b"{}"),
                   timestamp=str(now), nonce="n1")
parsed, identity = authenticate(
    issuer.public_hex, proof, RequestParts("POST", "/lease", b"{}"),
    now=now, revoked_ids=frozenset(), max_skew_seconds=300,
)
check_policy(parsed, request_facts={"req_right": "lease", "req_lane": "transcribe"},
             allow="allow if req_right($r), right($r), req_lane($l), lane($l)", now=now)
```

## Status

Local package (no published release yet). Consumed by **easy-podcast**; designed to be reused by
other services (e.g. antibody-analysis) — each writes a thin adapter that maps routes → rights,
supplies the policy strings, and owns its replay + data-predicates.
