Contents
IAM Module Instructions — @Architect + @SecurityKMS
Scope:
src/iam/pg_vault_tde_iam.c,src/include/pg_vault_tde_iam.h
Module Responsibility
Provides the tde_btree index access method with AES-256-SIV
deterministic encryption for B-Tree index keys. This enables equality
lookups (=, IN, ON CONFLICT) on encrypted columns without exposing
plaintext key values in the index.
Shared ownership:
- @Architect owns: amhandler, callback table, AM registration
- @SecurityKMS owns: tde_iam_encrypt_key(), tde_iam_decrypt_key(), SIV context pool
Algorithm: AES-256-SIV (RFC 5297)
| Property | Value |
|---|---|
| Algorithm | AES-256-SIV (deterministic authenticated encryption) |
| Key length | 64 bytes (double-key: two 32-byte AES keys) |
| Equality | Preserved — same plaintext → same ciphertext under same DEK |
| Ordering | NOT preserved — range scans return empty results |
Why AES-SIV, not AES-GCM
AES-GCM with random IVs produces different ciphertext for the same plaintext. B-Tree comparisons would fail. AES-SIV is deterministic: equal plaintexts produce equal ciphertexts, enabling exact-match index lookups.
Key Derivation
The 32-byte DEK is expanded to 64 bytes for SIV’s double-key requirement:
c
PKCS5_PBKDF2_HMAC(dek, TDE_DEK_LEN,
(unsigned char *)"tde-siv", 7,
1, /* 1 iteration — not for stretching */
EVP_sha256(), 64, siv_key);
OpenSSL 3.x Provider API
EVP_CIPHER *siv_cipher = EVP_CIPHER_fetch(NULL, "AES-256-SIV", NULL);
Do NOT use the legacy EVP_aes_256_siv() — it has no public C symbol in
many OpenSSL 3.x builds.
EVP Context Pool (Per-Backend)
static EVP_CIPHER_CTX *tde_iam_siv_enc_ctx = NULL;
static EVP_CIPHER_CTX *tde_iam_siv_dec_ctx = NULL;
Same pool pattern as the GCM layer:
- Create lazily on first use
- Reset with EVP_CIPHER_CTX_reset() between calls
- Free in on_proc_exit(tde_iam_siv_ctx_cleanup)
Implementation Status (v1.7)
| Hook | Status | Notes |
|---|---|---|
ambuild |
✅ Wired | Full index build with encryption; all types (varlena + fixed-size) |
aminsert |
✅ Wired | Per-key encryption on INSERT; all types encrypted |
amgettuple |
Delegates to btree | Returns encrypted key; heap fetch required for plaintext |
amrescan |
✅ Wired | Encrypts equality scan keys; range keys pass through (empty results) |
amendscan |
Delegates to btree | Standard btree behavior |
ambulkdelete |
Delegates to btree | Standard btree behavior |
amvacuumcleanup |
Delegates to btree | Standard btree behavior |
| Index-only scan | ❌ Not supported | By design: would expose raw AES-SIV ciphertext without decryption |
Known Limitations
Range Scans (by design, permanent)
WHERE col > 'x' on a tde_btree-indexed column returns empty results.
AES-SIV does not preserve ordering. This is documented in README.md and
doc/pg_vault_tde.md. Users must use sequential scans for range predicates.
Index-Only Scans (not supported, by design)
PostgreSQL index-only scans return column values directly from the index without
fetching the heap tuple. Since tde_btree index pages store AES-256-SIV ciphertexts,
an index-only scan would return raw ciphertext to the client — bypassing the TAM
decode_slot decryption path entirely. The tde_btree handler prevents the planner
from selecting this path.
Dependency Constraints
- MAY include
pg_vault_tde_crypto.h(for constants) - MAY include
pg_vault_tde_kms.h(to obtain DEK) - MUST NOT include
pg_vault_tde_tam.h - MUST NOT call any TAM functions
PG Version Compatibility Checklist (IAM)
When adding support for PostgreSQL N+1, audit every IAM callback:
- Diff
amapi.hbetween PG N and PG N+1 - Check each callback signature in the
IndexAmRoutinestruct - Add
#if PG_VERSION_NUMguards where needed - Verify
EVP_CIPHER_fetch(NULL, "AES-256-SIV", NULL)still works with the OpenSSL version shipped by PG N+1’s default platform - Update the Version-Specific API Differences table in
copilot-instructions.md§ 0.5 - Run
make ci-regressagainst PG N+1 (test 17 covers index scan)
Known Version Differences (IAM)
| Callback | PG 17 | PG 18 | Guard |
|---|---|---|---|
| (none so far) | — | — | — |
Add rows here when PG 19+ introduces IAM API changes.