From 9d3bf28e231cbc148120dcf10a43d58c547c5346 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:39:10 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + README_EN.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 37adf99..4d2db15 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Python이나 JPMS(Java Platform Module System)와 일관된 모듈 관리, 캡 - [ ] TLS 1.3 - [ ] [`draft-ietf-tls-ecdhe-mlkem`](https://datatracker.ietf.org/doc/draft-ietf-tls-ecdhe-mlkem/)에 따른 X25519MLKEM768 +- [ ] X9.146 QTLS 확장 표준 ## 인증 및 규정 준수 필요 diff --git a/README_EN.md b/README_EN.md index 1c1f399..3d57ca3 100644 --- a/README_EN.md +++ b/README_EN.md @@ -59,7 +59,7 @@ We need to implement a variety of supported classic cryptographic algorithm modu - [ ] PEM/DER Serializer - PKC Standard Pipeline - [ ] PKCS #8 - - [ ] PKCS #11 + - [PKCS #11](https://docs.oasis-open.org/pkcs11/pkcs11-base/v2.40/os/pkcs11-base-v2.40-os.html) - [ ] C-API FFI Mapping - [ ] Dynamic Loader (System Call-based) @@ -73,6 +73,7 @@ Additionally, the following TLS features must be supported. - [ ] TLS 1.3 - [ ] X25519MLKEM768 in accordance with `draft-ietf-tls-ecdhe-mlkem` +- [ ] X9.146 QTLS Extension Standard ## Certification and Compliance Required From 07a4723de46e82a1967fb0b97b9dc0163c8be7bd Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:06:05 +0900 Subject: [PATCH 2/7] =?UTF-8?q?PBKDF2=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto/pbkdf2/Cargo.toml | 13 ++++ crypto/pbkdf2/src/lib.rs | 10 +++ crypto/pbkdf2/src/pbkdf2.rs | 146 ++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 crypto/pbkdf2/Cargo.toml create mode 100644 crypto/pbkdf2/src/lib.rs create mode 100644 crypto/pbkdf2/src/pbkdf2.rs diff --git a/crypto/pbkdf2/Cargo.toml b/crypto/pbkdf2/Cargo.toml new file mode 100644 index 0000000..3b3017e --- /dev/null +++ b/crypto/pbkdf2/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "entlib-native-pbkdf2" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +entlib-native-secure-buffer.workspace = true +entlib-native-hmac.workspace = true + +[dev-dependencies] +entlib-native-hex.workspace = true diff --git a/crypto/pbkdf2/src/lib.rs b/crypto/pbkdf2/src/lib.rs new file mode 100644 index 0000000..b58f872 --- /dev/null +++ b/crypto/pbkdf2/src/lib.rs @@ -0,0 +1,10 @@ +#![no_std] + +extern crate alloc; + +mod pbkdf2; + +pub use pbkdf2::{ + Pbkdf2Error, PBKDF2HMACSHA3_224, PBKDF2HMACSHA3_256, PBKDF2HMACSHA3_384, PBKDF2HMACSHA3_512, + PBKDF2HMACSHA224, PBKDF2HMACSHA256, PBKDF2HMACSHA384, PBKDF2HMACSHA512, +}; diff --git a/crypto/pbkdf2/src/pbkdf2.rs b/crypto/pbkdf2/src/pbkdf2.rs new file mode 100644 index 0000000..3afae78 --- /dev/null +++ b/crypto/pbkdf2/src/pbkdf2.rs @@ -0,0 +1,146 @@ +use core::ptr::write_volatile; +use entlib_native_hmac::{ + HMACSHA3_224, HMACSHA3_256, HMACSHA3_384, HMACSHA3_512, HMACSHA224, HMACSHA256, HMACSHA384, + HMACSHA512, +}; +use entlib_native_secure_buffer::SecureBuffer; + +// NIST SP 800-132 Section 5.1: Salt 최소 길이 = 128 bits +const MIN_SALT_LEN: usize = 16; +// NIST SP 800-132 Section 5.2: 반복 횟수 최소 권고값 +const MIN_ITERATIONS: u32 = 1_000; + +/// NIST SP 800-132 PBKDF2 연산 중 발생할 수 있는 오류 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Pbkdf2Error { + /// Salt 길이가 NIST SP 800-132 Section 5.1 요구사항(최소 128 bits / 16 bytes) 미달 + WeakSalt, + /// 반복 횟수가 NIST SP 800-132 Section 5.2 권고값(최소 1,000) 미달 + InsufficientIterations, + /// DK 길이가 0이거나 NIST SP 800-132 최대값((2^32 - 1) * hLen) 초과 + InvalidDkLength, + /// 패스워드 길이가 NIST SP 800-107r1 최소 키 길이(112 bits / 14 bytes) 미달 + WeakPassword, + /// 내부 HMAC 연산 실패 + HmacError, +} + +macro_rules! impl_pbkdf2 { + ($struct_name:ident, $hmac_type:ty, $hash_len:expr) => { + /// NIST SP 800-132를 준수하는 PBKDF2 인스턴스 + pub struct $struct_name; + + impl Default for $struct_name { + fn default() -> Self { + Self::new() + } + } + + impl $struct_name { + pub const HASH_LEN: usize = $hash_len; + /// NIST SP 800-132 Section 5.3: DK 최대 길이 = (2^32 - 1) * hLen + pub const MAX_DK_LEN: u64 = (u32::MAX as u64) * ($hash_len as u64); + + pub fn new() -> Self { + Self + } + + /// NIST SP 800-132 Section 5: PBKDF2 키 유도 + /// + /// `password`로부터 `dk`에 파생 키를 출력합니다. + /// + /// # Arguments + /// - `password` - 패스워드 (SecureBuffer, 최소 112 bits / 14 bytes) + /// - `salt` - 솔트 (최소 128 bits / 16 bytes, NIST SP 800-132 Section 5.1) + /// - `iterations` - PRF 반복 횟수 (최소 1,000, NIST SP 800-132 Section 5.2) + /// - `dk` - 파생 키를 저장할 출력 버퍼 (최대 (2^32-1) * hLen bytes) + /// + /// # Security Note + /// 중간값(U 블록, T 블록)은 스택에서 연산 후 `write_volatile`로 강제 소거됩니다. + /// 패스워드는 호출자가 `SecureBuffer`로 관리해야 합니다. + pub fn derive_key( + &self, + password: &SecureBuffer, + salt: &[u8], + iterations: u32, + dk: &mut [u8], + ) -> Result<(), Pbkdf2Error> { + if salt.len() < MIN_SALT_LEN { + return Err(Pbkdf2Error::WeakSalt); + } + if iterations < MIN_ITERATIONS { + return Err(Pbkdf2Error::InsufficientIterations); + } + let dk_len = dk.len(); + if dk_len == 0 || dk_len as u64 > Self::MAX_DK_LEN { + return Err(Pbkdf2Error::InvalidDkLength); + } + + let block_count = dk_len.div_ceil(Self::HASH_LEN); + let mut offset: usize = 0; + + for i in 0..block_count { + let block_index = (i as u32).wrapping_add(1); + let copy_len = core::cmp::min(Self::HASH_LEN, dk_len - offset); + + // F(Password, Salt, c, i) = U_1 XOR U_2 XOR ... XOR U_c + let mut t = [0u8; $hash_len]; + let mut u = [0u8; $hash_len]; + + // U_1 = PRF(Password, Salt || INT(i)) + { + let mut hmac = <$hmac_type>::new(password.as_slice()).map_err(|e| { + use entlib_native_hmac::HmacError; + match e { + HmacError::WeakKeyLength => Pbkdf2Error::WeakPassword, + _ => Pbkdf2Error::HmacError, + } + })?; + hmac.update(salt); + hmac.update(&block_index.to_be_bytes()); + let mac = hmac.finalize().map_err(|_| Pbkdf2Error::HmacError)?; + u.copy_from_slice(mac.as_slice()); + } + t.copy_from_slice(&u); + + // U_j = PRF(Password, U_{j-1}) for j = 2..=c + for _ in 1..iterations { + let mut hmac = + <$hmac_type>::new(password.as_slice()).map_err(|_| Pbkdf2Error::HmacError)?; + hmac.update(&u); + let mac = hmac.finalize().map_err(|_| Pbkdf2Error::HmacError)?; + u.copy_from_slice(mac.as_slice()); + + // XOR 누산: 상수-시간 연산 + for j in 0..$hash_len { + t[j] ^= u[j]; + } + } + + dk[offset..offset + copy_len].copy_from_slice(&t[..copy_len]); + offset += copy_len; + + // 중간값 강제 소거 + for byte in &mut t { + unsafe { write_volatile(byte, 0) }; + } + for byte in &mut u { + unsafe { write_volatile(byte, 0) }; + } + } + + Ok(()) + } + } + }; +} + +impl_pbkdf2!(PBKDF2HMACSHA224, HMACSHA224, 28); +impl_pbkdf2!(PBKDF2HMACSHA256, HMACSHA256, 32); +impl_pbkdf2!(PBKDF2HMACSHA384, HMACSHA384, 48); +impl_pbkdf2!(PBKDF2HMACSHA512, HMACSHA512, 64); + +impl_pbkdf2!(PBKDF2HMACSHA3_224, HMACSHA3_224, 28); +impl_pbkdf2!(PBKDF2HMACSHA3_256, HMACSHA3_256, 32); +impl_pbkdf2!(PBKDF2HMACSHA3_384, HMACSHA3_384, 48); +impl_pbkdf2!(PBKDF2HMACSHA3_512, HMACSHA3_512, 64); \ No newline at end of file From ef1c5aca01f50291778a7e4fdcbfef3d5a3dd790 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:01:25 +0900 Subject: [PATCH 3/7] =?UTF-8?q?PBKDF2=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/pbkdf2/tests/pbkdf2_test.rs | 81 ++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 crypto/pbkdf2/tests/pbkdf2_test.rs diff --git a/crypto/pbkdf2/tests/pbkdf2_test.rs b/crypto/pbkdf2/tests/pbkdf2_test.rs new file mode 100644 index 0000000..0ac8a0b --- /dev/null +++ b/crypto/pbkdf2/tests/pbkdf2_test.rs @@ -0,0 +1,81 @@ +#[cfg(test)] +mod tests { + extern crate std; + use entlib_native_pbkdf2::*; + use entlib_native_secure_buffer::SecureBuffer; + + fn make_password(bytes: &[u8]) -> SecureBuffer { + let mut buf = SecureBuffer::new_owned(bytes.len()).unwrap(); + buf.as_mut_slice().copy_from_slice(bytes); + buf + } + + // RFC 7914 Section 11 / NIST CAVP PBKDF2 테스트 벡터 (HMAC-SHA256) + #[test] + fn pbkdf2_hmacsha256_rfc7914_vector() { + // Password: "passwd" (6 bytes → HMAC WeakKeyLength 오류 방지 위해 패딩) + // NIST SP 800-107r1: 최소 키 길이 14 bytes + // 대신 NIST CAVP 공식 벡터 사용: + // https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program + // + // COUNT = 0 + // PRF = HMAC_SHA256 + // Password = 70617373776f7264 ("password", 8 bytes → 14 bytes 미달 → WeakPassword 반환 확인) + let password = make_password(b"password"); + let salt = [0x73u8; 16]; // 16 bytes salt + let mut dk = [0u8; 32]; + let result = PBKDF2HMACSHA256::new().derive_key(&password, &salt, 1000, &mut dk); + assert_eq!(result, Err(Pbkdf2Error::WeakPassword)); + } + + #[test] + fn pbkdf2_hmacsha256_valid_password() { + // 14 bytes 이상 패스워드로 정상 동작 확인 + // Python 검증: + // import hashlib + // hashlib.pbkdf2_hmac('sha256', b'passwordpassword', b'saltsaltsaltsalt', 1000, 32).hex() + let password = make_password(b"passwordpassword"); // 16 bytes + let salt = b"saltsaltsaltsalt"; // 16 bytes + let mut dk = [0u8; 32]; + PBKDF2HMACSHA256::new() + .derive_key(&password, salt, 1000, &mut dk) + .unwrap(); + + // Python: hashlib.pbkdf2_hmac('sha256', b'passwordpassword', b'saltsaltsaltsalt', 1000, 32).hex() + // = f1dbae96c847de211bff540451f3f62b35c42545dcb7b4ff2b0f2920555c37d0 + let expected = [ + 0xf1u8, 0xdb, 0xae, 0x96, 0xc8, 0x47, 0xde, 0x21, + 0x1b, 0xff, 0x54, 0x04, 0x51, 0xf3, 0xf6, 0x2b, + 0x35, 0xc4, 0x25, 0x45, 0xdc, 0xb7, 0xb4, 0xff, + 0x2b, 0x0f, 0x29, 0x20, 0x55, 0x5c, 0x37, 0xd0, + ]; + assert_eq!(dk, expected); + } + + #[test] + fn pbkdf2_weak_salt_rejected() { + let password = make_password(b"passwordpassword"); + let salt = [0u8; 15]; // 15 bytes → 미달 + let mut dk = [0u8; 32]; + let result = PBKDF2HMACSHA256::new().derive_key(&password, &salt, 1000, &mut dk); + assert_eq!(result, Err(Pbkdf2Error::WeakSalt)); + } + + #[test] + fn pbkdf2_insufficient_iterations_rejected() { + let password = make_password(b"passwordpassword"); + let salt = [0u8; 16]; + let mut dk = [0u8; 32]; + let result = PBKDF2HMACSHA256::new().derive_key(&password, &salt, 999, &mut dk); + assert_eq!(result, Err(Pbkdf2Error::InsufficientIterations)); + } + + #[test] + fn pbkdf2_zero_dk_len_rejected() { + let password = make_password(b"passwordpassword"); + let salt = [0u8; 16]; + let mut dk = []; + let result = PBKDF2HMACSHA256::new().derive_key(&password, &salt, 1000, &mut dk); + assert_eq!(result, Err(Pbkdf2Error::InvalidDkLength)); + } +} From 4d9bebf27cba420d1e8f43b51a02cfba7fe7cee3 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:01:58 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EB=A3=A8=ED=8A=B8=20PBKDF2=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index c1b4e65..020001c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ entlib-native-hmac = { path = "crypto/hmac", version = entlib-native-sha2 = { path = "crypto/sha2", version = "2.0.0" } entlib-native-sha3 = { path = "crypto/sha3", version = "2.0.0" } entlib-native-mldsa = { path = "crypto/mldsa", version = "2.0.0" } +entlib-native-pbkdf2 = { path = "crypto/pbkdf2", version = "2.0.0" } entlib-native-chacha20 = { path = "crypto/chacha20", version = "2.0.0" } entlib-native-key-establishment = { path = "crypto/key-establishment", version = "2.0.0" } entlib-native-digital-signature = { path = "crypto/digital-signature", version = "2.0.0" } From 48ad1a15914a45e5d0c50d9cec7f00112d9730d0 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:27:28 +0900 Subject: [PATCH 5/7] =?UTF-8?q?AES-256=20CBC,=20GCM(HMAC-SHA-256)=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=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 --- Cargo.toml | 1 + crypto/aes/Cargo.toml | 13 ++ crypto/aes/src/aes.rs | 252 ++++++++++++++++++++++++++++++++++ crypto/aes/src/cbc.rs | 254 +++++++++++++++++++++++++++++++++++ crypto/aes/src/error.rs | 14 ++ crypto/aes/src/gcm.rs | 200 +++++++++++++++++++++++++++ crypto/aes/src/ghash.rs | 132 ++++++++++++++++++ crypto/aes/src/lib.rs | 14 ++ crypto/aes/tests/aes_test.rs | 167 +++++++++++++++++++++++ 9 files changed, 1047 insertions(+) create mode 100644 crypto/aes/Cargo.toml create mode 100644 crypto/aes/src/aes.rs create mode 100644 crypto/aes/src/cbc.rs create mode 100644 crypto/aes/src/error.rs create mode 100644 crypto/aes/src/gcm.rs create mode 100644 crypto/aes/src/ghash.rs create mode 100644 crypto/aes/src/lib.rs create mode 100644 crypto/aes/tests/aes_test.rs diff --git a/Cargo.toml b/Cargo.toml index 020001c..249ffc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ entlib-native-secure-buffer = { path = "core/secure-buffer", version = "2.0.0" } entlib-native-constant-time = { path = "core/constant-time", version = "2.0.0" } ### INTERNAL CRYPTO DEPENDENCIES ### entlib-native-tls = { path = "crypto/tls", version = "2.0.0" } +entlib-native-aes = { path = "crypto/aes", version = "2.0.0" } entlib-native-hkdf = { path = "crypto/hkdf", version = "2.0.0" } entlib-native-hmac = { path = "crypto/hmac", version = "2.0.0" } entlib-native-sha2 = { path = "crypto/sha2", version = "2.0.0" } diff --git a/crypto/aes/Cargo.toml b/crypto/aes/Cargo.toml new file mode 100644 index 0000000..bd51dea --- /dev/null +++ b/crypto/aes/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "entlib-native-aes" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +entlib-native-secure-buffer.workspace = true +entlib-native-constant-time.workspace = true +entlib-native-hmac.workspace = true + +[dev-dependencies] diff --git a/crypto/aes/src/aes.rs b/crypto/aes/src/aes.rs new file mode 100644 index 0000000..1db7e33 --- /dev/null +++ b/crypto/aes/src/aes.rs @@ -0,0 +1,252 @@ +use core::ptr::write_volatile; + +pub type Block = [u8; 16]; + +// AES-256 field 연산: x^8 + x^4 + x^3 + x + 1 (0x11b) +#[inline(always)] +fn xtime(a: u8) -> u8 { + let mask = (a >> 7).wrapping_neg(); + (a << 1) ^ (0x1b & mask) +} + +// GF(2^8) 곱셈 — 고정 8회 반복, 분기 없음 +#[inline(always)] +fn gmul(mut a: u8, mut b: u8) -> u8 { + let mut p = 0u8; + for _ in 0..8 { + let mask = (b & 1).wrapping_neg(); + p ^= a & mask; + let hi = (a >> 7).wrapping_neg(); + a = (a << 1) ^ (0x1b & hi); + b >>= 1; + } + p +} + +// GF(2^8) 역원: a^254 = a^(11111110b), a=0이면 0 반환 (분기 없음) +#[inline(always)] +fn gf_inv(a: u8) -> u8 { + let a2 = gmul(a, a); + let a4 = gmul(a2, a2); + let a8 = gmul(a4, a4); + let a16 = gmul(a8, a8); + let a32 = gmul(a16, a16); + let a64 = gmul(a32, a32); + let a128 = gmul(a64, a64); + gmul(gmul(gmul(gmul(gmul(gmul(a128, a64), a32), a16), a8), a4), a2) +} + +// SubBytes 아핀 변환: b = a ^ rot(a,1) ^ rot(a,2) ^ rot(a,3) ^ rot(a,4) ^ 0x63 +#[inline(always)] +pub fn sub_byte(a: u8) -> u8 { + let inv = gf_inv(a); + inv ^ inv.rotate_left(1) ^ inv.rotate_left(2) ^ inv.rotate_left(3) ^ inv.rotate_left(4) ^ 0x63 +} + +// InvSubBytes: 역 아핀 후 역원 +#[inline(always)] +fn inv_sub_byte(a: u8) -> u8 { + let t = a.rotate_left(1) ^ a.rotate_left(3) ^ a.rotate_left(6) ^ 0x05; + gf_inv(t) +} + +#[inline(always)] +fn sub_word(w: u32) -> u32 { + let b = w.to_be_bytes(); + u32::from_be_bytes([sub_byte(b[0]), sub_byte(b[1]), sub_byte(b[2]), sub_byte(b[3])]) +} + +fn sub_bytes(state: &mut Block) { + for b in state.iter_mut() { + *b = sub_byte(*b); + } +} + +fn inv_sub_bytes(state: &mut Block) { + for b in state.iter_mut() { + *b = inv_sub_byte(*b); + } +} + +// 상태: 열 우선(column-major). state[col*4 + row] +// ShiftRows: row r을 왼쪽으로 r칸 회전 +fn shift_rows(state: &mut Block) { + let t = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = t; + + state.swap(2, 10); + state.swap(6, 14); + + let t = state[15]; + state[15] = state[11]; + state[11] = state[7]; + state[7] = state[3]; + state[3] = t; +} + +fn inv_shift_rows(state: &mut Block) { + let t = state[13]; + state[13] = state[9]; + state[9] = state[5]; + state[5] = state[1]; + state[1] = t; + + state.swap(2, 10); + state.swap(6, 14); + + let t = state[3]; + state[3] = state[7]; + state[7] = state[11]; + state[11] = state[15]; + state[15] = t; +} + +// MixColumns: [2 3 1 1 / 1 2 3 1 / 1 1 2 3 / 3 1 1 2] × column +fn mix_columns(state: &mut Block) { + for col in 0..4 { + let b = col * 4; + let (s0, s1, s2, s3) = (state[b], state[b + 1], state[b + 2], state[b + 3]); + state[b] = xtime(s0) ^ (xtime(s1) ^ s1) ^ s2 ^ s3; + state[b + 1] = s0 ^ xtime(s1) ^ (xtime(s2) ^ s2) ^ s3; + state[b + 2] = s0 ^ s1 ^ xtime(s2) ^ (xtime(s3) ^ s3); + state[b + 3] = (xtime(s0) ^ s0) ^ s1 ^ s2 ^ xtime(s3); + } +} + +// InvMixColumns: [14 11 13 9 / 9 14 11 13 / 13 9 14 11 / 11 13 9 14] × column +fn inv_mix_columns(state: &mut Block) { + #[inline(always)] + fn m9(a: u8) -> u8 { + xtime(xtime(xtime(a))) ^ a + } + #[inline(always)] + fn m11(a: u8) -> u8 { + xtime(xtime(xtime(a))) ^ xtime(a) ^ a + } + #[inline(always)] + fn m13(a: u8) -> u8 { + xtime(xtime(xtime(a))) ^ xtime(xtime(a)) ^ a + } + #[inline(always)] + fn m14(a: u8) -> u8 { + xtime(xtime(xtime(a))) ^ xtime(xtime(a)) ^ xtime(a) + } + + for col in 0..4 { + let b = col * 4; + let (s0, s1, s2, s3) = (state[b], state[b + 1], state[b + 2], state[b + 3]); + state[b] = m14(s0) ^ m11(s1) ^ m13(s2) ^ m9(s3); + state[b + 1] = m9(s0) ^ m14(s1) ^ m11(s2) ^ m13(s3); + state[b + 2] = m13(s0) ^ m9(s1) ^ m14(s2) ^ m11(s3); + state[b + 3] = m11(s0) ^ m13(s1) ^ m9(s2) ^ m14(s3); + } +} + +#[inline(always)] +fn add_round_key(state: &mut Block, rk: &Block) { + for i in 0..16 { + state[i] ^= rk[i]; + } +} + +// AES-256 Rcon: i/8 = 1..7 → index 0..6 +const RCON: [u32; 7] = [ + 0x01000000, 0x02000000, 0x04000000, 0x08000000, + 0x10000000, 0x20000000, 0x40000000, +]; + +// 확장 키: 15개의 라운드 키 (AES-256) +pub struct KeySchedule { + pub round_keys: [Block; 15], +} + +impl KeySchedule { + pub fn new(key: &[u8; 32]) -> Self { + let mut w = [0u32; 60]; + + for i in 0..8 { + w[i] = u32::from_be_bytes([ + key[i * 4], + key[i * 4 + 1], + key[i * 4 + 2], + key[i * 4 + 3], + ]); + } + + for i in 8..60 { + let mut temp = w[i - 1]; + if i % 8 == 0 { + temp = sub_word(temp.rotate_left(8)) ^ RCON[i / 8 - 1]; + } else if i % 8 == 4 { + temp = sub_word(temp); + } + w[i] = w[i - 8] ^ temp; + } + + let mut round_keys = [[0u8; 16]; 15]; + for rk in 0..15 { + for j in 0..4 { + let bytes = w[rk * 4 + j].to_be_bytes(); + round_keys[rk][j * 4] = bytes[0]; + round_keys[rk][j * 4 + 1] = bytes[1]; + round_keys[rk][j * 4 + 2] = bytes[2]; + round_keys[rk][j * 4 + 3] = bytes[3]; + } + } + + // w에 잔존하는 키 파생 중간값 소거 + for word in &mut w { + unsafe { write_volatile(word, 0) }; + } + + Self { round_keys } + } +} + +impl Drop for KeySchedule { + fn drop(&mut self) { + for rk in &mut self.round_keys { + for b in rk { + unsafe { write_volatile(b, 0) }; + } + } + } +} + +pub fn aes256_encrypt_block(state: &mut Block, ks: &KeySchedule) { + add_round_key(state, &ks.round_keys[0]); + for round in 1..14 { + sub_bytes(state); + shift_rows(state); + mix_columns(state); + add_round_key(state, &ks.round_keys[round]); + } + sub_bytes(state); + shift_rows(state); + add_round_key(state, &ks.round_keys[14]); +} + +/// 단일 블록 ECB 암호화 — KAT(Known Answer Test) 전용 +#[cfg_attr(not(test), allow(dead_code))] +pub fn aes256_encrypt_ecb(key: &[u8; 32], plaintext: &[u8; 16]) -> Block { + let ks = KeySchedule::new(key); + let mut state = *plaintext; + aes256_encrypt_block(&mut state, &ks); + state +} + +pub fn aes256_decrypt_block(state: &mut Block, ks: &KeySchedule) { + add_round_key(state, &ks.round_keys[14]); + for round in (1..14).rev() { + inv_shift_rows(state); + inv_sub_bytes(state); + add_round_key(state, &ks.round_keys[round]); + inv_mix_columns(state); + } + inv_shift_rows(state); + inv_sub_bytes(state); + add_round_key(state, &ks.round_keys[0]); +} diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs new file mode 100644 index 0000000..72a7ea5 --- /dev/null +++ b/crypto/aes/src/cbc.rs @@ -0,0 +1,254 @@ +use core::ptr::write_volatile; +use entlib_native_constant_time::traits::ConstantTimeEq; +use entlib_native_hmac::HMACSHA256; +use entlib_native_secure_buffer::SecureBuffer; + +use crate::aes::{aes256_decrypt_block, aes256_encrypt_block, KeySchedule}; +use crate::error::AESError; + +pub const CBC_IV_LEN: usize = 16; +pub const CBC_HMAC_LEN: usize = 32; + +/// CBC 암호화 출력 크기: IV(16) || 패딩된 암호문 || HMAC-SHA256(32) +pub fn cbc_output_len(plaintext_len: usize) -> usize { + let padded = (plaintext_len / 16 + 1) * 16; + CBC_IV_LEN + padded + CBC_HMAC_LEN +} + +/// CBC 복호화 최대 평문 크기 (입력에서 IV·HMAC 제거, PKCS7 최소 1바이트) +pub fn cbc_plaintext_max_len(input_len: usize) -> Option { + input_len.checked_sub(CBC_IV_LEN + CBC_HMAC_LEN + 1) +} + +// 32바이트 슬라이스 상수-시간 비교 +fn ct_eq_32(a: &[u8], b: &[u8]) -> bool { + if a.len() != 32 || b.len() != 32 { + return false; + } + let mut r = 0xFFu8; + for i in 0..32 { + r &= a[i].ct_eq(&b[i]).unwrap_u8(); + } + r == 0xFF +} + +// PKCS7 패딩 검증 (복호화 후) — HMAC 검증 통과 후에만 호출 +fn pkcs7_unpad_len(data: &[u8]) -> Result { + if data.is_empty() || data.len() % 16 != 0 { + return Err(AESError::InternalError); + } + let pad_byte = data[data.len() - 1]; + let pad_len = pad_byte as usize; + if pad_len == 0 || pad_len > 16 { + return Err(AESError::InternalError); + } + // 패딩 바이트 상수-시간 검증 + let mut valid = 0xFFu8; + for i in (data.len() - pad_len)..data.len() { + let diff = data[i] ^ pad_byte; + let not_zero = (diff | diff.wrapping_neg()) >> 7; + valid &= (not_zero ^ 1).wrapping_neg(); + } + if valid == 0xFF { + Ok(data.len() - pad_len) + } else { + Err(AESError::InternalError) + } +} + +// Q. T. Felix NOTE: 설계 중에 알아차린건데, HMAC-SHA-256 단일로 계산되도록 구현해버림 +// fxxk@@@ ^^7 일단 커밋 하고 pr로 수정 +// + +/// AES-256-CBC + PKCS7 + Encrypt-then-MAC(HMAC-SHA256) +/// +/// CBC 모드는 단독으로 사용할 수 없습니다. 암호문 전체(IV 포함)에 +/// 대해 HMAC-SHA256 무결성 태그를 붙여야 합니다. +pub struct AES256CBCHmac; + +impl AES256CBCHmac { + /// CBC-HMAC 암호화 + /// + /// # Arguments + /// - `enc_key` — 256비트(32 bytes) AES 암호화 키 + /// - `mac_key` — HMAC-SHA256 무결성 키 (최소 14 bytes, 권장 32 bytes) + /// - `iv` — 128비트(16 bytes) 초기화 벡터 (각 메시지마다 고유해야 함) + /// - `plaintext` — 평문 + /// - `output` — 출력 버퍼, 최소 `cbc_output_len(plaintext.len())` bytes + /// + /// # Returns + /// 출력에 쓰인 바이트 수 + /// + /// # Security Note + /// 출력 형식: `IV(16) || CT_padded || HMAC-SHA256(IV||CT_padded)(32)` + /// IV는 각 암호화마다 고유한 값을 사용해야 합니다(nonce-reuse 금지). + pub fn encrypt( + enc_key: &SecureBuffer, + mac_key: &SecureBuffer, + iv: &[u8; CBC_IV_LEN], + plaintext: &[u8], + output: &mut [u8], + ) -> Result { + if enc_key.len() != 32 { + return Err(AESError::InvalidKeyLength); + } + let required = cbc_output_len(plaintext.len()); + if output.len() < required { + return Err(AESError::OutputBufferTooSmall); + } + + let enc_key_arr: [u8; 32] = { + let s = enc_key.as_slice(); + [ + s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], + s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15], + s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], + s[24], s[25], s[26], s[27], s[28], s[29], s[30], s[31], + ] + }; + let ks = KeySchedule::new(&enc_key_arr); + + // IV를 출력 선두에 기록 + output[..16].copy_from_slice(iv); + + let ct_start = 16usize; + let padded_len = (plaintext.len() / 16 + 1) * 16; + let ct_end = ct_start + padded_len; + + let mut prev_block = *iv; + let full_blocks = plaintext.len() / 16; + + // 완전한 블록 암호화 + for i in 0..full_blocks { + let mut block = [0u8; 16]; + block.copy_from_slice(&plaintext[i * 16..(i + 1) * 16]); + for j in 0..16 { + block[j] ^= prev_block[j]; + } + aes256_encrypt_block(&mut block, &ks); + output[ct_start + i * 16..ct_start + (i + 1) * 16].copy_from_slice(&block); + prev_block = block; + for b in &mut block { + unsafe { write_volatile(b, 0) }; + } + } + + // 마지막 블록: 나머지 바이트 + PKCS7 패딩 + let rem = plaintext.len() - full_blocks * 16; + let pad_byte = (padded_len - plaintext.len()) as u8; + let mut last_block = [pad_byte; 16]; + last_block[..rem].copy_from_slice(&plaintext[full_blocks * 16..]); + for j in 0..16 { + last_block[j] ^= prev_block[j]; + } + aes256_encrypt_block(&mut last_block, &ks); + output[ct_start + full_blocks * 16..ct_end].copy_from_slice(&last_block); + for b in &mut last_block { + unsafe { write_volatile(b, 0) }; + } + for b in &mut prev_block { + unsafe { write_volatile(b, 0) }; + } + + // Encrypt-then-MAC: HMAC-SHA256(IV || 암호문) + let mut hmac = HMACSHA256::new(mac_key.as_slice()) + .map_err(|_| AESError::InternalError)?; + hmac.update(&output[..ct_end]); + let mac = hmac.finalize().map_err(|_| AESError::InternalError)?; + output[ct_end..ct_end + 32].copy_from_slice(mac.as_slice()); + + Ok(ct_end + 32) + } + + /// CBC-HMAC 복호화 + /// + /// # Arguments + /// - `enc_key` — 256비트(32 bytes) AES 복호화 키 + /// - `mac_key` — HMAC-SHA256 검증 키 + /// - `input` — `IV(16) || CT || HMAC(32)` 형식의 입력 + /// - `output` — 평문 출력 버퍼 + /// + /// # Returns + /// 복호화된 평문 바이트 수 + /// + /// # Security Note + /// MAC 검증에 실패하면 복호화를 수행하지 않습니다. 패딩 오라클 공격 방지. + pub fn decrypt( + enc_key: &SecureBuffer, + mac_key: &SecureBuffer, + input: &[u8], + output: &mut [u8], + ) -> Result { + if enc_key.len() != 32 { + return Err(AESError::InvalidKeyLength); + } + // 최소 크기: IV(16) + 블록 1개(16) + HMAC(32) = 64 + if input.len() < 64 || (input.len() - 48) % 16 != 0 { + return Err(AESError::InvalidInputLength); + } + + let mac_start = input.len() - 32; + let received_mac = &input[mac_start..]; + let authenticated = &input[..mac_start]; + + // MAC 검증 (먼저, Encrypt-then-MAC) + let mut hmac = HMACSHA256::new(mac_key.as_slice()) + .map_err(|_| AESError::InternalError)?; + hmac.update(authenticated); + let expected_mac = hmac.finalize().map_err(|_| AESError::InternalError)?; + + // 상수-시간 MAC 비교 + if !ct_eq_32(expected_mac.as_slice(), received_mac) { + return Err(AESError::AuthenticationFailed); + } + + // MAC 검증 통과 후 복호화 + let iv: [u8; 16] = [ + input[0], input[1], input[2], input[3], + input[4], input[5], input[6], input[7], + input[8], input[9], input[10], input[11], + input[12], input[13], input[14], input[15], + ]; + let ciphertext = authenticated; + let ct_blocks = &ciphertext[16..]; // IV 제외한 암호문 부분 + + if output.len() < ct_blocks.len() { + return Err(AESError::OutputBufferTooSmall); + } + + let enc_key_arr: [u8; 32] = { + let s = enc_key.as_slice(); + [ + s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], + s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15], + s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], + s[24], s[25], s[26], s[27], s[28], s[29], s[30], s[31], + ] + }; + let ks = KeySchedule::new(&enc_key_arr); + + let block_count = ct_blocks.len() / 16; + let mut prev_block = iv; + + for i in 0..block_count { + let mut block = [0u8; 16]; + block.copy_from_slice(&ct_blocks[i * 16..(i + 1) * 16]); + let cipher_block = block; + aes256_decrypt_block(&mut block, &ks); + for j in 0..16 { + block[j] ^= prev_block[j]; + } + output[i * 16..(i + 1) * 16].copy_from_slice(&block); + prev_block = cipher_block; + for b in &mut block { + unsafe { write_volatile(b, 0) }; + } + } + for b in &mut prev_block { + unsafe { write_volatile(b, 0) }; + } + + let plaintext_len = pkcs7_unpad_len(&output[..ct_blocks.len()])?; + Ok(plaintext_len) + } +} diff --git a/crypto/aes/src/error.rs b/crypto/aes/src/error.rs new file mode 100644 index 0000000..9198409 --- /dev/null +++ b/crypto/aes/src/error.rs @@ -0,0 +1,14 @@ +/// AES-256 연산 중 발생할 수 있는 오류 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AESError { + /// 키 길이가 256비트(32 bytes)가 아님 + InvalidKeyLength, + /// GCM 출력 버퍼 부족 + OutputBufferTooSmall, + /// GCM 태그 검증 실패 또는 CBC HMAC 검증 실패 + AuthenticationFailed, + /// CBC 입력 형식 오류 (최소 길이 미달 또는 블록 크기 불일치) + InvalidInputLength, + /// 내부 오류 (PKCS7 패딩 손상, HMAC 연산 실패 등) + InternalError, +} diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs new file mode 100644 index 0000000..48b6e3c --- /dev/null +++ b/crypto/aes/src/gcm.rs @@ -0,0 +1,200 @@ +use core::ptr::write_volatile; +use entlib_native_constant_time::traits::ConstantTimeEq; +use entlib_native_secure_buffer::SecureBuffer; + +use crate::aes::{aes256_encrypt_block, KeySchedule}; +use crate::error::AESError; +use crate::ghash::GHashState; + +// GCM은 96비트(12 bytes) nonce만 지원 (NIST SP 800-38D 권고) +pub const GCM_NONCE_LEN: usize = 12; +pub const GCM_TAG_LEN: usize = 16; + +// J0 = nonce(12) || 0x00000001 +fn build_j0(nonce: &[u8; 12]) -> [u8; 16] { + let mut j0 = [0u8; 16]; + j0[..12].copy_from_slice(nonce); + j0[15] = 0x01; + j0 +} + +// inc32: J0의 하위 32비트를 빅엔디안으로 1 증가 +fn inc32(block: &[u8; 16]) -> [u8; 16] { + let mut out = *block; + let ctr = u32::from_be_bytes([block[12], block[13], block[14], block[15]]); + let next = ctr.wrapping_add(1).to_be_bytes(); + out[12] = next[0]; + out[13] = next[1]; + out[14] = next[2]; + out[15] = next[3]; + out +} + +// GCTR: CTR 모드 암·복호화 (J0+1부터 시작) +// output.len() == data.len() 보장 +fn gctr(ks: &KeySchedule, j0: &[u8; 16], data: &[u8], output: &mut [u8]) { + let mut ctr = inc32(j0); + let mut keystream = [0u8; 16]; + let mut i = 0; + while i < data.len() { + keystream = ctr; + aes256_encrypt_block(&mut keystream, ks); + let chunk = core::cmp::min(16, data.len() - i); + for j in 0..chunk { + output[i + j] = data[i + j] ^ keystream[j]; + } + i += chunk; + ctr = inc32(&ctr); + } + for b in &mut keystream { + unsafe { write_volatile(b, 0) }; + } +} + +// 16바이트 슬라이스 상수-시간 비교 +fn ct_eq_16(a: &[u8; 16], b: &[u8; 16]) -> bool { + let mut r = 0xFFu8; + for i in 0..16 { + r &= a[i].ct_eq(&b[i]).unwrap_u8(); + } + r == 0xFF +} + +/// AES-256-GCM (AEAD) +/// +/// NIST SP 800-38D 준거. 96비트 nonce만 지원. +pub struct AES256GCM; + +impl AES256GCM { + /// GCM 암호화 + /// + /// # Arguments + /// - `key` — 256비트(32 bytes) AES 키 + /// - `nonce` — 96비트(12 bytes) nonce (반드시 유일해야 함) + /// - `aad` — 추가 인증 데이터 (암호화되지 않음) + /// - `plaintext` — 평문 + /// - `ciphertext_out` — 암호문 출력 버퍼 (`plaintext.len()` bytes) + /// - `tag_out` — 16바이트 인증 태그 출력 + /// + /// # Security Note + /// 동일한 (key, nonce) 쌍을 재사용하면 기밀성·무결성이 완전히 붕괴됩니다. + pub fn encrypt( + key: &SecureBuffer, + nonce: &[u8; GCM_NONCE_LEN], + aad: &[u8], + plaintext: &[u8], + ciphertext_out: &mut [u8], + tag_out: &mut [u8; GCM_TAG_LEN], + ) -> Result<(), AESError> { + if key.len() != 32 { + return Err(AESError::InvalidKeyLength); + } + if ciphertext_out.len() < plaintext.len() { + return Err(AESError::OutputBufferTooSmall); + } + + let key_arr: [u8; 32] = { + let s = key.as_slice(); + [ + s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], + s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15], + s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], + s[24], s[25], s[26], s[27], s[28], s[29], s[30], s[31], + ] + }; + let ks = KeySchedule::new(&key_arr); + + // H = AES_K(0^128) + let mut h_block = [0u8; 16]; + aes256_encrypt_block(&mut h_block, &ks); + + let j0 = build_j0(nonce); + + // 평문 암호화 (CTR) + gctr(&ks, &j0, plaintext, ciphertext_out); + + // GHASH(AAD, CT) + let mut ghash = GHashState::new(&h_block); + ghash.update(aad); + ghash.update(&ciphertext_out[..plaintext.len()]); + let s = ghash.finalize(aad.len() as u64, plaintext.len() as u64); + + // 태그 = E_K(J0) XOR GHASH + let mut ej0 = j0; + aes256_encrypt_block(&mut ej0, &ks); + for i in 0..16 { + tag_out[i] = ej0[i] ^ s[i]; + } + + for b in &mut ej0 { + unsafe { write_volatile(b, 0) }; + } + for b in &mut h_block { + unsafe { write_volatile(b, 0) }; + } + Ok(()) + } + + /// GCM 복호화 및 태그 검증 + /// + /// # Security Note + /// 태그 검증에 실패하면 평문을 출력하지 않습니다. 상수-시간 비교를 사용합니다. + pub fn decrypt( + key: &SecureBuffer, + nonce: &[u8; GCM_NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; GCM_TAG_LEN], + plaintext_out: &mut [u8], + ) -> Result<(), AESError> { + if key.len() != 32 { + return Err(AESError::InvalidKeyLength); + } + if plaintext_out.len() < ciphertext.len() { + return Err(AESError::OutputBufferTooSmall); + } + + let key_arr: [u8; 32] = { + let s = key.as_slice(); + [ + s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], + s[8], s[9], s[10], s[11], s[12], s[13], s[14], s[15], + s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], + s[24], s[25], s[26], s[27], s[28], s[29], s[30], s[31], + ] + }; + let ks = KeySchedule::new(&key_arr); + + let mut h_block = [0u8; 16]; + aes256_encrypt_block(&mut h_block, &ks); + + let j0 = build_j0(nonce); + + // 태그 재계산 (복호화 전) + let mut ghash = GHashState::new(&h_block); + ghash.update(aad); + ghash.update(ciphertext); + let s = ghash.finalize(aad.len() as u64, ciphertext.len() as u64); + + let mut ej0 = j0; + aes256_encrypt_block(&mut ej0, &ks); + let mut expected_tag = [0u8; 16]; + for i in 0..16 { + expected_tag[i] = ej0[i] ^ s[i]; + } + + // 상수-시간 태그 검증 — 검증 통과 전에 평문 출력 금지 + if !ct_eq_16(&expected_tag, tag) { + for b in &mut ej0 { unsafe { write_volatile(b, 0) }; } + for b in &mut h_block { unsafe { write_volatile(b, 0) }; } + return Err(AESError::AuthenticationFailed); + } + + // 태그 검증 통과 후에만 복호화 + gctr(&ks, &j0, ciphertext, plaintext_out); + + for b in &mut ej0 { unsafe { write_volatile(b, 0) }; } + for b in &mut h_block { unsafe { write_volatile(b, 0) }; } + Ok(()) + } +} diff --git a/crypto/aes/src/ghash.rs b/crypto/aes/src/ghash.rs new file mode 100644 index 0000000..2d12498 --- /dev/null +++ b/crypto/aes/src/ghash.rs @@ -0,0 +1,132 @@ +use core::ptr::write_volatile; + +// GCM GF(2^128) 곱셈: 환원 다항식 x^128 + x^7 + x^2 + x + 1 +// R = 0xE100...00 (128비트, MSB 우선) +// 128회 고정 반복 — 분기 없음, 상수-시간 보장 +#[inline(never)] +fn gf128_mul(x: &mut [u64; 2], h: &[u64; 2]) { + let mut z = [0u64; 2]; + let mut v = *h; + + for i in 0u32..128 { + let word = (i >> 6) as usize; + let bit = 63 - (i & 63); + + // x의 i번째 비트 (MSB 우선) 추출 — 마스크 트릭, 분기 없음 + let xi = ((x[word] >> bit) & 1).wrapping_neg(); + z[0] ^= v[0] & xi; + z[1] ^= v[1] & xi; + + // v를 오른쪽으로 1비트 시프트 + let lsb = v[1] & 1; + v[1] = (v[1] >> 1) | (v[0] << 63); + v[0] >>= 1; + + // LSB가 1이면 R로 XOR 환원 — 분기 없음 + let r_mask = lsb.wrapping_neg(); + v[0] ^= 0xE100000000000000u64 & r_mask; + } + + *x = z; +} + +pub struct GHashState { + h: [u64; 2], + state: [u64; 2], +} + +impl GHashState { + pub fn new(h_block: &[u8; 16]) -> Self { + let h = [ + u64::from_be_bytes([ + h_block[0], h_block[1], h_block[2], h_block[3], + h_block[4], h_block[5], h_block[6], h_block[7], + ]), + u64::from_be_bytes([ + h_block[8], h_block[9], h_block[10], h_block[11], + h_block[12], h_block[13], h_block[14], h_block[15], + ]), + ]; + Self { h, state: [0u64; 2] } + } + + fn update_block(&mut self, block: &[u8; 16]) { + let b0 = u64::from_be_bytes([ + block[0], block[1], block[2], block[3], + block[4], block[5], block[6], block[7], + ]); + let b1 = u64::from_be_bytes([ + block[8], block[9], block[10], block[11], + block[12], block[13], block[14], block[15], + ]); + self.state[0] ^= b0; + self.state[1] ^= b1; + gf128_mul(&mut self.state, &self.h); + } + + pub fn update(&mut self, data: &[u8]) { + let mut i = 0; + while i + 16 <= data.len() { + let block: [u8; 16] = [ + data[i], data[i+1], data[i+2], data[i+3], + data[i+4], data[i+5], data[i+6], data[i+7], + data[i+8], data[i+9], data[i+10], data[i+11], + data[i+12], data[i+13], data[i+14], data[i+15], + ]; + self.update_block(&block); + i += 16; + } + let rem = data.len() - i; + if rem > 0 { + let mut buf = [0u8; 16]; + buf[..rem].copy_from_slice(&data[i..]); + self.update_block(&buf); + for b in &mut buf { + unsafe { write_volatile(b, 0) }; + } + } + } + + // 인증 태그 계산: 길이 블록 처리 후 최종 GHASH 값 반환 + pub fn finalize(mut self, aad_len: u64, ct_len: u64) -> [u8; 16] { + let aad_bits = aad_len * 8; + let ct_bits = ct_len * 8; + let mut len_block = [0u8; 16]; + len_block[0] = (aad_bits >> 56) as u8; + len_block[1] = (aad_bits >> 48) as u8; + len_block[2] = (aad_bits >> 40) as u8; + len_block[3] = (aad_bits >> 32) as u8; + len_block[4] = (aad_bits >> 24) as u8; + len_block[5] = (aad_bits >> 16) as u8; + len_block[6] = (aad_bits >> 8) as u8; + len_block[7] = aad_bits as u8; + len_block[8] = (ct_bits >> 56) as u8; + len_block[9] = (ct_bits >> 48) as u8; + len_block[10] = (ct_bits >> 40) as u8; + len_block[11] = (ct_bits >> 32) as u8; + len_block[12] = (ct_bits >> 24) as u8; + len_block[13] = (ct_bits >> 16) as u8; + len_block[14] = (ct_bits >> 8) as u8; + len_block[15] = ct_bits as u8; + self.update_block(&len_block); + + let s = self.state; + let mut out = [0u8; 16]; + let hi = s[0].to_be_bytes(); + let lo = s[1].to_be_bytes(); + out[..8].copy_from_slice(&hi); + out[8..].copy_from_slice(&lo); + out + } +} + +impl Drop for GHashState { + fn drop(&mut self) { + for w in &mut self.state { + unsafe { write_volatile(w, 0) }; + } + for w in &mut self.h { + unsafe { write_volatile(w, 0) }; + } + } +} diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs new file mode 100644 index 0000000..bfb4cc3 --- /dev/null +++ b/crypto/aes/src/lib.rs @@ -0,0 +1,14 @@ +#![no_std] + +extern crate alloc; + +mod aes; +mod cbc; +mod error; +mod gcm; +mod ghash; + +pub use aes::aes256_encrypt_ecb; +pub use cbc::{cbc_output_len, cbc_plaintext_max_len, AES256CBCHmac, CBC_HMAC_LEN, CBC_IV_LEN}; +pub use error::AESError; +pub use gcm::{AES256GCM, GCM_NONCE_LEN, GCM_TAG_LEN}; diff --git a/crypto/aes/tests/aes_test.rs b/crypto/aes/tests/aes_test.rs new file mode 100644 index 0000000..b29ee74 --- /dev/null +++ b/crypto/aes/tests/aes_test.rs @@ -0,0 +1,167 @@ +#[cfg(test)] +mod tests { + extern crate std; + use std::vec; + + use entlib_native_secure_buffer::SecureBuffer; + + use entlib_native_aes::*; + + fn make_key(bytes: &[u8]) -> SecureBuffer { + let mut buf = SecureBuffer::new_owned(bytes.len()).unwrap(); + buf.as_mut_slice().copy_from_slice(bytes); + buf + } + + // NIST FIPS 197 Appendix B: AES-256 ECB 단일 블록 암호화 + #[test] + fn fips197_aes256_ecb_encrypt() { + let key: [u8; 32] = [ + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f, + ]; + let pt: [u8; 16] = [ + 0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77, + 0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff, + ]; + let expected: [u8; 16] = [ + 0x8e,0xa2,0xb7,0xca,0x51,0x67,0x45,0xbf, + 0xea,0xfc,0x49,0x90,0x4b,0x49,0x60,0x89, + ]; + assert_eq!(aes256_encrypt_ecb(&key, &pt), expected); + } + + // NIST CAVP AES-256-GCM 암호화 검증 + #[test] + fn nist_cavp_aes256_gcm_encrypt() { + let key = make_key(&[ + 0xfe,0xff,0xe9,0x92,0x86,0x65,0x73,0x1c,0x6d,0x6a,0x8f,0x94,0x67,0x30,0x83,0x08, + 0xfe,0xff,0xe9,0x92,0x86,0x65,0x73,0x1c,0x6d,0x6a,0x8f,0x94,0x67,0x30,0x83,0x08, + ]); + let nonce: [u8; 12] = [0xca,0xfe,0xba,0xbe,0xfa,0xce,0xdb,0xad,0xde,0xca,0xf8,0x88]; + let aad = [ + 0xfe,0xed,0xfa,0xce,0xde,0xad,0xbe,0xef,0xfe,0xed,0xfa,0xce, + 0xde,0xad,0xbe,0xef,0xab,0xad,0xda,0xd2u8, + ]; + let pt = [ + 0xd9,0x31,0x32,0x25,0xf8,0x84,0x06,0xe5,0xa5,0x59,0x09,0xc5,0xaf,0xf5,0x26,0x9a, + 0x86,0xa7,0xa9,0x53,0x15,0x34,0xf7,0xda,0x2e,0x4c,0x30,0x3d,0x8a,0x31,0x8a,0x72, + 0x1c,0x3c,0x0c,0x95,0x95,0x68,0x09,0x53,0x2f,0xcf,0x0e,0x24,0x49,0xa6,0xb5,0x25, + 0xb1,0x6a,0xed,0xf5,0xaa,0x0d,0xe6,0x57,0xba,0x63,0x7b,0x39u8, + ]; + let expected_ct = [ + 0x52,0x2d,0xc1,0xf0,0x99,0x56,0x7d,0x07,0xf4,0x7f,0x37,0xa3,0x2a,0x84,0x42,0x7d, + 0x64,0x3a,0x8c,0xdc,0xbf,0xe5,0xc0,0xc9,0x75,0x98,0xa2,0xbd,0x25,0x55,0xd1,0xaa, + 0x8c,0xb0,0x8e,0x48,0x59,0x0d,0xbb,0x3d,0xa7,0xb0,0x8b,0x10,0x56,0x82,0x88,0x38, + 0xc5,0xf6,0x1e,0x63,0x93,0xba,0x7a,0x0a,0xbc,0xc9,0xf6,0x62u8, + ]; + let expected_tag: [u8; 16] = [ + 0x76,0xfc,0x6e,0xce,0x0f,0x4e,0x17,0x68,0xcd,0xdf,0x88,0x53,0xbb,0x2d,0x55,0x1b, + ]; + + let mut ct_out = vec![0u8; pt.len()]; + let mut tag_out = [0u8; 16]; + AES256GCM::encrypt(&key, &nonce, &aad, &pt, &mut ct_out, &mut tag_out).unwrap(); + assert_eq!(&ct_out[..], &expected_ct[..]); + assert_eq!(tag_out, expected_tag); + } + + // GCM: 암호화 후 복호화 일치 확인 + #[test] + fn aes256_gcm_roundtrip() { + let key = make_key(&[0x42u8; 32]); + let nonce = [0x01u8; 12]; + let aad = b"entlib-native"; + let pt = b"FIPS 140-3 AES-256-GCM test message"; + + let mut ct = vec![0u8; pt.len()]; + let mut tag = [0u8; 16]; + AES256GCM::encrypt(&key, &nonce, aad, pt, &mut ct, &mut tag).unwrap(); + + let mut recovered = vec![0u8; ct.len()]; + AES256GCM::decrypt(&key, &nonce, aad, &ct, &tag, &mut recovered).unwrap(); + assert_eq!(&recovered[..], &pt[..]); + } + + // GCM: 태그 1비트 변조 시 복호화 실패 + #[test] + fn aes256_gcm_tag_tamper_rejected() { + let key = make_key(&[0x11u8; 32]); + let nonce = [0xabu8; 12]; + let pt = b"tamper test"; + let mut ct = vec![0u8; pt.len()]; + let mut tag = [0u8; 16]; + AES256GCM::encrypt(&key, &nonce, b"", pt, &mut ct, &mut tag).unwrap(); + tag[0] ^= 0x01; + let mut out = vec![0u8; ct.len()]; + let result = AES256GCM::decrypt(&key, &nonce, b"", &ct, &tag, &mut out); + assert_eq!(result, Err(AESError::AuthenticationFailed)); + } + + // CBC-HMAC NIST SP 800-38A F.2.5 + HMAC 검증 + #[test] + fn nist_aes256_cbc_hmac_encrypt() { + let enc_key = make_key(&[ + 0x60,0x3d,0xeb,0x10,0x15,0xca,0x71,0xbe,0x2b,0x73,0xae,0xf0,0x85,0x7d,0x77,0x81, + 0x1f,0x35,0x2c,0x07,0x3b,0x61,0x08,0xd7,0x2d,0x98,0x10,0xa3,0x09,0x14,0xdf,0xf4, + ]); + let mac_key = make_key(&[0x00u8; 32]); + let iv: [u8; 16] = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]; + let pt = [ + 0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a, + 0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c,0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51, + 0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11,0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xefu8, + ]; + let expected_ct = [ + 0xf5,0x8c,0x4c,0x04,0xd6,0xe5,0xf1,0xba,0x77,0x9e,0xab,0xfb,0x5f,0x7b,0xfb,0xd6, + 0x9c,0xfc,0x4e,0x96,0x7e,0xdb,0x80,0x8d,0x67,0x9f,0x77,0x7b,0xc6,0x70,0x2c,0x7d, + 0x39,0xf2,0x33,0x69,0xa9,0xd9,0xba,0xcf,0xa5,0x30,0xe2,0x63,0x04,0x23,0x14,0x61, + 0x2f,0x8d,0xa7,0x07,0x64,0x3c,0x90,0xa6,0xf7,0x32,0xb3,0xde,0x1d,0x3f,0x5c,0xeeu8, + ]; + let expected_mac: [u8; 32] = [ + 0xa2,0xfa,0xcb,0x5d,0xa7,0xd3,0x35,0x49,0xd8,0x26,0x40,0x3b,0xe7,0x39,0xd5,0xae, + 0x21,0x25,0x14,0x2c,0xc8,0x26,0xa6,0xb6,0xc9,0xfc,0x83,0x97,0x06,0x0d,0x52,0x56, + ]; + + let mut output = vec![0u8; cbc_output_len(pt.len())]; + let written = AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, &pt, &mut output).unwrap(); + assert_eq!(written, output.len()); + assert_eq!(&output[16..80], &expected_ct[..]); // IV 다음이 CT + assert_eq!(&output[80..112], &expected_mac[..]); // 마지막 32바이트가 MAC + } + + // CBC-HMAC: 암호화 후 복호화 일치 + #[test] + fn aes256_cbc_hmac_roundtrip() { + let enc_key = make_key(&[0x23u8; 32]); + let mac_key = make_key(&[0x45u8; 32]); + let iv = [0x67u8; 16]; + let pt = b"CBC-HMAC roundtrip plaintext test message!"; + + let mut output = vec![0u8; cbc_output_len(pt.len())]; + AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, pt, &mut output).unwrap(); + + let mut recovered = vec![0u8; pt.len() + 16]; + let plain_len = + AES256CBCHmac::decrypt(&enc_key, &mac_key, &output, &mut recovered).unwrap(); + assert_eq!(&recovered[..plain_len], &pt[..]); + } + + // CBC-HMAC: MAC 1비트 변조 시 복호화 실패 + #[test] + fn aes256_cbc_hmac_mac_tamper_rejected() { + let enc_key = make_key(&[0x55u8; 32]); + let mac_key = make_key(&[0x77u8; 32]); + let iv = [0x99u8; 16]; + let pt = b"tamper detection test"; + + let mut output = vec![0u8; cbc_output_len(pt.len())]; + AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, pt, &mut output).unwrap(); + let last = output.len() - 1; + output[last] ^= 0x01; + + let mut recovered = vec![0u8; pt.len() + 16]; + let result = AES256CBCHmac::decrypt(&enc_key, &mac_key, &output, &mut recovered); + assert_eq!(result, Err(AESError::AuthenticationFailed)); + } +} \ No newline at end of file From d2ac9c7c53080bf5854ba2b80a1c647dbba7ed4e Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:30:58 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto/aes/README.md | 272 ++++++++++++++++++++++++++++++++++++++++ crypto/aes/README_EN.md | 272 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 544 insertions(+) create mode 100644 crypto/aes/README.md create mode 100644 crypto/aes/README_EN.md diff --git a/crypto/aes/README.md b/crypto/aes/README.md new file mode 100644 index 0000000..8c8131d --- /dev/null +++ b/crypto/aes/README.md @@ -0,0 +1,272 @@ +# AES-256 크레이트 (entlib-native-aes) + +> Q. T. Felix (수정: 26.03.22 UTC+9) +> +> [English README](README_EN.md) + +`entlib-native-aes`는 NIST FIPS 140-3 및 Common Criteria EAL4+ 인증 요구사항을 충족하도록 설계된 AES-256 암호화 모듈입니다. **256비트 키만 지원**하며, 기밀성과 무결성을 동시에 제공하는 두 가지 승인된 운용 모드를 구현합니다. + +- **AES-256-GCM** — NIST SP 800-38D 준거 AEAD (Authenticated Encryption with Associated Data) +- **AES-256-CBC-HMAC-SHA256** — NIST SP 800-38A + Encrypt-then-MAC 구성 (CBC 단독 사용 금지) + +**이 알고리즘은 128, 192 키 길이는 의도적으로 대응하지 않습니다.** FIPS 140-3은 AES-256 사용을 권고하며, 단일 키 크기만 노출하여 잘못된 키 길이 선택으로 인한 보안 약화를 사전에 차단합니다. + +## 보안 위협 모델 + +### 캐시 타이밍 공격 (Cache-Timing Attack) + +AES의 표준 소프트웨어 구현은 SubBytes 연산을 위해 256바이트 SBox 룩업 테이블을 사용합니다. 이 접근 방식은 치명적인 취약점을 내포합니다. 공격자가 동일한 CPU 캐시를 공유하는 환경(VPS, 클라우드)에서 캐시 히트·미스 패턴으로부터 접근된 테이블 인덱스, 즉 비밀 키 바이트를 통계적으로 복원할 수 있습니다. 다니엘 번스타인(D. J. Bernsteint)의 2005년 AES 타이밍 공격은 이를 실증적으로 증명하였습니다. + +본 크레이트는 룩업 테이블을 일체 사용하지 않습니다. SubBytes는 GF(2^8) 역원 계산과 아핀 변환을 순수 산술 비트 연산으로 수행하며, 모든 연산의 실행 시간은 비밀 키 및 평문 값과 완전히 독립적입니다. + +### 패딩 오라클 공격 (Padding Oracle Attack) + +CBC 모드에서 복호화 오류 응답이 패딩 유효성에 따라 달라지면, 공격자는 적응적 선택 암호문 공격(ACCA)으로 임의 암호문을 완전히 복호화할 수 있습니다(POODLE, Lucky 13 변종). 본 구현은 **Encrypt-then-MAC** 구성을 강제하여 이 공격 벡터를 원천 차단합니다. MAC 검증이 선행되며, MAC 실패 시 복호화 연산 자체를 수행하지 않습니다. + +### GCM Nonce 재사용 (Nonce Reuse) + +GCM에서 동일한 (키, nonce) 쌍이 두 번이라도 사용되면 두 암호문의 XOR로부터 평문의 XOR이 노출되어 기밀성이 완전히 붕괴됩니다. 나아가 GHASH 다항식 방정식 풀기를 통해 인증 키 H가 복원되어 무결성도 위협받습니다. 본 크레이트는 nonce 생성 정책을 호출자에 위임하며, API 문서에 명시적 경고를 부착합니다. 프로덕션 환경에서는 `entlib-native-rng`의 `HashDRBGSHA256`로 nonce를 생성하거나, 충돌 없음이 보장된 카운터 기반 구성을 사용하십시오. + +## 보안 핵심: 상수-시간 AES 코어 + +### GF(2^8) 산술 + +AES SubBytes는 유한체 GF(2^8) = GF(2)[x] / (x^8 + x^4 + x^3 + x + 1) 위의 역원을 계산한 후 아핀 변환(Affine Transformation)을 적용합니다. + +#### xtime: GF(2^8) 에서 x 를 곱함 + +$$ \text{xtime}(a) = \begin{cases} a \ll 1 & \text{if MSB}(a) = 0 \\ (a \ll 1) \oplus \texttt{0x1b} & \text{if MSB}(a) = 1 \end{cases} $$ + +분기문 없이 구현합니다. + +$$\text{mask} = -(a \gg 7), \quad \text{xtime}(a) = (a \ll 1) \oplus (\texttt{0x1b} \land \text{mask})$$ + +`mask`는 MSB가 1이면 `0xFF`, 0이면 `0x00`이므로, 단일 `SHR`, `NEG`, `AND`, `XOR` 명령어 4개로 컴파일됩니다. + +#### gmul: GF(2^8) 곱셈 — 고정 8회 반복 + +$$\text{gmul}(a, b) = \bigoplus_{i=0}^{7} \left( a \cdot x^i \land -(b_i) \right)$$ + +여기서 $b_i$는 $b$의 $i$번째 비트입니다. `-(b & 1).wrapping_neg()`로 비트를 마스크로 변환하여 분기 없이 조건부 XOR를 수행합니다. 반복 횟수는 비밀 데이터와 무관한 고정 값 8이므로 타이밍이 일정합니다. + +#### gf_inv: GF(2^8) 역원 — 페르마의 소정리 + +유한체에서 $a \ne 0$이면 $a^{-1} = a^{2^8 - 2} = a^{254}$입니다. $a = 0$이면 $0^{254} = 0$이 자연히 반환되므로 분기가 필요하지 않습니다. + +> [!NOTE] +> **Square-and-Multiply 전개**: $254 = \texttt{11111110}_2$이므로 +> +> $$a^{254} = a^{128} \cdot a^{64} \cdot a^{32} \cdot a^{16} \cdot a^8 \cdot a^4 \cdot a^2$$ +> +> 7번의 제곱(squaring)과 6번의 곱셈으로 총 13회의 `gmul` 호출로 계산됩니다. 테이블 접근이 전혀 없으므로 캐시 타이밍 채널이 존재하지 않습니다. + +#### sub_byte: SubBytes 아핀 변환 + +역원 $a^{-1}$에 아핀 변환 $M \cdot a^{-1} + c$를 적용합니다. + +$$b_i = a^{-1}_i \oplus a^{-1}_{(i+4) \bmod 8} \oplus a^{-1}_{(i+5) \bmod 8} \oplus a^{-1}_{(i+6) \bmod 8} \oplus a^{-1}_{(i+7) \bmod 8} \oplus c_i$$ + +비트 회전으로 동치 표현합니다 ($c = \texttt{0x63}$). + +```math +\text{sub\_byte}(a) = a^{-1} \oplus \text{ROL}(a^{-1}, 1) \oplus \text{ROL}(a^{-1}, 2) \oplus \text{ROL}(a^{-1}, 3) \oplus \text{ROL}(a^{-1}, 4) \oplus \texttt{0x63} +``` + +역 SubBytes (`inv_sub_byte`)는 역 아핀 변환 후 역원을 계산합니다. + +```math +\text{inv\_sub\_byte}(a) = \text{gf\_inv}\!\left(\text{ROL}(a,1) \oplus \text{ROL}(a,3) \oplus \text{ROL}(a,6) \oplus \texttt{0x05}\right) +``` + +### 키 스케줄 (Key Schedule) + +AES-256은 32바이트 마스터 키로부터 15개의 라운드 키(각 16바이트)를 생성합니다. 키 확장에 사용되는 중간 배열 `w: [u32; 60]`은 라운드 키 추출 직후 `write_volatile`로 소거됩니다. `KeySchedule` 구조체는 `Drop` 트레이트를 구현하여, 스코프 이탈 시 240바이트 라운드 키 전체를 자동으로 강제 소거합니다. + +```rust +impl Drop for KeySchedule { + fn drop(&mut self) { + for rk in &mut self.round_keys { + for b in rk { + unsafe { write_volatile(b, 0) }; + } + } + } +} +``` + +## AES-256-GCM + +NIST SP 800-38D §7.1에 따른 구현입니다. 96비트(12 bytes) nonce만 지원합니다. 임의 길이의 IV를 허용하는 일반화 경로(GHASH를 이용한 IV 파생)는 nonce 충돌 위험을 증가시키므로 의도적으로 제외하였습니다. + +### 내부 동작 + +1. **해시 부키 생성** + - $`H = E_K(0^{128})`$ +2. **초기 카운터 블록** + - $`J_0 = \text{nonce}_{96} \| \texttt{0x00000001}_{32}`$ +3. **암호화 (GCTR)** + - $`C = \text{GCTR}_K(\text{inc}_{32}(J_0),\ P)`$ + - $`\text{inc}_{32}`$는 하위 32비트를 빅엔디안으로 1 증가시킵니다. +4. **인증 태그** + - $`T = E_K(J_0) \oplus \text{GHASH}_H(A,\ C)`$ + +여기서 GHASH는 AAD, 암호문, 길이 블록 $`[\text{len}(A)]_{64} \| [\text{len}(C)]_{64}`$를 순서대로 처리합니다. + +### GHASH: GF(2^128) 곱셈 — 상수-시간 보장 + +GCM 인증은 $\text{GF}(2^{128})$ 위에서 이루어집니다. 환원 다항식은 $f(x) = x^{128} + x^7 + x^2 + x + 1$이며, 이는 비트열 `0xE1000...0` (128비트, MSB 우선)으로 표현됩니다. + +> [!NOTE] +> **상수-시간 GF(2^128) 곱셈**: NIST SP 800-38D Algorithm 1의 표준 구현은 비밀 값에 의존하는 조건 분기를 포함합니다. 본 구현은 고정 128회 반복과 비트 마스크 트릭으로 이를 제거합니다. +> +> 각 반복에서 $X$의 $i$번째 비트 $X_i$를 마스크로 변환하여 분기 없이 누산합니다. +> +> $$\text{mask} = -(X_i), \quad Z \mathrel{⊕}= V \land \text{mask}$$ +> +> $V$의 우측 시프트 후 조건부 환원도 동일한 방식으로 처리됩니다. +> +> ```math +> \text{lsb\_mask} = -(V_{127}), \quad V_{\text{high}} \mathrel{⊕}= \texttt{0xE100...00} \land \text{lsb\_mask} +> ``` + +`GHashState`는 `Drop` 트레이트를 구현하여, 내부 상태 $Z$와 해시 부키 $H$를 `write_volatile`로 소거합니다. + +### 복호화 검증 원칙 + +복호화 시 태그를 먼저 재계산하고, `ConstantTimeEq::ct_eq()`를 사용하여 16바이트를 상수-시간으로 비교합니다. 검증에 실패하면 `AESError::AuthenticationFailed`를 반환하고 평문 출력을 일체 수행하지 않습니다. + +```rust +// 상수-시간 16바이트 비교 +let mut r = 0xFFu8; +for i in 0..16 { +r &= expected_tag[i].ct_eq(&received_tag[i]).unwrap_u8(); +} +if r != 0xFF { return Err(AESError::AuthenticationFailed); } +// 검증 통과 후에만 복호화 수행 +``` + +### API + +```rust +AES256GCM::encrypt( +key: &SecureBuffer, // 256비트 AES 키 +nonce: &[u8; 12], // 96비트 nonce (반드시 유일해야 함) +aad: &[u8], // 추가 인증 데이터 +plaintext: &[u8], +ciphertext_out: &mut [u8], // plaintext.len() bytes +tag_out: &mut [u8; 16], // 인증 태그 출력 +) -> Result<(), AESError> + +AES256GCM::decrypt( +key: &SecureBuffer, +nonce: &[u8; 12], +aad: &[u8], +ciphertext: &[u8], +tag: &[u8; 16], // 수신한 인증 태그 +plaintext_out: &mut [u8], // ciphertext.len() bytes +) -> Result<(), AESError> // 태그 불일치 시 AuthenticationFailed +``` + +> [!WARNING] +> 동일한 `(key, nonce)` 쌍을 두 번 이상 사용하면 기밀성과 무결성이 모두 파괴됩니다. nonce는 `entlib-native-rng`의 `HashDRBGSHA256`를 통해 생성하거나, 단조 증가 카운터로 관리하십시오. + +## AES-256-CBC-HMAC-SHA256 + +NIST SP 800-38A의 CBC 모드 단독 사용은 기밀성만 보장하고 무결성을 제공하지 않습니다. 본 구현은 **Encrypt-then-MAC** 구성을 강제합니다. 암호화 후 `IV || 암호문`에 HMAC-SHA256 태그를 생성하여 출력에 부착합니다. + +### 출력 형식 + +``` +┌─────────────────┬────────────────────────────────────────┬───────────────────────────────┐ +│ IV (16 B) │ Ciphertext + PKCS7 Padding (N×16 B) │ HMAC-SHA256(IV||CT) (32 B) │ +└─────────────────┴────────────────────────────────────────┴───────────────────────────────┘ +``` + +PKCS7 패딩은 항상 추가됩니다. 평문이 블록 경계에 정확히 맞아도 16바이트(`0x10` × 16)의 완전한 패딩 블록이 추가되므로, 출력 암호문의 길이는 항상 $`\lceil P / 16 \rceil + 1`$블록입니다. + +> [!NOTE] +> **PKCS7 상수-시간 검증**: 복호화 시 패딩 바이트 검증은 XOR와 비트 마스크로 수행합니다. +> +> ```math +> \begin{align} +> \text{diff}_i &= \text{data}[i] \oplus \text{pad\_byte}, \quad \text{not\_zero}_i = \frac{\text{diff}_i \mathbin{|} (-\text{diff}_i)}{2^7} \\ +> \text{valid} &= \bigwedge_{i} \overline{(\text{not\_zero}_i - 1)} \quad (\text{0xFF이면 유효}) +> \end{align} +>``` +> +> MAC 검증 통과 후에만 패딩 검증이 수행되므로, 공격자가 유효한 MAC 없이 패딩 오라클을 이용하는 것은 불가능합니다. + +### 복호화 순서 + +1. 입력 형식 검증 (최소 64바이트, 블록 크기 정렬) +2. HMAC-SHA256 재계산 → `ct_eq_32`로 상수-시간 비교 (`AESError::AuthenticationFailed` 또는 통과) +3. MAC 검증 통과 후에만 AES-256-CBC 복호화 수행 +4. PKCS7 패딩 검증 및 제거 + +### API + +```rust +AES256CBCHmac::encrypt( + enc_key: &SecureBuffer, // 256비트 AES 암호화 키 + mac_key: &SecureBuffer, // HMAC-SHA256 키 (최소 14 bytes, 권장 32 bytes) + iv: &[u8; 16], // 128비트 IV (메시지마다 고유해야 함) + plaintext: &[u8], + output: &mut [u8], // 최소 cbc_output_len(plaintext.len()) bytes +) -> Result // 출력에 쓰인 바이트 수 + +AES256CBCHmac::decrypt( + enc_key: &SecureBuffer, + mac_key: &SecureBuffer, + input: &[u8], // IV(16) || CT || HMAC(32) 형식 + output: &mut [u8], +) -> Result // 복호화된 평문 바이트 수 + +// 버퍼 크기 계산 헬퍼 +cbc_output_len(plaintext_len: usize) -> usize +cbc_plaintext_max_len(input_len: usize) -> Option +``` + +> [!IMPORTANT] +> `enc_key`와 `mac_key`는 반드시 독립적인 별개의 키를 사용해야 합니다. 동일한 키를 두 용도에 재사용하면 암호화 스킴의 안전성 증명이 무효가 됩니다. 키 파생이 필요한 경우 `entlib-native-hkdf`를 사용하여 마스터 키로부터 두 개의 독립적인 서브키를 파생하십시오. + +## 키 관리 요구사항 + +| 파라미터 | 요구사항 | 근거 | +|-----------|------------------------------|-----------------------------| +| AES 키 | 정확히 256비트 (32 bytes) | FIPS 140-3, NIST SP 800-38D | +| GCM nonce | 96비트 (12 bytes), 유일 | NIST SP 800-38D §8.2 | +| CBC IV | 128비트 (16 bytes), 각 메시지마다 고유 | NIST SP 800-38A §6.2 | +| CBC MAC 키 | AES 키와 독립, 최소 112비트 | NIST SP 800-107r1 | + +모든 키는 반드시 `entlib-native-secure-buffer`의 `SecureBuffer`로 관리하여 mlock 기반 메모리 잠금과 Drop 시 자동 소거를 보장해야 합니다. + +## 검증 + +### NIST CAVP 테스트 벡터 + +| 테스트 | 출처 | 결과 | +|--------------------|---------------------------------------|----| +| AES-256 ECB 블록 암호화 | NIST FIPS 197 Appendix B | O | +| AES-256-GCM 암호화 | NIST CAVP (OpenSSL 교차 검증) | O | +| AES-256-GCM 복호화 | 역방향 라운드트립 | O | +| AES-256-CBC 암호문 | NIST SP 800-38A F.2.5 (OpenSSL 교차 검증) | O | +| GCM 태그 1비트 변조 | 조작된 태그 → `AuthenticationFailed` | O | +| CBC MAC 1비트 변조 | 조작된 MAC → `AuthenticationFailed` | O | + +```bash +cargo test -p entlib-native-aes +``` + +> [!WARNING] +> KAT(Known Answer Test) 테스트 벡터를 엄밀하게 통과하기 위한 준비 중에 있습니다. +> +> 위 표의 근거는 테스트 벡터의 개별 테스트 블럭의 일치 여부를 검증하는 테스트 모듈 `aes_test.rs`입니다. + +## 설계 원칙 요약 + +1. **256비트 단일 키 강제** — 키 크기 선택 오류로 인한 보안 약화를 API 수준에서 차단합니다. +2. **룩업 테이블 완전 배제** — SBox를 포함한 모든 연산이 순수 산술 비트 연산으로 수행되어 캐시 타이밍 채널이 존재하지 않습니다. +3. **고정 반복 횟수** — `gmul`(8회), `gf128_mul`(128회) 등 모든 내부 루프는 비밀 데이터와 무관한 상수로 고정됩니다. +4. **Encrypt-then-MAC 강제** — CBC 단독 사용 API를 노출하지 않아 패딩 오라클 공격을 구조적으로 차단합니다. +5. **검증 후 복호화 원칙** — GCM 태그와 CBC HMAC 모두 상수-시간 검증 통과 전에 평문을 출력하지 않습니다. +6. **키 소재 즉시 소거** — `KeySchedule`, `GHashState`, 블록 연산 중간값 모두 `write_volatile`로 사용 직후 소거됩니다. diff --git a/crypto/aes/README_EN.md b/crypto/aes/README_EN.md new file mode 100644 index 0000000..22f9f3d --- /dev/null +++ b/crypto/aes/README_EN.md @@ -0,0 +1,272 @@ +# AES-256 Crate (entlib-native-aes) + +> Q. T. Felix (Modified: 26.03.22 UTC+9) +> +> [Korean README](README.md) + +`entlib-native-aes` is an AES-256 encryption module designed to meet the requirements of NIST FIPS 140-3 and Common Criteria EAL4+ certification. It **only supports 256-bit keys** and implements two approved modes of operation that provide both confidentiality and integrity. + +- **AES-256-GCM** — AEAD (Authenticated Encryption with Associated Data) compliant with NIST SP 800-38D +- **AES-256-CBC-HMAC-SHA256** — Encrypt-then-MAC configuration with NIST SP 800-38A (CBC is not used alone) + +**This algorithm intentionally does not support 128 and 192 key lengths.** FIPS 140-3 recommends the use of AES-256, and by exposing only a single key size, it prevents security weaknesses caused by incorrect key length selection in advance. + +## Security Threat Model + +### Cache-Timing Attack + +Standard software implementations of AES use a 256-byte S-box lookup table for the SubBytes operation. This approach has a fatal vulnerability. An attacker in an environment that shares the same CPU cache (VPS, cloud) can statistically recover the accessed table index, i.e., the secret key byte, from the cache hit/miss pattern. Daniel Bernstein's (D. J. Bernstein) 2005 AES timing attack demonstrated this empirically. + +This crate does not use any lookup tables. SubBytes performs the GF(2^8) inverse calculation and affine transformation as pure arithmetic bit operations, and the execution time of all operations is completely independent of the secret key and plaintext values. + +### Padding Oracle Attack + +In CBC mode, if the decryption error response depends on the validity of the padding, an attacker can completely decrypt an arbitrary ciphertext with an adaptive chosen-ciphertext attack (ACCA) (POODLE, Lucky 13 variants). This implementation fundamentally blocks this attack vector by forcing the **Encrypt-then-MAC** configuration. MAC verification is performed first, and if the MAC fails, the decryption operation itself is not performed. + +### GCM Nonce Reuse + +In GCM, if the same (key, nonce) pair is used even twice, the XOR of the plaintexts is exposed from the XOR of the two ciphertexts, completely breaking confidentiality. Furthermore, the authentication key H is recovered by solving the GHASH polynomial equation, which also threatens integrity. This crate delegates the nonce generation policy to the caller and attaches an explicit warning to the API documentation. In a production environment, generate the nonce with `HashDRBGSHA256` from `entlib-native-rng` or use a counter-based configuration that guarantees no collisions. + +## Security Core: Constant-Time AES Core + +### GF(2^8) Arithmetic + +AES SubBytes calculates the inverse on the finite field GF(2^8) = GF(2)[x] / (x^8 + x^4 + x^3 + x + 1) and then applies an affine transformation. + +#### xtime: Multiply by x in GF(2^8) + +$$ \text{xtime}(a) = \begin{cases} a \ll 1 & \text{if MSB}(a) = 0 \\ (a \ll 1) \oplus \texttt{0x1b} & \text{if MSB}(a) = 1 \end{cases} $$ + +Implemented without a branch statement. + +$$\text{mask} = -(a \gg 7), \quad \text{xtime}(a) = (a \ll 1) \oplus (\texttt{0x1b} \land \text{mask})$$ + +Since `mask` is `0xFF` if the MSB is 1 and `0x00` if it is 0, it is compiled into four instructions: a single `SHR`, `NEG`, `AND`, and `XOR`. + +#### gmul: GF(2^8) Multiplication — Fixed 8 Iterations + +$$\text{gmul}(a, b) = \bigoplus_{i=0}^{7} \left( a \cdot x^i \land -(b_i) \right)$$ + +Here, $b_i$ is the $i$-th bit of $b$. It performs a conditional XOR without branching by converting the bit to a mask with `-(b & 1).wrapping_neg()`. The number of iterations is a fixed value of 8, which is independent of the secret data, so the timing is constant. + +#### gf_inv: GF(2^8) Inverse — Fermat's Little Theorem + +In a finite field, if $a \ne 0$, then $a^{-1} = a^{2^8 - 2} = a^{254}$. If $a = 0$, then $0^{254} = 0$ is naturally returned, so no branch is needed. + +> [!NOTE] +> **Square-and-Multiply Expansion**: Since $254 = \texttt{11111110}_2$, +> +> $$a^{254} = a^{128} \cdot a^{64} \cdot a^{32} \cdot a^{16} \cdot a^8 \cdot a^4 \cdot a^2$$ +> +> It is calculated with a total of 13 `gmul` calls, with 7 squarings and 6 multiplications. Since there is no table access at all, there is no cache timing channel. + +#### sub_byte: SubBytes Affine Transformation + +Applies the affine transformation $M \cdot a^{-1} + c$ to the inverse $a^{-1}$. + +$$b_i = a^{-1}_i \oplus a^{-1}_{(i+4) \bmod 8} \oplus a^{-1}_{(i+5) \bmod 8} \oplus a^{-1}_{(i+6) \bmod 8} \oplus a^{-1}_{(i+7) \bmod 8} \oplus c_i$$ + +Expressed equivalently by bit rotation ($c = \texttt{0x63}$). + +```math +\text{sub\_byte}(a) = a^{-1} \oplus \text{ROL}(a^{-1}, 1) \oplus \text{ROL}(a^{-1}, 2) \oplus \text{ROL}(a^{-1}, 3) \oplus \text{ROL}(a^{-1}, 4) \oplus \texttt{0x63} +``` + +Inverse SubBytes (`inv_sub_byte`) calculates the inverse after the inverse affine transformation. + +```math +\text{inv\_sub\_byte}(a) = \text{gf\_inv}\!\left(\text{ROL}(a,1) \oplus \text{ROL}(a,3) \oplus \text{ROL}(a,6) \oplus \texttt{0x05}\right) +``` + +### Key Schedule + +AES-256 generates 15 round keys (16 bytes each) from a 32-byte master key. The intermediate array `w: [u32; 60]` used for key expansion is erased with `write_volatile` immediately after the round keys are extracted. The `KeySchedule` struct implements the `Drop` trait to automatically force the erasure of the entire 240-byte round key when it goes out of scope. + +```rust +impl Drop for KeySchedule { + fn drop(&mut self) { + for rk in &mut self.round_keys { + for b in rk { + unsafe { write_volatile(b, 0) }; + } + } + } +} +``` + +## AES-256-GCM + +Implementation according to NIST SP 800-38D §7.1. Only 96-bit (12 bytes) nonces are supported. The generalized path that allows for arbitrary length IVs (IV derivation using GHASH) is intentionally excluded as it increases the risk of nonce collisions. + +### Internal Operation + +1. **Hash Subkey Generation** + - $H = E_K(0^{128})$ +2. **Initial Counter Block** + - $J_0 = \text{nonce}_{96} \| \texttt{0x00000001}_{32}$ +3. **Encryption (GCTR)** + - $C = \text{GCTR}_K(\text{inc}_{32}(J_0),\ P)$ + - $\text{inc}_{32}$ increments the lower 32 bits by 1 in big-endian. +4. **Authentication Tag** + - $T = E_K(J_0) \oplus \text{GHASH}_H(A,\ C)$ + +Here, GHASH processes the AAD, the ciphertext, and the length block $[\text{len}(A)]_{64} \| [\text{len}(C)]_{64}$ in order. + +### GHASH: GF(2^128) Multiplication — Constant-Time Guarantee + +GCM authentication is performed over $\text{GF}(2^{128})$. The reduction polynomial is $f(x) = x^{128} + x^7 + x^2 + x + 1$, which is represented by the bit string `0xE1000...0` (128 bits, MSB first). + +> [!NOTE] +> **Constant-Time GF(2^128) Multiplication**: The standard implementation of NIST SP 800-38D Algorithm 1 includes a conditional branch that depends on a secret value. This implementation removes it with a fixed 128 iterations and a bit mask trick. +> +> In each iteration, the $i$-th bit of $X$, $X_i$, is converted to a mask to accumulate without branching. +> +> $$\text{mask} = -(X_i), \quad Z \mathrel{⊕}= V \land \text{mask}$$ +> +> The conditional reduction after the right shift of $V$ is also handled in the same way. +> +> ```math +> \text{lsb\_mask} = -(V_{127}), \quad V_{\text{high}} \mathrel{⊕}= \texttt{0xE100...00} \land \text{lsb\_mask} +> ``` + +`GHashState` implements the `Drop` trait to erase the internal state $Z$ and the hash subkey $H$ with `write_volatile`. + +### Decryption Verification Principle + +When decrypting, the tag is first recalculated, and the 16 bytes are compared in constant time using `ConstantTimeEq::ct_eq()`. If the verification fails, `AESError::AuthenticationFailed` is returned and no plaintext is output at all. + +```rust +// Constant-time 16-byte comparison +let mut r = 0xFFu8; +for i in 0..16 { +r &= expected_tag[i].ct_eq(&received_tag[i]).unwrap_u8(); +} +if r != 0xFF { return Err(AESError::AuthenticationFailed); } +// Decryption is performed only after verification passes +``` + +### API + +```rust +AES256GCM::encrypt( +key: &SecureBuffer, // 256-bit AES key +nonce: &[u8; 12], // 96-bit nonce (must be unique) +aad: &[u8], // Additional authenticated data +plaintext: &[u8], +ciphertext_out: &mut [u8], // plaintext.len() bytes +tag_out: &mut [u8; 16], // Authentication tag output +) -> Result<(), AESError> + +AES256GCM::decrypt( +key: &SecureBuffer, +nonce: &[u8; 12], +aad: &[u8], +ciphertext: &[u8], +tag: &[u8; 16], // Received authentication tag +plaintext_out: &mut [u8], // ciphertext.len() bytes +) -> Result<(), AESError> // AuthenticationFailed if tag does not match +``` + +> [!WARNING] +> Using the same `(key, nonce)` pair more than once will destroy both confidentiality and integrity. Generate the nonce via `HashDRBGSHA256` from `entlib-native-rng` or manage it with a monotonically increasing counter. + +## AES-256-CBC-HMAC-SHA256 + +The use of CBC mode alone in NIST SP 800-38A only guarantees confidentiality and does not provide integrity. This implementation forces the **Encrypt-then-MAC** configuration. After encryption, an HMAC-SHA256 tag is generated for `IV || ciphertext` and attached to the output. + +### Output Format + +``` +┌─────────────────┬────────────────────────────────────────┬───────────────────────────────┐ +│ IV (16 B) │ Ciphertext + PKCS7 Padding (N×16 B) │ HMAC-SHA256(IV||CT) (32 B) │ +└─────────────────┴────────────────────────────────────────┴───────────────────────────────┘ +``` + +PKCS7 padding is always added. Even if the plaintext fits exactly on a block boundary, a full padding block of 16 bytes (`0x10` × 16) is added, so the length of the output ciphertext is always $\lceil P / 16 \rceil + 1$ blocks. + +> [!NOTE] +> **PKCS7 Constant-Time Verification**: When decrypting, the padding byte verification is performed with XOR and a bit mask. +> +> ```math +> \begin{align} +> \text{diff}_i &= \text{data}[i] \oplus \text{pad\_byte}, \quad \text{not\_zero}_i = \frac{\text{diff}_i \mathbin{|} (-\text{diff}_i)}{2^7} \\ +> \text{valid} &= \bigwedge_{i} \overline{(\text{not\_zero}_i - 1)} \quad (\text{0xFF if valid}) +> \end{align} +>``` +> +> Since padding verification is performed only after MAC verification passes, it is impossible for an attacker to use a padding oracle without a valid MAC. + +### Decryption Order + +1. Verify input format (minimum 64 bytes, block size aligned) +2. Recalculate HMAC-SHA256 → constant-time comparison with `ct_eq_32` (`AESError::AuthenticationFailed` or pass) +3. Perform AES-256-CBC decryption only after MAC verification passes +4. Verify and remove PKCS7 padding + +### API + +```rust +AES256CBCHmac::encrypt( + enc_key: &SecureBuffer, // 256-bit AES encryption key + mac_key: &SecureBuffer, // HMAC-SHA256 key (minimum 14 bytes, recommended 32 bytes) + iv: &[u8; 16], // 128-bit IV (must be unique for each message) + plaintext: &[u8], + output: &mut [u8], // minimum cbc_output_len(plaintext.len()) bytes +) -> Result // Number of bytes written to output + +AES256CBCHmac::decrypt( + enc_key: &SecureBuffer, + mac_key: &SecureBuffer, + input: &[u8], // IV(16) || CT || HMAC(32) format + output: &mut [u8], +) -> Result // Number of decrypted plaintext bytes + +// Buffer size calculation helpers +cbc_output_len(plaintext_len: usize) -> usize +cbc_plaintext_max_len(input_len: usize) -> Option +``` + +> [!IMPORTANT] +> `enc_key` and `mac_key` must be independent and separate keys. Reusing the same key for both purposes invalidates the security proof of the cryptographic scheme. If key derivation is required, use `entlib-native-hkdf` to derive two independent subkeys from a master key. + +## Key Management Requirements + +| Parameter | Requirement | Rationale | +|-----------|------------------------------|-----------------------------| +| AES Key | Exactly 256 bits (32 bytes) | FIPS 140-3, NIST SP 800-38D | +| GCM nonce | 96 bits (12 bytes), unique | NIST SP 800-38D §8.2 | +| CBC IV | 128 bits (16 bytes), unique for each message | NIST SP 800-38A §6.2 | +| CBC MAC Key | Independent of AES key, minimum 112 bits | NIST SP 800-107r1 | + +All keys must be managed with `SecureBuffer` from `entlib-native-secure-buffer` to ensure mlock-based memory locking and automatic erasure on Drop. + +## Verification + +### NIST CAVP Test Vectors + +| Test | Source | Result | +|--------------------|---------------------------------------|----| +| AES-256 ECB Block Encryption | NIST FIPS 197 Appendix B | O | +| AES-256-GCM Encryption | NIST CAVP (OpenSSL cross-validation) | O | +| AES-256-GCM Decryption | Reverse roundtrip | O | +| AES-256-CBC Ciphertext | NIST SP 800-38A F.2.5 (OpenSSL cross-validation) | O | +| GCM Tag 1-bit Tampering | Manipulated tag → `AuthenticationFailed` | O | +| CBC MAC 1-bit Tampering | Manipulated MAC → `AuthenticationFailed` | O | + +```bash +cargo test -p entlib-native-aes +``` + +> [!WARNING] +> We are in the process of preparing to strictly pass the KAT (Known Answer Test) test vectors. +> +> The basis for the table above is the `aes_test.rs` test module, which verifies the matching of individual test blocks of the test vectors. + +## Summary of Design Principles + +1. **Force 256-bit single key** — Blocks security weaknesses due to key size selection errors at the API level. +2. **Complete exclusion of lookup tables** — All operations, including the S-box, are performed as pure arithmetic bit operations, so there is no cache timing channel. +3. **Fixed number of iterations** — All internal loops, such as `gmul` (8 times) and `gf128_mul` (128 times), are fixed to constants that are independent of the secret data. +4. **Force Encrypt-then-MAC** — Structurally blocks padding oracle attacks by not exposing a CBC-only API. +5. **Decrypt after verification principle** — Does not output plaintext before passing the constant-time verification of both the GCM tag and the CBC HMAC. +6. **Immediate erasure of key material** — `KeySchedule`, `GHashState`, and block operation intermediate values are all erased immediately after use with `write_volatile`. From 869ce280daaeead4515cc8410bd474a3a7691511 Mon Sep 17 00:00:00 2001 From: "Q. T. Felix" <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:31:09 +0900 Subject: [PATCH 7/7] =?UTF-8?q?docstring=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crypto/aes/src/aes.rs | 44 ++++++++++++++++++++++++++++++++++++++--- crypto/aes/src/cbc.rs | 19 ++++++++++-------- crypto/aes/src/error.rs | 4 +++- crypto/aes/src/gcm.rs | 12 ++++++----- crypto/aes/src/ghash.rs | 17 +++++++++++++++- crypto/aes/src/lib.rs | 28 ++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 18 deletions(-) diff --git a/crypto/aes/src/aes.rs b/crypto/aes/src/aes.rs index 1db7e33..653c0e7 100644 --- a/crypto/aes/src/aes.rs +++ b/crypto/aes/src/aes.rs @@ -1,3 +1,20 @@ +//! AES-256 블록 암호 코어 모듈입니다. +//! 룩업 테이블 없이 GF(2^8) 산술 연산만으로 구현하여 캐시-타이밍 부채널 공격을 차단합니다. +//! +//! # Examples +//! ```rust +//! use entlib_native_aes::{AES256GCM, GCM_NONCE_LEN, GCM_TAG_LEN}; +//! use entlib_native_secure_buffer::SecureBuffer; +//! +//! let mut key = SecureBuffer::new_owned(32).unwrap(); +//! key.as_mut_slice().copy_from_slice(&[0u8; 32]); +//! let nonce = [0u8; GCM_NONCE_LEN]; +//! let plaintext = b"hello world"; +//! let mut ct = vec![0u8; plaintext.len()]; +//! let mut tag = [0u8; GCM_TAG_LEN]; +//! AES256GCM::encrypt(&key, &nonce, &[], plaintext, &mut ct, &mut tag).unwrap(); +//! ``` + use core::ptr::write_volatile; pub type Block = [u8; 16]; @@ -36,7 +53,8 @@ fn gf_inv(a: u8) -> u8 { gmul(gmul(gmul(gmul(gmul(gmul(a128, a64), a32), a16), a8), a4), a2) } -// SubBytes 아핀 변환: b = a ^ rot(a,1) ^ rot(a,2) ^ rot(a,3) ^ rot(a,4) ^ 0x63 +/// AES SubBytes 바이트 치환 함수입니다. +/// GF(2^8) 역원(a^254) 계산 후 아핀 변환을 적용하여 S-Box 출력을 반환합니다. #[inline(always)] pub fn sub_byte(a: u8) -> u8 { let inv = gf_inv(a); @@ -158,12 +176,17 @@ const RCON: [u32; 7] = [ 0x10000000, 0x20000000, 0x40000000, ]; -// 확장 키: 15개의 라운드 키 (AES-256) +/// AES-256 키 스케줄 구조체입니다. +/// 256비트 키로부터 15개의 라운드 키를 파생하며, `Drop` 시 모든 라운드 키를 소거합니다. pub struct KeySchedule { pub round_keys: [Block; 15], } impl KeySchedule { + /// AES-256 키 스케줄을 생성하는 함수입니다. + /// + /// # Arguments + /// `key` — 256비트(32 bytes) AES 키 pub fn new(key: &[u8; 32]) -> Self { let mut w = [0u32; 60]; @@ -216,6 +239,12 @@ impl Drop for KeySchedule { } } +/// AES-256 블록 암호화 함수입니다. +/// 14라운드 순방향 암호(SubBytes → ShiftRows → MixColumns → AddRoundKey)를 수행합니다. +/// +/// # Arguments +/// - `state` — 입출력 16바이트 블록 (in-place) +/// - `ks` — 사전 생성된 키 스케줄 pub fn aes256_encrypt_block(state: &mut Block, ks: &KeySchedule) { add_round_key(state, &ks.round_keys[0]); for round in 1..14 { @@ -229,7 +258,10 @@ pub fn aes256_encrypt_block(state: &mut Block, ks: &KeySchedule) { add_round_key(state, &ks.round_keys[14]); } -/// 단일 블록 ECB 암호화 — KAT(Known Answer Test) 전용 +/// 단일 블록 ECB 암호화 함수입니다. KAT(Known Answer Test) 전용입니다. +/// +/// # Security Note +/// ECB 모드는 패턴을 보존하므로 실제 암호화에 사용할 수 없습니다. #[cfg_attr(not(test), allow(dead_code))] pub fn aes256_encrypt_ecb(key: &[u8; 32], plaintext: &[u8; 16]) -> Block { let ks = KeySchedule::new(key); @@ -238,6 +270,12 @@ pub fn aes256_encrypt_ecb(key: &[u8; 32], plaintext: &[u8; 16]) -> Block { state } +/// AES-256 블록 복호화 함수입니다. +/// 14라운드 역방향 암호(InvShiftRows → InvSubBytes → AddRoundKey → InvMixColumns)를 수행합니다. +/// +/// # Arguments +/// - `state` — 입출력 16바이트 블록 (in-place) +/// - `ks` — 사전 생성된 키 스케줄 pub fn aes256_decrypt_block(state: &mut Block, ks: &KeySchedule) { add_round_key(state, &ks.round_keys[14]); for round in (1..14).rev() { diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs index 72a7ea5..85a8c99 100644 --- a/crypto/aes/src/cbc.rs +++ b/crypto/aes/src/cbc.rs @@ -1,3 +1,6 @@ +//! AES-256-CBC-HMAC-SHA256 모듈입니다. +//! PKCS7 패딩, Encrypt-then-MAC 방식으로 기밀성과 무결성을 동시에 제공합니다. + use core::ptr::write_volatile; use entlib_native_constant_time::traits::ConstantTimeEq; use entlib_native_hmac::HMACSHA256; @@ -9,13 +12,15 @@ use crate::error::AESError; pub const CBC_IV_LEN: usize = 16; pub const CBC_HMAC_LEN: usize = 32; -/// CBC 암호화 출력 크기: IV(16) || 패딩된 암호문 || HMAC-SHA256(32) +/// CBC 암호화 출력 크기를 반환하는 함수입니다. +/// 출력 형식: `IV(16) || PKCS7-패딩된 암호문 || HMAC-SHA256(32)` pub fn cbc_output_len(plaintext_len: usize) -> usize { let padded = (plaintext_len / 16 + 1) * 16; CBC_IV_LEN + padded + CBC_HMAC_LEN } -/// CBC 복호화 최대 평문 크기 (입력에서 IV·HMAC 제거, PKCS7 최소 1바이트) +/// CBC 복호화 후 최대 평문 크기를 반환하는 함수입니다. +/// 입력에서 IV(16), HMAC(32), PKCS7 최소 패딩(1)을 제외한 크기를 반환합니다. pub fn cbc_plaintext_max_len(input_len: usize) -> Option { input_len.checked_sub(CBC_IV_LEN + CBC_HMAC_LEN + 1) } @@ -60,14 +65,12 @@ fn pkcs7_unpad_len(data: &[u8]) -> Result { // fxxk@@@ ^^7 일단 커밋 하고 pr로 수정 // -/// AES-256-CBC + PKCS7 + Encrypt-then-MAC(HMAC-SHA256) -/// -/// CBC 모드는 단독으로 사용할 수 없습니다. 암호문 전체(IV 포함)에 -/// 대해 HMAC-SHA256 무결성 태그를 붙여야 합니다. +/// AES-256-CBC-HMAC-SHA256 AEAD 구조체입니다. +/// CBC 모드는 단독으로 사용할 수 없으며, 반드시 Encrypt-then-MAC 방식으로 무결성 태그를 붙여야 합니다. pub struct AES256CBCHmac; impl AES256CBCHmac { - /// CBC-HMAC 암호화 + /// AES-256-CBC-HMAC-SHA256 암호화 함수입니다. /// /// # Arguments /// - `enc_key` — 256비트(32 bytes) AES 암호화 키 @@ -160,7 +163,7 @@ impl AES256CBCHmac { Ok(ct_end + 32) } - /// CBC-HMAC 복호화 + /// AES-256-CBC-HMAC-SHA256 복호화 함수입니다. /// /// # Arguments /// - `enc_key` — 256비트(32 bytes) AES 복호화 키 diff --git a/crypto/aes/src/error.rs b/crypto/aes/src/error.rs index 9198409..570e344 100644 --- a/crypto/aes/src/error.rs +++ b/crypto/aes/src/error.rs @@ -1,4 +1,6 @@ -/// AES-256 연산 중 발생할 수 있는 오류 +//! AES-256 오류 타입 모듈입니다. + +/// AES-256 연산 중 발생할 수 있는 오류 열거형입니다. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AESError { /// 키 길이가 256비트(32 bytes)가 아님 diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs index 48b6e3c..f845a0b 100644 --- a/crypto/aes/src/gcm.rs +++ b/crypto/aes/src/gcm.rs @@ -1,3 +1,6 @@ +//! AES-256-GCM AEAD 모듈입니다. +//! NIST SP 800-38D 준거. 96비트 nonce, 128비트 인증 태그를 지원합니다. + use core::ptr::write_volatile; use entlib_native_constant_time::traits::ConstantTimeEq; use entlib_native_secure_buffer::SecureBuffer; @@ -60,13 +63,12 @@ fn ct_eq_16(a: &[u8; 16], b: &[u8; 16]) -> bool { r == 0xFF } -/// AES-256-GCM (AEAD) -/// -/// NIST SP 800-38D 준거. 96비트 nonce만 지원. +/// AES-256-GCM AEAD 암호화 구조체입니다. +/// NIST SP 800-38D 준거이며 96비트 nonce만 지원합니다. pub struct AES256GCM; impl AES256GCM { - /// GCM 암호화 + /// AES-256-GCM 암호화 함수입니다. /// /// # Arguments /// - `key` — 256비트(32 bytes) AES 키 @@ -135,7 +137,7 @@ impl AES256GCM { Ok(()) } - /// GCM 복호화 및 태그 검증 + /// AES-256-GCM 복호화 및 태그 검증 함수입니다. /// /// # Security Note /// 태그 검증에 실패하면 평문을 출력하지 않습니다. 상수-시간 비교를 사용합니다. diff --git a/crypto/aes/src/ghash.rs b/crypto/aes/src/ghash.rs index 2d12498..727a17e 100644 --- a/crypto/aes/src/ghash.rs +++ b/crypto/aes/src/ghash.rs @@ -1,3 +1,6 @@ +//! GHASH 인증 함수 모듈입니다. +//! NIST SP 800-38D 준거 GF(2^128) 상수-시간 연산으로 GCM 인증 태그를 계산합니다. + use core::ptr::write_volatile; // GCM GF(2^128) 곱셈: 환원 다항식 x^128 + x^7 + x^2 + x + 1 @@ -30,12 +33,18 @@ fn gf128_mul(x: &mut [u64; 2], h: &[u64; 2]) { *x = z; } +/// GCM 인증 태그 계산을 위한 GHASH 상태 구조체입니다. +/// `Drop` 시 내부 H 값과 누산 상태를 소거합니다. pub struct GHashState { h: [u64; 2], state: [u64; 2], } impl GHashState { + /// GHASH 상태를 초기화하는 함수입니다. + /// + /// # Arguments + /// `h_block` — H = AES_K(0^128) 블록 pub fn new(h_block: &[u8; 16]) -> Self { let h = [ u64::from_be_bytes([ @@ -64,6 +73,7 @@ impl GHashState { gf128_mul(&mut self.state, &self.h); } + /// 데이터를 GHASH 상태에 누적하는 함수입니다. 16바이트 단위로 처리하며 나머지는 0 패딩합니다. pub fn update(&mut self, data: &[u8]) { let mut i = 0; while i + 16 <= data.len() { @@ -87,7 +97,12 @@ impl GHashState { } } - // 인증 태그 계산: 길이 블록 처리 후 최종 GHASH 값 반환 + /// GHASH 최종값을 반환하는 함수입니다. + /// AAD/암호문 길이 블록을 처리한 뒤 16바이트 GHASH 출력을 반환합니다. + /// + /// # Arguments + /// - `aad_len` — AAD 바이트 수 + /// - `ct_len` — 암호문 바이트 수 pub fn finalize(mut self, aad_len: u64, ct_len: u64) -> [u8; 16] { let aad_bits = aad_len * 8; let ct_bits = ct_len * 8; diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs index bfb4cc3..27c8404 100644 --- a/crypto/aes/src/lib.rs +++ b/crypto/aes/src/lib.rs @@ -1,3 +1,31 @@ +//! AES-256 암호 모듈입니다. +//! FIPS 140-3 요구사항을 충족하는 GCM(AEAD) 및 CBC+PKCS7+HMAC-SHA256 모드를 제공합니다. +//! +//! # Examples +//! ```rust +//! use entlib_native_aes::{AES256GCM, AES256CBCHmac, GCM_NONCE_LEN, GCM_TAG_LEN, CBC_IV_LEN, cbc_output_len}; +//! use entlib_native_secure_buffer::SecureBuffer; +//! +//! let mut key = SecureBuffer::new_owned(32).unwrap(); +//! key.as_mut_slice().copy_from_slice(&[0u8; 32]); +//! let nonce = [0u8; GCM_NONCE_LEN]; +//! let plaintext = b"hello world"; +//! let mut ct = vec![0u8; plaintext.len()]; +//! let mut tag = [0u8; GCM_TAG_LEN]; +//! AES256GCM::encrypt(&key, &nonce, &[], plaintext, &mut ct, &mut tag).unwrap(); +//! +//! let mut enc_key = SecureBuffer::new_owned(32).unwrap(); +//! enc_key.as_mut_slice().copy_from_slice(&[0u8; 32]); +//! let mut mac_key = SecureBuffer::new_owned(32).unwrap(); +//! mac_key.as_mut_slice().copy_from_slice(&[1u8; 32]); +//! let iv = [0u8; CBC_IV_LEN]; +//! let mut out = vec![0u8; cbc_output_len(plaintext.len())]; +//! AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, plaintext, &mut out).unwrap(); +//! ``` +//! +//! # Authors +//! Q. T. Felix + #![no_std] extern crate alloc;