Skip to content

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)"]

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):

  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.


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.


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-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:

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.


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.