You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by as...@apache.org on 2026/04/22 01:17:07 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 11.4.4 (vote_encryption_and_crypto)

This is an automated email from the ASF dual-hosted git repository.

asf-tooling pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new badeedc  ASVS L3 audit: 11.4.4 (vote_encryption_and_crypto)
badeedc is described below

commit badeedc4081c010f868d87ae7b95f7e05294f437
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 03:17:02 2026 +0200

    ASVS L3 audit: 11.4.4 (vote_encryption_and_crypto)
---
 .../d0aa7e9/vote_encryption_and_crypto/11.4.4.md   | 341 +++++++++++++++++++++
 1 file changed, 341 insertions(+)

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/vote_encryption_and_crypto/11.4.4.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/vote_encryption_and_crypto/11.4.4.md
new file mode 100644
index 0000000..3bc3ab5
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/vote_encryption_and_crypto/11.4.4.md
@@ -0,0 +1,341 @@
+# ASVS 11.4.4 Security Audit Report
+
+**Repository:** apache/steve  
+**Audit Date:** Apr 22, 2026  
+**Auditor:** Tooling Agents  
+**ASVS Requirement:** 11.4.4 — Verify that the application uses approved key derivation functions with key stretching parameters when deriving secret keys from passwords. The parameters in use must balance security and performance to prevent brute-force attacks from compromising the resulting cryptographic key.
+
+---
+
+## Executive Summary
+
+**Repository:** apache/steve  
+**Audit Date:** Apr 22, 2026  
+**Auditor:** Tooling Agents  
+**ASVS Requirement:** 11.4.4 — ASVS Requirement 11.4.4  
+**Description:** Verify that the application uses approved key derivation functions with key stretching parameters when deriving secret keys from passwords. The parameters in use must balance security and performance to prevent brute-force attacks from compromising the resulting cryptographic key.  
+**Level:** 2  
+**Section:** Hashing and Hash-based Functions  
+**Section Description:** Cryptographic hashes are used in a wide variety of cryptographic protocols, such as digital signatures, HMAC, key derivation functions (KDF), random bit generation, and password storage. The security of the cryptographic system is only as strong as the underlying hash functions used.
+
+**Files analyzed:** 4 relevant out of 4 total, 0 skipped
+
+### Finding Counts
+
+| Severity | Count |
+|----------|-------|
+| Critical | 0 |
+| High | 0 |
+| Medium | 2 |
+| Low | 0 |
+| **Total** | **2** |
+
+### Summary
+
+The cryptographic architecture implements a layered key derivation chain: Argon2 for key stretching → HKDF-SHA256 for key derivation → Fernet for authenticated encryption. The overall design is sound, with Argon2 providing the key stretching and HKDF appropriately used for deriving keys from the high-entropy Argon2 output. Two medium-severity findings relate to (1) the Argon2 variant selection where production code diverges from both the project's own benchmark and industry recommendatio [...]
+
+---
+
+## Security Controls Inventory
+
+| Control | Defined In | Called From | Purpose | Coverage |
+|---------|-----------|-------------|---------|----------|
+| `gen_salt()` (CSPRNG, 16 bytes) | `crypto.py:28` | `election.py:open()`, `election.py:add_salts()` | Cryptographic salt generation | ✅ Complete |
+| `_hash()` (Argon2 KDF) | `crypto.py:78` | `gen_opened_key()`, `gen_vote_token()` | Key stretching with Argon2 | ✅ Complete |
+| `gen_opened_key()` (BLAKE2b→Argon2) | `crypto.py:32` | `election.py:open()`, `election.py:is_tampered()` | Tamper detection key derivation | ✅ Complete |
+| `gen_vote_token()` (Argon2) | `crypto.py:43` | `election.py:add_vote()`, `tally_issue()`, `has_voted_upon()` | Per-voter token generation | ✅ Complete |
+| `_b64_vote_key()` (HKDF-SHA256) | `crypto.py:50` | `create_vote()`, `decrypt_votestring()` | Encryption key derivation | ✅ Complete |
+| `create_vote()` (Fernet encrypt) | `crypto.py:63` | `election.py:add_vote()` | Vote encryption | ✅ Complete |
+| `decrypt_votestring()` (Fernet decrypt) | `crypto.py:71` | `election.py:tally_issue()` | Vote decryption | ✅ Complete |
+| `is_tampered()` (integrity check) | `election.py:303` | `tally.py:main()` | Election integrity verification | ✅ Complete |
+
+---
+
+## Critical File Review
+
+### `v3/steve/crypto.py` — Core Cryptographic Module
+
+| Function | Purpose | Algorithm | Key Stretching | Status |
+|----------|---------|-----------|----------------|--------|
+| `gen_salt()` | Salt generation | `secrets.token_bytes(16)` | N/A | ✅ CSPRNG, proper length |
+| `_hash()` | Core KDF | Argon2d (t=2, m=64MB, p=4) | ✅ Present | ⚠️ Wrong variant (Type.D) |
+| `gen_opened_key()` | Tamper key | BLAKE2b → Argon2d | ✅ Via `_hash()` | ⚠️ Inherits `_hash()` issue |
+| `gen_vote_token()` | Voter token | Argon2d | ✅ Via `_hash()` | ⚠️ Inherits `_hash()` issue |
+| `_b64_vote_key()` | Key derivation | HKDF-SHA256 | N/A (not needed) | ✅ Correct for high-entropy input |
+| `create_vote()` | Encryption | Fernet (AES-128-CBC+HMAC) | N/A | ✅ Authenticated encryption |
+| `decrypt_votestring()` | Decryption | Fernet | N/A | ✅ Authenticated decryption |
+| `shuffle()` | Vote ordering | Fisher-Yates | N/A | ✅ `secrets.randbelow()` |
+| `create_id()` | ID generation | `secrets.token_hex(5)` | N/A | ✅ CSPRNG |
+
+### `v3/steve/election.py` — Election State Machine
+
+| Method | Crypto Operation | Control Applied | Status |
+|--------|-----------------|-----------------|--------|
+| `open()` | Salt gen + opened_key | `gen_salt()`, `gen_opened_key()` | ✅ |
+| `add_salts()` | Per-voter salt gen | `gen_salt()` per mayvote row | ✅ |
+| `add_vote()` | Token gen + encryption | `gen_vote_token()`, `create_vote()` | ✅ |
+| `tally_issue()` | Token gen + decryption | `gen_vote_token()`, `decrypt_votestring()` | ✅ |
+| `is_tampered()` | Integrity check | `gen_opened_key()` recomputation | ✅ |
+| `has_voted_upon()` | Token gen | `gen_vote_token()` | ✅ |
+
+### `v3/schema.sql` — Database Schema
+
+| Column | Crypto Constraint | Status |
+|--------|------------------|--------|
+| `election.salt` | BLOB, 16 bytes | ✅ CHECK enforced |
+| `election.opened_key` | BLOB, 32 bytes | ✅ CHECK enforced |
+| `mayvote.salt` | BLOB, 16 bytes | ✅ CHECK enforced |
+| `vote.vote_token` | BLOB, 32 bytes | ✅ CHECK enforced |
+| `vote.ciphertext` | BLOB, NOT NULL | ✅ |
+
+---
+
+## Findings
+
+### Medium Severity
+
+#### ASVS-1144-MED-001: Production Code Uses Argon2d Instead of Recommended Argon2id
+
+**Location:**
+- **File:** `v3/steve/crypto.py`
+- **Function:** `_hash()` (line ~88)
+- **Related Functions:** `gen_opened_key()`, `gen_vote_token()`
+
+**Gap Type:** Type B — Correct variant known but not applied in production
+
+**Vulnerable Code:**
+
+```python
+# crypto.py, _hash() function, line ~88
+def _hash(data: bytes, salt: bytes) -> bytes:
+    "Use Argon2 to hash the data, with default tuning parameters."
+
+    return argon2.low_level.hash_secret_raw(
+        secret=data,
+        salt=salt,
+        time_cost=2,
+        memory_cost=65536,
+        parallelism=4,
+        hash_len=32,
+        type=argon2.low_level.Type.D,  # <-- Argon2d: side-channel vulnerable
+    )
+```
+
+**Inconsistency Evidence — Benchmark uses correct variant:**
+
+```python
+# crypto.py, benchmark_argon2() function, line ~127
+                argon2.low_level.hash_secret_raw(
+                    ...
+                    type=argon2.low_level.Type.ID,  # <-- Argon2id: recommended variant
+                )
+```
+
+**Data Flow:**
+
+The `_hash()` function is the sole Argon2 entry point, called by:
+1. `gen_opened_key(edata, salt)` → produces the tamper-detection key stored in `election.opened_key`
+2. `gen_vote_token(opened_key, pid, iid, salt)` → produces per-voter tokens for vote encryption/decryption
+
+Both paths protect election integrity and ballot secrecy — the system's two most critical security properties.
+
+**Description:**
+
+The production code uses **Argon2d** (`Type.D`) while the project's own benchmark function uses **Argon2id** (`Type.ID`), which is the recommended variant per RFC 9106, OWASP Password Storage Cheat Sheet, and NIST guidance.
+
+**Argon2d** uses data-dependent memory access patterns, making it vulnerable to side-channel attacks (cache-timing, speculative execution). While it provides the strongest resistance against GPU-based brute-force attacks, it is NOT the recommended general-purpose variant.
+
+**Argon2id** is a hybrid that provides Argon2i's side-channel resistance in the first pass and Argon2d's GPU resistance in subsequent passes. It is recommended by:
+- [RFC 9106, Section 4](https://www.rfc-editor.org/rfc/rfc9106#section-4): *"If you do not know the difference between them or you consider side-channel attacks as a viable threat, pick Argon2id."*
+- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html): Lists Argon2id as the first recommended algorithm.
+- NIST SP 800-63B references Argon2 with a preference for the `id` variant.
+
+**Impact:**
+
+In a shared hosting or multi-tenant environment, an attacker with co-residency on the same physical server could exploit data-dependent memory access patterns through cache-timing side channels to extract information about the Argon2 input. Since `gen_opened_key()` and `gen_vote_token()` protect election integrity and voter anonymity respectively, this could theoretically compromise both properties.
+
+The risk is mitigated by the fact that this is likely deployed in a controlled server environment, but the fix is trivial and the benchmark code already demonstrates the correct approach.
+
+**Proof of Concept:**
+
+N/A — Side-channel exploitation requires co-location on the same physical hardware.
+
+**Remediation:**
+
+```python
+def _hash(data: bytes, salt: bytes) -> bytes:
+    "Use Argon2id to hash the data, with tuned parameters."
+
+    return argon2.low_level.hash_secret_raw(
+        secret=data,
+        salt=salt,
+        time_cost=2,
+        memory_cost=65536,
+        parallelism=4,
+        hash_len=32,
+        type=argon2.low_level.Type.ID,  # Argon2id per RFC 9106, OWASP
+    )
+```
+
+> **⚠️ Migration Note:** Changing the variant will produce different hash outputs. Any existing elections with stored `opened_key` values (open or closed elections) will fail tamper detection. A migration strategy is required — either re-derive keys for open elections, or store the variant alongside the hash.
+
+---
+
+#### ASVS-1144-MED-002: Argon2 Parameters Adopted from Passlib Defaults Without Application-Specific Tuning
+
+**Location:**
+- **File:** `v3/steve/crypto.py`
+- **Function:** `_hash()` (line ~78)
+- **Related Functions:** `benchmark_argon2()`, `gen_opened_key()`, `gen_vote_token()`
+
+**Gap Type:** Type A — Missing documented tuning rationale
+
+**Vulnerable Code:**
+
+```python
+def _hash(data: bytes, salt: bytes) -> bytes:
+    "Use Argon2 to hash the data, with default tuning parameters."
+
+    return argon2.low_level.hash_secret_raw(
+        secret=data,
+        salt=salt,
+        time_cost=2,       # Passlib default
+        memory_cost=65536,  # Passlib default
+        parallelism=4,      # Passlib default
+        hash_len=32,
+        type=argon2.low_level.Type.D,
+    )
+```
+
+**Description:**
+
+The parameters are explicitly annotated as "Passlib default" with no evidence of application-specific tuning. ASVS 11.4.4 requires parameters that "balance security and performance to prevent brute-force attacks." While the values are within a reasonable range, the concern is:
+
+1. **Parallelism of 4** — Higher parallelism allows faster computation for both the server and attackers with multi-core hardware. OWASP's recommended configurations use `p=1`:
+   - Configuration 1: `m=47104, t=1, p=1`
+   - Configuration 2: `m=19456, t=2, p=1`
+
+2. **No documented tuning rationale** — The `benchmark_argon2()` function exists specifically for parameter tuning, showing the developers built the tooling but the production parameters still use untuned defaults. The benchmark tests 8 different configurations but the production code doesn't reflect any benchmark-informed decision.
+
+3. **Parameter adequacy for threat model** — For this system, Argon2 protects:
+   - `gen_opened_key()`: election integrity (tamper detection) — input is high-entropy election data, so Argon2 stretching is less critical here.
+   - `gen_vote_token()`: voter anonymity — input includes the 32-byte `opened_key` plus identifiers, so entropy is sufficient.
+
+**Data Flow:**
+
+The `_hash()` function with untuned parameters is called by:
+- `gen_opened_key()` → used in `election.py:open()` and `election.py:is_tampered()`
+- `gen_vote_token()` → used in `election.py:add_vote()`, `tally_issue()`, and `has_voted_upon()`
+
+**Impact:**
+
+The untuned defaults are not inherently weak for this use case (high-entropy inputs), but they represent a process gap — the ASVS requirement expects deliberate parameter selection based on the application's performance budget and threat model. If the input entropy profile changes (e.g., if identifiers become shorter or more predictable), the current parameters may not provide sufficient protection.
+
+**Proof of Concept:**
+
+N/A — This is a process/documentation gap rather than an exploitable vulnerability.
+
+**Remediation:**
+
+1. Run the existing `benchmark_argon2()` on the production hardware.
+2. Select parameters that target 100-500ms computation time per derivation.
+3. Document the tuning rationale alongside the parameters:
+
+```python
+def _hash(data: bytes, salt: bytes) -> bytes:
+    """Use Argon2id to hash the data.
+    
+    Parameters tuned on [hardware description] targeting ~200ms per derivation.
+    Benchmarked [date] using benchmark_argon2(). Review annually or on hardware change.
+    See: OWASP Password Storage Cheat Sheet, RFC 9106 Section 4.
+    """
+    return argon2.low_level.hash_secret_raw(
+        secret=data,
+        salt=salt,
+        time_cost=3,        # Tuned: increased from default 2
+        memory_cost=65536,   # 64 MiB - above OWASP minimum of 46 MiB
+        parallelism=1,       # Reduced from 4 to match OWASP recommendation
+        hash_len=32,
+        type=argon2.low_level.Type.ID,
+    )
+```
+
+---
+
+## Positive Security Patterns
+
+1. **Layered key derivation architecture**: The chain `BLAKE2b → Argon2 → HKDF → Fernet` properly separates concerns — compression, stretching, derivation, and encryption are handled by purpose-built primitives.
+
+2. **Per-voter cryptographic isolation**: Each `(pid, iid)` pair gets a unique 16-byte CSPRNG salt in the `mayvote` table, ensuring vote tokens and encryption keys are unique per voter per issue. This prevents correlation even by database administrators.
+
+3. **CSPRNG usage throughout**: All random values use `secrets` module — `token_bytes()` for salts, `token_hex()` for IDs, `randbelow()` for shuffling. No use of `random` module for security operations.
+
+4. **Authenticated encryption**: Fernet provides AES-128-CBC + HMAC-SHA256, preventing both confidentiality and integrity attacks on stored votes.
+
+5. **Salt length enforcement at schema level**: Database CHECK constraints enforce exact 16-byte salt and 32-byte key/token lengths, providing defense-in-depth against programming errors.
+
+6. **Cryptographic shuffle for vote ordering**: `crypto.shuffle()` uses Fisher-Yates with `secrets.randbelow()`, preventing any information leakage through vote ordering during tally.
+
+7. **Tamper detection before tally**: `tally.py:main()` calls `election.is_tampered()` before processing any votes, preventing tallying of modified elections.
+
+8. **Transaction safety for salt generation**: `add_salts()` wraps all salt insertions in a transaction, preventing partial salt assignment that could leave some voters without encryption keys.
+
+9. **HKDF context separation**: The `info` parameter in HKDF (`b'xchacha20_key'`) provides domain separation for the derived key, even though the label is currently mismatched with the actual algorithm.
+
+---
+
+## Architecture Observations
+
+1. **Key derivation chain integrity**: The full chain provides appropriate security for the threat model:
+   ```
+   election_data + salt → BLAKE2b(64B) → Argon2(32B) → opened_key
+   opened_key + pid + iid + salt → Argon2(32B) → vote_token
+   vote_token + salt → HKDF-SHA256(32B) → encryption_key
+   encryption_key → Fernet → ciphertext
+   ```
+   Each step uses an appropriate algorithm for its purpose. The Argon2 steps provide the key stretching required by ASVS 11.4.4; the HKDF step correctly handles derivation from already-strong material.
+
+2. **HKDF info/comment mismatch**: The `_b64_vote_key()` function's docstring says "Key-stretch" and references "PBKDF," and the HKDF `info` parameter says `xchacha20_key`, but the actual encryption uses Fernet (AES-128). The `###` comment acknowledges a planned migration. While not a security vulnerability (the key derivation is cryptographically sound), this creates maintenance risk if the migration introduces a second derivation path with the same info label.
+
+3. **Non-constant-time comparison in `is_tampered()`**: The comparison `opened_key != md.opened_key` uses Python's standard `!=` which short-circuits. In practice, this is not exploitable because (a) Argon2 computation time (~100ms+) dominates any comparison timing difference, and (b) the method is called from CLI tooling, not web endpoints. However, using `hmac.compare_digest()` would be best practice for defense-in-depth.
+
+4. **Election key lifecycle**: The `opened_key` serves dual purpose — tamper detection AND as input to vote token generation. Compromise of the opened_key (stored in the election table) would allow an attacker with database read access to derive all vote tokens and decrypt all votes. This is acknowledged in the threat model per the known false positive patterns.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions
+
+| Priority | Action | Finding | Effort |
+|----------|--------|---------|--------|
+| 1 | Change Argon2 variant from `Type.D` to `Type.ID` in `_hash()` | ASVS-1144-MED-001 | Low (one-line change + migration plan) |
+| 2 | Run `benchmark_argon2()` on production hardware and document parameter rationale | ASVS-1144-MED-002 | Low |
+
+### Short-term Actions
+
+| Priority | Action | Finding | Effort |
+|----------|--------|---------|--------|
+| 3 | Consider reducing parallelism from 4 to 1 per OWASP guidance | ASVS-1144-MED-002 | Low |
+| 4 | Update HKDF `info` parameter to match actual algorithm (`b'fernet_vote_key'`) | Observation | Low |
+| 5 | Update `_b64_vote_key()` docstring to correctly describe HKDF usage | Observation | Trivial |
+
+### Long-term Actions
+
+| Priority | Action | Finding | Effort |
+|----------|--------|---------|--------|
+| 6 | Use `hmac.compare_digest()` in `is_tampered()` for defense-in-depth | Observation | Trivial |
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list</summary>
+
+- `v3/schema.sql` (relevance: 10)
+- `v3/server/bin/tally.py` (relevance: 10)
+- `v3/steve/crypto.py` (relevance: 10)
+- `v3/steve/election.py` (relevance: 10)
+
+</details>
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org