edge 14 min read

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:

  1. Generate a new key pair in your CI pipeline.
  2. Re-sign the bundle with the new private key.
  3. Embed the new public key in the OTA update payload alongside the bundle. Do not overwrite the old anchor yet.
  4. On the device, write the new public key to trust_anchor.json.new.
  5. Atomically rename trust_anchor.json.new to trust_anchor.json after the bundle is unpacked and before the runtime restarts. On Linux, os.rename() is atomic within the same filesystem.
  6. 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 see private_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.