mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-01 21:50:16 +02:00
- 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
945 lines
25 KiB
Python
945 lines
25 KiB
Python
"""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()
|