Ed25519 Signatures
How Truestamp uses Ed25519 to sign the 32-byte domain-separated hashes of items, blocks, entropy observations, and proof bundles, what a signature proves to a verifier, and how to fetch and pin the public signing keyring (key ids, rotation lineage) from /.well-known/keyring.json.
Overview
A digital signature is a small piece of data that proves a specific private key approved a specific message, and that the message was not altered afterward. Truestamp signs every load-bearing piece of its evidence with Ed25519, a modern elliptic-curve signature scheme built on Curve25519 and standardized in RFC 8032. What actually gets signed is never the raw user data: it is the 32-byte SHA-256 fingerprint of the object, computed with a domain prefix that keeps each kind of object in its own hash space (see domain-separated hashing). When a verifier checks one of these signatures with Truestamp’s public key, a valid result proves two things at once: the fingerprint is exactly the one Truestamp committed to, and Truestamp (the holder of the private key) vouched for it. Ed25519 was chosen because its signatures are deterministic, fast to make and check, small enough to embed everywhere, and resistant to the timing side-channels that have broken older signature schemes.
What Ed25519 signs at Truestamp
Truestamp does not sign documents or raw bytes directly. It signs a fixed 32-byte SHA-256 digest of the object, and the digest is always computed with a single distinguishing domain prefix so that a signature made for one kind of object can never be reinterpreted as a signature for another. The signature-relevant objects are:
- Items. The
item_hash(a 32-byte digest of the timestamped item) is signed when the item is processed. - Blocks. The
block_hash(a 32-byte digest of the block) is signed when the block is finalized. - Entropy observations. The observation hash captured from an external randomness source is signed.
- Proof bundles. A proof bundle packs a compact fixed-width payload (version, type, key id, timestamp, subject hash, block hash, and epoch roots), hashes it to 32 bytes, and signs that digest. See the proof bundle wire format for the exact byte layout.
Because each of these digests carries a different domain prefix before it is ever signed, the four signing contexts are cryptographically disjoint. No extra “envelope” or wrapper is added at the signing layer; the domain-separated hash is the whole message.
A practical consequence: the input to signing and verifying must be the raw 32-byte hash, not its hexadecimal text form. Signing the ASCII characters of a hex string would produce a valid-looking but meaningless signature over the wrong bytes.
What a signature proves to a verifier
A verifier holds Truestamp’s Ed25519 public key and the signature attached to an object. Checking the signature answers a single, precise question: did the private key that pairs with this public key sign exactly this 32-byte fingerprint?
- If the check passes: the fingerprint is authentic and unmodified, and Truestamp vouched for it. Any change to the underlying object would change its hash, and the old signature would no longer verify against the new hash.
- If the check fails: either the object (and therefore its hash) was altered, the signature was tampered with, or it was made by a different key. The verifier cannot tell which, only that the object no longer matches a signature it can trust.
The signature by itself proves authenticity and integrity. It does not by itself prove when the object was submitted; that guarantee comes from the item being included in a finalized block whose hash is later committed to a public blockchain. The signature is the piece that binds Truestamp’s identity to each fingerprint in that chain of evidence.
Each signature also records which key made it, via a short 8-character key identifier derived from the public key. A verifier can therefore look up the exact public key that produced a given signature rather than assuming a single fixed key, which is what lets signatures made by an earlier key continue to verify unchanged after the signing key is later rotated.
The properties that make Ed25519 a good choice
Ed25519 gives Truestamp a set of properties that matter directly for long-lived, verifiable evidence:
- Deterministic. The same message signed by the same key always yields the exact same signature. There is no per-signature random nonce, so a whole class of catastrophic failures (where a weak or reused random value leaks the private key) simply cannot occur. It also makes signatures reproducible and easy to test.
- Fast. Signing and verifying are cheap, which keeps signing on the hot path of item and block processing inexpensive and makes independent verification practical even at scale.
- Small. A signature is 64 bytes and a public key is 32 bytes. In Base64 text that is an 88-character signature and a 44-character public key. Being small means signatures can be embedded directly in proof bundles, stored compactly, and carried over the wire without bloating the evidence.
- 128-bit security level. Ed25519 targets roughly 128 bits of security, the same conservative level used across modern cryptographic systems, with a large safety margin against brute-force attacks.
- Timing-attack resistant. Ed25519’s algorithm is designed to run in constant time, without secret-dependent branches or memory lookups that would let an attacker learn the private key by measuring how long operations take. Truestamp reinforces this on its own side: whenever it compares stored signatures or key identifiers for equality, it uses a constant-time comparison that does not stop early at the first differing byte, so the comparison time does not reveal how close a guess was.
Under the hood, Truestamp does not ship its own Ed25519 implementation. It uses the well-reviewed Ed25519 support built into the Erlang runtime’s cryptographic library (which is in turn backed by OpenSSL), rather than a bespoke or third-party curve implementation. Relying on a mature, widely-audited primitive is itself a security property: there is less novel code to get wrong.
Obtaining and pinning Truestamp’s public keys
Truestamp publishes every signing key it has ever used at a stable public endpoint that requires no authentication:
GET https://www.truestamp.com/.well-known/keyring.json
The response is a small JSON document with a version (currently "1.0") and a keys list. Each entry carries:
key_id: the 8-character hex identifier described above, derived by hashing the public key under its own domain prefix and keeping the first 4 bytes.public_key: the 32-byte Ed25519 public key, Base64 encoded.sequence: the key’s position in the rotation lineage. The original key is sequence 0, and each rotation increments the sequence by one.active:trueon exactly one entry, the key currently signing new objects;falseon every retired key.
An illustrative response (values are examples, not real keys):
{
"version": "1.0",
"keys": [
{"key_id": "4ceefa4a", "public_key": "R3n...44 chars...=", "sequence": 0, "active": false},
{"key_id": "b81f0c2e", "public_key": "mQz...44 chars...=", "sequence": 1, "active": true}
]
}
Retired keys stay in the keyring, which is what keeps old signatures verifiable after a rotation: read the key identifier recorded next to a signature, find the matching key_id in the keyring, and verify against that entry’s public key. That pattern applies to a stored Item or Block record, where the signing_key_id column sits beside the signature column and names the key that made it.
A proof bundle works differently, and the difference matters under rotation:
- The bundle carries the signer’s public key itself. A verifier base64-decodes
pk, checks that it is exactly 32 bytes, and verifies the bundle’s Ed25519 signature against that key. It does not look a key up to obtain it. - The signer’s key id is derived, not read. It is
SHA-256(0x51 || pk)truncated to 4 bytes, computed from the bundle’s ownpk. That derived value is what fills the key-id slot of the signed payload, and it is what a verifier compares against the pinned keyring. It must never be read froms.kidorb.kidfor that purpose. - The stored
s.kidandb.kidmay legitimately differ from it. They record the key that was current when the subject and block composites were frozen into the Merkle tree. If the signing key was rotated afterward, a genuine proof carries an olders.kid/b.kidand a newer derived signer key id. A verifier that requires them to be equal, or that resolves the keyring byb.kid, retrieves the retired public key and rejects a valid proof. s.kidandb.kidare consumed verbatim in exactly one place: the length-prefixed composite preimages that recompute the subject hash (0x13or0x23) and the block hash (0x32). Feeding the derived key id into those preimages recomputes the wrong hash and falsely rejects a rotated proof.
Verifying the signature is one step of verifying a proof; cross-checking the derived key id and pk against the pinned keyring is a separate step, and it is the only one that turns “some key signed this bundle” into “Truestamp signed this bundle”.
The keyring is not a hand-edited list. Every key event, the original key and each rotation after it, is recorded in Truestamp’s own block ledger, and the endpoint serves the keys derived from those records. Verifiers should still pin rather than blindly re-fetch: retrieve the keyring over HTTPS, store the entries you rely on, and compare on later fetches. A rotation appends a new higher-sequence entry and moves the active flag; an existing entry whose key_id or public_key changes, or a lineage that no longer extends the one you pinned, is a red flag, not a routine update.
Limitations
- A signature proves who and what, not when. A valid signature proves the object’s fingerprint is authentic and was vouched for by Truestamp’s key. The submission-timing guarantee comes from block inclusion and public-blockchain commitment, not from the signature alone.
- The public key is the trust root. A signature is only as meaningful as the verifier’s confidence that a given public key really belongs to Truestamp. Verifiers should obtain and pin the correct public key (and its key identifier) from the published keyring described above, rather than trusting whatever key is presented alongside a signature.
- Constant-time behavior is delegated to the platform. Ed25519 is constant-time by design and the runtime’s implementation is intended to preserve that, but the absolute side-channel resistance of a specific build on specific hardware is a property of that platform, not something Truestamp asserts independently.
Citations
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA). Defines Ed25519, including its determinism, key and signature sizes, and constant-time design goals.
- RFC 6962: Certificate Transparency. Source of the byte-prefix domain-separation approach used to keep each kind of hashed object in its own hash space before signing.
- A Hybrid Approach to the Problems of Time-Stamping
- Byte-Prefix Registry
- Cryptographic Accelerators for Digital Signature Based on Ed25519
- Difference between X25519 and Ed25519 - Cryptography Stack Exchange
- Domain-Separated Hashing (SHA-256 Byte Prefixes)
- Ed25519 Deep Dive Addendum (r/crypto)
- Ed25519 signature
- Ed25519: high-speed high-security signatures
- EdDSA (Wikipedia)
- Entropy Verification Levels
- OpenTimestamps
- Proving the Integrity of Digital Evidence with Time
- Ralph Merkle - Wikipedia
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
- Signing-key ID (kid)
- ssh-keygen: Wikipedia
- The Provable Security of Ed25519: Theory and Practice
- Things that use Ed25519 - IANIX
- Verify a Proof
- Why is implementation relevant to timing attacks?