Knowledge Base

Browse the concepts behind Truestamp. Follow the links between concepts, or search across everything.

Ed25519 Signatures

How Truestamp uses Ed25519 to sign the domain-separated hashes of items, blocks, entropy observations, and proof bundles, what a signature proves to a verifier, how to fetch and pin the public keyring from /.well-known/keyring.json, and how a bundle can carry the key event that introduced its key.

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, computed under a distinguishing domain prefix that keeps each kind of object in its own hash space. The signed objects are:

  • Items. The item_hash, the composite fingerprint binding the item’s id, its claims hash, its submission-window metadata hash, and the signing key id, is signed as the item is submitted.
  • Blocks. The block_hash (a 32-byte digest of the block) is signed when the block is finalized.
  • Entropy observations. The composite observation_hash, binding the observation’s id, the hash of the captured external randomness, its metadata hash, and the signing key id, 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.
  • Ledger commitment records. The record that ties a subject into the Merkle tree of its block, and the records that carry a range of blocks onto a public blockchain, each bind their own fields into a 32-byte commitment hash, and that hash is signed.

Items, entropy observations, blocks, and proof bundles each hash under their own domain prefix, so a signature made in one of those contexts can never be reinterpreted in another. The ledger commitment records share a single commitment prefix and are told apart inside it by the length-prefixed fields bound into each hash. 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: true on exactly one entry, the key currently signing new objects; false on 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 field sits beside the signature field 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 public_key, 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 || public_key) truncated to 4 bytes, computed from the bundle’s own public_key. 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 from the subject’s or a block’s signing_key_id for that purpose.
  • The stored signing_key_id values may legitimately differ from it. The subject and every block map in the bundle carry a signing_key_id recording the key that was current when that composite was frozen into the Merkle tree. If the signing key was rotated afterward, a genuine proof carries older stored values and a newer derived signer key id. A verifier that requires them to be equal, or that resolves the keyring by the block’s signing_key_id, retrieves the retired public key and rejects a valid proof.
  • A stored signing_key_id is consumed verbatim in exactly one place: the length-prefixed composite preimages that recompute the subject hash (0x13 or 0x23) 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 public_key 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. It serves them only while Truestamp’s own check of that key history has passed. Before the check has run, when it fails, and while a newly recorded key event is still being checked, the endpoint answers 503 Service Unavailable with an empty body rather than publish keys it has not verified; retry later. The did:web document at /.well-known/did.json, which is derived from the same keys, follows the same rule. 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.

Take the keyring from Truestamp and from nowhere else. A keyring that arrives with a proof, or from whoever gave you the proof, proves nothing, because that party can list its own key in it. Truestamp’s own service never uses a keyring from outside: it matches every proof it verifies against the keys derived from its own block ledger, before it believes anything else in the bundle, and no request can supply a different one. An independent verifier may run without the keyring, which keeps proofs checkable if Truestamp cannot be reached; it then reports that the signature verified against the key the bundle carries and that the key was not matched to one Truestamp published.

The signing-key event carried in a bundle

Pinning the keyring answers “is this one of Truestamp’s keys” by comparing against a list a verifier fetched earlier. A bundle can also carry the evidence for that answer with it, as an optional signing_key_event: the ledger block whose metadata records the key event that introduced the signing key, together with that block’s own public-chain commitments. It is a witness of the signature rather than of the submission, which is why it sits at the bundle’s top level instead of inside the subject, and why no item metadata commits to it.

The carried event holds the key event’s type (genesis, rotation, or emergency_rotation), its key_id, its sequence, and the public_key it introduced. Checking it has three parts, reported together under a “Signing Key Event” group:

  1. Binding. The event’s public_key must equal the bundle’s own public_key, and the event’s key_id must equal the key id derived from that public key. Either mismatch fails: the bundle would be carrying a key event for a different key.
  2. Block hash. The carried block map is hashed under 0x32 from its own five fields, the same recomputation every other block map in the bundle gets. Nothing is taken on trust.
  3. Commitments. Each of the event block’s commitments has its epoch proof walked to its epoch root, and, when the verifier is online, the same public-chain confirmation the bundle’s own commitments receive.

A proof generated when the key-event block has not yet been committed to a public chain simply omits the event, because an uncommitted event would establish nothing a verifier could check independently. When the event is present and passes, the trust step stops being “compare against a list you fetched” and becomes “the key that signed this was introduced by an event recorded in Truestamp’s ledger and committed to a public blockchain”. The pinned-keyring cross-check is unchanged and still runs; the two are independent, and a verifier that has pinned the keyring gains the event as corroboration rather than as a replacement.

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

  1. RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA). Defines Ed25519, including its determinism, key and signature sizes, and constant-time design goals.