Skip to main content

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:

ClaimMeaning
issIssuer URI
subURI-compatible, provider-neutral subject
audIntended audience
iat / expIssued-at / expiry (short window)
jtiUnique passport id (replay key)
trust_domainAdministrative trust boundary
cnfConfirmation claim — the key/identity to prove at request time

Verification algorithm (what authcore does):

  1. Parse passport.
  2. Resolve issuer key (from local trust material).
  3. Verify signature.
  4. Validate iss, sub, aud, iat, exp, jti, trust_domain, cnf.
  5. Check expected audience and trust domain.
  6. Check request proof binding through cnf.
  7. Check replay/jti state when enabled.
  8. Return allow or deny with a clear reason.

A passport is accepted only when the caller can prove possession of the key referenced by cnf for 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.

Cross-language conformance

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
ClassRankWhat it means
software10A software-held private key. Proves possession only.
remote_kms20Key bytes stay in a KMS, but may be callable with portable credentials.
hardware_local30Non-exportable local hardware key.
attested_workload40Broker 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-v1 envelope.

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:

ClassBehaviorDeny when stale
realtimeRequires a current bundle viewstale_bundle_fail_closed
boundedAllows known age up to max_staleness_secondsfail-closed after window
offline-okExplicitly allows stale/offline use(never, by design)

Unknown freshness class → bundle_freshness_unknown. Bounded without a positive max age → fail-closed as misconfigured.

Unsigned skeletons are local development artifacts

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.