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)
|