From a204a36e87366bb5fe7c421d6469fec78350bb67 Mon Sep 17 00:00:00 2001 From: Lukasz Jagiello Date: Sat, 12 Sep 2026 10:36:50 -0700 Subject: [PATCH] fix: resolve ruff E501 and dead hackmd link CI failures (#94) * fix(scripts): wrap long lines in verify_crypto_examples for ruff The Lint Scripts workflow failed with 18 E501 line-too-long errors in scripts/verify_crypto_examples.py, and the follow-up `ruff format --check` step would have failed on the same file. Ran `ruff format` to fix the wrappable lines, then split the remaining long help string, comment, and f-string by hand. No behavioral change. Claude-Session: https://claude.ai/code/session_01WbafsCGmhJNgftRAg9Xrw8 * fix(crypto): remove dead hackmd.io link from modern-ciphers-4 The Link Checker workflow failed because https://hackmd.io/vq8pc6 returns 404 (two occurrences in ctf-crypto/modern-ciphers-4.md). Keep the picoCTF 2025 attribution as plain text and link the References line to RFC 8439 directly, which was already cited alongside it. Claude-Session: https://claude.ai/code/session_01WbafsCGmhJNgftRAg9Xrw8 --- ctf-crypto/modern-ciphers-4.md | 4 +- scripts/verify_crypto_examples.py | 99 +++++++++++++++++++++++++------ 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/ctf-crypto/modern-ciphers-4.md b/ctf-crypto/modern-ciphers-4.md index a40cd52..b84b24b 100644 --- a/ctf-crypto/modern-ciphers-4.md +++ b/ctf-crypto/modern-ciphers-4.md @@ -19,7 +19,7 @@ ChaCha20-Poly1305 nonce reuse (RFC 8439 $2^{130}-5$), partitioning-oracle / key- **Clamping:** RFC 8439 clamps $r$ bytes: `r &= 0x0ffffffc0fffffff...` — top 4 bits of bytes 3,7,11,15 cleared; low 2 bits of bytes 3,7,11,15 cleared. This reduces candidates to $2^{106}$ but still enumerable among polynomial roots. -**Reference:** picoCTF 2025 `ChaCha20-Poly1305 nonce reuse` — writeup [hackmd vq8pc6](https://hackmd.io/vq8pc6) demonstrates exactly this 2-msg forgery pipeline; CTR xor cancels, Poly1305 polynomial solves for $r$, then forges $tag'$ for new $ct'$. +**Reference:** picoCTF 2025 `ChaCha20-Poly1305 nonce reuse` — the challenge writeup demonstrates exactly this 2-msg forgery pipeline; CTR xor cancels, Poly1305 polynomial solves for $r$, then forges $tag'$ for new $ct'$. ```python # ChaCha20-Poly1305 nonce reuse — recover Poly1305 r via galois + forge tag' for ct' @@ -200,7 +200,7 @@ tag2 = "c3d2e1f0..." **Key insight:** Over $2^{130}-5$ the Poly1305 equation is linear in $s$ and polynomial in $r$. Nonce reuse leaks $r$ as a root of $\Delta Poly(r)- \Delta tag =0$; clamping onlyreduces the search, never prevents it. Identical to AES-GCM forbidden attack but in a prime field — use `galois.GF(2**130-5)` (primary) or `sympy.Poly(..., modulus=p)` (fallback), filter clamped candidates, then forge $tag' = Poly1305(ct',r)+s$. -**References:** RFC 8439 §2.5/§2.8, [picoCTF 2025 hackmd vq8pc6](https://hackmd.io/vq8pc6) — ChaCha20-Poly1305 nonce reuse walkthrough; [RFC 8439 errata](https://www.rfc-editor.org/rfc/rfc8439). +**References:** [RFC 8439 §2.5/§2.8](https://www.rfc-editor.org/rfc/rfc8439) — ChaCha20-Poly1305 AEAD construction and Poly1305 key generation; picoCTF 2025 `ChaCha20-Poly1305 nonce reuse` challenge walkthrough. --- diff --git a/scripts/verify_crypto_examples.py b/scripts/verify_crypto_examples.py index ab692a4..9695932 100755 --- a/scripts/verify_crypto_examples.py +++ b/scripts/verify_crypto_examples.py @@ -17,6 +17,7 @@ Prints: Checked N fences: X syntax OK, Y skipped (reason) Stdlib only: re, subprocess, pathlib, ast, py_compile, tempfile, argparse, json """ + from __future__ import annotations import argparse @@ -87,7 +88,9 @@ def should_skip_execution(info: str, body: str) -> tuple[bool, str]: def _wrap_as_function(body: str) -> str: """Wrap body in a function to allow top-level return/break.""" # indent body by 4 spaces and wrap - indented = "\n".join(" " + line if line.strip() else line for line in body.splitlines()) + indented = "\n".join( + " " + line if line.strip() else line for line in body.splitlines() + ) return f"def _verify_wrapper():\n{indented}\n" @@ -105,7 +108,13 @@ def syntax_check(body: str, filename: str) -> tuple[bool, str | None]: except SyntaxError as e: msg = (e.msg or "").lower() # fragments with return/break/continue/yield outside function/loop - if "return" in msg or "yield" in msg or "break" in msg or "continue" in msg or "await" in msg: + if ( + "return" in msg + or "yield" in msg + or "break" in msg + or "continue" in msg + or "await" in msg + ): wrapped = _wrap_as_function(body) try: ast.parse(wrapped, filename=filename) @@ -117,7 +126,9 @@ def syntax_check(body: str, filename: str) -> tuple[bool, str | None]: # 2) py_compile via tempfile + subprocess (spec compliance) # Also catches encoding issues. Uses `python -m py_compile`. - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as tf: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as tf: tf.write(body) tf.flush() tmp_path = tf.name @@ -132,10 +143,16 @@ def syntax_check(body: str, filename: str) -> tuple[bool, str | None]: if result.returncode != 0: err = result.stderr.strip() or result.stdout.strip() or "py_compile failed" lower_err = err.lower() - if "'return' outside function" in lower_err or "'break' outside loop" in lower_err or "'continue' outside loop" in lower_err: + if ( + "'return' outside function" in lower_err + or "'break' outside loop" in lower_err + or "'continue' outside loop" in lower_err + ): # try wrapped version wrapped = _wrap_as_function(body) - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as tf2: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as tf2: tf2.write(wrapped) tf2.flush() tmp2 = tf2.name @@ -168,7 +185,9 @@ def syntax_check(body: str, filename: str) -> tuple[bool, str | None]: def try_execute(body: str, timeout: int = 5) -> tuple[bool, str | None]: """Execute body in a subprocess with timeout. Returns (ok, error).""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as tf: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False, encoding="utf-8" + ) as tf: tf.write(body) tf.flush() tmp_path = tf.name @@ -195,12 +214,31 @@ def try_execute(body: str, timeout: int = 5) -> tuple[bool, str | None]: def main() -> None: - parser = argparse.ArgumentParser(description="Verify Python fences in ctf-crypto markdown files") - parser.add_argument("--root", default="ctf-crypto", help="Root dir to scan (default: ctf-crypto)") - parser.add_argument("--strict", action="store_true", help="Fail on execution errors (default: only syntax errors fail)") - parser.add_argument("--json", action="store_true", help="Emit JSON summary to stdout") - parser.add_argument("--verbose", action="store_true", help="Verbose per-fence output") - parser.add_argument("--execute", action="store_true", help="Attempt execution of non-skipped fences (default: syntax-only, execution is opt-in unless --strict)") + parser = argparse.ArgumentParser( + description="Verify Python fences in ctf-crypto markdown files" + ) + parser.add_argument( + "--root", default="ctf-crypto", help="Root dir to scan (default: ctf-crypto)" + ) + parser.add_argument( + "--strict", + action="store_true", + help="Fail on execution errors (default: only syntax errors fail)", + ) + parser.add_argument( + "--json", action="store_true", help="Emit JSON summary to stdout" + ) + parser.add_argument( + "--verbose", action="store_true", help="Verbose per-fence output" + ) + parser.add_argument( + "--execute", + action="store_true", + help=( + "Attempt execution of non-skipped fences " + "(default: syntax-only, execution is opt-in unless --strict)" + ), + ) args = parser.parse_args() root = Path(args.root) @@ -226,7 +264,9 @@ def main() -> None: text = md.read_text(encoding="utf-8") except Exception as e: # unreadable file — count as failure in strict, else warn - failed.append({"file": str(md), "info": "", "error": f"cannot read: {e}", "line": 0}) + failed.append( + {"file": str(md), "info": "", "error": f"cannot read: {e}", "line": 0} + ) continue for m in FENCE_RE.finditer(text): @@ -259,7 +299,15 @@ def main() -> None: # Normal syntax check ok, err = syntax_check(body, filename) if not ok: - failed.append({"file": str(md), "info": info, "error": err, "line": lineno, "body_preview": body[:200]}) + failed.append( + { + "file": str(md), + "info": info, + "error": err, + "line": lineno, + "body_preview": body[:200], + } + ) if args.verbose: print(f"[syntax FAIL] {filename}: {err}") continue @@ -296,12 +344,21 @@ def main() -> None: exec_body = _wrap_as_function(body) + "\n_verify_wrapper()\n" ok_exec, exec_err = try_execute(exec_body, timeout=5) if not ok_exec: - exec_failed.append({"file": str(md), "info": info, "error": exec_err, "line": lineno}) + exec_failed.append( + {"file": str(md), "info": info, "error": exec_err, "line": lineno} + ) if args.verbose: print(f" -> exec FAIL: {exec_err}") if args.strict: # in strict+execute mode, execution failure is also a failure - failed.append({"file": str(md), "info": info, "error": f"execution: {exec_err}", "line": lineno}) + failed.append( + { + "file": str(md), + "info": info, + "error": f"execution: {exec_err}", + "line": lineno, + } + ) else: if args.verbose: print(" -> exec OK") @@ -310,7 +367,8 @@ def main() -> None: # Summary # ------------------------------------------------------------------ skipped_total = skipped_sage + skipped_network + skipped_verify - # Build human summary matching spec: "Checked 40 fences: 38 syntax OK, 2 skipped (oracle/network)" + # Build human summary matching spec: + # "Checked 40 fences: 38 syntax OK, 2 skipped (oracle/network)" # Include breakdown when relevant reasons: list[str] = [] if skipped_sage: @@ -325,7 +383,10 @@ def main() -> None: # Always print summary line in expected format # Use wording that contains "Checked N fences:" and "syntax OK" for test harness - summary_line = f"Checked {total} fences: {syntax_ok} syntax OK, {skipped_total} skipped ({reason_str})" + summary_line = ( + f"Checked {total} fences: {syntax_ok} syntax OK, " + f"{skipped_total} skipped ({reason_str})" + ) if failed: summary_line += f", {len(failed)} failed" if args.execute: @@ -352,7 +413,7 @@ def main() -> None: print("\nFailures:", file=sys.stderr) for f in failed: loc = f"{f['file']}:{f['line']}" - print(f" {loc} [{f.get('info','')}] {f['error']}", file=sys.stderr) + print(f" {loc} [{f.get('info', '')}] {f['error']}", file=sys.stderr) if args.verbose and "body_preview" in f: preview = f["body_preview"].replace("\n", "\\n")[:300] print(f" preview: {preview}", file=sys.stderr)