Course update: lessons, CTF 0x0011a_cb, and documentation

- 0x0011a_cb (Operation Dark Vector): nation-state CTF redesign with an
  AES-128-ECB sealed target and a plaintext launch origin; RP2350 firmware with
  bearing-driven servo, tri-color LEDs, GSV stats, and a realistic no-fix path
- docs: story-driven classified brief, GDB and Ghidra tutorials with deep
  step-throughs, regenerated artifacts and PDFs
- scripts: docstring standard, AES per-student randomizer, telemetry monitor
- week 3 to week 5 lessons: Ghidra patching tutorial, CMSIS-SVD hardware RE,
  double floating-point and GPIO architecture chapters, README structure
This commit is contained in:
Kevin Thomas committed 2026-09-27 14:18:56 -04:00
1 parent 5201ee4b6b
commit 35eacd2c0e
162 files changed
+125658 -232

No files matched your search

+944
View File
@@ -0,0 +1,944 @@
"""Generate hardened demo artifacts for the RP2350 Ouroboros firmware.
This script writes the same JSON schema used by the Rust demo and also emits
the generated C header consumed by the embedded firmware.
"""
import argparse
import json
import platform
import secrets
import sys
from pathlib import Path
from typing import Optional
DEFAULT_PASSPHRASE = (
"orbit olive ladder marble quartz canyon "
"ripple saddle violet ember walnut falcon"
)
DEFAULT_TEXT = "hello"
DEFAULT_OUTPUT_JSON = "scripts/demo_artifact.json"
DEFAULT_OUTPUT_HEADER = "include/demo_artifact.h"
DEFAULT_MEMORY_KIB = 64
DEFAULT_ITERATIONS = 3
DEFAULT_PARALLELISM = 1
ARTIFACT_FORMAT = "ouroboros-hardened-demo-v1"
_KEY_HELP = "12-word lowercase passphrase"
_TEXT_HELP = "Text to place in payload bytes 1..7"
_OUT_HELP = "Output JSON artifact path"
_HEADER_OUT_HELP = "Output generated C header path"
_FROM_JSON_HELP = (
"Load existing JSON artifact and emit header without re-encrypting"
)
_CHECK_HEADER_HELP = (
"Optional path to compare against generated header and fail if stale"
)
_SALT_HEX_HELP = "Optional fixed 16-byte salt as hex"
_NONCE_HEX_HELP = "Optional fixed 24-byte nonce as hex"
_NO_CRLF_HELP = "Do not append CRLF to payload text"
_LED_OFF_HELP = "Encode LED off instead of on"
_MEMORY_HELP = "Argon2 memory cost in KiB"
_ITERATIONS_HELP = "Argon2 time cost"
_PARALLELISM_HELP = "Argon2 parallel lanes"
_POLICY_ERROR = (
"Hardened mode requires exactly 12 lowercase ASCII words in --key."
)
_PAYLOAD_TOO_LONG = (
"Output text is too long for fixed dispatch "
"(max 7 bytes after CRLF handling)."
)
_INVALID_JSON = "Artifact JSON at {0} is invalid JSON."
_MISMATCH_PREFIX = "Detected a Python native-extension architecture mismatch. "
_REINSTALL_DEPS = ("Recreate this virtual environment with a native Python "
"and reinstall deps:")
_REINSTALL_LINES = (
"rm -rf .venv",
"python3 -m venv .venv",
"source .venv/bin/activate",
"python3 -m pip install -U pip setuptools wheel",
"python3 -m pip install argon2-cffi pynacl",
)
_HEADER_TEMPLATE = """// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
// This file is generated by scripts/dec.py. Do not edit by hand.
#ifndef DEMO_ARTIFACT_H
#define DEMO_ARTIFACT_H
#include <stdint.h>
#define DEMO_ARTIFACT_FORMAT "{artifact_format}"
#define DEMO_MEMORY_KIB {memory_kib}u
#define DEMO_ITERATIONS {iterations}u
#define DEMO_PARALLELISM {parallelism}u
static const uint8_t DEMO_SALT[16] = {{
{salt_body}
}};
static const uint8_t DEMO_NONCE[24] = {{
{nonce_body}
}};
static const uint8_t DEMO_CIPHERTEXT_AND_TAG[64] = {{
{cipher_body}
}};
#endif // DEMO_ARTIFACT_H
"""
def _raise_dependency_error(package_name, install_hint, exc):
"""Raise a RuntimeError with environment-aware dependency diagnostics.
Parameters
----------
package_name : str
Package common name for the error message.
install_hint : str
Pip install command in the error message.
exc : Exception
Import error observed while loading the native module.
Returns
-------
None
"""
message = "Hardened mode requires {0}. Install with: {1}".format(
package_name, install_hint)
message += _reinstall_message() if _is_arch_mismatch(exc) else ""
raise RuntimeError(message) from exc
def _is_arch_mismatch(exc):
"""Report whether the interpreter likely has a native-extension mismatch.
Parameters
----------
exc : Exception
Import error observed while loading the native module.
Returns
-------
bool
True when the error text matches a native architecture mismatch.
"""
detail = str(exc)
return (
"incompatible architecture" in detail
or "_cffi_backend" in detail
or "mach-o file, but is an incompatible architecture" in detail
)
def _reinstall_message():
"""Build the native-interpreter reinstall diagnostic text.
Parameters
----------
None
Returns
-------
str
Newline-delimited machine and reinstall details, or an empty string.
"""
machine = platform.machine()
head = (_MISMATCH_PREFIX
+ "Current interpreter reports machine=" + machine
+ ", executable=" + sys.executable + ".")
lines = [" {0}".format(item) for item in _REINSTALL_LINES]
return ("\n" + head + "\n" + _REINSTALL_DEPS + "\n"
+ "\n".join(lines))
def _is_policy_compliant(passphrase):
"""Return True when passphrase is exactly 12 lowercase ASCII words.
Parameters
----------
passphrase : str
Candidate operator passphrase.
Returns
-------
bool
True when the passphrase satisfies the gate policy.
"""
words = passphrase.split()
if len(words) != 12:
return False
return all(
word and all(ch.isascii() and ch.islower() for ch in word)
for word in words
)
def _build_payload(text_str, led_on=True, append_crlf=True):
"""Build the fixed 48-byte payload dispatched by the firmware.
Parameters
----------
text_str : str
Console text placed in payload bytes 1..7.
led_on : bool
True turns the LED byte on, False leaves it off.
append_crlf : bool
True appends CRLF to the console text.
Returns
-------
bytes
Fixed 48-byte dispatch payload.
"""
tx_bytes = text_str.encode() + (b"\r\n" if append_crlf else b"")
if len(tx_bytes) > 7:
raise ValueError(_PAYLOAD_TOO_LONG)
payload = bytearray(48)
payload[0] = 1 if led_on else 0
payload[1:1 + len(tx_bytes)] = tx_bytes
return bytes(payload)
def _resolve_salt_nonce(salt, nonce):
"""Confirm or generate the 16-byte salt and 24-byte nonce.
Parameters
----------
salt : bytes or None
Optional fixed salt value.
nonce : bytes or None
Optional fixed nonce value.
Returns
-------
tuple
Confirmed (salt, nonce) byte values.
"""
salt_word = secrets.token_bytes(16) if salt is None else salt
nonce_word = secrets.token_bytes(24) if nonce is None else nonce
if len(salt_word) != 16:
raise ValueError("Hardened salt must be exactly 16 bytes.")
if len(nonce_word) != 24:
raise ValueError("Hardened nonce must be exactly 24 bytes.")
return salt_word, nonce_word
def _optional_hex(value, length, label):
"""Decode an optional hex argument, leaving absent values as None.
Parameters
----------
value : str or None
Hex string supplied on the command line.
length : int
Expected decoded byte length.
label : str
Field name used in validation errors.
Returns
-------
bytes or None
Decoded bytes, or None when value is absent.
"""
return _hex_decode(value, length, label) if value else None
def _load_argon2():
"""Load the Argon2 low-level binding with dependency diagnostics.
Parameters
----------
None
Returns
-------
tuple
Argon2 Type enum and hash_secret_raw callable.
"""
try:
from argon2.low_level import Type, hash_secret_raw
except ImportError as exc:
install_hint = "python3 -m pip install argon2-cffi"
_raise_dependency_error("argon2-cffi", install_hint, exc)
return Type, hash_secret_raw
def _load_nacl_encrypt():
"""Load the XChaCha20-Poly1305 encrypt binding with diagnostics.
Parameters
----------
None
Returns
-------
callable
PyNaCl crypto_aead_xchacha20poly1305_ietf_encrypt function.
"""
try:
from nacl.bindings import (
crypto_aead_xchacha20poly1305_ietf_encrypt as encrypt,
)
except ImportError as exc:
install_hint = "python3 -m pip install pynacl"
_raise_dependency_error("PyNaCl", install_hint, exc)
return encrypt
def _derive_hardened_key(passphrase, salt, memory_kib, iterations,
parallelism):
"""Derive a 32-byte key with Argon2id.
Parameters
----------
passphrase : str
Policy-compliant operator passphrase.
salt : bytes
16-byte Argon2 salt.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
bytes
Derived 32-byte key.
"""
arg2_type, hash_secret_raw = _load_argon2()
args = (
passphrase.encode(), salt, iterations, memory_kib,
parallelism, 32, arg2_type,
)
return hash_secret_raw(*args)
def _entry_key(phrase, seed, memory_kib, iterations, parallelism):
"""Derive the entry key word for the hardened artifact.
Parameters
----------
phrase : str
Policy-compliant operator passphrase.
seed : bytes
16-byte Argon2 salt.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
bytes
Derived 32-byte entry key.
"""
return _derive_hardened_key(
phrase, seed, memory_kib, iterations, parallelism
)
def _build_key_material(phrase, salt, nonce, memory_kib, iterations,
parallelism):
"""Resolve salt, nonce, and the entry key.
Parameters
----------
phrase : str
Policy-compliant operator passphrase.
salt : bytes or None
Optional fixed salt value.
nonce : bytes or None
Optional fixed nonce value.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
tuple
Resolved (salt, nonce, key) values.
"""
seed, nonce_word = _resolve_salt_nonce(salt, nonce)
key = _entry_key(phrase, seed, memory_kib, iterations, parallelism)
return seed, nonce_word, key
def build_hardened_entry(
key_str,
text_str,
led_on=True,
append_crlf=True,
salt: Optional[bytes] = None,
nonce: Optional[bytes] = None,
memory_kib=DEFAULT_MEMORY_KIB,
iterations=DEFAULT_ITERATIONS,
parallelism=DEFAULT_PARALLELISM,
):
"""Build a hardened encrypted entry with Argon2id + XChaCha20-Poly1305.
Parameters
----------
key_str : str
Policy-compliant 12-word operator passphrase.
text_str : str
Console text placed in payload bytes 1..7.
led_on : bool
True turns the LED byte on, False leaves it off.
append_crlf : bool
True appends CRLF to the console text.
salt : bytes or None
Optional fixed 16-byte salt.
nonce : bytes or None
Optional fixed 24-byte nonce.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
tuple
Resulting (salt, nonce, ciphertext_and_tag) values.
"""
if not _is_policy_compliant(key_str):
raise ValueError(_POLICY_ERROR)
salt_word, nonce_word, key = _build_key_material(
key_str, salt, nonce, memory_kib, iterations, parallelism)
payload = _build_payload(text_str, led_on, append_crlf)
encrypt = _load_nacl_encrypt()
ciphertext_and_tag = encrypt(payload, b"", nonce_word, key)
return salt_word, nonce_word, ciphertext_and_tag
def _hex_decode(value, expected_len, label):
"""Decode a hex string and validate the expected byte length.
Parameters
----------
value : str
Hex string to decode.
expected_len : int
Required decoded byte length.
label : str
Field name used in validation errors.
Returns
-------
bytes
Decoded bytes of the expected length.
"""
try:
decoded = bytes.fromhex(value)
except ValueError as exc:
raise ValueError("{0} must be valid hex.".format(label)) from exc
if len(decoded) != expected_len:
message = "{0} must decode to exactly {1} bytes.".format(
label, expected_len)
raise ValueError(message)
return decoded
def _artifact_dict(memory_kib, iterations, parallelism, salt, nonce, cipher):
"""Compose the canonical artifact dictionary.
Parameters
----------
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
salt : bytes
16-byte salt.
nonce : bytes
24-byte nonce.
cipher : bytes
64-byte ciphertext and tag.
Returns
-------
dict
Canonical artifact fields with hex-encoded byte values.
"""
return {
"format": ARTIFACT_FORMAT, "memory_kib": memory_kib,
"iterations": iterations, "parallelism": parallelism,
"salt_hex": salt.hex(), "nonce_hex": nonce.hex(),
"ciphertext_and_tag_hex": cipher.hex(),
}
def _format_c_array(data, width=8):
"""Format bytes as an indented C array literal body.
Parameters
----------
data : bytes
Bytes to serialize as a C array.
width : int
Byte values emitted per source line.
Returns
-------
str
Indented, comma-joined C array body.
"""
items = [f"0x{value:02X}u" for value in data]
rows = [", ".join(items[offset:offset + width])
for offset in range(0, len(items), width)]
return ",\n".join(" " + row for row in rows)
def _render_header(artifact):
"""Render the generated firmware header text from the artifact.
Parameters
----------
artifact : dict
Canonical artifact dictionary.
Returns
-------
str
Complete generated C header text.
"""
cipher = bytes.fromhex(artifact["ciphertext_and_tag_hex"])
return _HEADER_TEMPLATE.format(
artifact_format=ARTIFACT_FORMAT, memory_kib=artifact["memory_kib"],
iterations=artifact["iterations"], parallelism=artifact["parallelism"],
salt_body=_format_c_array(bytes.fromhex(artifact["salt_hex"])),
nonce_body=_format_c_array(bytes.fromhex(artifact["nonce_hex"])),
cipher_body=_format_c_array(cipher),
)
def _write_header(path, artifact):
"""Write the generated firmware header from the hardened artifact.
Parameters
----------
path : str
Destination header file path.
artifact : dict
Canonical artifact dictionary.
Returns
-------
Path
Resolved destination header path.
"""
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(_render_header(artifact), encoding="utf-8")
return output_path.resolve()
def _parse_json_file(path):
"""Load and parse the artifact JSON file.
Parameters
----------
path : str
Artifact JSON file path.
Returns
-------
dict
Parsed JSON document.
"""
raw = Path(path).read_text(encoding="utf-8")
try:
parsed = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(_INVALID_JSON.format(path)) from exc
return parsed
def _validate_artifact_format(parsed):
"""Reject artifact JSON with an unexpected format marker.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
None
"""
actual = parsed.get("format")
if actual != ARTIFACT_FORMAT:
raise ValueError(
"Artifact format must be '{0}', got '{1}'.".format(
ARTIFACT_FORMAT, actual))
def _parsed_ints(parsed):
"""Parse the integer cost fields from artifact JSON.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
list
Parsed memory_kib, iterations, and parallelism integer values.
"""
try:
int_names = ("memory_kib", "iterations", "parallelism")
return [int(parsed[name]) for name in int_names]
except (KeyError, TypeError, ValueError) as exc:
raise ValueError(
"Artifact must include integer memory_kib, "
"iterations, and parallelism fields.") from exc
def _artifact_ints(parsed):
"""Unpack the three integer cost fields from artifact JSON.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
tuple
(memory_kib, iterations, parallelism) integer values.
"""
values = _parsed_ints(parsed)
return values[0], values[1], values[2]
def _artifact_from_parsed(parsed):
"""Reconstruct a canonical artifact dict from parsed JSON.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
dict
Canonical artifact dictionary.
"""
_validate_artifact_format(parsed)
memory_kib, iterations, parallelism = _artifact_ints(parsed)
cipher_field = "ciphertext_and_tag_hex"
salt = _hex_decode(parsed.get("salt_hex", ""), 16, "salt_hex")
nonce = _hex_decode(parsed.get("nonce_hex", ""), 24, "nonce_hex")
cipher = _hex_decode(parsed.get(cipher_field, ""), 64, cipher_field)
return _artifact_dict(
memory_kib, iterations, parallelism, salt, nonce, cipher)
def _load_artifact_json(path):
"""Load and validate a hardened artifact JSON for header generation.
Parameters
----------
path : str
Artifact JSON file path.
Returns
-------
dict
Canonical artifact dictionary.
"""
parsed = _parse_json_file(path)
return _artifact_from_parsed(parsed)
def _check_header_match(generated_path, expected_path):
"""Fail when the generated header does not match an expected file.
Parameters
----------
generated_path : str
Generated header file path.
expected_path : str
Expected committed header file path.
Returns
-------
None
"""
generated = Path(generated_path).read_text(encoding="utf-8")
expected = Path(expected_path).read_text(encoding="utf-8")
if generated != expected:
raise RuntimeError(
"Generated header does not match committed "
"include/demo_artifact.h. Regenerate and commit "
"updated artifacts with scripts/dec.py.")
def _print_header_check(header_path, expected_path):
"""Print the verified header match result.
Parameters
----------
header_path : str
Generated header file path.
expected_path : str
Expected committed header file path.
Returns
-------
None
"""
_check_header_match(header_path, expected_path)
print("Verified header matches: {0}".format(Path(expected_path).resolve()))
def _parse_args():
"""Parse command-line arguments.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed command-line arguments.
"""
parser = argparse.ArgumentParser(description=__doc__)
for argument_group in _ARGUMENT_GROUPS:
for name, kwargs in argument_group:
parser.add_argument(name, **kwargs)
return parser.parse_args()
def _build_from_args(args, salt, nonce):
"""Build the hardened entry from parsed arguments.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
salt : bytes or None
Optional fixed salt value.
nonce : bytes or None
Optional fixed nonce value.
Returns
-------
tuple
Resulting (salt, nonce, ciphertext_and_tag) values.
"""
return build_hardened_entry(
key_str=args.key, text_str=args.text, led_on=not args.led_off,
append_crlf=not args.no_crlf, salt=salt, nonce=nonce,
memory_kib=args.memory_kib, iterations=args.iterations,
parallelism=args.parallelism,
)
def _flush_outputs(args, salt, nonce, cipher):
"""Write the artifact JSON and header, then return the header path.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
salt : bytes
16-byte salt.
nonce : bytes
24-byte nonce.
cipher : bytes
64-byte ciphertext and tag.
Returns
-------
Path
Resolved generated header path.
"""
json_path, artifact = _write_demo_json(
args.out, args.memory_kib, args.iterations, args.parallelism,
salt, nonce, cipher)
header_path = _write_header(args.header_out, artifact)
print("Wrote hardened demo artifact JSON: {0}".format(json_path))
print("Wrote generated firmware header: {0}".format(header_path))
return header_path
def _write_demo_json(path, memory_kib, iterations, parallelism, salt,
nonce, ciphertext_and_tag):
"""Write the hardened JSON artifact consumed by docs and validation.
Parameters
----------
path : str
Destination JSON file path.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
salt : bytes
16-byte salt.
nonce : bytes
24-byte nonce.
ciphertext_and_tag : bytes
64-byte ciphertext and tag.
Returns
-------
tuple
Resolved (path, artifact) values.
"""
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
artifact = _artifact_dict(
memory_kib, iterations, parallelism, salt, nonce, ciphertext_and_tag)
text = json.dumps(artifact, indent=2) + "\n"
output_path.write_text(text, encoding="utf-8")
return output_path.resolve(), artifact
def _flush_from_json(args):
"""Write the header from an existing artifact JSON.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
Path
Resolved generated header path.
"""
artifact = _load_artifact_json(args.from_json)
header_path = _write_header(args.header_out, artifact)
print("Wrote generated firmware header: {0}".format(header_path))
return header_path
def _run_from_json(args):
"""Generate the header only from committed artifact JSON.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
None
"""
header_path = _flush_from_json(args)
if args.check_header_path:
_print_header_check(header_path, args.check_header_path)
def _run_from_generate(args):
"""Encrypt fresh artifact material, then emit JSON and the header.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
None
"""
salt = _optional_hex(args.salt_hex, 16, "salt_hex")
nonce = _optional_hex(args.nonce_hex, 24, "nonce_hex")
header_path = _flush_outputs(
args, *_build_from_args(args, salt, nonce),
)
if args.check_header_path:
_print_header_check(header_path, args.check_header_path)
def main():
"""Generate the hardened artifact JSON and C header.
Parameters
----------
None
Returns
-------
None
"""
args = _parse_args()
if args.from_json:
_run_from_json(args)
else:
_run_from_generate(args)
_ARGUMENT_GROUPS = (
(
("--key", dict(default=DEFAULT_PASSPHRASE, help=_KEY_HELP)),
("--salt-hex", dict(help=_SALT_HEX_HELP)),
("--nonce-hex", dict(help=_NONCE_HEX_HELP)),
),
(
("--text", dict(default=DEFAULT_TEXT, help=_TEXT_HELP)),
("--out", dict(default=DEFAULT_OUTPUT_JSON, help=_OUT_HELP)),
("--header-out",
dict(default=DEFAULT_OUTPUT_HEADER, help=_HEADER_OUT_HELP)),
("--from-json", dict(help=_FROM_JSON_HELP)),
("--no-crlf", dict(action="store_true", help=_NO_CRLF_HELP)),
("--led-off", dict(action="store_true", help=_LED_OFF_HELP)),
),
(
("--memory-kib",
dict(type=int, default=DEFAULT_MEMORY_KIB, help=_MEMORY_HELP)),
("--iterations",
dict(type=int, default=DEFAULT_ITERATIONS, help=_ITERATIONS_HELP)),
("--parallelism",
dict(type=int, default=DEFAULT_PARALLELISM,
help=_PARALLELISM_HELP)),
("--check-header-path", dict(help=_CHECK_HEADER_HELP)),
),
)
if __name__ == "__main__":
main()
+9
View File
@@ -0,0 +1,9 @@
{
"format": "ouroboros-hardened-demo-v1",
"memory_kib": 64,
"iterations": 3,
"parallelism": 1,
"salt_hex": "f2d518639a82019dc2d7afa5cdb6d871",
"nonce_hex": "1cef790d779e7c04e7f066dd90d080708797671f79efc4e4",
"ciphertext_and_tag_hex": "2c23b27e9562b8ed9e08e06dd99db4913e819a778bb47b71bc661e6e731a8154cdb536a4767e9bf8533e031db8e5ae7aadb431cf12d9f9c45fa9b94b80dcbbde"
}
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Verify every technical claim of Operation Copperhead against CTF-02.bin.
Exits 0 only when every address, byte, hash, and derived value in CTF-R.md and
CTF-S.md matches the shipped image and the compiled ELF.
"""
import hashlib
import struct
import sys
from pathlib import Path
BASE = 0x10000000
ROOT = Path(__file__).resolve().parent.parent
BIN = ROOT / "CTF-02.bin"
UF2 = ROOT / "CTF-02.uf2"
EXPECTED_BIN_SHA = "85330c37cd0897746b1af447e4bac371dde2042abd2d61d58a61fe2a8eef3537"
EXPECTED_UF2_SHA = "f3cd4840260db820d792758cecacc5297bef1971b9eacf7601279256d8af1eab"
FRAME_THRESHOLD_A = 0x10000302
FRAME_THRESHOLD_B = 0x10000312
FRAME_TRACK = 0x1000C4B8
FRAME_BLOCKLEN = 0x1000C4F0
FRAME_SIGNALKEY = 0x1000C51C
FRAME_GATE = 0x1000C544
FRAME_AUTH = 0x1000C57C
FRAME_OK = 0x1000C438
FRAME_MISMATCH = 0x1000C43C
FRAME_SPEC_LITERAL = 0x100004FC
FRAME_DOUBLE = 0x1000EC60
FRAME_SEED = 0x1000EC70
FRAME_SALT = 0x1000CEEC
FRAME_NONCE = 0x1000CED4
FRAME_CT = 0x1000CE94
DOUBLE_3_2 = bytes.fromhex("9A99999999990940")
DOUBLE_0_32 = bytes.fromhex("7B14AE47E17AD43F")
SEED_BAD = bytes.fromhex("0A0A0A0A")
SEED_GOOD = bytes.fromhex("7465206B")
SPEC_VALUE = 0x2D879291
BUG_KEY = 0x915DCFF8
RESULTS = []
def check(label, ok, detail=""):
"""Record one verification result.
Parameters
----------
label : str
Human-readable check name.
ok : bool
Whether the check passed.
detail : str
Extra context printed with the result.
Returns
-------
None
"""
RESULTS.append(ok)
print(f"[{'PASS' if ok else 'FAIL'}] {label} {detail}")
def rotl(v, s):
"""Rotate a 32-bit value left."""
return ((v << s) & 0xFFFFFFFF) | (v >> (32 - s))
def qr_phase(a, b, c, d, s):
"""Apply one ARX phase of a ChaCha quarter round."""
a = (a + b) & 0xFFFFFFFF
d ^= a
d = rotl(d, s)
c = (c + d) & 0xFFFFFFFF
b ^= c
b = rotl(b, s)
return a, b, c, d
def derive(seed, iv):
"""Derive the firmware signal key from a seed and IV."""
a, b, c, d = seed, iv, 0x61707865, 0x3320646E
for _ in range(4):
for s in (16, 12, 8, 7):
a, b, c, d = qr_phase(a, b, c, d, s)
return (a ^ d) & 0xFFFFFFFF
def main():
"""Run all verification checks.
Returns
-------
int
Zero when every check passes, else one.
"""
data = BIN.read_bytes()
check("CTF-02.bin SHA-256", hashlib.sha256(data).hexdigest() == EXPECTED_BIN_SHA)
check("CTF-02.uf2 SHA-256",
hashlib.sha256(UF2.read_bytes()).hexdigest() == EXPECTED_UF2_SHA)
check("CTF-02.bin size", len(data) == 62308, f"({len(data)})")
check("vector table", data[0:32].hex() ==
"002008205b0100101b0100101d01001011010010110100101101001011010010")
check("initial SP", struct.unpack("<I", data[0:4])[0] == 0x20082000)
check("reset vector", struct.unpack("<I", data[4:8])[0] == 0x1000015B)
check("compare site A immediate", data[FRAME_THRESHOLD_A - BASE:
FRAME_THRESHOLD_A - BASE + 2] == bytes.fromhex("5E2B"))
check("compare site B immediate", data[FRAME_THRESHOLD_B - BASE:
FRAME_THRESHOLD_B - BASE + 2] == bytes.fromhex("5E2B"))
check("TRACK banner string", data[FRAME_TRACK - BASE:FRAME_TRACK - BASE + 13]
== b"TRACK: NORMAL")
check("BLOCK LENGTH string", data[FRAME_BLOCKLEN - BASE:
FRAME_BLOCKLEN - BASE + 18] == b"BLOCK LENGTH: %u M")
check("SIGNAL KEY string", data[FRAME_SIGNALKEY - BASE:
FRAME_SIGNALKEY - BASE + len(b"SIGNAL KEY: 0x%08X %s")]
== b"SIGNAL KEY: 0x%08X %s")
check("gate message", data[FRAME_GATE - BASE:FRAME_GATE - BASE
+ len(b"Enter exactly 12 lowercase words separated by spaces.")]
== b"Enter exactly 12 lowercase words separated by spaces.")
check("AUTHORITY FRAME string", data[FRAME_AUTH - BASE:FRAME_AUTH - BASE
+ len(b"AUTHORITY FRAME: VERIFIED")] == b"AUTHORITY FRAME: VERIFIED")
check("OK string", data[FRAME_OK - BASE:FRAME_OK - BASE + 3] == b"OK\x00")
check("MISMATCH string", data[FRAME_MISMATCH - BASE:
FRAME_MISMATCH - BASE + 9] == b"MISMATCH\x00")
check("SIGNAL_SPEC literal", struct.unpack("<I", data[
FRAME_SPEC_LITERAL - BASE:FRAME_SPEC_LITERAL - BASE + 4])[0] == SPEC_VALUE)
check("3.2 double bytes", data[FRAME_DOUBLE - BASE:FRAME_DOUBLE - BASE + 8]
== DOUBLE_3_2)
check("0.32 target bytes", DOUBLE_0_32 == bytes.fromhex("7B14AE47E17AD43F"))
check("bug seed bytes", data[FRAME_SEED - BASE:FRAME_SEED - BASE + 4] == SEED_BAD)
check("good seed bytes", SEED_GOOD == bytes.fromhex("7465206B"))
for label, addr in (("salt", FRAME_SALT), ("nonce", FRAME_NONCE),
("ciphertext", FRAME_CT)):
check(f"{label} present", data[addr - BASE:addr - BASE + 16] != b"\x00" * 16)
iv = derive(0x6B206574, 0)
check("derived IV", iv == 0x43C974F6, f"(0x{iv:08X})")
check("bug-derived key", derive(0x0A0A0A0A, iv) == BUG_KEY,
f"(0x{derive(0x0A0A0A0A, iv):08X})")
honest = derive(0x6B206574, iv)
check("honest key equals SIGNAL_SPEC", honest == SPEC_VALUE, f"(0x{honest:08X})")
total = sum(RESULTS)
print(f"\n{total}/{len(RESULTS)} checks passed")
return 0 if total == len(RESULTS) else 1
if __name__ == "__main__":
sys.exit(main())