Cryptographic documents in RavenDB

time to read 9 min | 1730 words

We got an interesting use case from a customer - they need to verify that documents in RavenDB have not been modified by any external party, including users with administrator credentials for the database.

This is known as the Rogue Root problem, where you have to protect yourself from potentially malicious root users. That is not an easy problem - in theory, you can safeguard yourself using various means, for example the whole premise of SELinux is based on that.

I don’t really like that approach, since I assume that if a user has (valid) root access, they also likely have physical access. In other words, they can change the operating system to bypass any hurdles in the way.

Luckily, the scenario we were presented with involved detecting changes made by an administrator, which is significantly easier. And we can also use some cryptography tools to help us handle even the case of detecting malicious tampering.

First, I’m going to show how to make this work with RavenDB, then we’ll discuss the implications of this approach for the overall security of the system.

The implementation

The RavenDB client API allows you to hook into the saving process of documents, as you can see in the code below. In this example, I’m using a user-specific ECDsa key (by calling the GetSigningKeyForUser() method).


store.OnBeforeStore += (sender, e) =>
{
    using var obj = e.Session.JsonConverter.ToBlittable(e.Entity, null);
    var date = DateTime.UtcNow.ToString("O");
    var data = Encoding.UTF8.GetBytes( e.DocumentId + date + obj);
    
    using ECDsa key = GetSigningKeyForUser(CurrentUser);
    var signData = key.SignData(data, HashAlgorithmName.SHA256);


    e.DocumentMetadata["DigitalSignature"] = new Dictionary<string, string>
    {
        ["User"] = CurrentUser,
        ["Signature"] = Convert.ToBase64String(signData),
        ["Date"] = date,
        ["PublicKey"] = key.ExportSubjectPublicKeyInfoPem()
    };
};

What you can see here is that we are using the user’s key to generate a signature that is composed of:

  • The document’s ID.
  • The current signature time.
  • The JSON content of the entity.

After we generate the signature, we add it to the document’s metadata. This allows us to verify that the entity is indeed valid and was signed by the proper user.

To validate this afterward, we use the following code:


bool ValidateEntity<T>(IAsyncDocumentSession session,T entity)
{
    var metadata = session.Advanced.GetMetadataFor(entity);
    var documentId = session.Advanced.GetDocumentId(entity);
    var digitalSignature = metadata.GetObject("DigitalSignature") ??
          throw new IOException("Signature is missing for " + documentId);
    var date = digitalSignature.GetString("Date");
    var user = digitalSignature.GetString("User");
    var signature = digitalSignature.GetString("Signature");
    using var key = GetPublicKeyForUser(user);
    using var obj = session.Advanced.JsonConverter.ToBlittable(entity, null);
    var data = Encoding.UTF8.GetBytes(documentId + date + obj);
    var bytes = Convert.FromBase64String(signature);
    return key.VerifyData(data, bytes, HashAlgorithmName.SHA256);
}

Note that here, too, we are using the GetPublicKeyForUser() to get the proper public key to validate the signature. We use the specified user from the metadata to get the key, and we verify the signature against the document ID, the date in the metadata, and the JSON of the entity.

We are also saving the public key of the signing user in the metadata. But we haven’t used it so far, why are we doing this?

The reason we use GetPublicKeyForUser() in the ValidateEntity() call is pretty simple: we want to get the user’s key from the same source. This assumes that the user’s key is stored in a safe location (a secure vault or a hardware key like YubiKey, etc.).

The reason we want to store the public key in the metadata is so we can verify the data on the server side. I created the following index:


from c in docs.Companies
let unverified = Crypto.Verify(c)
where unverified is not null
select new 
{ 
    Problem = unverified
}

I’m using RavenDB’s additional sources feature to add the following code to the index. This exposes the Crypto.Verify() call to the index, and the code uses the public key in the metadata (as well as the other information there) to verify that the document signature is valid.

The index code above will filter all the documents whose signature is valid, so you can easily get all the problematic documents. In other words, it is a quick way of saying: “Find me all the documents whose verification failed”. For compliance, that is quite important and usually requires going over the entire dataset to answer it.

The implications

Let’s consider the impact of such a system. We now have cryptographic verification that the document was modified by a specific user. Any tampering with the document will invalidate the digital signature (or require signing it with your key).

Combine that with RavenDB’s revisions, and you have an immutable log that you can verify using modern cryptography. No, it isn’t a blockchain, but it will put a significant roadblock in the path of anyone trying to just modify the data.

The fact that we do the signing on the client side, rather than the server, means that the server never actually has access to the signing keys (only the public keys). The server’s administrator, in the same manner, doesn’t have a way to get those signing keys and forge a document.

In other words, we solved the Rogue Root problem, and we ensured that a user cannot repudiate a document they signed. It is easy to audit the system for invalid documents (and, combined with revisions, go back to a valid one).

Escape hatch design

If you need this sort of feature for compliance only, you may want to skip the ValidateEntity() call. That would allow an administrator to manually change a document (thus invalidating the digital signature) and still have the rest of the system work. That goes against what we are trying to do, yes, but it is sometimes desirable.

That isn’t required for the normal course of operations, but it can be required for troubleshooting, for example. I’m sure you can think of a number of reasons why it would make things a lot easier to fix if you could just modify the database’s data.

For example, an Order contains a ZipCode with the value "02116" (note the leading zero), which a downstream system turns into the integer 02116. An administrator can change the value to be " 02116", with a leading space, preventing this problem (the downstream system will not convert this to a number, thus keeping the leading 0). Silly, yes - but it happens all the time.

Even though we are invalidating the digital signature, we may want to do that anyway. The index we defined would alert on this, but we can proceed with processing the order, then fix it up later. Or just make a note of this for compliance purposes.

Summary

This post walks you through building a cryptographic solution to protect document integrity within a RavenDB environment, addressing the Rogue Root problem. The core mechanism is a client-side OnBeforeStore hook that generates an ECDsa digital signature for each document. This design ensures that the private keys are never exposed on the server, preventing a database administrator from forging signatures and providing true non-repudiation.

A RavenDB index is used to automatically and asynchronously verify every document's signature against its current content. This index filters for any documents where the digital signature is valid, providing an efficient server-side audit mechanism to find all the documents with invalid signatures.

The really fun part here is that there isn’t really a lot of code or complexity involved, and you get strong cryptographic proof that your data has not been tampered with.