README.md
| 1 | # PQC Agent Wallet |
| 2 | |
| 3 |  |
| 4 |  |
| 5 |  |
| 6 |  |
| 7 |  |
| 8 |  |
| 9 | |
| 10 | **A quantum-resistant credential vault for AI agents.** Stop scattering API keys across `.env` files, `os.environ`, and LangChain memory. This library gives each AI agent a single encrypted `*.wallet` file, unlocked with a passphrase or an **ML-KEM-768** encapsulated key, with credentials encrypted at rest using **AES-256-GCM** and every access signed into a tamper-evident **ML-DSA** audit log. Drop-in integrations for LangChain, AutoGen, and CrewAI. |
| 11 | |
| 12 | ## The Problem |
| 13 | |
| 14 | AI agents need credentials: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, database passwords, OAuth tokens, client certs. Today, those live in: |
| 15 | |
| 16 | - **`.env` files** on developer laptops and production containers (plaintext on disk). |
| 17 | - **Classical secret managers** (Vault, AWS Secrets Manager) that protect data in transit with RSA/ECDSA - breakable by a sufficiently large quantum computer ("harvest now, decrypt later"). |
| 18 | - **Agent memory** (LangChain `ChatOpenAI(api_key=...)`) - held in plaintext in process RAM, accessible to every tool the agent invokes. |
| 19 | |
| 20 | If any of those stores is exfiltrated today and an adversary holds it until a CRQC (cryptographically relevant quantum computer) exists, every classical-crypto-protected secret is retroactively broken. |
| 21 | |
| 22 | ## The Solution |
| 23 | |
| 24 | Each agent gets a local `*.wallet` file: |
| 25 | |
| 26 | - Credentials encrypted with **AES-256-GCM** (FIPS 197 - symmetric, quantum-resistant at 128-bit Grover-adjusted security). |
| 27 | - Unlock key derived either from a passphrase via **PBKDF2-HMAC-SHA256** (600k iterations) or encapsulated to a recipient's **ML-KEM-768** public key (FIPS 203, NIST PQC). |
| 28 | - Wallet file signed with the owner's **ML-DSA-65** key (FIPS 204) - tamper-evident at rest. |
| 29 | - Every `get`, `put`, `delete`, `unlock` operation recorded as a signed entry in an append-only audit log. |
| 30 | |
| 31 | ## Installation |
| 32 | |
| 33 | ```bash |
| 34 | pip install pqc-agent-wallet |
| 35 | ``` |
| 36 | |
| 37 | With LangChain helpers: |
| 38 | |
| 39 | ```bash |
| 40 | pip install "pqc-agent-wallet[langchain]" |
| 41 | ``` |
| 42 | |
| 43 | Development: |
| 44 | |
| 45 | ```bash |
| 46 | pip install -e ".[dev]" |
| 47 | ``` |
| 48 | |
| 49 | ## Quick Start |
| 50 | |
| 51 | ```python |
| 52 | from quantumshield import AgentIdentity |
| 53 | from pqc_agent_wallet import Wallet |
| 54 | |
| 55 | owner = AgentIdentity.create("my-agent") |
| 56 | |
| 57 | # Create + populate |
| 58 | w = Wallet.create_with_passphrase("agent.wallet", "hunter2", owner) |
| 59 | w.put("openai_api_key", "sk-...", service="openai", tags=["prod"]) |
| 60 | w.put("postgres_password", "db-pass", service="postgres", scheme="password") |
| 61 | w.save() |
| 62 | w.lock() |
| 63 | |
| 64 | # Later (same process or another)... |
| 65 | w = Wallet.load("agent.wallet", owner) |
| 66 | w.unlock_with_passphrase("hunter2") |
| 67 | api_key = w.get("openai_api_key") |
| 68 | ``` |
| 69 | |
| 70 | ## Architecture |
| 71 | |
| 72 | ``` |
| 73 | Passphrase Wallet file (*.wallet) |
| 74 | ---------- ---------------------- |
| 75 | | | |
| 76 | | PBKDF2-HMAC-SHA256 | |
| 77 | | (600k iterations) | |
| 78 | | | |
| 79 | v v |
| 80 | 32-byte key ----+ +--> [ML-DSA-65 signature] |
| 81 | | | over canonical |
| 82 | | | payload bytes |
| 83 | v | |
| 84 | [AES-256-GCM] -->| |
| 85 | per-credential | |
| 86 | (random nonce) | |
| 87 | | |
| 88 | +------------------+-----------------+ |
| 89 | | | |
| 90 | v v |
| 91 | encrypted_credentials kdf / kem_encapsulation |
| 92 | { name -> (nonce, ct, meta) } (how to re-derive the key) |
| 93 | | |
| 94 | +--> sealed, authenticated (GCM tag) and individually decryptable |
| 95 | |
| 96 | Every get / put / delete / unlock --> [ML-DSA-signed audit entry] |
| 97 | (actor DID, ts, op, target) |
| 98 | ``` |
| 99 | |
| 100 | For KEM-unlocked wallets, swap the passphrase branch for: |
| 101 | |
| 102 | ``` |
| 103 | Recipient ML-KEM-768 pubkey --> encapsulate() --> (ct, symmetric key) |
| 104 | | |
| 105 | v |
| 106 | same AES-256-GCM path |
| 107 | ``` |
| 108 | |
| 109 | The recipient later runs `decapsulate(ct, their_priv_key)` to recover the symmetric key and unlock. |
| 110 | |
| 111 | ## Cryptography |
| 112 | |
| 113 | | Layer | Primitive | Standard | Notes | |
| 114 | |---|---|---|---| |
| 115 | | Symmetric encryption | AES-256-GCM | FIPS 197 | 12-byte nonce, 16-byte GCM tag | |
| 116 | | Key derivation (passphrase mode) | PBKDF2-HMAC-SHA256 | RFC 8018 | 600,000 iterations (OWASP 2023) | |
| 117 | | Key encapsulation | ML-KEM-768 | FIPS 203 | Via QuantumShield; stub path for dev | |
| 118 | | Signatures (wallet + audit) | ML-DSA-65 | FIPS 204 | Via QuantumShield; Ed25519 fallback | |
| 119 | | Hashing | SHA3-256 | FIPS 202 | For canonical digests before signing | |
| 120 | |
| 121 | AES-256-GCM is already considered quantum-resistant: Grover's algorithm halves the effective security of symmetric ciphers, so AES-256 provides ~128-bit post-quantum security - still well above the practical attack floor. |
| 122 | |
| 123 | ## Threat Model |
| 124 | |
| 125 | | Threat | Mitigation | |
| 126 | |---|---| |
| 127 | | **Quantum decryption of stored secrets** ("harvest now, decrypt later") | Symmetric key never travels over a classical asymmetric channel; only ever PBKDF2-derived or ML-KEM-encapsulated. | |
| 128 | | **Wallet file tampering** (attacker edits encrypted payload) | ML-DSA signature over the entire canonical payload is re-verified on every load; any mutation fails verification. | |
| 129 | | **Wrong passphrase accepted** | Unlock tries to decrypt a stored credential; GCM tag failure surfaces as `InvalidPassphraseError`. | |
| 130 | | **Credential leak via logs** | `Credential.to_safe_dict()` redacts the value. Audit log stores only the credential name, not its value. | |
| 131 | | **Unauthorized access by another agent** | Agents have separate wallets. Per-agent DID embedded in audit entries. | |
| 132 | | **Stale credential abuse** | `rotate()` updates the stored value and `rotated_at` timestamp; downstream policy can reject credentials with stale `rotated_at`. | |
| 133 | | **Offline attacker with a GPU farm** | PBKDF2 at 600k iterations makes brute force expensive; KEM mode removes passphrase brute force entirely. | |
| 134 | |
| 135 | ## Integrations |
| 136 | |
| 137 | ### LangChain (or any "callable that returns a secret" pattern) |
| 138 | |
| 139 | ```python |
| 140 | from pqc_agent_wallet.integrations import make_langchain_secret_provider |
| 141 | |
| 142 | provider = make_langchain_secret_provider(wallet) |
| 143 | # provider("openai_api_key") -> "sk-..." |
| 144 | |
| 145 | # Or bulk-resolve env-style mapping: |
| 146 | from pqc_agent_wallet.integrations import walletize_env |
| 147 | env = walletize_env(wallet, {"OPENAI_API_KEY": "openai_api_key"}) |
| 148 | ``` |
| 149 | |
| 150 | ### AutoGen / CrewAI / anything that reads `os.getenv` |
| 151 | |
| 152 | ```python |
| 153 | from pqc_agent_wallet.integrations import install_env_shim |
| 154 | install_env_shim(wallet) |
| 155 | |
| 156 | # Legacy code that does os.getenv("OPENAI_API_KEY") now transparently |
| 157 | # falls back to wallet.get("openai_api_key") when the env var is unset. |
| 158 | ``` |
| 159 | |
| 160 | ### Per-agent isolation (CrewAI pattern) |
| 161 | |
| 162 | ```python |
| 163 | # Give every agent its own wallet; one agent's compromise doesn't leak others. |
| 164 | researcher_wallet = Wallet.load("researcher.wallet", researcher_identity) |
| 165 | writer_wallet = Wallet.load("writer.wallet", writer_identity) |
| 166 | ``` |
| 167 | |
| 168 | ## API Reference |
| 169 | |
| 170 | ### `Wallet` |
| 171 | |
| 172 | | Method | Description | |
| 173 | |---|---| |
| 174 | | `Wallet.create_with_passphrase(path, passphrase, owner)` | New wallet unlocked via PBKDF2(passphrase). | |
| 175 | | `Wallet.create_with_kem(path, recipient_pubkey, alg, owner)` | New wallet unlocked via ML-KEM decapsulation. | |
| 176 | | `Wallet.load(path, owner)` | Load + verify ML-DSA signature. | |
| 177 | | `unlock_with_passphrase(p)` | Derive and validate the unlock key. | |
| 178 | | `unlock_with_kem_private_key(sk, alg)` | Decapsulate the stored ciphertext. | |
| 179 | | `lock()` | Zero the in-memory key. | |
| 180 | | `put(name, value, service=, description=, scheme=, tags=, expires_at=)` | Add or overwrite a credential. | |
| 181 | | `get(name) -> str` | Retrieve plaintext (unlocked only). | |
| 182 | | `get_credential(name) -> Credential` | Full Credential with metadata. | |
| 183 | | `delete(name)` | Remove a credential. | |
| 184 | | `rotate(name, new_value)` | Overwrite value, preserve created_at, update rotated_at. | |
| 185 | | `list_names() -> list[str]` | Sorted names. | |
| 186 | | `list_metadata() -> list[CredentialMetadata]` | All metadata. | |
| 187 | | `save()` | Sign payload with owner key, write file. | |
| 188 | | `audit` | `WalletAuditLog` instance. | |
| 189 | | `is_unlocked` | Bool property. | |
| 190 | |
| 191 | ### `Credential` / `CredentialMetadata` |
| 192 | |
| 193 | Dataclasses. `CredentialMetadata` fields: `name`, `scheme`, `service`, `description`, `created_at`, `rotated_at`, `expires_at`, `tags`. `Credential.to_safe_dict()` redacts the `value` for logging. |
| 194 | |
| 195 | ### `WalletAuditLog` + `WalletAuditEntry` |
| 196 | |
| 197 | Append-only log with ML-DSA-signed entries. Fields on each entry: `timestamp`, `operation`, `actor_did`, `credential_name`, `success`, `details`, `signer_did`, `algorithm`, `signature`. `log.entries(limit=, operation=, credential_name=)` returns the most recent matching entries. `entry.verify_signature(public_key_hex)` validates the ML-DSA signature. |
| 198 | |
| 199 | ### Exceptions |
| 200 | |
| 201 | | Exception | When | |
| 202 | |---|---| |
| 203 | | `WalletError` | Base class. | |
| 204 | | `WalletLockedError` | Operation requires unlocked wallet. | |
| 205 | | `CredentialNotFoundError` | Name not present. | |
| 206 | | `InvalidPassphraseError` | Passphrase failed GCM auth check. | |
| 207 | | `TamperedWalletError` | Wallet file signature failed verification. | |
| 208 | | `WalletFormatError` | Malformed or wrong-version wallet file. | |
| 209 | |
| 210 | ## Examples |
| 211 | |
| 212 | See the `examples/` directory: |
| 213 | |
| 214 | - **`basic_usage.py`** - create, save, reload, read, audit. |
| 215 | - **`langchain_integration.py`** - `make_langchain_secret_provider` + `walletize_env`. |
| 216 | - **`env_shim_demo.py`** - transparent `os.getenv` fallback to the wallet. |
| 217 | |
| 218 | Run them: |
| 219 | |
| 220 | ```bash |
| 221 | python examples/basic_usage.py |
| 222 | python examples/langchain_integration.py |
| 223 | python examples/env_shim_demo.py |
| 224 | ``` |
| 225 | |
| 226 | ## Why PQC Matters For Credentials |
| 227 | |
| 228 | Credential-protection systems typically rotate on multi-year cadences (annual key rotation is considered aggressive). A credential you encrypt with RSA-2048 today will be sitting on someone's disk - or in a backup bucket, or on a compromised laptop - for the entire decade-long runway to practical cryptanalytic quantum computers. ML-KEM and ML-DSA close that window now, so you never have to re-encrypt the whole corpus in a panic later. Symmetric AES-256-GCM remains safe with classical assumptions; the post-quantum concern is exclusively about how the symmetric key gets to the machine, which is exactly what ML-KEM protects. |
| 229 | |
| 230 | ## Development |
| 231 | |
| 232 | ```bash |
| 233 | pip install -e ".[dev]" |
| 234 | pytest |
| 235 | ruff check src/ tests/ examples/ |
| 236 | ``` |
| 237 | |
| 238 | ## Related |
| 239 | |
| 240 | Part of the [QuantaMrkt](https://quantamrkt.com) post-quantum tooling registry. See also: |
| 241 | |
| 242 | - **QuantumShield** - the underlying PQC toolkit (`AgentIdentity`, ML-DSA / ML-KEM primitives). |
| 243 | - **PQC RAG Signing** - sister tool for sealing RAG pipeline chunks with ML-DSA. |
| 244 | - **PQC MCP Transport** - sister tool for signing Model Context Protocol JSON-RPC messages. |
| 245 | |
| 246 | ## License |
| 247 | |
| 248 | Apache License 2.0. See [LICENSE](LICENSE). |
| 249 | |