Files
proofs/lean/Crypto/DEKDerivation.lean
zeekayandHanzo Dev 95870aa61d lean: report what each theorem rests on, and namespace the colliding pair
lean/axioms.py runs `#print axioms` over every theorem in the corpus. A green
build says each proof follows from what it was given; only this says what that
was. Lean's own propext, Classical.choice and Quot.sound are the ambient logic
and are not assumptions about our systems, so a theorem resting on those alone
is proved and anything else it names is load-bearing.

Measured here: 715 theorems queried, 594 proved outright, 121 resting on
axioms. It also ranks the axioms by how many theorems stand on them, which is
where the corpus's weight actually sits — Crypto.NonCustodial.sign carries 15,
and the four Quasar hardness axioms carry 7 each.

Building it surfaced a defect no per-file build could: `Vault.KeyIsolation` and
`Crypto.DEKDerivation` both declared `deriveOrgKEK` at top level, with no
namespace in either, so importing both failed and the corpus could not be
loaded as a whole. They were the only two files in 115 without a namespace, and
nothing imports either. Both now have one.

Modules in no library have no .olean, so importing one aborts the query. Those
are exactly the modules nothing checks; they are named and counted, not
imported.

vacuity.py caught one shape and now catches four, which took it from 55 to 75:

  - `... True`, as before
  - `∃ x, e = x`, closed by `⟨_, rfl⟩` — the witness is whatever the other side
    evaluates to, so the equation says only that it has a value
  - a conclusion that is one of its own hypotheses, `P → P`
  - `sorry` and `admit`, over comment-stripped source

And the claim of a `def` is its body, not the part before `:=`. Cutting at `:=`
for every kind is what let `TSUFBound : Prop := ∃ _a, True` read as content —
along with `MagnetarTSUFBound`, `corona_eufcma_to_mlwe`, and both
`NoLeakReduction`, all now reported.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 11:39:31 -07:00

233 lines
10 KiB
Lean4

