Raxol.AgentClientProtocol.Ext.Capability.Token (Raxol Agent Client Protocol v0.1.0)

Copy Markdown View Source

The RXC1 capability token — offline-verifiable detached Ed25519 signature.

The headline capability: an attach to a writerless session (a tar'd journal directory, no live authority, no network, no issuer reachable) is authorized cryptographically from the token bytes + a shipped public key ALONE.

Encoding — version-pinned, no alg field anywhere

token         := "RXC1." <> b64url_nopad(claims_json) <> "." <> b64url_nopad(sig)
signing_input := "RXC1." <> b64url_nopad(claims_json)      # prefix INCLUDED in signed bytes
sig           := Ed25519-sign(issuer_priv, signing_input)  # 64 bytes, detached

Encoding rules, each load-bearing:

  • No header, no alg. The literal prefix RXC1 is the algorithm binding (RXC1 ⟺ Ed25519). The JWT alg:none / RS256→HS256 confusion is structurally UNEXPRESSIBLE — no code path reads an algorithm name.
  • The prefix is inside the signed bytes, so a signature minted for a hypothetical RXC2 can never be replayed under RXC1 (downgrade).
  • The signature covers the encoded blob, not re-canonicalized JSON. The verifier base64url-decodes and JSON-decodes but NEVER re-encodes — there is no canonicalization step, hence no malleability class (key ordering, unicode escaping, duplicate keys are all bound by the signed bytes).
  • Strict base64url, padding: false. Any =/+///whitespace/ non-alphabet byte ⇒ decode failure ⇒ deny. Exactly three dot-separated non-empty segments — one token has exactly one valid byte form.

Verification is TOTAL

verify/4 is a pure function: same inputs ⇒ same output; no network, no process dictionary, no clock read (now is a parameter), no filesystem. For ALL binaries it returns {:ok, claims} | {:error, atom} — never raises, never creates an atom. Reason atoms are internal (telemetry only); the wire sees a single bare deny (anti-oracle).

Summary

Types

Verifier keyring: kid (binary) → 32-byte Ed25519 public key.

Functions

The iat clock-skew tolerance (seconds). exp has none.

The restrictive claim keys this verifier IMPLEMENTS (enforce-if-present).

Verify a token against a keyring, wall-clock now (unix seconds), and the expected session id. Total: every failure is {:error, atom}, never a raise.

Types

keyring()

@type keyring() :: %{optional(String.t()) => binary()}

Verifier keyring: kid (binary) → 32-byte Ed25519 public key.

Functions

clock_skew_s()

@spec clock_skew_s() :: non_neg_integer()

The iat clock-skew tolerance (seconds). exp has none.

restrictive_implemented()

@spec restrictive_implemented() :: [String.t()]

The restrictive claim keys this verifier IMPLEMENTS (enforce-if-present).

verify(token, keyring, now_unix, expected_session_id)

@spec verify(term(), keyring(), integer(), String.t()) ::
  {:ok, map()} | {:error, atom()}

Verify a token against a keyring, wall-clock now (unix seconds), and the expected session id. Total: every failure is {:error, atom}, never a raise.