diff --git a/.gitignore b/.gitignore index 7b0136b..9ae798b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,7 @@ .claude/ dist/ *.iml - -### ACVP Research -*_response.json +test-vectors/ ### Rust template # Generated by Cargo diff --git a/COMPLIANCE.md b/COMPLIANCE.md index 0c06d12..df6f24c 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -1,3 +1,34 @@ # 인증 및 규정 준수 사항 -작업 진행 중. \ No newline at end of file +> [!IMPORTANT] +> 테스트 벡터를 통과했다고 해서, 각 암호화 모듈 및 알고리즘 구현이 완전히 검증됐다는 것이 아닙니다. +> 이러한 테스트 벡터의 사용은 CAVP(Cryptographic Algorithm Validation Program)를 통해 얻은 검증을 대체하지 않습니다. + +NIST CAVP는 단일 알고리즘에 대한 검증 작업입니다. 실제 프로덕션 환경에서 사용되기 위해서는 FIPS 140-2/3에 따른 CMVP(Cryptographic Module Validation Program) 검증이 필요합니다. 즉, CAVP 인증은 CMVP 인증의 필수 선수 조건입니다. + +## RNG + +> [NIST CAVP - Random Number Generators](https://csrc.nist.gov/Projects/cryptographic-algorithm-validation-program/Random-Number-Generators) + +- [ ] SP 800-90A DRBG(Deterministic Random Bit Generators) + +## SHA2 + +> [NIST CAVP - Secure Hashing](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing) (ISO/IEC 10118-3) + +- [ ] FIPS 180-4 SHA Test Vectors for Hashing Bit/Byte-Oriented Messages + +> [KCMVP](https://seed.kisa.or.kr/kisa/kcmvp/EgovVerification.do) KS X ISO/IEC 10118-3:2001 + +- [ ] Security techniques - Hash-functions - Part 3: Dedicated hash-functions (2018) + +## SHA3 + +> [NIST CAVP - Secure Hashing](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing) (ISO/IEC 10118-3) + +- [X] FIPS 202 SHA-3 Hash Function Test Vectors for Hashing Bit/Byte-Oriented Messages +- [X] FIPS 202 SHA-3 XOF Test Vectors for Bit/Byte-Oriented Output + +> [KCMVP](https://seed.kisa.or.kr/kisa/kcmvp/EgovVerification.do) KS X ISO/IEC 10118-3:2001 + +- [X] Security techniques - Hash-functions - Part 3: Dedicated hash-functions (2018) \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 02cff0c..36db20e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,4 +24,4 @@ entlib-native-core-secure = { path = "./crypto/core-secure", version = entlib-native-rng = { path = "./crypto/rng", version = "1.1.2-Alpha" } entlib-native-sha2 = { path = "./crypto/sha2", version = "1.1.2-Alpha" } entlib-native-sha3 = { path = "./crypto/sha3", version = "1.1.2-Alpha" } -entlib-native-chacha20 = { path = "./crypto/chacha20", version = "1.1.2-Alpha" } \ No newline at end of file +entlib-native-chacha20 = { path = "./crypto/chacha20", version = "1.1.2-Alpha" } diff --git a/crypto/sha3/Cargo.toml b/crypto/sha3/Cargo.toml index b169460..dc5e9a5 100644 --- a/crypto/sha3/Cargo.toml +++ b/crypto/sha3/Cargo.toml @@ -9,12 +9,33 @@ license.workspace = true crate-type = ["cdylib", "rlib"] [dependencies] +hex = "0.4.3" # 다음 커밋에 제거 [dev-dependencies] criterion = { version = "0.8.2", features = ["html_reports"] } entlib-native-core-secure.workspace = true entlib-native-rng.workspace = true +[[bin]] +name = "cavp_sha3_bit" +path = "src/bin/cavp_sha3_bit.rs" + +[[bin]] +name = "cavp_sha3_byte" +path = "src/bin/cavp_sha3_byte.rs" + +[[bin]] +name = "cavp_shake_xof_bit" +path = "src/bin/cavp_shake_xof_bit.rs" + +[[bin]] +name = "cavp_shake_xof_byte" +path = "src/bin/cavp_shake_xof_byte.rs" + +[[bin]] +name = "kcmvp_sha3_byte" +path = "src/bin/kcmvp_sha3_byte.rs" + [[bench]] name = "sha3_bench" harness = false diff --git a/crypto/sha3/src/api.rs b/crypto/sha3/src/api.rs index 948f7c3..f2e117b 100644 --- a/crypto/sha3/src/api.rs +++ b/crypto/sha3/src/api.rs @@ -36,7 +36,12 @@ impl SHA3_224 { // 해시 연산 완료 및 다이제스트 반환 pub fn finalize(self) -> Vec { - self.0.finalize(28) + self.0.finalize(28, None) + } + + // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 + pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + self.0.finalize(28, Some((last_byte, valid_bits))) } } @@ -66,7 +71,12 @@ impl SHA3_256 { // 해시 연산 완료 및 다이제스트 반환 pub fn finalize(self) -> Vec { - self.0.finalize(32) + self.0.finalize(32, None) + } + + // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 + pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + self.0.finalize(32, Some((last_byte, valid_bits))) } } @@ -96,7 +106,12 @@ impl SHA3_384 { // 해시 연산 완료 및 다이제스트 반환 pub fn finalize(self) -> Vec { - self.0.finalize(48) + self.0.finalize(48, None) + } + + // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 + pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + self.0.finalize(48, Some((last_byte, valid_bits))) } } @@ -126,7 +141,12 @@ impl SHA3_512 { // 해시 연산 완료 및 다이제스트 반환 pub fn finalize(self) -> Vec { - self.0.finalize(64) + self.0.finalize(64, None) + } + + // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 + pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + self.0.finalize(64, Some((last_byte, valid_bits))) } } @@ -154,9 +174,14 @@ impl SHAKE128 { self.0.update(data); } - // XOF 연산 완료 및 지정된 길이의 다이제스트 반환 + // 바이트가 정확히 맞아떨어질 때 사용 (불완전 바이트 없음) pub fn finalize(self, output_len: usize) -> Vec { - self.0.finalize(output_len) + self.0.finalize(output_len, None) + } + + // 불완전한 마지막 바이트(last_byte)와 그 유효 비트 수(valid_bits)를 함께 받음 + pub fn finalize_bits(self, output_len: usize, last_byte: u8, valid_bits: usize) -> Vec { + self.0.finalize(output_len, Some((last_byte, valid_bits))) } } @@ -184,9 +209,14 @@ impl SHAKE256 { self.0.update(data); } - // XOF 연산 완료 및 지정된 길이의 다이제스트 반환 + // 바이트가 정확히 맞아떨어질 때 사용 (불완전 바이트 없음) pub fn finalize(self, output_len: usize) -> Vec { - self.0.finalize(output_len) + self.0.finalize(output_len, None) + } + + // 불완전한 마지막 바이트(last_byte)와 그 유효 비트 수(valid_bits)를 함께 받음 + pub fn finalize_bits(self, output_len: usize, last_byte: u8, valid_bits: usize) -> Vec { + self.0.finalize(output_len, Some((last_byte, valid_bits))) } } diff --git a/crypto/sha3/src/bin/cavp_sha3_bit.rs b/crypto/sha3/src/bin/cavp_sha3_bit.rs new file mode 100644 index 0000000..fe14385 --- /dev/null +++ b/crypto/sha3/src/bin/cavp_sha3_bit.rs @@ -0,0 +1,238 @@ +//! 얽힘 라이브러리(EntanglementLib) entlib-native 네이티브 SHA3 제품군 통합 Bit-Oriented & Monte Carlo CAVP 검증 도구 +//! 지원 알고리즘: SHA3_224, SHA3_256, SHA3_384, SHA3_512 + +use std::env; +use std::fs::File; +use std::io::{self, BufRead, Write}; + +use entlib_native_sha3::api::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + +/// 런타임에 해시 알고리즘을 동적으로 선택하기 위한 래퍼 열거형 +enum DynamicHasher { + Sha224(SHA3_224), + Sha256(SHA3_256), + Sha384(SHA3_384), + Sha512(SHA3_512), +} + +impl DynamicHasher { + fn new(algo: &str) -> Self { + match algo { + "224" | "SHA3_224" => DynamicHasher::Sha224(SHA3_224::new()), + "256" | "SHA3_256" => DynamicHasher::Sha256(SHA3_256::new()), + "384" | "SHA3_384" => DynamicHasher::Sha384(SHA3_384::new()), + "512" | "SHA3_512" => DynamicHasher::Sha512(SHA3_512::new()), + _ => panic!( + "지원하지 않는 알고리즘입니다. (224, 256, 384, 512 중 택일): {}", + algo + ), + } + } + + fn update(&mut self, data: &[u8]) { + match self { + DynamicHasher::Sha224(h) => h.update(data), + DynamicHasher::Sha256(h) => h.update(data), + DynamicHasher::Sha384(h) => h.update(data), + DynamicHasher::Sha512(h) => h.update(data), + } + } + // cargo run --bin cavp_sha3_bit -- 384 test-vectors/sha-3bittestvectors/SHA3_384ShortMsg.rsp && cargo run --bin cavp_sha3_bit -- 384 test-vectors/sha-3bittestvectors/SHA3_384LongMsg.rsp && cargo run --bin cavp_sha3_bit -- 384 test-vectors/sha-3bittestvectors/SHA3_384Monte.rsp && cargo run --bin cavp_sha3_bit -- 512 test-vectors/sha-3bittestvectors/SHA3_512ShortMsg.rsp && cargo run --bin cavp_sha3_bit -- 512 test-vectors/sha-3bittestvectors/SHA3_512LongMsg.rsp && cargo run --bin cavp_sha3_bit -- 512 test-vectors/sha-3bittestvectors/SHA3_512Monte.rsp + fn finalize(self) -> Vec { + match self { + DynamicHasher::Sha224(h) => h.finalize(), + DynamicHasher::Sha256(h) => h.finalize(), + DynamicHasher::Sha384(h) => h.finalize(), + DynamicHasher::Sha512(h) => h.finalize(), + } + } + + fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + match self { + DynamicHasher::Sha224(h) => h.finalize_bits(last_byte, valid_bits), + DynamicHasher::Sha256(h) => h.finalize_bits(last_byte, valid_bits), + DynamicHasher::Sha384(h) => h.finalize_bits(last_byte, valid_bits), + DynamicHasher::Sha512(h) => h.finalize_bits(last_byte, valid_bits), + } + } +} + +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 3 { + eprintln!( + "사용법: {} <224|256|384|512> [output.rsp]", + args[0] + ); + std::process::exit(1); + } + + let algo_str = &args[1]; + let input_path = &args[2]; + let output_path = args.get(3); + + println!( + "[NIST FIPS 202] SHA3-{} Bit-Oriented & Monte Carlo CAVP 검증 시작", + algo_str + ); + + let file = File::open(input_path)?; + let reader = io::BufReader::new(file); + let lines: Vec = reader.lines().map_while(Result::ok).collect(); + + let mut output_lines = Vec::new(); + let mut total = 0usize; + let mut passed = 0usize; + let mut i = 0; + + let mut current_mc_md: Vec = Vec::new(); + + while i < lines.len() { + let line = lines[i].trim(); + + if line.starts_with("Len = ") { + output_lines.push(lines[i].clone()); + let len_str = line.strip_prefix("Len = ").unwrap().trim(); + let len: usize = len_str.parse().expect("Len 파싱 실패"); + + i += 1; + let msg_line = lines[i].trim(); + let msg_hex = msg_line + .strip_prefix("Msg = ") + .unwrap_or("") + .trim() + .to_string(); + output_lines.push(lines[i].clone()); + + i += 1; + let expected_md = if i < lines.len() && lines[i].trim().starts_with("MD = ") { + lines[i] + .trim() + .strip_prefix("MD = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let rem = len % 8; + + // 동적 해셔 인스턴스 생성 + let mut hasher = DynamicHasher::new(algo_str); + + let computed_md: String = if rem == 0 { + let msg_data: Vec = if len == 0 { + vec![] + } else { + let byte_len = len / 8; + let decoded = hex::decode(&msg_hex).expect("Msg hex decode 실패"); + decoded[0..byte_len].to_vec() + }; + + hasher.update(&msg_data); + let digest = hasher.finalize(); + hex::encode(&digest).to_uppercase() + } else { + let byte_len = len.div_ceil(8); + let decoded = hex::decode(&msg_hex).expect("Msg hex decode 실패"); + let mut data = decoded[0..byte_len.min(decoded.len())].to_vec(); + + data[byte_len - 1] &= (1u8 << rem) - 1; + + let complete_bytes = &data[..byte_len - 1]; + hasher.update(complete_bytes); + + let last_byte = data[byte_len - 1]; + let digest = hasher.finalize_bits(last_byte, rem); + + hex::encode(&digest).to_uppercase() + }; + + output_lines.push(format!("MD = {}", computed_md)); + + if !expected_md.is_empty() { + total += 1; + if computed_md == expected_md { + passed += 1; + } else { + eprintln!("FAIL Len = {} bits", len); + eprintln!(" Expected: {}", expected_md); + eprintln!(" Computed: {}", computed_md); + } + } + } else if line.starts_with("Seed = ") { + output_lines.push(lines[i].clone()); + let seed_hex = line.strip_prefix("Seed = ").unwrap().trim(); + current_mc_md = hex::decode(seed_hex).expect("Seed hex decode 실패"); + } else if line.starts_with("COUNT = ") { + output_lines.push(lines[i].clone()); + let count_str = line.strip_prefix("COUNT = ").unwrap().trim(); + + i += 1; + let expected_md = if i < lines.len() && lines[i].trim().starts_with("MD = ") { + lines[i] + .trim() + .strip_prefix("MD = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let mut md = current_mc_md.clone(); + for _ in 0..1000 { + // 루프 내부에서도 동적 해셔 사용 + let mut hasher = DynamicHasher::new(algo_str); + hasher.update(&md); + md = hasher.finalize(); + } + + current_mc_md = md.clone(); + + let computed_md = hex::encode(&md).to_uppercase(); + output_lines.push(format!("MD = {}", computed_md)); + + if !expected_md.is_empty() { + total += 1; + if computed_md == expected_md { + passed += 1; + } else { + eprintln!("FAIL Monte Carlo COUNT = {}", count_str); + eprintln!(" Expected: {}", expected_md); + eprintln!(" Computed: {}", computed_md); + } + } + } else { + output_lines.push(lines[i].clone()); + } + i += 1; + } + + println!("\n=== CAVP 검증 결과 ==="); + println!("총 테스트 케이스 : {}", total); + println!("PASS : {}", passed); + println!("FAIL : {}", total - passed); + + if let Some(out_path) = output_path { + let mut f = File::create(out_path)?; + for line in &output_lines { + writeln!(f, "{}", line)?; + } + println!("응답 파일 생성: {}", out_path); + } + + if total > 0 && total == passed { + println!( + "\n모든 Bit-Oriented 및 Monte Carlo KAT 통과 (SHA3-{})", + algo_str + ); + } else { + println!("\n검증 실패에 따른 해시 처리 로직 및 입력 데이터 재확인 필요"); + } + + Ok(()) +} diff --git a/crypto/sha3/src/bin/cavp_sha3_byte.rs b/crypto/sha3/src/bin/cavp_sha3_byte.rs new file mode 100644 index 0000000..20dc34d --- /dev/null +++ b/crypto/sha3/src/bin/cavp_sha3_byte.rs @@ -0,0 +1,221 @@ +//! 얽힘 라이브러리(EntanglementLib) entlib-native 네이티브 SHA3 제품군 통합 Byte-Oriented & Monte Carlo CAVP 검증 도구 +//! 지원 알고리즘: SHA3_224, SHA3_256, SHA3_384, SHA3_512 +//! +//! 모든 입력 데이터의 Len이 8의 배수(Byte-aligned)임을 전제로 동작함 + +use std::env; +use std::fs::File; +use std::io::{self, BufRead, Write}; + +use entlib_native_sha3::api::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + +/// 런타임에 해시 알고리즘을 동적으로 선택하기 위한 래퍼 열거형 +enum DynamicHasher { + Sha224(SHA3_224), + Sha256(SHA3_256), + Sha384(SHA3_384), + Sha512(SHA3_512), +} + +impl DynamicHasher { + fn new(algo: &str) -> Self { + match algo { + "224" | "SHA3_224" => DynamicHasher::Sha224(SHA3_224::new()), + "256" | "SHA3_256" => DynamicHasher::Sha256(SHA3_256::new()), + "384" | "SHA3_384" => DynamicHasher::Sha384(SHA3_384::new()), + "512" | "SHA3_512" => DynamicHasher::Sha512(SHA3_512::new()), + _ => panic!( + "지원하지 않는 알고리즘입니다. (224, 256, 384, 512 중 택일): {}", + algo + ), + } + } + + fn update(&mut self, data: &[u8]) { + match self { + DynamicHasher::Sha224(h) => h.update(data), + DynamicHasher::Sha256(h) => h.update(data), + DynamicHasher::Sha384(h) => h.update(data), + DynamicHasher::Sha512(h) => h.update(data), + } + } + + fn finalize(self) -> Vec { + match self { + DynamicHasher::Sha224(h) => h.finalize(), + DynamicHasher::Sha256(h) => h.finalize(), + DynamicHasher::Sha384(h) => h.finalize(), + DynamicHasher::Sha512(h) => h.finalize(), + } + } +} + +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 3 { + eprintln!( + "사용법: {} <224|256|384|512> [output.rsp]", + args[0] + ); + std::process::exit(1); + } + + let algo_str = &args[1]; + let input_path = &args[2]; + let output_path = args.get(3); + + println!( + "[NIST FIPS 202] SHA3-{} Byte-Oriented & Monte Carlo CAVP 검증 시작", + algo_str + ); + + let file = File::open(input_path)?; + let reader = io::BufReader::new(file); + let lines: Vec = reader.lines().map_while(Result::ok).collect(); + + let mut output_lines = Vec::new(); + let mut total = 0usize; + let mut passed = 0usize; + let mut i = 0; + + let mut current_mc_md: Vec = Vec::new(); + + while i < lines.len() { + let line = lines[i].trim(); + + if line.starts_with("Len = ") { + output_lines.push(lines[i].clone()); + let len_str = line.strip_prefix("Len = ").unwrap().trim(); + let len: usize = len_str.parse().expect("Len 파싱 실패"); + + // Byte-Oriented 제약 조건 확인 (8의 배수) + if !len.is_multiple_of(8) { + eprintln!( + "경고: 입력된 Len({})이 8의 배수가 아닙니다. 이 모듈은 Byte-Oriented 전용입니다.", + len + ); + } + + i += 1; + let msg_line = lines[i].trim(); + let msg_hex = msg_line + .strip_prefix("Msg = ") + .unwrap_or("") + .trim() + .to_string(); + output_lines.push(lines[i].clone()); + + i += 1; + let expected_md = if i < lines.len() && lines[i].trim().starts_with("MD = ") { + lines[i] + .trim() + .strip_prefix("MD = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let mut hasher = DynamicHasher::new(algo_str); + let msg_data: Vec = if len == 0 { + vec![] + } else { + let byte_len = len / 8; + let decoded = hex::decode(&msg_hex).expect("Msg hex decode 실패"); + // 딱 떨어지는 바이트만큼만 추출 + decoded[0..byte_len].to_vec() + }; + + // 바이트 정렬이 보장되므로 단순 update & finalize 로직 사용 + hasher.update(&msg_data); + let digest = hasher.finalize(); + let computed_md = hex::encode(&digest).to_uppercase(); + + output_lines.push(format!("MD = {}", computed_md)); + + if !expected_md.is_empty() { + total += 1; + if computed_md == expected_md { + passed += 1; + } else { + eprintln!("FAIL Len = {} bits ({} bytes)", len, len / 8); + eprintln!(" Expected: {}", expected_md); + eprintln!(" Computed: {}", computed_md); + } + } + } else if line.starts_with("Seed = ") { + output_lines.push(lines[i].clone()); + let seed_hex = line.strip_prefix("Seed = ").unwrap().trim(); + current_mc_md = hex::decode(seed_hex).expect("Seed hex decode 실패"); + } else if line.starts_with("COUNT = ") { + output_lines.push(lines[i].clone()); + let count_str = line.strip_prefix("COUNT = ").unwrap().trim(); + + i += 1; + let expected_md = if i < lines.len() && lines[i].trim().starts_with("MD = ") { + lines[i] + .trim() + .strip_prefix("MD = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let mut md = current_mc_md.clone(); + for _ in 0..1000 { + let mut hasher = DynamicHasher::new(algo_str); + hasher.update(&md); + md = hasher.finalize(); + } + + // 다음 COUNT 루프를 위한 Seed 상태 연쇄(Chain) + current_mc_md = md.clone(); + + let computed_md = hex::encode(&md).to_uppercase(); + output_lines.push(format!("MD = {}", computed_md)); + + if !expected_md.is_empty() { + total += 1; + if computed_md == expected_md { + passed += 1; + } else { + eprintln!("FAIL Monte Carlo COUNT = {}", count_str); + eprintln!(" Expected: {}", expected_md); + eprintln!(" Computed: {}", computed_md); + } + } + } else { + output_lines.push(lines[i].clone()); + } + i += 1; + } + + println!("\n=== CAVP Byte-Oriented 검증 결과 ==="); + println!("총 테스트 케이스 : {}", total); + println!("PASS : {}", passed); + println!("FAIL : {}", total - passed); + + if let Some(out_path) = output_path { + let mut f = File::create(out_path)?; + for line in &output_lines { + writeln!(f, "{}", line)?; + } + println!("응답 파일 생성: {}", out_path); + } + + if total > 0 && total == passed { + println!( + "\n모든 Byte-Oriented 및 Monte Carlo KAT 통과 (SHA3-{})", + algo_str + ); + } else { + println!("\n검증 실패에 따른 해시 처리 로직 및 입력 데이터 재확인 필요"); + } + + Ok(()) +} diff --git a/crypto/sha3/src/bin/cavp_shake_xof_bit.rs b/crypto/sha3/src/bin/cavp_shake_xof_bit.rs new file mode 100644 index 0000000..0133aa4 --- /dev/null +++ b/crypto/sha3/src/bin/cavp_shake_xof_bit.rs @@ -0,0 +1,349 @@ +//! 얽힘 라이브러리(EntanglementLib) entlib-native 네이티브 SHAKE128 / SHAKE256 XOF Bit-Oriented & Monte Carlo CAVP 검증 도구 +//! ShortMsg, LongMsg, VariableOut, Monte 카를로 테스트 완벽 지원 +//! +//! nist sha3vs 및 acvp 규격에 따르면 +//! - 다음 루프의 입력 메시지(MSG[i])는 항상 이전 출력(MD[i-1])의 가장 왼쪽 128비트(16바이트)를 사용해야 함 +//! - 만약 이전 출력의 길이가 128비트보다 짧을 경우, 128비트가 될 때까지 오른쪽 끝에 0을 채워 넣음 + +use std::env; +use std::fs::File; +use std::io::{self, BufRead, Write}; + +use entlib_native_sha3::api::{SHAKE128, SHAKE256}; + +enum DynamicShakeHasher { + Shake128(SHAKE128), + Shake256(SHAKE256), +} + +impl DynamicShakeHasher { + fn new(algo: &str) -> Self { + match algo.to_uppercase().as_str() { + "SHAKE128" | "128" => DynamicShakeHasher::Shake128(SHAKE128::new()), + "SHAKE256" | "256" => DynamicShakeHasher::Shake256(SHAKE256::new()), + _ => panic!( + "지원하지 않는 알고리즘입니다. (SHAKE128, SHAKE256 중 택일): {}", + algo + ), + } + } + + fn update(&mut self, data: &[u8]) { + match self { + DynamicShakeHasher::Shake128(h) => h.update(data), + DynamicShakeHasher::Shake256(h) => h.update(data), + } + } + + fn finalize(self, output_len: usize) -> Vec { + match self { + DynamicShakeHasher::Shake128(h) => h.finalize(output_len), + DynamicShakeHasher::Shake256(h) => h.finalize(output_len), + } + } + + fn finalize_bits(self, output_len: usize, last_byte: u8, valid_bits: usize) -> Vec { + match self { + DynamicShakeHasher::Shake128(h) => h.finalize_bits(output_len, last_byte, valid_bits), + DynamicShakeHasher::Shake256(h) => h.finalize_bits(output_len, last_byte, valid_bits), + } + } +} + +/// 단일 SHAKE 연산을 수행하고 Bit-Oriented 입출력 LSB 마스킹을 적용하는 헬퍼 함수 +fn compute_shake( + algo: &str, + msg_data_full: &[u8], + in_len_bits: usize, + out_len_bits: usize, +) -> String { + let mut hasher = DynamicShakeHasher::new(algo); + let in_rem = in_len_bits % 8; + let out_byte_len = out_len_bits.div_ceil(8); + + let digest = if in_len_bits == 0 { + hasher.finalize(out_byte_len) + } else if in_rem == 0 { + let in_byte_len = in_len_bits / 8; + let data = &msg_data_full[0..in_byte_len]; + hasher.update(data); + hasher.finalize(out_byte_len) + } else { + let in_byte_len = in_len_bits.div_ceil(8); + let mut data = msg_data_full[0..in_byte_len].to_vec(); + data[in_byte_len - 1] &= (1u8 << in_rem) - 1; + + let complete_bytes = &data[..in_byte_len - 1]; + hasher.update(complete_bytes); + let last_byte = data[in_byte_len - 1]; + hasher.finalize_bits(out_byte_len, last_byte, in_rem) + }; + + let mut digest_masked = digest; + let out_rem = out_len_bits % 8; + if out_rem != 0 && !digest_masked.is_empty() { + let last_idx = digest_masked.len() - 1; + digest_masked[last_idx] &= (1u8 << out_rem) - 1; + } + + hex::encode(&digest_masked).to_uppercase() +} + +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 3 { + eprintln!( + "사용법: {} [output.rsp]", + args[0] + ); + std::process::exit(1); + } + + let algo_str = &args[1]; + let input_path = &args[2]; + let output_path = args.get(3); + + println!( + "[NIST FIPS 202] {} Bit-Oriented XOF CAVP 검증 시작", + algo_str + ); + + let file = File::open(input_path)?; + let reader = io::BufReader::new(file); + let lines: Vec = reader.lines().map_while(Result::ok).collect(); + + let mut output_lines = Vec::new(); + let mut total = 0usize; + let mut passed = 0usize; + let mut i = 0; + + let mut global_output_len: Option = None; + let mut global_input_len: Option = None; + let mut global_min_out_bytes: Option = None; + let mut global_max_out_bytes: Option = None; + + let mut monte_msg: Vec = Vec::new(); + let mut current_mc_md: Vec = Vec::new(); + let mut current_out_len_bytes: usize = 0; + + while i < lines.len() { + let line = lines[i].trim(); + + if line.starts_with("[Outputlen =") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + global_output_len = Some(val_str.parse().unwrap()); + } else if line.starts_with("[Input Length =") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + global_input_len = Some(val_str.parse().unwrap()); + } else if line.starts_with("[Minimum Output Length") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + global_min_out_bytes = Some(val_str.parse::().unwrap() / 8); + } else if line.starts_with("[Maximum Output Length") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + global_max_out_bytes = Some(val_str.parse::().unwrap() / 8); + } else if line.starts_with("Msg = ") { + output_lines.push(lines[i].clone()); + let hex_str = line.strip_prefix("Msg = ").unwrap().trim(); + monte_msg = hex::decode(hex_str).expect("Msg hex decode 실패"); + } else if line.starts_with("Len = ") { + // [1] ShortMsg / LongMsg 블록 처리 + output_lines.push(lines[i].clone()); + let len: usize = line.strip_prefix("Len = ").unwrap().trim().parse().unwrap(); + + i += 1; + let msg_hex = lines[i] + .trim() + .strip_prefix("Msg = ") + .unwrap_or("") + .trim() + .to_string(); + output_lines.push(lines[i].clone()); + + i += 1; + let expected_out = if i < lines.len() && lines[i].trim().starts_with("Output = ") { + lines[i] + .trim() + .strip_prefix("Output = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let out_bits = global_output_len.expect("global [Outputlen =] 가 누락되었습니다."); + let msg_data = hex::decode(&msg_hex).unwrap_or_default(); + + let computed_out = compute_shake(algo_str, &msg_data, len, out_bits); + output_lines.push(format!("Output = {}", computed_out)); + + if !expected_out.is_empty() { + total += 1; + if computed_out == expected_out { + passed += 1; + } else { + eprintln!("FAIL Short/LongMsg Len = {}", len); + } + } + } else if line.starts_with("COUNT = ") { + // [2] VariableOut / Monte 블록 처리 + output_lines.push(lines[i].clone()); + let count: usize = line + .strip_prefix("COUNT = ") + .unwrap() + .trim() + .parse() + .unwrap(); + + i += 1; + output_lines.push(lines[i].clone()); + let out_len_bits_from_file: usize = lines[i] + .trim() + .strip_prefix("Outputlen = ") + .unwrap() + .trim() + .parse() + .unwrap(); + + i += 1; + let mut msg_data = Vec::new(); + let mut is_var_out = false; + + if i < lines.len() && lines[i].trim().starts_with("Msg = ") { + is_var_out = true; + msg_data = + hex::decode(lines[i].trim().strip_prefix("Msg = ").unwrap().trim()).unwrap(); + output_lines.push(lines[i].clone()); + i += 1; + } + + let expected_out = if i < lines.len() && lines[i].trim().starts_with("Output = ") { + lines[i] + .trim() + .strip_prefix("Output = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let computed_out: String; + if is_var_out { + // VariableOut 테스트 + let in_len_bits = + global_input_len.expect("global [Input Length =] 가 누락되었습니다."); + computed_out = + compute_shake(algo_str, &msg_data, in_len_bits, out_len_bits_from_file); + } else { + // Monte Carlo 테스트 (NIST SP 800-185 규격) + // Q. T. Felix NOTE: 이 코드로 인해 SHAKE256을 테스트할 때 16바이트가 아닌 32바이트를 잘라내어 해시 + // 입력으로 사용하게 됨. 결국 첫 번째 루프 이후부터 입력값이 스펙과 완전히 달라짐. 그래서 다 실패 + // let target_input_bytes = if algo_str.contains("128") { 16 } else { 32 }; + let target_input_bytes = 16; + + if count == 0 { + current_mc_md = monte_msg.clone(); + current_out_len_bytes = global_max_out_bytes.unwrap(); + } + + let min_out_bytes = global_min_out_bytes.unwrap(); + let max_out_bytes = global_max_out_bytes.unwrap(); + let range = max_out_bytes - min_out_bytes + 1; + + let mut md = current_mc_md.clone(); + let mut out_bytes = current_out_len_bytes; + + for _ in 0..1000 { + let mut msg_i = md.clone(); + // 이전 출력의 leftmost 128(또는 256) bits만 다음 Seed로 사용 + if msg_i.len() >= target_input_bytes { + msg_i.truncate(target_input_bytes); + } else { + msg_i.resize(target_input_bytes, 0u8); + } + + let digest_hex = + compute_shake(algo_str, &msg_i, target_input_bytes * 8, out_bytes * 8); + md = hex::decode(&digest_hex).unwrap(); + + // NIST Spec: Rightmost 16 bits of Output_i 추출 및 정수 변환 + let rightmost_16_val = if md.len() >= 2 { + let b1 = md[md.len() - 2] as usize; + let b2 = md[md.len() - 1] as usize; + (b1 << 8) | b2 + } else { + md[0] as usize + }; + + // 다음 루프에서 사용할 동적 Outputlen 업데이트 + out_bytes = min_out_bytes + (rightmost_16_val % range); + } + + current_mc_md = md.clone(); + current_out_len_bytes = out_bytes; + + computed_out = hex::encode(&md).to_uppercase(); + } + + output_lines.push(format!("Output = {}", computed_out)); + + if !expected_out.is_empty() { + total += 1; + if computed_out == expected_out { + passed += 1; + } else { + eprintln!("FAIL COUNT = {}", count); + } + } + } else { + output_lines.push(lines[i].clone()); + } + i += 1; + } + + println!("\n=== CAVP 검증 결과 ==="); + println!("총 테스트 케이스 : {}", total); + println!("PASS : {}", passed); + println!("FAIL : {}", total - passed); + + if let Some(out_path) = output_path { + let mut f = File::create(out_path)?; + for line in &output_lines { + writeln!(f, "{}", line)?; + } + println!("응답 파일 생성: {}", out_path); + } + + Ok(()) +} diff --git a/crypto/sha3/src/bin/cavp_shake_xof_byte.rs b/crypto/sha3/src/bin/cavp_shake_xof_byte.rs new file mode 100644 index 0000000..b8a9df1 --- /dev/null +++ b/crypto/sha3/src/bin/cavp_shake_xof_byte.rs @@ -0,0 +1,319 @@ +//! 얽힘 라이브러리(EntanglementLib) entlib-native 네이티브 SHAKE128 / SHAKE256 XOF Byte-Oriented & Monte Carlo CAVP 검증 도구 +//! ShortMsg, LongMsg, VariableOut, Monte 카를로 테스트 완벽 지원 (Byte 정렬 전용) + +use std::env; +use std::fs::File; +use std::io::{self, BufRead, Write}; + +use entlib_native_sha3::api::{SHAKE128, SHAKE256}; + +enum DynamicShakeHasher { + Shake128(SHAKE128), + Shake256(SHAKE256), +} + +impl DynamicShakeHasher { + fn new(algo: &str) -> Self { + match algo.to_uppercase().as_str() { + "SHAKE128" | "128" => DynamicShakeHasher::Shake128(SHAKE128::new()), + "SHAKE256" | "256" => DynamicShakeHasher::Shake256(SHAKE256::new()), + _ => panic!( + "지원하지 않는 알고리즘입니다. (SHAKE128, SHAKE256 중 택일): {}", + algo + ), + } + } + + fn update(&mut self, data: &[u8]) { + match self { + DynamicShakeHasher::Shake128(h) => h.update(data), + DynamicShakeHasher::Shake256(h) => h.update(data), + } + } + + fn finalize(self, output_len: usize) -> Vec { + match self { + DynamicShakeHasher::Shake128(h) => h.finalize(output_len), + DynamicShakeHasher::Shake256(h) => h.finalize(output_len), + } + } +} + +/* +cargo run --bin cavp_shake_xof_byte -- 128 test-vectors/shakebytetestvectors/SHAKE128ShortMsg.rsp && +cargo run --bin cavp_shake_xof_byte -- 128 test-vectors/shakebytetestvectors/SHAKE128LongMsg.rsp && +cargo run --bin cavp_shake_xof_byte -- 128 test-vectors/shakebytetestvectors/SHAKE128Monte.rsp && +cargo run --bin cavp_shake_xof_byte -- 128 test-vectors/shakebytetestvectors/SHAKE128VariableOut.rsp && +cargo run --bin cavp_shake_xof_byte -- 256 test-vectors/shakebytetestvectors/SHAKE256ShortMsg.rsp && +cargo run --bin cavp_shake_xof_byte -- 256 test-vectors/shakebytetestvectors/SHAKE256LongMsg.rsp && +cargo run --bin cavp_shake_xof_byte -- 256 test-vectors/shakebytetestvectors/SHAKE256Monte.rsp && +cargo run --bin cavp_shake_xof_byte -- 256 test-vectors/shakebytetestvectors/SHAKE256VariableOut.rsp +*/ + +/// 단일 SHAKE 연산을 수행하는 헬퍼 함수 (바이트 정렬 보장) +fn compute_shake(algo: &str, msg_data: &[u8], out_len_bytes: usize) -> String { + let mut hasher = DynamicShakeHasher::new(algo); + hasher.update(msg_data); + let digest = hasher.finalize(out_len_bytes); + hex::encode(&digest).to_uppercase() +} + +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 3 { + eprintln!( + "사용법: {} [output.rsp]", + args[0] + ); + std::process::exit(1); + } + + let algo_str = &args[1]; + let input_path = &args[2]; + let output_path = args.get(3); + + println!( + "[NIST FIPS 202] {} Byte-Oriented XOF CAVP 검증 시작", + algo_str + ); + + let file = File::open(input_path)?; + let reader = io::BufReader::new(file); + let lines: Vec = reader.lines().map_while(Result::ok).collect(); + + let mut output_lines = Vec::new(); + let mut total = 0usize; + let mut passed = 0usize; + let mut i = 0; + + let mut global_output_len_bytes: Option = None; + let mut global_input_len_bytes: Option = None; + let mut global_min_out_bytes: Option = None; + let mut global_max_out_bytes: Option = None; + + let mut monte_msg: Vec = Vec::new(); + let mut current_mc_md: Vec = Vec::new(); + let mut current_out_len_bytes: usize = 0; + + while i < lines.len() { + let line = lines[i].trim(); + + if line.starts_with("[Outputlen =") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + // 비트 단위로 주어지는 값을 바이트로 변환 + global_output_len_bytes = Some(val_str.parse::().unwrap() / 8); + } else if line.starts_with("[Input Length =") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + global_input_len_bytes = Some(val_str.parse::().unwrap() / 8); + } else if line.starts_with("[Minimum Output Length") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + global_min_out_bytes = Some(val_str.parse::().unwrap() / 8); + } else if line.starts_with("[Maximum Output Length") { + output_lines.push(lines[i].clone()); + let val_str = line + .split('=') + .nth(1) + .unwrap() + .strip_suffix("]") + .unwrap() + .trim(); + global_max_out_bytes = Some(val_str.parse::().unwrap() / 8); + } else if line.starts_with("Msg = ") { + output_lines.push(lines[i].clone()); + let hex_str = line.strip_prefix("Msg = ").unwrap().trim(); + monte_msg = hex::decode(hex_str).expect("Msg hex decode 실패"); + } else if line.starts_with("Len = ") { + // [1] ShortMsg / LongMsg 블록 처리 + output_lines.push(lines[i].clone()); + let len_bits: usize = line.strip_prefix("Len = ").unwrap().trim().parse().unwrap(); + let len_bytes = len_bits / 8; + + i += 1; + let msg_hex = lines[i] + .trim() + .strip_prefix("Msg = ") + .unwrap_or("") + .trim() + .to_string(); + output_lines.push(lines[i].clone()); + + i += 1; + let expected_out = if i < lines.len() && lines[i].trim().starts_with("Output = ") { + lines[i] + .trim() + .strip_prefix("Output = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let out_bytes = + global_output_len_bytes.expect("global [Outputlen =] 가 누락되었습니다."); + let mut msg_data = hex::decode(&msg_hex).unwrap_or_default(); + msg_data.truncate(len_bytes); // 실제 길이만큼 자르기 + + let computed_out = compute_shake(algo_str, &msg_data, out_bytes); + output_lines.push(format!("Output = {}", computed_out)); + + if !expected_out.is_empty() { + total += 1; + if computed_out == expected_out { + passed += 1; + } else { + eprintln!("FAIL Short/LongMsg Len = {}", len_bits); + } + } + } else if line.starts_with("COUNT = ") { + // [2] VariableOut / Monte 블록 처리 + output_lines.push(lines[i].clone()); + let count: usize = line + .strip_prefix("COUNT = ") + .unwrap() + .trim() + .parse() + .unwrap(); + + i += 1; + output_lines.push(lines[i].clone()); + let out_len_bytes_from_file: usize = lines[i] + .trim() + .strip_prefix("Outputlen = ") + .unwrap() + .trim() + .parse::() + .unwrap() + / 8; + + i += 1; + let mut msg_data = Vec::new(); + let mut is_var_out = false; + + if i < lines.len() && lines[i].trim().starts_with("Msg = ") { + is_var_out = true; + msg_data = + hex::decode(lines[i].trim().strip_prefix("Msg = ").unwrap().trim()).unwrap(); + output_lines.push(lines[i].clone()); + i += 1; + } + + let expected_out = if i < lines.len() && lines[i].trim().starts_with("Output = ") { + lines[i] + .trim() + .strip_prefix("Output = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let computed_out: String; + if is_var_out { + // VariableOut 테스트 + let in_len_bytes = + global_input_len_bytes.expect("global [Input Length =] 가 누락되었습니다."); + msg_data.truncate(in_len_bytes); + computed_out = compute_shake(algo_str, &msg_data, out_len_bytes_from_file); + } else { + // Monte Carlo 테스트 (NIST SP 800-185 규격) + // 알고리즘 종류와 무관하게 이전 결과의 16바이트(128비트)만 사용 + let target_input_bytes = 16; + + if count == 0 { + current_mc_md = monte_msg.clone(); + current_out_len_bytes = global_max_out_bytes.unwrap(); + } + + let min_out_bytes = global_min_out_bytes.unwrap(); + let max_out_bytes = global_max_out_bytes.unwrap(); + let range = max_out_bytes - min_out_bytes + 1; + + let mut md = current_mc_md.clone(); + let mut out_bytes = current_out_len_bytes; + + for _ in 0..1000 { + let mut msg_i = md.clone(); + + if msg_i.len() >= target_input_bytes { + msg_i.truncate(target_input_bytes); + } else { + msg_i.resize(target_input_bytes, 0u8); + } + + let digest_hex = compute_shake(algo_str, &msg_i, out_bytes); + md = hex::decode(&digest_hex).unwrap(); + + // NIST Spec: Rightmost 16 bits of Output_i 추출 및 정수 변환 + let rightmost_16_val = if md.len() >= 2 { + let b1 = md[md.len() - 2] as usize; + let b2 = md[md.len() - 1] as usize; + (b1 << 8) | b2 + } else { + md[0] as usize + }; + + out_bytes = min_out_bytes + (rightmost_16_val % range); + } + + current_mc_md = md.clone(); + current_out_len_bytes = out_bytes; + + computed_out = hex::encode(&md).to_uppercase(); + } + + output_lines.push(format!("Output = {}", computed_out)); + + if !expected_out.is_empty() { + total += 1; + if computed_out == expected_out { + passed += 1; + } else { + eprintln!("FAIL COUNT = {}", count); + } + } + } else { + output_lines.push(lines[i].clone()); + } + i += 1; + } + + println!("\n=== CAVP 검증 결과 ==="); + println!("총 테스트 케이스 : {}", total); + println!("PASS : {}", passed); + println!("FAIL : {}", total - passed); + + if let Some(out_path) = output_path { + let mut f = File::create(out_path)?; + for line in &output_lines { + writeln!(f, "{}", line)?; + } + println!("응답 파일 생성: {}", out_path); + } + + Ok(()) +} diff --git a/crypto/sha3/src/bin/kcmvp_sha3_byte.rs b/crypto/sha3/src/bin/kcmvp_sha3_byte.rs new file mode 100644 index 0000000..b1b7454 --- /dev/null +++ b/crypto/sha3/src/bin/kcmvp_sha3_byte.rs @@ -0,0 +1,218 @@ +//! 얽힘 라이브러리(EntanglementLib) entlib-native 네이티브 SHA3 제품군 통합 KCMVP Byte-Oriented & Monte Carlo CAVP 검증 도구 +//! 지원 알고리즘: SHA3_224, SHA3_256, SHA3_384, SHA3_512 +//! +//! KCMVP 암호알고리즘 검증기준 V3.0에 따른 임의 메시지 검사(Monte Carlo) 규격 적용 + +use std::env; +use std::fs::File; +use std::io::{self, BufRead, Write}; + +use entlib_native_sha3::api::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + +enum DynamicHasher { + Sha224(SHA3_224), + Sha256(SHA3_256), + Sha384(SHA3_384), + Sha512(SHA3_512), +} + +impl DynamicHasher { + fn new(algo: &str) -> Self { + match algo { + "224" | "SHA3_224" => DynamicHasher::Sha224(SHA3_224::new()), + "256" | "SHA3_256" => DynamicHasher::Sha256(SHA3_256::new()), + "384" | "SHA3_384" => DynamicHasher::Sha384(SHA3_384::new()), + "512" | "SHA3_512" => DynamicHasher::Sha512(SHA3_512::new()), + _ => panic!( + "지원하지 않는 알고리즘입니다. (224, 256, 384, 512 중 택일): {}", + algo + ), + } + } + + fn update(&mut self, data: &[u8]) { + match self { + DynamicHasher::Sha224(h) => h.update(data), + DynamicHasher::Sha256(h) => h.update(data), + DynamicHasher::Sha384(h) => h.update(data), + DynamicHasher::Sha512(h) => h.update(data), + } + } + + fn finalize(self) -> Vec { + match self { + DynamicHasher::Sha224(h) => h.finalize(), + DynamicHasher::Sha256(h) => h.finalize(), + DynamicHasher::Sha384(h) => h.finalize(), + DynamicHasher::Sha512(h) => h.finalize(), + } + } +} + +/// KCMVP 규격: N = floor(r/n) + 1 계산 함수 +fn get_kcmvp_n(algo: &str) -> usize { + match algo { + "224" | "SHA3_224" => (1152 / 224) + 1, // 6 + "256" | "SHA3_256" => (1088 / 256) + 1, // 5 + "384" | "SHA3_384" => (832 / 384) + 1, // 3 + "512" | "SHA3_512" => (576 / 512) + 1, // 2 + _ => panic!("지원하지 않는 알고리즘"), + } +} + +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 3 { + eprintln!( + "사용법: {} <224|256|384|512> [output.rsp]", + args[0] + ); + std::process::exit(1); + } + + let algo_str = &args[1]; + let input_path = &args[2]; + let output_path = args.get(3); + + println!("[KCMVP] SHA3-{} 검증 시작", algo_str); + + let file = File::open(input_path)?; + let reader = io::BufReader::new(file); + let lines: Vec = reader.lines().map_while(Result::ok).collect(); + + let mut output_lines = Vec::new(); + let mut total = 0usize; + let mut passed = 0usize; + let mut i = 0; + + let mut current_mc_md: Vec = Vec::new(); + + while i < lines.len() { + let line = lines[i].trim(); + + if line.starts_with("Len = ") { + // [1] Short / Long Message 테스트 블록 (NIST와 동일) + output_lines.push(lines[i].clone()); + let len: usize = line.strip_prefix("Len = ").unwrap().trim().parse().unwrap(); + + i += 1; + let msg_hex = lines[i] + .trim() + .strip_prefix("Msg = ") + .unwrap_or("") + .trim() + .to_string(); + output_lines.push(lines[i].clone()); + + i += 1; + let expected_md = if i < lines.len() && lines[i].trim().starts_with("MD = ") { + lines[i] + .trim() + .strip_prefix("MD = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + let mut hasher = DynamicHasher::new(algo_str); + let msg_data: Vec = if len == 0 { + vec![] + } else { + let byte_len = len / 8; + let decoded = hex::decode(&msg_hex).expect("Msg hex decode 실패"); + decoded[0..byte_len].to_vec() + }; + + hasher.update(&msg_data); + let digest = hasher.finalize(); + let computed_md = hex::encode(&digest).to_uppercase(); + + output_lines.push(format!("MD = {}", computed_md)); + + if !expected_md.is_empty() { + total += 1; + if computed_md == expected_md { + passed += 1; + } else { + eprintln!("FAIL Len = {}", len); + } + } + } else if line.starts_with("Seed = ") { + // [2] Monte Carlo Seed 초기화 + output_lines.push(lines[i].clone()); + let seed_hex = line.strip_prefix("Seed = ").unwrap().trim(); + current_mc_md = hex::decode(seed_hex).expect("Seed hex decode 실패"); + } else if line.starts_with("COUNT = ") { + // [3] Monte Carlo COUNT 루프 + output_lines.push(lines[i].clone()); + let count_str = line.strip_prefix("COUNT = ").unwrap().trim(); + + i += 1; + let expected_md = if i < lines.len() && lines[i].trim().starts_with("MD = ") { + lines[i] + .trim() + .strip_prefix("MD = ") + .unwrap() + .trim() + .to_uppercase() + } else { + i -= 1; + String::new() + }; + + // KCMVP 임의 메시지 검사 파라미터 적용 + let n_val = get_kcmvp_n(algo_str); + + // MD_{0} ~ MD_{N-1} 까지 Seed로 배열 초기화 + let mut md_array: Vec> = vec![current_mc_md.clone(); n_val]; + + for k in n_val..(1000 + n_val) { + let mut hasher = DynamicHasher::new(algo_str); + + // 순서대로 hasher에 주입하여 Concatenation (Msg_i = MD_{k-N} || ... || MD_{k-1}) + for j in 0..n_val { + hasher.update(&md_array[k - n_val + j]); + } + + let digest = hasher.finalize(); + md_array.push(digest); + } + + // 다음 COUNT 루프를 위해 Seed 갱신 (MD_{1000+N-1}) + current_mc_md = md_array[1000 + n_val - 1].clone(); + + let computed_md = hex::encode(¤t_mc_md).to_uppercase(); + output_lines.push(format!("MD = {}", computed_md)); + + if !expected_md.is_empty() { + total += 1; + if computed_md == expected_md { + passed += 1; + } else { + eprintln!("FAIL Monte Carlo COUNT = {}", count_str); + } + } + } else { + output_lines.push(lines[i].clone()); + } + i += 1; + } + + println!("\n=== KCMVP CAVP Byte-Oriented 검증 결과 ==="); + println!("총 테스트 케이스 : {}", total); + println!("PASS : {}", passed); + println!("FAIL : {}", total - passed); + + if let Some(out_path) = output_path { + let mut f = File::create(out_path)?; + for line in &output_lines { + writeln!(f, "{}", line)?; + } + println!("응답 파일 생성: {}", out_path); + } + + Ok(()) +} diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 13f1827..66582ae 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -123,16 +123,62 @@ impl KeccakState { } /// 메시지 패딩 및 최종 블록 처리 - fn pad(&mut self) { - self.buffer[self.buffer_len] = self.domain; - self.buffer[self.buffer_len + 1..self.rate_bytes].fill(0); + /// + /// # Arguments + /// - last_byte_bits 마지막 바이트의 유효 비트 수 (0~7). 0인 경우 8비트(전체)가 유효하거나 바이트 정렬됨을 의미 + fn pad(&mut self, last_byte_opt: Option<(u8, usize)>) { + let mut valid_bits = 0; + + if let Some((last_byte, bits)) = last_byte_opt { + valid_bits = bits; + let mask = (1u8 << valid_bits) - 1; + self.buffer[self.buffer_len] = last_byte & mask; + } else { + self.buffer[self.buffer_len] = 0; + } + + // 도메인 구분자와 패딩 시작 비트(1) 병합 + let padding = (self.domain as u16) << valid_bits; + self.buffer[self.buffer_len] |= (padding & 0xFF) as u8; + self.buffer_len += 1; + + // 패딩이 바이트 경계를 넘어가는 경우 (오버플로) + if padding > 0xFF { + if self.buffer_len == self.rate_bytes { + self.process_buffer(); + self.buffer_len = 0; + } + self.buffer[self.buffer_len] = (padding >> 8) as u8; + self.buffer_len += 1; + } + + // Q. T. Felix NOTE: Keccak 10*1 패딩 비트 충돌(Collision) 감지 추가 + // 블록이 정확히 가득 찼는데(rate_bytes) 방금 추가한 도메인/시작 패딩이 + // 블록의 마지막 비트(0x80)를 점유했다면 즉시 압축하고 새 블록을 생성해야 함 + if self.buffer_len == self.rate_bytes && (self.buffer[self.rate_bytes - 1] & 0x80) != 0 { + self.process_buffer(); + self.buffer_len = 0; + } + + // 남은 공간 0으로 채움 + self.buffer[self.buffer_len..self.rate_bytes].fill(0); + + // 스펀지 구조의 최종 종료 패딩 비트(0x80) 설정 self.buffer[self.rate_bytes - 1] |= 0x80; + self.process_buffer(); } /// 해시 연산 종료 및 다이제스트(digest) 반환 - pub(crate) fn finalize(mut self, output_len: usize) -> Vec { - self.pad(); + /// + /// # Arguments + /// - last_byte_bits 마지막 바이트의 유효 비트 수 (0 = 바이트 정렬) + pub(crate) fn finalize( + mut self, + output_len: usize, + last_byte_opt: Option<(u8, usize)>, + ) -> Vec { + self.pad(last_byte_opt); let mut out = Vec::with_capacity(output_len); while out.len() < output_len { @@ -141,15 +187,13 @@ impl KeccakState { break; } let word_bytes = self.state[i].to_le_bytes(); - let take = min(8, output_len - out.len()); + let take = core::cmp::min(8, output_len - out.len()); out.extend_from_slice(&word_bytes[..take]); } if out.len() < output_len { Self::keccak_f1600(&mut self.state); } } - - // self가 범위를 벗어나면서 Drop 트레이트에 의해 내부 상태가 자동 소거됨 out } } diff --git a/internal/ffi/src/secure_buffer_ffi.rs b/internal/ffi/src/secure_buffer_ffi.rs index 86e9ed8..6b863ad 100644 --- a/internal/ffi/src/secure_buffer_ffi.rs +++ b/internal/ffi/src/secure_buffer_ffi.rs @@ -73,6 +73,78 @@ pub unsafe extern "C" fn entlib_secure_buffer_free(buf: *mut SecureBuffer) { } } +/// JNI/FFM 환경에서 여러 번의 컨텍스트 스위칭 오버헤드를 줄이기 위해, +/// 데이터 포인터와 길이를 한 번에 반환하는 구조체입니다. +#[repr(C)] +pub struct FfiSecureBufferView { + pub data: *const u8, + pub len: usize, +} + +/// 보안 버퍼 내 데이터의 메모리 주소와 길이를 동시에 반환합니다. +/// 기존의 `len` 및 `data` 개별 호출로 인한 병목(FFI 경계 횡단)을 1회로 줄입니다. +/// Java 측에서는 데이터를 안전하게 복사한 뒤 반드시 `entlib_secure_buffer_free`를 호출해야 합니다. +/// +/// # Safety +/// - 반환된 원시 포인터는 `SecureBuffer`가 해제되기 전까지만 유효합니다. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn entlib_secure_buffer_view( + buf: *const SecureBuffer, +) -> FfiSecureBufferView { + if buf.is_null() { + return FfiSecureBufferView { + data: ptr::null(), + len: 0, + }; + } + // 레퍼런스 차용을 통해 원본 소유권을 유지 + let buffer = unsafe { &*buf }; + FfiSecureBufferView { + data: buffer.inner.as_ptr(), + len: buffer.inner.len(), + } +} + +/// Java가 제공한 메모리로 데이터를 복사하고, Rust 버퍼를 즉각 소거합니다. +/// 기존 (Len 확인 -> Data 매핑 -> Free)의 3회 호출을 **단 1회의 FFI 호출**로 극단적으로 압축합니다. +/// +/// # Arguments +/// * `buf` - 해제할 `SecureBuffer`의 가변 포인터 +/// * `dest` - 복사될 Java 측 오프힙 메모리(또는 Secure Array)의 시작 포인터 +/// * `dest_capacity` - 버퍼 오버플로우 방지를 위한 `dest`의 최대 용량 +/// +/// # Returns +/// 실제 복사된 바이트 길이(usize). 만약 용량 부족 등 에러가 발생하면 0을 반환합니다. +/// +/// # Safety +/// - 호출 즉시 `buf`의 `Drop` 트레이트가 실행되어 메모리가 소거됩니다. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn entlib_secure_buffer_copy_and_free( + buf: *mut SecureBuffer, + dest: *mut u8, + dest_capacity: usize, +) -> usize { + if buf.is_null() || dest.is_null() { + return 0; + } + + // Box::from_raw로 소유권 획득 (스코프 종료 시 자동 소거) + let buffer = unsafe { Box::from_raw(buf) }; + let len = buffer.inner.len(); + + // Zero Trust 검증: Java 측 버퍼 용량이 실제 반환할 데이터보다 크거나 같은지 확인 + if dest_capacity >= len { + unsafe { + // 메모리 복사 수행 + ptr::copy_nonoverlapping(buffer.inner.as_ptr(), dest, len); + } + len + } else { + // 공간 부족 시 복사를 수행하지 않음 (단, 원본 buffer는 그대로 소멸하여 보안 유지) + 0 + } +} + /// Java 측 `SensitiveDataContainer`가 소유한 네이티브 메모리 세그먼트(memory segment)를 /// 안전하게 소거(zeroize)하는 ffi 엔드포인트입니다. ///