Contracts
Everything in TrustPlane Auth is built on a small set of versioned contracts. If you understand these five, you understand the system’s data model. They are frozen as the “contract lock,” and external code is expected to depend on them rather than on implementation details.
flowchart TB PP["passport-v1<br/>(the artifact)"] --> TR["transcript-v1<br/>(request binding)"] TR --> SG["signer taxonomy<br/>(key-binding strength)"] SG --> BN["bundle-v1<br/>(trust material + route policy)"] BN --> AU["audit-event-v0.1<br/>(decision record)"]
1. Passport (passport-v1)
Section titled “1. Passport (passport-v1)”A short-lived, proof-bound authorization artifact derived from workload identity. Not a bearer token. Minimum claims:
| Claim | Meaning |
|---|---|
iss |
Issuer URI |
sub |
URI-compatible, provider-neutral subject |
aud |
Intended audience |
iat / exp |
Issued-at / expiry (short window) |
jti |
Unique passport id (replay key) |
trust_domain |
Administrative trust boundary |
cnf |
Confirmation claim — the key/identity to prove at request time |
Verification algorithm (what authcore does):
- Parse passport.
- Resolve issuer key (from local trust material).
- Verify signature.
- Validate
iss,sub,aud,iat,exp,jti,trust_domain,cnf. - Check expected audience and trust domain.
- Check request proof binding through
cnf. - Check replay/
jtistate when enabled. - Return allow or deny with a clear reason.
A passport is accepted only when the caller can prove possession of the key referenced by
cnffor this request. Identity alone is never enough.
2. Request binding (transcript-v1)
Section titled “2. Request binding (transcript-v1)”The proof is a signature over a canonical transcript of the request. transcript-v1 binds:
- HTTP method
- authority (host)
- path
- query (normalized)
- selected headers (allow-list)
- nonce
- body hash
- audience
route_id- passport
jti - issued-at bucket
- key binding (signer class)
Because the proof is over all of these, tampering with any field after signing causes
request_binding_mismatch; a malformed/non-verifying proof is invalid_request_proof.
3. Signer taxonomy (key-binding classes)
Section titled “3. Signer taxonomy (key-binding classes)”A single ordered vocabulary for how strong the signing key is (pkg/authcore/signer.go):
software < remote_kms < hardware_local < attested_workload| Class | Rank | What it means |
|---|---|---|
software |
10 | A software-held private key. Proves possession only. |
remote_kms |
20 | Key bytes stay in a KMS, but may be callable with portable credentials. |
hardware_local |
30 | Non-exportable local hardware key. |
attested_workload |
40 | Broker verified workload identity (e.g. SPIFFE SVID) before issuance. |
A route demands a minimum via required_key_binding. If the presented class is lower, the
verifier denies with insufficient_key_binding. Off-host key-reuse prevention is only claimed
for hardware_local and attested_workload — see the
Security model.
4. Trust material & policy bundle (trustplane-bundle-v1)
Section titled “4. Trust material & policy bundle (trustplane-bundle-v1)”Two related local documents:
- Trust material — the trusted issuer/broker public keys (by
kid). - Policy bundle — per-route rules in the
trustplane-bundle-v1envelope.
A route entry looks like this (real fixture shape):
{ "route_id": "acme.demo.orders.read", "method": "GET", "path_template": "/orders", "freshness_class": "bounded", "max_staleness_seconds": 300, "allowed_sources": [ { "issuer": "https://issuer.acme.demo/external-jwks", "trust_domain": "acme.demo.external", "subject_exact": "external:jwks:hosted-demo-caller", "required_key_binding": "software", "context_policy": { "required_purpose": "read_orders" } }, { "issuer": "https://issuer.example.com/spire", "trust_domain": "example.local", "subject_prefix": "spiffe://example.local/ns/default/sa/", "required_key_binding": "attested_workload", "provenance_policy": { "profile": "trustplane-spiffe-spire-k8s-v1", "required_spiffe_trust_domain": "example.local", "required_posture": "spiffe_svid_verified" } } ]}Freshness classes decide how stale a bundle may be for a route:
| Class | Behavior | Deny when stale |
|---|---|---|
realtime |
Requires a current bundle view | stale_bundle_fail_closed |
bounded |
Allows known age up to max_staleness_seconds |
fail-closed after window |
offline-ok |
Explicitly allows stale/offline use | (never, by design) |
Unknown freshness class → bundle_freshness_unknown. Bounded without a positive max age →
fail-closed as misconfigured.
5. Audit event (trustplane-auth-audit-event-v0.1)
Section titled “5. Audit event (trustplane-auth-audit-event-v0.1)”Every allow/deny decision can be emitted as stable JSON. Required fields:
version, occurred_at, component, outcome, accepted, reason_code, detail_reason.
Stable optional fields: event_id, request_id, route_id, audience, issuer,
subject, jti, key_binding, required_key_binding, transcript_sha256, policy_id,
policy_version, source_profile, source_spiffe_id.
reserved_rls_deny exists only as a reserved future denial shape; this schema does not
implement row-level security or any managed policy evaluation.
Extend with proof kinds
Section titled “Extend with proof kinds”TrustPlane ships exactly one built-in proof kind, core, which verifies the standard
passport semantics (signature, iss, sub, aud, iat/exp, jti/replay, trust_domain,
cnf). External proof kinds plug in through a stable interface without changing the passport
contract:
type ProofVerifier interface { Kind() string Verify(ctx context.Context, req ProofRequest) (*ProofResult, error)}Passport verification answers “is this passport valid for this issuer/audience/trust-domain/time/ cnf?”. Proof-kind verification answers “does this request satisfy the required proof semantics?”. Together they let APIs reject static secrets in favor of short-lived, proof-bound artifacts.
Next steps
Section titled “Next steps”- Security model — the threat model and trust tiers.
- Capabilities — each contract in action, one capability per page.