The 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.
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)
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.
transcript-v1 has a canonical string and digest that must be identical across languages. The
runtime includes Go, JavaScript (scripts/conformance/transcript_v1_conformance.js), and Python
(...py) conformance harnesses, run by make transcript-conformance. This is what lets SDKs in
other languages produce proofs the Go verifier accepts byte-for-byte.
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 Security model.
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.
trustplane bundle build emits an unsigned skeleton until you sign the reviewed trust material
and policy bundle with trustplane bundle sign. Production-style adapter loading should require
the matching bundle-signing public key and reject unsigned or tampered bundles.
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.
Proof kinds: the extension point
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: the threat model and trust tiers → Security model.