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
This commit is contained in:
Lukasz Jagiello
2026-09-12 10:36:50 -07:00
committed by GitHub
parent 61c2efe0e8
commit a204a36e87
2 changed files with 82 additions and 21 deletions
+2 -2
View File
@@ -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.
---
+80 -19
View File
@@ -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)