22 KiB
22 KiB
Security Cards — Montana M1 cryptographic primitives
Mandatory documentation per Протокол/Код/CRITIC.md v1.6.0 Pass 17 — каждый primitive имеющий secret material обязан иметь заполненную Security Card перед статусом «closed».
Last verified: 2026-04-26 (M1-F audit + Pass 17 Security Card formalization) Scope: M1 foundational layer cryptographic primitives Automated regression: crates/mt-crypto/tests/security_invariants.rs — 13 invariants verified в CI
Card 1: SecretKey (ML-DSA-65)
Security Card для SecretKey (mt_crypto::SecretKey, ML-DSA-65 4032B):
Secret material:
Type: [u8; 4032] heap-allocated через Box<[u8; SECRET_KEY_SIZE]>
Site of construction: crates/mt-crypto/src/lib.rs — impl SecretKey { from_array, from_slice }
+ alloc_locked_secret_box helper (search by function name)
Site of destruction: crates/mt-crypto/src/lib.rs — impl Drop for SecretKey
(line numbers намеренно не фиксированы — синхронизация
с кодом через grep -n "fn from_array\|impl Drop for SecretKey")
Lifecycle:
Construction copies: 1 — bytes копируются один раз с stack source (либо FFI-fill direct в heap Box)
в heap-allocated Box. Stack source zeroized после copy.
Owning type: mt_crypto::SecretKey (private inner Box<[u8; N]>)
Transfer pattern: by-value move; Box pointer copy, не bytes memcpy
Destruction: Drop+zeroize: yes; explicit zeroize sites: 1 (Drop impl)
+ munlock heap страниц перед dealloc
Side-channel surface:
Branching on secret bytes: no (наш Rust shim не имеет операций над bytes;
все ML-DSA arithmetic внутри OpenSSL EVP API)
Memory access pattern: N/A в Layer 1; OpenSSL внутри использует
constant-time access patterns (FIPS 140-3 validated)
PartialEq impl на secret type: disabled (verified compile-time через
security_invariants.rs)
Comparison via ==: no (PartialEq не derived; verified)
Constant-time гарантии: inherited from OpenSSL 3.5.5 LTS — FIPS 140-3
constant-time crypto operations
OS-level hygiene:
mlock applied: yes; через alloc_locked_secret_box() в mt-crypto/src/lib.rs
best-effort (errno ignored на failure — fallback на encrypted swap)
Stack cleansing FFI buffers: explicit — keypair_from_seed аллоцирует Box ДО FFI call,
FFI пишет напрямую в heap; никаких stack temporary buffers
с secret bytes (verified в fn keypair_from_seed)
Swap protection: mlock primary; encrypted swap (FileVault macOS / LUKS Linux)
fallback assumption documented
Core dump protection: рекомендация для operator: setrlimit(RLIMIT_CORE=0) в
production deployment (документируется в Operator Guide,
не enforced на code level)
Logging surface:
println!/log macros на secret: 0 instances (verified file-content scan в
security_invariants.rs::no_println_or_log_on_secret_bytes_in_lib_code)
Debug impl на secret type: not derived; struct fields private (no field access)
Error messages с secret: нет (CryptoError variants не содержат secret bytes)
print_sk-like helper gates: yes — mt-examples/examples/m1_crypto.rs::print_sk gated через
env var M1_DUMP_SK=1 (default redacted; см. fn dump_sk_enabled)
Library properties:
Underlying impl: OpenSSL 3.5.5 LTS EVP_PKEY API (vendored через openssl-src)
Constant-time documented: yes — OpenSSL FIPS 140-3 validation requires constant-time
Audit history: OpenSSL Foundation governance + decades production deployment в
TLS world + FIPS 140-3 certified
Stack cleansing on cleanup: OpenSSL responsibility — EVP_PKEY_free clears internal state
Verified:
Pass 17 checks 1-8: 8/8 closed
1. Constant-time: ✅ inherited OpenSSL
2. Memory access: ✅ no secret-indexed access в Layer 1
3. Branch pattern: ✅ no secret-dependent branches в Layer 1
4. Zeroization on drop: ✅ Drop+zeroize verified
5. Library check: ✅ OpenSSL FIPS 140-3
6. Stack hygiene: ✅ heap-only via Box; FFI пишет в heap
7. OS-level mlock: ✅ best-effort applied
8. Memory barrier: ✅ zeroize crate имеет compiler_fence(SeqCst)
Status: closed
Card 2: MlkemSecretKey (ML-KEM-768)
Security Card для MlkemSecretKey (mt_crypto::MlkemSecretKey, ML-KEM-768 2400B):
Secret material:
Type: [u8; 2400] heap-allocated через Box<[u8; MLKEM_SECRET_KEY_SIZE]>
Site of construction: crates/mt-crypto/src/lib.rs — impl MlkemSecretKey { from_array, from_slice }
(search by function name; line numbers намеренно не фиксированы)
Site of destruction: crates/mt-crypto/src/lib.rs — impl Drop for MlkemSecretKey
Lifecycle:
Construction copies: 1 — bytes на heap, stack source zeroized
Owning type: mt_crypto::MlkemSecretKey (private Box)
Transfer pattern: by-value move (pointer copy)
Destruction: Drop+zeroize + munlock
Side-channel surface:
Branching on secret bytes: no (Layer 1 only передаёт pointer в OpenSSL EVP)
Memory access pattern: N/A в Layer 1; OpenSSL constant-time
PartialEq impl на secret type: disabled (verified)
Comparison via ==: no (verified)
Constant-time гарантии: inherited OpenSSL FIPS 140-3
OS-level hygiene:
mlock applied: yes; alloc_locked_secret_box best-effort
Stack cleansing FFI buffers: explicit — keypair_from_seed_mlkem uses heap
Box directly (mt-crypto/src/lib.rs fn keypair_from_seed_mlkem)
Swap protection: mlock primary; encrypted swap fallback
Core dump protection: operator-level (RLIMIT_CORE=0)
Logging surface:
println!/log macros на secret: 0 (file-content scan verified)
Debug impl: not derived
Error messages: sanitized
Helper gates: нет direct dump helpers для MlkemSK
Library properties:
Underlying impl: OpenSSL 3.5.5 LTS EVP_PKEY ML-KEM-768
Constant-time documented: yes — FIPS 140-3
Audit history: OpenSSL Foundation
Stack cleansing on cleanup: OpenSSL EVP_PKEY_free
Threat model (per-primitive — отличается от ML-DSA Card 1):
- Decapsulation timing: KEM decapsulation алгоритм по design содержит
secret-dependent control flow при failure mode. OpenSSL EVP реализация
делает implicit rejection в constant time (FIPS 203 §6.3 Algorithm 18).
- Plaintext checking attacks: Kyber известна за hijacking decapsulation
через crafted ciphertext (Hofheinz-Hövelmanns-Kiltz [HHK17]).
Защита — implicit rejection с pseudorandom output, реализована в
OpenSSL и проверяется через FIPS 140-3 validation.
- Encapsulation: PK material не секретный, но ciphertext путь содержит
секретный сеансовый ключ. Ciphertext output — public material.
- vs SecretKey (Card 1): ML-DSA SK используется только в Sign (один
secret-touch operation в lifecycle); ML-KEM SK используется в каждом
Decap (множественные exposures, выше критичность constant-time).
Verified:
Pass 17 checks 1-8 (per-primitive analysis для KEM):
1. Constant-time: ✅ inherited OpenSSL FIPS 140-3 (включая
implicit rejection путь для decap failure)
2. Memory access: ✅ no SK-indexed access в Layer 1
3. Branch pattern: ✅ no SK-dependent branches в Layer 1
(decap branching внутри OpenSSL constant-time)
4. Zeroization on drop: ✅ Drop+zeroize verified
5. Library check: ✅ OpenSSL FIPS 140-3
6. Stack hygiene: ✅ heap-only via Box; FFI пишет в heap
7. OS-level mlock: ✅ best-effort applied
8. Memory barrier: ✅ zeroize crate compiler_fence(SeqCst)
Status: closed
Card 3: keypair_from_seed — ML-DSA-65 KeyGen
Security Card для keypair_from_seed (mt_crypto::keypair_from_seed):
Secret material handled:
Input: seed: &[u8; 32] (caller-owned, function берёт по reference)
Output secret: SecretKey (4032B, owned)
Output public: PublicKey (1952B, public material)
Lifecycle:
seed lifecycle: owned by caller; функция читает через &[u8; N], не копирует
в local stack (FFI получает caller's pointer напрямую)
SK construction: heap Box allocated с mlock ДО FFI call (через
alloc_locked_secret_box). FFI пишет SK bytes напрямую
в locked heap memory; no intermediate stack copy.
Error path: sk_box.zeroize() явно вызван перед return Err
ensures partial-fill bytes не leak при FFI failure
Side-channel surface:
Branching on secret: no — Layer 1 проверяет только return code (i32)
Memory access: SK bytes в heap, accessed только OpenSSL внутри
Logging: 0 println/log calls на seed/sk
OS-level hygiene:
mlock на SK: yes (через alloc_locked_secret_box)
mlock на seed: no — seed caller-owned, function не контролирует
(caller responsibility — например mt-mnemonic для
derived seeds через PBKDF2/HKDF локирует master_seed
на своём слое; documented в audit-checklist)
Stack cleanup: no stack temp buffers с secret bytes
Library properties:
Underlying: OpenSSL EVP_PKEY ML-DSA-65 KeyGen (FIPS 204 Algorithm 1)
Determinism: guaranteed via OSSL_PKEY_PARAM_ML_DSA_SEED parameter
NIST conformance: verified byte-exact против NIST ACVP 25/25 KeyGen tests
Threat model (per-primitive — KeyGen specific):
- Seed quality: KeyGen output определяется seed; weak seed → predictable
SK (полная компрометация). Caller responsibility (mt-mnemonic
использует HKDF-Expand от mlocked master_seed; OS CSPRNG в keypair()
test helper через getrandom).
- Seed exposure: seed после KeyGen теоретически восстановим из SK
(FIPS 204 §5.1 ξ encoded внутри SK). Это by-design — recovery flow
через mnemonic regenerates seed → SK байт-идентично.
- Determinism как security feature: same seed → same (pk, sk).
Используется для consensus identity (mt-mnemonic), не уязвимость.
- Stack hygiene critical: KeyGen — единственный momento когда SK байты
появляются «из ниоткуда»; любой stack temp buffer = leak surface.
Защита — heap Box + mlock allocated ДО FFI call, FFI пишет
напрямую в heap memory.
- Error path leak: при FFI failure partial-fill bytes могут leak —
защита через явный sk_box.zeroize() перед return Err.
Verified:
Pass 17 checks (per-primitive analysis для KeyGen):
1. Constant-time: ✅ FIPS 204 Algorithm 1 KeyGen внутри
OpenSSL (validation pending external review,
hardware side-channel separately)
2. Memory access: ✅ no secret-indexed access в Layer 1
3. Branch pattern: ✅ Layer 1 проверяет только return code (i32);
no seed-dependent / sk-dependent branches
4. Zeroization on drop: ✅ через returned SecretKey type (Card 1)
+ явный sk_box.zeroize() на error path
5. Library check: ✅ OpenSSL FIPS 140-3 (KeyGen validated)
6. Stack hygiene: ✅ heap Box + mlock allocated ДО FFI;
no stack temporary buffers с secret bytes
7. OS-level mlock: ✅ via alloc_locked_secret_box (best-effort)
8. Memory barrier: ✅ inherited from SecretKey Drop
Status: closed
Card 4: keypair_from_seed_mlkem — ML-KEM-768 KeyGen
Security Card для keypair_from_seed_mlkem:
Secret material handled:
Input: seed: &[u8; 64] (d || z per FIPS 203 §6.1)
Output secret: MlkemSecretKey (2400B)
Output public: MlkemPublicKey (1184B)
Lifecycle:
seed lifecycle: caller-owned, &-borrow
SK construction: heap Box + mlock ДО FFI call (через alloc_locked_secret_box)
Error path: sk_box.zeroize() перед return Err
Side-channel surface:
Same as ML-DSA KeyGen — Layer 1 thin FFI shim, no secret-dependent operations
OS-level hygiene:
mlock на SK: yes
mlock на seed: caller responsibility
Stack cleanup: no stack temp с secret bytes
Library properties:
Underlying: OpenSSL EVP_PKEY ML-KEM-768 KeyGen (FIPS 203 Algorithm 16)
Determinism: guaranteed via OSSL_PKEY_PARAM_ML_KEM_SEED
NIST conformance: verified byte-exact против NIST ACVP 25/25 KeyGen tests
Threat model (per-primitive — KEM KeyGen specific, vs ML-DSA Card 3):
- Seed format: 64-byte d ‖ z per FIPS 203 §6.1 (vs 32-byte ξ для ML-DSA).
Двойная domain separation внутри seed (d для key generation polynomial,
z для implicit rejection PRF). Oba компонента secret-critical.
- Implicit rejection key: z часть seed становится PRF-ключом для
decapsulation failure mode. Compromised z = enable plaintext-checking
attack [HHK17]. Защита — z никогда не покидает SK heap.
- Stack hygiene: same as ML-DSA KeyGen (heap Box + mlock ДО FFI).
- Key reuse: ML-KEM SK можно использовать многократно в Decap (vs
ML-DSA SK в Sign — multiple operations OK). Lifetime exposure выше
чем для signature SK → mlock/zeroize критичнее.
Verified:
Pass 17 checks (per-primitive analysis для KEM KeyGen):
1. Constant-time: ✅ FIPS 203 Algorithm 16 KeyGen внутри
OpenSSL (FIPS 140-3 validated)
2. Memory access: ✅ no seed/sk-indexed access в Layer 1
3. Branch pattern: ✅ Layer 1 проверяет только return code
4. Zeroization on drop: ✅ через MlkemSecretKey type (Card 2)
+ явный sk_box.zeroize() на error path
5. Library check: ✅ OpenSSL FIPS 140-3
6. Stack hygiene: ✅ heap Box + mlock allocated ДО FFI
7. OS-level mlock: ✅ via alloc_locked_secret_box
8. Memory barrier: ✅ inherited from MlkemSecretKey Drop
Status: closed
Card 5: sign — ML-DSA-65 deterministic Sign
Security Card для sign (mt_crypto::sign):
Secret material handled:
Input: sk: &SecretKey (borrowed)
Output secret: none — signature is public material
Lifecycle:
sk access: read-only borrow; bytes остаются в caller's heap
Box на всё время вызова
signature construct: Stack-allocated [u8; 3309] — public material, не secret
Drop: sig is public, no zeroize нужен
Side-channel surface:
Branching on secret bytes: no (Layer 1 проверяет только return code)
Memory access: sk.0.as_ptr() передан в FFI; OpenSSL внутри делает
constant-time deterministic Sign (FIPS 204 Algorithm 2)
PartialEq на signature: derived (Signature: PartialEq) — OK, signature public
Logging: 0 на sk; no println на signature внутри sign()
OS-level hygiene:
mlock на sk: inherited from SecretKey (already locked)
Stack cleanup: none нужен (no stack secret bytes; signature public)
Library properties:
Underlying: OpenSSL EVP_DigestSign + OSSL_SIGNATURE_PARAM_DETERMINISTIC=1
Determinism: FIPS 204 Algorithm 2 deterministic variant — required для
Montana [I-3] consensus determinism
Constant-time: OpenSSL FIPS 140-3
NIST conformance: verified byte-exact против NIST ACVP 1/1 deterministic
SigGen test (empty context)
Threat model (per-primitive — Sign specific, vs SecretKey Card 1):
- Deterministic Sign critical для consensus: identical (sk, msg) → identical
signature. Required для Montana [I-3] determinism — две имплементации
подписывают тот же message и получают bit-identical signature. Random
signing variant запрещён в consensus path.
- Sign timing: внутренний rejection sampling в FIPS 204 Algorithm 2
может иметь secret-dependent number of iterations. OpenSSL FIPS 140-3
реализация делает constant-time за счёт fixed-iteration upper bound.
- Signature output: NOT secret material (signature + msg + pk → public).
Signature::PartialEq derived OK, no zeroize нужен, stack-allocated OK.
- Side-channel surface: Sign — main attack target в lattice schemes
(BLISS, Falcon, Dilithium все имели side-channel papers). OpenSSL
constant-time implementation проходит FIPS 140-3 attestation, но
hardware side-channel testing вне scope (см. AUDIT.md Out of Scope §5).
- SK exposure during Sign: sk.0.as_ptr() передан в FFI; OpenSSL читает
из heap-locked memory; никаких stack copies SK bytes в Layer 1.
Verified:
Pass 17 checks (per-primitive analysis для Sign):
1. Constant-time: ✅ FIPS 204 Algorithm 2 deterministic Sign
constant-time через OpenSSL FIPS 140-3
2. Memory access: ✅ no SK-indexed access в Layer 1
(FFI передаёт only pointer, не indexes)
3. Branch pattern: ✅ no SK-dependent branches в Layer 1
4. Zeroization on drop: ✅ Signature не содержит secret material
(no zeroize нужен); SK через Card 1
5. Library check: ✅ OpenSSL FIPS 140-3
6. Stack hygiene: ✅ no SK bytes на stack; signature output
на stack acceptable (public material)
7. OS-level mlock: ✅ inherited from SecretKey (already locked)
8. Memory barrier: ✅ inherited from SecretKey Drop
Status: closed
Card 6: verify — ML-DSA-65 SigVer
Security Card для verify (mt_crypto::verify):
Secret material handled: none
Input: pk: &PublicKey (public material)
msg: &[u8] (public material)
sig: &Signature (public material)
Output: bool (verify result)
Lifecycle: no secret material involved
Side-channel surface:
Branching on PK bytes: no (PK public, не secret — branching на PK acceptable)
Memory access: pk/sig bytes accessed только в OpenSSL (constant-time
не critical для public material, но OpenSSL делает
constant-time всё равно по design)
Logging: 0
Library properties:
Underlying: OpenSSL EVP_DigestVerify
Constant-time: FIPS 140-3 (по design, хотя для public material не critical)
Threat model (per-primitive — Verify specific, NO secret material):
- PK / msg / sig — все public. Branching на их bytes acceptable
(нечего leak-ать). Это фундаментальное отличие от Sign Card 5.
- Verify result — boolean, public-derivable от inputs. No timing leak
concern (timing зависит от inputs которые public).
- DoS surface: malformed signature → constant-time rejection? Не critical
(caller может rate-limit verify calls на чужих signatures).
- Cross-implementation conformance: Verify должен accept signature от
любой FIPS 204 imlementation. Это reverse направление от Sign
determinism — Sign даёт identical bytes, Verify accepts canonical encoding.
Pass 17 checks (per-primitive analysis для Verify):
Не applicable полностью — нет secret material:
1-3. Constant-time / memory access / branching: N/A для public material
4. Zeroization: N/A для public material
5. Library check: ✅ OpenSSL FIPS 140-3
6-8. Stack / mlock / barrier: N/A для public material
Status: closed (no secret material — Security Card minimal по design)
Re-audit schedule
Все Security Cards re-verified автоматически через:
crates/mt-crypto/tests/security_invariants.rs— каждый CI run- Manual re-audit обязателен:
- При смене upstream library (OpenSSL upgrade)
- При изменении FFI signature (mt-crypto-native API)
- При добавлении нового entry point с secret bytes
- Каждые 6 месяцев wallclock на existing primitives (next: 2026-10-26)
Cross-references
- Critic role enforcement: CRITIC.md v1.6.0 §«Mandatory Security Card per crypto primitive»
- Code under audit: crates/mt-crypto/src/lib.rs
- Automated invariants: crates/mt-crypto/tests/security_invariants.rs
- High-level audit package: AUDIT.md
- Pre-audit checklist: audit-checklist.md