Data Encryption - Terminal 3 Documentation
Encryption in transit
Client-to-node traffic
T3N uses two layers of encryption to protect data in transit:
- HTTPS protects the client connection to the deployment’s public load balancer.
- Core Session API paths add application-layer encryption. The client and node establish a shared secret with ML-KEM-768, derive directional keys with HKDF-SHA256, and encrypt payloads with AES-256-GCM.
Once the ML-KEM public key and an expected runtime measurement obtained through a trusted, out-of-band channel have been verified, the application-layer channel limits plaintext exposure to the client and the TEE. Without that verification, the client only knows that it encrypted to the key returned by the node’s /status endpoint.
In some cases (e.g., trusted B2B integrations), session-authenticated endpoints only rely on HTTPS and trusted party’s specific authentication (e.g, HMAC, EOA signatures, and bearer tokens).
Handshake
An end-to-end encrypted channel is established using ML-KEM (FIPS 203):
TEE NodeClientTEE NodeClientML-KEM.encaps(trinity_public_key) → (K, ciphertext)auth.handshake(ciphertext)ML-KEM.decaps(secret_key, ciphertext) → KGenerate session_id, store {key: K, expiry}Session-Id header + expiry (TLS-protected)
The shared secret K is never transmitted: the client derives it during encapsulation and the node recovers it by decapsulating inside the TEE. The handshake response itself carries only the session identifier and expiry — a lookup handle with no key material — protected by HTTPS like the rest of the exchange.The node does not contact other nodes during each client handshake to jointly decrypt the ML-KEM ciphertext. Instead:
- During startup, the node participates in the threshold process and reconstructs/recovers the wrapping secret key.
- It keeps that key inside the TEE.
- For each handshake, it uses the already-recovered key locally.
“Threshold” protects key provisioning/recovery at startup, but handshake decapsulation itself is fast and node-local.Both client and server derive two keys from the ML-KEM shared secret:
t3n-session-v1-c2s: client encrypts, server decrypts.t3n-session-v1-s2c: server encrypts, client decrypts.
Using different keys for each direction prevents a sender from decrypting its own outgoing ciphertext with its receive key.
Sessions are node-affine: the node that handles the handshake stores the session state in memory, and later requests in that session must return to that node.
Requests and responses
For paths that use Session API application-layer encryption, every encryption operation:
- Generate a fresh random 12-byte nonce
- Encrypt the payload with AES-256-GCM
- Prepend the nonce to the ciphertext
- Verify the authentication tag during decryption
Changing the nonce, ciphertext, or authentication tag causes decryption to fail.
Node-to-node traffic
Node-to-node traffic first uses libp2p Noise for an encrypted, peer-authenticated channel. T3N then applies a TEE protocol upgrade in which peers:
- Exchange fresh challenges
- Bind their libp2p identity to a TEE attestation quote
- Verify the quote and approved TEE measurements
- Compare a Keccak-256 digest of the cluster configuration
A peer is rejected when its quote matches no effective trusted measurement. The effective set includes the node’s own startup measurement, even when the configured allow-list is absent or empty. Raft traffic and key-recovery messages use this attested channel.
Encryption at rest
Private ledger data
T3N stores application state in named key-value collections called maps. Every committed transaction is recorded in the replicated ledger as a set of changes to those maps. Each ledger entry separates these changes into:
- a public domain, containing changes to maps whose names start with
public: - a private domain, containing changes to all other maps
The private domain is encrypted using AES-256-GCM with the 32-byte ledger encryption key. Each committed entry is identified by its Raft term and log index. T3N encodes this pair into the entry’s unique 12-byte encryption nonce. Conceptually,
nonce = derive_nonce(term=7, index=42)
ciphertext = AES-256-GCM(ledger_secret, nonce, private_data, public_data)
The entry’s wire header and complete public domain are authenticated but not encrypted. They remain readable, but modifying either will cause authentication to fail when the private domain is decrypted.Public maps are intentionally stored as plaintext in the replicated ledger. They must not contain personally identifiable information or other secrets.
Private binary large objects (blobs)
Content addressable storage (CAS) can be configured for storing large binary objects:
- Compute a SHA-256 content identifier (CID) from the plaintext;
- Encrypt the blob with AES-256-GCM under the CAS content encryption key (CEK);
- Store the blob under the CID-derived object key; and
- Store a
ValueRefin the KV map.
ValueRef records the CID, size, storage location, encryption status, and last-touch time. On read, T3N decrypts encrypted blobs and verifies that the plaintext hash matches the CID.
Quick reference
| Area | Current implementation |
|---|---|
| Client session establishment | ML-KEM-768 |
| Session key derivation | HKDF-SHA256 with separate client-to-server and server-to-client keys |
| Session payload | AES-256-GCM with a fresh random 12-byte nonce per message |
| Node-to-node transport | libp2p Noise plus mutual Intel TDX attestation and a Keccak-256 configuration-digest check |
| Private data in the ledger | AES-256-GCM |
| Private blobs | AES-256-GCM with a separate CAS Content Encryption Key (CEK), when encryption is enabled |