test(phase-02): add Nyquist validation tests — fill SEC-05, AUTH-08, SEC-03 and frontend gaps

8 test files, 60 new tests (14 backend + 46 frontend). All green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-31 12:04:21 +02:00
parent 6c79f92d70
commit d98e3ab7a1
8 changed files with 1381 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
"""
SEC-03: Constant-time comparison for token/code verification.
Two layers of verification:
1. Source-code inspection: confirm hmac.compare_digest is used in auth service.
2. Behavioral tests: confirm that wrong tokens/codes are rejected correctly
(proving the comparison pathway functions at minimum).
The behavioral tests do not prove timing properties (impossible without
statistical measurement), but they prove the reject-path works — a necessary
condition for constant-time implementations to be meaningful.
"""
from __future__ import annotations
import inspect
import os
import re
import pytest
# ── Source inspection ─────────────────────────────────────────────────────────
def test_hmac_compare_digest_used_in_hibp_check():
"""SEC-03: auth service uses hmac.compare_digest for HIBP suffix comparison.
The HIBP check compares SHA-1 suffixes; using == would leak timing information
about whether the prefix matches. hmac.compare_digest is the required approach.
"""
auth_path = os.path.join(
os.path.dirname(__file__), "..", "services", "auth.py"
)
with open(auth_path) as f:
source = f.read()
assert "hmac.compare_digest" in source, (
"SEC-03 VIOLATED: 'hmac.compare_digest' not found in services/auth.py. "
"All security-sensitive comparisons must use constant-time comparison."
)
def test_no_plain_equality_for_suffix_comparison():
"""SEC-03: The HIBP check must not use plain == to compare hash suffixes.
A plain == comparison leaks timing information — rejected by SEC-03.
The implementation must use hmac.compare_digest.
"""
auth_path = os.path.join(
os.path.dirname(__file__), "..", "services", "auth.py"
)
with open(auth_path) as f:
source = f.read()
# Detect patterns like: candidate_suffix == suffix or suffix == candidate_suffix
# These would be in the HIBP check or token comparison area
# We allow == for simple type/key checks but not for suffix/token/code comparisons
# Look specifically for suffix comparison using ==
dangerous_patterns = re.findall(
r'candidate_suffix\s*==\s*\w+|suffix\s*==\s*candidate_suffix',
source
)
assert len(dangerous_patterns) == 0, (
f"SEC-03 VIOLATED: Plain equality (==) used for suffix comparison: "
f"{dangerous_patterns}. Must use hmac.compare_digest."
)
# ── Behavioral: verify_password ───────────────────────────────────────────────
def test_verify_password_correct_password_returns_true_constant_time():
"""SEC-03: verify_password returns True for correct password."""
from services.auth import hash_password, verify_password
h = hash_password("Correct1Password!")
result = verify_password("Correct1Password!", h)
assert result is True, (
"verify_password should return True for the correct password"
)
def test_verify_password_wrong_password_returns_false_constant_time():
"""SEC-03: verify_password returns False for incorrect password.
This behavioral test confirms the rejection path works. The underlying
pwdlib uses constant-time Argon2 verification.
"""
from services.auth import hash_password, verify_password
h = hash_password("Correct1Password!")
result = verify_password("WrongPassword1!", h)
assert result is False, (
"SEC-03: verify_password must return False for wrong password — "
"the comparison pathway is broken if this fails."
)
def test_verify_password_empty_wrong_returns_false():
"""SEC-03: verify_password returns False for empty string against real hash."""
from services.auth import hash_password, verify_password
h = hash_password("ActualPassword1!")
result = verify_password("", h)
assert result is False
def test_verify_password_against_invalid_hash_returns_false():
"""SEC-03: verify_password returns False (not exception) for a malformed hash."""
from services.auth import verify_password
# verify_password must never raise — it catches exceptions and returns False
result = verify_password("anything", "not-a-valid-argon2-hash")
assert result is False, (
"verify_password must return False on exception, not propagate it"
)
# ── Behavioral: verify_backup_code ───────────────────────────────────────────
@pytest.mark.asyncio
async def test_backup_code_verification_is_constant_time(db_session):
"""SEC-03: verify_backup_code checks ALL rows (no early exit on match).
The implementation must iterate every unused backup code even after finding
a match, to prevent timing-based enumeration of which position matched.
This test checks the behavioral correctness of the implementation.
"""
import uuid as _uuid
from db.models import User, Quota
from services.auth import (
generate_backup_codes,
hash_password,
store_backup_codes,
verify_backup_code,
)
from sqlalchemy import select
from db.models import BackupCode
user_id = _uuid.uuid4()
user = User(
id=user_id,
handle=f"ct_user_{user_id.hex[:6]}",
email=f"ct_{user_id.hex[:6]}@example.com",
password_hash=hash_password("ValidP@ss1!"),
role="user",
is_active=True,
password_must_change=False,
)
quota = Quota(user_id=user_id, limit_bytes=104857600, used_bytes=0)
db_session.add(user)
db_session.add(quota)
await db_session.commit()
codes = generate_backup_codes(10)
await store_backup_codes(db_session, user_id, codes)
# Verify: correct code returns True
result = await verify_backup_code(db_session, user_id, codes[0])
assert result is True, "verify_backup_code must return True for a valid backup code"
# Verify: SAME code second time returns False (marked used)
result2 = await verify_backup_code(db_session, user_id, codes[0])
assert result2 is False, (
"verify_backup_code must return False when the code has already been used"
)
# Verify: wrong code returns False
result3 = await verify_backup_code(db_session, user_id, "XXXXXXXX")
assert result3 is False, (
"verify_backup_code must return False for a code that was never issued"
)
# Verify: after first code is consumed, remaining codes still work (independent rows)
result4 = await verify_backup_code(db_session, user_id, codes[1])
assert result4 is True, (
"verify_backup_code must still validate unused backup codes after another was consumed"
)
# Confirm the implementation inspects source — iterates all rows
auth_path = os.path.join(
os.path.dirname(__file__), "..", "services", "auth.py"
)
with open(auth_path) as f:
source = f.read()
assert "matched_row" in source and "keep iterating" in source, (
"SEC-03: verify_backup_code should iterate all rows even after matching "
"(constant-time invariant). Implementation comment 'keep iterating' not found."
)