429 lines
22 KiB
Markdown
429 lines
22 KiB
Markdown
# 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](../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](../CRITIC.md) v1.6.0 §«Mandatory Security Card per crypto primitive»
|
||
- Code under audit: [crates/mt-crypto/src/lib.rs](../crates/mt-crypto/src/lib.rs)
|
||
- Automated invariants: [crates/mt-crypto/tests/security_invariants.rs](../crates/mt-crypto/tests/security_invariants.rs)
|
||
- High-level audit package: [AUDIT.md](../AUDIT.md)
|
||
- Pre-audit checklist: [audit-checklist.md](audit-checklist.md)
|