What this is about
A hash chain in your own database can be recomputed by anyone who controls the database. They change an entry and recompute all following hashes consistently. The chain alone does not notice.
A timestamp helps against this: a signed statement that a certain hash already existed at a certain time. At fixed intervals CRMS has the latest entries of all journals stamped. This is called anchoring, and the result is an anchor.
How the journals are built is described in The two hash chains.
An anchoring pass
-
1CRMS→Databasereads the head of the system journal and the head of every tenant's journal, each in its own context
-
2CRMSbuilds one Merkle tree from all heads and computes its root
-
3CRMShas the root stamped by the timestamp authority (RFC 3161)
-
4CRMS→Databasestores the anchor in the system database
-
5CRMS→Databaseputs each journal's proof path into that journal's databaseResult: Every tenant holds the proof for its own journal
- How often: every
CODAMAI_CRMS_ANCHOR_INTERVAL(defaultPT1H, so hourly). The first pass starts one minute after the start. - Which tenants: all that CIAS knows, suspended and closed ones too. Their journals have to stay anchored as well.
- If a tenant cannot be reached: it is left out and the others are anchored. The next pass tries again.
- Who runs it: every instance with
CODAMAI_CRMS_SCHEDULING_ENABLED=true(default). With several instances, leave it on in exactly one.
The Merkle tree
A Merkle tree combines many hashes into a single one, the root. Two hashes at a time are hashed together until only one is left. So one timestamp is enough for all tenants, however many there are.
flowchart BT A["Leaf: system"] --> AB["Node"] B["Leaf: acme"] --> AB C["Leaf: beta"] --> CD["Node"] D["Leaf: gamma"] --> CD AB --> R["Root → timestamp"] CD --> R
- A leaf is the canonical JSON
{journal, sequence, hash}of the journal head. The leaves are sorted by journal, so the same heads always give the same root. - The tree follows RFC 6962: leaves get the byte
0x00before hashing, nodes0x01. - A journal’s proof path is the list of neighbor hashes on the way from the leaf to the root. With it, a tenant shows that its journal head is inside the stamped root, without seeing the other journals.
What the anchor check finds
The anchor check takes a journal’s newest proof path and checks it station by station. Together with verifying the chain itself, it also finds a journal that was recomputed consistently.
-
CRMSProofIs there a proof path for this journal?↳ no
journal-not-anchored -
CRMSAnchorIs the named anchor in the system database?↳ no
anchor-missing -
CRMSEntryIs the journal entry at the anchored position still the same one?↳ no
anchor-entry-mismatch– rewritten -
CRMSPathDoes the proof path lead to the anchor's root?↳ no
anchor-proof-invalid -
CRMSTimestampIs the timestamp on the root valid?↳ no
anchor-timestamp-invalid - The journal is proven up to the anchored entry
These results are keys in a check result, not error answers.
The timestamp authority
A timestamp authority (TSA) signs hashes together with a time. CRMS brings its own, which works according to RFC 3161:
- Its key comes as a PKCS#12 file, Base64 encoded, from
CODAMAI_CRMS_TSA_KEYSTORE, the password fromCODAMAI_CRMS_TSA_KEYSTORE_PASSWORD. Without a key, CRMS does not start. - The certificate must carry the usage
timeStamping, marked as critical. Otherwise CRMS does not start. - The time comes from the system clock. Keep it synchronized, for example with NTP.
- Every timestamp has a random 128-bit serial number and carries the certificate it was signed with.
Which timestamp CRMS trusts
A timestamp is verified against the certificate that signed it. That certificate must be trusted, and it must have been valid at the time of the stamp.
| Signed with the current certificate? | Certificate in the trust list? | Issued by a certificate in the list? | Is the stamp accepted? |
|---|---|---|---|
| yes | – | – | yes |
| no | yes | – | yes |
| no | no | yes | yes |
| no | no | no | no |
The trust list is CODAMAI_CRMS_TSA_TRUSTED_CERTIFICATES: PEM certificates one after the other, or that PEM as Base64 if the environment takes no line breaks. The certificates are public, but they always come from the configuration, never from the database. Otherwise the database administration could add a certificate of its own and stamp a rewritten history.
Expiry of the certificate
When: more than 60 days until it expires
CRMS starts and stamps as usual.
Result: Anchors are created as planned
When: fewer than 60 days until it expires
The start warns in the log with the expiry date. The anchoring pass warns as well, at most once a day.
Result: Anchors are still created. Replace the key now
When: the certificate is not valid at start
The start fails and names the certificate's validity dates.
Result: CRMS does not run
When: the certificate expires while CRMS runs
Every anchoring pass is refused with timestamp-certificate-expired. The journals stay intact, only no new anchor is added.
Result: No anchor until a valid key is deployed
Replacing the key
-
1Admincreates a new timestamping key with
timeStampingas a critical usage -
2Adminexports the current certificate as PEM first
-
3Adminputs the new key into
CODAMAI_CRMS_TSA_KEYSTOREand appends the old certificate toCODAMAI_CRMS_TSA_TRUSTED_CERTIFICATES -
4CRMSstamps with the new key from the next pass on. Nothing is stamped againResult: Old and new anchors stay verifiable
With a certificate authority of your own, you list that authority’s certificate instead. Then every timestamping certificate it issues is accepted, and a replacement needs no change to the list.
An overview of the variables is in Configuration.