# Crypto Implementation Audit Checklist _Last audited: 2026-03-23_ _Auditor: Claude (automated) + manual review_ --- ## 1. Constant-Time Comparisons **Target files**: `i2p_crypto/chacha20.py`, `i2p_crypto/aes_cbc.py`, `i2p_crypto/hmac_generator.py`, `i2p_crypto/eddsa.py`, `i2p_crypto/dsa.py`, `i2p_crypto/noise.py`, `i2p_crypto/garlic_crypto.py`, `i2p_data/i2np_codec.py` **Check**: Every MAC/tag/signature/hash verification uses `hmac.compare_digest()`. | Location | Pattern | Status | |----------|---------|--------| | `hmac_generator.py:63` | `hmac.compare_digest()` | PASS | | `password.py:31` | `secrets.compare_digest()` | PASS | | `console/auth.py:28` | `secrets.compare_digest()` | PASS | | `garlic_crypto.py:257` | `hmac.compare_digest()` on SHA-256 hash | PASS (fixed 2026-03-23) | | `i2np_codec.py:80` | `hmac.compare_digest()` on checksum byte | PASS (fixed 2026-03-23) | | `dsa.py`, `eddsa.py` | Delegated to `cryptography` library verify() | PASS | | `noise.py` CipherState | Delegated to `ChaCha20Poly1305.decrypt()` | PASS | **Result**: **PASS** -- All comparisons on security-sensitive data use constant-time functions. --- ## 2. Key Zeroization **Target files**: `i2p_crypto/x25519.py`, `i2p_crypto/noise.py`, `i2p_crypto/hkdf.py`, `i2p_data/session_key_manager.py` **Check**: Ephemeral keys stored in `bytearray`, explicitly zeroed after use. | Location | Key Type | Storage | Zeroed? | Status | |----------|----------|---------|---------|--------| | `noise.py:32` | CipherState key | `bytes` (immutable) | No | KNOWN LIMITATION | | `noise.py:162` | Ephemeral private key | `bytes` tuple | No | KNOWN LIMITATION | | `noise.py:360` | `_x25519_private` | `bytes` | No | KNOWN LIMITATION | | `hkdf.py:20-21` | Output buffers | `bytearray` | No explicit zeroing | ADVISORY | | `session_key_manager.py:103` | Session keys | `bytes` | No | KNOWN LIMITATION | **Result**: **ADVISORY** -- Python's immutable `bytes` type makes key zeroization impossible without switching to `bytearray`. This is a language-level limitation. The `cryptography` library handles its own key zeroization in C. For pure-Python key material, use `bytearray` with explicit zeroing in `finally` blocks. **Note**: Key zeroization in Python is best-effort. The garbage collector may retain copies, and `bytearray` zeroing can be optimized away. For production deployments, rely on the `cryptography` library's C-level key handling. --- ## 3. CSPRNG Usage **Target files**: `i2p_crypto/`, `i2p_transport/` **Check**: All randomness sourced from `os.urandom()` or `secrets.*`. | Location | Function | Source | Status | |----------|----------|--------|--------| | `elgamal.py:38-41` | Key generation | `os.urandom()` | PASS | | `garlic_crypto.py:19-20` | Padding/tags | `os.urandom()` | PASS | | `ssu2_handshake.py` | Connection IDs | `os.urandom(8)` | PASS | | `ssu2_server.py` | Nonces | `os.urandom(4)` | PASS | | `ntcp2_real_handshake.py` | Padding | `os.urandom()` | PASS | | `ssu2_connection.py:194` | Challenge data | `os.urandom(8)` | PASS | **Result**: **PASS** -- Zero uses of `random.*` in crypto or transport modules. --- ## 4. No Secrets in Logs **Target**: All files in `i2p_crypto/`, `i2p_transport/`, `i2p_data/` **Check**: No `logging.*`, `print()`, `repr()` calls that include key material. | Location | Log Content | Status | |----------|-------------|--------| | `ssu2_server.py` | Addresses, packet sizes | PASS | | `manager.py` | Peer hash prefix (first 8 bytes only) | PASS | | `upnp.py` | URLs only | PASS | **Result**: **PASS** -- No key material found in any logging statements. --- ## 5. Nonce Management **Target files**: NTCP2 and SSU2 frame encryption in `i2p_transport/` **Check**: Nonce counters increment monotonically, overflow detection, separate nonce spaces. | Check | Location | Status | |-------|----------|--------| | Monotonic increment (Noise) | `noise.py:51,60` | PASS -- `_n += 1` after each op | | Overflow detection (Noise) | `noise.py:48,57` | PASS (fixed 2026-03-23) -- checks `_n >= MAX_NONCE` | | Overflow detection (SSU2) | `ssu2_connection.py:119` | PASS (fixed 2026-03-23) -- checks `>= 0xFFFFFFFF` | | Separate nonce spaces | `ssu2_connection.py:65-67` | PASS -- send/recv use separate CipherState | | Nonce construction (SSU2) | `ssu2_connection.py:105-107` | PASS -- 12-byte nonce from packet number | **Result**: **PASS** -- All nonce management issues addressed. --- ## 6. AEAD Correctness **Target files**: `i2p_crypto/chacha20.py`, NTCP2/SSU2 frame processing **Check**: AD parameter includes header bytes; decryption returns no partial plaintext on auth failure. | Check | Location | Status | |-------|----------|--------| | AD binding (SSU2) | `ssu2_connection.py:127,170` | PASS -- header bound as AD | | AD binding (NTCP2) | `ntcp2_real_connection.py:81,103` | PASS -- empty AD (per NTCP2 data phase spec) | | Atomic decryption failure | `cryptography` library | PASS -- `InvalidTag` raised, no partial plaintext | | Nonce increment on failure | `noise.py:60` | PASS -- `_n += 1` only reached after successful decrypt | **Result**: **PASS** -- AEAD semantics correct. --- ## 7. HKDF Usage **Target**: `i2p_crypto/hkdf.py`, `i2p_crypto/noise.py` **Check**: Two-phase (Extract + Expand) usage; info parameters are protocol-specific strings. | Check | Location | Status | |-------|----------|--------| | Extract + Expand | `hkdf.py` | PASS -- separate `extract()` and `expand()` functions | | Protocol-specific info | `noise.py` MixKey/MixHash | PASS -- chaining key derived per Noise spec | | Output length check | `hkdf.py` | PASS -- `expand()` output <= 255 * HashLen | **Result**: **PASS** --- ## 8. Integer Overflow Masking **Target**: `i2p_crypto/siphash.py`, nonce counters **Check**: All arithmetic ported from Java applies explicit masks. | Check | Location | Status | |-------|----------|--------| | 64-bit mask in SipHash | `siphash.py:9,14,66,75,79` | PASS -- `& MASK64` on all operations | | IV masking | `siphash.py:98` | PASS -- `iv & MASK64` on init | | SipHash return | `siphash.py:61` | PASS -- `(v0 ^ v1 ^ v2 ^ v3) & MASK64` | **Result**: **PASS** -- All Java overflow semantics correctly ported with explicit masks. --- ## 9. String Encoding **Target**: RouterInfo properties, address book, SU3 metadata **Check**: All string-to-bytes conversions use explicit `.encode('utf-8')`. | Check | Status | |-------|--------| | RouterInfo encoding | PASS -- explicit UTF-8 in data serialization | | Password hashing | PASS -- `password.encode("utf-8")` | | Console auth | PASS -- explicit UTF-8 encoding | **Result**: **PASS** --- ## Summary | Section | Result | Fixes Applied | |---------|--------|---------------| | 1. Constant-time comparisons | PASS | 2 fixes (garlic_crypto, i2np_codec) | | 2. Key zeroization | ADVISORY | Language limitation documented | | 3. CSPRNG usage | PASS | None needed | | 4. No secrets in logs | PASS | None needed | | 5. Nonce management | PASS | 2 fixes (noise overflow, SSU2 overflow) | | 6. AEAD correctness | PASS | None needed | | 7. HKDF usage | PASS | None needed | | 8. Integer overflow masking | PASS | None needed | | 9. String encoding | PASS | None needed | **Overall**: 8/9 PASS, 1 ADVISORY (key zeroization -- Python language limitation) **Fixes applied**: 4 security hardening changes in this audit cycle