Ed25519 Trust Anchors
How trust_on_first_use() and load_trust_anchor() establish device identity, public key storage formats, migrating legacy anchors, key rotation, and the trust model for Pyvorin Edge bundles.
Published Jun 2, 2026
Trust in a Headless World
IoT gateways rarely have human operators staring at screens. When a Raspberry Pi 5 in a remote warehouse boots at 03:00 after a power outage, it must decide, without human intervention, whether the Python modules it is about to execute are authentic. The decision cannot rely on a cloud connection — the network may still be down. It must be resolved locally, using cryptographic trust anchors that were established when the device was first provisioned.
The Pyvorin Edge Runtime solves this with Ed25519 trust anchors. A trust anchor is a JSON file that stores only the public key of an Ed25519 key pair. The corresponding private key is used at build time to sign the edge bundle (manifest + code). At runtime, the device loads the public key from the trust anchor and verifies the signature. If the signature checks out, the bundle is authentic and has not been tampered with since signing.
trust_on_first_use(): Bootstrapping Trust
The first time a bundle is deployed to a device, there is no pre-existing trust anchor. The
trust_on_first_use() method in
edge_sdk/pyvorin_edge/packaging/verifier.py creates one. It generates a fresh
Ed25519 key pair, signs the bundle directory, writes the signed manifest, and persists only
the public key to a file named trust_anchor.json.
from pyvorin_edge.packaging.verifier import BundleVerifier
verifier = BundleVerifier()
# Run once during factory provisioning or first boot
verifier.trust_on_first_use(bundle_dir="/opt/pyvorin-edge/bundles/main")
# This creates:
# /opt/pyvorin-edge/bundles/main/manifest.json (signed manifest)
# /opt/pyvorin-edge/bundles/main/trust_anchor.json (public key only)
The trust anchor file is a plain JSON object:
{
"public_key": "a3f7b2d1e8c9a4b5..."
}
The hex string represents the 32 raw bytes of the Ed25519 public key. No PEM, no base64
wrappers, no X.509 ceremony — just the key material. This minimises parser attack surface and
makes the file human-inspectable with cat or jq.
load_trust_anchor(): Runtime Verification
On every subsequent boot, the runtime calls load_trust_anchor() to retrieve the
public key bytes, then passes those bytes to BundleSigner.verify_bundle() or
BundleVerifier.verify_at_runtime().
from pyvorin_edge.packaging.verifier import BundleVerifier
from pyvorin_edge.packaging.signer import BundleSigner
verifier = BundleVerifier()
# Load the anchor
public_key = verifier.load_trust_anchor(
bundle_dir="/opt/pyvorin-edge/bundles/main"
)
# Verify the bundle against the manifest
is_valid = BundleSigner.verify_bundle(
bundle_dir="/opt/pyvorin-edge/bundles/main",
manifest_path="/opt/pyvorin-edge/bundles/main/manifest.json",
public_key=public_key,
)
if is_valid:
print("Bundle integrity verified. Proceeding to load modules.")
else:
raise RuntimeError("Bundle verification failed. Halting.")
If trust_anchor.json is missing, load_trust_anchor() raises
BundleVerificationError. Your bootstrap code should catch this and either invoke
trust_on_first_use() (if this is genuinely a first boot) or enter a safe failure
mode (if an anchor has been deleted by malware).
Migrating Legacy Trust Anchors
Early versions of the Edge SDK accidentally stored the private key in the trust anchor
file instead of the public key. This is a serious security flaw: anyone with filesystem access
could read the private key and forge bundle signatures. The current load_trust_anchor()
implementation detects this legacy format and automatically migrates it.
# Inside load_trust_anchor() — migration path
if "private_key" in anchor:
logger.warning(
"Migrating legacy trust anchor at %s: "
"replacing private_key with public_key",
anchor_path,
)
private_bytes = bytes.fromhex(anchor["private_key"])
private_key = Ed25519PrivateKey.from_private_bytes(private_bytes)
public_key = private_key.public_key()
public_bytes = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
cleaned_anchor = {"public_key": public_bytes.hex()}
with open(anchor_path, "w", encoding="utf-8") as f:
json.dump(cleaned_anchor, f, indent=2)
return public_bytes
The migration process derives the public key from the private key using Ed25519's deterministic key-derivation property, rewrites the anchor file with only the public key, and returns the public bytes. The private key material is never written back to disk. If you discover legacy anchors in your fleet, rotate the key pair immediately after migration: generate a new pair, re-sign the bundle, and distribute the new anchor via your OTA channel.
Key Rotation Without Downtime
Cryptographic keys should be rotated periodically or after any suspected compromise. For edge devices, rotation is complicated by the fact that the device may be offline when the new key is issued. The recommended rotation workflow is:
- Generate a new key pair in your CI pipeline.
- Re-sign the bundle with the new private key.
- Embed the new public key in the OTA update payload alongside the bundle. Do not overwrite the old anchor yet.
- On the device, write the new public key to
trust_anchor.json.new. - Atomically rename
trust_anchor.json.newtotrust_anchor.jsonafter the bundle is unpacked and before the runtime restarts. On Linux,os.rename()is atomic within the same filesystem. - Retain the old public key in a secondary location for one rotation period so that offline devices can still verify bundles signed with the previous key if they missed an intermediate update.
import os
import json
from pathlib import Path
def rotate_trust_anchor(bundle_dir: str, new_public_key_hex: str) -> None:
anchor_path = Path(bundle_dir) / "trust_anchor.json"
backup_path = Path(bundle_dir) / "trust_anchor.json.prev"
new_path = Path(bundle_dir) / "trust_anchor.json.new"
# 1. Back up the old anchor
if anchor_path.exists():
os.replace(anchor_path, backup_path)
# 2. Write the new anchor atomically
new_path.write_text(
json.dumps({"public_key": new_public_key_hex}, indent=2),
encoding="utf-8",
)
os.replace(new_path, anchor_path)
print(f"Trust anchor rotated in {bundle_dir}")
The Trust Model
The trust anchor model is a form of trust on first use (TOFU), similar to SSH host keys. The first time the device sees a bundle, it has no basis for trusting it except the physical security of the provisioning process. Once the anchor is written, all future bundles must be signed by the corresponding private key. This is simple, stateless, and requires no online certificate authority.
The threat model assumes:
- The provisioning environment is physically secure. An attacker who compromises provisioning can write their own anchor and defeat the model entirely.
- The filesystem is integrity-protected after first boot. If an attacker gains root and
replaces the trust anchor, they can substitute arbitrary code. Use read-only rootfs, dm-verity,
or filesystem immutability flags (
chattr +i) to mitigate this. - The private key never touches the device. It lives only in your CI signing service or hardware security module (HSM). If the private key is leaked, the anchor must be rotated across the entire fleet.
Security Considerations
- File permissions. The trust anchor must be readable by the runtime user
but not writable by unprivileged users. On Linux:
chmod 644 /opt/pyvorin-edge/bundles/main/trust_anchor.json chown root:pyvorin /opt/pyvorin-edge/bundles/main/trust_anchor.json - No private key material. After migration, inspect your anchors with
jq 'keys'. If you seeprivate_key, the anchor is dangerous. Destroy it and regenerate. - Anchor diversity. Do not reuse the same key pair across your entire fleet. If one private key is compromised, every device becomes vulnerable. Use per-device or per-batch key pairs generated in your CI pipeline.
- Network independence. The verification process requires no network access. This is by design: a device must be able to verify its own code even when air-gapped or recovering from an outage.
Summary
Trust anchors are the bedrock of runtime security in Pyvorin Edge. The
trust_on_first_use() method bootstraps a fresh device by generating a key pair and
persisting only the public key. load_trust_anchor() retrieves that public key on
every boot, with automatic migration for legacy formats that mistakenly stored private keys.
Key rotation is supported through atomic file replacement, and the entire model is designed to
function without network connectivity. By keeping the private key off-device and protecting the
anchor with filesystem permissions and immutable flags, you create a verification chain that is
simple enough to audit and strong enough to resist tampering.