iqcc-shared 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ __pycache__
2
+ .venv
3
+ .mypy_cache
4
+ simulator.tar
5
+ /deployment/certs*
6
+ dist
7
+ /deployment_qolab
8
+ /test/with_qbridge
9
+ *_qolab*
10
+ .ruff_cache
11
+ /test/demo
12
+ playground
13
+ /test/with_qbridge
14
+ /test/with_compiler
15
+ *.ipynb
16
+ **/key_config.yaml
17
+ *.pdf
18
+ *.tar
19
+ iqcc-cloud-client/test
20
+ .coverage
21
+ .pytest_cache
22
+ *.jsonl
23
+ development_log/
24
+ *.whl
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: iqcc-shared
3
+ Version: 0.1.0
4
+ Summary: Hybrid cryptographic utilities: ML-KEM-768 key encapsulation, ML-DSA-65 signing, AES-256-GCM payload encryption, and msgpack serialization
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: cryptography>=48.0.0
7
+ Requires-Dist: msgpack>=1.0.8
@@ -0,0 +1,248 @@
1
+ # `iqcc-shared` - Symmetric Encryption with Post-Quantum Key Exchange
2
+
3
+ ## Overview
4
+ `iqcc-shared` is a lightweight, shared Python module providing hybrid encryption and automated key rotation utilities. It is used by both the `iqcc-cloud-client` SDK and the `worker` process to securely exchange payloads while ensuring confidentiality and per-field integrity via AES-256-GCM, with post-quantum ML-KEM-768 key encapsulation for session key exchange.
5
+
6
+ All data serialization uses **[msgpack](https://msgpack.org/)** instead of JSON - providing compact binary encoding, native binary field support, and zero base64 overhead. The sole exception is public key exchange, where ML-KEM-768 public keys are shared as JSON strings to allow format-independent distribution.
7
+
8
+ ## Architecture & Data Flow
9
+
10
+ ```text
11
+ [Worker] [Storage] [Client SDK]
12
+ │ │ │
13
+ │ 1. Publishes Public Key Record │ │
14
+ │ (current + previous keys) │ │
15
+ │─────────────────────────────────►│ │
16
+ │ │ 2. Fetches Public Key Record │
17
+ │ │◄─────────────────────────────│
18
+ │ │ 3. Generates AES-256-GCM │
19
+ │ │ session key │
20
+ │ │ 4. Encrypts target paths │
21
+ │ │ in payload │
22
+ │ │ 5. Encapsulates session key │
23
+ │ │ with worker public key │
24
+ │ │ 6. msgpack-serializes │
25
+ │ │ full message │
26
+ │ 7. Receives msgpack message │ │
27
+ │◄─────────────────────────────────│ │
28
+ │ 8. Decapsulates session key │ │
29
+ │ with worker private key │ │
30
+ │ 9. Decrypts target paths │ │
31
+ │ (AES-GCM auth tag verified) │ │
32
+ │ 10. Processes payload │ │
33
+ │ 11. Encrypts results with │ │
34
+ │ same session key (symmetric) │ │
35
+ │─────────────────────────────────►│ │
36
+ │ │ 12. Decrypts with session │
37
+ │ │ key (already known) │
38
+ ```
39
+
40
+ ## Cryptographic Design
41
+ | Component | Algorithm | Purpose |
42
+ |-----------|-----------|---------|
43
+ | **Asymmetric Key Encapsulation** | ML-KEM-768 (post-quantum) | Protects the symmetric session key during transit via key encapsulation |
44
+ | **Symmetric Payload Encryption** | AES-256-GCM | Encrypts target payload fields; provides confidentiality + integrity via per-field authentication tags |
45
+ | **Key Derivation/Nonce** | Cryptographically secure RNG (`os.urandom`) | Generates unique GCM nonces per operation |
46
+ | **Serialization** | msgpack (strict mode) | Compact binary format with native binary fields; no base64 needed |
47
+
48
+ > **Integrity model:** Each encrypted field carries its own AES-GCM authentication tag. On decryption, the tag is verified automatically - any tampering with ciphertext, nonce, or tag causes decryption to fail. Because the client and worker are in a trusted relationship (authenticated via API tokens), separate digital signatures are not needed.
49
+
50
+ ## Msgpack Security Hardening
51
+ msgpack's default settings allow untrusted input to allocate arbitrary memory. This module enforces strict decode parameters to reduce attack surface:
52
+
53
+ | Parameter | Value | Rationale |
54
+ |-----------|-------|-----------|
55
+ | `strict_map_key` | `True` | Rejects non-str/non-binary map keys; prevents deserialization confusion |
56
+ | `max_str_len` | 20 MiB | Limits string field size; prevents memory exhaustion from oversized strings |
57
+ | `max_bin_len` | 20 MiB | Limits binary field size; prevents memory exhaustion from oversized binary data |
58
+ | `max_array_len` | 500,000 | Limits array nesting depth; prevents stack overflow from deeply nested arrays |
59
+ | `max_map_len` | 100,000 | Limits map entries; prevents hash-flooding and memory exhaustion |
60
+ | `max_ext_len` | 1,000 | Limits extension type usage; extension types are not used but guarded against |
61
+ | `raw` | `False` | Auto-decodes strings to `str`; prevents raw `bytes` leakage into app logic |
62
+
63
+ These limits are configurable via module-level constants but are **enforced by default** on all public API entry points that accept serialized input.
64
+
65
+ ## Key Management & Rotation
66
+ The worker maintains a msgpack-serializable key record containing two public keys: `current` (actively used) and `previous` (overlap window for rotation). Each key entry includes a `key_id`, `public_key` (JSON-encoded string), and `expires_at` (ISO-8601 string). Public keys are stored as strings so they can be independently exchanged as JSON without msgpack coupling - other payloads remain msgpack-encoded as described below.
67
+
68
+ ### Record Structure
69
+ ```python
70
+ {
71
+ "current": {
72
+ "key_id": "uuid-v4",
73
+ "public_key": "{...}", # ML-KEM-768 public key as JSON string
74
+ "public_key_format": "ml-kem-768",
75
+ "expires_at": "2025-12-31T23:59:59Z"
76
+ },
77
+ "previous": {
78
+ "key_id": "uuid-v4",
79
+ "public_key": "{...}", # ML-KEM-768 public key as JSON string
80
+ "public_key_format": "ml-kem-768",
81
+ "expires_at": "2025-06-30T23:59:59Z"
82
+ },
83
+ "rotation_policy": "auto",
84
+ "updated_at": "2025-01-15T10:00:00Z"
85
+ }
86
+ ```
87
+
88
+ ## Core API Specification
89
+
90
+ ### 1. Key Record Creation & Rotation
91
+ **Used by:** Worker
92
+ **Signature:**
93
+ ```python
94
+ def create_or_rotate_key_record(
95
+ existing_record: Optional[dict] = None,
96
+ validity_days: int = 180,
97
+ overlap_days: int = 30
98
+ ) -> tuple[dict, str | None]
99
+ ```
100
+ **Behavior:**
101
+ - If `existing_record` is `None`, generates a fresh `current` key and returns a new record paired with the corresponding ML-KEM-768 private key as a hex-encoded string.
102
+ - If `existing_record` exists and `current.expires_at` is within the rotation window, generates a new key, shifts `current → previous`, updates expiry timestamps, and returns the rotated record paired with the new private key hex string.
103
+ - If no rotation is needed, returns the existing record as-is with `None` for the private key.
104
+ - Generates ML-KEM-768 key pairs using `cryptography.hazmat.primitives.asymmetric.mlkem.MLKEM768PrivateKey.generate()`.
105
+ - Returns a tuple `(key_record, private_key_hex)` where `key_record` is a fully valid dict ready for publishing (`public_key` fields are JSON-encoded strings) and `private_key_hex` is a hex-encoded string (128 hex chars) of the ML-KEM-768 private key suitable for storage in Kubernetes secrets, or `None` if no new key was generated.
106
+
107
+ ---
108
+
109
+ ### 2. Encrypt Payload
110
+ **Used by:** Client SDK (request) and Worker (response)
111
+ **Signature:**
112
+ ```python
113
+ def encrypt(
114
+ payload: dict,
115
+ paths: list[str],
116
+ target_public_key: PublicKey | None = None,
117
+ target_key_id: str = "",
118
+ session_key: bytes | None = None,
119
+ ) -> tuple[bytes, bytes]
120
+ ```
121
+ **Behavior:**
122
+ - Recursively traverses `payload` using dot-notation `paths` (supports `.*` wildcards for child matching).
123
+ - **Generates the AES-256-GCM session key:**
124
+ - If `session_key` is provided, uses it directly (key reuse across request/response).
125
+ - If `target_public_key` is provided and `session_key` is `None`: encapsulates via ML-KEM-768 `public_key.encapsulate()`, the returned shared secret becomes the session key.
126
+ - If both are `None`: generates a fresh 32-byte key via `os.urandom(AES_KEY_SIZE)`.
127
+ - For each matched value, encrypts using AES-256-GCM with the session key. The ciphertext, nonce, and authentication tag are stored as **native msgpack binary fields, no base64**.
128
+ - **If `target_public_key` was provided:** the encapsulation ciphertext is recorded as `encapsulated_session_key` in metadata. `target_key_id` is also recorded.
129
+ - **If `target_public_key` was `None`:** no key encapsulation occurs. The fields `encapsulated_session_key`, `target_public_key_id`, and `key_encapsulation_algorithm` are omitted from metadata. This is the *worker response* path, where the client already holds the session key from the initial request.
130
+ - Builds the full message dict with `encryption_metadata` and the modified `payload`.
131
+ - **Serializes the message to raw msgpack bytes** using strict encoder settings.
132
+ - Returns a tuple `(message_bytes, session_key)` where `message_bytes` is the final msgpack-serialized bytes ready for transmission, and `session_key` is the 32-byte AES key used (useful when no encapsulation occurred and the caller needs to store it for the response path).
133
+
134
+ ---
135
+
136
+ ### 3. Decrypt Payload
137
+ **Used by:** Worker (decrypts initial client payload) **and** Client SDK (decrypts worker results)
138
+ **Signature:**
139
+ ```python
140
+ def decrypt(
141
+ message_bytes: bytes,
142
+ paths: list[str],
143
+ server_private_key: Optional[PrivateKey | str] = None,
144
+ session_key: Optional[bytes] = None,
145
+ max_msgpack_len: int = MSGPACK_DEFAULT_MAX_SIZE # 20 MiB default
146
+ ) -> dict
147
+ ```
148
+ **Behavior:**
149
+ - **Validates message size** against `max_msgpack_len` before any deserialization (prevents memory exhaustion).
150
+ - **Deserializes msgpack with strict security limits** (`strict_map_key=True`, bounded lengths).
151
+ - Extracts `encryption_metadata`.
152
+ - Resolves the AES-256-GCM session key using one of two methods:
153
+ - **`session_key` provided directly:** Uses it as-is. Typical for the **Client SDK decrypting worker results** (the worker replies without encapsulation because the client already holds the session key).
154
+ - **`server_private_key` provided and message contains encapsulation:** Decapsulates via `server_private_key.decapsulate(ciphertext)`. Typical for the **Worker decrypting the initial client payload**. `server_private_key` accepts either an `MLKEM768PrivateKey` object or a hex-encoded string (as returned by `create_or_rotate_key_record`).
155
+ - Recursively traverses the payload using `paths` and decrypts matched values. Each field's AES-GCM authentication tag is verified automatically - tampered fields raise an exception.
156
+ - Returns the fully decrypted `payload` dict ready for processing.
157
+
158
+ ## Message Format (msgpack-serialized dict)
159
+ The wire format is a msgpack-encoded dict with this logical structure:
160
+
161
+ ```python
162
+ {
163
+ b"encryption_metadata": {
164
+ b"version": b"1.0",
165
+ b"key_encapsulation_algorithm": b"ML-KEM-768", # omitted when no encapsulation used
166
+ b"payload_encryption_algorithm": b"AES-256-GCM",
167
+ b"target_public_key_id": b"uuid-v4", # omitted when target_public_key is None
168
+ b"encapsulated_session_key": <bytes>, # ML-KEM-768 encapsulation ciphertext, omitted when no encapsulation
169
+ b"encrypted_paths": [b"auth.token", b"data.sensitive.*"],
170
+ b"timestamp": b"ISO-8601-timestamp"
171
+ },
172
+ b"payload": {
173
+ b"auth": {
174
+ b"token": {
175
+ b"encrypted": <bytes>, # AES-256-GCM ciphertext
176
+ b"nonce": <bytes>, # GCM nonce (native binary)
177
+ b"tag": <bytes> # GCM auth tag (native binary)
178
+ }
179
+ },
180
+ b"data": {
181
+ b"public_info": b"unencrypted-string",
182
+ b"sensitive": {
183
+ b"field1": {
184
+ b"encrypted": <bytes>,
185
+ b"nonce": <bytes>,
186
+ b"tag": <bytes>
187
+ },
188
+ b"field2": {
189
+ b"encrypted": <bytes>,
190
+ b"nonce": <bytes>,
191
+ b"tag": <bytes>
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ > All `bytes` fields above are stored as **native msgpack binary (bin 8/16/32)** - no base64 encoding/decoding overhead. When `target_public_key` is `None` (no encapsulation), `key_encapsulation_algorithm`, `target_public_key_id`, and `encapsulated_session_key` fields are **omitted** from `encryption_metadata`.
200
+
201
+ ## Implementation Roadmap
202
+ 1. **Scaffold Module**
203
+ - Create `iqcc-shared/pyproject.toml` with dependencies: `cryptography>=46.0.5`, `msgpack>=1.0.8`
204
+ - Create `src/iqcc_shared/__init__.py`, `src/iqcc_shared/crypto.py`, and `src/iqcc_shared/msgpack_utils.py`
205
+ 2. **Msgpack Serialization Layer**
206
+ - Define strict encode/decode constants and reusable helper functions in `msgpack_utils.py`
207
+ - Add size validation guards before all deserialization calls
208
+ 3. **Key Management**
209
+ - Implement ML-KEM-768 key generation; serialize public keys to JSON strings, private keys to hex strings
210
+ - Build `create_or_rotate_key_record()` with expiry/overlap logic
211
+ - Public keys stored as JSON strings for independent exchange; private keys hex-encoded for Kubernetes secrets compatibility
212
+ 4. **Path Traversal & Encryption/Decryption**
213
+ - Implement dot-notation path resolver with `.*` wildcard support
214
+ - Wrap AES-256-GCM operations (encrypt/decrypt with nonce & tag handling)
215
+ 5. **Hybrid Key Encapsulation & Encryption**
216
+ - Implement ML-KEM-768 session key encapsulation/decapsulation
217
+ - Assemble `encrypt()` and `decrypt()`
218
+ 6. **Testing**
219
+ - Unit tests for key rotation windows
220
+ - Integration tests for round-trip encrypt → verify → decrypt
221
+ - Path matching edge cases (nested dicts, lists, wildcards)
222
+ - **Msgpack security tests:** oversized payloads, invalid map keys, deeply nested structures
223
+ 7. **Wire into Client & Worker**
224
+ - Add path dependency to `webserver/pyproject.toml` and `iqcc-cloud-client/pyproject.toml`
225
+ - Import `from iqcc_shared.crypto import ...` in respective modules
226
+
227
+ ## Security Best Practices
228
+ - ✅ **Post-quantum key encapsulation:** ML-KEM-768 provides quantum-resistant key encapsulation, protecting against future quantum computing attacks.
229
+ - ✅ **Per-field integrity:** Each encrypted field carries an AES-GCM authentication tag, verified automatically on decryption. Tampered ciphertext or nonce causes immediate failure.
230
+ - ✅ **Unique encapsulation:** ML-KEM-768 `encapsulate()` generates fresh randomness per call; never reuse ciphertexts with the same public key.
231
+ - ✅ **Unique nonces:** AES-GCM nonces are generated per-field using CSPRNG; never reuse nonces with the same key.
232
+ - ✅ **Key isolation:** Session keys are single-use per payload exchange; results reuse the same key only within the same session lifecycle.
233
+ - ✅ **Rotation overlap:** `previous` key remains valid for `overlap_days` to ensure clients can finish in-flight requests during rotation.
234
+ - ✅ **Secure key storage:** ML-KEM private keys must be loaded from secure volumes/HSMs; never committed to version control.
235
+ - ✅ **Strict msgpack decoding:** All public-facing deserialization enforces `strict_map_key=True`, bounded lengths, and `raw=False` to prevent memory exhaustion and type confusion attacks.
236
+ - ✅ **Message size validation:** Raw msgpack bytes are checked against a configurable max size **before** any deserialization attempt.
237
+ - ✅ **No base64 overhead:** All cryptographic binary data (ciphertexts, nonces, tags) uses native msgpack binary fields, eliminating encoding ambiguity and reducing attack surface. ML-KEM-768 public keys are an exception - serialized as JSON strings for format-independent exchange.
238
+
239
+ ## Dependencies
240
+ | Package | Version | Purpose |
241
+ |---------|---------|---------|
242
+ | `cryptography` | `>=46.0.5` | ML-KEM-768, AES-256-GCM primitives |
243
+ | `msgpack` | `>=1.0.8` | Compact binary serialization with strict decode modes |
244
+ | `typing` | Python stdlib | Type hints (`Optional`, `dict`, `list`) |
245
+ | `os` / `time` | Python stdlib | CSPRNG, timestamp handling |
246
+
247
+ ---
248
+ *This module is designed to be framework-agnostic, testable in isolation, and easily integrated via `uv` path dependencies.*
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "iqcc-shared"
3
+ version = "0.1.0"
4
+ description = "Hybrid cryptographic utilities: ML-KEM-768 key encapsulation, ML-DSA-65 signing, AES-256-GCM payload encryption, and msgpack serialization"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "cryptography>=48.0.0",
8
+ "msgpack>=1.0.8",
9
+ ]
10
+
11
+ [dependency-groups]
12
+ dev = [
13
+ "pytest>=8.0",
14
+ "pytest-cov>=5.0",
15
+ ]
16
+
17
+ [tool.pytest.ini_options]
18
+ testpaths = ["tests"]
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
@@ -0,0 +1,67 @@
1
+ """iqcc-shared — Symmetric Encryption with Post-Quantum Key Exchange.
2
+
3
+ Provides hybrid encryption (ML-KEM-768 + AES-256-GCM),
4
+ automated key rotation, and strict msgpack serialization.
5
+
6
+ Key features:
7
+ • Post-quantum key encapsulation (ML-KEM-768)
8
+ • AES-256-GCM symmetric payload encryption with per-field auth tags
9
+ • Dot-notation path resolver with wildcard (``.*``) support
10
+ • Strict msgpack serialization with security hardening
11
+ • Automated key rotation with overlap window
12
+ """
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ # ── Public API ──────────────────────────────────────────────────────────
17
+
18
+ from .crypto import (
19
+ KEY_ROTATION_WINDOW_DAYS,
20
+ create_or_rotate_key_record,
21
+ decrypt,
22
+ encrypt,
23
+ generate_mlkem_keypair,
24
+ get_mlkem_public_key_bytes,
25
+ load_mlkem_public_key,
26
+ mlkem_private_key_from_hex,
27
+ mlkem_private_key_to_hex,
28
+ mlkem_public_key_from_json,
29
+ mlkem_public_key_to_json,
30
+ )
31
+ from .msgpack_utils import (
32
+ MSGPACK_DEFAULT_MAX_SIZE,
33
+ MSGPACK_MAX_ARRAY_LEN,
34
+ MSGPACK_MAX_BIN_LEN,
35
+ MSGPACK_MAX_EXT_LEN,
36
+ MSGPACK_MAX_MAP_LEN,
37
+ MSGPACK_MAX_STR_LEN,
38
+ msgpack_decode,
39
+ msgpack_encode,
40
+ validate_msgpack_size,
41
+ )
42
+
43
+ __all__ = [
44
+ # Key management
45
+ "KEY_ROTATION_WINDOW_DAYS",
46
+ "create_or_rotate_key_record",
47
+ "generate_mlkem_keypair",
48
+ "get_mlkem_public_key_bytes",
49
+ "load_mlkem_public_key",
50
+ "mlkem_private_key_from_hex",
51
+ "mlkem_private_key_to_hex",
52
+ "mlkem_public_key_from_json",
53
+ "mlkem_public_key_to_json",
54
+ # Encryption / decryption
55
+ "encrypt",
56
+ "decrypt",
57
+ # Msgpack utilities
58
+ "MSGPACK_DEFAULT_MAX_SIZE",
59
+ "MSGPACK_MAX_ARRAY_LEN",
60
+ "MSGPACK_MAX_BIN_LEN",
61
+ "MSGPACK_MAX_EXT_LEN",
62
+ "MSGPACK_MAX_MAP_LEN",
63
+ "MSGPACK_MAX_STR_LEN",
64
+ "msgpack_decode",
65
+ "msgpack_encode",
66
+ "validate_msgpack_size",
67
+ ]