Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ add_executable(moria
src/validators/jpeg.cpp
src/validators/png.cpp
src/validators/gif.cpp
src/validators/sxverity.cpp
src/validators/otra.cpp
src/validators/rae_rfp.cpp
src/validators/wince_rom.cpp
Expand Down Expand Up @@ -257,6 +258,7 @@ if(Python3_Interpreter_FOUND)
test_lzma
test_jzlzma
test_image_size
test_sxverity
test_rae_rfp
test_otra
test_wince_rom
Expand Down
26 changes: 26 additions & 0 deletions signatures/sxverity.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name = "sxverity"
category = "container"

# SpaceX "sxverity" container: a 4 KiB preamble of four 1024-byte records (the
# magic "sxverity" starts each record, at 0x000 / 0x400 / 0x800 / 0xC00), each
# carrying a dm-verity root hash, an embedded public key, and an ed25519 signature
# over the preceding fields; in the images observed the four records are
# byte-identical. The protected payload (a big-endian romfs) follows at 0x1000.
# It wraps the Linux dm-verity feature on Starlink User Terminal firmware
# (persistent partitions, the sx runtime partition, version_info, update bundles).
# The validator confirms the four records and the romfs payload, so the 8-byte
# ASCII magic reaches the consistent tier. Identify only: the finding owns the
# 4 KiB record table and the romfs payload surfaces as its own finding at 0x1000.
validator = "sxverity"
confidence = "consistent"

[[magic]]
ascii = "sxverity"
endian = "little"

[doc]
description = "sxverity container (SpaceX dm-verity wrapper): a 4 KiB signed record table over a romfs payload, used on Starlink User Terminal firmware."
references = [
{ title = "Diving into Starlink's User Terminal Firmware (Quarkslab)", url = "https://blog.quarkslab.com/starlink.html" },
{ title = "dm-verity (kernel docs)", url = "https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/verity.html" },
]
2 changes: 2 additions & 0 deletions src/validators/registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "validators/rae_rfp.hpp"
#include "validators/spiffs.hpp"
#include "validators/squashfs.hpp"
#include "validators/sxverity.hpp"
#include "validators/tar.hpp"
#include "validators/uboot_env.hpp"
#include "validators/ubi.hpp"
Expand Down Expand Up @@ -56,6 +57,7 @@ Validator find_validator(const std::string& name) {
if (name == "tar") return validate_tar;
if (name == "zip") return validate_zip;
if (name == "jpeg") return validate_jpeg;
if (name == "sxverity") return validate_sxverity;
if (name == "png") return validate_png;
if (name == "gif") return validate_gif;
if (name == "rae_rfp") return validate_rae_rfp;
Expand Down
51 changes: 51 additions & 0 deletions src/validators/sxverity.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// sxverity.cpp — SpaceX "sxverity" dm-verity container (Starlink UT firmware). #55
//
// On-disk layout (per Quarkslab, "Diving into Starlink's User Terminal Firmware",
// and confirmed against two sample partitions):
// 0x000 "sxverity" magic, record 0
// 0x400 "sxverity" magic, record 1 } four 1024-byte records, byte-identical
// 0x800 "sxverity" magic, record 2 } in the images observed; each holds a
// 0xC00 "sxverity" magic, record 3 } verity root hash, a public key, and an
// ed25519 signature over the record.
// 0x1000 "-rom1fs-" big-endian romfs payload (volume name also "sxverity").
// We confirm the four records and the payload, then size the finding to the 4 KiB
// record table so the preamble is named and the romfs is reported on its own.
#include "validators/sxverity.hpp"

#include <cstdint>
#include <cstring>

namespace ft {

namespace {
constexpr size_t kRecord = 1024; // record stride
constexpr size_t kRecords = 4; // four records = the 4 KiB table
constexpr size_t kPreamble = kRecord * kRecords; // 0x1000, payload starts here
constexpr uint8_t kMagic[8] = {'s', 'x', 'v', 'e', 'r', 'i', 't', 'y'};
constexpr uint8_t kRomfs[8] = {'-', 'r', 'o', 'm', '1', 'f', 's', '-'};

bool magic_at(const Reader& r, size_t off, const uint8_t (&m)[8]) {
auto b = r.bytes(off, 8);
return b && std::memcmp(b->data(), m, 8) == 0;
}
} // namespace

bool validate_sxverity(ValidatorCtx& ctx) {
const Reader& r = ctx.reader;
const size_t base = ctx.offset; // the matched magic (record 0)

// Require the full four-record table and the romfs payload right after it.
// This rejects a stray "sxverity" string that lacks the surrounding structure
// (the romfs volume name is itself "sxverity", and the magic recurs at each
// record boundary, so without this a single container yields spurious hits).
for (size_t i = 0; i < kRecords; ++i)
if (!magic_at(r, base + i * kRecord, kMagic)) return false;
if (!magic_at(r, base + kPreamble, kRomfs)) return false;

ctx.out.size = kPreamble; // own the 4 KiB record table; romfs is its own finding
ctx.out.set_confidence(Confidence::Consistent,
"sxverity: four-record signed table over a romfs payload");
return true;
}

} // namespace ft
12 changes: 12 additions & 0 deletions src/validators/sxverity.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once
#include "signature.hpp"
namespace ft {
// Confirm a SpaceX "sxverity" dm-verity container: four 1024-byte records whose
// magic "sxverity" starts each 0x400 boundary through 0xC00, followed by a
// big-endian romfs payload ("-rom1fs-") at 0x1000. Sizes the finding to the 4 KiB
// record table, so the preamble is named and the romfs payload surfaces as its
// own finding. Reaches `consistent` (magic + full record table + payload
// cross-check); rejects a stray "sxverity" string that lacks the structure (the
// romfs volume name is itself "sxverity"). See issue #55.
bool validate_sxverity(ValidatorCtx& ctx);
} // namespace ft
14 changes: 14 additions & 0 deletions tests/gen_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,19 @@ def romfs():
return bytes(b)


def sxverity():
# SpaceX sxverity container: four 1024-byte records each starting with the
# "sxverity" magic (a size field at +0x0c and pseudo hash/key/sig bytes so the
# record is not all-zero), then the romfs payload at 0x1000. The validator
# requires all four records + the romfs payload. See issue #55.
rec = bytearray(1024)
rec[0:8] = b"sxverity"
struct.pack_into("<I", rec, 0x0c, 0x800) # config/size field
struct.pack_into("<III", rec, 0x14, 2, 3, 3) # version / block-count fields
rec[0x20:0x40] = bytes(range(0x20, 0x40)) # stand-in root hash / key / sig
return bytes(rec) * 4 + romfs()


_CRC_T = []
for _i in range(256):
_c = _i
Expand Down Expand Up @@ -1377,6 +1390,7 @@ def spiffs_img():
("fat32.bin", fat32, "fat32", CONSISTENT),
("cramfs.bin", cramfs, "cramfs", CONSISTENT),
("romfs.bin", romfs, "romfs", CONSISTENT),
("image.sxv", sxverity, "sxverity", CONSISTENT),
("jffs2.bin", jffs2, "jffs2", VERIFIED),
("ubi.bin", ubi, "ubi", VERIFIED),
("ubifs.bin", ubifs, "ubifs", VERIFIED),
Expand Down
2 changes: 2 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ python3 tests/test_lzma.py
python3 tests/test_jzlzma.py
# GIF/PNG sized to their real terminator (0x3B / IEND), not to EOF (issue #58).
python3 tests/test_image_size.py
# sxverity container (SpaceX dm-verity wrapper) identify + payload + FP guards (#55).
python3 tests/test_sxverity.py
# RFP section extraction + LZARI decode round-trip (self-contained hard gate).
python3 tests/test_rae_rfp.py
# Windows CE XIP ROM: ROMHDR resolution, module PE rebuild, CECompress decode.
Expand Down
142 changes: 142 additions & 0 deletions tests/test_sxverity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""sxverity container identify regression (issue #55).

moria must name the SpaceX "sxverity" dm-verity container, not just the romfs
payload it wraps. The container is a 4 KiB preamble of four 1024-byte records
(each starting with the ASCII magic "sxverity") followed by a big-endian romfs
at 0x1000. Before this signature, moria reported only the romfs at offset 4096
and left the 4 KiB header as an unnamed gap.

Checks:
1. A synthetic container -> a `sxverity` finding at offset 0 covering the 4 KiB
record table (tier consistent), with the romfs surfaced as its own finding
at 0x1000, so the summary names the container.
2. The container found at a nonzero offset (embedded in a larger flash image).
3. FP guards: a lone "sxverity" string without the four-record + romfs
structure is not reported (the romfs volume name is itself "sxverity").
4. Optional: real sxverity images, if a directory of them is pointed to via
$MORIA_SXVERITY_SAMPLES (self-skips otherwise, so CI stays self-contained
with no sample path baked in). Any file in that dir starting with the magic
is checked, so no specific filename is assumed.

Run: python3 tests/test_sxverity.py
"""
import json
import os
import struct
import subprocess
import sys
import tempfile

HERE = os.path.dirname(os.path.abspath(__file__))
MORIA = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "build", "moria")


def moria_findings(data: bytes):
with tempfile.NamedTemporaryFile(suffix=".bin") as f:
f.write(data)
f.flush()
r = subprocess.run([MORIA, "-j", f.name], capture_output=True, timeout=120)
return json.loads(r.stdout).get("findings", [])


def findings_of(data: bytes, typ: str):
return [x for x in moria_findings(data) if x["type"] == typ]


def romfs(size: int = 256) -> bytes:
b = bytearray(size)
b[0:8] = b"-rom1fs-"
struct.pack_into(">I", b, 8, size) # full_size, big-endian
b[16:24] = b"sxverity" # volume name (as on the real device)
return bytes(b)


def container(payload: bytes = None) -> bytes:
if payload is None:
payload = romfs()
rec = bytearray(1024)
rec[0:8] = b"sxverity"
struct.pack_into("<I", rec, 0x0c, 0x800)
rec[0x20:0x40] = bytes(range(0x20, 0x40))
return bytes(rec) * 4 + payload


def main():
if not os.path.exists(MORIA):
print("moria not built", file=sys.stderr)
return 1
fails = []

def check(cond, msg):
if not cond:
fails.append(msg)

img = container()

# --- 1. identify the container, name the 4 KiB record table --------------
sx = findings_of(img, "sxverity")
check(len(sx) == 1, f"expected one sxverity finding, got {len(sx)}")
if sx:
check(sx[0]["offset"] == 0, f"sxverity offset {sx[0]['offset']:#x}, want 0")
check(sx[0]["size"] == 4096, f"sxverity size {sx[0]['size']}, want 4096 (record table)")
check(sx[0]["confidence_tier"] == "consistent",
f"sxverity tier {sx[0]['confidence_tier']}, want consistent")
rf = findings_of(img, "romfs")
check(len(rf) == 1 and rf[0]["offset"] == 4096,
f"romfs payload not surfaced at 0x1000: {[(x['offset']) for x in rf]}")

# --- 2. embedded at a nonzero offset ------------------------------------
off = 0x10000
emb = b"\x00" * off + img
sxe = findings_of(emb, "sxverity")
check(len(sxe) == 1 and sxe[0]["offset"] == off and sxe[0]["size"] == 4096,
f"embedded sxverity: {[(x['offset'], x['size']) for x in sxe]} want [({off}, 4096)]")

# --- 3. FP guards -------------------------------------------------------
# (a) a lone magic with no record table / payload
check(not findings_of(b"sxverity" + b"\x00" * 5000, "sxverity"),
"FP: a lone sxverity magic was reported")
# (b) the magic present but the romfs payload missing after four records
no_payload = (bytes(bytearray(b"sxverity" + b"\x00" * 1016)) * 4) + b"\x00" * 4096
check(not findings_of(no_payload, "sxverity"),
"FP: four records without a romfs payload were accepted")
# (c) only one record (no repeat at 0x400) then a romfs
one_rec = b"sxverity" + b"\x00" * 1016 + romfs() + b"\x00" * 4096
check(not findings_of(one_rec, "sxverity"),
"FP: a single record (no four-record table) was accepted")

# --- 4. optional real images (opt-in via env, no path baked in) ---------
ran_real = 0
sample_dir = os.environ.get("MORIA_SXVERITY_SAMPLES")
if sample_dir and os.path.isdir(sample_dir):
for name in sorted(os.listdir(sample_dir)):
p = os.path.join(sample_dir, name)
if not os.path.isfile(p):
continue
with open(p, "rb") as f:
head = f.read(8)
if head != b"sxverity": # only files that actually start with the magic
continue
with open(p, "rb") as f:
data = f.read()
ran_real += 1
sr = findings_of(data, "sxverity")
check(len(sr) == 1 and sr[0]["offset"] == 0 and sr[0]["size"] == 4096 and
sr[0]["confidence_tier"] == "consistent",
f"real sample: sxverity not identified as expected: {sr}")
check(any(x["offset"] == 4096 for x in findings_of(data, "romfs")),
"real sample: romfs payload not at 0x1000")

print("-" * 60)
if fails:
for m in fails:
print("FAIL:", m)
return 1
tail = f"(+{ran_real} real images)" if ran_real else "(real samples opt-in via $MORIA_SXVERITY_SAMPLES)"
print(f"PASS: sxverity container identify + payload + FP guards {tail}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading