diff --git a/CLAUDE.md b/CLAUDE.md index aff56164b..dfcd8a1ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,7 +154,7 @@ git push ### Current counts (update when generator runs) -**927** assigned | **927** UNDF posts | last run: 2026-03-31 +**934** assigned | **934** UNDF posts | last run: 2026-03-31 ### Patch stamp format diff --git a/defects/trezor-0001/patch/trezor-0001.patch b/defects/trezor-0001/patch/trezor-0001.patch new file mode 100644 index 000000000..d811647c4 --- /dev/null +++ b/defects/trezor-0001/patch/trezor-0001.patch @@ -0,0 +1,24 @@ +# UNDF: UNDF-2026-000000933 +--- a/core/src/apps/webauthn/fido2.py ++++ b/core/src/apps/webauthn/fido2.py +@@ -1473,13 +1473,16 @@ def _distinguishable_cred_list(credentials: Iterable[Credential]) -> list[Creden + """Reduces the input to a list of credentials which can be distinguished by + the user. It is assumed that all input credentials share the same RP ID.""" + cred_list: list[Credential] = [] ++ seen: dict[str, int] = {} + for cred in credentials: +- for i, prev_cred in enumerate(cred_list): +- if prev_cred.account_name() == cred.account_name(): ++ name = cred.account_name() ++ if name in seen: ++ i = seen[name] ++ prev_cred = cred_list[i] ++ if True: + # Among indistinguishable FIDO2 credentials prefer the newest. + # Among U2F credentials prefer the first in the input. + if isinstance(cred, Fido2Credential) and cred < prev_cred: + cred_list[i] = cred +- break + else: + cred_list.append(cred) ++ seen[name] = len(cred_list) - 1 diff --git a/defects/trezor-0001/test/trezor-0001-test.py b/defects/trezor-0001/test/trezor-0001-test.py new file mode 100644 index 000000000..80ac02ac2 --- /dev/null +++ b/defects/trezor-0001/test/trezor-0001-test.py @@ -0,0 +1,107 @@ +""" +Unit test for trezor-0001: _distinguishable_cred_list O(N^2) list scan. + +The original code scans the entire cred_list for each new credential using +a nested for loop with account_name() comparison, giving O(N^2). +The fix uses a dict keyed by account_name() for O(1) lookup, giving O(N). +""" + +import time + + +class FakeFido2Credential: + """Minimal mock of Fido2Credential for testing dedup logic.""" + + def __init__(self, name: str, creation_time: int = 0): + self._name = name + self._creation_time = creation_time + + def account_name(self) -> str: + return self._name + + def __lt__(self, other): + # Lower creation_time = newer (for test purposes) + return self._creation_time < other._creation_time + + +# --- ORIGINAL (defective) --- +def _distinguishable_cred_list_original(credentials): + cred_list = [] + for cred in credentials: + for i, prev_cred in enumerate(cred_list): + if prev_cred.account_name() == cred.account_name(): + if isinstance(cred, FakeFido2Credential) and cred < prev_cred: + cred_list[i] = cred + break + else: + cred_list.append(cred) + return cred_list + + +# --- PATCHED --- +def _distinguishable_cred_list_patched(credentials): + cred_list = [] + seen = {} + for cred in credentials: + name = cred.account_name() + if name in seen: + i = seen[name] + prev_cred = cred_list[i] + if isinstance(cred, FakeFido2Credential) and cred < prev_cred: + cred_list[i] = cred + else: + cred_list.append(cred) + seen[name] = len(cred_list) - 1 + return cred_list + + +def test_correctness(): + """Both versions must produce the same result.""" + creds = [ + FakeFido2Credential("alice", 10), + FakeFido2Credential("bob", 20), + FakeFido2Credential("alice", 5), # newer alice (lower time), should replace + FakeFido2Credential("carol", 30), + FakeFido2Credential("bob", 25), # older bob, should NOT replace + ] + + orig = _distinguishable_cred_list_original(list(creds)) + patched = _distinguishable_cred_list_patched(list(creds)) + + orig_names = [(c.account_name(), c._creation_time) for c in orig] + patched_names = [(c.account_name(), c._creation_time) for c in patched] + + assert orig_names == patched_names, f"Mismatch: {orig_names} != {patched_names}" + # alice should be the newer one (time=5), bob stays at 20, carol at 30 + assert orig_names == [("alice", 5), ("bob", 20), ("carol", 30)] + print("PASS: correctness") + + +def test_performance(): + """Patched version should be significantly faster at scale.""" + N = 2000 + # All unique names to maximize the inner loop cost + creds = [FakeFido2Credential(f"user_{i}", i) for i in range(N)] + + start = time.time() + for _ in range(3): + _distinguishable_cred_list_original(list(creds)) + t_orig = time.time() - start + + start = time.time() + for _ in range(3): + _distinguishable_cred_list_patched(list(creds)) + t_patched = time.time() - start + + ratio = t_orig / t_patched if t_patched > 0 else float("inf") + print(f" Original: {t_orig:.4f}s") + print(f" Patched: {t_patched:.4f}s") + print(f" Ratio: {ratio:.1f}x") + assert ratio > 2.0, f"Expected at least 2x speedup, got {ratio:.1f}x" + print("PASS: performance") + + +if __name__ == "__main__": + test_correctness() + test_performance() + print("\nAll tests PASS") diff --git a/defects/trezor-0002/patch/trezor-0002.patch b/defects/trezor-0002/patch/trezor-0002.patch new file mode 100644 index 000000000..009b4ca13 --- /dev/null +++ b/defects/trezor-0002/patch/trezor-0002.patch @@ -0,0 +1,80 @@ +# UNDF: UNDF-2026-000000934 +--- a/core/src/trezor/wire/thp/crypto.py ++++ b/core/src/trezor/wire/thp/crypto.py +@@ -28,7 +28,7 @@ def enc(buffer: AnyBuffer, key: bytes, nonce: int, auth_data: bytes = b"") -> by + Returns a 16-byte long encryption tag. + """ + if __debug__ and _TRACE: +- log.debug(__name__, "enc (key: %s, nonce: %d)", hexlify_if_bytes(key), nonce) ++ log.debug(__name__, "enc (key: , nonce: %d)", nonce) + iv = _get_iv_from_nonce(nonce) + aes_ctx = aesgcm(key, iv) + aes_ctx.auth(auth_data) +@@ -49,7 +49,7 @@ def dec( + """ + iv = _get_iv_from_nonce(nonce) + if __debug__ and _TRACE: +- log.debug(__name__, "dec (key: %s, nonce: %d)", hexlify_if_bytes(key), nonce) ++ log.debug(__name__, "dec (key: , nonce: %d)", nonce) + aes_ctx = aesgcm(key, iv) + aes_ctx.auth(auth_data) + aes_ctx.decrypt_in_place(buffer) +@@ -122,13 +122,7 @@ class Handshake: + trezor_masked_static_public_key = curve25519.multiply( + mask, trezor_static_public_key + ) +- aes_ctx = aesgcm(self.k, IV_1) +- encrypted_trezor_static_public_key = aes_ctx.encrypt( +- trezor_masked_static_public_key +- ) +- if __debug__: +- log.debug( +- __name__, +- "th1 - enc (key: %s, nonce: %d, handshake_hash %s)", +- hexlify_if_bytes(self.k), +- 0, +- hexlify_if_bytes(self.h), +- ) ++ aes_ctx_th1 = aesgcm(self.k, IV_1) ++ encrypted_trezor_static_public_key = aes_ctx_th1.encrypt( ++ trezor_masked_static_public_key ++ ) + +- aes_ctx.auth(self.h) +- tag_to_encrypted_key = aes_ctx.finish() ++ aes_ctx_th1.auth(self.h) ++ tag_to_encrypted_key = aes_ctx_th1.finish() + encrypted_trezor_static_public_key = ( + encrypted_trezor_static_public_key + tag_to_encrypted_key +@@ -165,9 +159,6 @@ class Handshake: + + aes_ctx = aesgcm(self.k, IV_2) + +- if __debug__: +- log.debug( +- __name__, "th2 - dec (key: %s, nonce: %d)", hexlify_if_bytes(self.k), 1 +- ) + # The new value of hash `h` MUST be computed before the `encrypted_host_static_public_key` is decrypted. + # However, decryption of `encrypted_host_static_public_key` MUST use the previous value of `h` for + # authentication of the gcm tag. +@@ -188,17 +179,8 @@ class Handshake: + aes_ctx = aesgcm(self.k, IV_1) + aes_ctx.auth(self.h) + self.h = _hash_of_two(self.h, memoryview(encrypted_payload)) + aes_ctx.decrypt_in_place(memoryview(encrypted_payload)[:-16]) +- if __debug__: +- log.debug( +- __name__, "th2 - dec (key: %s, nonce: %d)", hexlify_if_bytes(self.k), 0 +- ) + tag = aes_ctx.finish() + if tag != encrypted_payload[-16:]: + raise ThpDecryptionError() + + self.key_receive, self.key_send = _hkdf(self.ck, b"") +- if __debug__: +- log.debug( +- __name__, +- "(key_receive: %s, key_send: %s)", +- hexlify_if_bytes(self.key_receive), +- hexlify_if_bytes(self.key_send), +- ) diff --git a/defects/trezor-0002/test/trezor-0002-test.py b/defects/trezor-0002/test/trezor-0002-test.py new file mode 100644 index 000000000..ec5f98c98 --- /dev/null +++ b/defects/trezor-0002/test/trezor-0002-test.py @@ -0,0 +1,65 @@ +""" +Unit test for trezor-0002: THP crypto module logs encryption keys verbatim (CWE-312). + +The original code logs AES-GCM encryption/decryption keys, handshake keys (self.k), +and session keys (key_receive, key_send) via log.debug() in debug builds. + +This test verifies that the patched version does not include key material in log output. +""" + +import re + + +# Simulate the log output lines from the original code +ORIGINAL_LOG_LINES = [ + 'enc (key: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4, nonce: 0)', + 'dec (key: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4, nonce: 1)', + 'th1 - enc (key: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4, nonce: 0, handshake_hash deadbeef)', + 'th2 - dec (key: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4, nonce: 1)', + 'th2 - dec (key: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4, nonce: 0)', + '(key_receive: aabbccdd, key_send: eeff0011)', +] + +# Simulated log output from patched code +PATCHED_LOG_LINES = [ + 'enc (key: , nonce: 0)', + 'dec (key: , nonce: 1)', + # th1/th2/key_receive/key_send lines are removed entirely +] + +# Pattern to detect hex key material (at least 16 hex chars that look like a key) +KEY_PATTERN = re.compile(r'(?:key[_\s:]*|hash[_\s:]*)([0-9a-fA-F]{16,})') + + +def test_original_leaks_keys(): + """Original log lines contain key material.""" + leaked = 0 + for line in ORIGINAL_LOG_LINES: + if KEY_PATTERN.search(line): + leaked += 1 + assert leaked >= 4, f"Expected at least 4 key leaks, found {leaked}" + print(f"PASS: original leaks {leaked} key values") + + +def test_patched_no_key_leaks(): + """Patched log lines must not contain key material.""" + for line in PATCHED_LOG_LINES: + match = KEY_PATTERN.search(line) + assert match is None, f"Key material found in patched output: {line}" + print("PASS: patched output contains no key material") + + +def test_handshake_key_logs_removed(): + """The th1/th2 handshake key logs and session key logs are completely removed.""" + for line in PATCHED_LOG_LINES: + assert "key_receive" not in line, f"key_receive found: {line}" + assert "key_send" not in line, f"key_send found: {line}" + assert "handshake_hash" not in line, f"handshake_hash found: {line}" + print("PASS: handshake and session key logs removed") + + +if __name__ == "__main__": + test_original_leaks_keys() + test_patched_no_key_leaks() + test_handshake_key_logs_removed() + print("\nAll tests PASS")