{"actor":{"kind":"model","lab":"anthropic","name":"claude-opus-5-5"},"at":"2026-11-05T22:55:48Z","attempt_log":[{"attempt":1,"guard":{"added_required_params":[],"line_delta":6,"new_imports":[],"ok":true,"path":"webhook.py","public_api_preserved":true,"removed_lines":0,"removed_public":[],"violations":[],"warnings":[]},"reasons":[],"verdict":"VERIFIED_FIX"}],"claude":{"attempts":1,"first_attempt":{"input_tokens":808,"model":"claude-opus-5-5","output_tokens":1400,"request_id":null,"seconds":14.4,"stop_reason":"end_turn"},"input_tokens":808,"model":"claude-opus-5-5","output_tokens":1400,"request_id":null,"seconds":14.4,"stop_reason":"end_turn"},"claude_test_code":"import hashlib\nimport hmac\n\nimport webhook\n\n\ndef _sig(secret, body):\n    return hmac.new(secret, body, hashlib.sha256).hexdigest()\n\n\ndef test_valid_signature_accepted():\n    assert webhook.check_webhook(b\"k\", b\"payload\", _sig(b\"k\", b\"payload\")) is True\n\n\ndef test_wrong_signature_rejected():\n    assert webhook.check_webhook(b\"k\", b\"payload\", _sig(b\"other\", b\"payload\")) is False\n    assert webhook.check_webhook(b\"k\", b\"payload\", \"\") is False\n\n\ndef test_non_ascii_signature_rejected_without_error():\n    assert webhook.check_webhook(b\"k\", b\"payload\", \"\\u00e9\" * 64) is False\n\n\ndef test_uses_constant_time_compare():\n    calls = []\n    original = hmac.compare_digest\n\n    def spy(a, b):\n        calls.append((a, b))\n        return original(a, b)\n\n    hmac.compare_digest = spy\n    try:\n        result = webhook.check_webhook(b\"k\", b\"body\", _sig(b\"k\", b\"body\"))\n    finally:\n        hmac.compare_digest = original\n    assert result is True\n    assert len(calls) == 1\n","diff":"--- a/webhook.py\n+++ b/webhook.py\n@@ -4,5 +4,11 @@\n \n def check_webhook(secret: bytes, body: bytes, signature: str) -> bool:\n     \"\"\"True if `signature` is the hex HMAC-SHA256 of body under secret.\"\"\"\n+    if not isinstance(signature, str):\n+        return False\n     expected = hmac.new(secret, body, hashlib.sha256).hexdigest()\n-    return signature == expected\n+    # Compare as bytes so non-ASCII input cannot raise TypeError, and use a\n+    # constant-time comparison so timing does not leak the expected digest.\n+    return hmac.compare_digest(\n+        signature.encode(\"utf-8\"), expected.encode(\"ascii\")\n+    )\n","explanation":"The signature check compared the attacker-supplied signature against the expected HMAC with `==`, which short-circuits on the first differing character and can leak the expected digest through timing, so the function did not provide the safe verification a webhook check implies. The fix computes the same HMAC-SHA256 hex digest and compares it with hmac.compare_digest. Both sides are encoded to bytes first so a non-ASCII signature is rejected instead of raising TypeError, and a non-str signature returns False. The tests check that valid signatures are accepted, invalid and non-ASCII ones are rejected, and that hmac.compare_digest is actually called. The original file fails that last test.","falsework_after":{"findings":[],"grade":"REAL"},"falsework_before":{"findings":[{"check":"secret_compare_not_constant_time","detail":"`expected` is compared with ==/!=, which is not constant-time: use hmac.compare_digest.","line":8,"severity":"MEDIUM","snippet":"return signature == expected"}],"grade":"PARTIAL"},"finding":{"check":"secret_compare_not_constant_time","detail":"`expected` is compared with ==/!=, which is not constant-time: use hmac.compare_digest.","line":8,"severity":"MEDIUM","snippet":"return signature == expected"},"finding_fingerprint":"4a648d97ef4b57c445404a2fe4f1ab1f21e7f3f7c367634d3f580db5c995a007","fixed_code":"import hashlib\nimport hmac\n\n\ndef check_webhook(secret: bytes, body: bytes, signature: str) -> bool:\n    \"\"\"True if `signature` is the hex HMAC-SHA256 of body under secret.\"\"\"\n    if not isinstance(signature, str):\n        return False\n    expected = hmac.new(secret, body, hashlib.sha256).hexdigest()\n    # Compare as bytes so non-ASCII input cannot raise TypeError, and use a\n    # constant-time comparison so timing does not leak the expected digest.\n    return hmac.compare_digest(\n        signature.encode(\"utf-8\"), expected.encode(\"ascii\")\n    )\n","fixed_sha256":"017e0a778995097c4448cb6b0f7dd8f069374ec4b32e7563718109757958ab3b","guard":{"added_required_params":[],"line_delta":6,"new_imports":[],"ok":true,"path":"webhook.py","public_api_preserved":true,"removed_lines":0,"removed_public":[],"violations":[],"warnings":[]},"lines_changed":8,"original_code":"import hashlib\nimport hmac\n\n\ndef check_webhook(secret: bytes, body: bytes, signature: str) -> bool:\n    \"\"\"True if `signature` is the hex HMAC-SHA256 of body under secret.\"\"\"\n    expected = hmac.new(secret, body, hashlib.sha256).hexdigest()\n    return signature == expected\n","original_sha256":"28c72a441c26f9cb26f279d9b58662595eef8079e4f0604a8b74f19be93e9d5d","path":"webhook.py","prev_record_hash":"92801b41518909581778f57e225c9e2e3a40186cce5d20d78041dd8191fa04ee","public_key_b64":"b6hfA2rpAdV29FOdhNXGkk4t91mCV0AnfFYwq0HO0R8=","reasons":[],"record_hash":"171e88f4ee9cf58ca0d42a3eb28423d689a3858bde07f0254e584fa3f1fcd3ff","reference_test_author":"customer:live-probe","reference_test_author_lab":"customer","reference_test_independence":"independent","reference_test_origin":"caller","reference_test_sha256":"efd2ff535a84c27e0211ca6e7cf17411432a5db9848d3dbe39eb9f20567b07cd","run_id":"ks-20261105T225548Z-4f35c76d","sample_id":null,"schema":"cain42.keystone.fix-proof.v1","sequence":10,"signature_b64":"gj7KK9XF7VVKkQkU7K73F9BS1sAI9HYjIYkOzEhWd5BJb9SWr9g7RWwX6QfEnqpD5QX9nQXObkLXUiGnRj3TDQ==","standard":"cain42.keystone.proof-standard.v2","tests":{"claude":{"after":{"all_passed":true,"ran":true,"results":[{"file":"test_claude.py","outcome":"passed","test":"test_non_ascii_signature_rejected_without_error"},{"file":"test_claude.py","outcome":"passed","test":"test_uses_constant_time_compare"},{"file":"test_claude.py","outcome":"passed","test":"test_valid_signature_accepted"},{"file":"test_claude.py","outcome":"passed","test":"test_wrong_signature_rejected"}],"some_not_passed":false},"before":{"all_passed":false,"ran":true,"results":[{"file":"test_claude.py","outcome":"passed","test":"test_non_ascii_signature_rejected_without_error"},{"detail":"assertion","file":"test_claude.py","outcome":"not_passed","test":"test_uses_constant_time_compare"},{"file":"test_claude.py","outcome":"passed","test":"test_valid_signature_accepted"},{"file":"test_claude.py","outcome":"passed","test":"test_wrong_signature_rejected"}],"some_not_passed":true}},"reference":{"after":{"all_passed":true,"ran":true,"results":[{"file":"test_reference.py","outcome":"passed","test":"test_comparison_is_constant_time"},{"file":"test_reference.py","outcome":"passed","test":"test_genuine_signature_is_accepted"},{"file":"test_reference.py","outcome":"passed","test":"test_wrong_signature_is_rejected"}],"some_not_passed":false},"before":{"all_passed":false,"ran":true,"results":[{"detail":"assertion","file":"test_reference.py","outcome":"not_passed","test":"test_comparison_is_constant_time"},{"file":"test_reference.py","outcome":"passed","test":"test_genuine_signature_is_accepted"},{"file":"test_reference.py","outcome":"passed","test":"test_wrong_signature_is_rejected"}],"some_not_passed":true}}},"verdict":"VERIFIED_FIX"}