Reduce nRF52 Companion flash and stream Reader chapters
Use a 960-byte constant-time Ed25519 base table, compact hash/scalar code and unused-library trims while retaining Reader, BLE P-256 and the 8 KiB stack. RAK4631 Full saves 57,496 flash bytes and 3,152 static RAM bytes versus preview3. Support complete chapters and verse ranges with bounded app continuation replies and cooperative terminal output. Use nonblocking TCP writes under reader backpressure. Add scalar/signature comparisons and a linked ARM crypto/stack CI gate. Validated clean RAK4631, T1000-E, T096 and V4 Full builds, host regressions, and physical RAK chapter/reconnect and signing comparisons. Physical signing averages 35.9 ms versus 14.4 ms with the original table.
This commit is contained in:
1 parent
9ce2a076eb
commit
8c698dc2d1
21 files changed
+803
-86
No files matched your search
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the linked nRF52 Companion crypto and bound its function stack.
|
||||
|
||||
Requires unicorn==2.1.4. This runs the actual ARM instructions from a release
|
||||
ELF, not a host rebuild. It measures crypto call frames only: it does not
|
||||
include their firmware callers, interrupts, or SoftDevice tasks, and does not
|
||||
replace physical timing/transport tests or the full firmware RAM budget.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
|
||||
from firmware_elf import FirmwareElf
|
||||
from unicorn import Uc, UC_ARCH_ARM, UC_MODE_THUMB, UC_MODE_MCLASS, UC_HOOK_CODE
|
||||
from unicorn import arm_const as arm
|
||||
|
||||
|
||||
# RFC 8032 section 7.1, TEST 1 and TEST 2. Public test seeds, never device keys.
|
||||
VECTORS = (
|
||||
(
|
||||
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
||||
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
|
||||
"",
|
||||
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555f"
|
||||
"b8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
|
||||
),
|
||||
(
|
||||
"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
|
||||
"72",
|
||||
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da0"
|
||||
"85ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def check(path):
|
||||
elf = FirmwareElf(path)
|
||||
if elf.unpack("H", 18)[0] != 40:
|
||||
raise ValueError("expected an ARM firmware ELF")
|
||||
if elf.symbols.get("base", (0, 0))[1] != 960:
|
||||
raise ValueError("Companion must retain the 960-byte Ed25519 base table")
|
||||
reader = "_ZN4mesh5bible9generatedL10readerDataE"
|
||||
if reader not in elf.symbols:
|
||||
raise ValueError("Companion Reader corpus is missing")
|
||||
uc = Uc(UC_ARCH_ARM, UC_MODE_THUMB | UC_MODE_MCLASS)
|
||||
uc.mem_map(0, 0x100000)
|
||||
uc.mem_map(0x20000000, 0x40000)
|
||||
for section in elf.sections:
|
||||
if section[1] != 8 and section[2] & 2 and section[5]:
|
||||
uc.mem_write(section[3], elf.section_bytes(section))
|
||||
stack_top, stop = 0x2003F000, 0x1000
|
||||
uc.mem_write(stop, b"\x00\xbe")
|
||||
state = {"min_sp": stack_top}
|
||||
|
||||
def instruction(machine, _address, _size, _context):
|
||||
# Instruction-level hook includes stack space in leaf functions that
|
||||
# have no branch between reserving and releasing their stack frame.
|
||||
state["min_sp"] = min(state["min_sp"], machine.reg_read(arm.UC_ARM_REG_SP))
|
||||
|
||||
uc.hook_add(UC_HOOK_CODE, instruction)
|
||||
|
||||
def call(name, args):
|
||||
uc.reg_write(arm.UC_ARM_REG_SP, stack_top)
|
||||
uc.reg_write(arm.UC_ARM_REG_LR, stop | 1)
|
||||
for register, value in zip((arm.UC_ARM_REG_R0, arm.UC_ARM_REG_R1,
|
||||
arm.UC_ARM_REG_R2, arm.UC_ARM_REG_R3), args):
|
||||
uc.reg_write(register, value)
|
||||
for index, value in enumerate(args[4:]):
|
||||
uc.mem_write(stack_top + index * 4, struct.pack("<I", value))
|
||||
state["min_sp"] = stack_top
|
||||
uc.emu_start(elf.address(name) | 1, stop, timeout=10000000, count=20000000)
|
||||
if uc.reg_read(arm.UC_ARM_REG_PC) != stop:
|
||||
raise ValueError(name + " did not return within the instruction/time limit")
|
||||
if uc.reg_read(arm.UC_ARM_REG_SP) != stack_top:
|
||||
raise ValueError(name + " did not restore the stack")
|
||||
used = stack_top - state["min_sp"]
|
||||
if used > 2048:
|
||||
raise ValueError(f"{name} uses {used} stack bytes; maximum is 2048")
|
||||
return used
|
||||
|
||||
seed, public, private, signature, message = (
|
||||
0x20030000, 0x20030040, 0x20030080, 0x20030100, 0x20030200)
|
||||
rows = []
|
||||
for index, vector in enumerate(VECTORS, 1):
|
||||
source, expected_public, text, expected_signature = map(bytes.fromhex, vector)
|
||||
uc.mem_write(seed, source)
|
||||
uc.mem_write(message, text or b"\0")
|
||||
key_stack = call("ed25519_create_keypair", [public, private, seed])
|
||||
if bytes(uc.mem_read(public, 32)) != expected_public:
|
||||
raise ValueError(f"RFC 8032 vector {index}: public key mismatch")
|
||||
sign_stack = call("ed25519_sign", [signature, message, len(text), public, private])
|
||||
if bytes(uc.mem_read(signature, 64)) != expected_signature:
|
||||
raise ValueError(f"RFC 8032 vector {index}: signature mismatch")
|
||||
rows.append({"rfc8032_vector": index, "keypair_stack_bytes": key_stack,
|
||||
"sign_stack_bytes": sign_stack})
|
||||
return {
|
||||
"passed": True, "elf_sha256": hashlib.sha256(elf.data).hexdigest(),
|
||||
"base_table_bytes": elf.symbols["base"][1],
|
||||
"reader_data_bytes": elf.symbols[reader][1], "vectors": rows,
|
||||
"stack_limit_bytes": 2048,
|
||||
"scope": "Crypto function frames only; excludes caller/ISR/task stacks and physical timing.",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("elf")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(json.dumps(check(args.elf), indent=2))
|
||||
except (ValueError, OSError) as error:
|
||||
parser.exit(1, f"FAILED: {error}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Feature-preserving flash trim for nRF52 infrastructure, never Companions.
|
||||
"""Role-specific feature-preserving flash trims for nRF52 firmware.
|
||||
|
||||
Decide from the effective source filter as well as the environment/OTA alias
|
||||
and flags: build.sh can synthesize profiles over another PlatformIO recipe.
|
||||
Unknown roles fail closed. No shared framework or downloaded library is edited.
|
||||
CDC, USB host, sensor/GPS support, BLE P-256, and stored Ed25519 keys remain.
|
||||
The compact Ed25519 implementation trades more signing time/stack for flash;
|
||||
the nRF52 base keeps an 8 KiB loop stack for this and the existing OTA path.
|
||||
Infrastructure uses RAM-built compact Ed25519 and display-font trims.
|
||||
Companion uses a 960-byte Ed25519 flash table to keep signing stack bounded,
|
||||
and retains all UI fonts and the Reader. The nRF52 base keeps an 8 KiB loop
|
||||
stack for crypto and the existing OTA path.
|
||||
"""
|
||||
|
||||
import fnmatch
|
||||
@@ -49,6 +51,20 @@ TRIM_DEFINES = {
|
||||
CC310_ARCHIVE_SHA256 = "97dc648d44520e47252f62c402a71f976e36d7b3aef99235bbdc4de79a928577"
|
||||
CC310_ARCHIVE = "src/cortex-m4/fpv4-sp-d16-hard/libnrf_cc310_0.9.13-no-interrupts.a"
|
||||
|
||||
# Companion retains all UI fonts and uses a small Ed25519 flash table instead
|
||||
# of the infrastructure implementation's extra 1.8 KiB of signing stack.
|
||||
# These options remove library code/data that its application never uses;
|
||||
# CDC, DFU, BLE pairing, USB host and UBX navigation remain available.
|
||||
COMPANION_TRIM_DEFINES = {
|
||||
name: value for name, value in TRIM_DEFINES.items()
|
||||
if name not in ("MESH_NRF52_FLASH_TRIM", "ED25519_COMPACT_BASE",
|
||||
"ED25519_COMPACT_SHA512")
|
||||
}
|
||||
COMPANION_TRIM_DEFINES["MESH_NRF52_COMPANION_TRIM"] = 1
|
||||
COMPANION_TRIM_DEFINES["ED25519_SMALL_BASE"] = 1
|
||||
COMPANION_TRIM_DEFINES["ED25519_COMPACT_SHA512"] = 1
|
||||
COMPANION_TRIM_DEFINES["ED25519_COMPACT_SCALAR"] = 1
|
||||
|
||||
|
||||
def flag_defines(flags=(), cppdefines=()):
|
||||
"""Read effective defines without importing PlatformIO/SCons for tests."""
|
||||
@@ -133,6 +149,27 @@ def supported_cc310(framework_dir, mcu, defines):
|
||||
return archive.is_file() and hashlib.sha256(archive.read_bytes()).hexdigest() == CC310_ARCHIVE_SHA256
|
||||
|
||||
|
||||
def companion_trim_policy(platform, mcu, src_filter, flags=(), cppdefines=()):
|
||||
"""Qualify the actual Companion sources, independent of board/alias name."""
|
||||
if platform != "nordicnrf52" or not str(mcu).lower().startswith("nrf52"):
|
||||
return {}
|
||||
if not source_selected(src_filter, COMPANION_SOURCES[0]):
|
||||
return {}
|
||||
if any(source_selected(src_filter, source) for source in ROLE_SOURCES):
|
||||
return {}
|
||||
values = flag_defines(flags, cppdefines)
|
||||
for name in ("MESH_NRF52_FLASH_TRIM", "MESH_NRF52_COMPANION_TRIM"):
|
||||
if name in values and not _enabled(values[name]):
|
||||
return {}
|
||||
definitions = {name: value for name, value in COMPANION_TRIM_DEFINES.items()
|
||||
if name not in values}
|
||||
# The older compact option uses #ifdef, so even an explicit =0 selects
|
||||
# that path. Preserve any caller choice instead of enabling both modes.
|
||||
if values.get("ED25519_COMPACT_BASE") is not None:
|
||||
definitions.pop("ED25519_SMALL_BASE", None)
|
||||
return definitions
|
||||
|
||||
|
||||
def trim_tft_library(build_env, node):
|
||||
# PlatformIO invokes this on TFT_eSPI's library builder, which is already
|
||||
# a private clone. Do not add a global forced include to the project: its
|
||||
@@ -153,14 +190,21 @@ def install(build_env):
|
||||
cppdefines = build_env.get("CPPDEFINES", ())
|
||||
definitions = trim_policy(platform.name, mcu, build_env["PIOENV"],
|
||||
build_env.get("SRC_FILTER", ()), flags, cppdefines)
|
||||
infrastructure = bool(definitions)
|
||||
if not infrastructure:
|
||||
definitions = companion_trim_policy(platform.name, mcu,
|
||||
build_env.get("SRC_FILTER", ()), flags, cppdefines)
|
||||
if not definitions:
|
||||
return
|
||||
build_env["MESH_NRF52_FLASH_TRIM_ACTIVE"] = True
|
||||
build_env.AppendUnique(CPPDEFINES=list(definitions.items()))
|
||||
print("nRF52 infrastructure flash trim: compact crypto, unused USB device classes/GPS cache/vendor splash, compact OTA target names")
|
||||
if infrastructure:
|
||||
print("nRF52 infrastructure flash trim: compact crypto, unused USB device classes/GPS cache/vendor splash, compact OTA target names")
|
||||
else:
|
||||
print("nRF52 Companion flash trim: small crypto table, unused USB device classes/GPS cache/vendor splash; fonts and Reader retained")
|
||||
framework = platform.get_package_dir("framework-arduinoadafruitnrf52")
|
||||
values = flag_defines(flags, cppdefines)
|
||||
if values.get("ST7789") is not None:
|
||||
if infrastructure and values.get("ST7789") is not None:
|
||||
# Keep the font license with this qualified display build, including
|
||||
# cached DFU packages. The hook independently checks the effective
|
||||
# font override and excludes every Companion transport/profile.
|
||||
@@ -169,7 +213,7 @@ def install(build_env):
|
||||
font_license = importlib.util.module_from_spec(hook_spec)
|
||||
hook_spec.loader.exec_module(font_license)
|
||||
font_license.install(build_env)
|
||||
if (values.get("DISPLAY_CLASS") == "ST7735Display"
|
||||
if (infrastructure and values.get("DISPLAY_CLASS") == "ST7735Display"
|
||||
and source_selected(build_env.get("SRC_FILTER", ()), "helpers/ui/ST7735Display.cpp")):
|
||||
build_env.AddBuildMiddleware(trim_tft_library, "*TFT_eSPI*/*.cpp")
|
||||
if framework and supported_cc310(framework, mcu, values):
|
||||
@@ -177,7 +221,7 @@ def install(build_env):
|
||||
build_env.AppendUnique(CPPPATH=[str(Path(framework) / "libraries/Adafruit_nRFCrypto/src/nrf_cc310/include")])
|
||||
# BuildSources is called on this selected nRF52 environment only. The
|
||||
# wrapper is not in the common source filter and cannot leak to other
|
||||
# platforms or an excluded Companion role.
|
||||
# platforms or an unqualified role.
|
||||
build_env.BuildSources("$BUILD_DIR/nrf52-flash-trim",
|
||||
"$PROJECT_DIR/src/helpers/nrf52",
|
||||
"+<Cc310DomainTrim.c>")
|
||||
|
||||
Reference in new issue
Block a user