The Attack Surface of an Unprotected Boot
An edge gateway sitting in a remote location is a physically accessible computer. An attacker with a USB drive and five minutes of unsupervised access can replace the kernel, inject a malicious kernel module, or patch the Python runtime to exfiltrate data. If you cannot trust the boot process, you cannot trust anything that happens after it. Secure boot is not a luxury for edge deployments; it is the foundation on which every other security control rests.
This article covers the full stack: the Raspberry Pi secure boot chain (bootcode.bin, start.elf,
kernel.img), signing kernel modules with pyv-edge-sign, runtime integrity checks
using BundleVerifier.verify_at_runtime(), hardware attestation via
attestation.py, and continuous tamper detection.
Raspberry Pi Secure Boot Chain
The Raspberry Pi 5 (and Pi 4 with recent firmware) supports a secure boot mode where the VideoCore GPU bootloader verifies cryptographic signatures before executing each stage. The chain of trust flows from an OTP (One-Time Programmable) ROM inside the SoC to the operating system kernel.
Boot Stage Overview
- ROM inside the SoC. Contains a hash of the public key burned into OTP. This is immutable and serves as the root of trust.
- bootcode.bin. The second-stage bootloader loaded from EEPROM. In secure
boot mode, the ROM verifies an RSA signature over
bootcode.binbefore execution. - start.elf. The GPU firmware. It is signed with the same private key and
verified by
bootcode.bin. - kernel.img (or
kernel8.imgfor 64-bit). The ARM64 kernel is verified bystart.elfusing a PKCS#1 v1.5 signature. - Device Tree Blob (DTB) and overlays. Also signed and verified by
start.elf.
Enabling Secure Boot on the Pi 5
# 1. Generate a 2048-bit or 3072-bit RSA key pair for signing
openssl genrsa -out /secureboot/pi5_secure_boot_key.pem 2048
openssl rsa -in /secureboot/pi5_secure_boot_key.pem -pubout \
-out /secureboot/pi5_secure_boot_pubkey.pem
# 2. Convert the public key to the format expected by the bootloader
# (Raw modulus + exponent, or use the rpi-eeprom utilities)
rpi-eeprom-digest -i /boot/firmware/bootcode.bin \
-o /boot/firmware/bootcode.sig \
-k /secureboot/pi5_secure_boot_key.pem
# 3. Sign start.elf
rpi-eeprom-digest -i /boot/firmware/start.elf \
-o /boot/firmware/start.sig \
-k /secureboot/pi5_secure_boot_key.pem
# 4. Sign the kernel
rpi-eeprom-digest -i /boot/firmware/kernel8.img \
-o /boot/firmware/kernel8.sig \
-k /secureboot/pi5_secure_boot_key.pem
# 5. Burn the public key hash into OTP (IRREVERSIBLE)
# Replace with your actual key hash after extraction
rpi-eeprom-config --edit /boot/firmware/pieeprom.bin
# Add: BOOT_RSA_KEY=0x
# Then flash:
sudo rpi-eeprom-update -d -f /boot/firmware/pieeprom.bin
After reboot, the GPU will refuse to load any stage that lacks a valid signature. If an attacker
replaces kernel8.img with an unsigned image, the Pi will halt with a rainbow screen
or serial error code.
Signing Kernel Modules with pyv-edge-sign
Even with a signed kernel, an attacker can load a malicious kernel module (.ko or
.so) if module signing is disabled. Pyvorin Edge uses the pyv-edge-sign
tool (which wraps Ed25519) to sign and verify native modules. While the Pi secure boot chain
uses RSA, pyv-edge-sign uses Ed25519 for smaller signatures and faster verification
in Python space.
# Generate an Ed25519 key pair for module signing
pyv-edge-sign create-key --output /etc/pyvorin/keys/modules
# Sign a compiled native module
pyv-edge-sign sign \
--bundle /opt/pyvorin-edge/modules/sensor_driver.so \
--key /etc/pyvorin/keys/modules/private.pem \
--output /opt/pyvorin-edge/modules/sensor_driver.manifest.json
# Verify at load time
pyv-edge-sign verify \
--bundle /opt/pyvorin-edge/modules/sensor_driver.so \
--manifest /opt/pyvorin-edge/modules/sensor_driver.manifest.json
The pyv-edge-sign tool walks the bundle directory, computes SHA-256 hashes for every
file, and produces a signed manifest. The private key never leaves the build server; only the
public key and manifest travel to the device.
Runtime Integrity Checks
Secure boot verifies the kernel and initial ramdisk. After that, the userspace runtime is
responsible for verifying its own code. The BundleVerifier class in
edge_sdk/pyvorin_edge/packaging/verifier.py provides
verify_at_runtime(), which checks every file in a bundle against its manifest hash.
from pyvorin_edge.packaging.verifier import BundleVerifier
verifier = BundleVerifier()
# Called by EdgeAgent on startup
try:
verifier.verify_at_runtime("/opt/pyvorin-edge/bundles/main")
print("Bundle integrity verified. Proceeding to load modules.")
except Exception as exc:
print(f"RUNTIME VERIFICATION FAILED: {exc}")
# Halt the agent — do not run unverified code
raise SystemExit(1) from exc
The verify_at_runtime() method performs the following checks:
- Ensure
manifest.jsonexists in the bundle directory. - Parse the manifest and extract the
filesdictionary (relative path → SHA-256). - For each tracked file, compute its SHA-256 hash and compare against the expected value.
- Log every mismatch and raise
BundleVerificationErrorif any file fails.
# Inside verify_at_runtime() — simplified excerpt
manifest = signed_manifest.get("manifest")
files_info: dict[str, str] = manifest.get("files", {})
all_valid = True
for relative_path, expected_hash in files_info.items():
file_path = bundle_path / relative_path
if not file_path.is_file():
logger.error("Missing file during runtime verification: %s", relative_path)
all_valid = False
continue
actual_hash = self._hash_file(file_path)
if actual_hash != expected_hash:
logger.error(
"Hash mismatch for %s: expected %s, got %s",
relative_path, expected_hash, actual_hash,
)
all_valid = False
Hardware Attestation Deep Dive
Cryptographic signatures prove that a specific key signed a message. They do not prove which physical device holds that key. Hardware attestation closes this gap by collecting a fingerprint of the device and embedding it in every signed report.
The attestation.py module in edge_sdk/pyvorin_edge/attestation.py
provides the HardwareInfo dataclass and collect_hardware_info() function.
from pyvorin_edge.attestation import (
collect_hardware_info,
collect_runtime_info,
create_attestation_report,
)
# Gather hardware fingerprint
hw = collect_hardware_info()
print(f"Platform: {hw.platform}")
print(f"Machine: {hw.machine}")
print(f"CPU model: {hw.cpu_model}")
print(f"Serial: {hw.serial_number}")
print(f"Hardware rev: {hw.hardware_revision}")
print(f"CPU temp: {hw.cpu_temp_c:.1f}°C")
print(f"Throttled: {hw.cpu_throttled or 'No'}")
print(f"Uptime: {hw.uptime_seconds:.0f}s")
print(f"Memory: {hw.available_memory_kb}/{hw.total_memory_kb} KB")
How HardwareInfo Is Collected
platform,machine,processor— from Python'splatformmodule.cpu_model— parsed from/proc/cpuinfo(model namefield).serial_number— read from/proc/device-tree/serial-numberon Raspberry Pi, or the CPU serial fallback from/proc/cpuinfo.hardware_revision— device tree model string (e.g.,Raspberry Pi 5 Model B Rev 1.0).cpu_temp_c— fromvcgencmd measure_tempor thermal zones under/sys/class/thermal.cpu_throttled— hex flags fromvcgencmd get_throttledindicating under-voltage or thermal throttling.uptime_seconds— from/proc/uptime.total_memory_kb,available_memory_kb— from/proc/meminfo.
Creating an Attestation Report
The create_attestation_report() function wraps benchmark results with hardware and
runtime metadata, then computes a top-level integrity_hash that covers the entire
payload.
from pyvorin_edge.attestation import create_attestation_report
results = {
"pipeline_latency_ms": 12.4,
"throughput_ops_sec": 8500,
"reduction_percent": 94.2,
}
report = create_attestation_report(results)
# report contains:
# hardware: dict of HardwareInfo fields
# runtime: dict of RuntimeInfo fields
# results: the original benchmark results
# integrity_hash: SHA-256 of the canonical JSON of the above
print(f"Integrity hash: {report['integrity_hash']}")
Tamper Detection
Runtime verification on startup is necessary but not sufficient. An attacker who gains root after boot can still modify files. Continuous tamper detection monitors critical files and alerts when their hashes change.
Continuous File Monitor
import hashlib
import json
import time
import logging
from pathlib import Path
from pyvorin_edge.packaging.verifier import BundleVerifier
logger = logging.getLogger("tamper_detection")
WATCHED_FILES = [
"/opt/pyvorin-edge/bundles/main/manifest.json",
"/opt/pyvorin-edge/bundles/main/pipeline.py",
"/opt/pyvorin-edge/bundles/main/policy.json",
"/etc/pyvorin/certs/ca.cert.pem",
]
def _hash_file(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
def load_baseline(baseline_path: Path) -> dict:
if baseline_path.exists():
return json.loads(baseline_path.read_text())
return {}
def save_baseline(baseline_path: Path, hashes: dict) -> None:
baseline_path.write_text(json.dumps(hashes, indent=2))
def check_integrity(baseline_path: Path = Path("/var/lib/pyvorin/baseline_hashes.json")) -> list:
"""Compare current file hashes against the baseline. Return list of anomalies."""
baseline = load_baseline(baseline_path)
anomalies = []
current = {}
for file_path_str in WATCHED_FILES:
file_path = Path(file_path_str)
if not file_path.exists():
anomalies.append({"file": file_path_str, "issue": "missing"})
continue
digest = _hash_file(file_path)
current[file_path_str] = digest
expected = baseline.get(file_path_str)
if expected is None:
logger.info("New baseline entry for %s", file_path_str)
elif digest != expected:
anomalies.append({
"file": file_path_str,
"issue": "hash_mismatch",
"expected": expected,
"actual": digest,
})
save_baseline(baseline_path, current)
return anomalies
# Run every 60 seconds in a background thread
while True:
issues = check_integrity()
for issue in issues:
logger.critical("TAMPER DETECTED: %s — %s", issue["file"], issue["issue"])
# Trigger alert: syslog, MQTT alert topic, or GPIO buzzer
time.sleep(60)
Integration with BundleVerifier
You can also use BundleVerifier.verify_module() to check individual .so
files that are loaded dynamically after startup.
from pyvorin_edge.packaging.verifier import BundleVerifier
verifier = BundleVerifier()
# Check a dynamically loaded module
verifier.verify_module(
so_path="/opt/pyvorin-edge/modules/sensor_driver.so",
expected_hash="a3f7b2d1...",
)
Complete Verification Script
The following script combines secure boot status, runtime bundle verification, hardware attestation, and tamper detection into a single diagnostic tool. Run it from cron every hour and ship the JSON output to your SIEM.
#!/usr/bin/env python3
""" Comprehensive edge device security verification. """
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from pyvorin_edge.attestation import collect_hardware_info, collect_runtime_info
from pyvorin_edge.packaging.verifier import BundleVerifier, BundleVerificationError
BUNDLE_DIR = "/opt/pyvorin-edge/bundles/main"
BASELINE_PATH = Path("/var/lib/pyvorin/baseline_hashes.json")
def check_secure_boot() -> dict:
result = {"enabled": False, "details": ""}
try:
out = subprocess.run(
["vcgencmd", "bootloader_config"],
capture_output=True, text=True, check=False,
).stdout
result["details"] = out
if "BOOT_UART" in out or "secure_boot" in out.lower():
result["enabled"] = True
except FileNotFoundError:
result["details"] = "vcgencmd not available (not a Raspberry Pi?)"
return result
def verify_runtime_bundle() -> dict:
verifier = BundleVerifier()
try:
verifier.verify_at_runtime(BUNDLE_DIR)
return {"status": "ok", "bundle": BUNDLE_DIR}
except BundleVerificationError as exc:
return {"status": "failed", "error": str(exc)}
def main() -> int:
report = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"secure_boot": check_secure_boot(),
"bundle_verification": verify_runtime_bundle(),
"hardware": collect_hardware_info().__dict__,
"runtime": collect_runtime_info().__dict__,
}
print(json.dumps(report, indent=2, default=str))
if report["bundle_verification"]["status"] != "ok":
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
Operational Best Practices
- Burn OTP keys in a controlled environment. Once the public key hash is written to OTP, it cannot be changed. Have a second operator witness the hash before burning.
- Store signing keys in an HSM. The RSA key used for Pi secure boot and the
Ed25519 key used for
pyv-edge-signshould live in a hardware security module or cloud KMS, not on a developer's laptop. - Automate baseline generation. Run the tamper-detection script immediately after every OTA update to establish a new baseline. Do not manually edit baseline files.
- Alert on throttling. If
cpu_throttledis non-zero, the device may be under-voltage or overheating. Both conditions can cause silent bit-flips that corrupt hashes or signatures. - Rotate trust anchors quarterly. Use
trust_on_first_use()fromBundleVerifierto bootstrap new devices, then rotate the anchor via atomic file replacement during scheduled maintenance windows.
Summary
Secure boot and runtime integrity form a defence-in-depth stack for Pyvorin Edge. The Raspberry
Pi secure boot chain guarantees that only signed firmware and kernels execute. The
pyv-edge-sign tool extends that guarantee to userspace modules. The
BundleVerifier re-verifies bundle hashes on every startup, while hardware
attestation binds every report to a unique physical device. Continuous tamper detection closes
the loop by catching modifications that occur after boot. Together, these mechanisms make it
computationally infeasible for an attacker to run unauthorised code on your edge gateway
without immediate detection.