/-
Copyright (C) 2025, Lux Industries Inc. All rights reserved.
DEK Derivation Correctness (FieldEncryptor Model)
Extends Vault/KeyIsolation.lean to cover the full FieldEncryptor
key derivation hierarchy used in production:
masterKey → orgKEK → userDEK → fieldKey → AES-256-GCM(field_value)
where:
orgKEK = HMAC(masterKey, orgSlug)
userDEK = HMAC(orgKEK, orgSlug + ":" + userId)
fieldKey = HMAC(userDEK, fieldName)
s3Key = HMAC(masterKey, "s3:user:" + orgSlug + ":" + userId)
Maps to:
- lux/kms: FieldEncryptor key derivation
- lux/vault: per-field encryption at rest
- lux/kms/s3: S3 object key derivation
Properties:
1. Field isolation: different fields for same user get different keys
2. S3 key independence: S3 key is on a separate derivation path
3. DEK rotation safety: rotating masterKey invalidates all old ciphertext
4. No convergent encryption: same plaintext + different user → different ciphertext
5. Derivation determinism: same inputs always produce same DEK
-/
namespace Crypto.DEKDerivation
-- Model HMAC as a PRF: deterministic, collision-resistant, independent outputs
-- for independent inputs. Same axiom style as Vault/KeyIsolation.lean.
axiom PRF : List UInt8 → List UInt8 → List UInt8
axiom prf_deterministic : ∀ k m, PRF k m = PRF k m
axiom prf_collision_resistant :
∀ k m₁ m₂, m₁ ≠ m₂ → PRF k m₁ ≠ PRF k m₂
-- PRF with different keys produces independent outputs.
-- Standard assumption: a PRF keyed with k₁ ≠ k₂ behaves as two
-- independent random functions.
axiom prf_key_independent :
∀ k₁ k₂ m, k₁ ≠ k₂ → PRF k₁ m ≠ PRF k₂ m
-- String axioms: prefix disjointness and append injection.
-- These hold for UTF-8 strings but Lean's String library lacks the lemmas.
-- Stated against the `.toUTF8.toList` form because that's what
-- `PRF (·) (·.toUTF8.toList)` exposes after `simp` unfolding.
axiom string_prefix_disjoint :
∀ {a b : String}, a.toUTF8.toList = b.toUTF8.toList → False
axiom string_append_injective :
∀ (p s₁ s₂ : String),
p ++ ":" ++ s₁ = p ++ ":" ++ s₂ → s₁ = s₂
-- UTF-8 encoding is injective (RFC 3629). Stated here as an axiom
-- because Lean 4 v4.14 stdlib does not export the injectivity lemma
-- — only the validateUTF8 / fromUTF8 round-trip. Both the ByteArray
-- and the underlying List form are accepted because `String.toUTF8`
-- is post-composed with `.toList` in several DEK derivations.
axiom String.toUTF8_injective : ∀ {s t : String},
s.toUTF8.toList = t.toUTF8.toList → s = t
-- Model AES-256-GCM as an AEAD: encryption is randomized (via nonce),
-- and ciphertext depends on key. Different keys → different ciphertext
-- even for the same plaintext.
axiom AEAD_Enc : List UInt8 → List UInt8 → List UInt8 → List UInt8
-- key → nonce → plaintext → ciphertext
axiom aead_key_dependent :
∀ k₁ k₂ nonce pt, k₁ ≠ k₂ → AEAD_Enc k₁ nonce pt ≠ AEAD_Enc k₂ nonce pt
-- AEAD decryption fails with the wrong key
axiom AEAD_Dec : List UInt8 → List UInt8 → List UInt8 → Option (List UInt8)
-- key → nonce → ciphertext → plaintext?
axiom aead_wrong_key :
∀ k_enc k_dec nonce pt,
k_enc ≠ k_dec →
AEAD_Dec k_dec nonce (AEAD_Enc k_enc nonce pt) = none
/-! ## Key Derivation Functions (matching production FieldEncryptor) -/
noncomputable def deriveOrgKEK (masterKey : List UInt8) (orgSlug : String) : List UInt8 :=
PRF masterKey orgSlug.toUTF8.toList
noncomputable def deriveUserDEK (orgKEK : List UInt8) (orgSlug : String) (userId : String) : List UInt8 :=
PRF orgKEK (orgSlug ++ ":" ++ userId).toUTF8.toList
noncomputable def deriveFieldKey (userDEK : List UInt8) (fieldName : String) : List UInt8 :=
PRF userDEK fieldName.toUTF8.toList
noncomputable def deriveS3Key (masterKey : List UInt8) (orgSlug : String) (userId : String) : List UInt8 :=
PRF masterKey ("s3:user:" ++ orgSlug ++ ":" ++ userId).toUTF8.toList
/-! ## Main Theorems -/
/-- Theorem 1: Field isolation — different fields for the same user get
different encryption keys. This ensures that compromising one field's
ciphertext reveals nothing about another field's plaintext. -/
theorem field_isolation
(masterKey : List UInt8) (orgSlug userId : String)
(field1 field2 : String) (h : field1 ≠ field2) :
let orgKEK := deriveOrgKEK masterKey orgSlug
let userDEK := deriveUserDEK orgKEK orgSlug userId
deriveFieldKey userDEK field1 ≠ deriveFieldKey userDEK field2 := by
simp [deriveFieldKey]
apply prf_collision_resistant
intro heq
apply h
exact String.toUTF8_injective (by simpa using heq)
/-- Theorem 2: S3 key independence — the S3 encryption key is derived
directly from masterKey with a distinct prefix, not from the userDEK
chain. Compromising the field key hierarchy does not reveal the S3 key,
and vice versa. -/
theorem s3_key_independent
(masterKey : List UInt8) (orgSlug userId : String) :
let orgKEK := deriveOrgKEK masterKey orgSlug
let userDEK := deriveUserDEK orgKEK orgSlug userId
let s3Key := deriveS3Key masterKey orgSlug userId
-- S3 key is PRF(masterKey, "s3:user:...") while
-- orgKEK is PRF(masterKey, orgSlug). These have different messages
-- (the "s3:user:" prefix distinguishes them).
s3Key ≠ orgKEK := by
simp [deriveS3Key, deriveOrgKEK]
apply prf_collision_resistant
intro heq
-- "s3:user:" ++ orgSlug ++ ":" ++ userId = orgSlug would require
-- the s3 prefix to vanish, which is impossible for non-empty prefix.
exact string_prefix_disjoint heq
/-- Theorem 3: DEK rotation safety — when masterKey is rotated, all derived
keys change. Ciphertext encrypted under old keys cannot be decrypted
with new keys. This forces re-encryption on rotation. -/
theorem dek_rotation_safe
(oldMaster newMaster : List UInt8) (h_rotate : oldMaster ≠ newMaster)
(orgSlug userId fieldName : String) (nonce plaintext : List UInt8) :
let oldOrgKEK := deriveOrgKEK oldMaster orgSlug
let oldUserDEK := deriveUserDEK oldOrgKEK orgSlug userId
let oldFieldKey := deriveFieldKey oldUserDEK fieldName
let newOrgKEK := deriveOrgKEK newMaster orgSlug
let newUserDEK := deriveUserDEK newOrgKEK orgSlug userId
let newFieldKey := deriveFieldKey newUserDEK fieldName
-- Old and new field keys differ (different master → different everything)
oldFieldKey ≠ newFieldKey ∧
-- Decrypting old ciphertext with new key fails
AEAD_Dec newFieldKey nonce (AEAD_Enc oldFieldKey nonce plaintext) = none := by
simp only [deriveOrgKEK, deriveUserDEK, deriveFieldKey]
constructor
· -- Different masters → different orgKEKs → different userDEKs → different fieldKeys
apply prf_key_independent
apply prf_key_independent
apply prf_key_independent
exact h_rotate
· -- Wrong key → decryption fails (AEAD integrity)
apply aead_wrong_key
apply prf_key_independent
apply prf_key_independent
apply prf_key_independent
exact h_rotate
/-- Theorem 4: Convergent encryption disabled — the same plaintext encrypted
for different users produces different ciphertext, because different users
have different field keys. An attacker cannot determine if two users have
the same value in a field by comparing ciphertext. -/
theorem convergent_encryption_disabled
(masterKey : List UInt8) (orgSlug : String)
(user1 user2 : String) (h_users : user1 ≠ user2)
(fieldName : String) (nonce plaintext : List UInt8) :
let orgKEK := deriveOrgKEK masterKey orgSlug
let dek1 := deriveUserDEK orgKEK orgSlug user1
let dek2 := deriveUserDEK orgKEK orgSlug user2
let fk1 := deriveFieldKey dek1 fieldName
let fk2 := deriveFieldKey dek2 fieldName
AEAD_Enc fk1 nonce plaintext ≠ AEAD_Enc fk2 nonce plaintext := by
simp only [deriveOrgKEK, deriveUserDEK, deriveFieldKey]
apply aead_key_dependent
-- Different users → different userDEKs → different field keys
apply prf_key_independent
apply prf_collision_resistant
intro heq
apply h_users
-- (orgSlug ++ ":" ++ user1).toUTF8 = (orgSlug ++ ":" ++ user2).toUTF8 → strings equal
have hstr := String.toUTF8_injective heq
exact string_append_injective orgSlug user1 user2 hstr
/-- Theorem 5: Key derivation is deterministic — same inputs always produce
the same DEK. No randomness in the derivation path means key derivation
is reproducible and stateless. The server does not need to store derived
keys — it can recompute them from masterKey + identifiers. -/
theorem key_derivation_deterministic
(masterKey : List UInt8) (orgSlug userId fieldName : String) :
let orgKEK := deriveOrgKEK masterKey orgSlug
let userDEK := deriveUserDEK orgKEK orgSlug userId
let fieldKey := deriveFieldKey userDEK fieldName
let orgKEK' := deriveOrgKEK masterKey orgSlug
let userDEK' := deriveUserDEK orgKEK' orgSlug userId
let fieldKey' := deriveFieldKey userDEK' fieldName
fieldKey = fieldKey' := by
simp [deriveOrgKEK, deriveUserDEK, deriveFieldKey]
/-! ## Corollaries -/
/-- Corollary: user isolation — different users in the same org get
different DEKs (and therefore different field keys for every field). -/
theorem user_dek_isolation
(masterKey : List UInt8) (orgSlug : String)
(user1 user2 : String) (h : user1 ≠ user2) :
let orgKEK := deriveOrgKEK masterKey orgSlug
deriveUserDEK orgKEK orgSlug user1 ≠ deriveUserDEK orgKEK orgSlug user2 := by
simp [deriveUserDEK]
apply prf_collision_resistant
intro heq
apply h
have hstr := String.toUTF8_injective heq
exact string_append_injective orgSlug user1 user2 hstr
/-- Corollary: org isolation — different orgs produce different orgKEKs,
therefore different userDEKs and field keys. -/
theorem org_dek_isolation
(masterKey : List UInt8)
(org1 org2 : String) (h : org1 ≠ org2) :
deriveOrgKEK masterKey org1 ≠ deriveOrgKEK masterKey org2 := by
simp [deriveOrgKEK]
apply prf_collision_resistant
intro heq
exact h (String.toUTF8_injective (by simpa using heq))
end Crypto.DEKDerivation