From 9c80ff3fc1075b9e187c41389554b7b132fef5e3 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:10:00 +0900 Subject: [PATCH 1/4] =?UTF-8?q?ML-DSA=20=ED=83=84=EC=83=9D!!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTION.md | 1 + CONTRIBUTION_EN.md | 1 + crypto/mldsa/Cargo.toml | 12 + crypto/mldsa/src/error.rs | 25 ++ crypto/mldsa/src/field.rs | 57 +++ crypto/mldsa/src/lib.rs | 132 ++++++ crypto/mldsa/src/mldsa.rs | 592 +++++++++++++++++++++++++++ crypto/mldsa/src/mldsa_keys.rs | 364 +++++++++++++++++ crypto/mldsa/src/mldsa_sign.rs | 701 ++++++++++++++++++++++++++++++++ crypto/mldsa/src/ntt.rs | 599 +++++++++++++++++++++++++++ crypto/mldsa/src/pack.rs | 720 +++++++++++++++++++++++++++++++++ crypto/mldsa/src/poly.rs | 120 ++++++ crypto/mldsa/src/sample.rs | 171 ++++++++ 13 files changed, 3495 insertions(+) create mode 100644 crypto/mldsa/Cargo.toml create mode 100644 crypto/mldsa/src/error.rs create mode 100644 crypto/mldsa/src/field.rs create mode 100644 crypto/mldsa/src/lib.rs create mode 100644 crypto/mldsa/src/mldsa.rs create mode 100644 crypto/mldsa/src/mldsa_keys.rs create mode 100644 crypto/mldsa/src/mldsa_sign.rs create mode 100644 crypto/mldsa/src/ntt.rs create mode 100644 crypto/mldsa/src/pack.rs create mode 100644 crypto/mldsa/src/poly.rs create mode 100644 crypto/mldsa/src/sample.rs diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index e44001b..1321c38 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -64,6 +64,7 @@ _안녕하세요. 저희는 팀 퀀트(Quant)이며, 저는 Quant Theodore Felix - 공통 - **올바른 오류 전파 방법**: 많은 크레이트의 핵심 기능은 `Result` 열거형을 통해 `SecureBuffer` 구조체와 문자열 참조를 반환합니다. 이는 오류 전파에 부적절합니다. - **컴플라이언스 문제**: 암호 모듈 구현에 있어 국제적 인증 및 규정을 준수하지 않은 부분을 발견했다면, 즉시 연락주세요. + - **오류 메시지**: 오류 메시지는 기본적으로 모호해야 하지만 알아차리기 애-매한 정도로 진실성이 있어야 합니다. 현재 오류 메시지는 어때 보이시나요? - 보안 버퍼 크레이트 `entlib-native-secure-buffer` - **베어메탈 캐시 플러시 문제**: `zeroizer.rs` 내 no_std 폐쇄 환경을 위한 Fall-back 시, 해당 환경의 하드웨어(CPU) 특성에 따라 캐시 라인 플러시가 보장되지 않을 수 있다고 합니다. 이 부분에 대해 섬세한 평가검증이 필요합니다. - **이중 잠금**: JO(Java-Owned) 패턴을 통해 상호 작용 시 메모리 lock 수행 후 전달됩니다. Rust 측 `SecureMemoryBlock` 구조체는 이 데이터에 대해 한 번 더 lock을 수행합니다. 이 작업에 대해 어떻게 생각하시나요? diff --git a/CONTRIBUTION_EN.md b/CONTRIBUTION_EN.md index a28396f..910590f 100644 --- a/CONTRIBUTION_EN.md +++ b/CONTRIBUTION_EN.md @@ -64,6 +64,7 @@ Contributions corresponding to the following items for this project are classifi - Common - **Correct error propagation method**: The core function of many crates returns a `SecureBuffer` struct and a string reference through a `Result` enum. This is inappropriate for error propagation. - **Compliance issues**: If you find any parts that do not comply with international certifications and regulations in the implementation of the cryptographic module, please contact us immediately. + - **Error messages**: Error messages should be ambiguous by default, but they must be truthful enough to be subtly recognizable. What do you think of the current error messages? - Secure buffer crate `entlib-native-secure-buffer` - **Bare-metal cache flush issue**: When falling back for a no_std closed environment in `zeroizer.rs`, it is said that cache line flushing may not be guaranteed depending on the hardware (CPU) characteristics of the environment. Delicate evaluation and verification are needed for this part. - **Double lock**: When interacting through the JO (Java-Owned) pattern, the memory is locked and then transmitted. The `SecureMemoryBlock` struct on the Rust side performs another lock on this data. What do you think about this operation? diff --git a/crypto/mldsa/Cargo.toml b/crypto/mldsa/Cargo.toml new file mode 100644 index 0000000..d5a17ac --- /dev/null +++ b/crypto/mldsa/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "entlib-native-mldsa" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +entlib-native-constant-time.workspace = true +entlib-native-rng.workspace = true +entlib-native-secure-buffer.workspace = true +entlib-native-sha3.workspace = true \ No newline at end of file diff --git a/crypto/mldsa/src/error.rs b/crypto/mldsa/src/error.rs new file mode 100644 index 0000000..bf12e61 --- /dev/null +++ b/crypto/mldsa/src/error.rs @@ -0,0 +1,25 @@ +// mod 오 에러 정의하는것보단 enum이 괜찮을 듯 + +#[derive(Debug)] +pub enum MLDSAError { + /// 입력 바이트 슬라이스의 길이가 요구사항과 일치하지 않습니다. + InvalidLength(&'static str), + /// 내부 연산 실패 (예: 해시 함수 오류, 메모리 할당 실패) + InternalError(&'static str), + /// 난수 생성기(RNG) 오류 + RngError(&'static str), + /// ctx(컨텍스트) 길이가 FIPS 204 제한(255바이트)을 초과합니다. + ContextTooLong, + /// 서명 시도가 최대 반복 횟수를 초과하였습니다 (극히 희박한 경우). + SigningFailed, + /// 서명 검증 실패 + InvalidSignature, + /// 아직 구현되지 않은 기능입니다. + NotImplemented(&'static str), +} + +impl From<&'static str> for MLDSAError { + fn from(s: &'static str) -> Self { + MLDSAError::InternalError(s) + } +} diff --git a/crypto/mldsa/src/field.rs b/crypto/mldsa/src/field.rs new file mode 100644 index 0000000..1a93891 --- /dev/null +++ b/crypto/mldsa/src/field.rs @@ -0,0 +1,57 @@ +use crate::Q; +use crate::Q_INV; +use entlib_native_constant_time::traits::{ConstantTimeIsNegative, ConstantTimeSelect}; + +/// 유한체 Z_q의 원소를 나타내는 구조체 +#[derive(Clone, Copy, Debug, Default)] +pub struct Fq(pub i32); + +impl Fq { + /// 새로운 필드 요소를 생성합니다. (입력값은 [0, Q-1] 범위 내에 있어야 함) + #[inline(always)] + pub const fn new(val: i32) -> Self { + Self(val) + } + + /// 상수-시간 모듈러 덧셈 + pub fn add(self, other: Self) -> Self { + let sum = self.0 + other.0; + // sum은 최대 2Q - 2 값을 가질 수 있습니다. Q를 빼서 범위를 맞춥니다. + let sub = sum - Q; + + // sub가 음수(즉, sum < Q)이면 sum을 선택하고, 그렇지 않으면 sub를 선택합니다. + let is_neg = sub.ct_is_negative(); + Self(i32::ct_select(&sum, &sub, is_neg)) + } + + /// 상수-시간 모듈러 뺄셈 + pub fn sub(self, other: Self) -> Self { + let diff = self.0 - other.0; + // diff는 음수가 될 수 있으므로 Q를 더한 값을 준비합니다. + let add = diff + Q; + + // diff가 음수이면 Q를 더한 add를 선택하고, 그렇지 않으면 diff를 유지합니다. + let is_neg = diff.ct_is_negative(); + Self(i32::ct_select(&add, &diff, is_neg)) + } + + /// 몽고메리 환원을 이용한 상수-시간 모듈러 곱셈 + /// + /// a * b * R^(-1) mod Q 연산을 수행합니다. (여기서 R = 2^32) + pub fn mul(self, other: Self) -> Self { + let prod = (self.0 as i64) * (other.0 as i64); + + // t = (prod * Q_INV) mod 2^32 + let t = (prod as i32).wrapping_mul(Q_INV); + // t_q = t * Q + let t_q = (t as i64) * (Q as i64); + + // u = (prod - t_q) / 2^32 + let u = ((prod - t_q) >> 32) as i32; + + // u는 [-Q, Q-1] 범위에 있습니다. 음수인 경우 Q를 더해 보정합니다. + let is_neg = u.ct_is_negative(); + let u_plus_q = u + Q; + Self(i32::ct_select(&u_plus_q, &u, is_neg)) + } +} diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs new file mode 100644 index 0000000..bd6b340 --- /dev/null +++ b/crypto/mldsa/src/lib.rs @@ -0,0 +1,132 @@ +// 어중간한 에러는 모두 InternalError 로 모호하게 +mod error; +mod field; +mod mldsa; +mod mldsa_keys; +mod mldsa_sign; +mod ntt; +mod pack; +mod poly; +mod sample; + +#[cfg(test)] +mod _mldsa_test; + +// +// Commons +// + +/// 모듈러스 q +pub(crate) const Q: i32 = 8380417; +/// t에서 버려지는 비트 수 +pub(crate) const D: usize = 13; +// Z_q 내의 512 제곱근 +// pub(crate) const ZETA: i32 = 1753; +/// 몽고메리 환원을 위한 상수 q^(-1) mod 2^32 +pub(crate) const Q_INV: i32 = 58728449; +pub(crate) const SEED_LEN: usize = 32; + +// +// ML-DSA-44 Params +// + +mod mldsa44 { + /// ML-DSA-44 공개 키 길이 + pub(crate) const MLDSA44_PK_LEN: usize = 1312; + /// ML-DSA-44 비밀 키 길이 + pub(crate) const MLDSA44_SK_LEN: usize = 2560; + /// ML-DSA-44 서명 길이 + pub(crate) const MLDSA44_SIG_LEN: usize = 2420; + /// 행렬 A의 k 차원 + pub(crate) const K_44: usize = 4; + /// 행렬 A의 l 차원 + pub(crate) const L_44: usize = 4; + /// 개인키(Private key) 계수 범위 η (eta) + pub(crate) const ETA_44: i32 = 2; + /// 다항식 c에서 ±1의 개수 τ (tau) + pub(crate) const TAU_44: usize = 39; + /// β = τ * η + pub(crate) const BETA_44: i32 = 78; + /// c 틸다(tilde)의 충돌 강도 λ (lambda) + pub(crate) const LAMBDA_44: usize = 128; + /// y의 계수 범위 γ1 (gamma1) = 2^17 + pub(crate) const GAMMA1_44: i32 = 131072; + /// 하위 차수 반올림 범위 γ2 (gamma2) = (q - 1) / 88 + pub(crate) const GAMMA2_44: i32 = 95232; + /// 힌트 h에서 1의 최대 개수 ω (omega) + pub(crate) const OMEGA_44: usize = 80; +} + +// +// ML-DSA-65 Params +// + +mod mldsa65 { + /// ML-DSA-65 공개 키 길이 + pub(crate) const MLDSA65_PK_LEN: usize = 1952; + /// ML-DSA-65 비밀 키 길이 + pub(crate) const MLDSA65_SK_LEN: usize = 4032; + /// ML-DSA-65 서명 길이 + pub(crate) const MLDSA65_SIG_LEN: usize = 3309; + /// 행렬 A의 k 차원 + pub(crate) const K_65: usize = 6; + /// 행렬 A의 l 차원 + pub(crate) const L_65: usize = 5; + /// 개인키(Private key) 계수 범위 η (eta) + pub(crate) const ETA_65: i32 = 4; + /// 다항식 c에서 ±1의 개수 τ (tau) + pub(crate) const TAU_65: usize = 49; + /// β = τ * η + pub(crate) const BETA_65: i32 = 196; + /// c 틸다(tilde)의 충돌 강도 λ (lambda) + pub(crate) const LAMBDA_65: usize = 192; + /// y의 계수 범위 γ1 (gamma1) = 2^19 + pub(crate) const GAMMA1_65: i32 = 524288; + /// 하위 차수 반올림 범위 γ2 (gamma2) = (q - 1) / 32 + pub(crate) const GAMMA2_65: i32 = 261888; + /// 힌트 h에서 1의 최대 개수 ω (omega) + pub(crate) const OMEGA_65: usize = 55; +} + +// +// ML-DSA-87 Params +// + +mod mldsa87 { + /// ML-DSA-87 공개 키 길이 + pub(crate) const MLDSA87_PK_LEN: usize = 2592; + /// ML-DSA-87 비밀 키 길이 + pub(crate) const MLDSA87_SK_LEN: usize = 4896; + /// ML-DSA-87 서명 길이 + pub(crate) const MLDSA87_SIG_LEN: usize = 4627; + /// 행렬 A의 k 차원 + pub(crate) const K_87: usize = 8; + /// 행렬 A의 l 차원 + pub(crate) const L_87: usize = 7; + /// 개인키(Private key) 계수 범위 η (eta) + pub(crate) const ETA_87: i32 = 2; + /// 다항식 c에서 ±1의 개수 τ (tau) + pub(crate) const TAU_87: usize = 60; + /// β = τ * η + pub(crate) const BETA_87: i32 = 120; + /// c 틸다(tilde)의 충돌 강도 λ (lambda) + pub(crate) const LAMBDA_87: usize = 256; + /// y의 계수 범위 γ1 (gamma1) = 2^19 + pub(crate) const GAMMA1_87: i32 = 524288; + /// 하위 차수 반올림 범위 γ2 (gamma2) = (q - 1) / 32 + pub(crate) const GAMMA2_87: i32 = 261888; + /// 힌트 h에서 1의 최대 개수 ω (omega) + pub(crate) const OMEGA_87: usize = 75; +} + +// +// API Signature +// + +pub use error::MLDSAError; +pub use mldsa::{ + CtrDRBGRng, HashDRBGRng, MLDSA, MLDSAParameter, MLDSAPrivateKey, MLDSAPublicKey, MLDSARng, +}; + +// todo: 아마도 유닛 테스트는 src/ 에 추가 +// 외부 시그니처에 대한 테스트는 tests/ diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs new file mode 100644 index 0000000..6483613 --- /dev/null +++ b/crypto/mldsa/src/mldsa.rs @@ -0,0 +1,592 @@ +//! FIPS 204 명세에 따른 모듈 격자 기반 전자 서명(Module Lattice-based Digital Signature Algorithm, ML-DSA) +//! 알고리즘 구현 모듈입니다. 해당 명세 서명 스키마의 최상위 공개 인터페이스를 제공합니다. +//! +//! # Example +//! ```rust,ignore +//! use entlib_native_mldsa::{MLDSA, MLDSAParameter, HashDRBGRng}; +//! +//! // 1. RNG 초기화 (OS 엔트로피 소스 사용 — 임의 엔트로피 주입 불가) +//! let mut rng = HashDRBGRng::new_from_os(None).unwrap(); +//! +//! // 2. 키 쌍 생성 (ML-DSA-44) +//! let (pk_bytes, sk_buf) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).unwrap(); +//! +//! // 3. 서명 +//! let message = b"Hello, ML-DSA!"; +//! let ctx = b""; +//! let sig = MLDSA::sign(MLDSAParameter::MLDSA44, &sk_buf, message, ctx, &mut rng).unwrap(); +//! +//! // 4. 검증 +//! let ok = MLDSA::verify(MLDSAParameter::MLDSA44, &pk_bytes, message, &sig, ctx).unwrap(); +//! assert!(ok); +//! ``` + +use super::mldsa44::{ + BETA_44, ETA_44, GAMMA1_44, GAMMA2_44, K_44, L_44, LAMBDA_44, MLDSA44_PK_LEN, MLDSA44_SIG_LEN, + MLDSA44_SK_LEN, OMEGA_44, TAU_44, +}; +use super::mldsa65::{ + BETA_65, ETA_65, GAMMA1_65, GAMMA2_65, K_65, L_65, LAMBDA_65, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, + MLDSA65_SK_LEN, OMEGA_65, TAU_65, +}; +use super::mldsa87::{ + BETA_87, ETA_87, GAMMA1_87, GAMMA2_87, K_87, L_87, LAMBDA_87, MLDSA87_PK_LEN, MLDSA87_SIG_LEN, + MLDSA87_SK_LEN, OMEGA_87, TAU_87, +}; +use crate::error::MLDSAError; +use crate::mldsa_keys::keygen_internal; +use crate::mldsa_keys::{ + MLDSAPrivateKey as SkComponents, MLDSAPrivateKeyTrait, MLDSAPublicKey as PkComponents, + MLDSAPublicKeyTrait, +}; +use crate::mldsa_sign::{sign_internal_impl, verify_internal_impl}; +use entlib_native_rng::{DrbgError, HashDRBGSHA512}; +use entlib_native_secure_buffer::SecureBuffer; + +// +// RNG 추상화 트레이트 +// + +/// ML-DSA 연산에 사용되는 암호학적으로 안전한 난수 생성기 트레이트. +/// +/// 이 트레이트를 구현하는 타입은 NIST SP 800-90A Rev.1 이상의 보안 강도를 +/// 제공하는 결정론적 난수 비트 생성기(DRBG)여야 합니다. +/// +/// # Features +/// - [`HashDRBGRng`]: NIST Hash_DRBG (SHA-512, Security Strength 256-bit) +/// - [`CtrDRBGRng`]: NIST CTR_DRBG (AES-256-CTR) **향후 개발 예정** +pub trait MLDSARng { + /// `dest` 슬라이스를 암호학적으로 안전한 난수 바이트로 채웁니다. + /// + /// # Errors + /// - `MLDSAError::RngError`: RNG 내부 오류 또는 reseed가 필요한 경우 + fn fill_random(&mut self, dest: &mut [u8]) -> Result<(), MLDSAError>; +} + +// +// Hash_DRBG 래퍼 +// + +/// NIST SP 800-90A Rev.1 Hash_DRBG (SHA-512 기반) RNG 래퍼. +/// +/// ML-DSA 키 생성 및 서명에 사용하도록 설계되었습니다. +/// - Security Strength: **256-bit** +/// - 최소 엔트로피: 32바이트 +/// - 최소 Nonce: 16바이트 +/// - 내부 상태(V, C)는 [`SecureBuffer`]에 보관되어 Drop 시 자동 소거됩니다. +/// +/// # Security Note +/// `entropy_input`은 반드시 `/dev/urandom`, HWRNG, 또는 동등한 암호학적 +/// 엔트로피 소스에서 획득해야 합니다. 예측 가능한 값을 절대 사용하지 마세요. +pub struct HashDRBGRng { + inner: HashDRBGSHA512, +} + +impl HashDRBGRng { + /// OS 엔트로피 소스로부터 Hash_DRBG(SHA-512)를 초기화합니다. + /// + /// 이것이 유일한 초기화 경로입니다. 외부에서 임의 엔트로피를 주입할 수 없으며, + /// OS(Linux: `getrandom(2)`, macOS: `getentropy(2)`)가 수집한 엔트로피만 사용됩니다. + /// + /// # Arguments + /// - `personalization_string`: 선택적 응용 프로그램 식별 문자열 (최대 125 bytes) + /// + /// # Errors + /// - `MLDSAError::RngError`: OS 엔트로피 소스 접근 실패 또는 내부 오류 + pub fn new_from_os(personalization_string: Option<&[u8]>) -> Result { + let inner = HashDRBGSHA512::new_from_os(personalization_string).map_err(drbg_err)?; + Ok(Self { inner }) + } + + /// 현재 RNG 상태를 새 엔트로피로 갱신합니다. + /// + /// `MLDSAError::RngError(ReseedRequired)`를 수신한 경우 반드시 호출해야 합니다. + pub fn reseed( + &mut self, + entropy_input: &[u8], + additional_input: Option<&[u8]>, + ) -> Result<(), MLDSAError> { + self.inner + .reseed(entropy_input, additional_input) + .map_err(drbg_err) + } +} + +impl MLDSARng for HashDRBGRng { + fn fill_random(&mut self, dest: &mut [u8]) -> Result<(), MLDSAError> { + self.inner.generate(dest, None).map_err(drbg_err) + } +} + +// +// CTR_DRBG 래퍼 (미구현 — 확장 예약) +// + +/// NIST SP 800-90A Rev.1 CTR_DRBG (AES-256-CTR 기반) RNG 래퍼. +/// +/// AES-256-CTR 구현이 준비되면 이 구조체에 내부 상태를 추가하고 +/// [`MLDSARng`] impl 내에서 CTR_DRBG 알고리즘을 구현합니다. +/// +/// # Security Note +/// **현재 미구현 상태입니다.** +/// AES-256 블록 암호 구현 크레이트(`entlib-native-aes`) 완료 후 제공될 예정입니다. +pub struct CtrDRBGRng { + // todo: AES-256-CTR DRBG 상태 여기에 추가하면 됌 + // key: SecureBuffer (256-bit) + // value: SecureBuffer (128-bit, AES block size) + // reseed_counter: u64 + _private: (), +} + +impl CtrDRBGRng { + /// CTR_DRBG를 초기화합니다. + /// + /// **현재 항상 `MLDSAError::NotImplemented`를 반환합니다.** + pub fn new(_entropy_input: &[u8], _nonce: &[u8]) -> Result { + Err(MLDSAError::NotImplemented( + "CTR_DRBG: AES-256 구현 완료 후 제공됩니다", + )) + } +} + +impl MLDSARng for CtrDRBGRng { + fn fill_random(&mut self, _dest: &mut [u8]) -> Result<(), MLDSAError> { + Err(MLDSAError::NotImplemented( + "CTR_DRBG: AES-256 구현 완료 후 제공됩니다", + )) + } +} + +// +// ML-DSA 파라미터 셋 +// + +/// NIST FIPS 204에 정의된 ML-DSA 파라미터 셋 +/// +/// | 파라미터 셋 | NIST 카테고리 | pk 크기 | sk 크기 | 서명 크기 | +/// |-------------|:------------:|--------:|--------:|----------:| +/// | MLDSA44 | 2 (AES-128 동급) | 1312 B | 2560 B | 2420 B | +/// | MLDSA65 | 3 (AES-192 동급) | 1952 B | 4032 B | 3309 B | +/// | MLDSA87 | 5 (AES-256 동급) | 2592 B | 4896 B | 4627 B | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MLDSAParameter { + /// ML-DSA-44: NIST 보안 카테고리 2 (Security Strength ≥ 128-bit) + MLDSA44, + /// ML-DSA-65: NIST 보안 카테고리 3 (Security Strength ≥ 192-bit) + MLDSA65, + /// ML-DSA-87: NIST 보안 카테고리 5 (Security Strength ≥ 256-bit) + MLDSA87, +} + +impl MLDSAParameter { + /// 공개 키 바이트 길이를 반환합니다. + #[inline] + pub const fn pk_len(self) -> usize { + match self { + MLDSAParameter::MLDSA44 => 1312, + MLDSAParameter::MLDSA65 => 1952, + MLDSAParameter::MLDSA87 => 2592, + } + } + + /// 비밀 키 바이트 길이를 반환합니다. + #[inline] + pub const fn sk_len(self) -> usize { + match self { + MLDSAParameter::MLDSA44 => 2560, + MLDSAParameter::MLDSA65 => 4032, + MLDSAParameter::MLDSA87 => 4896, + } + } + + /// 서명 바이트 길이를 반환합니다. + #[inline] + pub const fn sig_len(self) -> usize { + match self { + MLDSAParameter::MLDSA44 => 2420, + MLDSAParameter::MLDSA65 => 3309, + MLDSAParameter::MLDSA87 => 4627, + } + } +} + +// +// 공개 키 / 비밀 키 타입 +// + +/// ML-DSA 공개 키. +/// +/// 인코딩된 공개 키 바이트(`ρ || SimpleBitPack(t1)`)와 파라미터 셋을 함께 보유합니다. +/// [`MLDSA::key_gen`]이 반환하며, [`MLDSA::verify`]에 직접 전달할 수 있습니다. +pub struct MLDSAPublicKey { + param: MLDSAParameter, + bytes: Vec, +} + +impl MLDSAPublicKey { + /// 이 공개 키가 속한 파라미터 셋을 반환합니다. + #[inline] + pub fn param(&self) -> MLDSAParameter { + self.param + } + + /// 인코딩된 공개 키 바이트 슬라이스를 반환합니다. + /// + /// 반환값은 FIPS 204 `pkEncode` 출력(`ρ || SimpleBitPack(t1, 10)`)과 동일합니다. + #[inline] + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// 인코딩된 공개 키의 바이트 길이를 반환합니다. + #[inline] + pub fn len(&self) -> usize { + self.bytes.len() + } + + /// 공개 키가 비어 있으면 `true`를 반환합니다 (정상적으로 생성된 키에서는 발생하지 않습니다). + #[inline] + pub fn is_empty(&self) -> bool { + self.bytes.is_empty() + } +} + +/// ML-DSA 비밀 키. +/// +/// 직렬화된 비밀 키 바이트를 OS 레벨 잠금 메모리([`SecureBuffer`])에 보관합니다. +/// `Drop` 시점에 메모리가 자동으로 소거(Zeroize)됩니다. +/// +/// [`MLDSA::key_gen`]이 반환하며, [`MLDSA::sign`]에 직접 전달할 수 있습니다. +pub struct MLDSAPrivateKey { + param: MLDSAParameter, + sk_buf: SecureBuffer, +} + +impl MLDSAPrivateKey { + /// 이 비밀 키가 속한 파라미터 셋을 반환합니다. + #[inline] + pub fn param(&self) -> MLDSAParameter { + self.param + } + + /// 인코딩된 비밀 키의 바이트 길이를 반환합니다. + #[inline] + pub fn len(&self) -> usize { + self.sk_buf.len() + } + + /// 비밀 키가 비어 있으면 `true`를 반환합니다 (정상적으로 생성된 키에서는 발생하지 않습니다). + #[inline] + pub fn is_empty(&self) -> bool { + self.sk_buf.is_empty() + } +} + +// +// MLDSA 공개 API +// + +/// NIST FIPS 204 ML-DSA 서명 스키마의 최상위 진입점. +/// +/// 모든 메소드는 정적(static)이며, 파라미터 셋 정보는 키 타입에 내장됩니다. +pub struct MLDSA; + +impl MLDSA { + // + // 외부 인터페이스 (FIPS 204 Algorithms 1–3) + // + + /// Algorithm 1: ML-DSA.KeyGen(λ) + /// + /// RNG로 32바이트 시드 ξ를 생성하고, 이를 바탕으로 공개 키와 비밀 키 쌍을 + /// 결정론적으로 유도합니다. + /// + /// # Returns + /// - [`MLDSAPublicKey`]: 파라미터 셋과 인코딩된 공개 키 바이트를 보유 + /// - [`MLDSAPrivateKey`]: 비밀 키를 OS 잠금 메모리([`SecureBuffer`])에 보관 (Drop 시 자동 소거) + /// + /// # Errors + /// - `MLDSAError::RngError`: RNG에서 시드를 얻지 못한 경우 + /// - `MLDSAError::InternalError`: 내부 연산 실패 + pub fn key_gen( + param: MLDSAParameter, + rng: &mut R, + ) -> Result<(MLDSAPublicKey, MLDSAPrivateKey), MLDSAError> { + // 32바이트 시드 ξ를 RNG에서 생성 + let mut xi = [0u8; 32]; + rng.fill_random(&mut xi)?; + + let (pk_bytes, sk_buf) = Self::key_gen_internal(param, &xi)?; + Ok(( + MLDSAPublicKey { + param, + bytes: pk_bytes, + }, + MLDSAPrivateKey { param, sk_buf }, + )) + } + + /// Algorithm 2: ML-DSA.Sign(dk, M, ctx) + /// + /// 비밀 키 `sk`와 메시지 `message`를 이용하여 디지털 서명을 생성합니다. + /// 서명은 RNG에서 얻은 32바이트 rnd 값으로 헤지드(hedged) 처리됩니다. + /// 파라미터 셋은 `sk`에 내장되어 있으므로 별도로 지정하지 않습니다. + /// + /// # Arguments + /// - `sk`: [`key_gen`](Self::key_gen)이 반환한 [`MLDSAPrivateKey`] + /// - `message`: 서명할 메시지 바이트 슬라이스 (크기 제한 없음) + /// - `ctx`: 응용 컨텍스트 문자열 (`ctx.len() ≤ 255`, FIPS 204 Section 5.2) + /// - `rng`: 헤지드 서명에 사용할 RNG + /// + /// # Returns + /// 직렬화된 서명을 OS 잠금 메모리([`SecureBuffer`])에 담아 반환합니다. + /// 길이 = `sk.param().sig_len()` + pub fn sign( + sk: &MLDSAPrivateKey, + message: &[u8], + ctx: &[u8], + rng: &mut R, + ) -> Result { + // ctx 길이 검증: FIPS 204 Section 5.2, 255바이트 이하 + if ctx.len() > 255 { + return Err(MLDSAError::ContextTooLong); + } + + // 32바이트 rnd를 RNG에서 생성 (헤지드 서명) + let mut rnd = [0u8; 32]; + rng.fill_random(&mut rnd)?; + + // M' = 0x00 || IntegerToBytes(|ctx|, 1) || ctx || M + let m_prime = build_m_prime(0x00, ctx, message); + + Self::sign_internal(sk.param, &sk.sk_buf, &m_prime, &rnd) + } + + /// Algorithm 3: ML-DSA.Verify(ek, M, σ, ctx) + /// + /// 공개 키 `pk`를 이용하여 서명 `sig`가 `message`에 대한 유효한 + /// ML-DSA 서명인지 검증합니다. + /// 파라미터 셋은 `pk`에 내장되어 있으므로 별도로 지정하지 않습니다. + /// + /// # Arguments + /// - `pk`: [`key_gen`](Self::key_gen)이 반환한 [`MLDSAPublicKey`] + /// - `message`: 원본 메시지 바이트 슬라이스 + /// - `sig`: 검증할 서명 바이트 슬라이스 + /// - `ctx`: 서명 시 사용한 컨텍스트 문자열 (동일해야 함) + /// + /// # Returns + /// - `Ok(true)`: 서명 유효 + /// - `Ok(false)`: 서명 무효 (상수-시간 비교) + /// - `Err(MLDSAError::ContextTooLong)`: ctx가 255바이트 초과 + pub fn verify( + pk: &MLDSAPublicKey, + message: &[u8], + sig: &[u8], + ctx: &[u8], + ) -> Result { + if ctx.len() > 255 { + return Err(MLDSAError::ContextTooLong); + } + + // 서명 길이 사전 검증 (빠른 거부) + if sig.len() != pk.param.sig_len() { + return Ok(false); + } + + // M' = 0x00 || IntegerToBytes(|ctx|, 1) || ctx || M + let m_prime = build_m_prime(0x00, ctx, message); + + Self::verify_internal(pk.param, &pk.bytes, &m_prime, sig) + } + + // + // 내부 인터페이스 (FIPS 204 Algorithms 4–7) + // + + /// Algorithm 4: ML-DSA.KeyGen_internal(ξ) + /// + /// 32바이트 시드 ξ로부터 공개 키와 비밀 키 쌍을 결정론적으로 생성합니다. + /// 주어진 ξ에 대해 항상 동일한 키 쌍을 반환합니다 (KAT 테스트에 사용). + /// + /// # Security Note + /// ξ는 암호학적으로 안전한 RNG로 생성해야 합니다. + /// 예측 가능하거나 재사용된 ξ는 심각한 보안 취약점을 야기합니다. + pub(crate) fn key_gen_internal( + param: MLDSAParameter, + xi: &[u8; 32], + ) -> Result<(Vec, SecureBuffer), MLDSAError> { + match param { + MLDSAParameter::MLDSA44 => { + keygen_encode::(xi) + } + MLDSAParameter::MLDSA65 => { + keygen_encode::(xi) + } + MLDSAParameter::MLDSA87 => { + keygen_encode::(xi) + } + } + } + + /// Algorithm 5: ML-DSA.Sign_internal(dk, M', rnd) + /// + /// 결정론적 서명 내부 알고리즘. `rnd`가 [0u8; 32]이면 순수 결정론적 서명, + /// 그 외에는 헤지드(hedged) 서명입니다. + /// + /// 거절 샘플링 기반 서명 루프(ExpandMask, Decompose, MakeHint, SigEncode)를 + /// 파라미터 셋별로 단형화하여 호출합니다. + pub(crate) fn sign_internal( + param: MLDSAParameter, + sk_buf: &SecureBuffer, + m_prime: &[u8], + rnd: &[u8; 32], + ) -> Result { + match param { + MLDSAParameter::MLDSA44 => sign_internal_impl::< + K_44, + L_44, + ETA_44, + GAMMA1_44, + GAMMA2_44, + BETA_44, + OMEGA_44, + LAMBDA_44, + TAU_44, + MLDSA44_SK_LEN, + MLDSA44_SIG_LEN, + >(sk_buf, m_prime, rnd), + MLDSAParameter::MLDSA65 => sign_internal_impl::< + K_65, + L_65, + ETA_65, + GAMMA1_65, + GAMMA2_65, + BETA_65, + OMEGA_65, + LAMBDA_65, + TAU_65, + MLDSA65_SK_LEN, + MLDSA65_SIG_LEN, + >(sk_buf, m_prime, rnd), + MLDSAParameter::MLDSA87 => sign_internal_impl::< + K_87, + L_87, + ETA_87, + GAMMA1_87, + GAMMA2_87, + BETA_87, + OMEGA_87, + LAMBDA_87, + TAU_87, + MLDSA87_SK_LEN, + MLDSA87_SIG_LEN, + >(sk_buf, m_prime, rnd), + } + } + + /// Algorithm 7: ML-DSA.Verify_internal(ek, M', σ) + /// + /// 결정론적 검증 내부 알고리즘. + /// w1' 재구성 및 챌린지 해시 비교를 파라미터 셋별로 단형화하여 호출합니다. + pub(crate) fn verify_internal( + param: MLDSAParameter, + pk_bytes: &[u8], + m_prime: &[u8], + sig: &[u8], + ) -> Result { + match param { + MLDSAParameter::MLDSA44 => verify_internal_impl::< + K_44, + L_44, + GAMMA1_44, + GAMMA2_44, + BETA_44, + OMEGA_44, + LAMBDA_44, + TAU_44, + MLDSA44_PK_LEN, + MLDSA44_SIG_LEN, + >(pk_bytes, m_prime, sig), + MLDSAParameter::MLDSA65 => verify_internal_impl::< + K_65, + L_65, + GAMMA1_65, + GAMMA2_65, + BETA_65, + OMEGA_65, + LAMBDA_65, + TAU_65, + MLDSA65_PK_LEN, + MLDSA65_SIG_LEN, + >(pk_bytes, m_prime, sig), + MLDSAParameter::MLDSA87 => verify_internal_impl::< + K_87, + L_87, + GAMMA1_87, + GAMMA2_87, + BETA_87, + OMEGA_87, + LAMBDA_87, + TAU_87, + MLDSA87_PK_LEN, + MLDSA87_SIG_LEN, + >(pk_bytes, m_prime, sig), + } + } +} + +// +// 내부 유틸리티 +// + +/// M' 구성: `domain_sep || IntegerToBytes(|ctx|, 1) || ctx || M` +/// +/// FIPS 204 Section 5.2에 따른 외부 인터페이스 메시지 전처리. +/// - ML-DSA.Sign/Verify: domain_sep = 0x00 +/// - HashML-DSA.Sign/Verify: domain_sep = 0x01 +fn build_m_prime(domain_sep: u8, ctx: &[u8], message: &[u8]) -> Vec { + let mut m_prime = Vec::with_capacity(2 + ctx.len() + message.len()); + m_prime.push(domain_sep); + m_prime.push(ctx.len() as u8); // |ctx| ≤ 255이므로 u8 안전 + m_prime.extend_from_slice(ctx); + m_prime.extend_from_slice(message); + m_prime +} + +/// 키 생성 + 인코딩 헬퍼 (파라미터 셋별 단형화) +/// +/// `keygen_internal`을 호출하고 pk를 바이트로, sk를 SecureBuffer로 직렬화합니다. +fn keygen_encode< + const K: usize, + const L: usize, + const ETA: i32, + const PK_LEN: usize, + const SK_LEN: usize, +>( + xi: &[u8; 32], +) -> Result<(Vec, SecureBuffer), MLDSAError> { + let (pk, sk) = keygen_internal::(xi)?; + + // pkEncode: ρ || SimpleBitPack(t1, 10) — PK_LEN 바이트 + let pk_bytes = as MLDSAPublicKeyTrait>::pk_encode(&pk); + + // skEncode: SecureBuffer (OS 잠금 메모리) + let sk_buf = as MLDSAPrivateKeyTrait>::sk_encode(&sk)?; + + Ok((pk_bytes.to_vec(), sk_buf)) +} + +/// `DrbgError`를 `MLDSAError::RngError`로 변환 +#[inline(always)] +fn drbg_err(e: DrbgError) -> MLDSAError { + match e { + DrbgError::ReseedRequired => { + MLDSAError::RngError("RNG reseed 필요: reseed() 호출 후 재시도") + } + DrbgError::EntropyTooShort => MLDSAError::RngError("엔트로피 길이 부족"), + DrbgError::NonceTooShort => MLDSAError::RngError("Nonce 길이 부족"), + DrbgError::RequestTooLarge => MLDSAError::RngError("요청 크기 초과"), + DrbgError::AllocationFailed => MLDSAError::RngError("RNG 메모리 할당 실패"), + _ => MLDSAError::RngError("RNG 내부 오류"), + } +} diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs new file mode 100644 index 0000000..664cc74 --- /dev/null +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -0,0 +1,364 @@ +use crate::error::MLDSAError; +use crate::error::MLDSAError::InvalidLength; +use crate::field::Fq; +use crate::ntt::N; +use crate::pack::{ + poly_simple_bit_pack_t1, poly_simple_bit_unpack_t1, polyvec_bit_pack_eta, polyvec_bit_pack_t0, + polyvec_bit_unpack_eta, polyvec_bit_unpack_t0, +}; +use crate::poly::PolyVec; +use crate::sample::{expand_a, expand_s}; +use crate::{Q, SEED_LEN}; +use entlib_native_constant_time::traits::{ConstantTimeIsNegative, ConstantTimeSelect}; +use entlib_native_secure_buffer::SecureBuffer; +use entlib_native_sha3::api::SHAKE256; + +// +// 트레이트 정의 +// + +pub trait MLDSAPublicKeyTrait { + /// Algorithm 22: pkEncode(ρ, t1) + /// + /// 공개 키를 바이트 문자열로 인코딩합니다. + /// 입력: ρ ∈ 𝔹^32, t1 ∈ R_q^k (계수 ∈ [0, 2^(bitlen(q-1)-d) - 1]) + /// 출력: pk ∈ 𝔹^(32 + 32k·(bitlen(q-1)-d)) + fn pk_encode(&self) -> [u8; PK_LEN]; + + /// Algorithm 23: pkDecode(pk) + /// + /// pkEncode의 역연산. + fn pk_decode(pk: &[u8; PK_LEN]) -> Self; +} + +pub trait MLDSAPrivateKeyTrait { + /// Algorithm 24: skEncode(ρ, K, tr, s1, s2, t0) + /// + /// 비밀 키를 SecureBuffer에 직렬화합니다. 민감 데이터는 OS 레벨로 잠긴 + /// 물리 메모리에 저장되며, Drop 시 자동 소거됩니다. + fn sk_encode(&self) -> Result; + + /// Algorithm 25: skDecode(sk) + /// + /// skEncode의 역연산. 길이 검증 후 필드를 복원합니다. + fn sk_decode(sk: &SecureBuffer) -> Result + where + Self: Sized; +} + +// +// 키 구조체 +// + +/// ML-DSA 공개 키 구조체 +pub struct MLDSAPublicKey { + pub(crate) rho: [u8; SEED_LEN], + pub(crate) t1: PolyVec, +} + +/// ML-DSA 비밀 키 구조체 +/// +/// `ETA`는 s1, s2의 계수 범위 [-η, η]를 결정하는 파라미터로, +/// sk_encode/sk_decode 시 올바른 비트 너비를 계산하는 데 사용됩니다. +/// 비밀 키는 SecureBuffer를 통해 외부에 직렬화하며, 구조체 자체는 +/// 스택에 임시로만 존재합니다. +pub struct MLDSAPrivateKey { + pub(crate) rho: [u8; 32], + pub(crate) k_seed: [u8; 32], + pub(crate) tr: [u8; 64], + pub(crate) s1: PolyVec, + pub(crate) s2: PolyVec, + pub(crate) t0: PolyVec, +} + +// +// 내부 유틸리티 +// + +/// bitlen(2η): s1, s2 인코딩에 사용하는 계수당 비트 수를 반환합니다. +/// +/// - η=2 → bitlen(4) = 3 +/// - η=4 → bitlen(8) = 4 +#[inline(always)] +fn eta_bit_width(eta: i32) -> usize { + (u32::BITS - (2 * eta as u32).leading_zeros()) as usize +} + +/// Power2Round (Algorithm 35) +/// +/// 다항식 벡터 t의 각 계수를 상위 10비트(t1)와 하위 13비트(t0)로 분할합니다. +/// - t1 = ⌈t / 2^d⌉, t0 = t - t1 * 2^d +/// - t0 ∈ [-2^(d-1)+1, 2^(d-1)], d = 13 +fn power2round_vec(t: &PolyVec) -> (PolyVec, PolyVec) { + let mut t1 = PolyVec::::new_zero(); + let mut t0 = PolyVec::::new_zero(); + + for i in 0..K { + for j in 0..N { + let a = t.vec[i].coeffs[j].0; + + // a1 = ⌈a / 2^13⌉ = (a + 2^12 - 1) >> 13 (상수-시간 올림 나눗셈) + let a1 = (a + 4095) >> 13; + let a0 = a - (a1 << 13); // a0 ∈ [-4095, 4096] + + // 음수 a0를 Fq 표현으로 상수-시간 변환 (부채널 방지) + let is_neg = a0.ct_is_negative(); + let a0_fq = i32::ct_select(&(a0 + Q), &a0, is_neg); + + t1.vec[i].coeffs[j] = Fq::new(a1); + t0.vec[i].coeffs[j] = Fq::new(a0_fq); + } + } + + (t1, t0) +} + +// +// Algorithm 6: ML-DSA.KeyGen_internal(ξ) +// + +/// Algorithm 6: ML-DSA.KeyGen_internal(ξ) +/// +/// 32바이트 시드 ξ로부터 공개키와 비밀키 쌍을 결정론적으로 생성합니다. +pub(crate) fn keygen_internal( + xi: &[u8; 32], +) -> Result<(MLDSAPublicKey, MLDSAPrivateKey), MLDSAError> { + // 1: (ρ, ρ', K) ← H(ξ || IntegerToBytes(k, 1) || IntegerToBytes(l, 1), 128) + let mut seed_input = [0u8; 34]; + seed_input[..32].copy_from_slice(xi); + seed_input[32] = K as u8; + seed_input[33] = L as u8; + + let mut shake = SHAKE256::new(); + shake.update(&seed_input); + // let rho sfkjwenfoinf + let expanded = shake.finalize(128)?; + let ex_slice = expanded.as_slice(); + + let mut rho = [0u8; 32]; + let mut rho_prime = [0u8; 64]; + let mut k_seed = [0u8; 32]; + rho.copy_from_slice(&ex_slice[0..32]); + rho_prime.copy_from_slice(&ex_slice[32..96]); + k_seed.copy_from_slice(&ex_slice[96..128]); + + // 3: A_hat ← ExpandA(ρ) + let a_hat = expand_a::(&rho)?; + + // 4: (s1, s2) ← ExpandS(ρ') + let (mut s1, s2) = expand_s::(&rho_prime)?; + + // 5: t ← INTT(A_hat ∘ NTT(s1)) + s2 + let s1_original = s1; + s1.ntt(); + let mut t = a_hat.multiply_vector(&s1); + t.intt(); + t = t.add(&s2); + + // 6: (t1, t0) ← Power2Round(t) + let (t1, t0) = power2round_vec(&t); + + // 8: pk_bytes ← pkEncode(ρ, t1) + // 9: tr ← H(pk_bytes, 64) + // + // FIPS 204에 따라 pkEncode 출력(ρ || SimpleBitPack(t1, 10))을 SHAKE256으로 해싱합니다. + // pkEncode는 rho(32B) || 각 t1 다항식(320B씩 K개) 순서로 구성됩니다. + // PK_LEN이 keygen_internal의 제네릭이 아니므로 인크리멘탈 해싱으로 처리합니다. + let mut shake_tr = SHAKE256::new(); + shake_tr.update(&rho); + for i in 0..K { + let mut t1_poly_bytes = [0u8; 320]; // 32 * 10 = 320 + poly_simple_bit_pack_t1(&t1.vec[i], &mut t1_poly_bytes); + shake_tr.update(&t1_poly_bytes); + } + let tr_buf = shake_tr.finalize(64)?; + let mut tr = [0u8; 64]; + tr.copy_from_slice(tr_buf.as_slice()); + + let pk = MLDSAPublicKey { rho, t1 }; + let sk = MLDSAPrivateKey { + rho, + k_seed, + tr, + s1: s1_original, + s2, + t0, + }; + + Ok((pk, sk)) +} + +// +// Algorithm 22/23: pkEncode / pkDecode +// + +impl MLDSAPublicKeyTrait for MLDSAPublicKey { + /// Algorithm 22: pkEncode(ρ, t1) + /// + /// pk = ρ (32B) || SimpleBitPack(t1[0], 10) || ... || SimpleBitPack(t1[K-1], 10) + /// t1 계수당 10비트, 다항식당 320바이트, 총 PK_LEN = 32 + 320K 바이트. + fn pk_encode(&self) -> [u8; PK_LEN] { + assert_eq!( + PK_LEN, + 32 + 320 * K, + "pkEncode: PK_LEN이 파라미터 셋과 일치하지 않습니다" + ); // todo: 어썰션은 중요한데 이렇게 잡아주는게 좋을지... 다른 인/디코딩 함수도 동일 + + let mut pk = [0u8; PK_LEN]; + + // 1. ρ (32바이트) + pk[..32].copy_from_slice(&self.rho); + + // 2. SimpleBitPack(t1[i], 10) (다항식당 320바이트) + for i in 0..K { + poly_simple_bit_pack_t1(&self.t1.vec[i], &mut pk[32 + i * 320..32 + (i + 1) * 320]); + } + + pk + } + + /// Algorithm 23: pkDecode(pk) + /// + /// pkEncode의 역연산. ρ와 t1을 복원합니다. + fn pk_decode(pk: &[u8; PK_LEN]) -> Self { + assert_eq!( + PK_LEN, + 32 + 320 * K, + "pkDecode: PK_LEN이 파라미터 셋과 일치하지 않습니다" + ); + + let mut rho = [0u8; SEED_LEN]; + rho.copy_from_slice(&pk[..32]); + + let mut t1 = PolyVec::::new_zero(); + for i in 0..K { + t1.vec[i] = poly_simple_bit_unpack_t1(&pk[32 + i * 320..32 + (i + 1) * 320]); + } + + Self { rho, t1 } + } +} + +// +// Algorithm 24/25: skEncode / skDecode +// + +impl + MLDSAPrivateKeyTrait for MLDSAPrivateKey +{ + /// Algorithm 24: skEncode(ρ, K, tr, s1, s2, t0) + /// + /// 비밀 키를 OS 잠금 메모리(SecureBuffer)에 직렬화합니다. + /// + /// 바이트 레이아웃: + /// ```text + /// ρ (32B) || K_seed (32B) || tr (64B) + /// || BitPack(s1[0..L-1], η, η) (L × 32 × bitlen(2η) 바이트) + /// || BitPack(s2[0..K-1], η, η) (K × 32 × bitlen(2η) 바이트) + /// || BitPack(t0[0..K-1], 4095, 4096) (K × 416 바이트) + /// ``` + fn sk_encode(&self) -> Result { + // ETA 비트 너비 및 각 섹션 크기 계산 + let eta_bw = eta_bit_width(ETA); + let s1_len = L * 32 * eta_bw; + let s2_len = K * 32 * eta_bw; + let t0_len = K * 32 * 13; + let expected_len = 32 + 32 + 64 + s1_len + s2_len + t0_len; + + debug_assert_eq!( + SK_LEN, expected_len, + "skEncode: SK_LEN이 파라미터 셋과 일치하지 않습니다" + ); + + // OS 레벨 메모리 잠금 + Drop 시 자동 소거 + let mut buf = SecureBuffer::new_owned(SK_LEN)?; + let sk_bytes = buf.as_mut_slice(); + + let mut off = 0; + + // 1. ρ (32바이트) + sk_bytes[off..off + 32].copy_from_slice(&self.rho); + off += 32; + + // 2. K_seed (32바이트) + sk_bytes[off..off + 32].copy_from_slice(&self.k_seed); + off += 32; + + // 3. tr (64바이트) + sk_bytes[off..off + 64].copy_from_slice(&self.tr); + off += 64; + + // 4. s1: BitPack(s1[i], η, η) — 계수 ∈ [-η, η] + polyvec_bit_pack_eta::(&self.s1, ETA, &mut sk_bytes[off..off + s1_len]); + off += s1_len; + + // 5. s2: BitPack(s2[i], η, η) — 계수 ∈ [-η, η] + polyvec_bit_pack_eta::(&self.s2, ETA, &mut sk_bytes[off..off + s2_len]); + off += s2_len; + + // 6. t0: BitPack(t0[i], 4095, 4096) — 계수 ∈ [-4095, 4096] + polyvec_bit_pack_t0::(&self.t0, &mut sk_bytes[off..off + t0_len]); + + Ok(buf) + } + + /// Algorithm 25: skDecode(sk) + /// + /// skEncode의 역연산. SecureBuffer에서 ρ, K_seed, tr, s1, s2, t0를 복원합니다. + fn sk_decode(sk: &SecureBuffer) -> Result { + let eta_bw = eta_bit_width(ETA); + let s1_len = L * 32 * eta_bw; + let s2_len = K * 32 * eta_bw; + let t0_len = K * 32 * 13; + let expected_len = 32 + 32 + 64 + s1_len + s2_len + t0_len; + + if sk.len() != expected_len { + return Err(InvalidLength("skDecode: 잘못된 비밀 키 길이")); + } + + // SK_LEN 상수와 런타임 길이 일치 검증 (파라미터 셋 오용 방지) + if SK_LEN != expected_len { + return Err(InvalidLength( + "skDecode: SK_LEN이 파라미터 셋과 일치하지 않습니다", + )); + } + + let b = sk.as_slice(); + let mut off = 0; + + // 1. ρ (32바이트) + let mut rho = [0u8; 32]; + rho.copy_from_slice(&b[off..off + 32]); + off += 32; + + // 2. K_seed (32바이트) + let mut k_seed = [0u8; 32]; + k_seed.copy_from_slice(&b[off..off + 32]); + off += 32; + + // 3. tr (64바이트) + let mut tr = [0u8; 64]; + tr.copy_from_slice(&b[off..off + 64]); + off += 64; + + // 4. s1: BitUnpack(s1[i], η, η) + let s1: PolyVec = polyvec_bit_unpack_eta(&b[off..off + s1_len], ETA); + off += s1_len; + + // 5. s2: BitUnpack(s2[i], η, η) + let s2: PolyVec = polyvec_bit_unpack_eta(&b[off..off + s2_len], ETA); + off += s2_len; + + // 6. t0: BitUnpack(t0[i], 4095, 4096) + let t0: PolyVec = polyvec_bit_unpack_t0(&b[off..off + t0_len]); + + Ok(Self { + rho, + k_seed, + tr, + s1, + s2, + t0, + }) + } +} diff --git a/crypto/mldsa/src/mldsa_sign.rs b/crypto/mldsa/src/mldsa_sign.rs new file mode 100644 index 0000000..4133132 --- /dev/null +++ b/crypto/mldsa/src/mldsa_sign.rs @@ -0,0 +1,701 @@ +//! FIPS 204 ML-DSA 서명 및 검증 내부 알고리즘 (Algorithms 5, 7) +//! +//! - Algorithm 5: ML-DSA.Sign_internal(sk, M', rnd) +//! - Algorithm 7: ML-DSA.Verify_internal(pk, M', σ) +//! +//! 지원 알고리즘: +//! - Algorithm 29: SampleInBall(ρ, τ) +//! - Algorithm 35: Decompose(r, α) → HighBits, LowBits +//! - Algorithm 36: ExpandMask(ρ'', κ) +//! - Algorithm 37: MakeHint(z, r, α) +//! - Algorithm 38: UseHint(h, r, α) + +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; + +use crate::error::MLDSAError; +use crate::field::Fq; +use crate::mldsa_keys::{ + MLDSAPrivateKey, MLDSAPrivateKeyTrait, MLDSAPublicKey, MLDSAPublicKeyTrait, +}; +use crate::ntt::N; +use crate::pack::{ + bit_unpack, hint_bit_pack, hint_bit_unpack, poly_simple_bit_pack_t1, polyvec_bit_pack_z, + polyvec_bit_unpack_z, polyvec_simple_bit_pack_w1, +}; +use crate::poly::{Poly, PolyVec}; +use crate::sample::expand_a; +use crate::{D, Q}; +use entlib_native_secure_buffer::SecureBuffer; +use entlib_native_sha3::api::SHAKE256; + +// +// 내부 유틸리티 +// + +/// FIPS 204 bitlen(x) = ⌊log2(x)⌋ + 1 (x > 0), 0 (x = 0) +#[inline(always)] +fn bitlen(n: u32) -> usize { + (u32::BITS - n.leading_zeros()) as usize +} + +/// Fq 계수를 중심 부호 있는 정수로 변환 (타이밍-가변 — 공개 데이터 전용) +/// +/// 거절 샘플링의 노름 검사처럼 결과가 공개(서명 재시도 여부)인 곳에서만 사용합니다. +#[inline(always)] +fn fq_to_signed(v: i32) -> i32 { + if v > Q / 2 { v - Q } else { v } +} + +/// 다항식 벡터의 무한 노름 (계수별 최대 절댓값) +fn inf_norm_vec(v: &PolyVec) -> i32 { + let mut max = 0i32; + for i in 0..D { + for j in 0..N { + let s = fq_to_signed(v.vec[i].coeffs[j].0).abs(); + if s > max { + max = s; + } + } + } + max +} + +/// PolyVec 뺄셈: a − b +fn polyvec_sub(a: &PolyVec, b: &PolyVec) -> PolyVec { + let mut r = PolyVec::::new_zero(); + for i in 0..D { + r.vec[i] = a.vec[i].sub(&b.vec[i]); + } + r +} + +/// PolyVec 부정: −v (각 계수를 Q − v[i][j] 로 계산) +fn polyvec_neg(v: &PolyVec) -> PolyVec { + let mut r = PolyVec::::new_zero(); + for i in 0..D { + for j in 0..N { + let c = v.vec[i].coeffs[j].0; + r.vec[i].coeffs[j] = Fq::new(if c == 0 { 0 } else { Q - c }); + } + } + r +} + +/// 단항식 c를 다항식 벡터의 각 원소와 NTT 점별 곱셈: c_hat ∘ v_hat +fn poly_mul_polyvec(c: &Poly, v: &PolyVec) -> PolyVec { + let mut r = PolyVec::::new_zero(); + for i in 0..D { + r.vec[i] = c.pointwise_montgomery(&v.vec[i]); + } + r +} + +/// t1 * 2^D 스케일링 (NTT 도메인 진입 전 수행) +/// +/// t1 계수 ∈ [0, 1023], 2^13 배 후 ∈ [0, 8380416] = [0, Q−1]. +fn polyvec_scale_2d(t1: &PolyVec) -> PolyVec { + let mut r = PolyVec::::new_zero(); + for i in 0..K { + for j in 0..N { + let v = t1.vec[i].coeffs[j].0 as i64; + r.vec[i].coeffs[j] = Fq::new(((v << D) % Q as i64) as i32); + } + } + r +} + +/// 상수-시간 바이트 슬라이스 동치 비교 +fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff: u8 = 0; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +// +// Algorithm 35: Decompose(r, α) → (r1, r0) +// + +/// Algorithm 35: Decompose(r, 2γ2) +/// +/// r ∈ Z_q 를 r = r1 * 2γ2 + r0 로 분해합니다. +/// - r1 = HighBits(r) ∈ [0, (q−1)/(2γ2) − 1] +/// - r0 = LowBits(r) ∈ [−(γ2−1), γ2] (부호 있는 정수로 반환) +/// - 특수: r+ − r0 = q − 1 → r1 = 0, r0 = r0 − 1 +/// +/// 입력 r은 Fq 표현 [0, Q−1]. +fn decompose(r: i32, gamma2: i32) -> (i32, i32) { + let alpha = 2 * gamma2; + let r_plus = r % Q; // 이미 [0, Q-1] 이므로 동일 + + let mut r0 = r_plus % alpha; + if r0 > gamma2 { + r0 -= alpha; + } + + if r_plus - r0 == Q - 1 { + (0, r0 - 1) + } else { + ((r_plus - r0) / alpha, r0) + } +} + +/// HighBits(w, 2γ2) — 다항식 벡터 버전 +/// +/// Fq 계수는 [0, Q-1] 표현이므로 decompose를 직접 적용합니다. +/// 반환값 r1 ∈ [0, (q−1)/(2γ2) − 1]는 Fq 표현 (= 비음수이므로 그대로 사용). +fn high_bits_vec(w: &PolyVec, gamma2: i32) -> PolyVec { + let mut r = PolyVec::::new_zero(); + for i in 0..D { + for j in 0..N { + let (r1, _) = decompose(w.vec[i].coeffs[j].0, gamma2); + r.vec[i].coeffs[j] = Fq::new(r1); + } + } + r +} + +/// LowBits(w, 2γ2) — 다항식 벡터 버전 +/// +/// 반환값 r0 ∈ [−(γ2−1), γ2]를 Fq 표현으로 반환 (음수는 + Q). +fn low_bits_vec(w: &PolyVec, gamma2: i32) -> PolyVec { + let mut r = PolyVec::::new_zero(); + for i in 0..D { + for j in 0..N { + let (_, r0) = decompose(w.vec[i].coeffs[j].0, gamma2); + let fq = if r0 < 0 { r0 + Q } else { r0 }; + r.vec[i].coeffs[j] = Fq::new(fq); + } + } + r +} + +// +// Algorithm 37: MakeHint(z, r, α) +// Algorithm 38: UseHint(h, r, α) +// + +/// Algorithm 37: MakeHint(−ct0, w − cs2 + ct0, 2γ2) — 계수 단위 +/// +/// h = 1 if HighBits(r + z, α) ≠ HighBits(r, α), else 0 +#[inline(always)] +fn make_hint_coeff(z_fq: i32, r_fq: i32, gamma2: i32) -> i32 { + let (r1, _) = decompose(r_fq, gamma2); + // (r + z) mod q + let rz = ((r_fq as i64 + z_fq as i64).rem_euclid(Q as i64)) as i32; + let (v1, _) = decompose(rz, gamma2); + if r1 != v1 { 1 } else { 0 } +} + +/// MakeHint 다항식 벡터 버전 — (h, count) 반환 +fn make_hint_vec( + z: &PolyVec, + r: &PolyVec, + gamma2: i32, +) -> (PolyVec, usize) { + let mut h = PolyVec::::new_zero(); + let mut count = 0usize; + for i in 0..K { + for j in 0..N { + let bit = make_hint_coeff(z.vec[i].coeffs[j].0, r.vec[i].coeffs[j].0, gamma2); + h.vec[i].coeffs[j] = Fq::new(bit); + count += bit as usize; + } + } + (h, count) +} + +/// Algorithm 38: UseHint(h, r, 2γ2) — 계수 단위 +#[inline(always)] +fn use_hint_coeff(h: i32, r_fq: i32, gamma2: i32) -> i32 { + let m = (Q - 1) / (2 * gamma2); // 최대 HighBits 값 + 1 + let (r1, r0) = decompose(r_fq, gamma2); + if h == 1 { + if r0 > 0 { + (r1 + 1).rem_euclid(m) + } else { + (r1 - 1).rem_euclid(m) + } + } else { + r1 + } +} + +/// UseHint 다항식 벡터 버전 +fn use_hint_vec(h: &PolyVec, r: &PolyVec, gamma2: i32) -> PolyVec { + let mut w1 = PolyVec::::new_zero(); + for i in 0..K { + for j in 0..N { + let bit = h.vec[i].coeffs[j].0; + let v = use_hint_coeff(bit, r.vec[i].coeffs[j].0, gamma2); + w1.vec[i].coeffs[j] = Fq::new(v); + } + } + w1 +} + +// +// Algorithm 29: SampleInBall(ρ, τ) +// + +/// Algorithm 29: SampleInBall(ρ, τ) +/// +/// c~ (c_tilde) 로부터 SHAKE256 XOF를 통해 정확히 τ 개의 ±1 계수를 가진 +/// 다항식 c를 샘플링합니다. +/// +/// 알고리즘: +/// 1. 첫 8바이트를 부호 비트(signs, 64-bit little-endian)로 사용 +/// 2. 이후 τ번: j ≤ i가 될 때까지 1바이트씩 샘플링 → swap c[i] ↔ c[j] → c[j] = ±1 +fn sample_in_ball(c_tilde: &[u8], tau: usize) -> Result { + // 넉넉한 버퍼를 한 번에 추출: 8 (부호) + 256 * 3 (거절 여유) + let buf_len = 8 + 256 + tau * 8; + + let mut shake = SHAKE256::new(); + shake.update(c_tilde); + let buf = shake.finalize(buf_len)?; + let data = buf.as_slice(); + + // 부호 비트 (64-bit little-endian) + let mut signs: u64 = 0; + for (k, &byte) in data.iter().enumerate().take(8) { + signs |= (byte as u64) << (8 * k); + } + + let mut c = Poly::new_zero(); + let mut idx = 8usize; // XOF 스트림 소비 위치 + + for i in (N - tau)..N { + // j ≤ i가 될 때까지 바이트 소비 + let j = loop { + if idx >= data.len() { + return Err(MLDSAError::InternalError( + "SampleInBall: SHAKE256 출력 소진 (극히 드문 경우)", + )); + } + let candidate = data[idx] as usize; + idx += 1; + if candidate <= i { + break candidate; + } + }; + + // c[i] = c[j], c[j] = (−1)^{signs bit} + c.coeffs[i] = c.coeffs[j]; + let sign_bit = (signs & 1) as i32; // 0 또는 1 + // 1 − 2*sign_bit: sign=0 → +1, sign=1 → −1 (Fq에서 음수는 Q−1) + c.coeffs[j] = if sign_bit == 0 { + Fq::new(1) + } else { + Fq::new(Q - 1) // −1 mod Q + }; + signs >>= 1; + } + + Ok(c) +} + +// +// Algorithm 36: ExpandMask(ρ'', κ) +// + +/// Algorithm 36: ExpandMask(ρ'', κ, γ1) → PolyVec +/// +/// 64바이트 시드 ρ''와 카운터 κ로부터 L개의 다항식 벡터 y를 생성합니다. +/// y[i] 계수 ∈ [−(γ1−1), γ1]. +fn expand_mask( + rho_pp: &[u8; 64], + kappa: u16, + gamma1: i32, +) -> Result, MLDSAError> { + // FIPS 204 §2.1: 계수 범위 [−(γ1−1), γ1] → 2γ1 개 값 → bitlen(2γ1−1) 비트/계수 + // (γ1이 2의 거듭제곱이면 bitlen(γ1−1)+1 = bitlen(2γ1−1)) + let c = bitlen((2 * gamma1 - 1) as u32); + let bpp = 32 * c; // bytes per polynomial + + let mut y = PolyVec::::new_zero(); + + for i in 0..L { + let nonce = kappa + i as u16; + // 시드 = ρ'' (64B) || IntegerToBytes(nonce, 2) + let mut seed = [0u8; 66]; + seed[..64].copy_from_slice(rho_pp); + seed[64] = (nonce & 0xFF) as u8; + seed[65] = (nonce >> 8) as u8; + + let mut shake = SHAKE256::new(); + shake.update(&seed); + let buf = shake.finalize(bpp)?; + + // BitUnpack(buf, γ1−1, γ1) → 계수 ∈ [−(γ1−1), γ1] + y.vec[i] = bit_unpack(buf.as_slice(), gamma1 - 1, gamma1); + } + + Ok(y) +} + +// +// Algorithm 26/27: SigEncode / SigDecode +// + +/// Algorithm 26: sigEncode(c~, z, h, γ1, ω) → σ +/// +/// 서명 인코딩: +/// ```text +/// σ = c~ (LAMBDA/4 B) || BitPack(z, γ1−1, γ1) (L×bpp B) || HintBitPack(h) (ω+K B) +/// ``` +fn sig_encode( + c_tilde: &[u8], // LAMBDA/4 바이트 + z: &PolyVec, + h: &PolyVec, + gamma1: i32, + omega: usize, +) -> Result<[u8; SIG_LEN], MLDSAError> { + let c_tilde_len = LAMBDA / 4; + let z_bw = bitlen((2 * gamma1 - 1) as u32); + let z_bpp = 32 * z_bw; + let z_total = L * z_bpp; + let h_total = omega + K; + + debug_assert_eq!( + SIG_LEN, + c_tilde_len + z_total + h_total, + "sigEncode: SIG_LEN이 파라미터와 일치하지 않습니다" + ); + + let mut sig = [0u8; SIG_LEN]; + let mut off = 0; + + // 1. c~ + sig[off..off + c_tilde_len].copy_from_slice(c_tilde); + off += c_tilde_len; + + // 2. BitPack(z[i], γ1−1, γ1) + polyvec_bit_pack_z::(z, gamma1, &mut sig[off..off + z_total]); + off += z_total; + + // 3. HintBitPack(h) + hint_bit_pack::(h, omega, &mut sig[off..off + h_total]); + + Ok(sig) +} + +/// Algorithm 27: sigDecode(σ, γ1, ω) → Option<(c_tilde, z, h)> +/// +/// 서명 디코딩. 유효하지 않은 인코딩이면 `None` 반환. +fn sig_decode( + sig: &[u8], + gamma1: i32, + omega: usize, +) -> Option<(Vec, PolyVec, PolyVec)> { + let c_tilde_len = LAMBDA / 4; + let z_bw = bitlen((2 * gamma1 - 1) as u32); + let z_bpp = 32 * z_bw; + let z_total = L * z_bpp; + let h_total = omega + K; + + if sig.len() != c_tilde_len + z_total + h_total { + return None; + } + + let mut off = 0; + + // c~ + let c_tilde = sig[off..off + c_tilde_len].to_vec(); + off += c_tilde_len; + + // z: BitUnpack(bytes, γ1−1, γ1) + let z: PolyVec = polyvec_bit_unpack_z(&sig[off..off + z_total], gamma1); + off += z_total; + + // h: HintBitUnpack 유효성 검증 포함 + let h: PolyVec = hint_bit_unpack(&sig[off..off + h_total], omega)?; + + Some((c_tilde, z, h)) +} + +// +// Algorithm 28: w1Encode(w1) +// + +/// Algorithm 28: w1Encode(w1, γ2) → bytes +/// +/// w1 계수 ∈ [0, (q−1)/(2γ2) − 1]를 비트 팩킹합니다. +fn w1_encode(w1: &PolyVec, gamma2: i32) -> Vec { + let max_coeff = (Q - 1) / (2 * gamma2) - 1; + let bw = bitlen(max_coeff as u32); + let bpp = 32 * bw; + let total = K * bpp; + let mut out = vec![0u8; total]; + polyvec_simple_bit_pack_w1::(w1, gamma2, &mut out); + out +} + +// +// Algorithm 5: ML-DSA.Sign_internal +// + +/// Algorithm 5: ML-DSA.Sign_internal(sk, M', rnd) +/// +/// 거절 샘플링 기반 서명을 생성합니다. `rnd`가 [0u8; 32]이면 +/// 순수 결정론적(deterministic) 서명, 그 외에는 헤지드(hedged) 서명입니다. +/// +/// # 파라미터 +/// - `K`, `L`: 행렬 차원 +/// - `ETA`: 비밀 키 계수 범위 η +/// - `GAMMA1`, `GAMMA2`: Decompose 범위 파라미터 +/// - `BETA`: β = τ · η (거절 임계값) +/// - `OMEGA`: 힌트 최대 1의 개수 +/// - `LAMBDA`: 충돌 강도 (비트), c~ = LAMBDA/4 바이트 +/// - `TAU`: 챌린지 다항식 ±1 계수 수 +/// - `SK_LEN`, `SIG_LEN`: 직렬화 크기 +pub(crate) fn sign_internal_impl< + const K: usize, + const L: usize, + const ETA: i32, + const GAMMA1: i32, + const GAMMA2: i32, + const BETA: i32, + const OMEGA: usize, + const LAMBDA: usize, + const TAU: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + sk_buf: &SecureBuffer, + m_prime: &[u8], + rnd: &[u8; 32], +) -> Result { + // 1: (ρ, K, tr, s1, s2, t0) ← skDecode(sk) + let sk = as MLDSAPrivateKeyTrait>::sk_decode(sk_buf)?; + + // 2: s1_hat, s2_hat, t0_hat ← NTT(s1), NTT(s2), NTT(t0) + let mut s1_hat = sk.s1; + s1_hat.ntt(); + let mut s2_hat = sk.s2; + s2_hat.ntt(); + let mut t0_hat = sk.t0; + t0_hat.ntt(); + + // 3: A_hat ← ExpandA(ρ) + let a_hat = expand_a::(&sk.rho)?; + + // 4: μ ← H(tr || M', 64) + let mut shake_mu = SHAKE256::new(); + shake_mu.update(&sk.tr); + shake_mu.update(m_prime); + let mu_buf = shake_mu.finalize(64)?; + let mut mu = [0u8; 64]; + mu.copy_from_slice(mu_buf.as_slice()); + + // 5: ρ'' ← H(K || rnd || μ, 64) + let mut shake_rho_pp = SHAKE256::new(); + shake_rho_pp.update(&sk.k_seed); + shake_rho_pp.update(rnd); + shake_rho_pp.update(&mu); + let rho_pp_buf = shake_rho_pp.finalize(64)?; + let mut rho_pp = [0u8; 64]; + rho_pp.copy_from_slice(rho_pp_buf.as_slice()); + + // 6: κ ← 0 (카운터; 각 반복마다 L씩 증가) + let mut kappa: u16 = 0; + + // 최대 반복 횟수 (확률적으로 극히 드물게 초과) + const MAX_ITER: usize = 1000; + + for _ in 0..MAX_ITER { + // a: y ← ExpandMask(ρ'', κ) + let y = expand_mask::(&rho_pp, kappa, GAMMA1)?; + + // b: w ← INTT(A_hat ∘ NTT(y)) + let mut y_hat = y; + y_hat.ntt(); + let mut w = a_hat.multiply_vector(&y_hat); + w.intt(); + + // c: w1 ← HighBits(w, 2γ2) + let w1 = high_bits_vec::(&w, GAMMA2); + + // d: c~ ← H(μ || w1Encode(w1), LAMBDA/4 바이트) + let c_tilde_len = LAMBDA / 4; + let w1_bytes = w1_encode::(&w1, GAMMA2); + let mut shake_c = SHAKE256::new(); + shake_c.update(&mu); + shake_c.update(&w1_bytes); + let c_tilde_buf = shake_c.finalize(c_tilde_len)?; + let c_tilde = c_tilde_buf.as_slice(); + + // e/f: c ← SampleInBall(c~, τ), c_hat ← NTT(c) + let mut c_hat_poly = sample_in_ball(c_tilde, TAU)?; + c_hat_poly_ntt(&mut c_hat_poly); + let c_hat = &c_hat_poly; + + // g/h: z ← y + INTT(c_hat ∘ s1_hat) + let mut cs1 = poly_mul_polyvec::(c_hat, &s1_hat); + cs1.intt(); + let z = y.add(&cs1); + + // i/j: r0 ← LowBits(w − INTT(c_hat ∘ s2_hat), 2γ2) + let mut cs2 = poly_mul_polyvec::(c_hat, &s2_hat); + cs2.intt(); + let w_minus_cs2 = polyvec_sub::(&w, &cs2); + let r0 = low_bits_vec::(&w_minus_cs2, GAMMA2); + + // k: 거절 조건 1 — ||z||∞ ≥ γ1 − β 또는 ||r0||∞ ≥ γ2 − β + if inf_norm_vec::(&z) >= GAMMA1 - BETA || inf_norm_vec::(&r0) >= GAMMA2 - BETA { + kappa = kappa.wrapping_add(L as u16); + continue; + } + + // l: ct0 ← INTT(c_hat ∘ t0_hat) + let mut ct0 = poly_mul_polyvec::(c_hat, &t0_hat); + ct0.intt(); + + // m: h ← MakeHint(−ct0, w − cs2 + ct0, 2γ2) + let neg_ct0 = polyvec_neg::(&ct0); + let w_minus_cs2_plus_ct0 = w_minus_cs2.add(&ct0); + let (h, h_count) = make_hint_vec::(&neg_ct0, &w_minus_cs2_plus_ct0, GAMMA2); + + // n: 거절 조건 2 — ||ct0||∞ ≥ γ2 또는 Σh > ω + if inf_norm_vec::(&ct0) >= GAMMA2 || h_count > OMEGA { + kappa = kappa.wrapping_add(L as u16); + continue; + } + + // o: σ ← sigEncode(c~, z, h) + let sig_arr = sig_encode::(c_tilde, &z, &h, GAMMA1, OMEGA)?; + let mut sig_buf = SecureBuffer::new_owned(SIG_LEN)?; + sig_buf.as_mut_slice().copy_from_slice(&sig_arr); + return Ok(sig_buf); + } + + Err(MLDSAError::SigningFailed) +} + +// +// Algorithm 7: ML-DSA.Verify_internal +// + +/// Algorithm 7: ML-DSA.Verify_internal(pk, M', σ) +/// +/// # 검증 절차 +/// 1. pkDecode(pk) → (ρ, t1) +/// 2. sigDecode(σ) → (c~, z, h) — 실패 시 false 반환 +/// 3. A_hat ← ExpandA(ρ) +/// 4. tr ← H(pk, 64); μ ← H(tr || M', 64) +/// 5. c ← SampleInBall(c~, τ); c_hat ← NTT(c) +/// 6. w'_approx ← INTT(A_hat ∘ NTT(z) − c_hat ∘ NTT(t1 · 2^d)) +/// 7. w1' ← UseHint(h, w'_approx, 2γ2) +/// 8. c~' ← H(μ || w1Encode(w1'), LAMBDA/4 B) +/// 9. 검증: ||z||∞ < γ1 − β AND c~ = c~' +pub(crate) fn verify_internal_impl< + const K: usize, + const L: usize, + const GAMMA1: i32, + const GAMMA2: i32, + const BETA: i32, + const OMEGA: usize, + const LAMBDA: usize, + const TAU: usize, + const PK_LEN: usize, + const SIG_LEN: usize, +>( + pk_bytes: &[u8], + m_prime: &[u8], + sig: &[u8], +) -> Result { + // 길이 사전 검사 + if pk_bytes.len() != PK_LEN || sig.len() != SIG_LEN { + return Ok(false); + } + + // 1: (ρ, t1) ← pkDecode(pk) + let pk_arr: &[u8; PK_LEN] = pk_bytes + .try_into() + .map_err(|_| MLDSAError::InvalidLength("verify: pk 길이 변환 실패"))?; + let pk = as MLDSAPublicKeyTrait>::pk_decode(pk_arr); + + // 2: (c~, z, h) ← sigDecode(σ) + let (c_tilde, z, h) = match sig_decode::(sig, GAMMA1, OMEGA) { + Some(v) => v, + None => return Ok(false), + }; + + // 3: A_hat ← ExpandA(ρ) + let a_hat = expand_a::(&pk.rho)?; + + // 4: tr ← H(pk_bytes, 64) + let mut shake_tr = SHAKE256::new(); + shake_tr.update(&pk.rho); + for i in 0..K { + let mut t1_poly_bytes = [0u8; 320]; + poly_simple_bit_pack_t1(&pk.t1.vec[i], &mut t1_poly_bytes); + shake_tr.update(&t1_poly_bytes); + } + let tr_buf = shake_tr.finalize(64)?; + let mut tr = [0u8; 64]; + tr.copy_from_slice(tr_buf.as_slice()); + + // μ ← H(tr || M', 64) + let mut shake_mu = SHAKE256::new(); + shake_mu.update(&tr); + shake_mu.update(m_prime); + let mu_buf = shake_mu.finalize(64)?; + let mut mu = [0u8; 64]; + mu.copy_from_slice(mu_buf.as_slice()); + + // 5: c ← SampleInBall(c~, τ), c_hat ← NTT(c) + let mut c_hat_poly = sample_in_ball(&c_tilde, TAU)?; + c_hat_poly_ntt(&mut c_hat_poly); + let c_hat = &c_hat_poly; + + // 6: w'_approx ← INTT(A_hat ∘ NTT(z) − c_hat ∘ NTT(t1 · 2^d)) + let mut z_hat = z; + z_hat.ntt(); + let az_hat = a_hat.multiply_vector(&z_hat); // A_hat ∘ NTT(z) in NTT domain + + let t1_scaled = polyvec_scale_2d::(&pk.t1); + let mut t1_hat = t1_scaled; + t1_hat.ntt(); + let ct1_hat = poly_mul_polyvec::(c_hat, &t1_hat); // c_hat ∘ NTT(t1·2^d) + + let mut w_approx_hat = polyvec_sub::(&az_hat, &ct1_hat); + w_approx_hat.intt(); // INTT(...) + + // 7: w1' ← UseHint(h, w'_approx, 2γ2) + let w1_prime = use_hint_vec::(&h, &w_approx_hat, GAMMA2); + + // 9a: ||z||∞ < γ1 − β + // z는 sig_decode에서 Fq 표현으로 복원됐으며, z_hat = z 복사 후 z는 유효. + if inf_norm_vec::(&z) >= GAMMA1 - BETA { + return Ok(false); + } + + // 8: c~' ← H(μ || w1Encode(w1'), LAMBDA/4 B) + let c_tilde_len = LAMBDA / 4; + let w1_bytes = w1_encode::(&w1_prime, GAMMA2); + let mut shake_c = SHAKE256::new(); + shake_c.update(&mu); + shake_c.update(&w1_bytes); + let c_tilde_prime_buf = shake_c.finalize(c_tilde_len)?; + + // 9b: 상수-시간 비교 c~ == c~' + Ok(ct_eq_bytes(&c_tilde, c_tilde_prime_buf.as_slice())) +} + +// +// NTT 헬퍼 (단일 다항식) +// + +/// 단일 다항식에 NTT 변환 적용 (제자리 연산) +#[inline(always)] +fn c_hat_poly_ntt(c: &mut Poly) { + use crate::ntt::ntt as ntt_fn; + ntt_fn(&mut c.coeffs); +} diff --git a/crypto/mldsa/src/ntt.rs b/crypto/mldsa/src/ntt.rs new file mode 100644 index 0000000..8d8fd3c --- /dev/null +++ b/crypto/mldsa/src/ntt.rs @@ -0,0 +1,599 @@ +use crate::field::Fq; + +/// FIPS 204 다항식의 차수 n +pub(crate) const N: usize = 256; + +/// N^-1 * R^2 mod q (Montgomery "to-mont" INTT 정규화 상수) +/// +/// `pointwise_montgomery(a, b)` = a*b*R^(-1) 이므로 INTT 입력에 R^(-1) 인수가 남습니다. +/// 이를 보정하기 위해 INTT 마지막 단계에서 N^(-1) 대신 N^(-1)*R^2 를 곱합니다: +/// poly * (N^-1 * R^2) * R^(-1) = poly * N^-1 * R +/// 결과적으로 poly_mul_polyvec/multiply_vector → intt 체인이 +/// 올바른 다항식 곱셈 결과(표준형)를 반환합니다. +/// +/// 계산: N^(-1)*R^2 mod Q = 16382 * 4193792 mod 8380417 = 41978 +const INV_N_MONT: Fq = Fq::new(41978); + +/// 비트 반전(Bit-reversed) 순서로 미리 계산된 몽고메리 도메인의 원시 단위근(Zetas) 배열 +/// ZETAS[i] = (\zeta^{brv(i)} * 2^32) mod q +const ZETAS: [Fq; 256] = [ + Fq(4193792), + Fq(25847), + Fq(5771523), + Fq(7861508), + Fq(237124), + Fq(7602457), + Fq(7504169), + Fq(466468), + Fq(1826347), + Fq(2353451), + Fq(8021166), + Fq(6288512), + Fq(3119733), + Fq(5495562), + Fq(3111497), + Fq(2680103), + Fq(2725464), + Fq(1024112), + Fq(7300517), + Fq(3585928), + Fq(7830929), + Fq(7260833), + Fq(2619752), + Fq(6271868), + Fq(6262231), + Fq(4520680), + Fq(6980856), + Fq(5102745), + Fq(1757237), + Fq(8360995), + Fq(4010497), + Fq(280005), + Fq(2706023), + Fq(95776), + Fq(3077325), + Fq(3530437), + Fq(6718724), + Fq(4788269), + Fq(5842901), + Fq(3915439), + Fq(4519302), + Fq(5336701), + Fq(3574422), + Fq(5512770), + Fq(3539968), + Fq(8079950), + Fq(2348700), + Fq(7841118), + Fq(6681150), + Fq(6736599), + Fq(3505694), + Fq(4558682), + Fq(3507263), + Fq(6239768), + Fq(6779997), + Fq(3699596), + Fq(811944), + Fq(531354), + Fq(954230), + Fq(3881043), + Fq(3900724), + Fq(5823537), + Fq(2071892), + Fq(5582638), + Fq(4450022), + Fq(6851714), + Fq(4702672), + Fq(5339162), + Fq(6927966), + Fq(3475950), + Fq(2176455), + Fq(6795196), + Fq(7122806), + Fq(1939314), + Fq(4296819), + Fq(7380215), + Fq(5190273), + Fq(5223087), + Fq(4747489), + Fq(126922), + Fq(3412210), + Fq(7396998), + Fq(2147896), + Fq(2715295), + Fq(5412772), + Fq(4686924), + Fq(7969390), + Fq(5903370), + Fq(7709315), + Fq(7151892), + Fq(8357436), + Fq(7072248), + Fq(7998430), + Fq(1349076), + Fq(1852771), + Fq(6949987), + Fq(5037034), + Fq(264944), + Fq(508951), + Fq(3097992), + Fq(44288), + Fq(7280319), + Fq(904516), + Fq(3958618), + Fq(4656075), + Fq(8371839), + Fq(1653064), + Fq(5130689), + Fq(2389356), + Fq(8169440), + Fq(759969), + Fq(7063561), + Fq(189548), + Fq(4827145), + Fq(3159746), + Fq(6529015), + Fq(5971092), + Fq(8202977), + Fq(1315589), + Fq(1341330), + Fq(1285669), + Fq(6795489), + Fq(7567685), + Fq(6940675), + Fq(5361315), + Fq(4499357), + Fq(4751448), + Fq(3839961), + Fq(2091667), + Fq(3407706), + Fq(2316500), + Fq(3817976), + Fq(5037939), + Fq(2244091), + Fq(5933984), + Fq(4817955), + Fq(266997), + Fq(2434439), + Fq(7144689), + Fq(3513181), + Fq(4860065), + Fq(4621053), + Fq(7183191), + Fq(5187039), + Fq(900702), + Fq(1859098), + Fq(909542), + Fq(819034), + Fq(495491), + Fq(6767243), + Fq(8337157), + Fq(7857917), + Fq(7725090), + Fq(5257975), + Fq(2031748), + Fq(3207046), + Fq(4823422), + Fq(7855319), + Fq(7611795), + Fq(4784579), + Fq(342297), + Fq(286988), + Fq(5942594), + Fq(4108315), + Fq(3437287), + Fq(5038140), + Fq(1735879), + Fq(203044), + Fq(2842341), + Fq(2691481), + Fq(5790267), + Fq(1265009), + Fq(4055324), + Fq(1247620), + Fq(2486353), + Fq(1595974), + Fq(4613401), + Fq(1250494), + Fq(2635921), + Fq(4832145), + Fq(5386378), + Fq(1869119), + Fq(1903435), + Fq(7329447), + Fq(7047359), + Fq(1237275), + Fq(5062207), + Fq(6950192), + Fq(7929317), + Fq(1312455), + Fq(3306115), + Fq(6417775), + Fq(7100756), + Fq(1917081), + Fq(5834105), + Fq(7005614), + Fq(1500165), + Fq(777191), + Fq(2235880), + Fq(3406031), + Fq(7838005), + Fq(5548557), + Fq(6709241), + Fq(6533464), + Fq(5796124), + Fq(4656147), + Fq(594136), + Fq(4603424), + Fq(6366809), + Fq(2432395), + Fq(2454455), + Fq(8215696), + Fq(1957272), + Fq(3369112), + Fq(185531), + Fq(7173032), + Fq(5196991), + Fq(162844), + Fq(1616392), + Fq(3014001), + Fq(810149), + Fq(1652634), + Fq(4686184), + Fq(6581310), + Fq(5341501), + Fq(3523897), + Fq(3866901), + Fq(269760), + Fq(2213111), + Fq(7404533), + Fq(1717735), + Fq(472078), + Fq(7953734), + Fq(1723600), + Fq(6577327), + Fq(1910376), + Fq(6712985), + Fq(7276084), + Fq(8119771), + Fq(4546524), + Fq(5441381), + Fq(6144432), + Fq(7959518), + Fq(6094090), + Fq(183443), + Fq(7403526), + Fq(1612842), + Fq(4834730), + Fq(7826001), + Fq(3919660), + Fq(8332111), + Fq(7018208), + Fq(3937738), + Fq(1400424), + Fq(7534263), + Fq(1976782), +]; + +/// 역방향 변환용 원시 단위근 배열 (음수 지수) +const INTT_ZETAS: [Fq; 256] = [ + Fq(4186625), + Fq(8354570), + Fq(2608894), + Fq(518909), + Fq(8143293), + Fq(777960), + Fq(876248), + Fq(7913949), + Fq(6554070), + Fq(6026966), + Fq(359251), + Fq(2091905), + Fq(5260684), + Fq(2884855), + Fq(5268920), + Fq(5700314), + Fq(5654953), + Fq(7356305), + Fq(1079900), + Fq(4794489), + Fq(549488), + Fq(1119584), + Fq(5760665), + Fq(2108549), + Fq(2118186), + Fq(3859737), + Fq(1399561), + Fq(3277672), + Fq(6623180), + Fq(19422), + Fq(4369920), + Fq(8100412), + Fq(5674394), + Fq(8284641), + Fq(5303092), + Fq(4849980), + Fq(1661693), + Fq(3592148), + Fq(2537516), + Fq(4464978), + Fq(3861115), + Fq(3043716), + Fq(4805995), + Fq(2867647), + Fq(4840449), + Fq(300467), + Fq(6031717), + Fq(539299), + Fq(1699267), + Fq(1643818), + Fq(4874723), + Fq(3821735), + Fq(4873154), + Fq(2140649), + Fq(1600420), + Fq(4680821), + Fq(7568473), + Fq(7849063), + Fq(7426187), + Fq(4499374), + Fq(4479693), + Fq(2556880), + Fq(6308525), + Fq(2797779), + Fq(3930395), + Fq(1528703), + Fq(3677745), + Fq(3041255), + Fq(1452451), + Fq(4904467), + Fq(6203962), + Fq(1585221), + Fq(1257611), + Fq(6441103), + Fq(4083598), + Fq(1000202), + Fq(3190144), + Fq(3157330), + Fq(3632928), + Fq(8253495), + Fq(4968207), + Fq(983419), + Fq(6232521), + Fq(5665122), + Fq(2967645), + Fq(3693493), + Fq(411027), + Fq(2477047), + Fq(671102), + Fq(1228525), + Fq(22981), + Fq(1308169), + Fq(381987), + Fq(7031341), + Fq(6527646), + Fq(1430430), + Fq(3343383), + Fq(8115473), + Fq(7871466), + Fq(5282425), + Fq(8336129), + Fq(1100098), + Fq(7475901), + Fq(4421799), + Fq(3724342), + Fq(8578), + Fq(6727353), + Fq(3249728), + Fq(5991061), + Fq(210977), + Fq(7620448), + Fq(1316856), + Fq(8190869), + Fq(3553272), + Fq(5220671), + Fq(1851402), + Fq(2409325), + Fq(177440), + Fq(7064828), + Fq(7039087), + Fq(7094748), + Fq(1584928), + Fq(812732), + Fq(1439742), + Fq(3019102), + Fq(3881060), + Fq(3628969), + Fq(4540456), + Fq(6288750), + Fq(4972711), + Fq(6063917), + Fq(4562441), + Fq(3342478), + Fq(6136326), + Fq(2446433), + Fq(3562462), + Fq(8113420), + Fq(5945978), + Fq(1235728), + Fq(4867236), + Fq(3520352), + Fq(3759364), + Fq(1197226), + Fq(3193378), + Fq(7479715), + Fq(6521319), + Fq(7470875), + Fq(7561383), + Fq(7884926), + Fq(1613174), + Fq(43260), + Fq(522500), + Fq(655327), + Fq(3122442), + Fq(6348669), + Fq(5173371), + Fq(3556995), + Fq(525098), + Fq(768622), + Fq(3595838), + Fq(8038120), + Fq(8093429), + Fq(2437823), + Fq(4272102), + Fq(4943130), + Fq(3342277), + Fq(6644538), + Fq(8177373), + Fq(5538076), + Fq(5688936), + Fq(2590150), + Fq(7115408), + Fq(4325093), + Fq(7132797), + Fq(5894064), + Fq(6784443), + Fq(3767016), + Fq(7129923), + Fq(5744496), + Fq(3548272), + Fq(2994039), + Fq(6511298), + Fq(6476982), + Fq(1050970), + Fq(1333058), + Fq(7143142), + Fq(3318210), + Fq(1430225), + Fq(451100), + Fq(7067962), + Fq(5074302), + Fq(1962642), + Fq(1279661), + Fq(6463336), + Fq(2546312), + Fq(1374803), + Fq(6880252), + Fq(7603226), + Fq(6144537), + Fq(4974386), + Fq(542412), + Fq(2831860), + Fq(1671176), + Fq(1846953), + Fq(2584293), + Fq(3724270), + Fq(7786281), + Fq(3776993), + Fq(2013608), + Fq(5948022), + Fq(5925962), + Fq(164721), + Fq(6423145), + Fq(5011305), + Fq(8194886), + Fq(1207385), + Fq(3183426), + Fq(8217573), + Fq(6764025), + Fq(5366416), + Fq(7570268), + Fq(6727783), + Fq(3694233), + Fq(1799107), + Fq(3038916), + Fq(4856520), + Fq(4513516), + Fq(8110657), + Fq(6167306), + Fq(975884), + Fq(6662682), + Fq(7908339), + Fq(426683), + Fq(6656817), + Fq(1803090), + Fq(6470041), + Fq(1667432), + Fq(1104333), + Fq(260646), + Fq(3833893), + Fq(2939036), + Fq(2235985), + Fq(420899), + Fq(2286327), + Fq(8196974), + Fq(976891), + Fq(6767575), + Fq(3545687), + Fq(554416), + Fq(4460757), + Fq(48306), + Fq(1362209), + Fq(4442679), + Fq(6979993), + Fq(846154), + Fq(6403635), +]; + +/// 상수-시간 전방 NTT (Cooley-Tukey Butterfly) +/// +/// 입력 다항식 `poly`를 제자리(in-place)에서 NTT 도메인으로 변환합니다. +/// 메모리 접근 패턴과 루프 카운트는 비밀 데이터에 독립적입니다. +pub(crate) fn ntt(poly: &mut [Fq; N]) { + let mut len = 128; + let mut k = 1; + + // 분기 예측기를 자극하지 않는 고정된 루프 구조 + while len > 0 { + for start in (0..N).step_by(2 * len) { + let zeta = ZETAS[k]; + k += 1; + + for j in start..(start + len) { + // Cooley-Tukey 나비 연산 (Butterfly) + // t = zeta * poly[j + len] + let t = poly[j + len].mul(zeta); + + // poly[j + len] = poly[j] - t + poly[j + len] = poly[j].sub(t); + + // poly[j] = poly[j] + t + poly[j] = poly[j].add(t); + } + } + len >>= 1; + } +} + +/// 상수-시간 역방향 NTT (Gentleman-Sande Butterfly) +/// +/// NTT 도메인의 다항식 `poly`를 제자리에서 일반 다항식 도메인으로 복원합니다. +pub(crate) fn intt(poly: &mut [Fq; N]) { + let mut len = 1; + let mut k = 255; // 256 - 1 + + while len < N { + for start in (0..N).step_by(2 * len) { + let zeta = INTT_ZETAS[k]; + k -= 1; + + for j in start..(start + len) { + let t = poly[j]; + + // Gentleman-Sande 나비 연산 + // poly[j] = t + poly[j + len] + poly[j] = t.add(poly[j + len]); + + // poly[j + len] = (t - poly[j + len]) * zeta + let diff = t.sub(poly[j + len]); + poly[j + len] = diff.mul(zeta); + } + } + len <<= 1; + } + + // INTT 후에는 N^-1 (256^-1) 을 모든 계수에 곱하여 스케일을 정규화해야 합니다. + for coeff in poly.iter_mut() { + *coeff = coeff.mul(INV_N_MONT); + } +} diff --git a/crypto/mldsa/src/pack.rs b/crypto/mldsa/src/pack.rs new file mode 100644 index 0000000..18ec095 --- /dev/null +++ b/crypto/mldsa/src/pack.rs @@ -0,0 +1,720 @@ +//! FIPS 204 Section 10: 비트 수준 인코딩 (Algorithms 16–21) +//! +//! 다항식 및 다항식 벡터의 비트 수준 패킹/언패킹 함수를 구현합니다. +//! 민감 데이터(s1, s2, t0 등)의 인코딩에는 상수-시간(Constant-time) 연산을 사용합니다. + +use crate::Q; +use crate::field::Fq; +use crate::ntt::N; +use crate::poly::{Poly, PolyVec}; +use entlib_native_constant_time::traits::{ConstantTimeIsNegative, ConstantTimeSelect}; + +// +// 내부 유틸리티 +// + +/// FIPS 204 bitlen(x) = ⌊log2(x)⌋ + 1 (x > 0), bitlen(0) = 0 +#[inline(always)] +fn bitlen(n: u32) -> usize { + (u32::BITS - n.leading_zeros()) as usize +} + +/// Fq 양수 정규화 표현에서 부호 있는 i32로 상수-시간 변환 +/// +/// Fq 내부에서 음수 c는 (c + Q)로 저장됩니다. +/// `upper_bound`(= 범위 상한 b)를 기준으로: +/// - fq_val ≤ upper_bound → 비음수 → 반환값 = fq_val +/// - fq_val > upper_bound → 음수(Q가 더해진 상태) → 반환값 = fq_val − Q +#[inline(always)] +fn fq_to_signed_ct(fq_val: i32, upper_bound: i32) -> i32 { + // (fq_val - upper_bound - 1) < 0 ↔ fq_val ≤ upper_bound (비음수 영역) + let probe = fq_val.wrapping_sub(upper_bound).wrapping_sub(1); + let is_nonneg = probe.ct_is_negative(); + let as_negative = fq_val.wrapping_sub(Q); + i32::ct_select(&fq_val, &as_negative, is_nonneg) +} + +/// 상수-시간 음수 보정: signed < 0 이면 signed + Q를 반환합니다. +#[inline(always)] +fn signed_to_fq_ct(signed: i32) -> i32 { + let is_neg = signed.ct_is_negative(); + let with_q = signed.wrapping_add(Q); + i32::ct_select(&with_q, &signed, is_neg) +} + +// +// Algorithm 16: SimpleBitPack(w, b) +// Algorithm 18: SimpleBitUnpack(v, b) +// + +/// Algorithm 16: SimpleBitPack(w, b) +/// +/// 계수가 `[0, 2^b − 1]` 범위인 256-계수 다항식 `w`를 `b`비트/계수로 리틀-엔디언 패킹합니다. +/// +/// - 출력 크기: 32 × b 바이트 (N=256이므로 256×b 비트 = 32×b 바이트) +/// +/// FIPS 204 Algorithm 16 참조. +pub fn simple_bit_pack(w: &Poly, b: usize, out: &mut [u8]) { + debug_assert_eq!(out.len(), 32 * b, "simple_bit_pack: 출력 버퍼 크기 불일치"); + + let mask: u64 = if b >= 64 { u64::MAX } else { (1u64 << b) - 1 }; + let mut buf: u64 = 0; + let mut bits: usize = 0; + let mut idx: usize = 0; + + for coeff in &w.coeffs { + buf |= (coeff.0 as u64 & mask) << bits; + bits += b; + while bits >= 8 { + out[idx] = buf as u8; + idx += 1; + buf >>= 8; + bits -= 8; + } + } + // 256 × b 는 항상 8의 배수(256 = 2^8)이므로 bits == 0 이 보장됩니다. +} + +/// Algorithm 18: SimpleBitUnpack(v, b) +/// +/// `simple_bit_pack`의 역연산. `b`비트/계수로 패킹된 바이트 배열에서 다항식을 복원합니다. +/// +/// FIPS 204 Algorithm 18 참조. +pub fn simple_bit_unpack(v: &[u8], b: usize) -> Poly { + debug_assert_eq!(v.len(), 32 * b, "simple_bit_unpack: 입력 버퍼 크기 불일치"); + + let mask: u64 = if b >= 64 { u64::MAX } else { (1u64 << b) - 1 }; + let mut poly = Poly::new_zero(); + let mut buf: u64 = 0; + let mut bits: usize = 0; + let mut idx: usize = 0; + + for i in 0..N { + while bits < b { + buf |= (v[idx] as u64) << bits; + idx += 1; + bits += 8; + } + poly.coeffs[i] = Fq::new((buf & mask) as i32); + buf >>= b; + bits -= b; + } + + poly +} + +// +// Algorithm 17: BitPack(w, a, b) +// Algorithm 19: BitUnpack(v, a, b) +// + +/// Algorithm 17: BitPack(w, a, b) +/// +/// 계수가 `[−a, b]` 범위인 다항식 `w`를 `bitlen(a + b)`비트/계수로 패킹합니다. +/// 각 계수 c를 `(a + c)`로 인코딩합니다. +/// +/// 계수는 Fq 형태로 입력됩니다 (음수 c는 c + Q로 표현됨). +/// 상수-시간 변환으로 타이밍 누출을 방지합니다. +/// +/// 주요 사용처: +/// - s1, s2 (ETA=2 → 3비트, ETA=4 → 4비트) +/// - t0 (a=4095, b=4096 → 13비트) +/// - z (a=γ1−1, b=γ1 → 18 또는 20비트) +/// +/// FIPS 204 Algorithm 17 참조. +pub fn bit_pack(w: &Poly, a: i32, b: i32, out: &mut [u8]) { + let bw = bitlen((a + b) as u32); + debug_assert_eq!(out.len(), 32 * bw, "bit_pack: 출력 버퍼 크기 불일치"); + + let mask: u64 = if bw >= 64 { u64::MAX } else { (1u64 << bw) - 1 }; + let mut buf: u64 = 0; + let mut bits: usize = 0; + let mut idx: usize = 0; + + for coeff in &w.coeffs { + // 상수-시간으로 Fq → 부호 있는 정수 변환 (b가 비음수 범위 상한) + let signed = fq_to_signed_ct(coeff.0, b); + // encoded = a + signed ∈ [0, a+b] + let encoded = (a as i64 + signed as i64) as u64; + + buf |= (encoded & mask) << bits; + bits += bw; + while bits >= 8 { + out[idx] = buf as u8; + idx += 1; + buf >>= 8; + bits -= 8; + } + } +} + +/// Algorithm 19: BitUnpack(v, a, b) +/// +/// `bit_pack`의 역연산. 인코딩된 값 `e = a + c`에서 계수 `c = e − a`를 복원합니다. +/// 음수 계수는 Fq 표현(c + Q)으로 변환됩니다. +/// +/// FIPS 204 Algorithm 19 참조. +pub fn bit_unpack(v: &[u8], a: i32, b: i32) -> Poly { + let bw = bitlen((a + b) as u32); + debug_assert_eq!(v.len(), 32 * bw, "bit_unpack: 입력 버퍼 크기 불일치"); + + let mask: u64 = if bw >= 64 { u64::MAX } else { (1u64 << bw) - 1 }; + let mut poly = Poly::new_zero(); + let mut buf: u64 = 0; + let mut bits: usize = 0; + let mut idx: usize = 0; + + for i in 0..N { + while bits < bw { + buf |= (v[idx] as u64) << bits; + idx += 1; + bits += 8; + } + let encoded = (buf & mask) as i32; + buf >>= bw; + bits -= bw; + + // c = encoded − a; 음수이면 Fq 표현으로 상수-시간 변환 + let signed = encoded - a; + poly.coeffs[i] = Fq::new(signed_to_fq_ct(signed)); + } + + poly +} + +// +// Algorithm 20: HintBitPack(h) +// Algorithm 21: HintBitUnpack(y, ω) +// + +/// Algorithm 20: HintBitPack(h) +/// +/// 힌트 다항식 벡터 `h`를 압축 인코딩합니다. +/// 각 다항식의 계수는 0 또는 1이며, 1인 계수의 위치(인덱스)를 기록합니다. +/// +/// 출력 형식 (총 ω + K 바이트): +/// - `out[0..ω]`: 1인 계수의 위치들 (다항식 순서대로 연속 기록, 나머지는 0) +/// - `out[ω..ω+K]`: i번째 원소 = 처음 i+1개 다항식까지의 1의 누적 개수 +/// +/// 전제: h 전체에서 0이 아닌 계수의 총 수 ≤ ω +/// +/// FIPS 204 Algorithm 20 참조. +pub fn hint_bit_pack(h: &PolyVec, omega: usize, out: &mut [u8]) { + debug_assert_eq!(out.len(), omega + K, "hint_bit_pack: 출력 버퍼 크기 불일치"); + + for b in out.iter_mut() { + *b = 0; + } + + let mut index: usize = 0; + for i in 0..K { + for j in 0..N { + if h.vec[i].coeffs[j].0 != 0 { + debug_assert!(index < omega, "hint_bit_pack: 1의 개수가 ω를 초과합니다"); + out[index] = j as u8; + index += 1; + } + } + // i번째 다항식까지의 누적 1의 개수 + out[omega + i] = index as u8; + } +} + +/// Algorithm 21: HintBitUnpack(y, ω) +/// +/// `hint_bit_pack`의 역연산. 압축 인코딩된 힌트 바이트 배열에서 힌트 벡터를 복원합니다. +/// 인코딩이 유효하지 않으면 `None`을 반환합니다 (FIPS 204 Section 7.4의 유효성 조건 준수). +/// +/// FIPS 204 Algorithm 21 참조. +pub fn hint_bit_unpack(y: &[u8], omega: usize) -> Option> { + if y.len() != omega + K { + return None; + } + + let mut h = PolyVec::::new_zero(); + let mut index: usize = 0; + + for i in 0..K { + let limit = y[omega + i] as usize; + + // 단조 증가 및 ω 상한 검증 + if limit < index || limit > omega { + return None; + } + + let first = index; + while index < limit { + // 다항식 내에서 위치 인덱스는 엄격히 단조 증가해야 함 (중복/역순 방지) + if index > first && y[index] <= y[index - 1] { + return None; + } + h.vec[i].coeffs[y[index] as usize] = Fq::new(1); + index += 1; + } + } + + // 사용되지 않은 패딩 영역(index..omega)은 0이어야 합니다 (인코딩 가변성 방지) + if y[index..omega].iter().any(|&b| b != 0) { + return None; + } + + Some(h) +} + +// +// 다항식 벡터 수준 래퍼 함수들 +// + +/// PolyVec를 η 비트 패킹으로 직렬화합니다 (s1, s2 인코딩용). +/// +/// 각 다항식은 `bitlen(2η)`비트/계수로 인코딩됩니다. +/// - η=2 → 3비트/계수, 96바이트/다항식 +/// - η=4 → 4비트/계수, 128바이트/다항식 +/// +/// 출력 크기: D × 32 × bitlen(2η) 바이트 +pub fn polyvec_bit_pack_eta(vec: &PolyVec, eta: i32, out: &mut [u8]) { + let bw = bitlen((2 * eta) as u32); + let bpp = 32 * bw; // bytes per poly + for i in 0..D { + bit_pack(&vec.vec[i], eta, eta, &mut out[i * bpp..(i + 1) * bpp]); + } +} + +/// η 비트 언패킹으로 PolyVec를 복원합니다 (s1, s2 디코딩용). +pub fn polyvec_bit_unpack_eta(v: &[u8], eta: i32) -> PolyVec { + let bw = bitlen((2 * eta) as u32); + let bpp = 32 * bw; + let mut vec = PolyVec::::new_zero(); + for i in 0..D { + vec.vec[i] = bit_unpack(&v[i * bpp..(i + 1) * bpp], eta, eta); + } + vec +} + +/// PolyVec를 t0 13비트 패킹으로 직렬화합니다. +/// +/// t0 계수는 [-2^(d-1)+1, 2^(d-1)] = [-4095, 4096] 범위이며, +/// BitPack(t0, 2^(d-1)−1, 2^(d-1)) = BitPack(t0, 4095, 4096) 으로 인코딩됩니다. +/// +/// 출력 크기: D × 416 바이트 (32 × 13) +pub fn polyvec_bit_pack_t0(vec: &PolyVec, out: &mut [u8]) { + // d = 13, 2^(d-1) = 4096 + const A: i32 = (1 << 12) - 1; // 4095 + const B: i32 = 1 << 12; // 4096 + const BPP: usize = 32 * 13; // 416 bytes per poly + for i in 0..D { + bit_pack(&vec.vec[i], A, B, &mut out[i * BPP..(i + 1) * BPP]); + } +} + +/// t0 13비트 언패킹으로 PolyVec를 복원합니다. +pub fn polyvec_bit_unpack_t0(v: &[u8]) -> PolyVec { + const A: i32 = (1 << 12) - 1; // 4095 + const B: i32 = 1 << 12; // 4096 + const BPP: usize = 32 * 13; // 416 bytes per poly + let mut vec = PolyVec::::new_zero(); + for i in 0..D { + vec.vec[i] = bit_unpack(&v[i * BPP..(i + 1) * BPP], A, B); + } + vec +} + +/// PolyVec를 γ1 비트 패킹으로 직렬화합니다 (서명 z 인코딩용). +/// +/// z 계수는 [−γ1+1, γ1] 범위이며, BitPack(z, γ1−1, γ1)으로 인코딩됩니다. +/// - γ1 = 2^17 → 18비트/계수, 576바이트/다항식 +/// - γ1 = 2^19 → 20비트/계수, 640바이트/다항식 +pub fn polyvec_bit_pack_z(vec: &PolyVec, gamma1: i32, out: &mut [u8]) { + let a = gamma1 - 1; + let bw = bitlen((a + gamma1) as u32); + let bpp = 32 * bw; + for i in 0..D { + bit_pack(&vec.vec[i], a, gamma1, &mut out[i * bpp..(i + 1) * bpp]); + } +} + +/// γ1 비트 언패킹으로 PolyVec를 복원합니다 (서명 z 디코딩용). +pub fn polyvec_bit_unpack_z(v: &[u8], gamma1: i32) -> PolyVec { + let a = gamma1 - 1; + let bw = bitlen((a + gamma1) as u32); + let bpp = 32 * bw; + let mut vec = PolyVec::::new_zero(); + for i in 0..D { + vec.vec[i] = bit_unpack(&v[i * bpp..(i + 1) * bpp], a, gamma1); + } + vec +} + +/// PolyVec를 w1 비트 패킹으로 직렬화합니다 (분해 고차 비트 인코딩용). +/// +/// w1 계수는 [0, (q−1)/(2γ2) − 1] 범위의 비음수입니다. +/// - γ2 = (q−1)/88 → 최대값 43 → bitlen(43) = 6비트/계수 +/// - γ2 = (q−1)/32 → 최대값 15 → bitlen(15) = 4비트/계수 +pub fn polyvec_simple_bit_pack_w1(vec: &PolyVec, gamma2: i32, out: &mut [u8]) { + let max_coeff = (Q - 1) / (2 * gamma2) - 1; + let bw = bitlen(max_coeff as u32); + let bpp = 32 * bw; + for i in 0..D { + simple_bit_pack(&vec.vec[i], bw, &mut out[i * bpp..(i + 1) * bpp]); + } +} + +// +// 단일 다항식의 t1 패킹 (SimpleBitPack with b=10) +// + +/// t1 다항식 하나를 SimpleBitPack(t1, 10)으로 직렬화합니다. +/// +/// t1 계수는 [0, 2^10 − 1] = [0, 1023] 범위입니다. +/// 출력 크기: 320바이트 (32 × 10). +pub fn poly_simple_bit_pack_t1(w: &Poly, out: &mut [u8]) { + simple_bit_pack(w, 10, out); +} + +/// SimpleBitUnpack(v, 10)으로 t1 다항식을 복원합니다. +pub fn poly_simple_bit_unpack_t1(v: &[u8]) -> Poly { + simple_bit_unpack(v, 10) +} + +// +// 테스트 +// + +#[cfg(test)] +mod tests { // todo: 이거 옮기든가 축약하든가 해야되는데 + // 보기 안ㄴ좋음 + use super::*; + use crate::poly::PolyVec; + + /// 계수 배열로 다항식 생성 헬퍼 (순환 적용) + fn poly_from_slice(vals: &[i32]) -> Poly { + let mut p = Poly::new_zero(); + for (i, c) in p.coeffs.iter_mut().enumerate() { + *c = Fq::new(vals[i % vals.len()]); + } + p + } + + // + // bitlen + // + + #[test] + fn test_bitlen_fips204_values() { + assert_eq!(bitlen(0), 0); + assert_eq!(bitlen(1), 1); + assert_eq!(bitlen(4), 3); // η=2: bitlen(2η) = bitlen(4) = 3 + assert_eq!(bitlen(8), 4); // η=4: bitlen(2η) = bitlen(8) = 4 + assert_eq!(bitlen(1023), 10); // t1: bitlen(2^10 - 1) + assert_eq!(bitlen(8191), 13); // t0: bitlen(4095+4096) + assert_eq!(bitlen(262143), 18); // z,γ1=2^17: bitlen(2γ1-1) + assert_eq!(bitlen(1048575), 20); // z,γ1=2^19: bitlen(2γ1-1) + } + + // + // simple_bit_pack / simple_bit_unpack 왕복 테스트 + // + + #[test] + fn test_simple_bit_pack_roundtrip_b10() { + // t1: 계수 ∈ [0, 1023], b=10 + let vals: Vec = (0..256).map(|i| (i * 4) % 1024).collect(); + let poly = poly_from_slice(&vals); + + let mut buf = vec![0u8; 320]; // 32*10 + simple_bit_pack(&poly, 10, &mut buf); + let recovered = simple_bit_unpack(&buf, 10); + + for i in 0..N { + assert_eq!(poly.coeffs[i].0, recovered.coeffs[i].0, "계수 {i} 불일치"); + } + } + + #[test] + fn test_simple_bit_pack_roundtrip_b6() { + // w1 (γ2=(q-1)/88): 계수 ∈ [0, 43], b=6 + let vals: Vec = (0..256).map(|i| i % 44).collect(); + let poly = poly_from_slice(&vals); + + let mut buf = vec![0u8; 192]; // 32*6 + simple_bit_pack(&poly, 6, &mut buf); + let recovered = simple_bit_unpack(&buf, 6); + + for i in 0..N { + assert_eq!(poly.coeffs[i].0, recovered.coeffs[i].0, "계수 {i} 불일치"); + } + } + + #[test] + fn test_simple_bit_pack_roundtrip_b4() { + // w1 (γ2=(q-1)/32): 계수 ∈ [0, 15], b=4 + let vals: Vec = (0..256).map(|i| i % 16).collect(); + let poly = poly_from_slice(&vals); + + let mut buf = vec![0u8; 128]; // 32*4 + simple_bit_pack(&poly, 4, &mut buf); + let recovered = simple_bit_unpack(&buf, 4); + + for i in 0..N { + assert_eq!(poly.coeffs[i].0, recovered.coeffs[i].0, "계수 {i} 불일치"); + } + } + + // + // bit_pack / bit_unpack 왕복 테스트 + // + + /// Fq 표현으로 [-eta, eta] 범위의 계수를 가진 다항식 생성 + fn poly_eta(eta: i32) -> Poly { + let mut p = Poly::new_zero(); + let range = 2 * eta + 1; // [-eta, eta] → range 가지 값 + for (i, c) in p.coeffs.iter_mut().enumerate() { + let signed = (i as i32 % range) - eta; // -eta..=eta 순환 + let fq_val = if signed < 0 { signed + Q } else { signed }; + *c = Fq::new(fq_val); + } + p + } + + #[test] + fn test_bit_pack_roundtrip_eta2() { + // s: 계수 ∈ [-2, 2], 3비트/계수, 96바이트 + let poly = poly_eta(2); + let mut buf = vec![0u8; 96]; // 32*3 + bit_pack(&poly, 2, 2, &mut buf); + let recovered = bit_unpack(&buf, 2, 2); + + for i in 0..N { + assert_eq!( + poly.coeffs[i].0, recovered.coeffs[i].0, + "η=2 계수 {i} 불일치" + ); + } + } + + #[test] + fn test_bit_pack_roundtrip_eta4() { + // s: 계수 ∈ [-4, 4], 4비트/계수, 128바이트 + let poly = poly_eta(4); + let mut buf = vec![0u8; 128]; // 32*4 + bit_pack(&poly, 4, 4, &mut buf); + let recovered = bit_unpack(&buf, 4, 4); + + for i in 0..N { + assert_eq!( + poly.coeffs[i].0, recovered.coeffs[i].0, + "η=4 계수 {i} 불일치" + ); + } + } + + #[test] + fn test_bit_pack_roundtrip_t0() { + // t0: 계수 ∈ [-4095, 4096], 13비트/계수, 416바이트 + // a=4095, b=4096 + let mut poly = Poly::new_zero(); + for (i, c) in poly.coeffs.iter_mut().enumerate() { + // -4095 ~ +4096 범위를 순환 + let signed = (i as i32 % 8192) - 4095; + let fq_val = if signed < 0 { signed + Q } else { signed }; + *c = Fq::new(fq_val); + } + + let mut buf = vec![0u8; 416]; // 32*13 + bit_pack(&poly, 4095, 4096, &mut buf); + let recovered = bit_unpack(&buf, 4095, 4096); + + for i in 0..N { + assert_eq!( + poly.coeffs[i].0, recovered.coeffs[i].0, + "t0 계수 {i} 불일치" + ); + } + } + + #[test] + fn test_bit_pack_roundtrip_z_gamma1_17() { + // z: 계수 ∈ [-(γ1-1), γ1] = [-131071, 131072], 18비트, 576바이트 + let gamma1: i32 = 1 << 17; // 131072 + let a = gamma1 - 1; + + let mut poly = Poly::new_zero(); + for (i, c) in poly.coeffs.iter_mut().enumerate() { + let signed = (i as i32 % (2 * gamma1)) - (gamma1 - 1); + let fq_val = if signed < 0 { signed + Q } else { signed }; + *c = Fq::new(fq_val); + } + + let bw = bitlen((a + gamma1) as u32); // 18 + let mut buf = vec![0u8; 32 * bw]; + bit_pack(&poly, a, gamma1, &mut buf); + let recovered = bit_unpack(&buf, a, gamma1); + + for i in 0..N { + assert_eq!( + poly.coeffs[i].0, recovered.coeffs[i].0, + "z(γ1=2^17) 계수 {i} 불일치" + ); + } + } + + #[test] + fn test_bit_pack_roundtrip_z_gamma1_19() { + // z: 계수 ∈ [-(γ1-1), γ1] = [-524287, 524288], 20비트, 640바이트 + let gamma1: i32 = 1 << 19; // 524288 + let a = gamma1 - 1; + + let mut poly = Poly::new_zero(); + for (i, c) in poly.coeffs.iter_mut().enumerate() { + let signed = (i as i32 % (2 * 1024)) - 1023; // 단순화된 범위 + let fq_val = if signed < 0 { signed + Q } else { signed }; + *c = Fq::new(fq_val); + } + + let bw = bitlen((a + gamma1) as u32); // 20 + let mut buf = vec![0u8; 32 * bw]; + bit_pack(&poly, a, gamma1, &mut buf); + let recovered = bit_unpack(&buf, a, gamma1); + + for i in 0..N { + assert_eq!( + poly.coeffs[i].0, recovered.coeffs[i].0, + "z(γ1=2^19) 계수 {i} 불일치" + ); + } + } + + // + // hint_bit_pack / hint_bit_unpack 왕복 테스트 + // + + #[test] + fn test_hint_bit_pack_roundtrip() { + // K=4, ω=80 (ML-DSA-44) + const K: usize = 4; + const OMEGA: usize = 80; + + let mut h = PolyVec::::new_zero(); + // 각 다항식에 몇 개의 힌트를 설정 + h.vec[0].coeffs[0] = Fq::new(1); + h.vec[0].coeffs[5] = Fq::new(1); + h.vec[1].coeffs[3] = Fq::new(1); + h.vec[2].coeffs[200] = Fq::new(1); + h.vec[3].coeffs[100] = Fq::new(1); + h.vec[3].coeffs[255] = Fq::new(1); + + let mut buf = vec![0u8; OMEGA + K]; + hint_bit_pack(&h, OMEGA, &mut buf); + + let recovered = hint_bit_unpack::(&buf, OMEGA).expect("hint_bit_unpack 실패"); + + for i in 0..K { + for j in 0..N { + assert_eq!( + h.vec[i].coeffs[j].0, recovered.vec[i].coeffs[j].0, + "힌트 [{i}][{j}] 불일치" + ); + } + } + } + + #[test] + fn test_hint_bit_unpack_rejects_invalid_limit() { + const K: usize = 4; + const OMEGA: usize = 80; + + let mut buf = vec![0u8; OMEGA + K]; + // omega + 0 에 ω+1 = 81 을 써서 유효 범위 초과 + buf[OMEGA] = (OMEGA + 1) as u8; + + assert!(hint_bit_unpack::(&buf, OMEGA).is_none()); + } + + #[test] + fn test_hint_bit_unpack_rejects_nonmonotone() { + const K: usize = 4; + const OMEGA: usize = 80; + + let mut buf = vec![0u8; OMEGA + K]; + // 2개의 힌트를 역순으로 기록 (5, 3 → 단조 증가 위반) + buf[0] = 5; + buf[1] = 3; + buf[OMEGA] = 2; // 첫 번째 다항식에 2개 + + assert!(hint_bit_unpack::(&buf, OMEGA).is_none()); + } + + #[test] + fn test_hint_bit_unpack_rejects_nonzero_padding() { + const K: usize = 4; + const OMEGA: usize = 80; + + let mut buf = vec![0u8; OMEGA + K]; + // 힌트는 0개이지만 패딩 영역에 쓰레기 값 + buf[0] = 42; // 사용되지 않은 패딩 + + // 모든 limit = 0 (힌트 없음)이므로 패딩 검사에서 실패해야 함 + assert!(hint_bit_unpack::(&buf, OMEGA).is_none()); + } + + // + // polyvec 래퍼 왕복 테스트 + // + + #[test] + fn test_polyvec_eta2_roundtrip() { + const L: usize = 4; + let mut s1 = PolyVec::::new_zero(); + for i in 0..L { + s1.vec[i] = poly_eta(2); + } + + let bw = bitlen(4); // 3 + let size = L * 32 * bw; // 4*96 = 384 + let mut buf = vec![0u8; size]; + polyvec_bit_pack_eta(&s1, 2, &mut buf); + let recovered: PolyVec = polyvec_bit_unpack_eta(&buf, 2); + + for i in 0..L { + for j in 0..N { + assert_eq!(s1.vec[i].coeffs[j].0, recovered.vec[i].coeffs[j].0); + } + } + } + + #[test] + fn test_polyvec_t0_roundtrip() { + const K: usize = 4; + let mut t0 = PolyVec::::new_zero(); + for i in 0..K { + for (j, c) in t0.vec[i].coeffs.iter_mut().enumerate() { + let signed = (j as i32 % 8192) - 4095; + let fq_val = if signed < 0 { signed + Q } else { signed }; + *c = Fq::new(fq_val); + } + } + + let size = K * 32 * 13; // 4*416 = 1664 + let mut buf = vec![0u8; size]; + polyvec_bit_pack_t0(&t0, &mut buf); + let recovered: PolyVec = polyvec_bit_unpack_t0(&buf); + + for i in 0..K { + for j in 0..N { + assert_eq!(t0.vec[i].coeffs[j].0, recovered.vec[i].coeffs[j].0); + } + } + } + + #[test] + fn test_poly_t1_roundtrip() { + let vals: Vec = (0..256).map(|i| i % 1024).collect(); + let poly = poly_from_slice(&vals); + let mut buf = vec![0u8; 320]; + poly_simple_bit_pack_t1(&poly, &mut buf); + let recovered = poly_simple_bit_unpack_t1(&buf); + for i in 0..N { + assert_eq!(poly.coeffs[i].0, recovered.coeffs[i].0); + } + } +} diff --git a/crypto/mldsa/src/poly.rs b/crypto/mldsa/src/poly.rs new file mode 100644 index 0000000..9e38d4d --- /dev/null +++ b/crypto/mldsa/src/poly.rs @@ -0,0 +1,120 @@ +use crate::field::Fq; +use crate::ntt::{N, intt, ntt}; + +/// FIPS 204 다항식 구조체 +/// +/// 256개의 Fq 계수를 가지며, 복사(Copy)가 가능하여 메모리 상에서 +/// 안전하게 직렬화/역직렬화 및 소거(Zeroize)가 용이하도록 설계되었습니다. +#[derive(Clone, Copy)] +pub struct Poly { + pub coeffs: [Fq; N], +} + +impl Poly { + /// 모든 계수가 0인 영다항식을 생성합니다. + pub const fn new_zero() -> Self { + Self { + coeffs: [Fq::new(0); N], + } + } + + /// 두 다항식의 상수-시간 덧셈 + pub fn add(&self, other: &Self) -> Self { + let mut result = Self::new_zero(); + for i in 0..N { + result.coeffs[i] = self.coeffs[i].add(other.coeffs[i]); + } + result + } + + /// 두 다항식의 상수-시간 뺄셈 + pub fn sub(&self, other: &Self) -> Self { + let mut result = Self::new_zero(); + for i in 0..N { + result.coeffs[i] = self.coeffs[i].sub(other.coeffs[i]); + } + result + } + + /// NTT 도메인 상에서 두 다항식의 점별 몽고메리 곱셈 (Point-wise Montgomery Multiplication) + /// + /// FIPS 204에서는 다항식이 256개의 1차 인수로 완전히 분해되므로, + /// 나비 연산의 기본 단위(Basecase) 곱셈이 아닌 단순 계수별 곱셈만 수행합니다. + pub fn pointwise_montgomery(&self, other: &Self) -> Self { + let mut result = Self::new_zero(); + for i in 0..N { + result.coeffs[i] = self.coeffs[i].mul(other.coeffs[i]); + } + result + } +} + +/// 차원이 D인 다항식 벡터 (비밀 키 s1, s2, 혹은 서명 요소 z 등에 사용) +/// ML-DSA 파라미터(k, l)에 따라 D 값은 4, 5, 6, 7, 8 중 하나가 됩니다. +#[derive(Clone, Copy)] +pub struct PolyVec { + pub vec: [Poly; D], +} + +impl PolyVec { + pub const fn new_zero() -> Self { + Self { + vec: [Poly::new_zero(); D], + } + } + + /// 벡터 내의 모든 다항식을 NTT 도메인으로 변환합니다 (제자리 연산). + pub fn ntt(&mut self) { + for i in 0..D { + ntt(&mut self.vec[i].coeffs); + } + } + + /// 벡터 내의 모든 다항식을 역방향 NTT를 통해 일반 도메인으로 복원합니다. + pub fn intt(&mut self) { + for i in 0..D { + intt(&mut self.vec[i].coeffs); + } + } + + /// 벡터 간의 상수-시간 덧셈 + pub fn add(&self, other: &Self) -> Self { + let mut result = Self::new_zero(); + for i in 0..D { + result.vec[i] = self.vec[i].add(&other.vec[i]); + } + result + } +} + +/// K x L 크기의 다항식 행렬 (공개 행렬 A에 사용) +#[derive(Clone, Copy)] +pub struct PolyMatrix { + pub rows: [[Poly; L]; K], +} + +impl PolyMatrix { + pub const fn new_zero() -> Self { + Self { + rows: [[Poly::new_zero(); L]; K], + } + } + + /// FIPS 204 명세에 따른 행렬-벡터 곱셈 (t = A * s) + /// + /// 주의: 행렬 A와 벡터 s는 모두 NTT 도메인 상에 존재해야 합니다. + /// 결과 벡터 t 또한 NTT 도메인의 다항식 벡터로 반환됩니다. + pub fn multiply_vector(&self, s: &PolyVec) -> PolyVec { + let mut t = PolyVec::::new_zero(); + + for i in 0..K { + for j in 0..L { + // A_{i,j} * s_j (점별 몽고메리 곱셈) + let term = self.rows[i][j].pointwise_montgomery(&s.vec[j]); + // t_i = t_i + (A_{i,j} * s_j) + t.vec[i] = t.vec[i].add(&term); + } + } + t + } +} diff --git a/crypto/mldsa/src/sample.rs b/crypto/mldsa/src/sample.rs new file mode 100644 index 0000000..95b5350 --- /dev/null +++ b/crypto/mldsa/src/sample.rs @@ -0,0 +1,171 @@ +use crate::MLDSAError::InternalError; +use crate::field::Fq; +use crate::ntt::N; +use crate::poly::{Poly, PolyMatrix, PolyVec}; +use crate::{MLDSAError, Q}; +use entlib_native_sha3::api::{SHAKE128, SHAKE256}; + +/// Algorithm 32: ExpandA(ρ) +/// +/// 32바이트의 공개 시드 ρ로부터 k x l 크기의 다항식 행렬 A를 생성합니다. +/// 반환되는 행렬의 모든 다항식은 NTT 도메인에 위치합니다. +pub fn expand_a( + rho: &[u8; 32], +) -> Result, MLDSAError> { + let mut matrix = PolyMatrix::::new_zero(); + + // 명세에 따른 행렬 인덱싱: r (행, 0 to k-1), s (열, 0 to l-1) + for r in 0..K { + for s in 0..L { + // ρ' = ρ || IntegerToBytes(s, 1) || IntegerToBytes(r, 1) + let mut seed = [0u8; 34]; + seed[..32].copy_from_slice(rho); + seed[32] = s as u8; + seed[33] = r as u8; + + // RejNTTPoly(ρ') 호출 및 결과 할당 + matrix.rows[r][s] = rej_ntt_poly(&seed)?; + } + } + + Ok(matrix) +} + +/// RejNTTPoly(ρ') 서브루틴 +/// +/// SHAKE128을 사용하여 시드로부터 유한체 Fq 상의 계수 256개를 거절 샘플링합니다. +fn rej_ntt_poly(seed: &[u8; 34]) -> Result { + let mut shake = SHAKE128::new(); + shake.update(seed); + + // Q = 8380417, 23비트 최대값 = 8388607 + // 수용 확률(Acceptance Rate)은 8380417 / 8388608 ≈ 99.9% 임 + // 256개의 계수를 얻기 위해 최소 256 * 3 = 768 바이트가 필요하며 + // 거절 확률을 고려해 XOF에서 840 바이트를 한 번에 추출함(약 280회 샘플링 가능) + let buf = shake.finalize(840)?; + let data = buf.as_slice(); + + let mut poly = Poly::new_zero(); + let mut count = 0; + let mut i = 0; + + // 계수 256개를 모두 채울 때까지 반복 + while count < N && i + 3 <= data.len() { + // Little-endian 방식의 3바이트 파싱 + let b0 = data[i] as u32; + let b1 = data[i + 1] as u32; + let b2 = data[i + 2] as u32; + + // FIPS 204 명세에 따른 23비트 마스킹 (세 번째 바이트의 최상위 비트 무시) + let val = b0 | (b1 << 8) | ((b2 & 0x7F) << 16); + + // 거절 샘플링: 추출된 값이 Q 미만일 경우에만 다항식의 계수로 채택 + if val < Q as u32 { + poly.coeffs[count] = Fq::new(val as i32); + count += 1; + } + + i += 3; + } + + if count < N { + // 극단적으로 운이 나빠 840바이트 내에서 256개를 채우지 못한 경우 방어 로직 + return Err(InternalError("거절 샘플링 중 SHAKE128 출력이 부족합니다!")); + } + + Ok(poly) +} + +/// Algorithm 33: ExpandS(ρ') +/// +/// 64바이트의 비밀 시드 ρ'로부터 다항식 벡터 s1(크기 L)과 s2(크기 K)를 샘플링합니다. +/// 각 다항식의 계수는 [-ETA, ETA] 구간의 값을 가집니다. +/// +/// # Generics +/// - `K`, `L`: ML-DSA 파라미터 (행렬 차원) +/// - `ETA`: 오차 분포 범위 (ML-DSA-44/87의 경우 2, ML-DSA-65의 경우 4) +pub fn expand_s( + rho_prime: &[u8; 64], +) -> Result<(PolyVec, PolyVec), MLDSAError> { + let mut s1 = PolyVec::::new_zero(); + let mut s2 = PolyVec::::new_zero(); + + // 1. s1 생성 (r from 0 to L - 1) + for r in 0..L { + // IntegerToBytes(r, 2) + s1.vec[r] = rej_bounded_poly::(rho_prime, r as u16)?; + } + + // 2. s2 생성 (r from 0 to K - 1) + for r in 0..K { + // IntegerToBytes(r + L, 2) + s2.vec[r] = rej_bounded_poly::(rho_prime, (r + L) as u16)?; + } + + Ok((s1, s2)) +} + +/// Algorithm 34: RejBoundedPoly(ρ', nonce) +/// +/// SHAKE256을 사용하여 [-ETA, ETA] 범위 내의 계수 256개를 거절 샘플링합니다. +fn rej_bounded_poly(rho_prime: &[u8; 64], nonce: u16) -> Result { + // ρ' (64 bytes) || IntegerToBytes(nonce, 2) + let mut seed = [0u8; 66]; + seed[..64].copy_from_slice(rho_prime); + seed[64] = (nonce & 0xFF) as u8; + seed[65] = (nonce >> 8) as u8; + + let mut shake = SHAKE256::new(); + shake.update(&seed); + + // 1바이트에서 2개의 4비트(nibble) 값을 추출 + // ETA = 2일 경우: 0~4 수용. 수용 확률 ≈ 31.25%. 256개 추출을 위해 약 819바이트 필요 + // ETA = 4일 경우: 0~8 수용. 수용 확률 ≈ 56.25%. 256개 추출을 위해 약 455바이트 필요 + // XOF 재호출(Re-squeeze)을 방지하기 위해 넉넉한 버퍼를 한 번에 할당함 + let buf_len = if ETA == 2 { 1024 } else { 768 }; + let buf = shake.finalize(buf_len)?; // 반환된 SecureBuffer는 스코프 종료 시 자동 소거됨 + let data = buf.as_slice(); + + let mut poly = Poly::new_zero(); + let mut count = 0; + let mut i = 0; + + while count < N && i < data.len() { + let z = data[i]; + + // z0 = z mod 16, z1 = floor(z / 16) + let z0 = (z & 0x0F) as i32; + let z1 = (z >> 4) as i32; + + // 첫 번째 니블 검사 + if z0 <= 2 * ETA { + let mut val = ETA - z0; // [-ETA, ETA] 범위의 값 + // 음수일 경우 모듈러스 Q를 더해 정규화된 양수로 변환 (유한체 Fq의 표현 방식) + if val < 0 { + val += Q; + } + poly.coeffs[count] = Fq::new(val); + count += 1; + } + + // 두 번째 니블 검사 (count < N 확인 필수) + if count < N && z1 <= 2 * ETA { + let mut val = ETA - z1; + if val < 0 { + val += Q; + } + poly.coeffs[count] = Fq::new(val); + count += 1; + } + + i += 1; + } + + if count < N { + return Err(InternalError( + "RejBoundedPoly 실행 중 SHAKE256 출력이 부족합니다!", + )); + } + + Ok(poly) +} From f78dfb172dfe6362e8be2f045ae03c82397e1fb9 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:10:24 +0900 Subject: [PATCH 2/4] =?UTF-8?q?CSP=20=EC=A3=BC=EC=9E=85=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto/mldsa/src/_mldsa_test.rs | 276 ++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 crypto/mldsa/src/_mldsa_test.rs diff --git a/crypto/mldsa/src/_mldsa_test.rs b/crypto/mldsa/src/_mldsa_test.rs new file mode 100644 index 0000000..b3391a6 --- /dev/null +++ b/crypto/mldsa/src/_mldsa_test.rs @@ -0,0 +1,276 @@ +#[cfg(test)] +mod tests { + use crate::mldsa::{MLDSA, MLDSAParameter}; + use crate::mldsa_keys::{ + MLDSAPrivateKey, MLDSAPrivateKeyTrait, MLDSAPublicKey, MLDSAPublicKeyTrait, keygen_internal, + }; + use crate::ntt::N; + + // ML-DSA-44 파라미터 + const K44: usize = 4; + const L44: usize = 4; + const ETA44: i32 = 2; + const PK44_LEN: usize = 1312; + const SK44_LEN: usize = 2560; + + // ML-DSA-65 파라미터 + const K65: usize = 6; + const L65: usize = 5; + const ETA65: i32 = 4; + const PK65_LEN: usize = 1952; + const SK65_LEN: usize = 4032; + + // + // pkEncode / pkDecode 라운드트립 (ML-DSA-44) + // + + #[test] + fn test_pk_encode_decode_roundtrip_44() { + let xi = [0u8; 32]; + let (pk, _sk) = keygen_internal::(&xi).expect("keygen_internal failed"); + + // pkEncode + let pk_bytes: [u8; PK44_LEN] = + as MLDSAPublicKeyTrait>::pk_encode(&pk); + + // pkDecode + let pk2 = as MLDSAPublicKeyTrait>::pk_decode(&pk_bytes); + + // ρ 일치 검증 + assert_eq!(pk.rho, pk2.rho, "pkDecode: ρ 불일치"); + + // t1 계수 일치 검증 + for i in 0..K44 { + for j in 0..N { + assert_eq!( + pk.t1.vec[i].coeffs[j].0, pk2.t1.vec[i].coeffs[j].0, + "pkDecode: t1[{i}][{j}] 불일치" + ); + } + } + } + + // + // pkEncode / pkDecode 라운드트립 (ML-DSA-65) + // + + #[test] + fn test_pk_encode_decode_roundtrip_65() { + let xi = [1u8; 32]; + let (pk, _sk) = keygen_internal::(&xi).expect("keygen_internal failed"); + + let pk_bytes: [u8; PK65_LEN] = + as MLDSAPublicKeyTrait>::pk_encode(&pk); + let pk2 = as MLDSAPublicKeyTrait>::pk_decode(&pk_bytes); + + assert_eq!(pk.rho, pk2.rho, "pkDecode: ρ 불일치"); + for i in 0..K65 { + for j in 0..N { + assert_eq!( + pk.t1.vec[i].coeffs[j].0, pk2.t1.vec[i].coeffs[j].0, + "pkDecode: t1[{i}][{j}] 불일치" + ); + } + } + } + + // + // skEncode / skDecode 라운드트립 (ML-DSA-44) + // + + #[test] + fn test_sk_encode_decode_roundtrip_44() { + type SK44 = MLDSAPrivateKey; + + let xi = [2u8; 32]; + let (_pk, sk) = keygen_internal::(&xi).expect("keygen_internal failed"); + + // skEncode → SecureBuffer + let sk_buf = >::sk_encode(&sk) + .expect("skEncode failed"); + + // SecureBuffer 길이 검증 + assert_eq!(sk_buf.len(), SK44_LEN, "skEncode: 길이 불일치"); + + // skDecode + let sk2 = >::sk_decode(&sk_buf) + .expect("skDecode failed"); + + // 고정 필드 일치 검증 + assert_eq!(sk.rho, sk2.rho, "skDecode: ρ 불일치"); + assert_eq!(sk.k_seed, sk2.k_seed, "skDecode: K_seed 불일치"); + assert_eq!(sk.tr, sk2.tr, "skDecode: tr 불일치"); + + // s1 계수 일치 검증 + for i in 0..L44 { + for j in 0..N { + assert_eq!( + sk.s1.vec[i].coeffs[j].0, sk2.s1.vec[i].coeffs[j].0, + "skDecode: s1[{i}][{j}] 불일치" + ); + } + } + + // s2 계수 일치 검증 + for i in 0..K44 { + for j in 0..N { + assert_eq!( + sk.s2.vec[i].coeffs[j].0, sk2.s2.vec[i].coeffs[j].0, + "skDecode: s2[{i}][{j}] 불일치" + ); + } + } + + // t0 계수 일치 검증 + for i in 0..K44 { + for j in 0..N { + assert_eq!( + sk.t0.vec[i].coeffs[j].0, sk2.t0.vec[i].coeffs[j].0, + "skDecode: t0[{i}][{j}] 불일치" + ); + } + } + } + + // + // skEncode / skDecode 라운드트립 (ML-DSA-65) + // + + #[test] + fn test_sk_encode_decode_roundtrip_65() { + type SK65 = MLDSAPrivateKey; + + let xi = [3u8; 32]; + let (_pk, sk) = keygen_internal::(&xi).expect("keygen_internal failed"); + + let sk_buf = >::sk_encode(&sk) + .expect("skEncode failed"); + + assert_eq!(sk_buf.len(), SK65_LEN, "skEncode: 길이 불일치"); + + let sk2 = >::sk_decode(&sk_buf) + .expect("skDecode failed"); + + assert_eq!(sk.rho, sk2.rho, "skDecode: ρ 불일치"); + assert_eq!(sk.k_seed, sk2.k_seed, "skDecode: K_seed 불일치"); + assert_eq!(sk.tr, sk2.tr, "skDecode: tr 불일치"); + } + + // + // 서명 + 검증 종단 간 테스트 (ML-DSA-44) + // + + #[test] + fn test_sign_verify_roundtrip_44() { + let xi = [0xAAu8; 32]; + let (pk_bytes, sk_buf) = + MLDSA::key_gen_internal(MLDSAParameter::MLDSA44, &xi).expect("key_gen_internal failed"); + + let message = b"Hello, ML-DSA-44!"; + let m_prime = { + let mut v = Vec::new(); + v.push(0x00u8); // domain_sep + v.push(0u8); // |ctx| = 0 + v.extend_from_slice(message); + v + }; + let rnd = [0u8; 32]; // 결정론적 서명 + + let sig = MLDSA::sign_internal(MLDSAParameter::MLDSA44, &sk_buf, &m_prime, &rnd) + .expect("sign_internal failed"); + + assert_eq!(sig.len(), 2420, "서명 길이 불일치"); + + let ok = + MLDSA::verify_internal(MLDSAParameter::MLDSA44, &pk_bytes, &m_prime, sig.as_slice()) + .expect("verify_internal failed"); + + assert!(ok, "서명 검증 실패 (ML-DSA-44)"); + } + + // + // 서명 + 검증 종단 간 테스트 (ML-DSA-65) + // + + #[test] + fn test_sign_verify_roundtrip_65() { + let xi = [0xBBu8; 32]; + let (pk_bytes, sk_buf) = + MLDSA::key_gen_internal(MLDSAParameter::MLDSA65, &xi).expect("key_gen_internal failed"); + + let message = b"Hello, ML-DSA-65!"; + let m_prime = { + let mut v = Vec::new(); + v.push(0x00u8); + v.push(0u8); + v.extend_from_slice(message); + v + }; + let rnd = [0u8; 32]; + + let sig = MLDSA::sign_internal(MLDSAParameter::MLDSA65, &sk_buf, &m_prime, &rnd) + .expect("sign_internal failed"); + + assert_eq!(sig.len(), 3309, "서명 길이 불일치"); + + let ok = + MLDSA::verify_internal(MLDSAParameter::MLDSA65, &pk_bytes, &m_prime, sig.as_slice()) + .expect("verify_internal failed"); + + assert!(ok, "서명 검증 실패 (ML-DSA-65)"); + } + + // + // 변조된 메시지 검증 거부 테스트 + // + + #[test] + fn test_verify_rejects_tampered_message_44() { + let xi = [0xCCu8; 32]; + let (pk_bytes, sk_buf) = + MLDSA::key_gen_internal(MLDSAParameter::MLDSA44, &xi).expect("key_gen_internal failed"); + + let m_prime_orig = b"\x00\x00Hello"; + let rnd = [0u8; 32]; + + let sig = MLDSA::sign_internal(MLDSAParameter::MLDSA44, &sk_buf, m_prime_orig, &rnd) + .expect("sign_internal failed"); + + let m_prime_tampered = b"\x00\x00World"; + let ok = MLDSA::verify_internal( + MLDSAParameter::MLDSA44, + &pk_bytes, + m_prime_tampered, + sig.as_slice(), + ) + .expect("verify_internal error"); + + assert!(!ok, "변조된 메시지가 검증을 통과해서는 안 됩니다"); + } + + // + // 변조된 서명 검증 거부 테스트 + // + + #[test] + fn test_verify_rejects_tampered_signature_44() { + let xi = [0xDDu8; 32]; + let (pk_bytes, sk_buf) = + MLDSA::key_gen_internal(MLDSAParameter::MLDSA44, &xi).expect("key_gen_internal failed"); + + let m_prime = b"\x00\x00TestMessage"; + let rnd = [0u8; 32]; + + let mut sig = MLDSA::sign_internal(MLDSAParameter::MLDSA44, &sk_buf, m_prime, &rnd) + .expect("sign_internal failed"); + + // 서명 중간 바이트 비트 플립 + sig.as_mut_slice()[100] ^= 0xFF; + + let ok = + MLDSA::verify_internal(MLDSAParameter::MLDSA44, &pk_bytes, m_prime, sig.as_slice()) + .expect("verify_internal error"); + + assert!(!ok, "변조된 서명이 검증을 통과해서는 안 됩니다"); + } +} From f70296b94fce7985c2acced63d80264db188970d Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:10:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?NTT=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto/mldsa/tests/ntt_c_test.rs | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 crypto/mldsa/tests/ntt_c_test.rs diff --git a/crypto/mldsa/tests/ntt_c_test.rs b/crypto/mldsa/tests/ntt_c_test.rs new file mode 100644 index 0000000..d7ad502 --- /dev/null +++ b/crypto/mldsa/tests/ntt_c_test.rs @@ -0,0 +1,84 @@ +#[cfg(test)] +mod tests { + /// FIPS 204 (ML-DSA) 모듈러스 + const Q: i64 = 8380417; + + /// Q에 대한 512차 원시 단위근 + const ZETA: i64 = 1753; + + /// 몽고메리 변환 상수 R = 2^32 mod Q + const R: i64 = (1i64 << 32) % Q; + + /// 8비트 정수의 비트 반전 (Bit Reversal) + fn bit_reverse(mut n: u8) -> u8 { + let mut res = 0; + for _ in 0..8 { + res = (res << 1) | (n & 1); + n >>= 1; + } + res + } + + /// 모듈러 거듭제곱 (Base^Exp mod Q) + fn mod_pow(mut base: i64, mut exp: i64) -> i64 { + let mut res = 1; + base %= Q; + while exp > 0 { + if exp % 2 == 1 { + res = (res * base) % Q; + } + base = (base * base) % Q; + exp /= 2; + } + res + } + + /// 모듈러 역원 계산 (Fermat's Little Theorem: a^(Q-2) mod Q) + fn mod_inv(n: i64) -> i64 { + mod_pow(n, Q - 2) + } + + #[test] + fn generate_ntt_constants() { + println!("// === ZETAS 생성 결과 ==="); + println!("pub const ZETAS: [Fq; 256] = ["); + for i in 0..256 { + // 인덱스를 8비트 반전 + let brv = bit_reverse(i as u8) as i64; + + // zeta^brv mod Q + let z = mod_pow(ZETA, brv); + + // 몽고메리 도메인으로 변환: (z * R) mod Q + let z_mont = (z * R) % Q; + + println!(" Fq::new({}), // ZETAS[{}]", z_mont, i); + } + println!("];\n"); + + println!("// === INTT_ZETAS 생성 결과 ==="); + println!("pub const INTT_ZETAS: [Fq; 256] = ["); + + // INTT는 음수 지수(-brv)를 사용하므로, ZETA의 역원을 구합니다. + let zeta_inv = mod_inv(ZETA); + + for i in 0..256 { + let brv = bit_reverse(i as u8) as i64; + + // (zeta^-1)^brv mod Q + let z_inv = mod_pow(zeta_inv, brv); + + // 몽고메리 도메인으로 변환 + let z_inv_mont = (z_inv * R) % Q; + + println!(" Fq::new({}), // INTT_ZETAS[{}]", z_inv_mont, i); + } + println!("];"); + + // INV_N_MONT 검증 (256^-1 * 2^32 mod Q) + let n_inv = mod_inv(256); + let n_inv_mont = (n_inv * R) % Q; + println!("\n// === INV_N_MONT ==="); + println!("pub const INV_N_MONT: Fq = Fq::new({});", n_inv_mont); + } +} From ce0db98ce4b1819022b3fa897f4607c1de9b14d1 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:10:45 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EC=8B=9C=EA=B7=B8?= =?UTF-8?q?=EB=8B=88=EC=B2=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto/mldsa/tests/mldsa_test.rs | 182 +++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 crypto/mldsa/tests/mldsa_test.rs diff --git a/crypto/mldsa/tests/mldsa_test.rs b/crypto/mldsa/tests/mldsa_test.rs new file mode 100644 index 0000000..d219983 --- /dev/null +++ b/crypto/mldsa/tests/mldsa_test.rs @@ -0,0 +1,182 @@ +use entlib_native_mldsa::{HashDRBGRng, MLDSA, MLDSAParameter}; + +fn make_rng() -> HashDRBGRng { + HashDRBGRng::new_from_os(None).expect("OS 엔트로피 소스 초기화 실패") +} + +// +// 키 생성 — 길이 / 파라미터 셋 내장 검증 +// + +#[test] +fn test_keygen_lengths_44() { + let param = MLDSAParameter::MLDSA44; + let (pk, sk) = MLDSA::key_gen(param, &mut make_rng()).expect("key_gen 실패"); + assert_eq!(pk.len(), param.pk_len()); + assert_eq!(sk.len(), param.sk_len()); + assert_eq!(pk.param(), param); + assert_eq!(sk.param(), param); +} + +#[test] +fn test_keygen_lengths_65() { + let param = MLDSAParameter::MLDSA65; + let (pk, sk) = MLDSA::key_gen(param, &mut make_rng()).expect("key_gen 실패"); + assert_eq!(pk.len(), param.pk_len()); + assert_eq!(sk.len(), param.sk_len()); +} + +#[test] +fn test_keygen_lengths_87() { + let param = MLDSAParameter::MLDSA87; + let (pk, sk) = MLDSA::key_gen(param, &mut make_rng()).expect("key_gen 실패"); + assert_eq!(pk.len(), param.pk_len()); + assert_eq!(sk.len(), param.sk_len()); +} + +// +// 서명 + 검증 라운드트립 +// + +#[test] +fn test_sign_verify_roundtrip_44() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).expect("key_gen 실패"); + + let sig = MLDSA::sign(&sk, b"Hello, ML-DSA-44!", b"test-context", &mut rng).expect("sign 실패"); + assert_eq!(sig.len(), MLDSAParameter::MLDSA44.sig_len()); + + let ok = MLDSA::verify(&pk, b"Hello, ML-DSA-44!", sig.as_slice(), b"test-context") + .expect("verify 실패"); + assert!(ok); +} + +#[test] +fn test_sign_verify_roundtrip_65() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA65, &mut rng).expect("key_gen 실패"); + + let sig = MLDSA::sign(&sk, b"Hello, ML-DSA-65!", b"", &mut rng).expect("sign 실패"); + assert_eq!(sig.len(), MLDSAParameter::MLDSA65.sig_len()); + + let ok = MLDSA::verify(&pk, b"Hello, ML-DSA-65!", sig.as_slice(), b"").expect("verify 실패"); + assert!(ok); +} + +#[test] +fn test_sign_verify_roundtrip_87() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA87, &mut rng).expect("key_gen 실패"); + + let sig = + MLDSA::sign(&sk, b"Hello, ML-DSA-87!", b"security-level-5", &mut rng).expect("sign 실패"); + assert_eq!(sig.len(), MLDSAParameter::MLDSA87.sig_len()); + + let ok = MLDSA::verify( + &pk, + b"Hello, ML-DSA-87!", + sig.as_slice(), + b"security-level-5", + ) + .expect("verify 실패"); + assert!(ok); +} + +// +// 변조된 메시지 거부 +// + +#[test] +fn test_verify_rejects_tampered_message_44() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).expect("key_gen 실패"); + let sig = MLDSA::sign(&sk, b"original", b"", &mut rng).expect("sign 실패"); + let ok = MLDSA::verify(&pk, b"tampered", sig.as_slice(), b"").expect("verify 실패"); + assert!(!ok); +} + +#[test] +fn test_verify_rejects_tampered_message_65() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA65, &mut rng).expect("key_gen 실패"); + let sig = MLDSA::sign(&sk, b"original", b"", &mut rng).expect("sign 실패"); + let ok = MLDSA::verify(&pk, b"tampered", sig.as_slice(), b"").expect("verify 실패"); + assert!(!ok); +} + +#[test] +fn test_verify_rejects_tampered_message_87() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA87, &mut rng).expect("key_gen 실패"); + let sig = MLDSA::sign(&sk, b"original", b"", &mut rng).expect("sign 실패"); + let ok = MLDSA::verify(&pk, b"tampered", sig.as_slice(), b"").expect("verify 실패"); + assert!(!ok); +} + +// +// 변조된 서명 거부 +// + +#[test] +fn test_verify_rejects_tampered_signature_44() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).expect("key_gen 실패"); + let mut sig = MLDSA::sign(&sk, b"test message", b"", &mut rng).expect("sign 실패"); + sig.as_mut_slice()[100] ^= 0xFF; + let ok = MLDSA::verify(&pk, b"test message", sig.as_slice(), b"").expect("verify 실패"); + assert!(!ok); +} + +#[test] +fn test_verify_rejects_tampered_signature_65() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA65, &mut rng).expect("key_gen 실패"); + let mut sig = MLDSA::sign(&sk, b"test message", b"", &mut rng).expect("sign 실패"); + sig.as_mut_slice()[200] ^= 0xFF; + let ok = MLDSA::verify(&pk, b"test message", sig.as_slice(), b"").expect("verify 실패"); + assert!(!ok); +} + +#[test] +fn test_verify_rejects_tampered_signature_87() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA87, &mut rng).expect("key_gen 실패"); + let mut sig = MLDSA::sign(&sk, b"test message", b"", &mut rng).expect("sign 실패"); + sig.as_mut_slice()[300] ^= 0xFF; + let ok = MLDSA::verify(&pk, b"test message", sig.as_slice(), b"").expect("verify 실패"); + assert!(!ok); +} + +// +// 컨텍스트 불일치 거부 +// + +#[test] +fn test_verify_rejects_wrong_context() { + let mut rng = make_rng(); + let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).expect("key_gen 실패"); + let sig = MLDSA::sign(&sk, b"test message", b"ctx-A", &mut rng).expect("sign 실패"); + let ok = MLDSA::verify(&pk, b"test message", sig.as_slice(), b"ctx-B").expect("verify 실패"); + assert!(!ok); +} + +// +// 컨텍스트 길이 초과 오류 +// + +#[test] +fn test_sign_rejects_oversized_context() { + let mut rng = make_rng(); + let (_pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).expect("key_gen 실패"); + let long_ctx = vec![0u8; 256]; + assert!(MLDSA::sign(&sk, b"msg", &long_ctx, &mut rng).is_err()); +} + +#[test] +fn test_verify_rejects_oversized_context() { + let mut rng = make_rng(); + let (pk, _sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).expect("key_gen 실패"); + let dummy_sig = vec![0u8; MLDSAParameter::MLDSA44.sig_len()]; + let long_ctx = vec![0u8; 256]; + assert!(MLDSA::verify(&pk, b"msg", &dummy_sig, &long_ctx).is_err()); +}