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/21 21:26:34 UTC

(tooling-agents) branch main updated: ASVS L3 audit: 13.3.3 (secrets_and_configuration)

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 47c01fe  ASVS L3 audit: 13.3.3 (secrets_and_configuration)
47c01fe is described below

commit 47c01feb5d78bc901f0ef98429b0ea7a58aa2a74
Author: ASF Tooling Team <ro...@apache.org>
AuthorDate: Tue Apr 21 23:26:30 2026 +0200

    ASVS L3 audit: 13.3.3 (secrets_and_configuration)
---
 .../v3/d0aa7e9/secrets_and_configuration/13.3.3.md | 430 +++++++++++++++++++++
 1 file changed, 430 insertions(+)

diff --git a/repos/apache/ASVS/reports/steve/v3/d0aa7e9/secrets_and_configuration/13.3.3.md b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/secrets_and_configuration/13.3.3.md
new file mode 100644
index 0000000..d36d207
--- /dev/null
+++ b/repos/apache/ASVS/reports/steve/v3/d0aa7e9/secrets_and_configuration/13.3.3.md
@@ -0,0 +1,430 @@
+# ASVS 13.3.3 Security Audit Report
+
+## Executive Summary
+
+- **Repository:** apache/steve
+- **Audit Date:** Apr 21, 2026
+- **Auditor:** Tooling Agents
+- **ASVS Requirement:** 13.3.3 — Verify that all cryptographic operations are performed using an isolated security module (such as a vault or hardware security module) to securely manage and protect key material from exposure outside of the security module.
+- **ASVS Level:** 3
+- **Section:** Secret Management
+- **Files Analyzed:** 4 relevant out of 4 total, 0 skipped
+
+### Finding Summary
+
+| Severity | Count | IDs |
+|----------|-------|-----|
+| Critical | 0 | — |
+| High | 1 | ASVS-1333-HIGH-001 |
+| Medium | 2 | ASVS-1333-MED-002, ASVS-1333-MED-003 |
+| Low | 0 | — |
+| **Total** | **3** | |
+
+---
+
+## Security Controls Inventory
+
+| Control | Defined In | Purpose | Applied At | Coverage |
+|---------|-----------|---------|------------|----------|
+| Fernet symmetric encryption | `crypto.py:69-73` | Vote encryption | `election.py:232` (`add_vote`) | Applied at single entry point ✓ |
+| Fernet decryption | `crypto.py:76-80` | Vote decryption | `election.py:289-293` (`tally_issue`) | Applied at single entry point ✓ |
+| Argon2 key derivation | `crypto.py:83-93` | Key stretching | `crypto.py:43,53` via `gen_opened_key`, `gen_vote_token` | Internal use ✓ |
+| HKDF key derivation | `crypto.py:56-66` | Vote key stretching | `crypto.py:70,78` via `create_vote`, `decrypt_votestring` | Internal use ✓ |
+| Salt generation | `crypto.py:34-36` | Cryptographic salt | `election.py:80,109` | Applied at salt creation points ✓ |
+| Metadata filtering | `election.py:132-143` | Hide opened_key/salt from public API | `get_metadata()` | **Type B risk**: `_all_metadata()` freely returns key material |
+| HSM/Vault integration | **NOT DEFINED** | Key isolation | **NOWHERE** | **Complete gap** |
+| Secret management | **NOT DEFINED** | Config secret protection | **NOWHERE** | **Complete gap** |
+| Key material cleanup | **NOT DEFINED** | Memory protection | **NOWHERE** | **Complete gap** |
+
+---
+
+## Critical File Review
+
+### `v3/steve/crypto.py` — Cryptographic Operations Module
+
+| Line(s) | Operation | Key Material In Memory | Isolation | Risk |
+|---------|-----------|----------------------|-----------|------|
+| 38-47 | `gen_opened_key()` | `edata`, `digest`, returned `opened_key` | None (in-process) | HIGH |
+| 49-53 | `gen_vote_token()` | `opened_key`, returned `vote_token` | None (in-process) | HIGH |
+| 56-66 | `_b64_vote_key()` | `vote_token`, derived `vote_key`, base64 key | None (in-process) | HIGH |
+| 69-73 | `create_vote()` | `vote_token`, `b64key`, Fernet instance | None (in-process) | HIGH |
+| 76-80 | `decrypt_votestring()` | `vote_token`, `b64key`, Fernet instance, plaintext | None (in-process) | HIGH |
+
+### `v3/steve/election.py` — Key Material Data Flow
+
+| Function | Key Material Accessed | Source | Storage/Destination |
+|----------|----------------------|--------|-------------------|
+| `open()` (line 67) | `opened_key`, `salt` | Generated via `crypto.gen_opened_key()` | **Stored in SQLite DB** via `c_open.perform()` |
+| `_all_metadata()` (line 118) | `opened_key`, `salt` | **Retrieved from SQLite DB** | Returned as EasyDict to callers |
+| `add_vote()` (line 222) | `md.opened_key`, `mayvote.salt`, `vote_token` | DB → memory | Used for encryption, `vote_token` stored in DB |
+| `tally_issue()` (line 257) | `md.opened_key`, `mayvote.salt`, `vote_token`, plaintext votes | DB → memory | Decrypted votes accumulated in list |
+| `has_voted_upon()` (line 238) | `md.opened_key`, `row.salt`, `vote_token` | DB → memory | Used for lookup |
+| `is_tampered()` (line 302) | `md.salt`, `md.opened_key`, recomputed `opened_key` | DB → memory | Comparison only |
+
+### `v3/server/config.yaml.example` — Configuration
+
+| Setting | Value | Secret? | Protection Mechanism |
+|---------|-------|---------|---------------------|
+| `server.port` | 58383 | No | N/A |
+| `server.certfile` | `localhost...+3.pem` | No (path) | N/A |
+| `server.keyfile` | `localhost...+3-key.pem` | No (path) | N/A |
+| `db` | `steve.db` | No (path) | N/A |
+| OAuth client secret | **Not shown** | **Yes** | **Unknown** |
+| LDAP credentials | **Not shown** | **Yes** | **Unknown** |
+
+---
+
+## Findings
+
+### High Severity
+
+#### ASVS-1333-HIGH-001: Master Election Key (`opened_key`) Stored in Application Database Co-located with Encrypted Votes
+
+**Severity:** HIGH  
+**Gap Type:** Type A — No control exists for key storage isolation
+
+**Location:**
+- **File:** `v3/steve/election.py`
+- **Functions:** `open()` (line 67-84), `_all_metadata()` (line 118-131)
+- **Related Functions:** `add_vote()` (line 222-236), `tally_issue()` (line 257-299), `has_voted_upon()` (line 238-256)
+
+**Vulnerable Code:**
+
+Storage at election open (line 83):
+```python
+def open(self, pdb):
+    # ...
+    salt = crypto.gen_salt()
+    opened_key = crypto.gen_opened_key(edata, salt)
+    self.c_open.perform(salt, opened_key, self.eid)  # Stored directly in SQLite
+```
+
+Retrieval for every crypto operation (line 125):
+```python
+def _all_metadata(self, required_state=None):
+    md = self.q_metadata.first_row(self.eid)  # opened_key retrieved from DB
+    # ...
+    return md  # opened_key exposed to all callers
+```
+
+Usage in vote operations (lines 229-232):
+```python
+def add_vote(self, pid: str, iid: str, votestring: str):
+    md = self._all_metadata(self.S_OPEN)  # opened_key loaded from DB
+    mayvote = self.q_get_mayvote.first_row(pid, iid)
+    vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)
+    ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+```
+
+**Data Flow:**
+
+```
+crypto.gen_opened_key() → opened_key (in memory)
+    → c_open.perform() → SQLite metadata table (at rest, unprotected)
+    → q_metadata.first_row() → md.opened_key (back to memory)
+    → crypto.gen_vote_token() → vote_token → crypto.create_vote() → ciphertext
+                                                                      ↓
+                                              SQLite votes table ←────┘
+                                              SQLite mayvote table (salts)
+```
+
+All three components needed for complete de-anonymization (`opened_key`, per-voter `salt`, encrypted `ciphertext`) reside in the **same SQLite database file**. No external secret is required.
+
+**Proof of Concept:**
+
+An attacker with read access to `steve.db` can reconstruct every vote-to-voter mapping:
+
+```python
+import sqlite3
+from steve.crypto import gen_vote_token, decrypt_votestring
+
+db = sqlite3.connect('steve.db')
+# Step 1: Extract opened_key from metadata
+row = db.execute("SELECT salt, opened_key FROM metadata WHERE eid=?", (eid,)).fetchone()
+opened_key = row[1]
+
+# Step 2: For each voter/issue pair, derive vote_token and decrypt
+for pid, iid, salt in db.execute("SELECT pid, iid, salt FROM mayvote WHERE iid IN (SELECT iid FROM issue WHERE eid=?)", (eid,)):
+    vote_token = gen_vote_token(opened_key, pid, iid, salt)
+    row = db.execute("SELECT ciphertext FROM vote WHERE vote_token=? ORDER BY rowid DESC LIMIT 1", (vote_token,)).fetchone()
+    if row:
+        votestring = decrypt_votestring(vote_token, salt, row[0])
+        print(f"Voter {pid} voted: {votestring}")  # Complete de-anonymization
+```
+
+With Argon2 parameters (time_cost=2, memory_cost=64MB), each `gen_vote_token` call takes ~100-200ms. For 500 voters × 5 issues = 2,500 computations ≈ **4-8 minutes** for complete de-anonymization of an entire election. This is not meaningful "cryptographic separation."
+
+**Impact:**
+
+A single database file compromise (backup leak, unauthorized filesystem access, misconfigured permissions) enables an attacker to **decrypt every vote and attribute it to the specific voter who cast it**, completely defeating the stated security goal of voter anonymity. No additional secret or privilege is required beyond read access to the SQLite file. ASVS 13.3.3 requires that key material be managed in an isolated security module specifically to prevent this class of attack — where co [...]
+
+**Remediation:**
+
+Store the `opened_key` in an external secrets manager, requiring a separate compromise vector. At minimum, split the key derivation so that one component comes from outside the database:
+
+```python
+# Option A: Use HashiCorp Vault or cloud KMS
+import hvac
+
+def open(self, pdb):
+    assert self.is_editable()
+    self.add_salts()
+    edata = self.gather_election_data(pdb)
+    salt = crypto.gen_salt()
+    opened_key = crypto.gen_opened_key(edata, salt)
+
+    # Store key in vault, only store a reference in DB
+    vault_client = hvac.Client(url=VAULT_URL, token=VAULT_TOKEN)
+    vault_client.secrets.kv.v2.create_or_update_secret(
+        path=f'elections/{self.eid}',
+        secret={'opened_key': base64.b64encode(opened_key).decode()}
+    )
+    # Store salt in DB (without opened_key)
+    self.c_open.perform(salt, None, self.eid)  # opened_key NOT in DB
+
+# Option B: Minimum — XOR opened_key with a master key from environment variable
+import os
+MASTER_KEY = os.environ['STEVE_MASTER_KEY']  # 32 bytes, hex-encoded
+
+def _protect_key(opened_key: bytes) -> bytes:
+    master = bytes.fromhex(MASTER_KEY)
+    return bytes(a ^ b for a, b in zip(opened_key, master))
+```
+
+---
+
+### Medium Severity
+
+#### ASVS-1333-MED-002: No Secret Management System Integration for Application Configuration
+
+**Severity:** MEDIUM  
+**Gap Type:** Type A — No control exists
+
+**Location:**
+- **File:** `v3/server/config.yaml.example` (entire file)
+- **File:** `v3/server/main.py` (line 41)
+
+**Vulnerable Code:**
+
+`config.yaml.example` — no vault references, no environment variable patterns:
+```yaml
+server:
+    port: 58383
+    certfile: localhost.apache.org+3.pem
+    keyfile: localhost.apache.org+3-key.pem
+db: steve.db
+```
+
+`main.py` — app created from config file with no vault integration:
+```python
+app = asfquart.construct('steve', app_dir=THIS_DIR, static_folder=None)
+```
+
+**Data Flow:**
+
+```
+config.yaml → asfquart.construct() → app.cfg.* (all config in memory)
+                                          ↓
+                         No vault/KMS/env-var substitution visible
+```
+
+**Impact:**
+
+The domain context identifies that the application manages OAuth client secrets, database encryption keys, LDAP credentials, and Fernet encryption keys. The configuration system shows no integration with any secret management solution (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, SOPS, etc.). The example configuration template contains no placeholder patterns for environment variable substitution (e.g., `${ENV_VAR}`) or vault references. While `asfquart` may support environment [...]
+
+This means that secrets are likely stored in plaintext in the configuration file or passed directly as environment variables without centralized management, rotation support, or access auditing.
+
+**Remediation:**
+
+Integrate a secret management system and document the pattern:
+
+```yaml
+# config.yaml with vault references
+server:
+    port: 58383
+    certfile: vault://secret/steve/tls#certfile
+    keyfile: vault://secret/steve/tls#keyfile
+
+oauth:
+    client_secret: env://STEVE_OAUTH_SECRET  # or vault reference
+
+db: steve.db
+# Database encryption key from vault
+db_encryption_key: vault://secret/steve/db#key
+```
+
+Or at minimum, document environment variable usage:
+```yaml
+# All secrets MUST be provided via environment variables:
+# STEVE_OAUTH_SECRET, STEVE_DB_KEY, STEVE_LDAP_PASS
+# See deployment/secrets.md for provisioning instructions
+```
+
+---
+
+#### ASVS-1333-MED-003: Key Material Exposed in Python Exception Tracebacks and Debug Logging
+
+**Severity:** MEDIUM  
+**Gap Type:** Type A — No control exists for key material exposure prevention
+
+**Location:**
+- **File:** `v3/steve/crypto.py` — lines 38-80 (all crypto functions)
+- **File:** `v3/steve/election.py` — lines 222-236 (`add_vote`), 257-299 (`tally_issue`), 238-256 (`has_voted_upon`)
+- **File:** `v3/server/main.py` — lines 57, 97 (DEBUG logging level)
+
+**Vulnerable Code:**
+
+Key material passed as plain function parameters (election.py lines 229-232):
+```python
+def add_vote(self, pid: str, iid: str, votestring: str):
+    md = self._all_metadata(self.S_OPEN)
+    mayvote = self.q_get_mayvote.first_row(pid, iid)
+    vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)
+    # If any exception occurs here, traceback shows: opened_key, vote_token, salt, votestring
+    ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+    self.c_add_vote.perform(vote_token, ciphertext)
+```
+
+No exception wrapping in crypto.py (lines 69-73):
+```python
+def create_vote(vote_token: bytes, salt: bytes, votestring: str) -> bytes:
+    b64key = _b64_vote_key(vote_token, salt)  # Derived key in local variable
+    f = cryptography.fernet.Fernet(b64key)    # Key loaded into Fernet
+    return f.encrypt(votestring.encode())      # If encrypt fails, traceback shows b64key
+```
+
+Debug logging enabled in production entry points (main.py lines 56-57):
+```python
+def run_standalone():
+    logging.basicConfig(level=logging.DEBUG, ...)
+```
+
+```python
+def run_asgi():
+    logging.basicConfig(level=logging.DEBUG, ...)
+    _LOGGER.setLevel(logging.DEBUG)
+```
+
+Evidence of historical key material logging (election.py lines 80-81, commented out):
+```python
+#print('SALT:', salt)
+#print('KEY:', opened_key)
+```
+
+**Data Flow:**
+
+```
+Normal flow: opened_key → vote_token → b64key → Fernet → ciphertext
+                                                              ↓
+Exception flow:  Any error at any step → Python traceback
+                                              ↓
+                  Traceback includes ALL local variables and function arguments
+                                              ↓
+                  Logging framework (at DEBUG level) → log files/stdout
+                                              ↓
+                  opened_key, vote_token, b64key, plaintext votestring EXPOSED
+```
+
+**Impact:**
+
+If any exception occurs during cryptographic operations (database connectivity issues, malformed data, library errors), Python's default exception handling will include all function arguments and local variables in the traceback. With logging set to DEBUG, these tracebacks will be written to logs. The exposed key material includes the master `opened_key`, per-voter `vote_token`, derived Fernet encryption keys (`b64key`), and plaintext vote content (`votestring`). Log aggregation systems, [...]
+
+The commented-out `print` statements for `SALT` and `KEY` demonstrate that this exposure has occurred during development, creating risk of re-enabling during debugging.
+
+**Remediation:**
+
+1. Wrap cryptographic operations in exception handlers that sanitize key material:
+
+```python
+def add_vote(self, pid: str, iid: str, votestring: str):
+    md = self._all_metadata(self.S_OPEN)
+    mayvote = self.q_get_mayvote.first_row(pid, iid)
+    try:
+        vote_token = crypto.gen_vote_token(md.opened_key, pid, iid, mayvote.salt)
+        ciphertext = crypto.create_vote(vote_token, mayvote.salt, votestring)
+        self.c_add_vote.perform(vote_token, ciphertext)
+    except Exception as e:
+        _LOGGER.error(f'Vote encryption failed for issue {iid}: {type(e).__name__}')
+        raise VoteProcessingError(iid) from None  # "from None" suppresses original traceback
+    finally:
+        # Clear local key references
+        vote_token = None
+        ciphertext = None
+```
+
+2. Set production logging to INFO or WARNING:
+
+```python
+def run_asgi():
+    log_level = os.environ.get('STEVE_LOG_LEVEL', 'INFO')
+    logging.basicConfig(level=getattr(logging, log_level), ...)
+```
+
+3. Remove commented-out key printing statements entirely from the codebase.
+
+---
+
+## Positive Security Patterns
+
+| Pattern | Location | Description |
+|---------|----------|-------------|
+| Public/private metadata separation | `election.py:132-143` | `get_metadata()` explicitly strips `salt` and `opened_key` before returning; comment documents intent |
+| Issue salt filtering | `election.py:153` | `get_issue()` includes explicit comment "NEVER return issue.salt" |
+| Cryptographic RNG throughout | `crypto.py:35,100,112` | All random generation uses `secrets` module (CSPRNG) |
+| Strong key derivation chain | `crypto.py:38-93` | BLAKE2b → Argon2 → HKDF provides defense-in-depth key stretching |
+| Per-voter salt uniqueness | `election.py:94-113` | Each voter/issue pair gets a unique `crypto.gen_salt()` salt |
+| Vote shuffling before return | `election.py:296` | `crypto.shuffle(votes)` prevents database-order information leakage in tally results |
+| State machine enforcement | `election.py` throughout | Consistent `assert self.is_editable()` / `is_open()` / `is_closed()` before state-sensitive operations |
+| Transactional integrity | `election.py:62-66,106-112` | Explicit `BEGIN TRANSACTION`/`COMMIT` for multi-row operations |
+| Parameterized queries | `election.py:34` | All SQL via `asfpy.db` with YAML-defined parameterized queries |
+| Collision-safe ID generation | `election.py:161-166,352-357` | Retry loop catches `IntegrityError` for cryptographic ID conflicts |
+
+---
+
+## Architecture Observations
+
+1. **Single Security Boundary:** The entire system (application logic, cryptographic operations, key storage, encrypted data) operates within a single security boundary — the application server and its SQLite database file. ASVS 13.3.3 Level 3 expects at least two boundaries: the application and an isolated security module.
+
+2. **Defense-in-Depth Gap:** The cryptographic design is sound in isolation (Argon2 + HKDF + Fernet with per-voter salts), but all defensive layers collapse upon database compromise because the root key (`opened_key`) is co-located with the data it protects. Introducing a single external secret (vault-stored key, environment-provided master key) would make the entire cryptographic chain resistant to database-only compromise.
+
+3. **Operational Logging Risk:** Both server entry points (`run_standalone`, `run_asgi`) default to `DEBUG` logging level. Combined with the lack of exception wrapping around cryptographic operations, this creates a systemic risk of key material appearing in operational logs.
+
+4. **Crypto Module Abstraction:** The `crypto.py` module provides a clean abstraction layer over cryptographic primitives. This is architecturally good — it would be straightforward to introduce a vault/HSM-backed implementation behind the same interface without changing callers.
+
+---
+
+## Recommendations Summary
+
+### Immediate Actions (0-30 days)
+
+| Priority | Recommendation | Effort | Impact |
+|----------|---------------|--------|--------|
+| 1 | **Add external master key component** — Store a master key in a vault or environment variable that is XOR'd or HKDF'd with `opened_key` before database storage. This ensures database compromise alone cannot derive vote encryption keys. | Medium | Eliminates single-point-of-failure for vote anonymity |
+| 2 | **Wrap crypto operations in exception handlers** — Prevent key material from appearing in tracebacks sent to logging systems. Use `raise ... from None` to suppress traceback chains. | Low | Prevents operational key material exposure |
+
+### Short-term Actions (1-3 months)
+
+| Priority | Recommendation | Effort | Impact |
+|----------|---------------|--------|--------|
+| 3 | **Set production logging to INFO/WARNING** — Make DEBUG logging opt-in via environment variable, not the default. Remove all commented-out key printing statements. | Low | Reduces attack surface for log-based key extraction |
+| 4 | **Integrate secret management for configuration** — Use HashiCorp Vault, cloud KMS, or at minimum documented environment variable patterns for OAuth secrets, LDAP credentials, and any other application secrets. | Medium | Establishes secret management baseline |
+
+### Long-term Actions (3-6 months)
+
+| Priority | Recommendation | Effort | Impact |
+|----------|---------------|--------|--------|
+| 5 | **Add vault/KMS abstraction to crypto.py** — Design `crypto.py` to support pluggable key storage backends (local, vault, HSM) for future ASVS L3 compliance without architectural changes. | Medium-High | Path to full ASVS 13.3.3 compliance |
+
+---
+
+## Appendix: Files Analyzed
+
+<details>
+<summary>Click to expand file list (4 files)</summary>
+
+1. `v3/server/config.yaml.example` (relevance: 10)
+2. `v3/server/main.py` (relevance: 10)
+3. `v3/steve/crypto.py` (relevance: 10)
+4. `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