Skip to content

ADR-0004: Ed25519 for optional receipt signing

Status: Accepted Date: 2026-03 (retroactive — recorded 2026-06) Issue: #1197

Context

Action receipts form a per-tenant hash chain (receipt_hash = SHA-256(canonicalize(body)), body includes prev_receipt_hash) so tampering or re-linking is detectable (docs/action-receipt-spec.md). That chain proves internal consistency — that the gateway's own stored records weren't edited after the fact — but doesn't, by itself, let a third party (an auditor, a customer's compliance tooling) verify that a given receipt was produced by a specific gateway instance's key, independent of trusting the database. src/src/sign.rs adds optional asymmetric signing over receipt_hash to close that gap when a tenant opts in.

Decision

Use Ed25519 (ed25519-dalek crate) for the optional signature, loaded from a hex-encoded 32-byte secret via an environment variable. The signature (signature field, 64 bytes / 128 hex chars) is additive — explicitly documented as not part of the hashed body — so verification can be checked independently of the hash chain itself, and the absence of a signature never weakens the chain's own tamper-evidence.

Consequences

  • Small, fast signatures (64 bytes) and fast verification, with no known malleability issues (unlike early ECDSA usage patterns) — cheap enough to do per-receipt without a measurable latency cost.
  • Aligns with the project's own stated inspiration (AegisAgent_Technical_Design.md §1: "Sigstore/transparency-log patterns inform the integrity primitives") — Sigstore/cosign use Ed25519 (or ECDSA P-256) as their default signing scheme, so this is a well-trodden choice for transparency-log-style evidence rather than a novel one.
  • Local key management supports a hex secret and key identifier. KMS-backed signing paths also exist, but provider-specific provisioning, rotation, and operational validation remain deployment responsibilities; signing is beta rather than a universal default.
  • Verification (GET /v1/receipts/:id/verify) only checks the signature against the stored receipt_hash — it does not (yet) anchor to an external transparency log, so "verifiable" today means "verifiable given the gateway's claimed public key," not "verifiable against independent public infrastructure."

Alternatives considered

  • RSA — much larger keys/signatures for equivalent security margin, and slower to verify; no advantage here over Ed25519 for this use case.
  • ECDSA (P-256/secp256k1) — viable and also used by Sigstore as an alternative, but carries well-known nonce-reuse footguns (k-value malleability) that Ed25519's deterministic signing scheme avoids by construction — a lower-risk default for a signing primitive most developers won't be auditing closely.
  • No signing, hash chain only — simplest, and remains the default (signing is opt-in). Rejected as the only option because some tenants' compliance requirements (SOC 2, EU AI Act Art. 14) call for evidence verifiable by a party that doesn't trust the gateway operator's database directly.

Revisit when

A tenant requires an unsupported HSM/provider, external transparency-log anchoring, multi-party key custody, or cryptographic agility beyond Ed25519.

Security consequences

Private keys never enter receipts or logs. Verification records the signer identity used at creation so historical receipts survive rotation. Signature success does not replace receipt hash-chain verification, and a gateway-claimed public key is not the same as an independently anchored identity.

Verification

cargo test -p gateway sign
python3 -m aegisagent.verify_receipts receipts.json

Test unsigned, locally signed, rotated-key, invalid-signature, and configured KMS paths separately.

References

Receipt Engine · Receipt Specification · Secret Rotation