Transport-layer command authentication for AI agents.
CRAFT proves who issued a command, when it was issued, and that nothing changed it in transit. It turns "we have logs" into "we have evidence."
It is a standalone Python library with zero dependencies — pure stdlib, installs in under a second. Use it independently or as the transport layer for UNWIND.
Requires Python 3.9+.
Standalone library extracted from the UNWIND monorepo (250+ commits, 1,859 tests). Full development history lives there.
pip install craft-authSee it in action (zero config, nothing modified):
pip install craft-auth
craft-auth demoRuns 4 scenarios in your terminal: valid envelope verification, tamper detection, hash chain integrity, and scoped capability tokens. Add --json for structured output.
from craft import derive_session_keys, state_commit_0, CraftVerifier, CraftSessionState
# Derive session keys from shared secret
keys = derive_session_keys(
ikm=shared_secret,
salt0=session_salt,
ctx=b"myapp/agent/v1",
epoch=0,
server_secret=server_secret,
)
# Create session and verifier
session = CraftSessionState.from_session_keys(
session_id="sess_01", account_id="acct_01",
channel_id="ch_01", conversation_id="conv_01",
context_type="agent", epoch=0, keys=keys, ctx=b"myapp/agent/v1",
)
session.last_state_commit["c2p"] = state_commit_0(keys.c2p.k_state, b"myapp/agent/v1")
session.last_state_commit["p2c"] = state_commit_0(keys.p2c.k_state, b"myapp/agent/v1")
verifier = CraftVerifier()
result = verifier.verify_or_hold(envelope, session)
# result.accepted / result.error / result.held / result.drainedThe Quick Start shows the verifier side. If you are building the client shim — the code that produces authenticated envelopes — here is the complete flow:
import os
import time
from craft import (
derive_session_keys, state_commit_0, CraftSessionState,
CraftVerifier, mac_input_bytes, hmac_sha256, b64url_encode,
)
# 1. Derive keys (both sides must derive the same keys from the same secret)
keys = derive_session_keys(
ikm=shared_secret,
salt0=session_salt,
ctx=b"myapp/agent/v1",
epoch=0,
server_secret=server_secret,
)
# 2. Create session state
session = CraftSessionState.from_session_keys(
session_id="sess_01", account_id="acct_01",
channel_id="ch_01", conversation_id="conv_01",
context_type="agent", epoch=0, keys=keys, ctx=b"myapp/agent/v1",
)
session.last_state_commit["c2p"] = state_commit_0(keys.c2p.k_state, b"myapp/agent/v1")
session.last_state_commit["p2c"] = state_commit_0(keys.p2c.k_state, b"myapp/agent/v1")
# 3. Build the envelope fields (everything except mac and state_commit)
envelope = {
"v": "4.2",
"epoch": 0,
"session_id": "sess_01",
"account_id": "acct_01",
"channel_id": "ch_01",
"conversation_id": "conv_01",
"context_type": "agent",
"seq": 1,
"ts_ms": int(time.time() * 1000),
"msg_type": "tool_call",
"direction": "c2p",
"payload": {"tool": "fs_read", "args": {"path": "/tmp/data.txt"}},
}
# 4. Compute the MAC over the canonical encoding of the fields
mac = hmac_sha256(keys.c2p.k_msg, mac_input_bytes(envelope))
# 5. Compute the state commitment (this extends the hash chain)
# Formula: commit_n = HMAC(k_state, commit_{n-1} || mac_n)
prev_commit = session.last_state_commit["c2p"]
state_commit = hmac_sha256(keys.c2p.k_state, prev_commit + mac)
# 6. Attach both to the envelope (base64url encoded)
envelope["mac"] = b64url_encode(mac)
envelope["state_commit"] = b64url_encode(state_commit)
# 7. Send to verifier — this is what the proxy receives
verifier = CraftVerifier()
result = verifier.verify_or_hold(envelope, session)
assert result.accepted # True — envelope is authentic and in sequenceThe state commitment formula is the critical piece: each envelope's commit chains to the previous one via HMAC(k_state, prev_commit || mac). This is what makes the chain tamper-evident — modifying or removing any envelope breaks every subsequent commitment.
For privileged operations, issue a short-lived token that binds a specific tool call to an authenticated session:
from craft import CapabilityIssuer, ToolCall, b64url_encode
# Create issuer using the session's capability keys
issuer = CapabilityIssuer(cap_keys_by_epoch=session.cap_keys_by_epoch)
# Mint a token scoped to a specific tool and target
token = issuer.mint_capability(
session=session,
subject=session.account_id,
allowed_tools=["fs_write"],
arg_constraints={"exact": {"path": "/tmp/config.json"}},
target_constraints={"type": "exact", "value": "/tmp/config.json"},
bind_seq=1,
state_commit_at_issue=b64url_encode(session.last_state_commit["c2p"]),
purpose="write config file",
)
# Enforce at dispatch — does the token authorize this call?
tool_call = ToolCall(
session_id=session.session_id,
account_id=session.account_id,
channel_id=session.channel_id,
conversation_id=session.conversation_id,
context_type=session.context_type,
subject=session.account_id,
seq=1,
direction="c2p",
tool_id="fs_write",
args={"path": "/tmp/config.json"},
target="/tmp/config.json",
)
decision = issuer.enforce_at_tool_dispatch(
token=token, tool_call=tool_call, session=session,
)
assert decision.allowed # True — token is valid and scoped correctlyTokens are single-use by default, HMAC-authenticated, epoch-bound, and expire after 60 seconds.
CRAFT wraps every command in an HMAC envelope that cryptographically binds it to a session, a sequence number, and the full history of prior commands. The verifier checks each envelope before admitting it into your agent's execution pipeline.
Session setup derives directional key pairs from a shared secret:
- k_msg (c2p, p2c) — HMAC-SHA256 envelope authentication
- k_state (c2p, p2c) — Hash chain state commitments
- k_resync (c2p, p2c) — Resynchronisation challenge-response
- k_cap_srv — Capability token issuance (server-side only)
Keys are directional: a valid client-to-proxy MAC cannot be replayed in the proxy-to-client direction.
Every message carries a MAC computed over a canonical JSON encoding of its fields (JCS-like deterministic serialisation). The verifier recomputes the MAC and rejects any envelope where the content was modified in transit.
Envelopes must arrive in sequence order. Out-of-order envelopes are held in a bounded queue (default 32 slots, 5s timeout) and drained automatically when the gap is filled. Replays are rejected via a sliding bitmap window.
Each accepted envelope extends a running hash chain: commit_n = HMAC(k_state, commit_{n-1} || mac_n). Both sides maintain the chain independently. If the chains diverge, the session is in a provably inconsistent state — either something was tampered with, or messages were lost.
For privileged operations, CRAFT issues short-lived capability tokens that bind a specific tool call to an authenticated user intent:
- Scoped to session, epoch, tool, target, and arguments
- HMAC-authenticated by a server-side key the client never sees
- Single-use or bounded-use with TTL
- Chainable lineage (child tokens must reference a used parent)
- Transcript-consistent (bound to the state commit at time of issue)
Sessions rekey by deriving fresh key material from the current PRK and both directional state commitments. Old keys are dropped. A time-bounded grace window allows in-flight capability tokens from the previous epoch to land.
When a connection drops mid-session, the client can resync by replaying missed envelopes in a challenge-response protocol. The verifier walks the chain forward, validates every link, and rekeys on success. Rate-limited with exponential backoff. Bounds-exceeded terminates the session.
Each CRAFT envelope carries a where field — cryptographic proof of which machine and geographic location issued it. This opens up bespoke chain topologies for enterprise deployments:
- Separate chains per geographic region for data residency compliance
- Per-device audit trails (which laptop, which server, which edge node)
- Cross-site chain verification without centralised trust
Chains carry schema version metadata at the head, enabling:
- Chain identification and versioning across deployments
- Migration between protocol versions without breaking verification
- Multi-tenant environments where chains from different applications coexist
CRAFT is a transport-layer protocol. It authenticates the command stream, not the content.
- Not prompt injection defence. CRAFT does not analyse or filter what the agent says or does. That is the job of content-layer enforcement (like UNWIND's pipeline).
- Not tool output trust. CRAFT does not make model outputs or tool results trustworthy.
- Not semantic intent validation. A cryptographically authenticated command can still be unwise.
- Not host compromise immunity. If the machine is fully compromised, software-only controls are insufficient.
CRAFT is one layer in a defence-in-depth stack. It narrows the attack surface by ensuring that the commands entering your pipeline are authentic, ordered, and untampered — so your policy engine can focus on whether the action should be allowed, not whether the request is genuine.
| Symbol | Purpose |
|---|---|
hmac_sha256(key, msg) |
HMAC-SHA256 — used to compute envelope MACs and state commitments |
hkdf_extract(salt, ikm) |
HKDF extract step (RFC 5869) |
hkdf_expand(prk, info, length) |
HKDF expand step |
derive_session_keys(...) |
Full session key bundle from shared secret |
derive_keys_from_prk(...) |
Key bundle from existing PRK (used by rekey) |
derive_rekey_prk(...) |
Derive next-epoch PRK from current state |
state_commit_0(k_state, ctx) |
Initial state commitment for a direction |
b64url_encode(raw) |
URL-safe base64 encoding (no padding) |
b64url_decode(value) |
URL-safe base64 decoding |
| Symbol | Purpose |
|---|---|
canonicalize_for_mac(envelope) |
Build MAC input object (removes mac and state_commit) |
mac_input_bytes(envelope) |
UTF-8 bytes of canonical MAC input |
| Symbol | Purpose |
|---|---|
CraftVerifier |
Stateless verifier — call verify_or_hold() or verify_and_admit() |
CraftSessionState |
Mutable session state (keys, sequences, replay bitmap, hold queue) |
VerifyResult |
Result of verification: accepted, error, held, drained |
VerifyError |
Error enum: ERR_ENVELOPE_INVALID, ERR_REPLAY, ERR_STATE_DIVERGED, ERR_CONTEXT_MISMATCH, ERR_EPOCH_STALE |
| Symbol | Purpose |
|---|---|
CapabilityIssuer |
Mint, revoke, and enforce capability tokens |
CapabilityToken |
Frozen dataclass: cap_id, claims, cap_mac |
CapabilityDecision |
Enforcement result: allowed, error, subcode |
CapabilityError |
Error enum: ERR_CAP_REQUIRED, ERR_CAP_INVALID |
CapabilitySubcode |
Detailed failure: CAP_EXPIRED, CAP_REVOKED, CAP_EPOCH_MISMATCH, etc. |
ToolCall |
Frozen dataclass binding a tool invocation to session context |
StepUpChallenge |
Challenge for human-in-the-loop step-up authentication |
| Symbol | Purpose |
|---|---|
CraftLifecycleManager |
Rekey, resync, session expiry, and teardown |
RekeyPrepare |
Rekey boundary markers (epoch + sequence boundaries) |
ResyncChallenge |
Server-issued resync challenge with nonce and MAC |
ResyncResult |
Resync outcome: ok, error, new_epoch |
ResyncError |
Error enum: rate limit, bounds, challenge invalid, proof invalid, state diverged |
| Symbol | Purpose |
|---|---|
CraftStateStore |
Atomic JSON snapshot persistence for session state and tombstones |
| Attack | How |
|---|---|
| Replay | Strict monotonic sequence numbers + sliding replay bitmap |
| Tampering | HMAC-SHA256 over canonical envelope — any modification invalidates the MAC |
| Spoofing | Directional keys — client and proxy keys are distinct, cross-direction replay fails |
| Session hijack | Context binding — envelope must match session, account, channel, conversation, and context type |
| Confused deputy | Capability tokens — privileged tool calls require a server-issued, scope-bound, time-limited token |
| Chain break | State commitments — each envelope extends a running hash chain; gaps are detectable and provable |
| Epoch downgrade | Stale epoch envelopes rejected; grace windows are time-bounded and cover only the previous epoch |
| Attack | Why | What does |
|---|---|---|
| Prompt injection | Content-layer attack, not transport-layer | Content filtering, UNWIND pipeline |
| Malicious tool output | Trust is about the command source, not the result | Output validation, sandboxing |
| Host compromise | Software-only protocol, not hardware attestation | OS-level security, HSMs |
| Social engineering | Authenticated user can still issue bad commands | Policy enforcement, human review |
Most agent frameworks log what happened. Logs are mutable, unsigned, and trivially forgeable after the fact.
CRAFT produces a hash chain where every entry is cryptographically bound to the one before it. You cannot insert, remove, or modify an entry without breaking the chain. This is the difference between "our logs say it was fine" and "here is a cryptographic proof that every command was authentic and in order."
For compliance, incident response, and forensic audit, that distinction matters.
CRAFT is fully standalone. It has zero imports from UNWIND and zero external dependencies.
Inside UNWIND, CRAFT serves as the transport authentication layer — the first thing that touches an incoming command before it reaches the 15-stage enforcement pipeline. But you can use CRAFT without UNWIND in any agent framework, MCP server, or tool-calling system that needs command provenance.
| CRAFT standalone | CRAFT + UNWIND | |
|---|---|---|
| Command authentication | ✅ | ✅ |
| Tamper-evident audit chain | ✅ | ✅ |
| Capability tokens | ✅ | ✅ |
| Policy enforcement | — | 15-stage pipeline |
| File rollback | — | Smart snapshots |
| Trust dashboard | — | Web UI |
| Ghost Mode (dry-run) | — | Write interception + shadow VFS |
CRAFT uses only Python 3.9+ stdlib: hashlib, hmac, json, base64, os, time, dataclasses, pathlib, enum, socket, re, tempfile. No cryptography library. No C extensions. Installs in under a second.
AGPL-3.0-or-later