Verifying Events
EventSourcingDB gives every event a hash, which covers its metadata, its data, and the hash of the event before it. If the database runs with a signing key, it also signs every event it hands out. The store checks the hash of every event it reads, without being asked to.
To check the signatures as well, call signature_verification on the builder of the store, with the verification key of the database:
let store = Store::builder(client, "https://library.eventsourcingdb.io")
.signature_verification(verification_key)
.build();
The verification key is a VerifyingKey of the ed25519_dalek crate, which the kit brings along as architecturekit::ed25519_dalek, in the version the client SDK verifies signatures with. To read it from a PEM file, turn on the feature pem of the kit:
cargo add architecturekit --features pem
Then read the file, and call VerifyingKey::from_public_key_pem of the trait DecodePublicKey, which fails for a PEM that holds no Ed25519 public key:
use architecturekit::ed25519_dalek::{VerifyingKey, pkcs8::DecodePublicKey};
let pem = std::fs::read_to_string("verification-key.pem")?;
let verification_key = VerifyingKey::from_public_key_pem(&pem)?;
The store checks every event it reads, for execute, load, and read as well as for every kind of projection, and it does so before any upcaster sees the event. The events that execute has just written are not checked, since they are not read. If an event fails its verification, reading fails with Permanent::Unverified, a permanent failure of its own, since it may point to a security incident rather than a mistake (see Handling Errors). A projection stops rather than skipping the event.
The two checks prove different things:
- A matching hash proves that an event is what was written. It proves no more than that, since whoever can change the stored data can compute a new hash as well.
- A matching signature proves that an event comes from a database that holds the signing key. The database signs events when handing them out, so the signature guards the way from the database to your application, but not the stored data itself.
The hashes are enough wherever reading is under your control. Check the signatures as well where events cross a trust boundary, for example when reading from a database that another organization runs. For details, see Verifying Event Signatures.
Note that checking a hash takes about a microsecond per event, so there is rarely a reason to turn it off. If there is one, call without_hash_verification on the builder. It can not be combined with signature_verification, since checking a signature includes checking the hash, so build panics if it gets both. Checking a signature, on the other hand, takes some tens of microseconds per event, which adds up when a projection catches up on millions of events.
Note that the database signs with the key it has at the moment, so after the signing key is rotated, the store needs the new verification key.
Note that neither check detects a history that has been rewritten as a whole. For that, audit the chain of hashes (see Auditing the Event Store).