"""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 #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()