README.md
10.9 KB · 249 lines · markdown Raw
1 # PQC Agent Wallet
2
3 ![PQC Native](https://img.shields.io/badge/PQC-Native-blue)
4 ![ML-KEM-768](https://img.shields.io/badge/ML--KEM--768-FIPS%20203-green)
5 ![ML-DSA-65](https://img.shields.io/badge/ML--DSA--65-FIPS%20204-green)
6 ![AES-256-GCM](https://img.shields.io/badge/AES--256--GCM-FIPS%20197-green)
7 ![License](https://img.shields.io/badge/License-Apache%202.0-orange)
8 ![Version](https://img.shields.io/badge/version-0.1.0-lightgrey)
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