Secure Boot and Runtime Integrity

June 2, 2026 | 22 min read

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

  1. ROM inside the SoC. Contains a hash of the public key burned into OTP. This is immutable and serves as the root of trust.
  2. bootcode.bin. The second-stage bootloader loaded from EEPROM. In secure boot mode, the ROM verifies an RSA signature over bootcode.bin before execution.
  3. start.elf. The GPU firmware. It is signed with the same private key and verified by bootcode.bin.
  4. kernel.img (or kernel8.img for 64-bit). The ARM64 kernel is verified by start.elf using a PKCS#1 v1.5 signature.
  5. 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.json exists in the bundle directory.
  • Parse the manifest and extract the files dictionary (relative path → SHA-256).
  • For each tracked file, compute its SHA-256 hash and compare against the expected value.
  • Log every mismatch and raise BundleVerificationError if 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's platform module.
  • cpu_model — parsed from /proc/cpuinfo (model name field).
  • serial_number — read from /proc/device-tree/serial-number on 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 — from vcgencmd measure_temp or thermal zones under /sys/class/thermal.
  • cpu_throttled — hex flags from vcgencmd get_throttled indicating 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-sign should 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_throttled is 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() from BundleVerifier to 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.