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 14:17:01 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 6.5.2 (ch06_general)

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 a66ff9a  ASVS L3 audit: 6.5.2 (ch06_general)
a66ff9a is described below

commit a66ff9a8118c6c6656dc67dcc8afdda6d401fffc
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Wed Apr 22 16:16:55 2026 +0200

    ASVS L3 audit: 6.5.2 (ch06_general)
---
 .../reports/steve/v3/d0aa7e9/ch06_general/6.5.2.md | 253 +++++++++++++++++++++
 1 file changed, 253 insertions(+)

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch06_general/6.5.2.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch06_general/6.5.2.md
new file mode 100644
index 0000000..4a6a1ea
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/ch06_general/6.5.2.md
@@ -0,0 +1,253 @@
+# Security Audit Report: ASVS 6.5.2 — Lookup Secret Storage
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 22, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 6.5.2 — Verify that, when being stored in the application's backend, lookup secrets with less than 112 bits of entropy (19 random alphanumeric characters or 34 random digits) are hashed with an approved password storage hashing algorithm that incorporates a 32-bit random salt. A standard hash function can be used if the secret has 112 bits of entropy or more.
+  - **Level:** 2
+  - **Section:** General Multi-factor authentication requirements
+- **Files analyzed:** 4 relevant out of 49 total, 12 skipped
+
+### Finding Counts
+
+| Severity | Count |
+|----------|-------|
+| Critical | 0 |
+| High | 0 |
+| Medium | 1 |
+| Low | 0 |
+| **Total** | **1** |
+
+### Summary
+
+The apache/steve voting system demonstrates strong cryptographic practices overall, with no traditional MFA lookup secrets detected. All stored sensitive values use Argon2 hashing with 128-bit salts, far exceeding ASVS 6.5.2 requirements. However, one medium-severity finding was identified: the production code uses Argon2d (Type.D) instead of the recommended Argon2id (Type.ID) variant, creating potential side-channel vulnerabilities in shared hosting environments.
+
+---
+
+## Security Controls Inventory
+
+### Cryptographic Hashing Controls
+
+| Control | Location | Function | Line (Approx) | Purpose |
+|---------|----------|----------|---------------|---------|
+| Argon2 hashing | `crypto.py` | `_hash()` | 79–88 | Core hashing for all derived secrets |
+| BLAKE2b pre-hash | `crypto.py` | `gen_opened_key()` | 40–44 | Reduces large election data before Argon2 |
+| HKDF-SHA256 | `crypto.py` | `_b64_vote_key()` | 53–64 | Derives encryption keys from vote tokens |
+| CSPRNG salt gen | `crypto.py` | `gen_salt()` | 30–32 | 128-bit salts via `secrets.token_bytes(16)` |
+| Secure ID gen | `crypto.py` | `create_id()` | 107–109 | 40-bit entropy IDs via `secrets.token_hex(5)` |
+
+### Coverage Analysis: Secret Storage
+
+| Secret/Token | Entropy Source | Stored Form | Hashing Applied | ASVS 6.5.2 Compliance |
+|-------------|---------------|-------------|-----------------|----------------------|
+| `vote_token` | Argon2(opened_key+pid+iid, salt) — opened_key provides ≥256 bits | 32-byte Argon2 output in `vote` table | Already Argon2 hash output | ✅ Input entropy >112 bits; stored as hash |
+| `opened_key` | Argon2(BLAKE2b(edata), salt) — edata contains high-entropy components | 32-byte Argon2 output in `election` table | Already Argon2 hash output | ✅ Integrity token, not a lookup secret |
+| `election.salt` | `secrets.token_bytes(16)` — 128 bits | Raw bytes in `election` table | Not hashed (it's a salt, not a secret) | N/A — salt, not a lookup secret |
+| `mayvote.salt` | `secrets.token_bytes(16)` — 128 bits | Raw bytes in `mayvote` table | Not hashed (it's a salt, not a secret) | N/A — salt, not a lookup secret |
+| `eid` / `iid` | `secrets.token_hex(5)` — 40 bits | Plaintext in `election`/`issue` tables | None (primary keys) | See architecture observation below |
+
+---
+
+## Critical File Review
+
+### `v3/steve/crypto.py`
+
+| Line(s) | Element | Security Assessment |
+|---------|---------|-------------------|
+| 30–32 | `gen_salt()`: `secrets.token_bytes(16)` | ✅ 128-bit salt exceeds ASVS 6.5.2's 32-bit minimum |
+| 34–44 | `gen_opened_key()`: BLAKE2b → Argon2 | ✅ Proper two-stage hashing for large data; ⚠️ Uses Argon2d |
+| 46–51 | `gen_vote_token()`: Argon2(opened_key+pid+iid) | ✅ Input entropy >256 bits from opened_key; ⚠️ Uses Argon2d |
+| 53–64 | `_b64_vote_key()`: HKDF-SHA256 | ✅ Proper KDF with domain separation via `info` parameter |
+| 66–70 | `create_vote()`: Fernet encryption | ✅ AES-128-CBC + HMAC-SHA256 |
+| 73–77 | `decrypt_votestring()`: Fernet decryption | ✅ Authenticated decryption |
+| 79–88 | `_hash()`: Argon2 with Type.D | ⚠️ **FINDING ASVS-652-SEV-001** |
+| 92–102 | `shuffle()`: Fisher-Yates with `secrets.randbelow()` | ✅ Cryptographically secure shuffle |
+| 107–109 | `create_id()`: `secrets.token_hex(5)` | ✅ CSPRNG; see architecture note on entropy |
+
+### `v3/steve/election.py`
+
+| Line(s) | Element | Security Assessment |
+|---------|---------|-------------------|
+| 74–85 | `open()`: Election opening flow | ✅ Salt generation before state change; Argon2 key derivation |
+| 118–135 | `add_salts()`: Per-voter salt generation | ✅ Unique 128-bit salt per voter-issue pair within transaction |
+| 240–254 | `add_vote()`: Vote submission | ✅ State verification → token derivation → encryption → storage |
+| 256–310 | `tally_issue()`: Vote tallying | ✅ Re-derives tokens; shuffles before returning |
+| 337–352 | `is_tampered()`: Integrity check | ✅ Recomputes opened_key and compares |
+
+### `v3/schema.sql`
+
+| Element | Security Assessment |
+|---------|-------------------|
+| `election.salt CHECK (length=16)` | ✅ Enforces 128-bit salt at schema level |
+| `election.opened_key CHECK (length=32)` | ✅ Enforces 256-bit hash output |
+| `mayvote.salt CHECK (length=16)` | ✅ Enforces 128-bit per-voter salt |
+| `vote.vote_token CHECK (length=32)` | ✅ Enforces 256-bit token hash |
+| `STRICT` mode on all tables | ✅ Prevents type coercion |
+| `FOREIGN KEY ... ON DELETE RESTRICT` | ✅ Prevents orphaned records |
+
+---
+
+## Findings
+
+### Medium Severity
+
+#### ASVS-652-MED-001: Production Argon2 Uses Type.D (Argon2d) Instead of Recommended Type.ID (Argon2id)
+
+| Attribute | Value |
+|-----------|-------|
+| **Severity** | MEDIUM |
+| **Gap Type** | Type B — Correct variant exists in benchmark code but is NOT used in production hashing |
+| **File** | `v3/steve/crypto.py` |
+| **Function** | `_hash()`, line 79–88 |
+| **Callers affected** | `gen_opened_key()` (line 40), `gen_vote_token()` (line 49) |
+
+**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,
+        memory_cost=65536,
+        parallelism=4,
+        hash_len=32,
+        type=argon2.low_level.Type.D,  # ← Argon2d: side-channel vulnerable
+    )
+```
+
+**Benchmark code (correct variant, NOT used in production):**
+```python
+# In benchmark_argon2(), line ~130:
+argon2.low_level.hash_secret_raw(
+    ...
+    type=argon2.low_level.Type.ID,  # ← Argon2id: recommended variant
+)
+```
+
+**Data Flow:**
+- `election.py:open()` → `crypto.gen_opened_key(edata, salt)` → `_hash(digest, salt)` → **Argon2d** → stored as `election.opened_key`
+- `election.py:add_vote()` → `crypto.gen_vote_token(opened_key, pid, iid, salt)` → `_hash(...)` → **Argon2d** → stored as `vote.vote_token`
+- `election.py:tally_issue()` → same chain for vote lookup/decryption
+- `election.py:is_tampered()` → same chain for integrity verification
+
+**Issue:**
+
+ASVS 6.5.2 requires an "approved password storage hashing algorithm." While Argon2 as a family qualifies, the specific variant matters:
+
+- **Argon2d** (Type.D): Data-dependent memory access. Vulnerable to side-channel attacks (cache-timing, memory access pattern analysis). Designed for settings where side-channel attacks are impossible.
+- **Argon2id** (Type.ID): Hybrid mode combining Argon2i (first pass, side-channel resistant) and Argon2d (subsequent passes, memory-hard). Universally recommended by OWASP Password Storage Cheat Sheet, RFC 9106, and OWASP ASVS 2.4.4 which states: *"Verify that if argon2id is not available, that bcrypt, scrypt, or PBKDF2 is used."*
+
+The benchmark function (`benchmark_argon2()`) at line ~130 correctly uses `Type.ID`, demonstrating awareness of the recommended variant, but the production `_hash()` function uses `Type.D`.
+
+**Impact:**
+- In cloud/shared hosting (typical deployment), a co-located attacker could exploit cache-timing side-channels against Argon2d to recover information about hashed values
+- Compromised `vote_token` values could enable vote de-anonymization
+- Compromised `opened_key` could undermine election integrity detection
+- This is a systemic issue affecting ALL Argon2 hashing operations in the application
+
+**Proof of Concept:**
+
+A co-located process on the same physical host monitors L1/L2 cache access patterns during Argon2d execution. Because Argon2d's memory access pattern depends on the secret data, the attacker can extract bits of the hashed secret through differential timing analysis, unlike Argon2id which uses data-independent access patterns in its first pass.
+
+**Remediation:**
+```python
+def _hash(data: bytes, salt: bytes) -> bytes:
+    "Use Argon2id 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.ID,  # Argon2id: recommended by OWASP/RFC 9106
+    )
+```
+
+---
+
+## Positive Security Patterns
+
+1. **Strong Salt Generation**: All salts use `secrets.token_bytes(16)` (128 bits), far exceeding the ASVS 6.5.2 minimum of 32 bits. Salt uniqueness is enforced per voter-issue pair.
+
+2. **Argon2 for All Sensitive Hashing**: Despite the variant concern, the use of Argon2 (a memory-hard function with configurable parameters) for both `opened_key` and `vote_token` derivation is appropriate. Parameters (time_cost=2, memory_cost=64MB, parallelism=4) are reasonable for server-side operation.
+
+3. **Key Hierarchy Design**: The multi-layer key derivation (BLAKE2b → Argon2 → HKDF → Fernet) provides excellent separation of concerns and domain separation. Each layer serves a distinct purpose.
+
+4. **Schema-Level Cryptographic Field Validation**: CHECK constraints in `schema.sql` enforce salt/hash lengths at the database level, preventing storage of improperly sized cryptographic values.
+
+5. **Vote Token as Stored Hash**: The `vote_token` stored in the `vote` table is the Argon2 *output*, not a raw secret. The original identifying information (pid, iid) is not stored alongside the vote, preserving voter anonymity. The input entropy (>256 bits via `opened_key`) far exceeds the 112-bit threshold where a standard hash would suffice.
+
+6. **Transaction Safety for Salt Assignment**: The `add_salts()` method uses explicit `BEGIN TRANSACTION`/`COMMIT` to ensure atomic salt assignment for all voters, preventing partial-state issues.
+
+7. **Deterministic Election Data Assembly**: `gather_election_data()` uses repeatable ordering for hash input construction, ensuring `opened_key` verification is reliable.
+
+---
+
+## Architecture Observations
+
+1. **No Traditional MFA Lookup Secrets Detected**: The codebase does not implement traditional lookup secrets (recovery codes, TANs, grid codes) as described in ASVS 6.5.2. The closest analogs are vote tokens, which are server-computed Argon2 hashes with >112-bit input entropy, making them compliant even with a standard hash.
+
+2. **Election/Issue IDs as URL Tokens**: `create_id()` produces 40-bit entropy identifiers (10 hex chars). While explicitly designed for non-enumerability rather than authentication, if ballot links rely solely on these IDs for access scoping (even post-LDAP-auth), 40 bits provides ~1 trillion combinations. This is adequate for anti-enumeration but would be insufficient if ever used as standalone authentication tokens. The web layer (not provided) should be audited for how ballot links a [...]
+
+3. **Salt Reuse Across Algorithms**: The same `mayvote.salt` is used for both Argon2 token generation and HKDF key derivation. Domain separation is provided by different algorithms and HKDF's `info` parameter (`b'xchacha20_key'`), which is cryptographically sound per HKDF specification. No action needed.
+
+4. **Ballot Link Mechanism Not Auditable**: The `get_voters_for_email()` method suggests ballot links are sent to voters, but the link construction and token generation code is not in the provided files. If ballot links contain short-lived one-time tokens, those would fall under ASVS 6.5.2 and should be verified separately.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions (Critical Priority)
+
+| Action | Effort | Impact | Timeline |
+|--------|--------|--------|----------|
+| **Change Argon2 Type.D to Type.ID** in `_hash()` (ASVS-652-MED-001) | Trivial (single line) | Eliminates side-channel vulnerability across all hashing operations | Before next deployment |
+
+### Short-term Actions (High Priority)
+
+| Action | Effort | Impact | Timeline |
+|--------|--------|--------|----------|
+| **Audit ballot link generation** (web layer, not provided) for lookup secret compliance | Medium | Ensures any voter-facing tokens meet ASVS 6.5.2 entropy/hashing requirements | Within 1 sprint |
+
+### Long-term Actions (Medium Priority)
+
+| Action | Effort | Impact | Timeline |
+|--------|--------|--------|----------|
+| **Consider increasing ID entropy** in `create_id()` from 5 to 16 bytes | Trivial | Future-proofs identifiers if they gain security-sensitive roles; schema already accommodates variable-length TEXT | Next major version |
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list (4 files)</summary>
+
+1. **`v3/steve/crypto.py`** (relevance: 10/10)
+   - Core cryptographic operations
+   - Argon2 hashing implementation
+   - Salt generation
+   - Key derivation functions
+
+2. **`v3/docs/schema.md`** (relevance: 4/10)
+   - Database schema documentation
+   - Cryptographic field descriptions
+
+3. **`v3/schema.sql`** (relevance: 3/10)
+   - Database table definitions
+   - CHECK constraints for cryptographic fields
+   - Salt and hash length enforcement
+
+4. **`v3/steve/election.py`** (relevance: 2/10)
+   - Election lifecycle management
+   - Vote token generation workflows
+   - Salt assignment logic
+
+</details>
+
+---
+
+**End of Report**
\ 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