mirror of
https://github.com/luxfi/proofs.git
synced 2026-10-30 15:21:05 +00:00
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]>
233 lines
10 KiB
Lean4
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
|