Arc 22 D96 defines how sandboxed operators regenerate the same Lithtrix passport keypair after every environment reset — without Lithtrix ever receiving the master seed or private key.Documentation Index
Fetch the complete documentation index at: https://docs.lithtrix.ai/llms.txt
Use this file to discover all available pages before exploring further.
Normative algorithm
Given:master_seed_bytes— raw byte string used as the HMAC key (see Master seed encoding).agent_id— canonical lowercase UUID string (same suffix asdid:lithtrix:{agent_id}).
msg = utf8("lithtrix.passport.v1") || utf8(agent_id)seed_bytes = HMAC-SHA512(key=master_seed_bytes, msg=msg)ed25519_seed = seed_bytes[0:32](32-byte Ed25519 seed-from-bytes)- Derive Ed25519 keypair from
ed25519_seedusing standard Ed25519 (Ed25519PrivateKey.from_private_bytesin Python; PKCS#8 / SPKI PEM as Arc 21 register format).
(master_seed_bytes, agent_id) always yields identical public PEM (and identical private PEM). Different agent_id values yield unrelated keypairs even with the same master seed.
PEM encoding (Arc 21 register format)
| Material | Encoding |
|---|---|
| Private key | PKCS#8 PEM (-----BEGIN PRIVATE KEY-----) |
| Public key | SPKI PEM (-----BEGIN PUBLIC KEY-----) |
passport_public_key on POST /v1/register. Lithtrix stores public material only; the 201 response omits private_key when operator-derived.
Master seed encoding
Normative operator input: UTF-8 passphrase —master_seed_bytes = passphrase.encode("utf-8").
Test vectors and CLI verification: hex-decoded bytes (even length, no spaces) via --master-seed-hex.
Lithtrix never accepts master_seed on any HTTP route. Derivation is client-side only.
Reference implementations (MIT)
| Language | Path |
|---|---|
| Python | lithtrix-api/scripts/derive_passport.py (imports app.services.passport_derivation) |
| JavaScript | lithtrix-mcp/lib/derive-passport.js |
| MCP (local) | lithtrix_passport_derive in lithtrix-mcp 0.13.0+ — reads LITHTRIX_PASSPORT_MASTER_SEED from env; never POSTs seed to the API |
lithtrix-api/tests/fixtures/passport_derivation_vectors.json.
Register with operator-derived public key
passport_public_key is omitted, behavior is unchanged: Lithtrix generates the keypair server-side and returns private_key once in the 201 body.
When present:
- Response
passport.private_keyisnullwithderivation_method:"operator_derived". agents.passport_derivation_methodis set tooperator_derived(observability).- A public key already bound to another agent returns 409
PASSPORT_PUBLIC_KEY_ALREADY_BOUND.
Operating safely with master seeds
Treat the master seed like a root signing key for every passport you derive from it:- Prefer HSM, KMS, or a sealed secret store over plaintext env vars in production.
- Rotate the master seed on compromise or personnel change; re-derive and re-register (or rotate) affected passports.
- Wrap seeds at rest; never commit seeds to git or paste them into Lithtrix support channels.
Threat model — master seed compromise (pre-mortem #4)
If an attacker obtains your master seed, they can derive every passport private key for everyagent_id you ever registered with keys derived from that seed — until you rotate the seed and replace the public keys on Lithtrix.
Lithtrix cannot revoke operator-derived private material it never held. Compromise blast radius is full forgeability of challenge signatures for all derived agents under that seed. Do not downplay this: use strong seed generation, least-privilege storage, and rotation playbooks.
Operator-held PEM injection (legacy interim pattern in Passports § sandboxed agents) remains valid but deterministic derivation + passport_public_key on register is the preferred path after iter 86.
Related docs
- Passports — challenge sessions, capability split, MCP tools
- Passport migration — bearer vs session,
passport_present - Public overview: https://lithtrix.ai/passports.html