diff --git a/tools/scripts/image-peek.py b/tools/scripts/image-peek.py new file mode 100644 index 0000000000..71eb789f77 --- /dev/null +++ b/tools/scripts/image-peek.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# +# Usage: +# usage: image-peek.py [-h] [--header-size HEADER_SIZE] [--dump-payload OUT] [--verify-hash] [--verify-sig PUBKEY] [--alg {ecdsa-p256,ed25519}] image +# +# Example: +# ./tools/scripts/image-peek.py ./test_v1_signed.bin --verify-sig ./keystore_spki.der --alg ecdsa-p256 + +import argparse, struct, hashlib, sys, datetime +from pathlib import Path + +TYPE_NAMES = { + 0x0001: "version", + 0x0002: "timestamp", + 0x0003: "hash", + 0x0004: "attr", + 0x0010: "pubkey_hint", + 0x0020: "signature", +} + +def read_file(path: Path) -> bytes: + return path.read_bytes() + +def parse_header(data: bytes, header_size: int = 0x100): + if len(data) < 8: + raise ValueError("Input too small to contain header") + magic = data[0:4] + size_le = struct.unpack(" header_size: + break + t = struct.unpack(" header_size: + break + v = data[off:off+l] + off += l + tlvs.append((t, l, v)) + return {"magic": magic, "size": size_le, "header_size": header_size, "tlvs": tlvs} + +def tlv_dict(tlvs): + d = {} + for (t, l, v) in tlvs: + d.setdefault(t, []).append((l, v)) + return d + +# add this helper near the top-level functions, e.g., after tlv_dict() +def find_tlv(data: bytes, header_size: int, ttype: int): + """ + Scan the header TLV area and return (value_offset, value_len, tlv_start_offset) + for the first TLV matching 'ttype'. Returns None if not found. + """ + off = 8 # skip magic(4) + size(4) + while off + 4 <= header_size: + # skip padding bytes 0xFF + while off < header_size and data[off] == 0xFF: + off += 1 + if off + 4 > header_size: + break + t = int.from_bytes(data[off:off+2], "little") + l = int.from_bytes(data[off+2:off+4], "little") + tlv_hdr = off + off += 4 + if off + l > header_size: + break + if t == ttype: + return (off, l, tlv_hdr) # value starts at 'off' + off += l + return None + +def decode_timestamp(v: bytes): + ts = struct.unpack(" {'OK' if ok else 'MISMATCH'}") + if not ok: + print(f"[HASH] expected: {hash_bytes.hex()}") + print(f"[HASH] computed: {calc.hex()}") + + + if args.verify_sig: + if sig is None: + print("[SIG] No signature TLV found (type 0x0020)") + elif hash_bytes is None: + print("[SIG] Cannot verify without hash TLV (type 0x0003)") + else: + pubkey = try_load_public_key(Path(args.verify_sig)) + if isinstance(pubkey, Exception): + print(f"[SIG] Failed to load public key: {pubkey}") + else: + alg = args.alg + if not alg: + if len(sig) == 64 and len(hash_bytes) in (32,48,64): + alg = "ecdsa-p256" + else: + print(f"[SIG] Cannot infer algorithm (sig={len(sig)} bytes, hash={len(hash_bytes) if hash_bytes else 0})") + alg = "ecdsa-p256" + ok, msg = verify_signature(pubkey, alg, hash_bytes, sig) + print(f"[SIG] {msg} (alg={alg})") + +if __name__ == '__main__': + main() diff --git a/tools/scripts/wolfboot-ecc-der-to-spki.py b/tools/scripts/wolfboot-ecc-der-to-spki.py new file mode 100644 index 0000000000..418ff39142 --- /dev/null +++ b/tools/scripts/wolfboot-ecc-der-to-spki.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# Convert wolfBoot raw/public-key container to standard SPKI DER/PEM, next to input. +# Usage: +# +# ./tools/scripts/wolfboot-ecc-der-to-spki.py ./tools/keytools/keystore.der +# +# Optional: +# --curve p256|p384|p521 (only needed if auto-detect by length is not possible) +# +# Example (from [WOLFBOOT_ROOT]): +# ./tools/scripts/wolfboot-ecc-der-to-spki.py ./tools/keytools/keystore.der +# +import argparse +import sys +from pathlib import Path + +def main(): + ap = argparse.ArgumentParser( + description="Convert a wolfBoot public key file to SPKI DER/PEM next to the input. " + "Understands SPKI DER, raw X||Y (64/96/132), SEC1 0x04||X||Y (65/97/133), " + "and wolfBoot 16+X||Y containers (80/112/148)." + ) + ap.add_argument("input", help="Path to input public key file") + ap.add_argument("--curve", choices=["p256", "p384", "p521"], default=None, + help="Curve override if auto-detect by size is not possible") + args = ap.parse_args() + + in_path = Path(args.input).resolve() + if not in_path.is_file(): + print("ERROR: input path does not exist or is not a file:", in_path, file=sys.stderr) + sys.exit(2) + raw = in_path.read_bytes() + ln = len(raw) + + # Try SPKI DER first + key_obj = None + try: + from cryptography.hazmat.primitives import serialization + key_obj = serialization.load_der_public_key(raw) + # Success: already SPKI DER + except Exception: + key_obj = None + + if key_obj is None: + # Not SPKI DER; normalize into SEC1 uncompressed, then import with curve + # Cases: + # 1) raw X||Y (64/96/132) + # 2) SEC1 0x04||X||Y (65/97/133) + # 3) wolfBoot 16+X||Y (80/112/148) + data = raw + is_sec1 = False + + # Case 2: SEC1 uncompressed (leading 0x04, lengths 65/97/133) + if ln in (65, 97, 133) and raw[0] == 0x04: + sec1 = raw + is_sec1 = True + xy_len = ln - 1 + # Case 3: wolfBoot container 16+X||Y + elif ln in (80, 112, 148): + # Strip the first 16 bytes, keep last 64/96/132 + data = raw[16:] + if len(data) not in (64, 96, 132): + print("ERROR: Unexpected container size after stripping 16 bytes:", len(data), file=sys.stderr) + sys.exit(3) + sec1 = b"\x04" + data + is_sec1 = True + xy_len = len(data) + # Case 1: raw X||Y + elif ln in (64, 96, 132): + sec1 = b"\x04" + raw + is_sec1 = True + xy_len = ln + else: + print("ERROR: Unrecognized input size:", ln, file=sys.stderr) + print(" Expected one of: SPKI DER, 64/96/132 (X||Y), 65/97/133 (SEC1), 80/112/148 (16+X||Y).", file=sys.stderr) + sys.exit(3) + + # Pick curve by X||Y size if not specified + curve = args.curve + if curve is None: + if xy_len == 64: + curve = "p256" + elif xy_len == 96: + curve = "p384" + elif xy_len == 132: + curve = "p521" + else: + print("ERROR: Cannot infer curve from length:", xy_len, file=sys.stderr) + sys.exit(4) + + from cryptography.hazmat.primitives.asymmetric import ec + if curve == "p256": + crv = ec.SECP256R1() + elif curve == "p384": + crv = ec.SECP384R1() + else: + crv = ec.SECP521R1() + + try: + key_obj = ec.EllipticCurvePublicKey.from_encoded_point(crv, sec1) + except Exception as e: + print("ERROR: cannot wrap/parse key as SEC1/SPKI:", e, file=sys.stderr) + sys.exit(5) + + # Write SPKI next to input + out_der = in_path.with_name(in_path.stem + "_spki.der") + out_pem = in_path.with_name(in_path.stem + "_spki.pem") + + from cryptography.hazmat.primitives import serialization + der = key_obj.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + pem = key_obj.public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + out_der.write_bytes(der) + out_pem.write_bytes(pem) + + # Print SPKI SHA-256 for pubkey-hint comparison + try: + import hashlib, binascii + h = hashlib.sha256(der).digest() + print("Wrote:", out_der) + print("Wrote:", out_pem) + print("SPKI SHA-256 (hex):", binascii.hexlify(h).decode("ascii")) + except Exception: + print("Wrote:", out_der) + print("Wrote:", out_pem) + +if __name__ == "__main__": + main()