Docs / ArchitectureKit 2.0 / Guides / Verifying Events

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, hand over the WithSignatureVerification option with the verification key of the database:

store := architecturekit.NewStore(client, "https://library.eventsourcingdb.io", architecturekit.WithSignatureVerification(verificationKey))

The verification key is an ed25519.PublicKey. To read it from a PEM file, use the pem and x509 packages of the standard library:

block, _ := pem.Decode(pemBytes)
if block == nil {
  // ...
}

publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
  // ...
}

verificationKey, ok := publicKey.(ed25519.PublicKey)
if !ok {
  // ...
}

Note that a key of another length than ed25519.PublicKeySize makes WithSignatureVerification panic.

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 an error of the category ErrUnverified, which is a special case of ErrPermanent (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, hand over the WithoutHashVerification option. It can not be combined with WithSignatureVerification, since checking a signature includes checking the hash, so NewStore 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).