Whitepaper
Confidentiality and Verifiability
v1.4 · 11 October 2026 · Public edition
1Scope and definitions
Klarum makes two kinds of promise about customer data, Confidentiality and Verifiability. They differ in strength, and the difference is commercially material.
Confidentiality refers to who can read a document. It rests on encryption at rest with keys Klarum holds, plus tenant isolation in the databases. It is not a mathematical guarantee against Klarum itself, because Klarum controls both the encryption layer and the databases.
Verifiability refers to whether a record has been altered. It rests on a hash chain, an Ed25519 signature, and a Merkle root published on a public blockchain. It can be checked without trusting Klarum.
2Encryption at rest
Envelope construction
Document bytes are encrypted with AES-256-GCM using envelope encryption. The encryption runs inside the storage client itself, so every read and write inherits it and no calling feature has to request it.
- A fresh 32-byte Data Encryption Key (DEK) is generated per object.
- The DEK encrypts the document. The DEK is then itself encrypted under a key encryption key (KEK), with its own nonce.
- The envelope (algorithm, key id, nonce, wrapped DEK) is written to the object's metadata. Each object carries its own recipe, so enabling or disabling the feature needs no migration and no re-encryption: objects written under either state stay readable side by side.
The storage key is bound to the ciphertext as AES-GCM Additional Authenticated Data (ALG|storage_key).
Because the storage key is authenticated, an encrypted object cannot be renamed or copied in place. Nothing in the codebase does so today, and any future move path must re-encrypt rather than copy.
Reads decrypt transparently. An object written before the feature existed, or by a customer who has not enabled it, carries no envelope and is returned unchanged.
Policy controls
Two controls, kept separate from each other, govern whether new writes are encrypted.
| Control | Meaning |
|---|---|
| A per-organisation setting | The customer's choice, set by an organisation administrator through the API. The settings page and onboarding step that used to expose it have been withdrawn, so there is no interface for it today. Encryption at rest is offered and is not imposed. |
| An operator kill switch | An operator control for incidents. It halts new encryption globally without altering anyone's stated preference, so using it and reverting does not silently reset customer settings. |
The policy is read at write time only.
If the organisation's encryption preference cannot be read, the upload is refused with a 503 and no plaintext is written. The policy fails closed. The adjacent case behaves the same way: a customer who has opted in while no usable key is configured gets a refusal rather than an unencrypted write.
Plaintext exposure
Plaintext exists in four places:
- 1. In transit to Klarum, under TLS.
- 2. In the platform's memory, while the document is processed.
- 3. Briefly in a temporary file, on two remaining paths where a document parser accepts only a filesystem path. Both are marked in the code with the reason. Every other write path uploads from memory so that no readable copy is left on disk while the stored object is encrypted. The temporary directory is now a memory-backed mount rather than local disk, so this item no longer describes a disk write. Section 2.4 (temporary file staging) sets out what the mount does and does not achieve.
- 4. Decrypted on read, whenever an authorised reader or an internal service opens the document.
Temporary file staging
The exposure from temporary files is bounded. The file is removed in a finally block, so the window is the duration of one request. The service's ephemeral storage is destroyed with its container, so persistence beyond that requires a snapshot of a running container filesystem or host-level compromise.
Temporary files are not the only place plaintext exists. Item 2 in section 2.3 (plaintext exposure), process memory, holds plaintext on every request whether or not a temporary file is involved. A threat model that includes filesystem snapshots should equally include memory dumps.
Implemented. The temporary directory is a size-bounded tmpfs mount, so a staged document never reaches a block device. No parser change was required: Python's temporary-file machinery follows the environment's configured temporary directory and no part of the platform hardcodes a path around it, so every staging site inherits the mount.
The mount is bounded. An unbounded RAM-backed filesystem would either exhaust the container's memory or spill to disk under pressure, which is the outcome the mount exists to prevent. Writes past the ceiling fail.
The service verifies the mount at start-up, reading the kernel mount table to confirm the directory is memory-backed. A conventional temporary directory exists on disk whether or not a tmpfs is mounted over it, so a mount that silently stopped being applied would otherwise be indistinguishable from a working one. Where the deployment declares the mount required, a missing mount prevents the service from starting, and that condition is checked against a candidate release before it replaces the running one.
The mount removes the disk artefact and nothing more. Plaintext is present in process memory on every request whether or not a temporary file is involved.
Key management
The envelope carries a key id (kid), and the implementation resolves the active key by that id. More than one KEK can therefore exist at a time, and an object records which key wrapped it. This is the mechanism rotation requires.
The following are not in place:
| Question | Answer today |
|---|---|
| Where does the KEK live? | Application configuration, not a KMS or HSM. It is secret material held as environment configuration. |
| Is there a rotation procedure? | Rotation is possible (multiple keys, kid per object), but there is no documented procedure and no crypto-period defined. |
| Can existing objects be re-wrapped? | Not implemented. A new active key applies to new writes; older objects stay readable under the key that wrapped them, indefinitely. |
| Who can read the KEK? | Whoever can read the service's configuration. There is no separation between the service that decrypts and the store that holds the key. |
3Inference and embeddings
Inference routing
Semantic search and matching use vector embeddings at 1024 dimensions. The embedding provider is Cohere, model Embed v4, called through Cohere's own API. The generative models behind drafting, extraction, summarisation and the conversational agent run on a self-hosted inference host operated by Klarum. Neither provider is selectable: the embedding provider and the generative provider each accept one value.
Amazon Bedrock previously served both paths and was retired from the platform in September 2026.
Claims and limits on the inference path
Document text sent for embedding goes to Cohere's API over TLS. Klarum does not persist the provider's request or response bodies beyond the resulting vector.
No contractual no-retention or zero-data-retention posture with the inference provider is claimed. That posture is a property of the provider agreement and its configuration and not of Klarum code, and only what has been verified in the running system is stated here. Establishing and then citing it is on the roadmap (see the Roadmap section).
Provider retention and sub-processor status
Where inference happens does not determine whether the provider retains anything.
The inference provider is a sub-processor in the GDPR sense: customer document text is disclosed to it in order to produce the embedding. Article 28 requires that relationship to rest on written terms. A retention claim has to come from those terms and the service configuration, and Klarum source code, which is all that can be verified directly here, cannot supply it. Three items remain open:
- The provider's default behaviour and the applicable data-processing terms must be cited. Assuming retention is as unfounded as assuming its absence.
- Any provider-side invocation logging must be confirmed disabled by configuration, and that configuration is the artefact an auditor is shown.
- The provider must appear in the published sub-processor register, which the public trust page renders and a customer's DPA review reads.
Until those three are in place and verified, only the provider and the model are stated, and no retention claim is made.
Vector persistence
Vectors are persisted. They live in PostgreSQL via the pgvector extension, in a dedicated embeddings table that records which model produced each vector and which record it belongs to, and on several other tables that carry an embedding column directly. Klarum does not hold vectors only in memory or purge them after use. An embedding is durable derived data, stored so that search need not re-embed the corpus on every query.
- Embeddings are tenant-scoped by their owner columns, and reached through the same tenancy guards as the records they describe.
- Deleting the owning record removes the basis for its embedding row: retention follows the parent record, as described in the published retention statement.
Embeddings as personal data
An earlier revision of this document described an embedding as "not reversible to the source text by any supported operation". That statement was wrong and has been removed. The qualifier "by any supported operation" made it read as a safety property when it was not one.
Two consequences follow:
- An embedding is handled with the same care as the record it derives from. It is in scope for access control, retention and deletion.
- Embeddings are not covered by the document encryption in the Encryption at rest section, which applies to objects in the document store and not to database columns. Column-level encryption of vector data is not implemented and is not claimed. The controls between an embedding and an unauthorised reader are database access control, tenant scoping, and encryption of the database volume at the infrastructure layer. Application-level cryptography is not among them.
4Verifiability
Hash chains
Auditable events are appended to a tamper-evident hash chain. Each entry commits to the previous entry's hash, so altering or removing an entry breaks every entry after it. Three chains exist: the workspace audit log, agent action receipts, and a global coverage snapshot chain.
Merkle roots
Periodically the entries of a chain are hashed into a Merkle root following the Certificate Transparency construction of RFC 6962, implemented in the platform's hashing utility:
leaf_hash(L) = sha256(0x00 || L)node_hash(l, r) = sha256(0x01 || l || r)odd node = promoted unchanged to the next level, never self-paired
The construction makes three choices:
| Choice | Why |
|---|---|
| Hash raw bytes, not hex text | Lets a stock sha256sum and an on-chain verifier reproduce the arithmetic. Hashing the 64-character hex string hashes 128 ASCII bytes, which no EVM verifier reproduces. |
| 0x00 / 0x01 domain separation | Prevents an interior node being replayed as a leaf to forge an inclusion proof (CVE-2012-2459). |
| Promote the odd node, never duplicate it | Keeps [a,b,c] and [a,b,c,c] distinct, so a root committing to three entries cannot be confused with one committing to four. |
Position is preserved: pairs are concatenated in order and never sorted. A proof can therefore state that an entry was the third, in addition to stating that it exists somewhere in the set.
Each root is signed with Ed25519 using a key held outside the database. The public half is served unauthenticated at /.well-known/klarum-anchor-keys, because a signature is verifiable only if its key can be fetched.
The single-signer trust assumption
Klarum holds the records, the key encryption key, and the sole signing key. An actor with all three can rewrite a tenant's history, recompute a Merkle root over the rewritten entries, sign that root, and present the result as internally consistent. A hash chain plus a signature does not prevent this. It requires the rewrite to be wholesale rather than piecemeal.
On-chain publication
The global coverage root is published to `KlarumAnchor`, a contract on Base Sepolia (chain id 84532), which stores the root and can verify an inclusion proof against it on-chain:
function verify(uint64 batchId, bytes32 leaf, uint256 leafIndex, uint256 treeSize, bytes32[] calldata path) external view returns (bool)
The contract uses sha256 with the same domain separation as the platform, so a root computed by Klarum's own code verifies against the deployed contract. This has been executed end to end. A root computed by the platform was anchored, and the contract's verify function returned:
| Case | Result |
|---|---|
| a genuine proof | true |
| the same leaf with one byte altered | false |
| the correct leaf claimed at the wrong index | false |
| a batch that was never anchored | false |
The wrong-index case demonstrates the positional claim. It distinguishes a record that exists from a record that was the third action, with nothing inserted before it.
Testnet limitations
A testnet has no economic security and no continuity commitment. Its state can be reset, and testnets are periodically deprecated outright: several Ethereum testnets have been sunset. A root anchored only to Base Sepolia therefore provides no durable third-party timestamp. The record it depends on may not exist in a year, which is the horizon over which an audit trail matters.
The current anchoring establishes that the mechanism works end to end: the platform's Merkle arithmetic and the contract's agree, and a third party can run the verification. It does not establish evidence of when a root existed.
Independently of any chain, the hash chain and the Ed25519 signature retain their value, subject to the trust assumption in the section "The single-signer trust assumption" above. A chain with continuity guarantees adds a timestamp that no party controls. A testnet adds a timestamp that may be withdrawn.
The remedy is anchoring to a network with continuity guarantees. That is a business decision about irreversibility and has not been taken.
Independent verification
An anchor receipt (GET /workspaces/{id}/audit/receipt) is self-contained: the leaf, its audit path, the root, the tree recipe, the anchor summary and the signature. It is fed to a standalone verification script, which uses only the Python standard library, makes no network calls, and shares no code with the platform. The script re-derives the recipe from scratch, so a defect in the platform's Merkle implementation cannot hide inside the tool that checks it.
A receipt proves inclusion: the entry is one of the entries the root was built over, at that position, unaltered. It does not by itself prove the root is legitimate. That requires checking the root against its signature and, for the coverage chain, against the on-chain batch.
5Roadmap
None of the following is implemented, and none may be described as current.
- Customer-held signing keys. A browser-generated, non-exportable Ed25519 key (
crypto.subtle.generateKey(..., extractable: false)) whose public half is registered with Klarum, letting a customer sign their own actions. The capability is a signing one. It would let a customer prove authorship and would leave Klarum's ability to read documents unchanged. - Client-side or end-to-end document encryption. Out of scope for the current encryption work.
- A verified no-retention posture with the inference provider, with its sub-processor register entry and cited terms, per section 3.3 (Provider retention and sub-processor status).
- A managed key service for the KEK, with a documented rotation procedure, a defined crypto-period and a re-wrap path for existing objects, per section 2.5 (Key management). The crypto-period is unbounded until this exists.
- Column-level encryption for embedding vectors, per section 3.5 (Embeddings as personal data).
- Anchoring to a network with continuity guarantees, and anchoring the two per-tenant chains, per sections 4.4 (On-chain publication) and 4.5 (Testnet limitations).
- Qualified electronic signatures (eIDAS QES), which require a qualified trust service provider. Klarum's chain-of-custody evidence is not a qualified signature and is not presented as one.
6Control status
Each row is argued for in the section named against it in the prose above. Qualifications sit in the status column.
| Property | Status |
|---|---|
| AES-256-GCM envelope encryption at rest | Implemented, opt-in |
| Storage key bound as AAD (anti-substitution) | Implemented |
| Operator kill switch, separate from the customer setting | Implemented |
| Encryption policy fails closed on lookup failure | Implemented; a missing organisation row is treated as opted out |
| KEK in a managed key service, with rotation | Not implemented; the key is in application configuration |
| Zero-knowledge / client-side encryption | Not implemented |
| Embedding residency within the EU | Contractual, via the data processing agreement; not enforced per request |
| Generative inference on self-hosted infrastructure | Implemented; see section 3.1 (Inference routing) |
| Provider no-retention posture, sub-processor terms cited | Not verified |
| Vectors purged from memory after use | Not accurate; vectors are persisted |
| Embeddings encrypted at column level | Not implemented |
| Per-workspace audit trail independently verifiable | Not yet; see section 4.3 (The single-signer trust assumption) |
| Tamper-evident hash chains | Implemented |
| RFC 6962 Merkle roots, Ed25519 signed | Implemented |
| On-chain root publication, verifiable | Testnet only; not durable evidence |
| Offline third-party verification | Implemented |
| Mainnet anchoring | Roadmap |
| eIDAS QES | Roadmap, needs a QTSP |
7Revision history
Entries v1.1 to v1.4 each correct, narrow or close a claim.
| Edition | What changed |
|---|---|
| v1.0 | First internal edition. |
| v1.1 | Follows an external audit review. Removes an incorrect claim that embedding vectors were not reversible to their source text; embedding inversion is a known attack class and the original wording was wrong. States a then-known fail-open defect in the encryption policy, adds the key-management position including what is missing, separates inference region from sub-processor retention, and states the trust assumption a Klarum-held signing key leaves in place. |
| v1.2 | Closes a roadmap item: the temporary directory used by the remaining document-parsing paths is now a size-bounded, memory-backed mount whose presence the service verifies at start-up. Narrowed rather than deleted; plaintext is still present in process memory on every request, and section 2.3 (Plaintext exposure) continues to say so. The description of the blockchain network guard is also made precise: reaching a mainnet is possible in principle and requires a second deliberate setting that no deployment has set. |
| v1.3 | First edition prepared for publication, and corrects one factual claim in doing so: section 3 (Inference and embeddings) named Amazon Titan Text Embeddings V2 as the embedding model, when the platform had moved to Cohere Embed v4 on an EU inference profile. Section 3.1 (Inference routing) now separates the embedding path, which stays within the EU, from the conversational agent, whose routing is not yet pinned to the EU; that exception was previously unstated. Internal implementation references are removed throughout. No claim, limit or non-claim differs from v1.2. |
| v1.4 | Corrects the inference provider. Section 3 (Inference and embeddings) described embeddings as running on Amazon Bedrock under an EU inference profile. Bedrock was retired from the platform in September 2026: embeddings now call Cohere's own API directly, and the generative models run on a self-hosted host. The EU inference profile was a Bedrock routing mechanism and no longer exists, so embedding residency is stated as a contractual position rather than a per-request technical control, and the status table is amended to match. Closes the fail-open defect recorded in v1.1: an unresolvable encryption policy now refuses the upload with a 503, and section 2.2 (Policy controls) states the two cases that still resolve to plaintext. The opening summary previously called the audit trail independently verifiable without qualification, which section 4.3 (The single-signer trust assumption) contradicts; it is now scoped to the global coverage chain. Appendix paths are refreshed after a source reorganisation. |