mirror of
https://github.com/ljagiello/ctf-skills.git
synced 2026-09-14 14:17:37 +08:00
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:
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user