diff --git a/.github/workflows/cc-constant-time-audit.yml b/.github/workflows/cc-constant-time-audit.yml new file mode 100644 index 0000000..4d4402a --- /dev/null +++ b/.github/workflows/cc-constant-time-audit.yml @@ -0,0 +1,112 @@ +name: CC Constant-Time Hybrid Audit + +on: + push: + branches: [ "master", "release/*" ] + pull_request: + branches: [ "master" ] + +# 실제 운영(Production) 및 폐쇄망(Air-Gapped) 환경 적용 시에는 외부 Github Actions Runner 대신 +# 내부망에 구축된 Self-hosted Runner와 사내 Harbar같은 레지스트리에 검증 도구(cargo-show-asm, valgrind)가 +# 사전 설치된 무결성 컨테이너 이미지를 `container:` 지시어를 통해 사용해야 합니다!!! + +jobs: + # 정적 어셈블리 검증 + static-asm-audit: + name: Level 1 - Static ASM Analysis + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + target: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + + steps: + - name: Checkout Source Code + uses: actions/checkout@v3 + + - name: Install Rust Toolchain + uses: dtolnay/rust-toolchain@1.93.1 + with: + components: rust-src + targets: ${{ matrix.target }} + + # 폐쇄망 적용 시 이 단계는 생략되고 사전 빌드된 컨테이너 내장 도구를 사용해야 함 + - name: Install cargo-show-asm (Supply Chain Audit Required) + run: cargo install cargo-show-asm --locked + + - name: Inspect and Verify Assembly Output + shell: bash + run: | + echo "[entlib-native asm ct audit] 대상 아키텍처: ${{ matrix.target }}" + + if [[ "${{ matrix.target }}" == *"x86_64"* ]]; then + FORBIDDEN_REGEX="\b(je|jne|jg|jge|jl|jle|ja|jae|jb|jbe)\b" + elif [[ "${{ matrix.target }}" == *"aarch64"* ]]; then + FORBIDDEN_REGEX="\b(b\.[a-z]{2}|cbz|cbnz|tbz|tbnz)\b" + else + echo "지원하지 않는 아키텍처입니다!" + exit 1 + fi + + # 1. 추출 스크립트 실행 (동적 audit_wrapper 생성 포함) + chmod +x scripts/extract_ct_symbols.sh + scripts/extract_ct_symbols.sh ${{ matrix.target }} + + # 2. JSON 파싱을 통해 bash 배열로 복원 + mapfile -t DYNAMIC_TARGETS < <(jq -r '.[]' target_functions.json) + + # 3. 추출된 동적 래퍼 심볼에 대해 전수 검증 수행 + for FUNC in "${DYNAMIC_TARGETS[@]}"; do + echo "-> $FUNC 검증 중..." + + cargo asm -p entlib-native-constant-time --lib --features audit_mode --rust --target ${{ matrix.target }} --release "$FUNC" > asm_output.txt + + if grep -q -E "$FORBIDDEN_REGEX" asm_output.txt; then + echo "[CRITICAL] $FUNC 에서 타이밍 공격을 유발할 수 있는 조건부 분기 명령어가 탐지되었습니다!" + grep -E "$FORBIDDEN_REGEX" asm_output.txt + exit 1 + fi + done + echo "[SUCCESS] 추출된 ${#DYNAMIC_TARGETS[@]}개 함수의 어셈블리 무결성 전수 확인 완료" + + # 동적 통계적 타이밍 분석 (DudeCT) TODO: 퀀트 컨테이너 안정화 후 가동 (필요 자료에 대한 폐쇄망 무결성 검증 중) +# dynamic-timing-audit: +# name: Level 2 - Dynamic Timing Analysis (DudeCT) +# needs: static-asm-audit +# runs-on: [self-hosted, bare-metal] +# steps: +# - uses: actions/checkout@v3 +# - name: Install Rust Toolchain (Nightly) +# uses: dtolnay/rust-toolchain@nightly +# - name: Enforce CPU Frequency Scaling Policy (Linux) +# run: sudo cpupower frequency-set --governor performance || true +# - name: Run DudeCT Statistical Analysis +# run: | +# echo "[검증] DudeCT 기반 런타임 통계 분석 시작" +# scripts/catch_hex_dudect_audit.sh + + # 바이너리 메모리 오염 추적 (Taint Analysis) TODO: 어떻게 동작하는지 아직 잘 모르겠어서 추 후 재검토 +# taint-tracking-audit: +# name: Level 3 - Taint Flow Tracking (Valgrind) +# needs: static-asm-audit +# runs-on: ubuntu-latest +# steps: +# - name: Checkout Source Code +# uses: actions/checkout@v3 +# - name: Install Rust Toolchain (Nightly Only) +# uses: dtolnay/rust-toolchain@nightly +# with: +# components: rust-src +# # 폐쇄망 적용 시 이 단계는 생략되고 사전 빌드된 컨테이너(valgrind 내장)를 사용해야 함 +# - name: Install System Dependencies for C-FFI and Valgrind +# run: | +# sudo apt-get update && sudo apt-get install -y build-essential valgrind libc6-dbg +# cargo install cargo-valgrind --locked +# # 여기까지 +# +# - name: Run Memcheck with Uninitialized Value Tracking +# run: | +# echo "[검증] Valgrind Memcheck 기반 분기 오염 역추적" +# cargo +nightly valgrind test -p entlib-native-constant-time --test taint_audit --features valgrind_taint_audit -- --nocapture \ No newline at end of file diff --git a/.github/workflows/verify_ct_asm.yml b/.github/workflows/verify_ct_asm.yml index 0ef9d56..e1a4322 100644 --- a/.github/workflows/verify_ct_asm.yml +++ b/.github/workflows/verify_ct_asm.yml @@ -2,9 +2,9 @@ name: Constant-Time Assembly Verification on: push: - branches: [ "master", "fix/*", "feature/*" ] + branches: [ "master", "fix/*" ] pull_request: - branches: [ "master", "fix/*", "feature/*" ] + branches: [ "master" ] env: CARGO_TERM_COLOR: always diff --git a/COMPLIANCE.md b/COMPLIANCE.md index df6f24c..96932ee 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -12,23 +12,38 @@ NIST CAVP는 단일 알고리즘에 대한 검증 작업입니다. 실제 프로 - [ ] SP 800-90A DRBG(Deterministic Random Bit Generators) +> [KCMVP](https://seed.kisa.or.kr/kisa/kcmvp/EgovVerification.do) (KS X ISO/IEC 18031) + +- [ ] Security techniques - Hash-functions - Part 3: Dedicated hash-functions (2018) + ## SHA2 -> [NIST CAVP - Secure Hashing](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing) (ISO/IEC 10118-3) +> [NIST CAVP - Secure Hashing](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing) - [ ] 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 +> [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) +> [NIST CAVP - Secure Hashing](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/secure-hashing) - [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 +> [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) + +## HKDF + +> [NIST CAVP - Key Derivation](https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/key-derivation) (SP 800-108) -- [X] Security techniques - Hash-functions - Part 3: Dedicated hash-functions (2018) \ No newline at end of file +- [ ] KDF in Counter Mode Test Vectors +- [ ] KDF in Feedback Mode Test Vectors where no counter is used +- [ ] KDF in Feedback Mode Test Vectors where zero length IV is allowed +- [ ] KDF in Feedback Mode Test Vectors where zero length IV is not allowed +- [ ] KDF in Double-Pipeline Iteration Mode Test Vectors where no counter is used +- [ ] KDF in Double-Pipeline Iteration Mode Test Vectors where counter is used \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 40ddfd6..ba96379 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -1,5 +1,7 @@ # 기여 및 라이선스 +> [English CONTRIBUTION](CONTRIBUTION_EN.md) + _안녕하세요. 저희는 팀 퀀트(Quant)이며, 저는 Quant Theodore Felix라고 합니다._ **이 프로젝트에 기여해주시는 모든 여러분에게 정말 감사드리며,** 몇 가지 사전 준비 사항을 알려드리고자 합니다. 우선, 이 프로젝트는 [`MIT LICENSE`](LICENSE)를 따릅니다. @@ -50,6 +52,19 @@ _안녕하세요. 저희는 팀 퀀트(Quant)이며, 저는 Quant Theodore Felix - 데이터 소거에 대해, 그 방식 또는 현재 구현된 로직에 대해 의견이 있으신가요? - **현재 구현된 전반적인 보안 로직의 보안성이 엄밀하지 않다고 판단되시나요?** +# 최신 릴리즈 업데이트 기준 주요 기여 + +이 프로젝트에 있어 다음 항목에 해당하는 기여는 저희가 1순위로 검토합니다(물론 보안 기여는 0순위입니다). + +- 공통 + - 많은 크레이트의 핵심 기능은 `Result` 열거형을 통해 `SecureBuffer` 구조체와 문자열 참조를 반환합니다. 이는 오류 전파에 부적절합니다. +- 보안 버퍼 크레이트 `entlib-native-secure-buffer` + - `zeroizer.rs` 내 no_std 폐쇄 환경을 위한 Fall-back 시, 해당 환경의 하드웨어(CPU) 특성에 따라 캐시 라인 플러시가 보장되지 않을 수 있다고 합니다. 이 부분에 대해 섬세한 평가검증이 필요합니다. +- CI 워크플로 + - CC 상수-시간 감사 워크플로의 Level 3(바이너리 메모리 오염 추적)은 Unix 환경에서 Valgrind를 사용하여 테스트를 수행합니다. 하지만 저는 아직 이 부분에 대해 큰 아이디어가 없어 임시 비활성화해둔 상태입니다. 이 부분에 대해 좋은 아이디어를 가지고 있다면 알려주세요. + +# 연락 + 여러분은 위 내용에 있어 (특정 항목만 제외하고) 어떤 방식으로든 저희에게 연락하실 수 있습니다. -이메일 또는 디스코드 `qtfelix`를 사용하실 수 있습니다. \ No newline at end of file +이메일 또는 디스코드 `qtfelix`를 사용하실 수 있습니다. diff --git a/CONTRIBUTION_EN.md b/CONTRIBUTION_EN.md index 3eba08e..725714d 100644 --- a/CONTRIBUTION_EN.md +++ b/CONTRIBUTION_EN.md @@ -1,5 +1,7 @@ # Contribution and License +> [Korean CONTRIBUTION](CONTRIBUTION.md) + _Hello. We are Team Quant, and I am Quant Theodore Felix._ **We truly appreciate everyone who contributes to this project,** and we would like to inform you of a few prerequisites. First of all, this project follows the [`MIT LICENSE`](LICENSE). @@ -50,6 +52,19 @@ The above rules might just be an eyesore. If you fall into one of the following - Do you have opinions on data erasure, its method, or the currently implemented logic? - **Do you judge that the security of the overall security logic currently implemented is not rigorous?** +# Key Contributions Based on Latest Release Updates + +For this project, contributions corresponding to the following items are our top priority for review (of course, security contributions are priority 0). + +- Common + - Core functions in many crates return a `SecureBuffer` struct and a string reference via the `Result` enum. This is inappropriate for error propagation. +- Security Buffer Crate `entlib-native-secure-buffer` + - During fall-back for `no_std` isolated environments in `zeroizer.rs`, it is said that cache line flushing may not be guaranteed depending on the hardware (CPU) characteristics of that environment. Delicate evaluation and verification are required for this part. +- CI Workflow + - Level 3 (Binary Memory Corruption Tracking) of the CC Constant-Time Audit workflow performs tests using Valgrind in Unix environments. However, I do not have a significant idea for this part yet, so it is temporarily disabled. If you have any good ideas regarding this, please let us know. + +# Contact + You can contact us in any way regarding the above content (with the exception of specific items). You can use email or Discord `qtfelix`. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 36db20e..9f1b291 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,42 @@ [workspace] -members = ["internal/*", "crypto/*"] +members = ["internal/*", "crypto/*", "core/*"] resolver = "2" [workspace.package] -version = "1.1.2-Alpha" +version = "1.1.2" edition = "2024" authors = ["Q. T. Felix "] license = "MIT LICENSE" [profile.release] -strip = true +#strip = true +# 패닉 발생 시 스택 풀기를 비활성화하여 메모리 상태가 +# 외부에 노출되거나 조작될 가능성을 차단할 필요 있음 +#panic = "abort" +#lto = true +#codegen-units = 1 +#strip = true + +[profile.dev] +panic = "abort" [workspace.dependencies] -### INTERNAL CORE DEPENDENCIES ### -entlib-native-ffi = { path = "./internal/ffi", version = "1.1.2-Alpha" } -entlib-native-quantum-util = { path = "./internal/quantum-util", version = "1.1.2-Alpha" } -### EXTERNAL DEPENDENCIES ### -tokio = { version = "1", features = ["rt", "process"] } +### INTERNAL DEPENDENCIES ### +entlib-native-ffi = { path = "internal/ffi", version = "1.1.2" } +entlib-native-quantum-util = { path = "internal/quantum-util", version = "1.1.2" } +### CORE DEPENDENCIES ### +entlib-native-hex = { path = "core/hex", version = "1.1.2" } +entlib-native-result = { path = "core/result", version = "1.1.2" } +entlib-native-base64 = { path = "core/base64", version = "1.1.2" } +entlib-native-secure-buffer = { path = "core/secure-buffer", version = "1.1.2" } +entlib-native-constant-time = { path = "core/constant-time", version = "1.1.2" } ### INTERNAL CRYPTO DEPENDENCIES ### -entlib-native-base64 = { path = "./crypto/base64", version = "1.1.2-Alpha" } -entlib-native-constant-time = { path = "./crypto/constant-time", version = "1.1.2-Alpha" } -entlib-native-core-secure = { path = "./crypto/core-secure", version = "1.1.2-Alpha" } -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" } +entlib-native-rng = { path = "crypto/rng", version = "1.1.2" } +entlib-native-tls = { path = "crypto/tls", version = "1.1.2" } +entlib-native-hkdf = { path = "crypto/hkdf", version = "1.1.2" } +entlib-native-hmac = { path = "crypto/hmac", version = "1.1.2" } +entlib-native-sha2 = { path = "crypto/sha2", version = "1.1.2" } +entlib-native-sha3 = { path = "crypto/sha3", version = "1.1.2" } +entlib-native-chacha20 = { path = "crypto/chacha20", version = "1.1.2" } +entlib-native-key-establishment = { path = "crypto/key-establishment", version = "1.1.2" } +entlib-native-digital-signature = { path = "crypto/digital-signature", version = "1.1.2" } diff --git a/SECURE_NOTE.md b/SECURE_NOTE.md new file mode 100644 index 0000000..a6e753b --- /dev/null +++ b/SECURE_NOTE.md @@ -0,0 +1,121 @@ +# 주체 간 상호 작용 + +얽힘 라이브러리는 Java 베이스(주체), 그 네이티브(entlib-native)는 Rust 베이스입니다. 이 문서에선 편의를 위해 이 두 주체를 통합하여 "얽힘 라이브러리 프로젝트(ELIB)", 개별적으로는 각각 Java, Rust로 언급하겠습니다. + +ELIB에서 데이터의 생성은 다음 두 가지 케이스가 존재합니다. + +- Rust 측 최초 생성 +- Java 측 최초 생성 + +데이터가 선언된다면 반드시 "메모리를 할당한 주체가 메모리를 해제해야 한다." 라는 대원칙을 지켜야 합니다. 이를 고수하지 않으면 심각한 메모리 손상(Memory Corruption)이나 세그멘테이션 결함(Segmentation Fault)이 발생하여 시스템 전체의 안정성이 붕괴됩니다. + +ELIB은 이러한 대원칙을 준수하면서도, Java, Rust 측에서 "내가 해제를 해야하는거였어?!" 라는 오해를 해결하기 위해 "통합 제어 아키텍처(Unified Control Architecture, UCA)"를 제공합니다. + +## 통합 제어 아키텍처 + +UCA는 Java, Rust 각각에서 생성된(할당된) 데이터에 대해, 할당 해제(소유권) 여부를 해당 주체의 비즈니스 로직이 알 필요 없도록 하는 데 중점을 둡니다. + +Rust는 ELIB 네이티브로써, FFI 경계 통신을 위해 소유권 플래그가 포함된 C-호환 구조체를 FFI 표준 규격으로 정의했습니다. + +```rust +use entlib_native_ffi::FFIStandard; // 구조체 +``` + +Rust가 최초로 데이터를 생성하는 경우를 Rust-Owned(RO) 패턴, 동일한 상황에 대해 Java도 똑같이 JO 패턴이라고 하겠습니다. 이 표준 구조체의 필드엔 `bool`타입의 `is_rust_owned` 가 존재합니다. 이 값이 `true`일 경우 RO 패턴이 적용되고, 그렇지 않으면 JO 패턴이 적용됩니다. + +RO 패턴은 연산 수행 후 Rust 측 해제 전용 함수를 호출할 수 있습니다. + +```rust +use entlib_native_secure_buffer::buffer::entlib_side_secure_free; // 함수 +``` + +JO 패턴은 연산 수행 후 FFM API를 통해 `Arena#close()` 메소드를 사용하여 해제해야 합니다. 물론 이 기능은 직접 Raw하게 사용할 필요 없이 `SDCScopeContext` 객체를 사용할 수 있습니다. + +ELIB은 이러한 방식으로 RO, JO 패턴의 파편화를 해결했습니다. + +## RO 패턴을 통한 데이터 생성 시 + +그렇다면 RO 패턴을 통해 데이터가 생성되면 `FFIStandard` 구조체를 사용할 필요가 없을까요? 절대로 아닙니다. + +RO 패턴은 Rust 측에서 최초로 데이터를 생성하는 경우입니다. 암호화 키나 난수 등 Rust에서 안전하게 생성된 이 데이터는 결국 Java 측 비즈니스 로직에서 사용되어야 합니다. + +데이터 자체는 Rust의 메모리 공간에 `mlock`되어 있지만, Java가 이 데이터를 읽기 위해서는 원시 포인터와 길이를 전달받아야 합니다. 이때 Rust가 단순히 포인터만 던져주면, Java의 컨텍스트 객체는 이 메모리의 주인이 누구인지 알 방법이 없습니다. + +따라서 Rust는 데이터를 생성한 직후 해당 포인터와 길이, 그리고 소유권 플래그를 `FFIStandard` 구조체에 담아 Java로 반환해야 합니다. 이 구조체 내의 `is_rust_owned` 값이 true로 설정되어 있어야만, Java 측 컨텍스트가 생명 주기를 추적하다가 스코프가 끝나는 시점에 올바르게 Rust 측 해제 전용 함수 `entlib_side_secure_free`를 호출할 수 있습니다. + +Java는 `FFIStandard` 구조체를 전달받고 `ExternFFIStandard` 객체가 이를 래핑합니다. 스코프 종료 시 컨텍스트가 소유권 플래그를 확인하고 Rust 측 해제 함수 `entlib_side_secure_free`를 통해 소거 명령을 내립니다. + +## FFIStandard vs SecureBuffer + +내부 필드는 각각의 구조체가 동일한 연산을 취하는 것 처럼 보입니다만, 이 둘은 그 역할이 완전히 다릅니다. + +Rust -> Java의 경우, Rust 로직이 안전한 `SecureBuffer` 내부에서 민감 데이터를 모두 생성합니다. 외부로 보낼 때가 되면, `SecureBuffer`가 파기(`Drop`)되지 않도록 소유권을 해제(`into_raw` 방식)하고, 그 알맹이(`ptr`, `len`)를 꺼내 `FFIStandard`에 담아 Java로 보냅니다. + +반대로 Java -> Rust의 경우, Java 작업이 끝나고 파기 요청이 오면 Rust는 전달받은 `FFIStandard`의 `ptr`과 `len`을 다시 `SecureBuffer::from_raw_parts`로 감쌉니다. 감싸진 `SecureBuffer`가 함수 스코프를 종료하면서, 내장된 `Drop` 로직을 통해 완벽한 소거와 잠금 해제가 자동으로 수행되는 것입니다. + +따라서 이 두 구조체는 서로를 대체하는 것이 아니라, 통신 시점에 서로의 형태로 변환(Transformation)되는 관계입니다. + +## RO 및 JO 내부 로직 생명 주기 + +**RO 패턴**은 Rust가 주도적으로 보안 데이터를 생성하고, 소멸의 책임도 지는 구조입니다. + +- 할당 (Rust): Rust 내부에서 메모리를 할당, 0으로 초기화, OS 메모리 락(lock)을 수행합니다. +- 전달 (Rust -> Java): Rust는 `SecureBuffer`가 소멸(`Drop`)되지 않도록 우회한 뒤, `ptr`, `len`, 그리고 `is_rust_owned = true` 플래그를 `FFIStandard` 구조체에 담아 Java로 반환합니다. +- 사용 (Java): Java의 `ExternFFIStandard`가 이를 래핑하여 비즈니스 로직에 사용합니다. +- 파기 요청 (Java -> Rust): Java의 스코프가 종료되면, 컨텍스트가 플래그를 확인하고 Rust 측 FFI 함수인 `entlib_side_secure_free`를 호출합니다. +- 소거 및 해제 (Rust): Rust는 전달받은 포인터를 다시 `SecureBuffer`로 감싸 스코프를 종료시킵니다. 이때 내장된 Drop 로직이 발동하여 물리적 소거, 잠금 해제, 메모리 할당 해제를 원자적으로 완수합니다. + +**JO 패턴**은 Java가 민감 데이터 컨테이너(SDC) 객체인 `SensitiveDataContainer`(또는 `SDCScopeContext`)를 통해 데이터 공간을 선언하고, 물리적 소거 기능만 Rust의 강력한 제어력을 빌려 쓰는 구조입니다. + +- 할당 (Java): SDC 내부에서 FFM API의 `Arena`를 통해 `off-heap` 메모리를 할당 및 선언, OS 메모리 락(lock)을 수행합니다. +- 사용 (Java <-> Rust): 필요한 경우 해당 포인터를 `FFIStandard(is_rust_owned = false)`를 통해 Rust로 보내 연산을 수행합니다. +- 소거 요청 (Java -> Rust): `try-with-resources` 스코프를 벗어나는 시점에 Java는 메모리를 즉시 해제하지 않고 Rust 측에 물리적 데이터 소거 로직 수행을 먼저 요청합니다. +- 최종 해제 (Java): Rust의 철저한 소거(`Zeroizer::zeroize_raw`)가 완료되었음을 확인한 후, Java 측 컨텍스트가 `Arena`를 닫아(`close()`) 최종적으로 메모리를 할당 해제합니다. + +## 정리: 개별 패턴에 따른 상호 작용 + +이 장에서의 설명을 한꺼번에 정리하면 다음과 같습니다. + +Java와 Rust 간의 데이터 통신은 소유권 플래그가 포함된 `FFIStandard` 구조체를 통해 이루어집니다. 이 구조체 내부의 `is_rust_owned` 값에 따라 메모리 해제의 책임 주체가 결정되며, 시스템은 RO 패턴과 JO 패턴으로 분기합니다. + +### RO 패턴 + +RO 패턴은 Rust가 주도적으로 보안 데이터를 생성하고, 소멸의 책임도 지는 구조입니다. 암호화 키나 난수 등 최고 수준의 보안이 필요한 데이터를 생성할 때 사용됩니다. + +* **메모리 할당**: Rust 내부에서 `SecureMemoryBlock`을 사용하여 페이지 크기 배수로 올림 처리된 메모리를 할당합니다. +* **보안 적용**: 할당된 메모리는 즉시 0으로 초기화되며, OS 레벨에서 메모리 페이징을 방지하기 위한 메모리 락(`mlock` 또는 `VirtualLock`, OS별 상이)이 수행됩니다. +* **Java로의 전달**: Rust는 내부의 `SecureBuffer`가 파기되지 않도록 소유권을 해제합니다. 이후 `ptr`, `len`, 그리고 `is_rust_owned = true` 플래그를 `FFIStandard` 구조체에 담아 Java로 반환합니다. +* **Java 측 사용**: Java의 `ExternFFIStandard` 객체가 이를 래핑하여 비즈니스 로직에서 안전하게 읽습니다. +* **파기 및 원자적 소거**: Java의 스코프가 종료되면, 컨텍스트가 플래그를 확인하고 Rust 측 해제 전용 함수인 `entlib_side_secure_free`를 호출합니다. Rust는 포인터를 다시 `Box::from_raw`를 통해 회수하여 명시적으로 `Drop`을 발생시킵니다. 이 과정에서 `Zeroizer::zeroize_raw`를 통한 하드웨어 수준의 강제 물리적 소거, OS 잠금 해제, 메모리 할당 해제가 안전하게 연쇄 호출됩니다. + +### JO 패턴 + +JO 패턴은 Java가 데이터 공간을 선언하고, 물리적 소거 기능만 Rust의 강력한 제어력을 빌려 쓰는 Zero-Trust 기반 구조입니다. 외부 시스템에서 주입된 메모리의 안전한 파기를 보장합니다. + +* **메모리 선언**: Java 측 `SensitiveDataContainer` 내부에서 FFM API의 `Arena`를 통해 메모리를 할당합니다. +* **보안 적용**: 할당된 메모리는 OS 레벨에서 메모리 페이징을 방지하기 위한 메모리 락이 수행됩니다. +* **Rust로의 전달**: 연산이 필요한 경우, Java는 `is_rust_owned = false`로 설정된 `FFIStandard` 구조체를 Rust로 보냅니다. +* **보안 검증**: Rust의 `SecureBuffer::from_raw_parts` 함수는 외부에서 주입된 메모리가 페이지 경계(`PAGE_SIZE` 배수)에 맞게 정렬되었는지 엄격하게 검증하여 부채널 공격 등 보안 취약점을 차단합니다. +* **소거 요청**: 연산 후 Java의 `try-with-resources` 스코프를 벗어나는 시점에, Java는 메모리를 즉시 해제하지 않고 Rust에 물리적 데이터 소거 로직 수행을 먼저 요청합니다(민감한 암호화 로직의 경우 소거는 Rust 측에서 미리 수행되고, Java에게 상태 코드를 전달할 수도 있습니다). +* **물리적 소거**: Rust 측에서 `SecureBuffer`가 `Drop`되며 전체 메모리 용량(`capacity`)에 대해 0으로 덮어씁니다. 이때 Rust는 메모리 소유권이 없으므로(`owned_block: None`), 메모리 해제는 수행하지 않고 외부 소유 메모리의 잠금만 해제합니다. +* **최종 해제 및 규정 준수**: Rust의 소거가 완료되었음을 확인한 후, Java 측 컨텍스트가 `Arena`를 닫아(`close()`) 최종적으로 메모리를 해제합니다. 이로써 데이터 생명 주기가 단일 병목점을 통과하여 FIPS 140 규격을 충족합니다. + +### 핵심 구조체 역할 비교 + +`FFIStandard`와 `SecureBuffer`는 서로를 대체하는 것이 아니라, 통신 시점에 서로의 형태로 변환되는 관계입니다. + +| 특성 | RO 패턴 | JO 패턴 | +|----------------------|-----------------------------------------------------------|----------------------------------------------------| +| **초기 메모리 할당** | Rust (`SecureMemoryBlock::allocate_locked`) | Java (`Arena`) | +| **FFIStandard 플래그** | `is_rust_owned = true` | `is_rust_owned = false` | +| **SecureBuffer의 역할** | 데이터 생성 후 소유권을 우회하여 `FFIStandard`로 알맹이 추출 | `FFIStandard`의 포인터를 래핑(`from_raw_parts`)하여 검증 후 소거 | +| **최종 메모리 해제** | Rust (`entlib_side_secure_free` -> `deallocate_unlocked`) | Java (`Arena#close()`) | + +## 인증 및 규정 준수 측면 + +FIPS 140에 따르면 어떤 데이터의 전체 생명 주기는 하나의 객체에서 관리되어야 합니다(단일 병목점을 통과해야 합니다). JO 패턴 측면에서 볼 때 데이터는 반드시 위의 객체로만 생성 가능합니다. 따라서 ELIB은 다음의 생명 주기가 완벽히 보장되어, FIPS 140을 준수합니다. + +1. JO 패턴에서 컨텍스트 객체를 통해 데이터 선언 +2. 연산 수행(필요한 경우 Rust 연산 수행) +3. 컨텍스트가 `try-with-resouces` 스코프를 벗어남 -> Rust 측에서 데이터 소거 로직 수행 +4. 데이터 소거 완료 -> 지정된 `Arena`를 닫고 할당 해제 \ No newline at end of file diff --git a/benchmarks/ct-bench.md b/benchmarks/ct-bench.md index e493dec..cf635e5 100644 --- a/benchmarks/ct-bench.md +++ b/benchmarks/ct-bench.md @@ -2,134 +2,14 @@ > [이 벤치마킹은 어떻게 수행되나요?](README.md) -## 보안성 평가 +## DudeCT 벤치마킹 -상수-시간(Constant-Time, CT) 연산의 보안성 평가는 `constant_time.rs`의 각 함수에 대해 진행됩니다. 모든 함수는 `u8`, `u32`, `u64`, `u128` 타입에 대해 수행되었습니다. +이 벤치마킹은 다음과 같이 수행할 수 있습니다. -- [`ct_eq`](#ct_eq) -- [`ct_is_negative`](#ct_is_negative) -- [`ct_is_nonzero`](#ct_is_nonzero) -- [`ct_is_zero`](#ct_is_zero) -- [`ct_ne`](#ct_ne) -- [`ct_select`](#ct_select) +```bash +# 빌드 +$ cargo +nightly build --release -p entlib-native-constant-time --bench dudect_audit -Base64 인코딩 보안성 평가는 "문자 클래스별 타이밍 비교"를 엄밀하게 분석한 결과를 나타냅니다. - -> [!NOTE] -> 이 벤치마킹이 수행되는 환경은 `x86_64` 아키텍처이며 이는 Tier 1(full ASM), 2(barrier)에 해당됩니다. 인라인 어셈블리 로직으로 인해 `aarch64` 환경에서의 벤치마킹이 추가적으로 필요하며, 이 작업을 완료하면 아키텍처별로 문서를 나누어 정리하겠습니다. - -이 벤치마킹의 결과를 정리함에 있어 다중 베이스라인에 대한 비교치를 포함하지 않겠습니다. 상수-시간 연산에는 크게 의미 없는 수치이기 떄문입니다. 또한, 결과 표에는 검사 내용의 추정치만을 기록하겠습니다. **단위 확인에 유의하세요.** - -### ct_eq - -이 함수에서 개별 타입에 대한 검사 항목은 다음과 같습니다. - -- `equal_ones` -- `equal_zeros` -- `hamming_1_diff` -- `hamming_max` -- `random` - -#### u8 - -![security.ct.ct-eq.u8.violin.png](../public/assets/benchmarks/constant-time/security/security.ct.ct-eq.u8.violin.png) - -| 함수 | 기울기 | 평균 | 중앙값 | 중앙값 절대 편차 | -|------------------|:--------:|:--------:|:--------:|:---------:| -| `equal_ones` | 1.2367ns | 1.3136ns | 1.2319ns | 50.322ps | -| `equal_zeros` | 1.2238ns | 1.2330ns | 1.2114ns | 42.458ps | -| `hamming_1_diff` | 1.3581ns | 1.3211ns | 1.2210ns | 55.625ps | -| `hamming_max` | 1.3556ns | 1.2787ns | 1.2154ns | 50.333ps | -| `random` | 1.2334ns | 1.2533ns | 1.2246ns | 41.258ps | - -#### u32 - -![security.ct.ct-eq.u32.violin.png](../public/assets/benchmarks/constant-time/security/security.ct.ct-eq.u32.violin.png) - -| 함수 | 기울기 | 평균 | 중앙값 | 중앙값 절대 편차 | -|------------------|:--------:|:--------:|:--------:|:---------:| -| `equal_ones` | 1.5142ns | 1.6185ns | 1.5130ns | 71.710ps | -| `equal_zeros` | 1.5007ns | 1.5167ns | 1.4814ns | 52.763ps | -| `hamming_1_diff` | 1.6970ns | 1.5848ns | 1.4877ns | 62.003ps | -| `hamming_max` | 1.4963ns | 1.5180ns | 1.4858ns | 58.120ps | -| `random` | 1.5075ns | 1.5161ns | 1.4886ns | 61.396ps | - -#### u64 - -![security.ct.ct-eq.u64.violin.png](../public/assets/benchmarks/constant-time/security/security.ct.ct-eq.u64.violin.png) - -| 함수 | 기울기 | 평균 | 중앙값 | 중앙값 절대 편차 | -|------------------|:--------:|:--------:|:--------:|:---------:| -| `equal_ones` | 1.4959ns | 1.5097ns | 1.4782ns | 55.508ps | -| `equal_zeros` | 1.6809ns | 1.6181ns | 1.4948ns | 70.550ps | -| `hamming_1_diff` | 1.5004ns | 1.5059ns | 1.4717ns | 61.653ps | -| `hamming_max` | 1.5795ns | 1.7985ns | 1.5303ns | 108.93ps | -| `random` | 1.8837ns | 1.6573ns | 1.5150ns | 101.51ps | - -#### u128 - -![security.ct.ct-eq.u128.violin.png](../public/assets/benchmarks/constant-time/security/security.ct.ct-eq.u128.violin.png) - -| 함수 | 기울기 | 평균 | 중앙값 | 중앙값 절대 편차 | -|------------------|:--------:|:--------:|:--------:|:---------:| -| `equal_ones` | 6.3781ns | 6.4470ns | 6.2735ns | 261.01ps | -| `equal_zeros` | 6.3915ns | 6.5176ns | 6.3836ns | 271.79ps | -| `hamming_1_diff` | 6.7002ns | 6.8767ns | 6.2641ns | 284.01ps | -| `hamming_max` | 6.2321ns | 6.2935ns | 6.1827ns | 231.66ps | -| `random` | 6.2785ns | 6.3071ns | 6.1993ns | 231.12ps | - -#### 종합 - -**모든 데이터 타입에서 상수 시간 특성이 강력하게 유지되고 있습니다.** - -입력값의 특성(모두 같음, 비트 하나 다름, 완전히 다름 등)에 따른 실행 시간의 편차(variance)가 극히 미미하며, 이는 양자-내성 암호(Post-Quantum Cryptography, PQC) 구현에 필수적인 보안 요구사항을 충족합니다. 티어별 다음의 결과로 나타낼 수 있습니다. - -- Tier 1(Full ASM, `u32`/`u64`) - - 아키텍처별 어셈블리 명령어를 직접 사용하여 가장 안정적인 타이밍을 보입니다. -- Tier 2(Barrier + Rust Logic, `u8`/`u128`) - - 컴파일러 최적화 방지 배리어(`ct_fence`)와 비트 연산을 조합한 방식으로, 순수 ASM보다는 오버헤드가 있으나 여전히 훌륭한 상수 시간성을 보장합니다. - -`u128`은 네이티브 레지스터 크기를 초과하므로, 소스 코드에서 두 개의 `u64`로 분할하여 처리합니다. 이 데이터 타입에서 `u64`($`\approx 1.5\text{ns}`$) 대비 약 4배 느린 $`\approx 6.3\text{ns}`$를 기록했습니다. 이는 단순히 두 번 연산하는 것을 넘어, 상위/하위 비트 분할, 별도의 `ct_fence` 호출, 그리고 어셈블리가 아닌 Rust 로직(`impl_ct_rust_ops!`)을 타기 때문에 발생하는 오버헤드입니다. - -안정성 측면에서 절대적인 시간은 길어졌으나, `equal_zeros`($6.38\text{ns}$)와 `hamming_max`($6.18\text{ns}$)의 차이는 전체 실행 시간 대비 매우 작으므로 **상수 시간 성질은 잘 유지**되고 있습니다. - -> [!NOTE] -> 현재 `u128` 구현은 범용성을 위해 Rust 로직을 사용합니다. -> 만약 PQC 알고리즘에서 `u128` 연산 비중이 매우 높다면, `x86_64`의 SSE/AVX 또는 `AArch64`의 NEON 레지스터를 활용한 SIMD 기반 인라인 어셈블리를 도입하여 `u64`의 2배 수준($`\approx 3.0\text{ns}`$)으로 단축할 여지가 있을 것 같습니다. (단, 현재의 $`6.3\text{ns}`$도 보안적으로는 문제가 없다고 판단됩니다.) - -결론적으로 이 함수의 모든 케이스에서 입력값에 따른 타이밍 누수(timing leakage)가 관측되지 않았습니다. - -### ct_is_negative - -#### u8 -#### u32 -#### u64 -#### u128 - -### ct_is_nonzero - -#### u8 -#### u32 -#### u64 -#### u128 - -### ct_is_zero - -#### u8 -#### u32 -#### u64 -#### u128 - -### ct_ne - -#### u8 -#### u32 -#### u64 -#### u128 - -### ct_select - -#### u8 -#### u32 -#### u64 -#### u128 \ No newline at end of file +# 실행 +$ ./target/release/deps/dudect_audit-... +``` \ No newline at end of file diff --git a/crypto/base64/Cargo.toml b/core/base64/Cargo.toml similarity index 54% rename from crypto/base64/Cargo.toml rename to core/base64/Cargo.toml index 7bc30a1..687883e 100644 --- a/crypto/base64/Cargo.toml +++ b/core/base64/Cargo.toml @@ -7,10 +7,11 @@ license.workspace = true [dependencies] entlib-native-constant-time.workspace = true +entlib-native-secure-buffer = { workspace = true, features = ["std"] } [dev-dependencies] -criterion = { version = "0.8.2", features = ["html_reports"] } +#criterion = { version = "0.8.2", features = ["html_reports"] } -[[bench]] -name = "base64_bench" -harness = false \ No newline at end of file +#[[bench]] +#name = "base64_bench" +#harness = false \ No newline at end of file diff --git a/core/base64/src/base64.rs b/core/base64/src/base64.rs new file mode 100644 index 0000000..718fd8e --- /dev/null +++ b/core/base64/src/base64.rs @@ -0,0 +1,95 @@ +use entlib_native_constant_time::traits::{ + ConstantTimeEq, ConstantTimeIsNegative, ConstantTimeSelect, +}; + +/// 단일 6비트 값을 상수-시간으로 Base64 문자(ASCII)로 인코딩합니다. +/// +/// # Arguments +/// `c`는 반드시 `0..=63` 범위의 값이어야 합니다. +/// +/// # Security Note +/// - 분기 없는 비트 마스크 연산으로 타이밍 공격을 방어합니다. +/// - `ct_is_negative`: `wrapping_sub` 결과의 MSB를 이용한 상수-시간 범위 판별 +/// - `ct_select`: CPU 분기 없는 조건부 값 선택 +#[inline(always)] +pub fn ct_bin_to_b64_u8(c: u8) -> u8 { + use entlib_native_constant_time::choice::Choice; + + // c < 26 이면 대문자 영역 (MSB 기반 언더플로우 감지) + let mask_upper: Choice = c.wrapping_sub(26).ct_is_negative(); + // 26 <= c < 52 이면 소문자 영역 + let mask_lower: Choice = c.wrapping_sub(52).ct_is_negative() & !mask_upper; + // 52 <= c < 62 이면 숫자 영역 + let mask_digit: Choice = c.wrapping_sub(62).ct_is_negative() & !mask_lower & !mask_upper; + // c == 62 이면 '+' + let mask_plus: Choice = c.ct_eq(&62u8); + // c == 63 이면 '/' + let mask_slash: Choice = c.ct_eq(&63u8); + + let v_upper = c.wrapping_add(65); // c + 'A' (0..=25 → 'A'..='Z') + let v_lower = c.wrapping_add(71); // c - 26 + 'a' (26..=51 → 'a'..='z') + let v_digit = c.wrapping_sub(4); // c - 52 + '0' (52..=61 → '0'..='9') + let v_plus = b'+'; + let v_slash = b'/'; + + // 후순위 마스크가 낮은 것부터 적용: 최종 유효 마스크가 res를 덮어씁니다. + let mut res = 0u8; + res = u8::ct_select(&v_upper, &res, mask_upper); + res = u8::ct_select(&v_lower, &res, mask_lower); + res = u8::ct_select(&v_digit, &res, mask_digit); + res = u8::ct_select(&v_plus, &res, mask_plus); + res = u8::ct_select(&v_slash, &res, mask_slash); + + res +} + +/// 단일 Base64 문자(ASCII)를 상수-시간으로 6비트 값으로 디코딩합니다. +/// +/// # Returns +/// | 반환값 | 의미 | +/// |-------------|------------------------------| +/// | `0x00..=0x3F` | 유효한 Base64 값 (0..=63) | +/// | `0x80` | 공백 문자 (skip 권장) | +/// | `0x81` | 패딩 문자 `'='` | +/// | `0xFF` | 유효하지 않은 문자 | +/// +/// # Security Note +/// - 모든 경로에서 동일한 수의 연산을 수행하여 타이밍 공격을 방어합니다. +#[inline(always)] +pub fn ct_b64_to_bin_u8(b: u8) -> u8 { + use entlib_native_constant_time::choice::Choice; + + // 각 문자 범위에 대한 상수-시간 마스크 생성 + // 범위 [lo, hi): !b.wrapping_sub(lo).ct_is_negative() & b.wrapping_sub(hi).ct_is_negative() + // = b >= lo AND b < hi + let mask_upper: Choice = !b.wrapping_sub(65).ct_is_negative() // b >= 'A' + & b.wrapping_sub(91).ct_is_negative(); // b < '[' + let mask_lower: Choice = !b.wrapping_sub(97).ct_is_negative() // b >= 'a' + & b.wrapping_sub(123).ct_is_negative(); // b < '{' + let mask_digit: Choice = !b.wrapping_sub(48).ct_is_negative() // b >= '0' + & b.wrapping_sub(58).ct_is_negative(); // b < ':' + let mask_plus: Choice = b.ct_eq(&b'+'); + let mask_slash: Choice = b.ct_eq(&b'/'); + let mask_pad: Choice = b.ct_eq(&b'='); + let mask_ws: Choice = b.ct_eq(&b' ') | b.ct_eq(&b'\t') | b.ct_eq(&b'\r') | b.ct_eq(&b'\n'); + + let v_upper: u8 = b.wrapping_sub(65); // 'A'..='Z' → 0..=25 + let v_lower: u8 = b.wrapping_sub(71); // 'a'..='z' → 26..=51 + let v_digit: u8 = b.wrapping_add(4); // '0'..='9' → 52..=61 + let v_plus: u8 = 62; + let v_slash: u8 = 63; + let v_pad: u8 = 0x81; + let v_ws: u8 = 0x80; + let v_invalid: u8 = 0xFF; // 기본값: 무효 + + let mut res = v_invalid; + res = u8::ct_select(&v_upper, &res, mask_upper); + res = u8::ct_select(&v_lower, &res, mask_lower); + res = u8::ct_select(&v_digit, &res, mask_digit); + res = u8::ct_select(&v_plus, &res, mask_plus); + res = u8::ct_select(&v_slash, &res, mask_slash); + res = u8::ct_select(&v_pad, &res, mask_pad); + res = u8::ct_select(&v_ws, &res, mask_ws); + + res +} diff --git a/core/base64/src/lib.rs b/core/base64/src/lib.rs new file mode 100644 index 0000000..35e89ae --- /dev/null +++ b/core/base64/src/lib.rs @@ -0,0 +1,197 @@ +pub mod base64; + +use base64::{ct_b64_to_bin_u8, ct_bin_to_b64_u8}; +use entlib_native_secure_buffer::SecureBuffer; + +/// RFC 4648 표준 Base64 인코딩 함수입니다. +/// +/// 인코딩된 결과는 [SecureBuffer]에 저장됩니다. 반환된 버퍼가 스코프를 벗어나면 +/// OS 레벨의 잠긴(mlock) 메모리가 강제 소거(`Zeroize`)됩니다. +/// +/// # Arguments +/// - `input` 인코딩할 데이터를 담은 [SecureBuffer] +/// +/// # Security Note +/// - 내부적으로 상수-시간 룩업(`ct_bin_to_b64_u8`)을 사용하여 비밀 데이터의 내용에 +/// 의존적인 타이밍 변동을 제거합니다. +/// - 반환 버퍼는 OS의 메모리 락 시스템 콜을 통해 잠겨 디스크 스왑 유출이 방지됩니다. +/// - 외부 크레이트 의존성 없이 순수 Rust로 구현되어 Air-Gapped 환경에서 작동합니다. +/// +/// # Panic +/// OS 메모리 잠금(`mlock`) 실패 또는 메모리 부족 시 `Err(&'static str)`. +/// +/// # Usage +/// ``` +/// use entlib_native_base64::encode; +/// use entlib_native_secure_buffer::SecureBuffer; +/// +/// let mut input = SecureBuffer::new_owned(3).unwrap(); +/// input.as_mut_slice().copy_from_slice(b"Man"); +/// let encoded = encode(&input).unwrap(); +/// assert_eq!(encoded.as_slice(), b"TWFu"); +/// // input, encoded 모두 여기서 Drop되면서 내용이 자동 소거됨 +/// ``` +pub fn encode(input: &SecureBuffer) -> Result { + let input = input.as_slice(); + + let full_groups = input.len() / 3; + let remaining = input.len() % 3; + let out_groups = full_groups + if remaining > 0 { 1 } else { 0 }; + let output_size = out_groups * 4; + + // OS mlock으로 잠긴 페이지 정렬 메모리 할당 (Drop 시 Zeroize) + let mut buf = SecureBuffer::new_owned(output_size)?; + let out = buf.as_mut_slice(); + + // 완전한 3바이트 그룹 처리 + for i in 0..full_groups { + let (b0, b1, b2) = (input[i * 3], input[i * 3 + 1], input[i * 3 + 2]); + out[i * 4] = ct_bin_to_b64_u8(b0 >> 2); + out[i * 4 + 1] = ct_bin_to_b64_u8((b0 & 0x03) << 4 | b1 >> 4); + out[i * 4 + 2] = ct_bin_to_b64_u8((b1 & 0x0F) << 2 | b2 >> 6); + out[i * 4 + 3] = ct_bin_to_b64_u8(b2 & 0x3F); + } + + // 나머지 바이트 처리 (패딩 추가) + let base = full_groups * 4; + let src = full_groups * 3; + if remaining == 1 { + let b0 = input[src]; + out[base] = ct_bin_to_b64_u8(b0 >> 2); + out[base + 1] = ct_bin_to_b64_u8((b0 & 0x03) << 4); + out[base + 2] = b'='; + out[base + 3] = b'='; + } else if remaining == 2 { + let (b0, b1) = (input[src], input[src + 1]); + out[base] = ct_bin_to_b64_u8(b0 >> 2); + out[base + 1] = ct_bin_to_b64_u8((b0 & 0x03) << 4 | b1 >> 4); + out[base + 2] = ct_bin_to_b64_u8((b1 & 0x0F) << 2); + out[base + 3] = b'='; + } + + Ok(buf) +} + +/// RFC 4648 표준 Base64 디코딩 함수입니다. +/// +/// 디코딩된 결과는 [SecureBuffer]에 저장됩니다. 반환된 버퍼가 스코프를 벗어나면 +/// OS 레벨의 잠긴(mlock) 메모리가 강제 소거(`Zeroize`)됩니다. +/// +/// # Arguments +/// - `input` 디코딩할 Base64 문자열을 담은 [SecureBuffer] +/// +/// # Security Note +/// - 모든 문자의 유효성 검사는 상수-시간(`ct_b64_to_bin_u8`)으로 수행됩니다. +/// - 유효성 오류를 나타내는 `invalid` 플래그는 **전체 입력을 처리한 후에만** 검사하여 +/// 조기 종료로 인한 타이밍 정보 유출을 방지합니다. +/// - 반환 버퍼는 OS의 `mlock`/`VirtualLock`으로 잠겨 디스크 스왑 유출이 방지됩니다. +/// - 오류 경로에서 할당된 `SecureBuffer`는 즉시 `Drop`되어 중간값까지 소거됩니다. +/// +/// # Panic +/// - 잘못된 형식(길이, 패딩, 유효하지 않은 문자): `Err("invalid base64: ...")` +/// - OS 메모리 잠금 실패 또는 메모리 부족: `Err("...")` +/// +/// # Usage +/// ``` +/// use entlib_native_base64::decode; +/// use entlib_native_secure_buffer::SecureBuffer; +/// +/// let mut input = SecureBuffer::new_owned(4).unwrap(); +/// input.as_mut_slice().copy_from_slice(b"TWFu"); +/// let decoded = decode(&input).unwrap(); +/// assert_eq!(decoded.as_slice(), b"Man"); +/// // input, decoded 모두 여기서 Drop되면서 내용이 자동 소거됨 +/// +/// let mut invalid = SecureBuffer::new_owned(4).unwrap(); +/// invalid.as_mut_slice().copy_from_slice(b"!!!!"); +/// assert!(decode(&invalid).is_err()); +/// ``` +pub fn decode(input: &SecureBuffer) -> Result { + let input = input.as_slice(); + + if !input.len().is_multiple_of(4) { + return Err("invalid base64: length must be a multiple of 4"); + } + if input.is_empty() { + return SecureBuffer::new_owned(0); + } + + let num_groups = input.len() / 4; + + // 마지막 그룹의 패딩 문자를 미리 확인하여 정확한 출력 크기 계산 + // 패딩 위치는 입력으로부터 공개 정보이므로 분기 허용 + let last = (num_groups - 1) * 4; + let pad3 = (input[last + 3] == b'=') as usize; + let pad2 = (input[last + 2] == b'=') as usize; + let last_group_bytes = 3usize.saturating_sub(pad2 + pad3); + let output_size = (num_groups - 1) * 3 + last_group_bytes; + + // OS mlock으로 잠긴 페이지 정렬 메모리 할당 (Drop 시 Zeroize) + let mut buf = SecureBuffer::new_owned(output_size)?; + let out = buf.as_mut_slice(); + let mut out_pos = 0usize; + + // CT 유효성 누산기: 0x00 = 유효, 0이 아님 = 무효 + // 전체 입력 처리 후에만 검사하여 타이밍 정보 유출 방지 + let mut invalid: u8 = 0; + + for g in 0..num_groups { + let idx = g * 4; + let d = [ + ct_b64_to_bin_u8(input[idx]), + ct_b64_to_bin_u8(input[idx + 1]), + ct_b64_to_bin_u8(input[idx + 2]), + ct_b64_to_bin_u8(input[idx + 3]), + ]; + + // d[0], d[1]은 모든 그룹에서 반드시 유효한 Base64 값(0x00..=0x3F)이어야 함. + // 0x40 이상(0x80=공백, 0x81=패딩, 0xFF=무효)은 비트 6 또는 7이 설정됨. + invalid |= d[0] >> 6; + invalid |= d[1] >> 6; + + if g < num_groups - 1 { + // 비-마지막 그룹: d[2], d[3]도 반드시 유효 + invalid |= d[2] >> 6; + invalid |= d[3] >> 6; + out[out_pos] = (d[0] << 2) | (d[1] >> 4); + out[out_pos + 1] = (d[1] << 4) | (d[2] >> 2); + out[out_pos + 2] = (d[2] << 6) | d[3]; + out_pos += 3; + } else { + // 마지막 그룹: 패딩 처리 (0x81 = '=', 0..=63 = 유효) + if out_pos < out.len() { + out[out_pos] = (d[0] << 2) | (d[1] >> 4); + out_pos += 1; + } + if d[2] == 0x81 { + // '==' 패딩: d[3]도 반드시 '=' + if d[3] != 0x81 { + invalid |= 1; + } + } else if d[2] < 64 { + if out_pos < out.len() { + out[out_pos] = (d[1] << 4) | (d[2] >> 2); + out_pos += 1; + } + if d[3] == 0x81 { + // '=' 패딩 1개: 종료 + } else if d[3] < 64 { + if out_pos < out.len() { + out[out_pos] = (d[2] << 6) | d[3]; + } + } else { + invalid |= 1; // d[3] 무효 + } + } else { + invalid |= 1; // d[2] 무효 + } + } + } + + if invalid != 0 { + // buf는 여기서 Drop되며 중간값을 포함한 내용 자동 소거 + Err("invalid base64: illegal character or padding") + } else { + Ok(buf) + } +} diff --git a/crypto/base64/tests/base64_test.rs b/core/base64/tests/base64_test.rs similarity index 100% rename from crypto/base64/tests/base64_test.rs rename to core/base64/tests/base64_test.rs diff --git a/crypto/constant-time/Cargo.toml b/core/constant-time/Cargo.toml similarity index 51% rename from crypto/constant-time/Cargo.toml rename to core/constant-time/Cargo.toml index 14c54c3..56b1d46 100644 --- a/crypto/constant-time/Cargo.toml +++ b/core/constant-time/Cargo.toml @@ -5,20 +5,24 @@ edition.workspace = true authors.workspace = true license.workspace = true -[features] -ct-tests = [] - [dependencies] [dev-dependencies] -subtle = "2.6.1" -proptest = "1.10.0" -criterion = { version = "0.8.2", features = ["html_reports"] } +dudect-bencher = "0.6.0" +rand = "0.10.0" + +[build-dependencies] +cc = "1.0.106" + +[features] +default = [] +audit_mode = [] +valgrind_taint_audit = [] [[bench]] -name = "ct_ops_security" +name = "dudect_audit" harness = false -[[bench]] -name = "ct_ops_throughput" -harness = false \ No newline at end of file +[[test]] +name = "taint_audit" +path = "tests/taint_audit.rs" \ No newline at end of file diff --git a/core/constant-time/benches/dudect_audit.rs b/core/constant-time/benches/dudect_audit.rs new file mode 100644 index 0000000..ffd40c8 --- /dev/null +++ b/core/constant-time/benches/dudect_audit.rs @@ -0,0 +1,88 @@ +//! TODO: 베어메탈에서 실행되야 할 벤치마킹입니다. 구글링해보니 퍼블릭 클라우드 인프라 서비스는 +//! VM에 노이즈와 하이퍼바이저 개입으로 인해 t-test 값이 오염된다고 합니다. +//! 운영체제 전원 관리 및 CPU 클럭 변동 기술을 BIOS/UEFI 수준에서 비활성화하거나, +//! OS에서 주파수를 고정해야 한다고 하네요... 잘은 모르겠습니다. +//! +//! DudeCT 벤치마크는 최적화 방지 및 런타임 벤치마킹 기능 활용을 위해 nightly 툴체인이 권장됩니다. +//! 다음과 같이 빌드 후 바이너리를 실행하여 벤치마킹 할 수 있습니다. +//! ```bash +//! # 빌드 +//! $ cargo +nightly build --release -p entlib-native-constant-time --bench dudect_audit +//! +//! # 실행 +//! $ ./target/release/deps/dudect_audit-... +//! ``` + +use dudect_bencher::rand::Rng; +use dudect_bencher::{BenchRng, Class, CtRunner, ctbench_main}; +use entlib_native_constant_time::choice::Choice; +use entlib_native_constant_time::traits::{ConstantTimeEq, ConstantTimeSelect}; + +/// [ConstantTimeEq::ct_eq] 검증 +/// 두 값이 완벽히 동일한 경우([Class::Right])와 서로 다른 경우([Class::Left])의 +/// 연산 소요 시간 분포를 교차 검증합니다. +fn bench_ct_eq(runner: &mut CtRunner, rng: &mut BenchRng) { + let mut inputs = vec![(0u64, 0u64); 100_000]; + let mut classes = vec![Class::Right; 100_000]; + + for (input, class) in inputs.iter_mut().zip(classes.iter_mut()) { + let a: u64 = rng.r#gen(); + let b: u64 = rng.r#gen(); + + // 50% 확률로 동일한 값 쌍과 다른 값 쌍을 생성 + if rng.r#gen::() { + *input = (a, a); + *class = Class::Right; // 일치 상태 + } else { + *input = (a, b); + *class = Class::Left; // 불일치 상태 + } + } + + for (class, (a, b)) in classes.into_iter().zip(inputs.into_iter()) { + runner.run_one(class, || { + // black_box를 통해 컴파일러 DCE 최적화 억제 + let _ = + core::hint::black_box(core::hint::black_box(a).ct_eq(core::hint::black_box(&b))); + }); + } +} + +/// [ConstantTimeSelect::ct_select] 검증 +/// 조건 마스크가 0xFF(True)인 경우([Class::Right])와 0x00(False)인 경우([Class::Left]) +/// 데이터 선택 과정에서 타이밍 차이가 발생하는지 검증합니다. +fn bench_ct_select(runner: &mut CtRunner, rng: &mut BenchRng) { + let mut inputs = vec![(0u64, 0u64, 0u8); 100_000]; + let mut classes = vec![Class::Right; 100_000]; + + for (input, class) in inputs.iter_mut().zip(classes.iter_mut()) { + let a: u64 = rng.r#gen(); + let b: u64 = rng.r#gen(); + + if rng.r#gen::() { + *input = (a, b, 0xFF); + *class = Class::Right; // True 분기 + } else { + *input = (a, b, 0x00); + *class = Class::Left; // False 분기 + } + } + + for (class, (a, b, mask)) in classes.into_iter().zip(inputs.into_iter()) { + // Choice 구조체의 내부 필드는 private이라서 벤치마크 환경에 한하여 + // 메모리 transmute를 통해 강제로 마스크 값 주입 + let choice = unsafe { core::mem::transmute::(mask) }; + + runner.run_one(class, || { + let _ = core::hint::black_box(u64::ct_select( + core::hint::black_box(&a), + core::hint::black_box(&b), + core::hint::black_box(choice), + )); + }); + } +} + +// todo: 더 + +ctbench_main!(bench_ct_eq, bench_ct_select); diff --git a/core/constant-time/build.rs b/core/constant-time/build.rs new file mode 100644 index 0000000..3b6cc6f --- /dev/null +++ b/core/constant-time/build.rs @@ -0,0 +1,19 @@ +use std::env; + +fn main() { + // valgrind_audit 플래그가 활성화된 경우에만 C Shim 코드를 컴파일해서 메인 빌드 오염 방지 + let is_audit_enabled = env::var("CARGO_FEATURE_VALGRIND_AUDIT").is_ok(); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + + if is_audit_enabled && target_os == "linux" { + println!("cargo:rerun-if-changed=tests/taint_shim.c"); + + cc::Build::new() + .file("tests/taint_shim.c") + // 최적화 억제 유지 (Valgrind 매크로 소실 방지) + .opt_level(0) + .compile("taint_shim"); + + println!("cargo:rustc-link-lib=static=taint_shim"); + } +} diff --git a/core/constant-time/src/choice.rs b/core/constant-time/src/choice.rs new file mode 100644 index 0000000..eca9673 --- /dev/null +++ b/core/constant-time/src/choice.rs @@ -0,0 +1,137 @@ +use core::ops::{BitAnd, BitOr, BitXor, Not}; + +/// 비밀 데이터의 상태를 안전하게 표현하는 상수-시간 논리 타입입니다. +/// +/// 이 구조체는 암호학적 연산에서 조건 분기(Branch)를 제거하여 타이밍 공격(Timing Attack)을 +/// 방지하기 위해 설계되었습니다. 내부적으로 `0x00`(False) 또는 `0xFF`(True) 값만을 가집니다. +/// +/// # Security Note +/// `Choice`는 반드시 `0x00` 또는 `0xFF` 두 값 중 하나만을 가져야 합니다. +/// 이 불변 조건이 유지될 때, 비트 연산(`&`, `|`, `^`, `!`)은 논리 연산(AND, OR, XOR, NOT)과 +/// 수학적으로 동치이며, CPU 분기 예측기를 자극하지 않는 상수-시간(Constant-Time) 연산을 보장합니다. +/// +/// # Safety +/// - 내부 필드 `u8`은 절대로 공개(`pub`)처리되어선 안 됩니다. +#[derive(Clone, Copy, Debug)] +#[repr(transparent)] +pub struct Choice(u8); + +impl Choice { + /// 비밀 데이터의 상태 마스크를 상수-시간으로 안전하게 정규화합니다. + /// + /// # Security Note + /// 어떠한 바이트(u8) 입력이 들어오더라도 수학적 비트 연산을 통해 + /// 0x00(False) 또는 0xFF(True)로 강제 변환합니다. + #[inline(always)] + pub(crate) fn from_mask_normalized(mask: u8) -> Self { + // 값의 존재 유무(Non-Zero) 탐지 + // mask가 0이면 msb_set은 0x00, 0이 아니면 2의 보수 성질에 의해 msb_set의 MSB는 무조건 1이 됨 + let msb_set = mask | mask.wrapping_neg(); + + // 최상위 비트(MSB)를 추출하여 0 또는 1의 상태로 매핑 + let is_nonzero = msb_set >> 7; + + // 2의 보수를 취해 최종 마스크 생성 (0x00 또는 0xFF) + // is_nonzero가 0이면 0x00, 1이면 0xFF를 반환 (단일 NEG 명령어로 컴파일) + let secure_mask = is_nonzero.wrapping_neg(); + + Choice(secure_mask) + } + + /// 내부 값을 반환합니다. 컴파일러 최적화를 방지하기 위해 `black_box`를 사용합니다. + /// + /// # Returns + /// * `0x00` - False + /// * `0xFF` - True + #[inline(always)] + pub fn unwrap_u8(self) -> u8 { + core::hint::black_box(self.0) + } + + /// `Choice` 값을 논리적으로 반전(NOT)합니다. + /// + /// `!choice` 연산자와 동일한 동작을 수행합니다. + #[inline(always)] + pub fn choice_not(self) -> Self { + Choice(!self.0) + } +} + +impl BitAnd for Choice { + type Output = Choice; + + /// 논리 AND 연산을 수행합니다. + /// + /// 두 `Choice` 값이 모두 참(`0xFF`)일 때만 참(`0xFF`)을 반환합니다. + /// + /// # Constant-Time + /// `&` 연산자는 단일 CPU 명령어(AND)로 컴파일되며, 입력 값에 상관없이 항상 일정한 + /// CPU 사이클을 소모합니다. 분기문(`if`)을 대체하여 비밀 데이터에 의존적인 제어 흐름을 제거합니다. + /// + /// # Examples + /// * `0xFF & 0xFF = 0xFF` (True AND True = True) + /// * `0xFF & 0x00 = 0x00` (True AND False = False) + /// * `0x00 & 0x00 = 0x00` (False AND False = False) + #[inline(always)] + fn bitand(self, rhs: Choice) -> Choice { + Choice(self.0 & rhs.0) + } +} + +impl BitOr for Choice { + type Output = Choice; + + /// 논리 OR 연산을 수행합니다. + /// + /// 두 `Choice` 값 중 하나라도 참(`0xFF`)이면 참(`0xFF`)을 반환합니다. + /// + /// # Constant-Time + /// `|` 연산자는 단일 CPU 명령어(OR)로 컴파일되며, 분기 없이 실행됩니다. + /// + /// # Examples + /// * `0xFF | 0x00 = 0xFF` (True OR False = True) + /// * `0x00 | 0x00 = 0x00` (False OR False = False) + #[inline(always)] + fn bitor(self, rhs: Choice) -> Choice { + Choice(self.0 | rhs.0) + } +} + +impl BitXor for Choice { + type Output = Choice; + + /// 논리 XOR 연산을 수행합니다. + /// + /// 두 `Choice` 값이 서로 다를 때만 참(`0xFF`)을 반환합니다. + /// + /// # Constant-Time + /// `^` 연산자는 단일 CPU 명령어(XOR)로 컴파일됩니다. + /// + /// # Examples + /// * `0xFF ^ 0xFF = 0x00` (True XOR True = False) + /// * `0xFF ^ 0x00 = 0xFF` (True XOR False = True) + /// * `0x00 ^ 0x00 = 0x00` (False XOR False = False) + #[inline(always)] + fn bitxor(self, rhs: Choice) -> Choice { + Choice(self.0 ^ rhs.0) + } +} + +impl Not for Choice { + type Output = Choice; + + /// 논리 NOT 연산을 수행합니다. + /// + /// 참(`0xFF`)을 거짓(`0x00`)으로, 거짓(`0x00`)을 참(`0xFF`)으로 반전합니다. + /// + /// # Constant-Time + /// `!` 연산자는 단일 CPU 명령어(NOT)로 컴파일됩니다. + /// + /// # Examples + /// * `!0xFF = 0x00` (NOT True = False) + /// * `!0x00 = 0xFF` (NOT False = True) + #[inline(always)] + fn not(self) -> Choice { + Choice(!self.0) + } +} diff --git a/core/constant-time/src/lib.rs b/core/constant-time/src/lib.rs new file mode 100644 index 0000000..aa170c2 --- /dev/null +++ b/core/constant-time/src/lib.rs @@ -0,0 +1,195 @@ +#![no_std] + +pub mod choice; +pub mod traits; + +#[cfg(feature = "audit_mode")] +mod wrapper; + +use choice::Choice; +use traits::{ + ConstantTimeEq, ConstantTimeIsNegative, ConstantTimeIsZero, ConstantTimeSelect, + ConstantTimeSwap, +}; + +/// 기본 부호 없는 정수형(Unsigned Integers)에 대한 상수-시간 연산을 +/// 일괄 구현하는 매크로입니다. +macro_rules! impl_constant_time_for_uint { + ($t:ty) => { + impl ConstantTimeEq for $t { + #[inline(always)] + fn ct_eq(&self, other: &Self) -> Choice { + // XOR 연산 + // 두 값이 같으면 v는 0, 다르면 0이 아닌 값이 됨 + let v = *self ^ *other; + + // OR와 2의 보수(wrapping_neg) 활용 + // v가 0이면 v | v.wrapping_neg() 도 0임 + // v가 0이 아니면, v | v.wrapping_neg() 의 최상위 비트(MSB)는 항상 1이 됨 + // 이를 통해 MSB를 LSB 위치로 이동시킴 + let msb = (v | v.wrapping_neg()) >> (<$t>::BITS - 1); + + // 마스크 생성 + // v가 0(같음)이면 msb는 0. msb ^ 1 은 1. 1의 2의 보수는 0xFF (True) + // v가 0이 아님(다름)이면 msb는 1. msb ^ 1 은 0. 0의 2의 보수는 0x00 (False) + let mask = ((msb as u8) ^ 1).wrapping_neg(); + + Choice::from_mask_normalized(mask) + } + + #[inline(always)] + fn ct_is_ge(&self, other: &Self) -> Choice { + // 부호 없는 정수의 대소 비교(self >= other)를 상수-시간으로 판별하기 위해 + // 뺄셈 연산의 언더플로우(Borrow) 발생 여부를 비트 논리로 계산 + // Borrow 방정식: (~A & B) | (~(A ^ B) & (A - B)) + // 결과의 최상위 비트(MSB)가 1이면 self < other (언더플로우 발생), 0이면 self >= other + let sub = self.wrapping_sub(*other); + let borrow = (!*self & *other) | (!(*self ^ *other) & sub); + + // 최상위 비트(MSB) 추출 + // 이전 ::BITS 하드코딩으로 인한 치명적 결함을 동적 타입 크기(<$t>::BITS)로 해결 + let borrow_msb = (borrow >> (<$t>::BITS - 1)) as u8; + + // 마스크 생성 + // borrow_msb가 0 (self >= other) -> 0 ^ 1 = 1 -> wrapping_neg(1) = 0xFF (True) + // borrow_msb가 1 (self < other) -> 1 ^ 1 = 0 -> wrapping_neg(0) = 0x00 (False) + let mask = (borrow_msb ^ 1).wrapping_neg(); + + Choice::from_mask_normalized(mask) + } + } + + impl ConstantTimeSelect for $t { + #[inline(always)] + fn ct_select(a: &Self, b: &Self, choice: Choice) -> Self { + // 부호 확장(Sign-Extension) 트릭 + // 0x00 as i8 -> 0 -> $t로 캐스팅하면 모든 비트가 0 (0x0000...) + // 0xFF as i8 -> -1 -> $t로 캐스팅하면 모든 비트가 1 (0xFFFF...) + let mask = (choice.unwrap_u8() as i8) as $t; + + // 마스크에 따라 a 또는 b를 비트 단위로 선택 (분기 없음) + (a & mask) | (b & !mask) + } + } + + impl ConstantTimeSwap for $t { + #[inline(always)] + fn ct_swap(a: &mut Self, b: &mut Self, choice: Choice) { + let mask = (choice.unwrap_u8() as i8) as $t; + // XOR 스왑 알고리즘을 마스크와 결합 + // mask가 모든 비트가 1이면 t = a ^ b, mask가 0이면 t = 0 + let t = (*a ^ *b) & mask; + + // t가 0이면 원래 값을 유지, t가 a ^ b이면 값이 교환됨 + *a ^= t; + *b ^= t; + } + } + + impl ConstantTimeIsZero for $t { + #[inline(always)] + fn ct_is_zero(&self) -> Choice { + // 값과 0을 상수-시간으로 비교 + self.ct_eq(&0) + } + } + + impl ConstantTimeIsNegative for $t { + #[inline(always)] + fn ct_is_negative(&self) -> Choice { + // MSB(최상위 비트)를 LSB 위치로 이동시켜 0 또는 1을 추출 + // 예: u8에서 *self >> 7, u64에서 *self >> 63 + let msb = (*self >> (<$t>::BITS - 1)) as u8 & 1; + + // 1u8.wrapping_neg() = 0xFF (True), 0u8.wrapping_neg() = 0x00 (False) + // 단일 NEG 명령어로 컴파일되어 분기가 없음 + Choice::from_mask_normalized(msb.wrapping_neg()) + } + } + }; +} + +impl_constant_time_for_uint!(u8); +impl_constant_time_for_uint!(u16); +impl_constant_time_for_uint!(u32); +impl_constant_time_for_uint!(u64); +impl_constant_time_for_uint!(u128); +impl_constant_time_for_uint!(usize); + +/// 기본 부호 있는 정수형(Signed Integers)에 대한 상수-시간 연산을 +/// 분기 없이 안전하게 일괄 구현하는 매크로입니다. +macro_rules! impl_constant_time_for_sint { + ($s_type:ty, $u_type:ty) => { + impl ConstantTimeEq for $s_type { + #[inline(always)] + fn ct_eq(&self, other: &Self) -> Choice { + // 동일성 비교는 비트 패턴의 일치 여부만 확인하기 때문에 + // 부호 없는 정수로 강제 캐스팅하여 기존 산술 시프트 취약점 회피 + let a = *self as $u_type; + let b = *other as $u_type; + + // 기 검증된 Unsigned의 상수-시간 동일성 비교 로직으로 위임 + a.ct_eq(&b) + } + + #[inline(always)] + fn ct_is_ge(&self, other: &Self) -> Choice { + // 부호 있는 정수의 대소 비교 시 타이밍 공격 방지 + // 2의 보수 체계에서 부호 비트(MSB)를 반전(XOR)시키면 + // 값의 수학적 대소 순서를 보존한 채 부호 없는 정수 도메인으로 안전하게 매핑됨 + let sign_mask = (1 as $u_type) << (<$s_type>::BITS - 1); + let a_mapped = (*self as $u_type) ^ sign_mask; + let b_mapped = (*other as $u_type) ^ sign_mask; + + // 변환된 값을 바탕으로 안전성이 입증된 부호 없는 정수의 대소 비교 수행 + a_mapped.ct_is_ge(&b_mapped) + } + } + + impl ConstantTimeIsNegative for $s_type { + #[inline(always)] + fn ct_is_negative(&self) -> Choice { + // 산술 시프트로 인한 마스크 오염(예: 0x02) 방지를 위해 + // 부호 없는 정수로 변환 후 논리 시프트 강제 + let u_val = *self as $u_type; + let msb = (u_val >> (<$s_type>::BITS - 1)) as u8 & 1; + + // 단일 NEG 명령어로 분기 없이 마스크 생성 (0x00 또는 0xFF 보장) + Choice::from_mask_normalized(msb.wrapping_neg()) + } + } + + impl ConstantTimeSelect for $s_type { + #[inline(always)] + fn ct_select(a: &Self, b: &Self, choice: Choice) -> Self { + // Sign-Extension 트릭은 부호 있는 정수에서도 비트 마스킹에 유효 + let mask = (choice.unwrap_u8() as i8) as $s_type; + (a & mask) | (b & !mask) + } + } + + impl ConstantTimeSwap for $s_type { + #[inline(always)] + fn ct_swap(a: &mut Self, b: &mut Self, choice: Choice) { + let mask = (choice.unwrap_u8() as i8) as $s_type; + let t = (*a ^ *b) & mask; + *a ^= t; + *b ^= t; + } + } + + impl ConstantTimeIsZero for $s_type { + #[inline(always)] + fn ct_is_zero(&self) -> Choice { + self.ct_eq(&0) + } + } + }; +} + +impl_constant_time_for_sint!(i8, u8); +impl_constant_time_for_sint!(i16, u16); +impl_constant_time_for_sint!(i32, u32); +impl_constant_time_for_sint!(i64, u64); +impl_constant_time_for_sint!(i128, u128); +impl_constant_time_for_sint!(isize, usize); diff --git a/core/constant-time/src/traits.rs b/core/constant-time/src/traits.rs new file mode 100644 index 0000000..0733a1f --- /dev/null +++ b/core/constant-time/src/traits.rs @@ -0,0 +1,54 @@ +use crate::choice::Choice; + +/// 두 데이터의 동일성 여부를 상수-시간으로 판별합니다. +pub trait ConstantTimeEq { + /// 두 값이 동일하면 Choice(0xFF)를, 다르면 Choice(0x00)을 반환합니다. + fn ct_eq(&self, other: &Self) -> Choice; + + /// 두 값이 다르면 Choice(0xFF)를 반환합니다. + /// 기본적으로 ct_eq의 결과를 반전(NOT)시켜 제공합니다. + #[inline(always)] + fn ct_ne(&self, other: &Self) -> Choice { + self.ct_eq(other).choice_not() + } + + fn ct_is_ge(&self, other: &Self) -> Choice; +} + +/// 조건에 따라 두 값 중 하나를 상수-시간으로 선택합니다. +pub trait ConstantTimeSelect: Sized { + /// choice가 True(0xFF)이면 `a`를, False(0x00)이면 `b`를 반환합니다. + /// 어떠한 분기문(if/else, match)도 사용되어서는 안 됩니다. + fn ct_select(a: &Self, b: &Self, choice: Choice) -> Self; +} + +/// 조건에 따라 두 변수의 값을 상수-시간으로 교환(Swap)합니다. +/// Montgomery Ladder와 같은 암호화 알고리즘에 필수적입니다. +pub trait ConstantTimeSwap: Sized { + /// choice가 True(0xFF)이면 `a`와 `b`의 값을 교환하고, False(0x00)이면 그대로 둡니다. + fn ct_swap(a: &mut Self, b: &mut Self, choice: Choice); +} + +/// 값이 0인지 상수-시간으로 판별합니다. (BigInt 및 메모리 소거 검증용) +pub trait ConstantTimeIsZero { + /// 값이 0이면 Choice(0xFF)를, 0이 아니면 Choice(0x00)을 반환합니다. + fn ct_is_zero(&self) -> Choice; +} + +/// 값의 최상위 비트(MSB)가 1인지 상수-시간으로 판별합니다. +/// +/// 암호학적 다중 정밀도 연산에서 `wrapping_sub`의 언더플로우(Borrow)를 +/// 분기 없이 감지하는 데 활용됩니다. +/// +/// # 사용 사례 +/// `a.wrapping_sub(b)` 수행 후 결과의 MSB가 1이면 `a < b`임을 상수-시간으로 판별합니다. +/// 이 특성은 상수-시간 모듈로 보정, 조건부 스왑, 상수-시간 Base64 범위 검사 등에 +/// 광범위하게 사용됩니다. +/// +/// # 보안 보장 +/// - MSB 추출은 단일 우측 시프트(SHR) 명령어로 수행되며 분기가 없습니다. +/// - `wrapping_neg`를 이용한 마스크 생성은 CPU 분기 예측기를 자극하지 않습니다. +pub trait ConstantTimeIsNegative { + /// MSB가 1이면 `Choice(0xFF)`, 0이면 `Choice(0x00)`을 반환합니다. + fn ct_is_negative(&self) -> Choice; +} diff --git a/core/constant-time/src/wrapper.rs b/core/constant-time/src/wrapper.rs new file mode 100644 index 0000000..9358747 --- /dev/null +++ b/core/constant-time/src/wrapper.rs @@ -0,0 +1,62 @@ +use crate::choice::Choice; +use crate::traits::*; + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_u64_ct_eq(a: &u64, b: &u64) -> Choice { + a.ct_eq(b) +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_u64_ct_is_ge(a: &u64, b: &u64) -> Choice { + a.ct_is_ge(b) +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_u64_ct_is_negative(a: &u64) -> Choice { + a.ct_is_negative() +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_u64_ct_ne(a: &u64, b: &u64) -> Choice { + a.ct_ne(b) +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_u64_ct_is_zero(a: &u64) -> Choice { + a.ct_is_zero() +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_u64_ct_select(a: &u64, b: &u64, choice: Choice) -> u64 { + u64::ct_select(a, b, choice) +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_u64_ct_swap(a: &mut u64, b: &mut u64, choice: Choice) { + u64::ct_swap(a, b, choice) +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_choice_from_mask_normalized(a: u8) -> Choice { + Choice::from_mask_normalized(a) +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_choice_not(choice: Choice) -> Choice { + choice.choice_not() +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub fn audit_verify_choice_unwrap_u8(choice: Choice) -> u8 { + choice.unwrap_u8() +} diff --git a/core/constant-time/tests/taint_audit.rs b/core/constant-time/tests/taint_audit.rs new file mode 100644 index 0000000..f1f1f5f --- /dev/null +++ b/core/constant-time/tests/taint_audit.rs @@ -0,0 +1,135 @@ +#![cfg(test)] +#![cfg(target_os = "linux")] +#![cfg(feature = "valgrind_taint_audit")] + +use core::arch::asm; +use core::hint::black_box; +use core::ptr::read_volatile; +use core::sync::atomic::{Ordering, compiler_fence}; +use entlib_native_constant_time::choice::Choice; +use entlib_native_constant_time::traits::{ConstantTimeEq, ConstantTimeSelect}; + +const VALGRIND_MAKE_MEM_UNDEFINED: usize = 0x4d43_0001; +const VALGRIND_MAKE_MEM_DEFINED: usize = 0x4d43_0002; +const VALGRIND_RUNNING_ON_VALGRIND: usize = 0x1001; + +#[cfg(target_arch = "x86_64")] +#[inline(always)] +unsafe fn do_valgrind_request(request: usize, arg1: usize, arg2: usize) -> usize { + let args: [usize; 6] = [request, arg1, arg2, 0, 0, 0]; + let mut result: usize; + + black_box(args.as_ptr()); + compiler_fence(Ordering::SeqCst); + + asm!( + "rol rdi, 3", + "rol rdi, 13", + "rol rdi, 61", + "rol rdi, 51", + "xchg rbx, rbx", + inout("rax") args.as_ptr() => _, + inout("rdx") 0usize => result, + out("rdi") _, + out("rcx") _, + out("r8") _, + out("r9") _, + out("r10") _, + out("r11") _ + ); + + compiler_fence(Ordering::SeqCst); + result +} + +#[cfg(not(target_arch = "x86_64"))] +#[inline(always)] +unsafe fn do_valgrind_request(_request: usize, _arg1: usize, _arg2: usize) -> usize { + 0 +} + +#[inline(always)] +fn is_running_on_valgrind() -> bool { + unsafe { do_valgrind_request(VALGRIND_RUNNING_ON_VALGRIND, 0, 0) > 0 } +} + +unsafe fn taint_memory(data: *const T) { + if is_running_on_valgrind() { + do_valgrind_request( + VALGRIND_MAKE_MEM_UNDEFINED, + data as usize, + core::mem::size_of::(), + ); + compiler_fence(Ordering::SeqCst); + } +} + +unsafe fn untaint_memory(data: *const T) { + if is_running_on_valgrind() { + do_valgrind_request( + VALGRIND_MAKE_MEM_DEFINED, + data as usize, + core::mem::size_of::(), + ); + compiler_fence(Ordering::SeqCst); + } +} + +#[test] +fn audit_taint_flow_ct_eq() { + // Constant Folding 방지 및 Taint Tracking 강제를 위한 mut 및 black_box 적용 + let mut a: u64 = black_box(0xDEADBEEFC0DECAFE); + let mut b: u64 = black_box(0xDEADBEEFC0DECAFE); + + unsafe { + taint_memory(&a); + taint_memory(&b); + } + + let result = a.ct_eq(&b); + + unsafe { + untaint_memory(&a); + untaint_memory(&b); + untaint_memory(&result); + + // LLVM 레지스터 캐싱 무효화 및 안전한 메모리 강제 로드 + let safe_result = read_volatile(&result); + assert_eq!(safe_result.unwrap_u8(), 0xFF); + } +} + +#[test] +fn audit_taint_flow_ct_select() { + // Constant Folding 방지 및 Taint Tracking 강제를 위한 mut 및 black_box 적용 + let mut a: u32 = black_box(0x11111111); + let mut b: u32 = black_box(0x22222222); + + // ConstantTimeEq로 Choice(0x00) 생성 + // 0u8과 1u8은 다르므로 False(0x00) Choice가 반환 + // ct_select에서 b를 선택하도록 + let mut choice_seed_1 = black_box(0u8); + let mut choice_seed_2 = black_box(1u8); + let mut choice = black_box(choice_seed_1.ct_eq(&choice_seed_2)); + + unsafe { + taint_memory(&a); + taint_memory(&b); + taint_memory(&choice); + } + + // choice가 False(0x00) -> b(0x22222222)가 선택되어야 함 + let selected = u32::ct_select(&a, &b, choice); + + unsafe { + untaint_memory(&a); + untaint_memory(&b); + untaint_memory(&choice); + untaint_memory(&selected); + + // LLVM 레지스터 캐싱 무효화 및 안전한 메모리 강제 로드 + let safe_selected = read_volatile(&selected); + // b의 값이 안전하게 반환되었나?? + assert_eq!(safe_selected, 0x22222222); + } +} diff --git a/core/hex/Cargo.toml b/core/hex/Cargo.toml new file mode 100644 index 0000000..ac7c225 --- /dev/null +++ b/core/hex/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "entlib-native-hex" +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 diff --git a/core/hex/src/hex.rs b/core/hex/src/hex.rs new file mode 100644 index 0000000..3457f73 --- /dev/null +++ b/core/hex/src/hex.rs @@ -0,0 +1,131 @@ +use entlib_native_constant_time::choice::Choice; +use entlib_native_constant_time::traits::{ + ConstantTimeEq, ConstantTimeIsNegative, ConstantTimeSelect, +}; + +/// 0~15 사이의 니블(Nibble)을 상수 시간으로 Hex ASCII 문자로 변환합니다. +/// +/// # Security +/// 자체 구현된 `ConstantTimeIsNegative` 및 `ConstantTimeSelect` 트레이트를 사용하여 +/// 데이터에 의존하는 어떠한 분기문(Branch)이나 메모리 룩업(Lookup)도 발생하지 않도록 통제합니다. +#[inline(always)] +fn encode_nibble_ct(nibble: u8) -> u8 { + let v = nibble & 0x0F; + + // 1. 논리적 음수(Underflow) 유도 + // v가 10보다 작으면 언더플로우가 발생하여 MSB가 1이 됩니다. + let diff = v.wrapping_sub(10); + + // 2. Choice 타입 생성 + // diff의 최상위 비트(MSB)를 확인하여 언더플로우 발생 여부를 + // 안전하게 캡슐화된 Choice(0xFF 또는 0x00)로 반환받습니다. + let is_lt_10 = diff.ct_is_negative(); + + // 3. 상수 시간 선택 (Constant-Time Select) + // is_lt_10이 Choice(0xFF)일 경우 (참) -> a 인자 (48 + v) 선택 ('0'~'9') + // is_lt_10이 Choice(0x00)일 경우 (거짓) -> b 인자 (87 + v) 선택 ('a'~'f') + // 인자를 참조(&Self)로 전달하여 트레이트 규격을 준수합니다. + u8::ct_select(&(48 + v), &(87 + v), is_lt_10) +} + +/// 주어진 평문 슬라이스를 상수 시간으로 Hex 인코딩하여 출력 슬라이스에 작성합니다. +pub(crate) fn encode_hex_core_ct(input: &[u8], output: &mut [u8]) { + // 출력 버퍼의 크기가 입력의 2배인지 엄격히 검증 (Zero-Trust 원칙) + assert!( + output.len() >= input.len() * 2, + "Security Violation: Output buffer overflow" + ); + + for (i, &byte) in input.iter().enumerate() { + let high = (byte >> 4) & 0x0F; + let low = byte & 0x0F; + + output[i * 2] = encode_nibble_ct(high); + output[i * 2 + 1] = encode_nibble_ct(low); + } +} + +/// 단일 ASCII 문자를 상수-시간으로 0~15 사이의 니블(Nibble)로 디코딩합니다. +/// +/// # Returns +/// (디코딩된 값, 유효성 여부를 나타내는 Choice) 튜플을 반환합니다. +/// 문자가 유효하지 않더라도 연산 시간은 동일하며, 반환되는 Choice는 거짓(0x00)이 됩니다. +#[inline(always)] +#[allow(non_snake_case)] // for a, A, f, F +fn decode_nibble_ct(c: u8) -> (u8, Choice) { + // 1. '0' ~ '9' (48 ~ 57) 판별 + // c < 48 이면 MSB가 1, 57 < c 이면 MSB가 1이 됨 (언더플로우 활용) + let is_lt_0 = c.wrapping_sub(b'0').ct_is_negative(); + let is_gt_9 = b'9'.wrapping_sub(c).ct_is_negative(); + // ! 연산자는 Choice에 대해 상수-시간 NOT 연산을 수행합니다. + let is_digit = !is_lt_0 & !is_gt_9; + + // 2. 'a' ~ 'f' (97 ~ 102) 판별 + let is_lt_a = c.wrapping_sub(b'a').ct_is_negative(); + let is_gt_f = b'f'.wrapping_sub(c).ct_is_negative(); + let is_lower_hex = !is_lt_a & !is_gt_f; + + // 3. 'A' ~ 'F' (65 ~ 70) 판별 + let is_lt_A = c.wrapping_sub(b'A').ct_is_negative(); + let is_gt_F = b'F'.wrapping_sub(c).ct_is_negative(); + let is_upper_hex = !is_lt_A & !is_gt_F; + + // 4. 단일 문자 유효성 병합 + // | 연산자는 Choice에 대해 상수-시간 OR 연산을 수행합니다. + let is_valid = is_digit | is_lower_hex | is_upper_hex; + + // 5. 각 케이스별 논리적 반환값 계산 (분기 없이 모두 계산) + let val_digit = c.wrapping_sub(b'0'); + let val_lower = c.wrapping_sub(87); // c - 97 + 10 + let val_upper = c.wrapping_sub(55); // c - 65 + 10 + + // 6. 상수 시간 선택 (Constant-Time Select) + // 기본값 0에서 시작하여 조건이 참(0xFF)일 때만 해당 값을 덮어씁니다. + let mut result = 0u8; + result = u8::ct_select(&result, &val_digit, is_digit); + result = u8::ct_select(&result, &val_lower, is_lower_hex); + result = u8::ct_select(&result, &val_upper, is_upper_hex); + + (result, is_valid) +} + +/// 주어진 Hex 인코딩 슬라이스를 상수-시간으로 디코딩하여 출력 슬라이스에 작성합니다. +/// +/// # Security +/// - 입력의 내용과 상관없이 항상 일정한 시간(O(N))에 실행됩니다. +/// - 에러가 발생해도 즉시 반환(Early Return)하지 않고 전체 길이를 모두 처리합니다. +pub(crate) fn decode_hex_core_ct(input: &[u8], output: &mut [u8]) -> Choice { + assert!( + output.len() >= input.len() / 2, + "Security Violation: Output buffer overflow" + ); + + // 길이 검증: Hex 문자열은 항상 짝수 길이여야 합니다. + // 길이는 비밀 데이터가 아니므로 일반 분기(if)를 사용해도 무방하지만, + // 엄격한 상수-시간 제어를 위해 Choice를 생성합니다. + let is_even_len = (input.len() % 2_usize).ct_eq(&0usize); + + // from_mask_unchecked 대신 ct_eq를 사용하여 True(0xFF) Choice를 초기화합니다. + let mut all_valid = 0u8.ct_eq(&0u8); + // 길이 홀수 에러도 통합 에러 상태에 병합 + all_valid = all_valid & is_even_len; + + // 실제 디코딩 루프 (입력 길이의 절반만큼 무조건 수행) + let iter_count = input.len() / 2; + for i in 0..iter_count { + let (high_nibble, high_valid) = decode_nibble_ct(input[i * 2]); + let (low_nibble, low_valid) = decode_nibble_ct(input[i * 2 + 1]); + + // & 연산자는 Choice에 대해 상수-시간 AND 연산을 수행합니다. + let byte_valid = high_valid & low_valid; + all_valid = all_valid & byte_valid; + + // 바이트 조합 + let decoded_byte = (high_nibble << 4) | low_nibble; + + // 유효하지 않은 바이트인 경우 출력 버퍼에 0을 기록하여 쓰레기값 생성을 방지합니다. + output[i] = u8::ct_select(&0, &decoded_byte, byte_valid); + } + + all_valid +} diff --git a/core/hex/src/lib.rs b/core/hex/src/lib.rs new file mode 100644 index 0000000..5f01420 --- /dev/null +++ b/core/hex/src/lib.rs @@ -0,0 +1,72 @@ +mod hex; + +use crate::hex::{decode_hex_core_ct, encode_hex_core_ct}; +use entlib_native_secure_buffer::SecureBuffer; + +/// 군사급 보안 요구사항을 충족하는 상수 시간 Hex 인코딩 함수입니다. +/// +/// Rust 내부에서 할당된 안전한 메모리 블록(RO 패턴)에 결과를 담아 반환합니다. +/// +/// # Arguments +/// - `input` - 인코딩할 원본 평문 데이터가 담긴 SecureBuffer +/// +/// # Returns +/// - `Ok(SecureBuffer)` - Hex 인코딩이 완료된 새 버퍼 (OS 레벨 잠금 완료) +/// - `Err` - 메모리 할당 실패 시 +pub fn encode(input: &SecureBuffer) -> Result { + // 1. 읽기 전용 및 쓰기 전용 슬라이스 확보 + // 내부 데이터를 다룰 때 as_slice 및 as_mut_slice를 통해 반환된 슬라이스는 SecureBuffer의 수명에 묶여 있습니다. + let input_slice = input.as_slice(); + let required_len = input_slice.len() * 2; + + // 2. 출력용 SecureBuffer 생성 (RO 패턴) + // new_owned 메소드를 통해 Rust 내부에서 페이지 정렬된 안전한 메모리를 새로 할당합니다. + let mut output_buffer = SecureBuffer::new_owned(required_len)?; + + // 3. 상수 시간 인코딩 연산 수행 + encode_hex_core_ct(input_slice, output_buffer.as_mut_slice()); + + // 4. 안전하게 래핑된 버퍼 반환 + Ok(output_buffer) +} + +/// 군사급 보안 요구사항을 충족하는 상수-시간 Hex 디코딩 함수입니다. +/// +/// Rust 내부에서 할당된 안전한 메모리 블록(RO 패턴)에 디코딩된 바이너리 결과를 담아 반환합니다. +/// 디코딩 중 유효하지 않은 문자가 발견되더라도 연산 시간은 동일하며, +/// 실패 시 중간에 생성된 버퍼는 즉시 물리적으로 소거(Zeroize)됩니다. +/// +/// # Arguments +/// - `input` - 디코딩할 Hex 문자열 데이터가 담긴 SecureBuffer +/// +/// # Returns +/// - `Ok(SecureBuffer)` - 디코딩이 완료된 새 버퍼 (OS 레벨 잠금 완료) +/// - `Err` - 메모리 할당 실패 또는 유효하지 않은 Hex 문자열 입력 시 +pub fn decode(input: &SecureBuffer) -> Result { + // 1. 읽기 전용 슬라이스 확보 + let input_slice = input.as_slice(); + + // 디코딩된 데이터의 길이는 입력 Hex 문자열의 절반입니다. + let required_len = input_slice.len() / 2; + + // 2. 출력용 SecureBuffer 생성 (RO 패턴 적용) + // new_owned를 통해 Rust 내부에서 페이지 정렬된 안전한 메모리를 새로 할당받고 OS 잠금을 수행합니다. + let mut output_buffer = SecureBuffer::new_owned(required_len)?; + + // 3. 상수-시간 디코딩 연산 수행 + // 입력에 유효하지 않은 문자가 포함되어 있어도 즉시 반환(Early Return)하지 않고 끝까지 연산합니다. + let is_valid = decode_hex_core_ct(input_slice, output_buffer.as_mut_slice()); + + // 4. 디코딩 성공 여부 검증 (상수-시간 영역 -> 일반 제어 흐름 영역) + // Choice::unwrap_u8()을 호출하여 결과가 0xFF(True)인지 확인합니다. + if is_valid.unwrap_u8() == 0xFF { + Ok(output_buffer) + } else { + // 5. 에러 발생 시의 안티 포렌식(Anti-Forensics) 및 물리적 파기 + // 함수가 Err를 반환하며 스코프를 벗어날 때, output_buffer의 Drop 로직이 자동으로 호출됩니다. + // 이때 할당된 전체 capacity에 대해 Zeroizer::zeroize_raw가 수행되어 불완전한 데이터가 물리적으로 소거됩니다. + + // 타이밍/패딩 오라클 공격 방지를 위해 에러 원인(위치, 발생한 문자 등)을 상세히 밝히지 않고 균일한 메시지를 반환합니다. + Err("Security Violation: Invalid hex encoding detected.") + } +} diff --git a/core/result/Cargo.toml b/core/result/Cargo.toml new file mode 100644 index 0000000..b20112c --- /dev/null +++ b/core/result/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "entlib-native-result" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] diff --git a/core/result/src/lib.rs b/core/result/src/lib.rs new file mode 100644 index 0000000..6b93055 --- /dev/null +++ b/core/result/src/lib.rs @@ -0,0 +1,27 @@ +use std::ffi::c_void; +use std::ptr::null_mut; + +#[repr(C)] +pub struct EntLibResult { + type_id: i8, + status: i8, + data: *mut c_void, +} + +impl EntLibResult { + pub fn new(type_id: i8, status: i8) -> Self { + Self { + type_id, + status, + data: null_mut(), + } + } + + /// 구조체 기본 값으로 크레이트 식별자와 상태 코드를 반환하려고 할 때, + /// 추가적인 인자를 반환할 수 있도록 하는 함수입니다. + pub fn add_additional(mut self, value: T) -> Self { + // 데이터를 힙에 고정(Box)하고 포인터로 변환하여 소유권을 수동 관리로 전환 + self.data = Box::into_raw(Box::new(value)) as *mut c_void; + self + } +} diff --git a/core/secure-buffer/Cargo.toml b/core/secure-buffer/Cargo.toml new file mode 100644 index 0000000..7ed8e7f --- /dev/null +++ b/core/secure-buffer/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "entlib-native-secure-buffer" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[features] +std = [] + +[dependencies] +entlib-native-result.workspace = true \ No newline at end of file diff --git a/core/secure-buffer/README.md b/core/secure-buffer/README.md new file mode 100644 index 0000000..7f3b4e5 --- /dev/null +++ b/core/secure-buffer/README.md @@ -0,0 +1,39 @@ +# entlib-native-secure-buffer 기술 명세서 (초기) + +> [보안 노트](../../SECURE_NOTE.md) 참고 + +이 크레이트는 통합 제어 아키텍처(UCA)를 기반으로 구축되어, Java와 Rust 간의 상호작용에서 발생하는 메모리 소유권의 불확실성을 완벽하게 제어합니다. + +## 보안 버퍼 핵심 작동 방식 + +`SecureBuffer`는 데이터 생성 주체에 따라 두 가지 생명 주기 패턴(RO 패턴, JO 패턴)을 통해 작동하며, 모든 메모리 접근은 Zero-Trust 원칙을 따릅니다. + +* **메모리 잠금 및 할당 (OS-Level Lock)** + * 시스템의 기본 페이지 크기(4096 바이트)의 배수로 메모리를 할당하며, 할당 시점에 `heap` 영역의 잔여 데이터를 0으로 덮어씁니다. 할당된 메모리는 Unix의 `mlock` 또는 Windows의 `VirtualLock`을 통해 OS 레벨에서 잠겨, 민감 데이터가 디스크의 스왑(Swap) 영역으로 유출되는 것을 원천 차단합니다. +* **RO 패턴 작동 흐름** + * Rust 내부에서 `SecureBuffer::new_owned`를 통해 페이지 정렬 및 잠금 처리된 안전한 메모리를 할당하여 데이터를 생성합니다. + * Java로 데이터를 전달할 때는 `SecureBuffer` 내부의 `Drop` 로직이 실행되지 않도록 우회한 뒤, 포인터(`ptr`)와 길이(`len`), 그리고 `is_rust_owned = true` 플래그를 `FFIStandard` 구조체에 담아 반환합니다. + * Java 측 스코프가 종료되면 Rust 측 해제 전용 함수인 `entlib_side_secure_free`가 호출됩니다. 이 함수는 `Box::from_raw`를 통해 불투명 포인터의 소유권을 회수하고 스코프를 벗어나게 하여, 내장된 `Drop` 트레이트를 즉각 실행시킵니다. +* **JO 패턴 작동 흐름** + * Java의 FFM API `Arena`를 통해 선언된 메모리를 `SecureBuffer::from_raw_parts`를 이용하여 Rust로 주입받습니다. + * Zero-Trust 방어적 프로그래밍 원칙에 따라, 주입된 포인터와 길이가 페이지 크기(`PAGE_SIZE`)의 배수로 정확히 정렬되어 있는지 엄격히 검증하며, 어긋날 경우 사이드 채널 공격 등의 취약점으로 간주하여 즉시 거부합니다. + * Rust 내부에서 외부 메모리를 사용하는 동안에도 OS 잠금을 시도하여 스왑을 방지합니다. 연산 스코프 종료 시 Rust는 물리적 소거만 수행하며, 메모리의 최종 반환은 Java의 `Arena#close()` 호출에 위임합니다. + +## 무결성을 보장하는 물리적 소거 방식 + +보안 데이터의 사용이 끝난 후 `SecureBuffer`의 `Drop` 트레이트가 호출되면, 단순한 메모리 반환이 아닌 하드웨어 아키텍처 수준의 강력한 물리적 소거(`Zeroizer::zeroize_raw`)가 선행됩니다. 데이터 유효 길이뿐만 아니라 패딩 영역이 포함된 전체 용량(`capacity`)에 대해 꼼꼼하게 소거를 수행합니다. + +* **x86_64 아키텍처 특화 소거** + * CPU 마이크로코드에서 가장 효율적으로 0을 채우도록 설계된 `rep stosb` 어셈블리 명령어를 사용하여 메모리 구간을 고속 초기화합니다. + * 이후 `clflush` 명령어를 64바이트 캐시 라인 단위로 순회 적용하여, L1/L2/L3 캐시에 남아있을 수 있는 데이터를 무효Zero 데이터를 강제로 메인 메모리(DRAM)로 밀어내 물리적인 덮어쓰기를 완수합니다. + * 마지막으로 `mfence` 메모리 배리어를 통해 모든 저장 및 플러시 작업이 완료될 때까지 CPU의 파이프라인 실행을 강제 동기화합니다. +* **AArch64 (ARM) 아키텍처 특화 소거** + * `write_volatile`을 통해 컴파일러 최적화를 무시하고 메모리를 0으로 덮어씁니다. + * `dc civac` (Data Cache Clean and Invalidate) 명령어를 사용하여 캐시 라인을 정리하고 무효화합니다. + * `dsb sy` 명령어를 통해 데이터 동기화 배리어를 세워 소거의 원자성을 보장합니다. +* **안티 포렌식 및 컴파일러 최적화 방어** + * 컴파일러가 불필요한 연산으로 간주하여 소거 로직을 삭제해 버리는 DSE(Dead Store Elimination) 취약점을 원천 차단하기 위해, `compiler_fence(Ordering::SeqCst)` 및 `fence(Ordering::SeqCst)`를 수행하여 하드웨어 및 컴파일러 동기화를 강제합니다. + +## 규정 준수 측면 + +이러한 설계는 FIPS 140의 엄격한 요구사항을 완벽히 충족합니다. 특히 데이터의 전체 생명 주기가 하나의 컨텍스트 객체라는 단일 병목점 내에서 철저히 관리되며, 데이터 선언 ➔ 연산 ➔ 스코프 종료에 따른 물리적 소거 ➔ 완전한 할당 해제의 흐름이 코드 스코프에 의해 기계적으로 보장됩니다. \ No newline at end of file diff --git a/core/secure-buffer/src/buffer.rs b/core/secure-buffer/src/buffer.rs new file mode 100644 index 0000000..433ac89 --- /dev/null +++ b/core/secure-buffer/src/buffer.rs @@ -0,0 +1,146 @@ +use crate::memory::SecureMemoryBlock; +use crate::zeroize::{SecureZeroize, Zeroizer}; + +/// 군사급 보안 요구사항을 충족하는 고수준 보안 버퍼입니다. +/// +/// 이 구조체는 `SecureMemoryBlock`을 래핑하여, 메모리 할당부터 소멸까지의 전체 생명주기를 +/// 안전하게 관리합니다. Rust 내부 할당뿐만 아니라 외부(FFI)에서 주입된 메모리도 지원합니다. +/// +/// # Features +/// - **자동 소거 (Zeroization)**: `Drop` 시점에 할당된 전체 메모리(`capacity`)를 강제로 0으로 덮어씁니다. +/// - **메모리 잠금 (Memory Locking)**: 스왑(Swap) 영역으로의 데이터 유출을 방지하기 위해 OS 레벨 잠금을 수행합니다. +/// - **페이지 정렬 검증 (Page Alignment Check)**: 외부 메모리 주입 시, 보안 강화를 위해 페이지 정렬 여부를 엄격히 검사합니다. +pub struct SecureBuffer { + /// 데이터가 저장된 메모리의 시작 포인터 + ptr: *mut u8, + /// 데이터의 유효 길이 (바이트 단위) + len: usize, + /// 할당된 전체 메모리 용량 (바이트 단위, 소거 대상) + capacity: usize, + /// Rust가 할당한 메모리 블록 정보 (소유권이 있는 경우에만 존재) + owned_block: Option, +} + +impl SecureBuffer { + /// Rust 내부에서 페이지 정렬된 안전한 메모리를 새로 할당합니다. + /// + /// `SecureMemoryBlock`을 사용하여 OS 레벨에서 잠긴(Locked) 메모리를 할당받습니다. + /// + /// # Arguments + /// - `size` - 필요한 메모리 크기 (바이트). 내부적으로 페이지 크기 배수로 올림 처리됩니다. + /// + /// # Returns + /// - `Ok(SecureBuffer)` - 할당 및 잠금 성공 시 + /// - `Err(&'static str)` - 메모리 할당 실패 또는 OS 리소스 제한 도달 시 + pub fn new_owned(size: usize) -> Result { + let block = SecureMemoryBlock::allocate_locked(size)?; + + Ok(Self { + ptr: block.ptr, + len: size, + capacity: block.capacity, + owned_block: Some(block), + }) + } + + /// Java 등 외부 시스템에서 FFM API를 통해 전달한 메모리를 래핑합니다. + /// + /// 외부에서 할당된 메모리를 `SecureBuffer`로 감싸서, Rust 쪽에서 안전하게 사용하고 + /// 소거할 수 있게 합니다. 단, 메모리 해제는 수행하지 않습니다. + /// + /// # Security Note + /// 외부에서 주입된 메모리가 페이지 경계에 맞게 정렬되었는지(`PAGE_SIZE` 배수) 엄격하게 검증합니다. + /// 정렬되지 않은 메모리는 보안 취약점(Side-channel attack 등)의 원인이 될 수 있으므로 거부합니다. + /// + /// # Safety + /// - `ptr`은 유효한 메모리 주소를 가리켜야 합니다. + /// - `len`은 해당 메모리 영역의 올바른 크기여야 합니다. + /// - 호출자는 `ptr`이 가리키는 메모리가 `len`만큼 유효함을 보장해야 합니다. + pub unsafe fn from_raw_parts(ptr: *mut u8, len: usize) -> Result { + let ps = crate::memory::page_size(); + // 외부에서 주입된 메모리가 페이지 경계에 맞게 정렬되었는지 강제 검증 (Zero-Trust) + if !(ptr as usize).is_multiple_of(ps) { + return Err("Security Violation: External memory pointer is not page-aligned."); + } + if !len.is_multiple_of(ps) { + return Err( + "Security Violation: External memory length is not a multiple of PAGE_SIZE.", + ); + } + + #[cfg(feature = "std")] + unsafe { + // 외부 메모리라도 Rust 쪽에서 사용 중에는 스왑되지 않도록 잠금 시도 + if !crate::memory::os_lock::lock_memory(ptr, len) { + return Err("Failed to lock external memory segment."); + } + } + + Ok(Self { + ptr, + len, + capacity: len, + owned_block: None, // 외부 소유 메모리이므로 None + }) + } + + /// 버퍼의 유효 데이터 길이(바이트)를 반환합니다. + #[inline(always)] + pub fn len(&self) -> usize { + self.len + } + + /// 버퍼의 유효 데이터가 없으면 `true`를 반환합니다. + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// 버퍼의 내용을 읽기 전용 슬라이스로 반환합니다. + /// + /// # Security Note + /// 반환된 슬라이스는 `SecureBuffer`의 수명에 묶여 있습니다. + /// 슬라이스를 통해 얻은 데이터는 별도로 복사하지 말고 제자리에서 사용하십시오. + #[inline(always)] + pub fn as_slice(&self) -> &[u8] { + unsafe { core::slice::from_raw_parts(self.ptr, self.len) } + } + + /// 버퍼의 내용을 변경 가능한 슬라이스로 반환합니다. + /// + /// # Safety + /// 반환된 슬라이스를 통해 데이터를 읽거나 쓸 수 있습니다. + /// 동시성 문제가 발생하지 않도록 주의해야 합니다. + pub fn as_mut_slice(&mut self) -> &mut [u8] { + unsafe { core::slice::from_raw_parts_mut(self.ptr, self.len) } + } +} + +impl Drop for SecureBuffer { + fn drop(&mut self) { + if self.ptr.is_null() { + return; + } + + // 강제 물리적 소거 + // 할당된 '전체 capacity'에 대해 수행하여, 패딩 영역까지 꼼꼼하게 지움 + unsafe { + Zeroizer::zeroize_raw(self.ptr, self.capacity); + } + + // 소유권에 따른 메모리 해제 및 잠금 해제 분기 + if let Some(block) = &self.owned_block { + // Rust가 소유한 메모리: SecureMemoryBlock에게 해제 위임 + // (내부적으로 잠금 해제 및 dealloc 수행) + unsafe { + block.deallocate_unlocked(); + } + } else { + // 외부가 소유한 메모리: 잠금만 해제하고, 메모리 반환은 Java Arena 등에 위임 + #[cfg(feature = "std")] + unsafe { + crate::memory::os_lock::unlock_memory(self.ptr, self.capacity); + } + } + } +} diff --git a/core/secure-buffer/src/lib.rs b/core/secure-buffer/src/lib.rs new file mode 100644 index 0000000..1cf7837 --- /dev/null +++ b/core/secure-buffer/src/lib.rs @@ -0,0 +1,10 @@ +#![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +mod buffer; +mod memory; +mod zeroize; + +extern crate alloc; + +pub use buffer::SecureBuffer; diff --git a/core/secure-buffer/src/memory.rs b/core/secure-buffer/src/memory.rs new file mode 100644 index 0000000..1f73900 --- /dev/null +++ b/core/secure-buffer/src/memory.rs @@ -0,0 +1,356 @@ +use alloc::alloc::{Layout, alloc_zeroed, dealloc}; + +#[cfg(feature = "std")] +use std::sync::OnceLock; + +/// 시스템(런타임)의 실제 페이지 크기를 반환합니다. +pub(crate) fn page_size() -> usize { + #[cfg(feature = "std")] + { + static OS_PAGE_SIZE: OnceLock = OnceLock::new(); + *OS_PAGE_SIZE.get_or_init(|| unsafe { + #[cfg(unix)] + // 커널 계층과 직접 통신하여 페이지 크기 획득 + let size = fetch_os_page_size(); + + // 변조된 커널 응답 방어 (최소 4kb 및 2배수 확인) + if size < 4096 || !size.is_power_of_two() { + panic!("Security Violation: 안전하지 않거나 변조된 OS 페이지 크기가 감지되었습니다! ({})", size); + } + size + }) + } + + #[cfg(not(feature = "std"))] + { + // Q. T. Felix NOTE: no_std 환경에서는 빌드 타임 타겟 환경 변수 또는 하드웨어 레지스터 직접 조회 방식 적용이 필요합니다 + // 현재는 안전을 위해 최소 기준인 4096을 반환하되, 실제 환경에 맞춘 엄격한 포팅이 요구됩니다. + 4096 + } +} + +#[cfg(all(feature = "std", target_os = "linux"))] +unsafe fn fetch_os_page_size() -> usize { + let path = b"/proc/self/auxv\0"; + let fd: isize; + + // SYS_open (x86_64) / SYS_openat (aarch64) + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!("syscall", + in("rax") 2, // SYS_open + in("rdi") path.as_ptr(), + in("rsi") 0, // O_RDONLY + lateout("rax") fd, + options(nostack, preserves_flags) + ); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!("svc #0", + in("x8") 56, // SYS_openat + in("x0") -100isize, // AT_FDCWD + in("x1") path.as_ptr(), + in("x2") 0, // O_RDONLY + in("x3") 0, // Mode + lateout("x0") fd, + options(nostack, preserves_flags) + ); + } + + if fd < 0 { + panic!("Critical Fault: 커널이 auxv를 열기 위한 원시 시스템 호출을 거부했습니다!"); + } + + let mut buf = [0usize; 64]; + let mut extracted_page_size = 0; + let mut bytes_read: isize; + + // SYS_read 루프 + loop { + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!( + "syscall", + in("rax") 0, // SYS_read + in("rdi") fd, + in("rsi") buf.as_mut_ptr(), + in("rdx") buf.len() * core::mem::size_of::(), + lateout("rax") bytes_read, + options(nostack, preserves_flags) + ); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!( + "svc #0", + in("x8") 63, // SYS_read + in("x0") fd, + in("x1") buf.as_mut_ptr(), + in("x2") buf.len() * core::mem::size_of::(), + lateout("x0") bytes_read, + options(nostack, preserves_flags) + ); + } + + if bytes_read <= 0 { + break; + } + + let count = (bytes_read as usize) / core::mem::size_of::(); + let mut i = 0; + + while i < count { + if buf[i] == 6 { + // 상수 AT_PAGESZ = 6 + extracted_page_size = buf[i + 1]; + break; + } + if buf[i] == 0 { + // 상수 AT_NULL = 0 (auxv의 끝) + break; + } + i += 2; + } + if extracted_page_size != 0 { + break; + } + } + + // SYS_close + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!( + "syscall", + in("rax") 3, // SYS_close + in("rdi") fd, + lateout("rax") _, + options(nostack, preserves_flags) + ); + } + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!( + "svc #0", + in("x8") 57, // SYS_close + in("x0") fd, + lateout("x0") _, + options(nostack, preserves_flags) + ); + } + + if extracted_page_size == 0 { + panic!( + "Critical Fault: 프로세스 보조 벡터(auxiliary vector)에서 AT_PAGESZ를 찾을 수 없습니다!" + ); + } + + extracted_page_size +} + +#[cfg(all(feature = "std", unix, not(target_os = "linux")))] +fn fetch_os_page_size() -> usize { + unsafe extern "C" { + fn get_pagesize() -> core::ffi::c_int; + } + + let size = unsafe { get_pagesize() }; + + // 비정상적인 OS 응답 방어 + if size <= 0 { + panic!("Critical Fault: OS가 페이지 크기 획득을 거부했거나 시스템 에러가 발생했습니다!"); + } + + size as usize +} + +/// 요청된 크기를 시스템 페이지 크기의 배수로 올림 처리합니다. +/// +/// 메모리 할당 시 페이지 정렬(Page Alignment)을 보장하기 위해 사용됩니다. +fn align_to_page(size: usize) -> usize { + let ps = page_size(); + let remainder = size % ps; + if remainder == 0 { + size + } else { + size + (ps - remainder) + } +} + +/// 보안 요구사항을 충족하는 저수준 메모리 블록입니다. +/// +/// 이 구조체는 `Vec`과 달리, 메모리 할당 시점부터 보안을 고려하여 설계되었습니다. +/// +/// # Features +/// 1. 페이지 정렬(Page Alignment): 메모리 시작 주소가 페이지 경계에 맞춰지도록 할당합니다. +/// 2. Zero-Initialization**: 할당된 메모리는 즉시 0으로 초기화되어, 이전 데이터(Heap Residue)의 유출을 방지합니다. +/// 3. 메모리 잠금(Memory Locking): `std` 기능 활성화 시, OS 레벨에서 메모리 페이징(Swap)을 방지합니다. +pub struct SecureMemoryBlock { + /// 할당된 메모리의 시작 포인터 + pub ptr: *mut u8, + /// 할당된 메모리의 총 용량 (바이트 단위, 페이지 정렬됨) + pub capacity: usize, + /// 메모리 할당에 사용된 레이아웃 정보 (해제 시 필요) + pub layout: Layout, +} + +impl SecureMemoryBlock { + /// 페이지 정렬된 메모리를 할당하고, 즉시 0으로 초기화한 뒤 OS 레벨 잠금을 시도합니다. + /// + /// # Arguments + /// - `size` - 필요한 메모리 크기 (바이트). 내부적으로 페이지 크기 배수로 올림 처리됩니다. + /// + /// # Returns + /// - `Ok(SecureMemoryBlock)` - 할당 및 잠금 성공 시 + /// - `Err(&'static str)` - 메모리 할당 실패 또는 잠금 실패(리소스 제한 등) 시 + /// + /// # Safety + /// 내부적으로 `alloc_zeroed`를 사용하여 초기화되지 않은 메모리 접근(UB)을 방지합니다. + /// 하지만 OS의 메모리 잠금 제한(RLIMIT_MEMLOCK 등)에 걸릴 경우 실패할 수 있습니다. + pub fn allocate_locked(size: usize) -> Result { + let capacity = align_to_page(size); + let ps = page_size(); + // 페이지 크기로 정렬된 레이아웃 생성 + let layout = Layout::from_size_align(capacity, ps) + .map_err(|_| "Invalid memory layout: Size or alignment error")?; + + // 할당 시 남는 패딩 영역의 기존 heap 찌꺼기 데이터를 0으로 덮어씀 (Zero-Initialization) + // Safety: layout이 유효하므로 alloc_zeroed 호출은 안전함 + let ptr = unsafe { alloc_zeroed(layout) }; + if ptr.is_null() { + return Err("Memory allocation failed: Out of memory"); + } + + #[cfg(feature = "std")] + unsafe { + // OS별 메모리 잠금 수행 + if !os_lock::lock_memory(ptr, capacity) { + // 잠금 실패 시, 할당했던 메모리를 즉시 해제하고 에러 반환 + dealloc(ptr, layout); + return Err("OS memory lock (mlock/VirtualLock) failed. Resource limit reached."); + } + } + + Ok(Self { + ptr, + capacity, + layout, + }) + } + + /// 메모리 잠금을 해제하고 할당을 취소(해제)하는 함수입니다. + /// + /// # Safety + /// - 이 함수는 `Drop` 트레이트 구현 등에서 한 번만 호출되어야 합니다. + /// - 이미 해제된 메모리에 대해 호출하면 Double Free 오류가 발생합니다. + /// - 호출 전, 메모리 내용 소거는 별도로 수행되어야 합니다. (이 함수는 소거를 수행하지 않음) + pub unsafe fn deallocate_unlocked(&self) { + #[cfg(feature = "std")] + // 메모리 잠금 해제 (페이지 아웃 허용) + unsafe { + os_lock::unlock_memory(self.ptr, self.capacity); + } + + // 메모리 할당 해제 + unsafe { + dealloc(self.ptr, self.layout); + } + } +} + +/// OS별 메모리 잠금/해제 구현 모듈 +#[cfg(feature = "std")] +pub(crate) mod os_lock { + use core::ffi::c_void; + + #[cfg(unix)] + unsafe extern "C" { + fn mlock(addr: *const c_void, len: usize) -> i32; + fn munlock(addr: *const c_void, len: usize) -> i32; + } + + #[cfg(windows)] + extern "system" { + fn VirtualLock(lpAddress: *const c_void, dwSize: usize) -> i32; + fn VirtualUnlock(lpAddress: *const c_void, dwSize: usize) -> i32; + } + + /// Unix 계열(Linux, macOS 등)에서의 메모리 잠금 구현 + /// + /// `mlock` 시스템 콜을 사용하여 지정된 범위의 가상 주소 공간을 RAM에 고정합니다. + /// 성공 시 `true`, 실패 시 `false`를 반환합니다. + #[cfg(unix)] + pub unsafe fn lock_memory(ptr: *mut u8, len: usize) -> bool { + // 1차 잠금 시도 + if unsafe { mlock(ptr as *const c_void, len) == 0 } { + return true; + } + + // 1차 실패 + // os 리소스 제한 동적 해제 시도 + #[cfg(target_os = "linux")] + { + #[repr(C)] + struct Rlimit { + rlim_cur: u64, + rlim_max: u64, + } + + unsafe extern "C" { + fn get_rlimit(resource: i32, rlim: *mut Rlimit) -> i32; + fn set_rlimit(resource: i32, rlim: *const Rlimit) -> i32; + } + + const RLIMIT_MEMLOCK: i32 = 8; + const RLIM_INFINITY: u64 = u64::MAX; + + let mut rlim = Rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + + unsafe { + if get_rlimit(RLIMIT_MEMLOCK, &mut rlim) == 0 { + rlim.rlim_cur = RLIM_INFINITY; + rlim.rlim_max = RLIM_INFINITY; + + // 한도 상향 성공 시 2차 잠금 재시도 + if set_rlimit(RLIMIT_MEMLOCK, &rlim) == 0 { + return mlock(ptr as *const c_void, len) == 0; + } + } + } + } + // 권한 부 족같은 이유로 최종 실패 + false + } + + /// Unix 계열에서의 메모리 잠금 해제 구현 + /// + /// `munlock` 시스템 콜을 사용합니다. + #[cfg(unix)] + pub unsafe fn unlock_memory(ptr: *mut u8, len: usize) { + unsafe { + munlock(ptr as *const c_void, len); + } + } + + /// Windows에서의 메모리 잠금 구현 + /// + /// `VirtualLock` API를 사용하여 프로세스의 워킹 셋(Working Set)에 페이지를 고정합니다. + /// 성공 시 `true` (0이 아님), 실패 시 `false` (0)를 반환합니다. + #[cfg(windows)] + pub unsafe fn lock_memory(ptr: *mut u8, len: usize) -> bool { + VirtualLock(ptr as *const c_void, len) != 0 + } + + /// Windows에서의 메모리 잠금 해제 구현 + /// + /// `VirtualUnlock` API를 사용합니다. + #[cfg(windows)] + pub unsafe fn unlock_memory(ptr: *mut u8, len: usize) { + VirtualUnlock(ptr as *const c_void, len); + } +} diff --git a/core/secure-buffer/src/zeroize.rs b/core/secure-buffer/src/zeroize.rs new file mode 100644 index 0000000..294d339 --- /dev/null +++ b/core/secure-buffer/src/zeroize.rs @@ -0,0 +1,157 @@ +use core::sync::atomic::{Ordering, compiler_fence, fence}; + +#[cfg(target_arch = "x86_64")] +#[inline(always)] +fn get_cache_line_size() -> usize { + // CPUID Leaf 1을 호출하여 ebx 레지스터에서 clflush 크기 추출 + unsafe { + let cpuid = core::arch::x86_64::__cpuid(1); + let clflush_size = ((cpuid.ebx >> 8) & 0xFF) as usize * 8; + // CPUID 실패 또는 비정상 반환 시 안전한 기본값(Fallback)으로 64바이트 반환 + if clflush_size == 0 { 64 } else { clflush_size } + } +} + +#[cfg(target_arch = "aarch64")] +#[inline(always)] +fn get_cache_line_size() -> usize { + let ctr_el0: u64; + unsafe { + // 시스템 레지스터 CTR_EL0 (Cache Type Register) 직접 조회 + core::arch::asm!( + "mrs {}, ctr_el0", + out(reg) ctr_el0, + options(nomem, nostack, preserves_flags) + ); + } + // DminLine (bits [19:16]): 가장 작은 데이터 캐시 라인 크기의 로그(Base 2) 값 + let dminline = (ctr_el0 >> 16) & 0xF; + + // 바이트 단위 크기 계산: 4 bytes (1 word) * 2^DminLine + 4 << dminline +} + +/// 원시 포인터 기반의 물리적 메모리 소거 트레이트 +pub trait SecureZeroize { + /// 할당된 전체 용량(capacity)에 대해 소거를 수행합니다. + /// + /// # Safety + /// `ptr`은 유효한 메모리여야 하며, `capacity` 범위를 초과하여 접근하지 않아야 합니다. + unsafe fn zeroize_raw(ptr: *mut u8, capacity: usize); +} + +pub struct Zeroizer; + +impl SecureZeroize for Zeroizer { + #[inline(never)] + unsafe fn zeroize_raw(ptr: *mut u8, capacity: usize) { + if ptr.is_null() || capacity == 0 { + return; + } + + #[cfg(target_arch = "x86_64")] + unsafe { + // 하드웨어 수준의 고속 메모리 초기화 (DSE 원천 차단) + // rep stosb 명령어는 CPU 마이크로코드에서 가장 효율적으로 0을 채우도록 최적화됨 + core::arch::asm!( + "rep stosb", + inout("rcx") capacity => _, + inout("rdi") ptr => _, + in("al") 0u8, + options(nostack, preserves_flags) + ); + + // 하드웨어 레지스터 기반 동적 캐시 라인 크기 획득 + let cache_line_size = get_cache_line_size(); + + let mut flush_ptr = ptr as usize; + let end_ptr = flush_ptr + capacity; + + // 동적 크기를 반영한 Cache Line Flush 적용 + while flush_ptr < end_ptr { + core::arch::asm!( + "clflush [{0}]", + in(reg) flush_ptr, + options(readonly, nostack, preserves_flags) + ); + flush_ptr += cache_line_size; + } + + core::arch::asm!("mfence", options(nostack, preserves_flags)); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + // ARM 기반 (서버/임베디드) 환경을 위한 소거 루틴 + let mut current_ptr = ptr as usize; + let end_ptr = current_ptr + capacity; + + // 메모리 초기화 루틴 + while current_ptr < end_ptr { + core::ptr::write_volatile(current_ptr as *mut u8, 0); + current_ptr += 1; + } + + // AArch64 시스템 레지스터(CTR_EL0) 기반 동적 크기 획득 + let cache_line_size = get_cache_line_size(); + + let mut flush_ptr = ptr as usize; + let end_ptr = ptr as usize + capacity; + + // 동적 크기를 반영한 Data Cache Clean and Invalidate + while flush_ptr < end_ptr { + core::arch::asm!( + "dc civac, {0}", + in(reg) flush_ptr, + options(nostack, preserves_flags) + ); + flush_ptr += cache_line_size; + } + + core::arch::asm!("dsb sy", options(nostack, preserves_flags)); + } + + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + unsafe { + // OS가 제공하는 네이티브 안전 소거 API + #[cfg(all(unix, feature = "std"))] + { + extern "C" { + // OpenBSD, FreeBSD, Linux(glibc 2.25+) 등에서 지원 + // 컴파일러 DSE 최적화를 완벽히 우회하고 메모리에 강제 반영 + fn explicit_bzero(s: *mut core::ffi::c_void, n: usize); + } + explicit_bzero(ptr as *mut core::ffi::c_void, capacity); + } + + #[cfg(all(windows, feature = "std"))] + { + extern "system" { + // Windows 커널에서 보장하는 강제 소거 로직 + fn RtlSecureZeroMemory( + ptr: *mut core::ffi::c_void, + cnt: usize, + ) -> *mut core::ffi::c_void; + } + RtlSecureZeroMemory(ptr as *mut core::ffi::c_void, capacity); + } + + // no_std 폐쇄 환경을 위한 Fall-back + #[cfg(not(all(any(unix, windows), feature = "std")))] + { + // OS API가 부재한 베어메탈 환경에서는 기존과 같이 volatile 기반 강제 쓰기 수행 + // Q. T. Felix TODO: 해당 환경의 하드웨어(CPU) 특성에 따라 캐시 라인 플러시가 보장되지 않을 수 있다고 합니다. + // thumbv6m-none-eabi 와 같은 arm 베어메탈 아키텍처에서의 연구가 필요합니다!!!!! + let mut byte_ptr = ptr; + for _ in 0..capacity { + core::ptr::write_volatile(byte_ptr, 0); + byte_ptr = byte_ptr.add(1); + } + } + } + + // 컴파일러 및 하드웨어 파이프라인 동기화 + compiler_fence(Ordering::SeqCst); + fence(Ordering::SeqCst); + } +} diff --git a/crypto/base64/benches/base64_bench.rs b/crypto/base64/benches/base64_bench.rs deleted file mode 100644 index f793cde..0000000 --- a/crypto/base64/benches/base64_bench.rs +++ /dev/null @@ -1,175 +0,0 @@ -use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use std::time::Duration; - -use entlib_native_base64::base64::{ct_b64_to_bin_u8, ct_bin_to_b64_u8}; - -// -// 보안성 — encode: 문자 클래스별 타이밍 비교 -// - -fn b64_encode_security(c: &mut Criterion) { - let mut group = c.benchmark_group("security/b64_encode"); - group.measurement_time(Duration::from_secs(5)); - group.sample_size(1000); - - // 대문자 범위 (0-25 → 'A'-'Z') - let uppercase: Vec = (0..26).collect(); - // 소문자 범위 (26-51 → 'a'-'z') - let lowercase: Vec = (26..52).collect(); - // 숫자 범위 (52-61 → '0'-'9') - let digits: Vec = (52..62).collect(); - // 특수 문자 (62 → '+', 63 → '/') - let special: Vec = vec![62, 63]; - // 전범위 (0-63) - let full_range: Vec = (0..64).collect(); - - for (name, inputs) in [ - ("uppercase", &uppercase), - ("lowercase", &lowercase), - ("digits", &digits), - ("special", &special), - ("full_range", &full_range), - ] { - group.bench_with_input(BenchmarkId::new(name, ""), inputs, |b, inputs| { - b.iter(|| { - for &byte in inputs { - std::hint::black_box(ct_bin_to_b64_u8(std::hint::black_box(byte))); - } - }) - }); - } - - group.finish(); -} - -// -// 보안성 — decode: 입력 클래스별 타이밍 비교 -// - -fn b64_decode_security(c: &mut Criterion) { - let mut group = c.benchmark_group("security/b64_decode"); - group.measurement_time(Duration::from_secs(5)); - group.sample_size(1000); - - // 유효 대문자 ('A'-'Z') - let valid_upper: Vec = (b'A'..=b'Z').collect(); - // 유효 소문자 ('a'-'z') - let valid_lower: Vec = (b'a'..=b'z').collect(); - // 유효 숫자 ('0'-'9') - let valid_digit: Vec = (b'0'..=b'9').collect(); - // 무효 문자 (ASCII 제어 문자 + 비표준) - let invalid: Vec = vec![0x00, 0x01, 0x7F, b'@', b'[', b'`', b'{', b'~', b'!', b'#']; - // 공백 문자 - let whitespace: Vec = vec![b' ', b'\t', b'\r', b'\n']; - - for (name, inputs) in [ - ("valid_upper", &valid_upper), - ("valid_lower", &valid_lower), - ("valid_digit", &valid_digit), - ("invalid", &invalid), - ("whitespace", &whitespace), - ] { - group.bench_with_input(BenchmarkId::new(name, ""), inputs, |b, inputs| { - b.iter(|| { - for &byte in inputs { - std::hint::black_box(ct_b64_to_bin_u8(std::hint::black_box(byte))); - } - }) - }); - } - - group.finish(); -} - -// -// 처리량 — encode 전범위 (0..64), decode 전범위 (0..255) -// - -fn b64_encode_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/b64_encode"); - let inputs: Vec = (0..64).collect(); - group.throughput(Throughput::Elements(inputs.len() as u64)); - - group.bench_function("0..64", |b| { - b.iter(|| { - for &byte in &inputs { - std::hint::black_box(ct_bin_to_b64_u8(std::hint::black_box(byte))); - } - }) - }); - - group.finish(); -} - -fn b64_decode_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/b64_decode"); - let inputs: Vec = (0..=255).collect(); - group.throughput(Throughput::Elements(inputs.len() as u64)); - - group.bench_function("0..255", |b| { - b.iter(|| { - for &byte in &inputs { - std::hint::black_box(ct_b64_to_bin_u8(std::hint::black_box(byte))); - } - }) - }); - - group.finish(); -} - -fn b64_encode_16kib_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/b64_16kib_encode"); - - // 16KiB (16384 bytes) 크기의 바이너리 입력 데이터 생성 - // 데이터 캐싱 최적화를 방지하기 위해 0~255 값을 순환 배정 - let inputs: Vec = (0..16384).map(|i| (i % 256) as u8).collect(); - - // Throughput 단위를 Elements에서 Bytes로 변경하여 정확한 대역폭(MB/s) 산출 - group.throughput(Throughput::Bytes(inputs.len() as u64)); - - group.bench_function("16KiB", |b| { - b.iter(|| { - for &byte in &inputs { - std::hint::black_box(ct_bin_to_b64_u8(std::hint::black_box(byte))); - } - }) - }); - - group.finish(); -} - -fn b64_decode_16kib_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/b64_16kib_decode"); - - // 유효한 base64 문자 집합을 순환하여 16KiB 데이터 구성 - // 디코딩 시 유효하지 않은 문자로 인한 조기 반환(early exit) 분기 예측을 방지 - let b64_chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - let inputs: Vec = (0..16384).map(|i| b64_chars[i % 64]).collect(); - - group.throughput(Throughput::Bytes(inputs.len() as u64)); - - group.bench_function("16KiB", |b| { - b.iter(|| { - for &byte in &inputs { - std::hint::black_box(ct_b64_to_bin_u8(std::hint::black_box(byte))); - } - }) - }); - - group.finish(); -} - -// -// Criterion 설정 -// - -criterion_group!( - benches, - b64_encode_security, - b64_decode_security, - b64_encode_throughput, - b64_decode_throughput, - b64_encode_16kib_throughput, - b64_decode_16kib_throughput -); -criterion_main!(benches); diff --git a/crypto/base64/src/base64.rs b/crypto/base64/src/base64.rs deleted file mode 100644 index 0199ce9..0000000 --- a/crypto/base64/src/base64.rs +++ /dev/null @@ -1,66 +0,0 @@ -use entlib_native_constant_time::constant_time::ConstantTimeOps; - -/// 단일 바이트 상수-시간 `Base64` 인코딩을 수행하는 함수입니다. -#[inline(always)] -pub fn ct_bin_to_b64_u8(c: u8) -> u8 { - // 0 <= c < 26 - let mask_upper = c.wrapping_sub(26).ct_is_negative(); - // 26 <= c < 52 - let mask_lower = c.wrapping_sub(52).ct_is_negative() & !mask_upper; - // 52 <= c < 62 - let mask_digit = c.wrapping_sub(62).ct_is_negative() & !mask_lower & !mask_upper; - - let mask_plus = c.ct_eq(62); - let mask_slash = c.ct_eq(63); - - let v_upper = c.wrapping_add(65); // c + 'A' - let v_lower = c.wrapping_add(71); // c - 26 + 'a' - let v_digit = c.wrapping_sub(4); // c - 52 + '0' - let v_plus = b'+'; - let v_slash = b'/'; - - let mut res = 0u8; - res = v_upper.ct_select(res, mask_upper); - res = v_lower.ct_select(res, mask_lower); - res = v_digit.ct_select(res, mask_digit); - res = v_plus.ct_select(res, mask_plus); - res = v_slash.ct_select(res, mask_slash); - - res -} - -/// 단일 바이트 상수-시간 `Base64` 디코딩을 수행하는 함수입니다. -#[inline(always)] -pub fn ct_b64_to_bin_u8(b: u8) -> u8 { - // 범위 검사 - let mask_upper = !b.wrapping_sub(65).ct_is_negative() & b.wrapping_sub(91).ct_is_negative(); - let mask_lower = !b.wrapping_sub(97).ct_is_negative() & b.wrapping_sub(123).ct_is_negative(); - let mask_digit = !b.wrapping_sub(48).ct_is_negative() & b.wrapping_sub(58).ct_is_negative(); - - let mask_plus = b.ct_eq(b'+'); - let mask_slash = b.ct_eq(b'/'); - let mask_pad = b.ct_eq(b'='); - - // 공백 문자 허용 처리 - let mask_ws = b.ct_eq(b' ') | b.ct_eq(b'\t') | b.ct_eq(b'\r') | b.ct_eq(b'\n'); - - let v_upper = b.wrapping_sub(65); - let v_lower = b.wrapping_sub(71); - let v_digit = b.wrapping_add(4); - let v_plus = 62; - let v_slash = 63; - let v_pad = 0x81; - let v_ws = 0x80; - let v_invalid = 0xFF; - - let mut res = v_invalid; - res = v_upper.ct_select(res, mask_upper); - res = v_lower.ct_select(res, mask_lower); - res = v_digit.ct_select(res, mask_digit); - res = v_plus.ct_select(res, mask_plus); - res = v_slash.ct_select(res, mask_slash); - res = v_pad.ct_select(res, mask_pad); - res = v_ws.ct_select(res, mask_ws); - - res -} diff --git a/crypto/base64/src/lib.rs b/crypto/base64/src/lib.rs deleted file mode 100644 index 39abdce..0000000 --- a/crypto/base64/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod base64; diff --git a/crypto/chacha20/Cargo.toml b/crypto/chacha20/Cargo.toml index 72e58a7..ba2f376 100644 --- a/crypto/chacha20/Cargo.toml +++ b/crypto/chacha20/Cargo.toml @@ -5,13 +5,4 @@ edition.workspace = true authors.workspace = true license.workspace = true -[dependencies] -entlib-native-constant-time.workspace = true -entlib-native-core-secure.workspace = true - -[dev-dependencies] -criterion = { version = "0.8.2", features = ["html_reports"] } - -[[bench]] -name = "chacha20_bench" -harness = false \ No newline at end of file +[dependencies] \ No newline at end of file diff --git a/crypto/chacha20/benches/chacha20_bench.rs b/crypto/chacha20/benches/chacha20_bench.rs deleted file mode 100644 index 80779d0..0000000 --- a/crypto/chacha20/benches/chacha20_bench.rs +++ /dev/null @@ -1,118 +0,0 @@ -use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use std::hint::black_box; - -// 프로젝트 구조에 맞게 경로를 수정하여 사용하십시오. -use entlib_native_chacha20::chacha20::{chacha20_poly1305_decrypt, chacha20_poly1305_encrypt}; -use entlib_native_chacha20::chacha20_state::process_chacha20; - -/// 순수 ChaCha20 블록 암호화 처리량 (Throughput) 벤치마크 -/// 다양한 페이로드 크기(64B ~ 64KB)에 대한 스트림 암호화 성능을 측정합니다. -fn bench_chacha20_pure(c: &mut Criterion) { - let mut group = c.benchmark_group("ChaCha20_Pure_Throughput"); - let key = [0u8; 32]; - let nonce = [0u8; 12]; - let counter = 1; - - for size in [64, 1024, 8192, 65536].iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &s| { - let data = vec![0u8; s]; - b.iter(|| { - // black_box를 통해 컴파일러의 Dead Code Elimination 최적화 방지 - let _ = process_chacha20( - black_box(&key), - black_box(&nonce), - black_box(counter), - black_box(&data), - ); - }); - }); - } - group.finish(); -} - -/// ChaCha20-Poly1305 AEAD 암호화 처리량 벤치마크 -/// MAC 계산 및 메모리 복사 오버헤드가 포함된 실제 프로토콜 레벨의 암호화 성능을 측정합니다. -fn bench_aead_encrypt(c: &mut Criterion) { - let mut group = c.benchmark_group("ChaCha20_Poly1305_Encrypt"); - let key = [0u8; 32]; - let nonce = [0u8; 12]; - let aad = [0u8; 16]; // 일반적인 프로토콜 헤더 사이즈 가정 - - for size in [64, 1024, 8192, 65536].iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &s| { - let pt = vec![0u8; s]; - b.iter(|| { - let _ = chacha20_poly1305_encrypt( - black_box(&key), - black_box(&nonce), - black_box(&aad), - black_box(&pt), - ); - }); - }); - } - group.finish(); -} - -/// 보안성 검증: 상수-시간(Constant-Time) 복호화 타이밍 벤치마크 -/// 가장 중요한 보안 벤치마크입니다. 조작된 MAC이 입력되었을 때 Early-Return 하지 않고 -/// 항상 동일한 복호화 연산을 수행하는지(Zero-Trust, 분기 완전 제거) 측정합니다. -/// 두 함수의 실행 시간 그래프가 완전히 일치(Overlapping)해야 안전한 구현입니다. -fn bench_aead_decrypt_constant_time(c: &mut Criterion) { - let mut group = c.benchmark_group("ChaCha20_Poly1305_Decrypt_Security_Test"); - let key = [0u8; 32]; - let nonce = [0u8; 12]; - let aad = [0u8; 16]; - - // 8KB 페이로드 기준 테스트 - let pt_size = 8192; - let pt = vec![0u8; pt_size]; - - // 정상적인 암호문 및 태그 생성 - let valid_ct_buf = chacha20_poly1305_encrypt(&key, &nonce, &aad, &pt); - let valid_ct = valid_ct_buf.inner.clone(); - - // 조작된 태그를 가진 암호문 (마지막 바이트 1비트 반전) - let mut invalid_ct = valid_ct.clone(); - let last_idx = invalid_ct.len() - 1; - invalid_ct[last_idx] ^= 1; - - group.throughput(Throughput::Bytes(pt_size as u64)); - - // Case 1: 인증에 성공하는 정상 트래픽 - group.bench_function("Valid_MAC_Traffic", |b| { - b.iter(|| { - let _ = chacha20_poly1305_decrypt( - black_box(&key), - black_box(&nonce), - black_box(&aad), - black_box(&valid_ct), - ); - }); - }); - - // Case 2: 인증에 실패하는 공격 트래픽 - // 정상 트래픽과 처리 시간/CPU 사이클이 동일해야 타이밍 공격에 안전합니다. - group.bench_function("Invalid_MAC_Attack_Traffic", |b| { - b.iter(|| { - let _ = chacha20_poly1305_decrypt( - black_box(&key), - black_box(&nonce), - black_box(&aad), - black_box(&invalid_ct), - ); - }); - }); - - group.finish(); -} - -criterion_group!( - benches, - bench_chacha20_pure, - bench_aead_encrypt, - bench_aead_decrypt_constant_time -); -criterion_main!(benches); diff --git a/crypto/chacha20/src/chacha20.rs b/crypto/chacha20/src/chacha20.rs deleted file mode 100644 index b5bb71d..0000000 --- a/crypto/chacha20/src/chacha20.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! CHaCha20 (poly1305) 암호화 모듈입니다. -//! -//! # Authors -//! Q. T. Felix - -use crate::chacha20_state::process_chacha20; -use crate::poly1305::generate_poly1305; -use core::ptr::write_volatile; -use core::sync::atomic::{Ordering, compiler_fence}; -use entlib_native_constant_time::constant_time::ConstantTimeOps; -use entlib_native_core_secure::secure_buffer::SecureBuffer; - -const MAX_MAC_BUFFER_LEN: usize = 2048; // todo: 프로토콜 최대 패킷(MTU) 사양에 맞게 조정, def2048 - -#[inline(always)] -fn append_padded(buffer: &mut [u8], offset: &mut usize, data: &[u8]) { - let len = data.len(); - buffer[*offset..*offset + len].copy_from_slice(data); - *offset += len; - - let rem = *offset % 16; - if rem != 0 { - *offset += 16 - rem; - } -} - -/// RFC 8439 ChaCha20-Poly1305 AEAD 암호화 함수입니다. -/// -/// # Security -/// - mac_data Vec 재할당 완전 차단 (정확한 최대 크기 사전 capacity) -/// - 모든 민감 데이터 사용 즉시 zeroize -/// -/// # Return -/// `ciphertext || tag` 형태의 `SecureBuffer` -pub fn chacha20_poly1305_encrypt( - key: &[u8; 32], - nonce: &[u8; 12], - aad: &[u8], - plaintext: &[u8], -) -> SecureBuffer { - // 1. Poly1305 One-Time Key (counter = 0) - let otk_secure = process_chacha20(key, nonce, 0, &[0u8; 32]); - let mut otk = [0u8; 32]; - otk.copy_from_slice(&otk_secure.inner[0..32]); - - // 2. Ciphertext (counter = 1) - let ct_secure = process_chacha20(key, nonce, 1, plaintext); - - // 3. Poly1305 MAC 입력 구성 (고정 크기 스택 배열 사용) - let max_mac_len = aad.len() + ct_secure.inner.len() + 48; - assert!( - max_mac_len <= MAX_MAC_BUFFER_LEN, - "MAC data exceeds max buffer length" - ); - - let mut mac_data = [0u8; MAX_MAC_BUFFER_LEN]; - let mut offset = 0; - - append_padded(&mut mac_data, &mut offset, aad); - append_padded(&mut mac_data, &mut offset, &ct_secure.inner); - - mac_data[offset..offset + 8].copy_from_slice(&(aad.len() as u64).to_le_bytes()); - offset += 8; - mac_data[offset..offset + 8].copy_from_slice(&(ct_secure.inner.len() as u64).to_le_bytes()); - offset += 8; - - // 4. Tag 생성 (전체 버퍼가 아닌 실제 데이터 길이만큼만 전달) - let tag_secure = generate_poly1305(&otk, &mac_data[..offset]); - - // 5. 최종 결과 = CT + Tag - let ct_len = ct_secure.inner.len(); - let mut result = SecureBuffer { - inner: vec![0u8; ct_len + 16], - }; - result.inner[0..ct_len].copy_from_slice(&ct_secure.inner); - result.inner[ct_len..].copy_from_slice(&tag_secure.inner); - - // 6. 모든 민감 데이터 즉시 zeroize - drop(otk_secure); - drop(ct_secure); - drop(tag_secure); - - for b in mac_data.iter_mut() { - unsafe { - write_volatile(b, 0); - } - } - for b in otk.iter_mut() { - unsafe { - write_volatile(b, 0); - } - } - compiler_fence(Ordering::SeqCst); - - result -} - -/// RFC 8439 ChaCha20-Poly1305 AEAD 복호화 함수입니다. -/// -/// # Security -/// - Verify-then-Decrypt 패턴 완벽 준수 (MAC 검증 -> 성공시에만 복호화) -/// - mac_data Vec 재할당 완전 차단 -/// - 검증 실패 시 `None` 반환 -> 0-byte 평문 충돌 방지 및 명확한 에러 핸들링 보장 -pub fn chacha20_poly1305_decrypt( - key: &[u8; 32], - nonce: &[u8; 12], - aad: &[u8], - ciphertext_with_tag: &[u8], -) -> Option { - // 0. 기본 길이 검증 (공개 정보) - if ciphertext_with_tag.len() < 16 { - return None; - } - - let ct_len = ciphertext_with_tag.len() - 16; - let ct = &ciphertext_with_tag[0..ct_len]; - let received_tag = &ciphertext_with_tag[ct_len..]; - - // 1. Poly1305 One-Time Key 생성 - let otk_secure = process_chacha20(key, nonce, 0, &[0u8; 32]); - let mut otk = [0u8; 32]; - otk.copy_from_slice(&otk_secure.inner[0..32]); - - // 2. MAC 입력 데이터 구성 (반드시 고정 크기 스택 배열 사용) - let max_mac_len = aad.len() + ct.len() + 48; - if max_mac_len > MAX_MAC_BUFFER_LEN { - return None; // 스펙 초과 시 즉시 실패 - } - - let mut mac_data = [0u8; MAX_MAC_BUFFER_LEN]; - let mut offset = 0; - - append_padded(&mut mac_data, &mut offset, aad); - append_padded(&mut mac_data, &mut offset, ct); - - mac_data[offset..offset + 8].copy_from_slice(&(aad.len() as u64).to_le_bytes()); - offset += 8; - mac_data[offset..offset + 8].copy_from_slice(&(ct.len() as u64).to_le_bytes()); - offset += 8; - - // 3. Expected Tag 계산 - let expected_tag_secure = generate_poly1305(&otk, &mac_data[..offset]); - - // 4. 상수-시간 tag 검증 - let mut xor_diff = 0u8; - for (i, item) in received_tag.iter().enumerate().take(16) { - xor_diff |= item ^ received_tag[i]; // Q. T. Felix NOTE: 클리피 수정 - } - let mask = xor_diff.ct_is_zero(); // valid -> 0xFF, invalid -> 0x00 - - // 5. 조건 분기 없는 완전한 항상 복호화 수행 및 ct_select 선택 - let pt_secure = process_chacha20(key, nonce, 1, ct); - let mut result_buf = SecureBuffer { - inner: vec![0u8; ct_len], - }; - - // 마스크에 따라 원본 복호화 데이터(0xFF) 또는 0(0x00)을 상수-시간으로 선택 - for i in 0..ct_len { - result_buf.inner[i] = pt_secure.inner[i].ct_select(0, mask); - } - drop(pt_secure); - - // 6. 민감 데이터 즉시 소거 (스택 배열은 사용된 길이까지만 소거) - drop(otk_secure); - drop(expected_tag_secure); - - for byte in mac_data.iter_mut().take(offset) { - unsafe { - write_volatile(byte, 0); - } - } - for b in otk.iter_mut() { - unsafe { - write_volatile(b, 0); - } - } - compiler_fence(Ordering::SeqCst); - - // 7. 결과 반환 - // Q. T. Felix NOTE: 여기서 논리적 수행을 위한 분기를 사용하는데 암호화된 값 반환에 대한 조건문이라서 - // 크게 상관없을거라 판단했고 일단 놔두겠지만, 추 후 상수-시간 연산 도입 고려해볼 만 함 - if mask == 0xFF { Some(result_buf) } else { None } -} diff --git a/crypto/chacha20/src/chacha20_state.rs b/crypto/chacha20/src/chacha20_state.rs deleted file mode 100644 index 4341932..0000000 --- a/crypto/chacha20/src/chacha20_state.rs +++ /dev/null @@ -1,183 +0,0 @@ -use core::ptr::write_volatile; -use core::sync::atomic::{Ordering, compiler_fence}; -use entlib_native_constant_time::constant_time::ConstantTimeOps; -use entlib_native_core_secure::secure_buffer::SecureBuffer; - -/// 내부 연산을 위해 캡슐화된 ChaCha20 상태(state) 구조체입니다. -/// 메모리 안전성을 위해 스코프를 벗어날 때 즉각적으로 데이터를 소거(zeroize)합니다. -struct ChaCha20State { - inner: [u32; 16], -} - -impl ChaCha20State { - #[inline(always)] - fn new(key: &[u8; 32], nonce: &[u8; 12], counter: u32) -> Self { - let mut state = [0u32; 16]; - - // 상수(constants) "expand 32-byte k" - state[0] = 0x61707865; - state[1] = 0x3320646e; - state[2] = 0x79622d32; - state[3] = 0x6b206574; - - // 키(key) - for i in 0..8 { - state[4 + i] = u32::from_le_bytes(key[i * 4..(i + 1) * 4].try_into().unwrap()); - } - - // 카운터(counter) - state[12] = counter; - - // 논스(nonce) - for i in 0..3 { - state[13 + i] = u32::from_le_bytes(nonce[i * 4..(i + 1) * 4].try_into().unwrap()); - } - - Self { inner: state } - } - - #[inline(always)] - fn process_block(&mut self, output: &mut [u8; 64]) { - let mut working_state = self.inner; - - for _ in 0..10 { - // 열 라운드(column round) - self.quarter_round_on_state(&mut working_state, 0, 4, 8, 12); - self.quarter_round_on_state(&mut working_state, 1, 5, 9, 13); - self.quarter_round_on_state(&mut working_state, 2, 6, 10, 14); - self.quarter_round_on_state(&mut working_state, 3, 7, 11, 15); - - // 대각선 라운드(diagonal round) - self.quarter_round_on_state(&mut working_state, 0, 5, 10, 15); - self.quarter_round_on_state(&mut working_state, 1, 6, 11, 12); - self.quarter_round_on_state(&mut working_state, 2, 7, 8, 13); - self.quarter_round_on_state(&mut working_state, 3, 4, 9, 14); - } - - for i in 0..16 { - working_state[i] = working_state[i].wrapping_add(self.inner[i]); - let bytes = working_state[i].to_le_bytes(); - output[i * 4..(i + 1) * 4].copy_from_slice(&bytes); - } - - // 블록 처리 완료 후 워킹 상태 명시적 소거(zeroize) - for word in working_state.iter_mut() { - unsafe { - write_volatile(word, 0); - } - } - compiler_fence(Ordering::SeqCst); - } - - #[inline(always)] - fn quarter_round_on_state( - &self, - state: &mut [u32; 16], - a: usize, - b: usize, - c: usize, - d: usize, - ) { - state[a] = state[a].wrapping_add(state[b]); - state[d] ^= state[a]; - state[d] = state[d].rotate_left(16); - state[c] = state[c].wrapping_add(state[d]); - state[b] ^= state[c]; - state[b] = state[b].rotate_left(12); - state[a] = state[a].wrapping_add(state[b]); - state[d] ^= state[a]; - state[d] = state[d].rotate_left(8); - state[c] = state[c].wrapping_add(state[d]); - state[b] ^= state[c]; - state[b] = state[b].rotate_left(7); - } - - #[inline(always)] - fn increment_counter(&mut self) { - self.inner[12] = self.inner[12].wrapping_add(1); - } -} - -impl Drop for ChaCha20State { - fn drop(&mut self) { - for word in self.inner.iter_mut() { - unsafe { - write_volatile(word, 0); - } - } - compiler_fence(Ordering::SeqCst); - } -} - -/// ChaCha20 알고리즘을 사용하여 데이터를 암호화 또는 복호화합니다. -/// 연산 결과는 자바(java) 힙의 생명주기와 분리된 `SecureBuffer`로 반환됩니다. -pub fn process_chacha20( - key: &[u8; 32], - nonce: &[u8; 12], - initial_counter: u32, - data: &[u8], -) -> SecureBuffer { - let mut state = ChaCha20State::new(key, nonce, initial_counter); - let mut result = SecureBuffer { - inner: vec![0u8; data.len()], - }; - - let mut key_stream_block = [0u8; 64]; - let mut chunks = data.chunks_exact(64); - let mut out_chunks = result.inner.chunks_exact_mut(64); - - // 전체 블록(full block) 처리 - for (chunk, out_chunk) in chunks.by_ref().zip(out_chunks.by_ref()) { - state.process_block(&mut key_stream_block); - for i in 0..64 { - out_chunk[i] = chunk[i] ^ key_stream_block[i]; - } - state.increment_counter(); - } - - // 잔여 블록(partial block) 처리 - let remainder = chunks.remainder(); - if !remainder.is_empty() { - let out_remainder = out_chunks.into_remainder(); - let rem_len = remainder.len(); - state.process_block(&mut key_stream_block); - - // 메모리 경계(out-of-bounds) 오류를 방지하기 위해 64바이트 임시 버퍼 사용 - let mut temp_block = [0u8; 64]; - - temp_block[..rem_len].copy_from_slice(&remainder[..rem_len]); - - // 전체 블록 길이에 대해 고정 반복하여 암호화 핵심 연산의 타이밍 리스크 제거 - for i in 0..64 { - let xored = temp_block[i] ^ key_stream_block[i]; - - // 인덱스 `i`가 잔여 블록 길이(`rem_len`) 미만인지 판별하는 상수-시간 마스크(mask) 생성 - // `i - rem_len`이 음수이면 참(All 1s), 0 이상이면 거짓(0) 반환 - let diff = (i as isize).wrapping_sub(rem_len as isize); - let mask = diff.ct_is_negative() as u8; - - // 마스크에 따라 XOR된 값 또는 원본 임시 버퍼 값 선택 (CT MUX) - temp_block[i] = xored.ct_select(temp_block[i], mask); - } - - // 마스킹이 완료된 안전한 데이터를 결과 버퍼에 복사 - out_remainder[..rem_len].copy_from_slice(&temp_block[..rem_len]); - - // 임시 버퍼 안전 소거(zeroize) - for byte in temp_block.iter_mut() { - unsafe { - write_volatile(byte, 0); - } - } - } - - // 키 스트림 블록 안전 소거 - for byte in key_stream_block.iter_mut() { - unsafe { - write_volatile(byte, 0); - } - } - compiler_fence(Ordering::SeqCst); - - result -} diff --git a/crypto/chacha20/src/lib.rs b/crypto/chacha20/src/lib.rs index 947c845..0c9ac1a 100644 --- a/crypto/chacha20/src/lib.rs +++ b/crypto/chacha20/src/lib.rs @@ -1,3 +1 @@ -pub mod chacha20; -pub mod chacha20_state; -pub mod poly1305; +#![no_std] diff --git a/crypto/chacha20/src/poly1305.rs b/crypto/chacha20/src/poly1305.rs deleted file mode 100644 index 69c9ad1..0000000 --- a/crypto/chacha20/src/poly1305.rs +++ /dev/null @@ -1,248 +0,0 @@ -use core::ptr::write_volatile; -use core::sync::atomic::{Ordering, compiler_fence}; -use entlib_native_constant_time::constant_time::ConstantTimeOps; -use entlib_native_core_secure::secure_buffer::SecureBuffer; - -/// 내부 연산을 위해 캡슐화된 Poly1305 상태(state) 구조체입니다. -/// 메모리 안전성을 위해 스코프를 벗어날 때 즉각적으로 데이터를 소거(zeroize)합니다. -struct Poly1305State { - r: [u32; 5], - h: [u32; 5], - pad: [u32; 4], -} - -impl Poly1305State { - #[inline(always)] - fn new(key: &[u8; 32]) -> Self { - // 상수-시간 클램핑(clamping) 적용 - let t0 = u32::from_le_bytes(key[0..4].try_into().unwrap()); - let t1 = u32::from_le_bytes(key[4..8].try_into().unwrap()); - let t2 = u32::from_le_bytes(key[8..12].try_into().unwrap()); - let t3 = u32::from_le_bytes(key[12..16].try_into().unwrap()); - - let r0 = t0 & 0x03ffffff; - let r1 = ((t0 >> 26) | (t1 << 6)) & 0x03ffff03; - let r2 = ((t1 >> 20) | (t2 << 12)) & 0x03ffc0ff; - let r3 = ((t2 >> 14) | (t3 << 18)) & 0x03f03fff; - let r4 = (t3 >> 8) & 0x000fffff; - - let pad0 = u32::from_le_bytes(key[16..20].try_into().unwrap()); - let pad1 = u32::from_le_bytes(key[20..24].try_into().unwrap()); - let pad2 = u32::from_le_bytes(key[24..28].try_into().unwrap()); - let pad3 = u32::from_le_bytes(key[28..32].try_into().unwrap()); - - Self { - r: [r0, r1, r2, r3, r4], - h: [0; 5], - pad: [pad0, pad1, pad2, pad3], - } - } - - #[inline(always)] - fn process_block(&mut self, block: &[u8; 16], is_full: u8) { - let m0 = u32::from_le_bytes(block[0..4].try_into().unwrap()); - let m1 = u32::from_le_bytes(block[4..8].try_into().unwrap()); - let m2 = u32::from_le_bytes(block[8..12].try_into().unwrap()); - let m3 = u32::from_le_bytes(block[12..16].try_into().unwrap()); - - // Radix-26 분할 및 129번째 비트(패딩) 병합 - let limb0 = m0 & 0x03ffffff; - let limb1 = ((m0 >> 26) | (m1 << 6)) & 0x03ffffff; - let limb2 = ((m1 >> 20) | (m2 << 12)) & 0x03ffffff; - let limb3 = ((m2 >> 14) | (m3 << 18)) & 0x03ffffff; - let mut limb4 = m3 >> 8; - - // 전체 블록일 경우에만 129번째 비트를 추가 (CT MUX 사용) - let pad_bit = 1u32 << 24; - let mask = (is_full as u32).wrapping_neg(); // 1 -> 0xFFFFFFFF, 0 -> 0x00000000 - limb4 |= pad_bit.ct_select(0, mask); - - self.h[0] += limb0; - self.h[1] += limb1; - self.h[2] += limb2; - self.h[3] += limb3; - self.h[4] += limb4; - - let s1 = self.r[1] * 5; - let s2 = self.r[2] * 5; - let s3 = self.r[3] * 5; - let s4 = self.r[4] * 5; - - // u64 캐스팅을 통한 오버플로우 방지 모듈러 곱셈 - let d0 = (self.h[0] as u64 * self.r[0] as u64) - + (self.h[1] as u64 * s4 as u64) - + (self.h[2] as u64 * s3 as u64) - + (self.h[3] as u64 * s2 as u64) - + (self.h[4] as u64 * s1 as u64); - let d1 = (self.h[0] as u64 * self.r[1] as u64) - + (self.h[1] as u64 * self.r[0] as u64) - + (self.h[2] as u64 * s4 as u64) - + (self.h[3] as u64 * s3 as u64) - + (self.h[4] as u64 * s2 as u64); - let d2 = (self.h[0] as u64 * self.r[2] as u64) - + (self.h[1] as u64 * self.r[1] as u64) - + (self.h[2] as u64 * self.r[0] as u64) - + (self.h[3] as u64 * s4 as u64) - + (self.h[4] as u64 * s3 as u64); - let d3 = (self.h[0] as u64 * self.r[3] as u64) - + (self.h[1] as u64 * self.r[2] as u64) - + (self.h[2] as u64 * self.r[1] as u64) - + (self.h[3] as u64 * self.r[0] as u64) - + (self.h[4] as u64 * s4 as u64); - let d4 = (self.h[0] as u64 * self.r[4] as u64) - + (self.h[1] as u64 * self.r[3] as u64) - + (self.h[2] as u64 * self.r[2] as u64) - + (self.h[3] as u64 * self.r[1] as u64) - + (self.h[4] as u64 * self.r[0] as u64); - - // 캐리(carry) 전파 - let mut c = (d0 >> 26) as u32; - self.h[0] = (d0 as u32) & 0x03ffffff; - let d1_v = d1 + c as u64; - c = (d1_v >> 26) as u32; - self.h[1] = (d1_v as u32) & 0x03ffffff; - let d2_v = d2 + c as u64; - c = (d2_v >> 26) as u32; - self.h[2] = (d2_v as u32) & 0x03ffffff; - let d3_v = d3 + c as u64; - c = (d3_v >> 26) as u32; - self.h[3] = (d3_v as u32) & 0x03ffffff; - let d4_v = d4 + c as u64; - c = (d4_v >> 26) as u32; - self.h[4] = (d4_v as u32) & 0x03ffffff; - self.h[0] += c * 5; - c = self.h[0] >> 26; - self.h[0] &= 0x03ffffff; - self.h[1] += c; - } - - #[inline(always)] - fn finalize(mut self, output: &mut [u8; 16]) { - // 잔여 캐리 완벽 전파 - let mut c = self.h[1] >> 26; - self.h[1] &= 0x03ffffff; - self.h[2] += c; - c = self.h[2] >> 26; - self.h[2] &= 0x03ffffff; - self.h[3] += c; - c = self.h[3] >> 26; - self.h[3] &= 0x03ffffff; - self.h[4] += c; - c = self.h[4] >> 26; - self.h[4] &= 0x03ffffff; - self.h[0] += c * 5; - c = self.h[0] >> 26; - self.h[0] &= 0x03ffffff; - self.h[1] += c; - - // h + 5 모듈러 감산을 위한 임시 값 계산 - let mut g0 = self.h[0] + 5; - c = g0 >> 26; - g0 &= 0x03ffffff; - let mut g1 = self.h[1] + c; - c = g1 >> 26; - g1 &= 0x03ffffff; - let mut g2 = self.h[2] + c; - c = g2 >> 26; - g2 &= 0x03ffffff; - let mut g3 = self.h[3] + c; - c = g3 >> 26; - g3 &= 0x03ffffff; - let mut g4 = self.h[4] + c; - - // g4에서 2^130 (1 << 26)을 뺐을 때 음수인지 판별 - let sub = g4.wrapping_sub(1 << 26); - let mask = sub.ct_is_negative(); - - // 뺄셈 결과가 음수면 원래의 h를, 아니면 g를 선택 - g4 = sub.ct_select(g4, mask) & 0x03ffffff; - let f0 = self.h[0].ct_select(g0, mask); - let f1 = self.h[1].ct_select(g1, mask); - let f2 = self.h[2].ct_select(g2, mask); - let f3 = self.h[3].ct_select(g3, mask); - let f4 = self.h[4].ct_select(g4, mask); - - // 32비트 결합 - let mut out0 = f0 | (f1 << 26); - let mut out1 = (f1 >> 6) | (f2 << 20); - let mut out2 = (f2 >> 12) | (f3 << 14); - let mut out3 = (f3 >> 18) | (f4 << 8); - - // 패딩(pad) 값 상수-시간 병합 - let (v0, c0) = out0.overflowing_add(self.pad[0]); - out0 = v0; - let (v1, c1) = out1.overflowing_add(self.pad[1].wrapping_add(c0 as u32)); - out1 = v1; - let (v2, c2) = out2.overflowing_add(self.pad[2].wrapping_add(c1 as u32)); - out2 = v2; - let (v3, _) = out3.overflowing_add(self.pad[3].wrapping_add(c2 as u32)); - out3 = v3; - - output[0..4].copy_from_slice(&out0.to_le_bytes()); - output[4..8].copy_from_slice(&out1.to_le_bytes()); - output[8..12].copy_from_slice(&out2.to_le_bytes()); - output[12..16].copy_from_slice(&out3.to_le_bytes()); - } -} - -impl Drop for Poly1305State { - fn drop(&mut self) { - for word in self - .r - .iter_mut() - .chain(self.h.iter_mut()) - .chain(self.pad.iter_mut()) - { - unsafe { - write_volatile(word, 0); - } - } - compiler_fence(Ordering::SeqCst); - } -} - -/// Poly1305 MAC을 생성합니다. -/// 연산 결과는 Java 힙의 생명주기와 분리된 `SecureBuffer`로 반환됩니다. -pub fn generate_poly1305(key: &[u8; 32], data: &[u8]) -> SecureBuffer { - let mut state = Poly1305State::new(key); - let mut chunks = data.chunks_exact(16); - - for chunk in chunks.by_ref() { - let block: &[u8; 16] = chunk.try_into().unwrap(); - state.process_block(block, 1); - } - - let remainder = chunks.remainder(); - if !remainder.is_empty() { - let mut pad_block = [0u8; 16]; - let rem_len = remainder.len(); - - pad_block[..rem_len].copy_from_slice(&remainder[..rem_len]); - pad_block[rem_len] = 1; // RFC 8439 잔여 블록 패딩 적용 - - state.process_block(&pad_block, 0); - - for byte in pad_block.iter_mut() { - unsafe { - write_volatile(byte, 0); - } - } - } - - let mut mac = [0u8; 16]; - state.finalize(&mut mac); - - let mut result = SecureBuffer { - inner: vec![0u8; 16], - }; - result.inner.copy_from_slice(&mac); - - for byte in mac.iter_mut() { - unsafe { - write_volatile(byte, 0); - } - } - compiler_fence(Ordering::SeqCst); - - result -} diff --git a/crypto/chacha20/tests/chacha20_test.rs b/crypto/chacha20/tests/chacha20_test.rs deleted file mode 100644 index f9d4fda..0000000 --- a/crypto/chacha20/tests/chacha20_test.rs +++ /dev/null @@ -1,155 +0,0 @@ -#[cfg(test)] -mod tests { - use entlib_native_chacha20::chacha20::{chacha20_poly1305_decrypt, chacha20_poly1305_encrypt}; - - /// RFC 8439 Section 2.8.2. ChaCha20-Poly1305 Test Vector - #[test] - fn test_rfc8439_aead_vector() { - let key: [u8; 32] = [ - 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, - 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, - 0x9c, 0x9d, 0x9e, 0x9f, - ]; - let nonce: [u8; 12] = [ - 0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, - ]; - let aad: [u8; 12] = [ - 0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, - ]; - let plaintext = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; - - let expected_ciphertext: [u8; 114] = [ - 0xd3, 0x1a, 0x8d, 0x34, 0x64, 0x8e, 0x60, 0xdb, 0x7b, 0x86, 0xaf, 0xbc, 0x53, 0xef, - 0x7e, 0xc2, 0xa4, 0xad, 0xed, 0x51, 0x29, 0x6e, 0x08, 0xfe, 0xa9, 0xe2, 0xb5, 0xa7, - 0x36, 0xee, 0x62, 0xd6, 0x3d, 0xbe, 0xa4, 0x5e, 0x8c, 0xa9, 0x67, 0x12, 0x82, 0xfa, - 0xfb, 0x69, 0xda, 0x92, 0x72, 0x8b, 0x1a, 0x71, 0xde, 0x0a, 0x9e, 0x06, 0x0b, 0x29, - 0x05, 0xd6, 0xa5, 0xb6, 0x7e, 0xcd, 0x3b, 0x36, 0x92, 0xdd, 0xbd, 0x7f, 0x2d, 0x77, - 0x8b, 0x8c, 0x98, 0x03, 0xae, 0xe3, 0x28, 0x09, 0x1b, 0x58, 0xfa, 0xb3, 0x24, 0xe4, - 0xfa, 0xd6, 0x75, 0x94, 0x55, 0x85, 0x80, 0x8b, 0x48, 0x31, 0xd7, 0xbc, 0x3f, 0xf4, - 0xde, 0xf0, 0x8e, 0x4b, 0x7a, 0x9d, 0xe5, 0x76, 0xd2, 0x65, 0x86, 0xce, 0xc6, 0x4b, - 0x61, 0x16, - ]; - let expected_tag: [u8; 16] = [ - 0x1a, 0xe1, 0x0b, 0x59, 0x4f, 0x09, 0xe2, 0x6a, 0x7e, 0x90, 0x2e, 0xcb, 0xd0, 0x60, - 0x06, 0x91, - ]; - - // 암호화 검증 - let ct_secure = chacha20_poly1305_encrypt(&key, &nonce, &aad, plaintext); - let ct_len = ct_secure.inner.len(); - - assert_eq!( - ct_len, - expected_ciphertext.len() + 16, - "Ciphertext length mismatch" - ); - assert_eq!( - &ct_secure.inner[..ct_len - 16], - &expected_ciphertext[..], - "Ciphertext data mismatch" - ); - assert_eq!( - &ct_secure.inner[ct_len - 16..], - &expected_tag[..], - "MAC tag mismatch" - ); - - // 복호화 검증 - let pt_secure_opt = chacha20_poly1305_decrypt(&key, &nonce, &aad, &ct_secure.inner); - assert!( - pt_secure_opt.is_some(), - "Decryption failed for valid RFC 8439 vector" - ); - - let pt_secure = pt_secure_opt.unwrap(); - assert_eq!( - &pt_secure.inner[..], - plaintext, - "Decrypted plaintext mismatch" - ); - } - - /// 조작된 암호문(Ciphertext)이 입력될 경우 Fail-fast (None 반환) 처리되는지 검증합니다. - #[test] - fn test_aead_tampered_ciphertext() { - let key = [0xAA; 32]; - let nonce = [0xBB; 12]; - let aad = b"header_data"; - let plaintext = b"secret_message"; - - let valid_ct = chacha20_poly1305_encrypt(&key, &nonce, aad, plaintext); - - // 1바이트 변조 (가장 첫 번째 암호문 바이트) - let mut invalid_ct = valid_ct.inner.clone(); - invalid_ct[0] ^= 0x01; - - let result = chacha20_poly1305_decrypt(&key, &nonce, aad, &invalid_ct); - assert!( - result.is_none(), - "Tampered ciphertext should NOT decrypt successfully" - ); - } - - /// 조작된 연관 데이터(AAD)가 입력될 경우 Fail-fast (None 반환) 처리되는지 검증합니다. - #[test] - fn test_aead_tampered_aad() { - let key = [0x11; 32]; - let nonce = [0x22; 12]; - let aad = b"protocol_v1"; - let plaintext = b"login_request"; - - let valid_ct = chacha20_poly1305_encrypt(&key, &nonce, aad, plaintext); - - // AAD 변조 (버전 정보 조작 시도) - let tampered_aad = b"protocol_v2"; - - let result = chacha20_poly1305_decrypt(&key, &nonce, tampered_aad, &valid_ct.inner); - assert!( - result.is_none(), - "Tampered AAD should NOT decrypt successfully" - ); - } - - /// 0-byte 평문 충돌 결함이 해결되었는지 검증합니다. - /// 빈 평문을 암호화하고 정상적으로 복호화되는지 확인합니다. - #[test] - fn test_aead_empty_plaintext() { - let key = [0x33; 32]; - let nonce = [0x44; 12]; - let aad = b"empty_payload_test"; - let plaintext: &[u8] = b""; - - let ct_secure = chacha20_poly1305_encrypt(&key, &nonce, aad, plaintext); - - // 암호문은 순수하게 16바이트의 MAC Tag로만 구성되어야 함 - assert_eq!(ct_secure.inner.len(), 16); - - let pt_secure_opt = chacha20_poly1305_decrypt(&key, &nonce, aad, &ct_secure.inner); - assert!( - pt_secure_opt.is_some(), - "0-byte plaintext should decrypt successfully" - ); - - let pt_secure = pt_secure_opt.unwrap(); - assert_eq!( - pt_secure.inner.len(), - 0, - "Decrypted plaintext should be empty" - ); - } - - /// 입력 길이가 16바이트(MAC 크기) 미만인 악의적인 패킷에 대한 검증 로직 테스트 - #[test] - fn test_aead_too_short_ciphertext() { - let key = [0x55; 32]; - let nonce = [0x66; 12]; - let aad = b""; - let short_ct = [0u8; 15]; // MAC 16바이트보다 짧은 데이터 - - let result = chacha20_poly1305_decrypt(&key, &nonce, aad, &short_ct); - assert!( - result.is_none(), - "Ciphertext shorter than 16 bytes must return None" - ); - } -} diff --git a/crypto/constant-time/benches/ct_ops_security.rs b/crypto/constant-time/benches/ct_ops_security.rs deleted file mode 100644 index 67cfc74..0000000 --- a/crypto/constant-time/benches/ct_ops_security.rs +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright (c) 2025-2026 Quant - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the “Software”), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; -use std::time::Duration; - -use entlib_native_constant_time::constant_time::ConstantTimeOps; - -// -// Macros — 반복 제거를 위한 보안성 벤치마크 매크로 -// - -/// 단항 CT 연산의 타이밍 일관성 벤치마크. -/// 같은 그룹 내 입력 클래스들의 신뢰구간이 겹쳐야 상수-시간 보장. -macro_rules! security_bench_unary { - ($c:expr, $group_name:expr, $op:ident, - $($class:expr => $val:expr),+ $(,)?) => {{ - let mut group = $c.benchmark_group($group_name); - group.measurement_time(Duration::from_secs(5)); - group.sample_size(1000); - $( - group.bench_with_input( - BenchmarkId::new($class, ""), - &$val, - |b, &v| b.iter(|| std::hint::black_box(std::hint::black_box(v).$op())), - ); - )+ - group.finish(); - }}; -} - -/// 이항 CT 연산의 타이밍 일관성 벤치마크. -macro_rules! security_bench_binary { - ($c:expr, $group_name:expr, $op:ident, - $($class:expr => ($a:expr, $b:expr)),+ $(,)?) => {{ - let mut group = $c.benchmark_group($group_name); - group.measurement_time(Duration::from_secs(5)); - group.sample_size(1000); - $( - group.bench_with_input( - BenchmarkId::new($class, ""), - &($a, $b), - |b, &(a, other)| b.iter(|| { - std::hint::black_box( - std::hint::black_box(a).$op(std::hint::black_box(other)) - ) - }), - ); - )+ - group.finish(); - }}; -} - -/// 삼항 ct_select 연산의 타이밍 일관성 벤치마크. -macro_rules! security_bench_select { - ($c:expr, $group_name:expr, - $($class:expr => ($a:expr, $b:expr, $mask:expr)),+ $(,)?) => {{ - let mut group = $c.benchmark_group($group_name); - group.measurement_time(Duration::from_secs(5)); - group.sample_size(1000); - $( - group.bench_with_input( - BenchmarkId::new($class, ""), - &($a, $b, $mask), - |b, &(a, other, mask)| b.iter(|| { - std::hint::black_box( - std::hint::black_box(a).ct_select( - std::hint::black_box(other), - std::hint::black_box(mask), - ) - ) - }), - ); - )+ - group.finish(); - }}; -} - -// -// ct_eq — 이항 (equal 판별) -// - -fn ct_eq_security(c: &mut Criterion) { - // u8 (Tier2) - security_bench_binary!(c, "security/ct_eq/u8", ct_eq, - "equal_zeros" => (0u8, 0u8), - "equal_ones" => (0xFFu8, 0xFFu8), - "hamming_1_diff" => (0u8, 1u8), - "hamming_max" => (0x55u8, 0xAAu8), - "random" => (0x5Cu8, 0xA3u8), - ); - // u32 (Tier1) - security_bench_binary!(c, "security/ct_eq/u32", ct_eq, - "equal_zeros" => (0u32, 0u32), - "equal_ones" => (!0u32, !0u32), - "hamming_1_diff" => (0u32, 1u32), - "hamming_max" => (0x5555_5555u32, 0xAAAA_AAAAu32), - "random" => (0x5C3A_7F91u32, 0xA3C5_806Eu32), - ); - // u64 (Tier1) - security_bench_binary!(c, "security/ct_eq/u64", ct_eq, - "equal_zeros" => (0u64, 0u64), - "equal_ones" => (!0u64, !0u64), - "hamming_1_diff" => (0u64, 1u64), - "hamming_max" => (0x5555_5555_5555_5555u64, 0xAAAA_AAAA_AAAA_AAAAu64), - "random" => (0x5C3A_7F91_D2B8_4E06u64, 0xA3C5_806E_2D47_B1F9u64), - ); - // u128 (Tier2) - security_bench_binary!(c, "security/ct_eq/u128", ct_eq, - "equal_zeros" => (0u128, 0u128), - "equal_ones" => (!0u128, !0u128), - "hamming_1_diff" => (0u128, 1u128), - "hamming_max" => (0x5555_5555_5555_5555_5555_5555_5555_5555u128, - 0xAAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAAu128), - "random" => (0x5C3A_7F91_D2B8_4E06_1A2B_3C4D_5E6F_7089u128, - 0xA3C5_806E_2D47_B1F9_E5D4_C3B2_A190_8F76u128), - ); -} - -// -// ct_ne — 이항 (not-equal 판별) -// - -fn ct_ne_security(c: &mut Criterion) { - security_bench_binary!(c, "security/ct_ne/u8", ct_ne, - "equal_zeros" => (0u8, 0u8), - "equal_ones" => (0xFFu8, 0xFFu8), - "hamming_1_diff" => (0u8, 1u8), - "hamming_max" => (0x55u8, 0xAAu8), - "random" => (0x5Cu8, 0xA3u8), - ); - security_bench_binary!(c, "security/ct_ne/u32", ct_ne, - "equal_zeros" => (0u32, 0u32), - "equal_ones" => (!0u32, !0u32), - "hamming_1_diff" => (0u32, 1u32), - "hamming_max" => (0x5555_5555u32, 0xAAAA_AAAAu32), - "random" => (0x5C3A_7F91u32, 0xA3C5_806Eu32), - ); - security_bench_binary!(c, "security/ct_ne/u64", ct_ne, - "equal_zeros" => (0u64, 0u64), - "equal_ones" => (!0u64, !0u64), - "hamming_1_diff" => (0u64, 1u64), - "hamming_max" => (0x5555_5555_5555_5555u64, 0xAAAA_AAAA_AAAA_AAAAu64), - "random" => (0x5C3A_7F91_D2B8_4E06u64, 0xA3C5_806E_2D47_B1F9u64), - ); - security_bench_binary!(c, "security/ct_ne/u128", ct_ne, - "equal_zeros" => (0u128, 0u128), - "equal_ones" => (!0u128, !0u128), - "hamming_1_diff" => (0u128, 1u128), - "hamming_max" => (0x5555_5555_5555_5555_5555_5555_5555_5555u128, - 0xAAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAAu128), - "random" => (0x5C3A_7F91_D2B8_4E06_1A2B_3C4D_5E6F_7089u128, - 0xA3C5_806E_2D47_B1F9_E5D4_C3B2_A190_8F76u128), - ); -} - -// -// ct_is_zero — 단항 -// - -fn ct_is_zero_security(c: &mut Criterion) { - security_bench_unary!(c, "security/ct_is_zero/u8", ct_is_zero, - "all_zeros" => 0u8, - "all_ones" => 0xFFu8, - "hamming_1" => 1u8, - "hamming_max" => 0xAAu8, - "random" => 0x5Cu8, - ); - security_bench_unary!(c, "security/ct_is_zero/u32", ct_is_zero, - "all_zeros" => 0u32, - "all_ones" => !0u32, - "hamming_1" => 1u32, - "hamming_max" => 0xAAAA_AAAAu32, - "random" => 0x5C3A_7F91u32, - ); - security_bench_unary!(c, "security/ct_is_zero/u64", ct_is_zero, - "all_zeros" => 0u64, - "all_ones" => !0u64, - "hamming_1" => 1u64, - "hamming_max" => 0xAAAA_AAAA_AAAA_AAAAu64, - "random" => 0x5C3A_7F91_D2B8_4E06u64, - ); - security_bench_unary!(c, "security/ct_is_zero/u128", ct_is_zero, - "all_zeros" => 0u128, - "all_ones" => !0u128, - "hamming_1" => 1u128, - "hamming_max" => 0xAAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAAu128, - "random" => 0x5C3A_7F91_D2B8_4E06_1A2B_3C4D_5E6F_7089u128, - ); -} - -// -// ct_is_nonzero — 단항 -// - -fn ct_is_nonzero_security(c: &mut Criterion) { - security_bench_unary!(c, "security/ct_is_nonzero/u8", ct_is_nonzero, - "all_zeros" => 0u8, - "all_ones" => 0xFFu8, - "hamming_1" => 1u8, - "hamming_max" => 0xAAu8, - "random" => 0x5Cu8, - ); - security_bench_unary!(c, "security/ct_is_nonzero/u32", ct_is_nonzero, - "all_zeros" => 0u32, - "all_ones" => !0u32, - "hamming_1" => 1u32, - "hamming_max" => 0xAAAA_AAAAu32, - "random" => 0x5C3A_7F91u32, - ); - security_bench_unary!(c, "security/ct_is_nonzero/u64", ct_is_nonzero, - "all_zeros" => 0u64, - "all_ones" => !0u64, - "hamming_1" => 1u64, - "hamming_max" => 0xAAAA_AAAA_AAAA_AAAAu64, - "random" => 0x5C3A_7F91_D2B8_4E06u64, - ); - security_bench_unary!(c, "security/ct_is_nonzero/u128", ct_is_nonzero, - "all_zeros" => 0u128, - "all_ones" => !0u128, - "hamming_1" => 1u128, - "hamming_max" => 0xAAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAAu128, - "random" => 0x5C3A_7F91_D2B8_4E06_1A2B_3C4D_5E6F_7089u128, - ); -} - -// -// ct_is_negative — 단항 (MSB 검사) -// - -fn ct_is_negative_security(c: &mut Criterion) { - security_bench_unary!(c, "security/ct_is_negative/u8", ct_is_negative, - "all_zeros" => 0u8, - "all_ones" => 0xFFu8, - "hamming_1" => 1u8, - "hamming_max" => 0xAAu8, - "random" => 0x5Cu8, - ); - security_bench_unary!(c, "security/ct_is_negative/u32", ct_is_negative, - "all_zeros" => 0u32, - "all_ones" => !0u32, - "hamming_1" => 1u32, - "hamming_max" => 0xAAAA_AAAAu32, - "random" => 0x5C3A_7F91u32, - ); - security_bench_unary!(c, "security/ct_is_negative/u64", ct_is_negative, - "all_zeros" => 0u64, - "all_ones" => !0u64, - "hamming_1" => 1u64, - "hamming_max" => 0xAAAA_AAAA_AAAA_AAAAu64, - "random" => 0x5C3A_7F91_D2B8_4E06u64, - ); - security_bench_unary!(c, "security/ct_is_negative/u128", ct_is_negative, - "all_zeros" => 0u128, - "all_ones" => !0u128, - "hamming_1" => 1u128, - "hamming_max" => 0xAAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAA_AAAAu128, - "random" => 0x5C3A_7F91_D2B8_4E06_1A2B_3C4D_5E6F_7089u128, - ); -} - -// -// ct_select — 삼항 (mask 기반 조건 선택) -// - -fn ct_select_security(c: &mut Criterion) { - // u8 (Tier2) - security_bench_select!(c, "security/ct_select/u8", - "mask_true_zeros" => (0u8, 0u8, !0u8), - "mask_false_zeros" => (0u8, 0u8, 0u8), - "mask_true_ones" => (0xFFu8, 0x00u8, !0u8), - "mask_false_ones" => (0x00u8, 0xFFu8, 0u8), - "random_true" => (0x5Cu8, 0xA3u8, !0u8), - ); - // u32 (Tier1) - security_bench_select!(c, "security/ct_select/u32", - "mask_true_zeros" => (0u32, 0u32, !0u32), - "mask_false_zeros" => (0u32, 0u32, 0u32), - "mask_true_ones" => (!0u32, 0u32, !0u32), - "mask_false_ones" => (0u32, !0u32, 0u32), - "random_true" => (0x5C3A_7F91u32, 0xA3C5_806Eu32, !0u32), - ); - // u64 (Tier1) - security_bench_select!(c, "security/ct_select/u64", - "mask_true_zeros" => (0u64, 0u64, !0u64), - "mask_false_zeros" => (0u64, 0u64, 0u64), - "mask_true_ones" => (!0u64, 0u64, !0u64), - "mask_false_ones" => (0u64, !0u64, 0u64), - "random_true" => (0x5C3A_7F91_D2B8_4E06u64, 0xA3C5_806E_2D47_B1F9u64, !0u64), - ); - // u128 (Tier2) - security_bench_select!(c, "security/ct_select/u128", - "mask_true_zeros" => (0u128, 0u128, !0u128), - "mask_false_zeros" => (0u128, 0u128, 0u128), - "mask_true_ones" => (!0u128, 0u128, !0u128), - "mask_false_ones" => (0u128, !0u128, 0u128), - "random_true" => (0x5C3A_7F91_D2B8_4E06_1A2B_3C4D_5E6F_7089u128, - 0xA3C5_806E_2D47_B1F9_E5D4_C3B2_A190_8F76u128, !0u128), - ); -} - -// -// Criterion 설정 -// - -criterion_group!( - benches, - ct_eq_security, - ct_ne_security, - ct_is_zero_security, - ct_is_nonzero_security, - ct_is_negative_security, - ct_select_security, -); -criterion_main!(benches); diff --git a/crypto/constant-time/benches/ct_ops_throughput.rs b/crypto/constant-time/benches/ct_ops_throughput.rs deleted file mode 100644 index a6a8cae..0000000 --- a/crypto/constant-time/benches/ct_ops_throughput.rs +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2025-2026 Quant - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the “Software”), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; - -use entlib_native_constant_time::constant_time::ConstantTimeOps; - -const BATCH: u64 = 4096; - -// -// ct_eq 처리량 — u8 ~ usize 전체 타입 -// - -fn ct_eq_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/ct_eq"); - group.throughput(Throughput::Elements(BATCH)); - - macro_rules! bench_eq { - ($($ty:ty),+) => {$( - group.bench_function(stringify!($ty), |b| { - let pairs: Vec<($ty, $ty)> = (0..BATCH as usize) - .map(|i| (i as $ty, i.wrapping_add(1) as $ty)) - .collect(); - b.iter(|| { - for &(a, other) in &pairs { - std::hint::black_box(a.ct_eq(other)); - } - }) - }); - )+}; - } - - bench_eq!(u8, u16, u32, u64, u128, usize); - group.finish(); -} - -// -// ct_is_zero 처리량 — u32, u64 -// - -fn ct_is_zero_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/ct_is_zero"); - group.throughput(Throughput::Elements(BATCH)); - - group.bench_function("u32", |b| { - let vals: Vec = (0..BATCH as u32).collect(); - b.iter(|| { - for &v in &vals { - std::hint::black_box(v.ct_is_zero()); - } - }) - }); - - group.bench_function("u64", |b| { - let vals: Vec = (0..BATCH).collect(); - b.iter(|| { - for &v in &vals { - std::hint::black_box(v.ct_is_zero()); - } - }) - }); - - group.finish(); -} - -// -// ct_is_negative 처리량 — u32, u64 -// - -fn ct_is_negative_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/ct_is_negative"); - group.throughput(Throughput::Elements(BATCH)); - - group.bench_function("u32", |b| { - let vals: Vec = (0..BATCH as u32).collect(); - b.iter(|| { - for &v in &vals { - std::hint::black_box(v.ct_is_negative()); - } - }) - }); - - group.bench_function("u64", |b| { - let vals: Vec = (0..BATCH).collect(); - b.iter(|| { - for &v in &vals { - std::hint::black_box(v.ct_is_negative()); - } - }) - }); - - group.finish(); -} - -// -// ct_select 처리량 — u64 -// - -fn ct_select_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/ct_select"); - group.throughput(Throughput::Elements(BATCH)); - - group.bench_function("u64", |b| { - let triples: Vec<(u64, u64, u64)> = (0..BATCH) - .map(|i| (i, i.wrapping_mul(7), if i % 2 == 0 { !0u64 } else { 0u64 })) - .collect(); - b.iter(|| { - for &(a, other, mask) in &triples { - std::hint::black_box(a.ct_select(other, mask)); - } - }) - }); - - group.finish(); -} - -// -// 티어 비교 — ct_eq: u32(Tier1) vs u8(Tier2) vs u128(Tier2) -// - -fn ct_eq_tier_comparison(c: &mut Criterion) { - let mut group = c.benchmark_group("tier_comparison/ct_eq"); - group.throughput(Throughput::Elements(BATCH)); - - group.bench_function("u32_tier1", |b| { - let pairs: Vec<(u32, u32)> = (0..BATCH as usize) - .map(|i| (i as u32, i.wrapping_add(1) as u32)) - .collect(); - b.iter(|| { - for &(a, other) in &pairs { - std::hint::black_box(a.ct_eq(other)); - } - }) - }); - - group.bench_function("u8_tier2", |b| { - let pairs: Vec<(u8, u8)> = (0..BATCH as usize) - .map(|i| (i as u8, i.wrapping_add(1) as u8)) - .collect(); - b.iter(|| { - for &(a, other) in &pairs { - std::hint::black_box(a.ct_eq(other)); - } - }) - }); - - group.bench_function("u128_tier2", |b| { - let pairs: Vec<(u128, u128)> = (0..BATCH as usize) - .map(|i| (i as u128, i.wrapping_add(1) as u128)) - .collect(); - b.iter(|| { - for &(a, other) in &pairs { - std::hint::black_box(a.ct_eq(other)); - } - }) - }); - - group.finish(); -} - -// -// Criterion 설정 -// - -criterion_group!( - benches, - ct_eq_throughput, - ct_is_zero_throughput, - ct_is_negative_throughput, - ct_select_throughput, - ct_eq_tier_comparison, -); -criterion_main!(benches); diff --git a/crypto/constant-time/constant_time.md b/crypto/constant-time/constant_time.md deleted file mode 100644 index c6f2677..0000000 --- a/crypto/constant-time/constant_time.md +++ /dev/null @@ -1,91 +0,0 @@ -# 상수-시간(constant-time) 연산 모듈 - -이 문서는 [상수-시간 연산 모듈](src/constant_time.rs)에 대해 설명합니다. - -## 개요 - -`constant_time.rs` 모듈은 암호학적 구현에서 필수적인 **상수 시간(Constant-Time)** 연산을 지원하기 위해 설계되었습니다. 이 모듈은 민감한 데이터(예: 비밀키, 난수 등)를 처리할 때, -데이터의 값에 따라 연산 시간이 달라지는 **타이밍 공격(timing attack)** 및 부채널 공격(side-channel attack)을 방지하는 것을 목적으로 합니다. - -이 모듈의 핵심 원칙은 CPU의 **분기 예측(branch prediction)** 실패나 조건부 점프에 의한 클럭 사이클 차이를 제거하기 위해, 제어 흐름(control flow)을 분기하지 않고 순수 비트 -연산(bitwise operation)만으로 논리를 구현하는 것입니다. - -## 주요 설계 및 보안 고려사항 - -### 최적화 방지 - -LLVM 등의 현대 컴파일러는 비트 연산으로 구현된 로직을 분석하여 더 빠른 조건부 분기문으로 역최적화(reverse-optimization)할 가능성이 있습니다. 이를 방지하기 위해 Rust 표준 라이브러리의 -`core::hint::black_box`를 사용합니다. - -* **목적:** 컴파일러가 입력값과 결과값을 미리 계산하거나, 로직을 분기문으로 변경하는 것을 차단합니다. -* **한계:** `black_box`는 최적화 힌트일 뿐이며, 특정 아키텍처에서는 여전히 안전하지 않은 어셈블리가 생성될 수 있으므로 최종 바이너리에 대한 검증이 권장됩니다. - -### 반환값 정책 - -이 모듈의 비교 연산은 일반적인 `bool`(`true`/`false`)을 반환하지 않고, 비트 마스크 형태의 `Self` 타입을 반환합니다. - -* **참(True):** 모든 비트가 1 (`!0`, 예: `0xFFFFFFFF` for `u32`) -* **거짓(False):** 모든 비트가 0 (`0`, 예: `0x00000000` for `u32`) - 이러한 마스크 값은 `ct_select` 함수에서 비트 연산을 통해 조건부 값을 선택하는 데 직접 사용됩니다. - -## API 상세 명세 - -### `ConstantTimeOps` 트레이트 - -모든 정수형 기본 타입(`u8` ~ `u128`, `i8` ~ `i128`, `usize`/`isize`)에 대해 구현되어 있습니다. - -#### `fn ct_is_zero(self) -> Self` - -값이 `0`인지 판별합니다. - -* **논리:** `ct_is_nonzero`의 결과를 비트 반전(NOT)하여 반환합니다. -* **반환:** 입력이 `0`이면 `!0`(All 1s), 아니면 `0`. - -#### `fn ct_is_nonzero(self) -> Self` - -값이 `0`이 아닌지 판별합니다. - -* **알고리즘:** - $$\text{result} = ((x \lor -x) \gg (\text{BITS} - 1)) \ \& \ 1$$ - 2의 보수 표현법에서 $0$이 아닌 모든 정수 $x$에 대해, $x$ 혹은 $-x$ 중 하나는 반드시 최상위 비트(MSB)가 $1$이 된다는 성질을 이용합니다. -* **반환:** 입력이 `0`이 아니면 `!0`(All 1s), `0`이면 `0`. - -#### `fn ct_is_negative(self) -> Self` - -값의 최상위 비트(MSB, Sign Bit)가 설정되어 있는지 확인합니다. - -* **Unsigned 타입:** MSB가 1인 큰 수인지 판별합니다. -* **Signed 타입:** 음수인지 판별합니다. -* **구현:** 산술 시프트(arithmetic shift)의 부호 확장 문제를 피하기 위해 MSB를 추출한 후 `1`과 AND 연산하여 정규화하고, 이를 `wrapping_neg()`를 통해 마스크로 확장합니다. - -#### `fn ct_eq(self, other: Self) -> Self` - -두 값이 비트 단위로 동일한지 비교합니다. - -* **알고리즘:** - $$\text{diff} = a \oplus b$$ - $$\text{result} = \text{NOT}(\text{is\_nonzero}(\text{diff}))$$ - XOR 연산은 두 비트가 다를 때만 `1`을 반환하므로, $a \oplus b = 0$ 이면 두 값은 같습니다. - -#### `fn ct_select(self, other: Self, mask: Self) -> Self` - -마스크 값에 따라 두 입력값 중 하나를 선택합니다. (Conditional Move와 유사) - -* **수식:** - $$\text{result} = b \oplus (\text{mask} \& (a \oplus b))$$ -* **동작 원리:** - * **Case 1:** $\text{mask} = 11\dots1$ (참)인 경우: - $$b \oplus (11\dots1 \& (a \oplus b)) = b \oplus (a \oplus b) = (b \oplus b) \oplus a = 0 \oplus a = a$$ - $\rightarrow$ `self` 반환 - * **Case 2:** $\text{mask} = 00\dots0$ (거짓)인 경우: - $$b \oplus (00\dots0 \& (a \oplus b)) = b \oplus 0 = b$$ - $\rightarrow$ `other` 반환 -* **주의사항:** `mask`는 반드시 `ConstantTimeOps`의 비교 연산 결과(`0` 또는 `!0`)여야 합니다. 임의의 정수(예: `1`)를 넣을 경우 비트가 섞인 예측 불가능한 값이 반환됩니다. - -## 구현 세부 사항 - -이 모듈은 `macro_rules!`인 `impl_ct_ops!`를 사용하여 코드 중복을 최소화하고 모든 정수 타입에 일관된 로직을 적용합니다. - -* **아키텍처 독립성:** `core::mem::size_of`를 사용하여 컴파일 타임에 타입의 비트 수(`BITS`)를 계산하므로, 32비트/64비트 아키텍처에 관계없이 정확한 MSB 위치를 참조합니다. -* **Wrapping Arithmetic:** Rust의 디버그 모드에서 오버플로우 체크로 인한 패닉을 방지하기 위해, 모든 연산에 `wrapping_neg`, `wrapping_add` 등의 래핑 연산을 - 명시적으로 사용합니다. \ No newline at end of file diff --git a/crypto/constant-time/constant_time_asm.md b/crypto/constant-time/constant_time_asm.md deleted file mode 100644 index 634a347..0000000 --- a/crypto/constant-time/constant_time_asm.md +++ /dev/null @@ -1,51 +0,0 @@ -# 아키텍처별 인라인 어셈블리 기반 상수-시간 프리미티브(inline asm-based constant-time primitive) 모듈 - -이 문서는 `entlib-native`의 하위 모듈인 [아키텍처별 인라인 어셈블리 기반 상수-시간 프리미티브 모듈](src/constant_time_asm.rs)의 기술적 역할과 구현 세부 사항에 대해 설명합니다. - -## 개요 - -`constant_time_asm.rs` 모듈은 컴파일러의 예상치 못한 최적화로 인해 상수-시간(constant-time) 특성이 훼손되는 것을 원천적으로 차단하기 위해 설계된 저수준 프리미티브(low-level primitive) 모듈입니다. 상위 모듈인 `constant_time.rs`에서 요구하는 연산들을 대상 아키텍처에 최적화된 인라인 어셈블리(inline assembly) 명령어로 직접 구현하여, 타이밍 공격(timing attack) 및 부채널 공격(side-channel attack)에 대한 물리적 수준의 방어력을 제공합니다. - -## 아키텍처 지원 및 구현 티어 - -이 모듈은 대상 시스템의 명령어 집합 아키텍처(instruction set architecture)와 데이터 타입의 크기에 따라 세 가지 티어로 나뉘어 동작합니다. - -* **티어 1 (full asm):** `x86_64` 및 `aarch64` 환경의 `u32`, `u64` 타입에 적용됩니다. 연산의 전 과정을 순수 인라인 어셈블리로 처리하여, 상위 언어의 개입 없이 최고 수준의 보안성과 성능을 보장합니다. -* **티어 2 (barrier):** `x86_64` 및 `aarch64` 환경의 `u8`, `u16`, `u128` 타입에 적용됩니다. 어셈블리를 통한 최적화 배리어(optimization barrier) 설정으로 값을 보호한 후, 안전성이 검증된 Rust 비트 로직을 수행합니다. -* **티어 3 (fallback):** 그 외의 지원되지 않는 아키텍처에 적용됩니다. 표준 라이브러리의 컴파일러 힌트인 `black_box` 함수를 활용하여 최적화를 억제하고 순수 비트 연산으로 폴백(fallback)합니다. - -## 주요 설계 및 보안 고려사항 - -### 어셈블리 수준의 통제 - -현대의 진보된 컴파일러는 중간 표현 단계에서 복잡한 비트 연산을 분석하여 더 빠른 실행 속도를 위해 **조건부 점프 명령어**로 역최적화(reverse-optimization)할 가능성이 있습니다. 이 모듈은 분기가 없는 어셈블리 명령어 시퀀스를 하드코딩하여 컴파일러의 분기 예측(branch prediction) 최적화 개입을 근본적으로 차단합니다. - -### 플래그 및 레지스터 보존 - -인라인 어셈블리를 실행할 때 `preserves_flags` 옵션을 적절히 부여하고, 상태 플래그가 영향을 받는 산술 명령어들을 세밀하게 통제하여 메모리 및 스택 참조를 배제(`nomem`, `nostack`)함으로써 부수 효과를 최소화합니다. - -## API 상세 명세 - -### `CtPrimitive` 트레이트 - -조건 분기를 배제하고 상수 시간 내에 마스크를 생성하거나 값을 선택하는 저수준 연산을 정의합니다. 반환되는 마스크는 모든 비트가 1인 상태(참) 혹은 모든 비트가 0인 상태(거짓)로 구성됩니다. - -* `fn ct_fence(self) -> Self` - 값에 대한 컴파일러의 추론 및 레지스터 최적화를 강제로 차단하는 펜스(fence) 역할을 수행합니다. 빈 어셈블리 블록을 통과시켜 컴파일러가 해당 변수의 상태를 알 수 없도록 만듭니다. -* `fn ct_nonzero(self) -> Self` - 값이 0이 아닐 경우 참 마스크를, 0일 경우 거짓 마스크를 반환합니다. `x86_64`에서는 캐리 플래그(carry flag)를 발생시키는 `neg`와 이를 차감하는 `sbb` 명령어를, `aarch64`에서는 상태 레지스터를 갱신하는 `subs`와 `sbc` 명령어를 활용하여 분기 없이 마스크를 생성합니다. -* `fn ct_zero(self) -> Self` - `ct_nonzero`의 결과에 비트 반전(not) 명령어를 추가하여 0일 때 참 마스크를 반환하도록 구현합니다. -* `fn ct_negative(self) -> Self` - 값의 최상위 비트(msb)를 추출하여 전체 비트로 확장합니다. 산술적 우측 시프트 동작과의 혼동을 피하기 위해 논리적 우측 시프트(lsr 또는 shr) 명령어를 사용한 후, 단항 음수화(neg)를 통해 마스크를 완성합니다. -* `fn ct_equal(self, other: Self) -> Self` - 두 값을 배타적 논리합(xor)한 뒤, 두 값의 차이가 0인지 검사하는 어셈블리 시퀀스를 전개하여 동일 여부에 대한 마스크를 반환합니다. -* `fn ct_not_equal(self, other: Self) -> Self` - `ct_equal` 명령어 시퀀스에서 마지막 반전(not 또는 mvn) 연산을 생략하여 서로 다를 때 참이 되는 마스크를 생성합니다. -* `fn ct_mux(self, other: Self, mask: Self) -> Self` - 주어진 마스크 값에 따라 두 입력값 중 하나를 선택하는 다중화기(multiplexer) 역할을 합니다. 두 값에 대한 배타적 논리합 연산 후 마스크를 통한 논리곱(and)을 수행하고, 다시 배타적 논리합을 취하여 상태 분기 없이 단일 값을 계산해냅니다. - -## 구현 세부 사항 - -* **타입 위임(type delegation):** 매크로(`impl_ct_delegate!`)를 활용하여 `i8`부터 `i128`까지의 부호 있는(signed) 타입과 `usize`, `isize` 등 포인터 폭(pointer width)에 의존적인 타입들을 동일한 비트 폭을 갖는 부호 없는(unsigned) 기본 타입으로 캐스팅하여 로직을 재사용합니다. -* **하위 워드 처리 한계:** `aarch64` 등의 아키텍처에서는 8비트 및 16비트 레지스터에 대한 직접적인 논리 연산 제어가 까다로우므로, `u8` 및 `u16` 타입은 `u32` 레지스터 규격으로 승격시켜 배리어를 통과한 후 Rust 상의 비트 연산으로 마무리하도록 설계되었습니다. \ No newline at end of file diff --git a/crypto/constant-time/src/constant_time.rs b/crypto/constant-time/src/constant_time.rs deleted file mode 100644 index aaaa71a..0000000 --- a/crypto/constant-time/src/constant_time.rs +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2025-2026 Quant - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the “Software”), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -//! 상수-시간(Constant-Time) 연산을 위한 통합 트레이트 및 구현체 -//! -//! 이 모듈은 민감 데이터를 다룰 때 타이밍 공격(timing attack)을 방지하기 위해 -//! CPU 분기(branch) 없이 비트 연산만으로 로직을 수행하는 메소드들을 제공합니다. -//! -//! # Author -//! Q. T. Felix -//! -//! # Security Warning -//! 이 코드는 `entlib-native`의 일부로, 컴파일러 최적화 레벨이나 타겟 아키텍처에 따라 -//! 안전성이 달라질 수 있습니다. `x86_64` 및 `aarch64`에서는 인라인 어셈블리를 사용하여 -//! 컴파일러 최적화를 원천 차단하며, 기타 아키텍처에서는 `internal::hint::black_box`로 폴백합니다. -//! 최종 바이너리에 대한 어셈블리 검증이 권장됩니다. -//! -//! 이 모듈은 단순한 개념 증명(PoC) 기능을 가지지 않습니다. -//! 군사적 보안에 다다르기 위해 완벽한 상수 시간 기능을 구현해야 한다 판단했고, -//! 이를 위해 다음의 기능을 구현 및 수행했습니다. -//! - (O) 인라인 어셈블리 (`constant_time_asm` 모듈) -//! - (O) 어셈블리 검증 파이프라인 -//! - (O) 검증 라이브러리 교차 검증 (`subtle`, `proptest` 크레이트 검증) - -pub trait ConstantTimeOps: Copy + Sized { - /// 값이 0이면 참(All 1s), 아니면 거짓(0)을 반환합니다. - fn ct_is_zero(self) -> Self; - - /// 값이 0이 아니면 참(All 1s), 0이면 거짓(0)을 반환합니다. - fn ct_is_nonzero(self) -> Self; - - /// 값이 음수(MSB가 1)이면 참(All 1s), 아니면 거짓(0)을 반환합니다. - /// Unsigned 타입의 경우 MSB가 1인지(큰 수인지)를 판별합니다. - fn ct_is_negative(self) -> Self; - - /// 두 값이 같으면 참(All 1) 마스크를, 다르면 거짓(0) 마스크를 반환합니다. - fn ct_eq(self, other: Self) -> Self; - - /// 두 값이 다르면 참(All 1) 마스크를, 같으면 거짓(0) 마스크를 반환합니다. - fn ct_ne(self, other: Self) -> Self; - - /// 마스크에 따라 값을 선택합니다. - /// - /// # Logic - /// `mask`가 참(`!0`)이면 `self`, 거짓(`0`)이면 `other`를 반환합니다. - /// - /// # Safety - /// `mask`는 반드시 `ct_eq` 등의 결과로 생성된 유효한 마스크 값(`0` 또는 `!0`)이어야 합니다. - /// 잘못된 마스크 값(예: `1`, `2`)이 입력될 경우 예측 불가능한 결과가 혼합되어 반환됩니다. - fn ct_select(self, other: Self, mask: Self) -> Self; -} - -macro_rules! impl_ct_ops { - ($($t:ty),+) => { - $( - impl ConstantTimeOps for $t { - #[inline(always)] - fn ct_is_negative(self) -> Self { - crate::constant_time_asm::CtPrimitive::ct_negative(self) - } - - #[inline(always)] - fn ct_is_nonzero(self) -> Self { - crate::constant_time_asm::CtPrimitive::ct_nonzero(self) - } - - #[inline(always)] - fn ct_is_zero(self) -> Self { - crate::constant_time_asm::CtPrimitive::ct_zero(self) - } - - #[inline(always)] - fn ct_eq(self, other: Self) -> Self { - crate::constant_time_asm::CtPrimitive::ct_equal(self, other) - } - - #[inline(always)] - fn ct_ne(self, other: Self) -> Self { - crate::constant_time_asm::CtPrimitive::ct_not_equal(self, other) - } - - #[inline(always)] - fn ct_select(self, other: Self, mask: Self) -> Self { - crate::constant_time_asm::CtPrimitive::ct_mux(self, other, mask) - } - } - )+ - }; -} - -// 모든 정수 타입에 대해 구현 적용 (비트 수 자동 계산) -impl_ct_ops!( - u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize -); diff --git a/crypto/constant-time/src/constant_time_asm.rs b/crypto/constant-time/src/constant_time_asm.rs deleted file mode 100644 index f1fd7d9..0000000 --- a/crypto/constant-time/src/constant_time_asm.rs +++ /dev/null @@ -1,592 +0,0 @@ -/* - * Copyright (c) 2025-2026 Quant - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the “Software”), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -//! 아키텍처별 인라인 어셈블리 기반 상수-시간 프리미티브 -//! -//! # Author -//! Q. T. Felix -//! -//! | 티어 | 아키텍처 | 타입 | 방식 | -//! |------|----------|------|------| -//! | 1 (Full ASM) | x86_64, aarch64 | u32, u64 | 전체 인라인 어셈블리 | -//! | 2 (Barrier) | x86_64, aarch64 | u8, u16, u128 | ASM 배리어 + Rust 비트 로직 | -//! | 3 (Fallback) | 기타 | 전체 | `black_box` 폴백 | - -/// 아키텍처별 인라인 어셈블리 기반 상수-시간 프리미티브 트레이트 -/// -/// 이 트레이트는 조건 분기(branch)를 사용하지 않고 비트 연산과 인라인 어셈블리를 통해 -/// 상수 시간(Constant-Time)에 동작하는 저수준 연산들을 정의합니다. -/// 암호학적 연산이나 사이드 채널 공격(side-channel attack)에 민감한 로직을 구현할 때 사용됩니다. -/// -/// # Usage -/// ```rust -/// use entlib_native_constant_time::constant_time_asm::CtPrimitive; -/// -/// let a = 10u32; -/// let b = 20u32; -/// -/// // 두 값이 같은지 비교 (같으면 !0, 다르면 0 반환) -/// let mask = a.ct_equal(b); -/// assert_eq!(mask, 0); -/// -/// // 마스크를 이용한 값 선택 (Constant-Time MUX) -/// // mask가 0이므로 b가 선택됨 -/// let selected = a.ct_mux(b, mask); -/// assert_eq!(selected, b); -/// ``` -/// -/// # Safety -/// 이 트레이트의 구현체들은 컴파일러 최적화를 방지하기 위해 `internal::hint::black_box` 또는 -/// 인라인 어셈블리(`asm!`)를 사용합니다. 하지만 특정 아키텍처나 컴파일러 버전에서는 -/// 의도치 않은 최적화가 발생할 수 있으므로, 중요한 보안 로직에 사용할 경우 -/// 생성된 어셈블리 코드를 검증하는 것이 좋습니다. -pub trait CtPrimitive: Copy + Sized { - /// 최적화 배리어: 컴파일러가 값에 대한 추론을 불가능하게 만듦 - fn ct_fence(self) -> Self; - /// != 0 → all 1s, == 0 → 0 - fn ct_nonzero(self) -> Self; - /// == 0 → all 1s, != 0 → 0 - fn ct_zero(self) -> Self; - /// MSB == 1 → all 1s, MSB == 0 → 0 - fn ct_negative(self) -> Self; - /// self == other → all 1s, self != other → 0 - fn ct_equal(self, other: Self) -> Self; - /// self != other → all 1s, self == other → 0 - fn ct_not_equal(self, other: Self) -> Self; - /// mask == all 1s → self, mask == 0 → other - fn ct_mux(self, other: Self, mask: Self) -> Self; -} - -// -// 공통 매크로 - start -// - -/// ct_fence 기반 Rust 비트 로직으로 나머지 연산을 구현합니다. -/// ct_fence는 호출 측에서 별도 구현해야 합니다. -macro_rules! impl_ct_rust_ops { - ($t:ty) => { - #[inline(always)] - fn ct_nonzero(self) -> Self { - const BITS: u32 = (core::mem::size_of::<$t>() * 8) as u32; - let val = self.ct_fence(); - let or_neg = val | val.wrapping_neg(); - let msb = or_neg >> (BITS - 1); - (msb & 1).wrapping_neg() - } - - #[inline(always)] - fn ct_zero(self) -> Self { - !self.ct_nonzero() - } - - #[inline(always)] - fn ct_negative(self) -> Self { - const BITS: u32 = (core::mem::size_of::<$t>() * 8) as u32; - let val = self.ct_fence(); - let msb = val >> (BITS - 1); - (msb & 1).wrapping_neg() - } - - #[inline(always)] - fn ct_equal(self, other: Self) -> Self { - const BITS: u32 = (core::mem::size_of::<$t>() * 8) as u32; - let a = self.ct_fence(); - let b = other.ct_fence(); - let diff = a ^ b; - let diff_or_neg = diff | diff.wrapping_neg(); - let non_zero_bit = (diff_or_neg >> (BITS - 1)) & 1; - (non_zero_bit ^ 1).wrapping_neg() - } - - #[inline(always)] - fn ct_not_equal(self, other: Self) -> Self { - !self.ct_equal(other) - } - - #[inline(always)] - fn ct_mux(self, other: Self, mask: Self) -> Self { - let a = self.ct_fence(); - let b = other.ct_fence(); - let m = mask.ct_fence(); - b ^ (m & (a ^ b)) - } - }; -} - -/// 다른 프리미티브 타입으로 위임합니다 (signed → unsigned, usize → u64/u32 등). -macro_rules! impl_ct_delegate { - ($from:ty, $to:ty) => { - impl CtPrimitive for $from { - #[inline(always)] - fn ct_fence(self) -> Self { - (self as $to).ct_fence() as Self - } - #[inline(always)] - fn ct_nonzero(self) -> Self { - (self as $to).ct_nonzero() as Self - } - #[inline(always)] - fn ct_zero(self) -> Self { - (self as $to).ct_zero() as Self - } - #[inline(always)] - fn ct_negative(self) -> Self { - (self as $to).ct_negative() as Self - } - #[inline(always)] - fn ct_equal(self, other: Self) -> Self { - (self as $to).ct_equal(other as $to) as Self - } - #[inline(always)] - fn ct_not_equal(self, other: Self) -> Self { - (self as $to).ct_not_equal(other as $to) as Self - } - #[inline(always)] - fn ct_mux(self, other: Self, mask: Self) -> Self { - (self as $to).ct_mux(other as $to, mask as $to) as Self - } - } - }; -} - -/// x86_64 전체 인라인 어셈블리 (u32, u64) -/// -/// | 연산 | 명령어 시퀀스 | 설명 | -/// |------|-------------|------| -/// | ct_fence | (빈 asm) | 값 배리어 | -/// | ct_nonzero | neg → sbb | CF 기반 마스크 생성 | -/// | ct_zero | neg → sbb → not | nonzero의 반전 | -/// | ct_negative | shr → neg | MSB 추출 후 확장 | -/// | ct_equal | xor → neg → sbb → not | 차이=0 검사 | -/// | ct_not_equal | xor → neg → sbb | 차이≠0 검사 | -/// | ct_mux | xor → and → xor | XOR-swap 선택 | -#[cfg(target_arch = "x86_64")] -macro_rules! impl_ct_full_asm_x86_64 { - ($t:ty, $mod:literal, $shift:literal) => { - impl CtPrimitive for $t { - #[inline(always)] - fn ct_fence(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - concat!("/* {0", $mod, "} */"), - inout(reg) val, - options(nomem, nostack, preserves_flags) - ); - } - val - } - - #[inline(always)] - fn ct_nonzero(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - concat!("neg {val", $mod, "}"), - concat!("sbb {val", $mod, "}, {val", $mod, "}"), - val = inout(reg) val, - options(nomem, nostack) - ); - } - val - } - - #[inline(always)] - fn ct_zero(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - concat!("neg {val", $mod, "}"), - concat!("sbb {val", $mod, "}, {val", $mod, "}"), - concat!("not {val", $mod, "}"), - val = inout(reg) val, - options(nomem, nostack) - ); - } - val - } - - #[inline(always)] - fn ct_negative(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - concat!("shr {val", $mod, "}, ", $shift), - concat!("neg {val", $mod, "}"), - val = inout(reg) val, - options(nomem, nostack) - ); - } - val - } - - #[inline(always)] - fn ct_equal(self, other: Self) -> Self { - let mut a = self; - unsafe { - core::arch::asm!( - concat!("xor {a", $mod, "}, {b", $mod, "}"), - concat!("neg {a", $mod, "}"), - concat!("sbb {a", $mod, "}, {a", $mod, "}"), - concat!("not {a", $mod, "}"), - a = inout(reg) a, - b = in(reg) other, - options(nomem, nostack) - ); - } - a - } - - #[inline(always)] - fn ct_not_equal(self, other: Self) -> Self { - let mut a = self; - unsafe { - core::arch::asm!( - concat!("xor {a", $mod, "}, {b", $mod, "}"), - concat!("neg {a", $mod, "}"), - concat!("sbb {a", $mod, "}, {a", $mod, "}"), - a = inout(reg) a, - b = in(reg) other, - options(nomem, nostack) - ); - } - a - } - - #[inline(always)] - fn ct_mux(self, other: Self, mask: Self) -> Self { - let mut a = self; - unsafe { - core::arch::asm!( - concat!("xor {a", $mod, "}, {b", $mod, "}"), - concat!("and {a", $mod, "}, {mask", $mod, "}"), - concat!("xor {a", $mod, "}, {b", $mod, "}"), - a = inout(reg) a, - b = in(reg) other, - mask = in(reg) mask, - options(nomem, nostack) - ); - } - a - } - } - }; -} - -#[cfg(target_arch = "x86_64")] -impl_ct_full_asm_x86_64!(u64, "", "63"); -#[cfg(target_arch = "x86_64")] -impl_ct_full_asm_x86_64!(u32, ":e", "31"); - -/// aarch64 전체 인라인 어셈블리 (u32, u64) -/// -/// | 연산 | 명령어 시퀀스 | 설명 | -/// |------|-------------|------| -/// | ct_fence | (빈 asm) | 값 배리어 | -/// | ct_nonzero | subs zr, zr, val → sbc val, zr, zr | 캐리 기반 마스크 | -/// | ct_zero | 위 + mvn | nonzero 반전 | -/// | ct_negative | lsr → neg | MSB 추출 후 확장 | -/// | ct_equal | eor → subs → sbc → mvn | | -/// | ct_not_equal | eor → subs → sbc | | -/// | ct_mux | eor → and → eor | | -#[cfg(target_arch = "aarch64")] -macro_rules! impl_ct_full_asm_aarch64 { - ($t:ty, $zr:literal, $shift:literal, $mod:literal) => { - impl CtPrimitive for $t { - #[inline(always)] - fn ct_fence(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - concat!("/* {0", $mod, "} */"), - inout(reg) val, - options(nomem, nostack, preserves_flags) - ); - } - val - } - - #[inline(always)] - fn ct_nonzero(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - concat!("subs ", $zr, ", ", $zr, ", {val", $mod, "}"), - concat!("sbc {val", $mod, "}, ", $zr, ", ", $zr), - val = inout(reg) val, - options(nomem, nostack) - ); - } - val - } - - #[inline(always)] - fn ct_zero(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - concat!("subs ", $zr, ", ", $zr, ", {val", $mod, "}"), - concat!("sbc {val", $mod, "}, ", $zr, ", ", $zr), - concat!("mvn {val", $mod, "}, {val", $mod, "}"), - val = inout(reg) val, - options(nomem, nostack) - ); - } - val - } - - #[inline(always)] - fn ct_negative(self) -> Self { - let mut val = self; - unsafe { - // NOTE: 이 로직에서 lsr 후 neg 을 통해 마스크를 확장하는 로직을 사용함 - // 이건 완벽하다고 생각하지만, C/C++ 등 타 언어의 산술 시프트 동작과 - // 혼동될 수 있다는걸 이제야 깨달음. C/C++ 랑 혼동하지 마세요!!!!! - // 쉽게 말해 논리적 우측 시프트(lsr) 사용됨 - core::arch::asm!( - concat!("lsr {val", $mod, "}, {val", $mod, "}, #", $shift), - concat!("neg {val", $mod, "}, {val", $mod, "}"), - val = inout(reg) val, - options(nomem, nostack, preserves_flags) - ); - } - val - } - - #[inline(always)] - fn ct_equal(self, other: Self) -> Self { - let mut a = self; - unsafe { - core::arch::asm!( - concat!("eor {a", $mod, "}, {a", $mod, "}, {b", $mod, "}"), - concat!("subs ", $zr, ", ", $zr, ", {a", $mod, "}"), - concat!("sbc {a", $mod, "}, ", $zr, ", ", $zr), - concat!("mvn {a", $mod, "}, {a", $mod, "}"), - a = inout(reg) a, - b = in(reg) other, - options(nomem, nostack) - ); - } - a - } - - #[inline(always)] - fn ct_not_equal(self, other: Self) -> Self { - let mut a = self; - unsafe { - core::arch::asm!( - concat!("eor {a", $mod, "}, {a", $mod, "}, {b", $mod, "}"), - concat!("subs ", $zr, ", ", $zr, ", {a", $mod, "}"), - concat!("sbc {a", $mod, "}, ", $zr, ", ", $zr), - a = inout(reg) a, - b = in(reg) other, - options(nomem, nostack) - ); - } - a - } - - #[inline(always)] - fn ct_mux(self, other: Self, mask: Self) -> Self { - let mut a = self; - unsafe { - core::arch::asm!( - concat!("eor {a", $mod, "}, {a", $mod, "}, {b", $mod, "}"), - concat!("and {a", $mod, "}, {a", $mod, "}, {mask", $mod, "}"), - concat!("eor {a", $mod, "}, {a", $mod, "}, {b", $mod, "}"), - a = inout(reg) a, - b = in(reg) other, - mask = in(reg) mask, - options(nomem, nostack, preserves_flags) - ); - } - a - } - } - }; -} - -#[cfg(target_arch = "aarch64")] -impl_ct_full_asm_aarch64!(u64, "xzr", "63", ":x"); -#[cfg(target_arch = "aarch64")] -impl_ct_full_asm_aarch64!(u32, "wzr", "31", ":w"); - -// -// ASM 배리어 + Rust 비트 로직 (u8, u16, u128) - start -// - -// --- u8: x86_64 (reg_byte) --- -#[cfg(target_arch = "x86_64")] -impl CtPrimitive for u8 { - #[inline(always)] - fn ct_fence(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - "/* {0} */", - inout(reg_byte) val, - options(nomem, nostack, preserves_flags) - ); - } - val - } - - impl_ct_rust_ops!(u8); -} - -// --- u8: aarch64 (u32 승격) --- -#[cfg(target_arch = "aarch64")] -impl CtPrimitive for u8 { - #[inline(always)] - fn ct_fence(self) -> Self { - let mut val = self as u32; - unsafe { - core::arch::asm!( - "/* {0:w} */", - inout(reg) val, - options(nomem, nostack, preserves_flags) - ); - } - val as u8 - } - - impl_ct_rust_ops!(u8); -} - -// --- u16: x86_64 (reg) --- -#[cfg(target_arch = "x86_64")] -impl CtPrimitive for u16 { - #[inline(always)] - fn ct_fence(self) -> Self { - let mut val = self; - unsafe { - core::arch::asm!( - "/* {0:x} */", - inout(reg) val, - options(nomem, nostack, preserves_flags) - ); - } - val - } - - impl_ct_rust_ops!(u16); -} - -// --- u16: aarch64 (u32 승격) --- -#[cfg(target_arch = "aarch64")] -impl CtPrimitive for u16 { - #[inline(always)] - fn ct_fence(self) -> Self { - let mut val = self as u32; - unsafe { - core::arch::asm!( - "/* {0:w} */", - inout(reg) val, - options(nomem, nostack, preserves_flags) - ); - } - val as u16 - } - - impl_ct_rust_ops!(u16); -} - -// --- u128: x86_64/aarch64 (u64 분할) --- -#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] -impl CtPrimitive for u128 { - #[inline(always)] - fn ct_fence(self) -> Self { - let mut lo = self as u64; - let mut hi = (self >> 64) as u64; - unsafe { - // NOTE: 구글링해보니 aarch64에서는 :x가 64비트를 명시하고, x86_64에서는 무시되거나 호환된다고 함 - // 아키텍처 공통으로 사용할 때 크기 경고를 방지하기 위해 각각 명시합니다 - #[cfg(target_arch = "aarch64")] - core::arch::asm!( - "/* {0:x} {1:x} */", - inout(reg) lo, - inout(reg) hi, - options(nomem, nostack, preserves_flags) - ); - - #[cfg(target_arch = "x86_64")] - core::arch::asm!( - "/* {0} {1} */", - inout(reg) lo, - inout(reg) hi, - options(nomem, nostack, preserves_flags) - ); - } - (hi as u128) << 64 | (lo as u128) - } - - impl_ct_rust_ops!(u128); -} - -// -// Tier 3: Fallback (x86_64/aarch64 이외 아키텍처) - start -// - -#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] -macro_rules! impl_ct_fallback { - ($($t:ty),+) => { - $( - impl CtPrimitive for $t { - #[inline(always)] - fn ct_fence(self) -> Self { - core::hint::black_box(self) - } - - impl_ct_rust_ops!($t); - } - )+ - }; -} - -#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] -impl_ct_fallback!(u8, u16, u32, u64, u128); - -// -// usize 위임 (포인터 폭에 따라 u64 또는 u32로) - start -// - -#[cfg(target_pointer_width = "64")] -impl_ct_delegate!(usize, u64); - -#[cfg(target_pointer_width = "32")] -impl_ct_delegate!(usize, u32); - -// -// Signed 타입 위임 (unsigned 대응 타입으로 캐스트, 비트 패턴 동일) - start -// - -impl_ct_delegate!(i8, u8); -impl_ct_delegate!(i16, u16); -impl_ct_delegate!(i32, u32); -impl_ct_delegate!(i64, u64); -impl_ct_delegate!(i128, u128); - -#[cfg(target_pointer_width = "64")] -impl_ct_delegate!(isize, u64); - -#[cfg(target_pointer_width = "32")] -impl_ct_delegate!(isize, u32); diff --git a/crypto/constant-time/src/lib.rs b/crypto/constant-time/src/lib.rs deleted file mode 100644 index be1cdae..0000000 --- a/crypto/constant-time/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod constant_time; - -#[cfg(feature = "ct-tests")] -#[doc(hidden)] -pub mod constant_time_asm; - -#[cfg(not(feature = "ct-tests"))] -pub(crate) mod constant_time_asm; diff --git a/crypto/constant-time/tests/constant_time_asm_test.rs b/crypto/constant-time/tests/constant_time_asm_test.rs deleted file mode 100644 index a013eda..0000000 --- a/crypto/constant-time/tests/constant_time_asm_test.rs +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2025-2026 Quant - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the “Software”), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#![cfg(feature = "ct-tests")] -use entlib_native_constant_time::constant_time_asm::CtPrimitive; - -macro_rules! test_ct_primitive { - ($test_name:ident, $t:ty, $zero:expr, $one:expr, $neg:expr) => { - #[test] - fn $test_name() { - let zero: $t = $zero; - let one: $t = $one; - let neg: $t = $neg; - let max: $t = !0; - - // ct_nonzero - assert_eq!(zero.ct_nonzero(), 0); - assert_eq!(one.ct_nonzero(), max); - assert_eq!(neg.ct_nonzero(), max); - - // ct_zero - assert_eq!(zero.ct_zero(), max); - assert_eq!(one.ct_zero(), 0); - - // ct_negative - // Unsigned types: check MSB - // Signed types: check sign bit - assert_eq!(zero.ct_negative(), 0); - assert_eq!(one.ct_negative(), 0); - assert_eq!(neg.ct_negative(), max); - - // ct_equal - assert_eq!(zero.ct_equal(zero), max); - assert_eq!(zero.ct_equal(one), 0); - assert_eq!(one.ct_equal(one), max); - - // ct_not_equal - assert_eq!(zero.ct_not_equal(zero), 0); - assert_eq!(zero.ct_not_equal(one), max); - - // ct_mux - // mask = max (all 1s) -> select first arg - assert_eq!(one.ct_mux(zero, max), one); - // mask = 0 -> select second arg - assert_eq!(one.ct_mux(zero, 0), zero); - } - }; -} - -// Unsigned types -test_ct_primitive!(test_u8, u8, 0, 1, 0x80); -test_ct_primitive!(test_u16, u16, 0, 1, 0x8000); -test_ct_primitive!(test_u32, u32, 0, 1, 0x80000000); -test_ct_primitive!(test_u64, u64, 0, 1, 0x8000000000000000); -test_ct_primitive!(test_u128, u128, 0, 1, 0x80000000000000000000000000000000); -test_ct_primitive!(test_usize, usize, 0, 1, 1 << (usize::BITS - 1)); - -// Signed types -test_ct_primitive!(test_i8, i8, 0, 1, -1); -test_ct_primitive!(test_i16, i16, 0, 1, -1); -test_ct_primitive!(test_i32, i32, 0, 1, -1); -test_ct_primitive!(test_i64, i64, 0, 1, -1); -test_ct_primitive!(test_i128, i128, 0, 1, -1); -test_ct_primitive!(test_isize, isize, 0, 1, -1); - -#[test] -fn test_ct_fence_optimization_barrier() { - // This test mainly checks if it compiles and runs without crashing. - // Verifying actual optimization barrier behavior is hard in unit tests. - let val = 42u32; - let fenced = val.ct_fence(); - assert_eq!(val, fenced); -} diff --git a/crypto/constant-time/tests/constant_time_test.rs b/crypto/constant-time/tests/constant_time_test.rs deleted file mode 100644 index c5a7b77..0000000 --- a/crypto/constant-time/tests/constant_time_test.rs +++ /dev/null @@ -1,78 +0,0 @@ -use entlib_native_constant_time::constant_time::ConstantTimeOps; - -#[test] -fn test_ct_eq_u32() { - let a: u32 = 0xDEADBEEF; - let b: u32 = 0xDEADBEEF; - let c: u32 = 0xCAFEBABE; - - // Equal case - assert_eq!(a.ct_eq(b), !0); // Expect All 1s - assert_eq!(a.ct_ne(b), 0); // Expect 0 - - // Not Equal case - assert_eq!(a.ct_eq(c), 0); // Expect 0 - assert_eq!(a.ct_ne(c), !0); // Expect All 1s -} - -#[test] -fn test_ct_integrated_flow() { - let secret = 12345u64; - let guess_correct = 12345u64; - let guess_wrong = 67890u64; - - let val_true = 100u64; - let val_false = 200u64; - - // Scenario 1: Correct Guess - let mask = secret.ct_eq(guess_correct); - let result = val_true.ct_select(val_false, mask); - assert_eq!(result, val_true); - - // Scenario 2: Wrong Guess - let mask = secret.ct_eq(guess_wrong); - let result = val_true.ct_select(val_false, mask); - assert_eq!(result, val_false); -} - -#[test] -fn test_ct_is_zero_nonzero() { - let zero: i32 = 0; - let non_zero: i32 = 123; - let neg_val: i32 = -1; - - // Zero check - assert_eq!(zero.ct_is_zero(), !0); - assert_eq!(zero.ct_is_nonzero(), 0); - - // Non-zero check - assert_eq!(non_zero.ct_is_zero(), 0); - assert_eq!(non_zero.ct_is_nonzero(), !0); - - // Negative value check (also non-zero) - assert_eq!(neg_val.ct_is_zero(), 0); - assert_eq!(neg_val.ct_is_nonzero(), !0); -} - -#[test] -fn test_ct_is_negative() { - let pos: i32 = 100; - let neg: i32 = -100; - let zero: i32 = 0; - - // Positive number - assert_eq!(pos.ct_is_negative(), 0); - - // Negative number - assert_eq!(neg.ct_is_negative(), !0); - - // Zero is not negative - assert_eq!(zero.ct_is_negative(), 0); - - // Unsigned types (MSB check) - let u_small: u8 = 0x0F; // 0000 1111 - let u_large: u8 = 0xF0; // 1111 0000 (MSB set) - - assert_eq!(u_small.ct_is_negative(), 0); - assert_eq!(u_large.ct_is_negative(), !0); -} diff --git a/crypto/constant-time/tests/ct_cross_validation.rs b/crypto/constant-time/tests/ct_cross_validation.rs deleted file mode 100644 index 3da648c..0000000 --- a/crypto/constant-time/tests/ct_cross_validation.rs +++ /dev/null @@ -1,131 +0,0 @@ -#[cfg(test)] -mod tests { - use entlib_native_constant_time::constant_time::ConstantTimeOps; - use proptest::prelude::*; - use subtle::Choice; - - /// subtle 라이브러리의 구현 한계를 우회하기 위한 헬퍼 트레이트 - trait SubtleFallback { - fn subtle_eq(&self, other: &Self) -> Choice; - fn subtle_select(a: &Self, b: &Self, choice: Choice) -> Self; - } - - /// subtle이 공식 지원하는 고정 폭 정수용 위임 매크로 - macro_rules! impl_subtle_native { - ($($t:ty),*) => { - $( - impl SubtleFallback for $t { - #[inline(always)] - fn subtle_eq(&self, other: &Self) -> Choice { - subtle::ConstantTimeEq::ct_eq(self, other) - } - #[inline(always)] - fn subtle_select(a: &Self, b: &Self, choice: Choice) -> Self { - <$t as subtle::ConditionallySelectable>::conditional_select(a, b, choice) - } - } - )* - } - } - impl_subtle_native!(u8, u16, u32, u64); - - /// subtle이 지원하지 않는 타입(usize, u128)을 위한 시맨틱 폴백 매크로 - /// (테스트 환경은 상수 시간 제약이 필요 없으므로 일반 분기문을 사용해 결과값만 대조) - macro_rules! impl_subtle_manual { - ($($t:ty),*) => { - $( - impl SubtleFallback for $t { - #[inline(always)] - fn subtle_eq(&self, other: &Self) -> Choice { - Choice::from(if self == other { 1 } else { 0 }) - } - #[inline(always)] - fn subtle_select(a: &Self, b: &Self, choice: Choice) -> Self { - if choice.unwrap_u8() == 1 { *b } else { *a } - } - } - )* - } - } - impl_subtle_manual!(usize, u128); - - /// 모든 타입에 대해 proptest를 생성하는 매크로 - macro_rules! test_ct_ops_cross_validation { - ($type:ty, $mod_name:ident) => { - mod $mod_name { - use super::*; - - proptest! { - #![proptest_config(ProptestConfig::with_cases(10_000))] - - #[test] - fn verify_ct_eq(a in any::<$type>(), b in any::<$type>()) { - let subtle_choice = <$type as SubtleFallback>::subtle_eq(&a, &b); - let expected_mask: $type = if subtle_choice.unwrap_u8() == 1 { !0 } else { 0 }; - - let actual_mask = a.ct_eq(b); - prop_assert_eq!(actual_mask, expected_mask, "ct_eq 불일치: a={}, b={}", a, b); - } - - #[test] - fn verify_ct_ne(a in any::<$type>(), b in any::<$type>()) { - let subtle_choice = <$type as SubtleFallback>::subtle_eq(&a, &b); - let expected_mask: $type = if subtle_choice.unwrap_u8() == 1 { 0 } else { !0 }; - - let actual_mask = a.ct_ne(b); - prop_assert_eq!(actual_mask, expected_mask, "ct_ne 불일치: a={}, b={}", a, b); - } - - #[test] - fn verify_ct_is_zero(a in any::<$type>()) { - let zero: $type = 0; - let subtle_choice = <$type as SubtleFallback>::subtle_eq(&a, &zero); - let expected_mask: $type = if subtle_choice.unwrap_u8() == 1 { !0 } else { 0 }; - - let actual_mask = a.ct_is_zero(); - prop_assert_eq!(actual_mask, expected_mask, "ct_is_zero 불일치: a={}", a); - } - - #[test] - fn verify_ct_is_nonzero(a in any::<$type>()) { - let zero: $type = 0; - let subtle_choice = <$type as SubtleFallback>::subtle_eq(&a, &zero); - let expected_mask: $type = if subtle_choice.unwrap_u8() == 1 { 0 } else { !0 }; - - let actual_mask = a.ct_is_nonzero(); - prop_assert_eq!(actual_mask, expected_mask, "ct_is_nonzero 불일치: a={}", a); - } - - #[test] - fn verify_ct_select(a in any::<$type>(), b in any::<$type>(), select_a in any::()) { - let choice = subtle::Choice::from(if select_a { 1 } else { 0 }); - let expected_val = <$type as SubtleFallback>::subtle_select(&a, &b, choice); - - let mask: $type = if select_a { 0 } else { !0 }; - let actual_val = a.ct_select(b, mask); - - prop_assert_eq!(actual_val, expected_val, "ct_select 불일치: a={}, b={}, select_a={}", a, b, select_a); - } - - #[test] - fn verify_ct_is_negative(a in any::<$type>()) { - let bits = core::mem::size_of::<$type>() as u32 * 8; - let is_neg = (a >> (bits - 1)) & 1; - let expected_mask: $type = if is_neg == 1 { !0 } else { 0 }; - - let actual_mask = a.ct_is_negative(); - prop_assert_eq!(actual_mask, expected_mask, "ct_is_negative 불일치: a={}", a); - } - } - } - }; - } - - test_ct_ops_cross_validation!(u32, verify_u32); - test_ct_ops_cross_validation!(u64, verify_u64); - test_ct_ops_cross_validation!(usize, verify_usize); - - test_ct_ops_cross_validation!(u8, verify_u8); - test_ct_ops_cross_validation!(u16, verify_u16); - test_ct_ops_cross_validation!(u128, verify_u128); -} diff --git a/crypto/core-secure/Cargo.toml b/crypto/core-secure/Cargo.toml deleted file mode 100644 index 6f473db..0000000 --- a/crypto/core-secure/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "entlib-native-core-secure" -version.workspace = true -edition.workspace = true -authors.workspace = true -license.workspace = true - -[dependencies] - -[dev-dependencies] -criterion = { version = "0.8.2", features = ["html_reports"] } - -[[bench]] -name = "secure_buffer_bench" -harness = false \ No newline at end of file diff --git a/crypto/core-secure/benches/secure_buffer_bench.rs b/crypto/core-secure/benches/secure_buffer_bench.rs deleted file mode 100644 index 6922b08..0000000 --- a/crypto/core-secure/benches/secure_buffer_bench.rs +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2025-2026 Quant - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the “Software”), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; - -use entlib_native_core_secure::secure_buffer::SecureBuffer; - -const SIZES: &[usize] = &[64, 256, 1_024, 4_096, 65_536, 1_048_576]; - -// -// SecureBuffer drop(wipe) 처리량 -// - -fn secure_buffer_wipe_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("throughput/secure_buffer_wipe"); - - for &size in SIZES { - group.throughput(Throughput::Bytes(size as u64)); - - let batch_size = if size >= 65_536 { - BatchSize::LargeInput - } else { - BatchSize::SmallInput - }; - - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - b.iter_batched( - || SecureBuffer { - inner: vec![0xAA; size], - }, - drop, // |buf| drop(buf) - batch_size, - ) - }); - } - - group.finish(); -} - -// -// Criterion 설정 -// - -criterion_group!(benches, secure_buffer_wipe_throughput); -criterion_main!(benches); diff --git a/crypto/core-secure/src/lib.rs b/crypto/core-secure/src/lib.rs deleted file mode 100644 index 0da0cd6..0000000 --- a/crypto/core-secure/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod secure_buffer; diff --git a/crypto/core-secure/src/secure_buffer.rs b/crypto/core-secure/src/secure_buffer.rs deleted file mode 100644 index 2e2c562..0000000 --- a/crypto/core-secure/src/secure_buffer.rs +++ /dev/null @@ -1,63 +0,0 @@ -use core::ptr::write_volatile; -use core::sync::atomic::{Ordering, compiler_fence}; - -/// 메모리 소거를 보장하는 보안 버퍼 구조체입니다. -/// -/// 이 구조체는 쉽게 말해 Rust가 할당하고 소유하는 메모리입니다. -/// 이 네이티브 코드상에서 연산의 '결과물'을 새로 생성할 때 사용됩니다. -/// `Base64` 디코딩 결과, 암호화된 사이퍼텍스트 생성 등의 상황을 예로 -/// 들 수 있습니다. -/// -/// 민감한 데이터를 사용한 연산 후 그 결과를 Java Heap으로 전달하는 경우, -/// 가비지 컬렉터의 생명주기에 종속되어 위험에 다시 노출되는 딜레마를 가지게 -/// 됩니다. 이 문제를 해결하기 위해 단순히 `byte[]`와 같은 Java 데이터로 -/// 직렬화(serialize)하지 않고, 이 구조체에 저장합니다. -/// -/// Java 측으로는 오직 해당 메모리의 원시 포인만이 전달되기 떄문에 안전한 -/// 데이터 관리가 가능합니다. -pub struct SecureBuffer { - pub inner: Vec, -} - -impl Drop for SecureBuffer { - fn drop(&mut self) { - for byte in self.inner.iter_mut() { - // volatile write -> 컴파일러 dce 최적화 방지함 - unsafe { - write_volatile(byte, 0); - } - } - // 메모리 배리어로 소거 순서 보장 - compiler_fence(Ordering::SeqCst); - } -} - -/// 메모리 소거를 보장하며, Java 측에서 사용되는 보안 버퍼 구조체입니다. -/// -/// 이 구조체는 Java 측에서 민감 데이터를 이미 Off-Heap 영역에 할당하여 -/// 들고 있을 때 사용됩니다. -/// -/// 민감한 데이터를 사용한 연산 후 그 결과를 Java Heap으로 전달하는 경우, -/// 가비지 컬렉터의 생명주기에 종속되어 위험에 다시 노출되는 딜레마를 가지게 -/// 됩니다. 이 문제를 해결하기 위해 단순히 `byte[]`와 같은 Java 데이터로 -/// 직렬화(serialize)하지 않고, 이 구조체에 저장합니다. -#[repr(C)] -pub struct FFIExternalSecureBuffer { - pub inner: *mut u8, - pub len: usize, -} - -impl Drop for FFIExternalSecureBuffer { - fn drop(&mut self) { - if self.inner.is_null() || self.len == 0 { - return; - } - unsafe { - let slice = core::slice::from_raw_parts_mut(self.inner, self.len); - for byte in slice.iter_mut() { - write_volatile(byte, 0); - } - } - compiler_fence(Ordering::SeqCst); - } -} diff --git a/crypto/digital-signature/Cargo.toml b/crypto/digital-signature/Cargo.toml new file mode 100644 index 0000000..7846a1d --- /dev/null +++ b/crypto/digital-signature/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "entlib-native-digital-signature" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] diff --git a/crypto/digital-signature/src/ed25519/mod.rs b/crypto/digital-signature/src/ed25519/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crypto/digital-signature/src/ed25519/mod.rs @@ -0,0 +1 @@ + diff --git a/crypto/digital-signature/src/ed448/mod.rs b/crypto/digital-signature/src/ed448/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crypto/digital-signature/src/ed448/mod.rs @@ -0,0 +1 @@ + diff --git a/crypto/digital-signature/src/lib.rs b/crypto/digital-signature/src/lib.rs new file mode 100644 index 0000000..cb37526 --- /dev/null +++ b/crypto/digital-signature/src/lib.rs @@ -0,0 +1,2 @@ +pub mod ed25519; +pub mod ed448; diff --git a/crypto/hkdf/Cargo.toml b/crypto/hkdf/Cargo.toml new file mode 100644 index 0000000..43e532f --- /dev/null +++ b/crypto/hkdf/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "entlib-native-hkdf" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +entlib-native-secure-buffer.workspace = true +entlib-native-hmac.workspace = true \ No newline at end of file diff --git a/crypto/hkdf/src/hkdf.rs b/crypto/hkdf/src/hkdf.rs new file mode 100644 index 0000000..da7667f --- /dev/null +++ b/crypto/hkdf/src/hkdf.rs @@ -0,0 +1,165 @@ +use core::cmp::min; +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; + +/// HKDF 연산 중 발생할 수 있는 상태 및 오류를 정의합니다. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HKDFState { + Success, + /// 요청된 OKM 길이가 최대 제한을 초과했거나 입력 버퍼가 잘못된 경우 + InvalidLength, + /// 보안 버퍼(SecureBuffer) 할당 실패 또는 OS 메모리 잠금 실패 + AllocationFailed, + /// 내부 HMAC 연산 실패 + HmacError, +} + +macro_rules! impl_hkdf { + ( + $struct_name:ident, + $hmac_type:ty, + $hash_len:expr + ) => { + /// NIST SP 800-56Cr2를 준수하는 HKDF 인스턴스 + pub struct $struct_name; + + impl Default for $struct_name { + fn default() -> Self { + Self::new() + } + } + + impl $struct_name { + /// 이 HKDF 구현에서 사용하는 기반 해시 함수의 출력 크기 (바이트) + pub const HASH_LEN: usize = $hash_len; + /// RFC 5869에 따른 OKM 최대 출력 크기 제한 (255 * HashLen) + pub const MAX_OKM_LEN: usize = 255 * $hash_len; + + /// 새로운 HKDF 인스턴스를 생성합니다. + #[inline(always)] + pub fn new() -> Self { + Self + } + + /// Extract (추출) + /// + /// 입력된 키 구성 물질(IKM)과 Salt를 사용하여 고정된 길이의 의사난수 키(PRK)를 생성합니다. + /// + /// 반환되는 PRK는 `SecureBuffer`에 안전하게 보관되어, + /// 메모리 스왑 방지 및 사용 후 즉각적인 Zeroization이 보장됩니다. + pub fn extract( + &self, + salt: Option<&[u8]>, + ikm: &[u8], + ) -> Result { + let default_salt = [0u8; $hash_len]; + let actual_salt = salt.unwrap_or(&default_salt); + + let mut hmac = <$hmac_type>::new(actual_salt).map_err(|_| HKDFState::HmacError)?; + hmac.update(ikm); + let prk_mac = hmac.finalize().map_err(|_| HKDFState::HmacError)?; + + // PRK는 SecureBuffer를 통해 관리 + let mut prk_buffer = + SecureBuffer::new_owned($hash_len).map_err(|_| HKDFState::AllocationFailed)?; + prk_buffer + .as_mut_slice() + .copy_from_slice(prk_mac.as_slice()); + + Ok(prk_buffer) + } + + /// Expand (확장) + /// + /// PRK와 컨텍스트(info)를 결합하여 원하는 길이(length)의 출력 키 물질(OKM)을 생성합니다. + pub fn expand( + &self, + prk: &SecureBuffer, + okm: &mut [u8], + info: &[u8], + length: usize, + ) -> Result<(), HKDFState> { + // 입력 길이 및 버퍼 크기에 대한 엄격한 검증 + if length > Self::MAX_OKM_LEN || okm.len() < length { + return Err(HKDFState::InvalidLength); + } + if prk.len() < Self::HASH_LEN { + return Err(HKDFState::InvalidLength); + } + + let mut t = [0u8; $hash_len]; + let mut okm_offset = 0; + let mut block_index: u8 = 1; + + let n = length.div_ceil(Self::HASH_LEN); + + for i in 0..n { + let mut hmac = + <$hmac_type>::new(prk.as_slice()).map_err(|_| HKDFState::HmacError)?; + + if i > 0 { + hmac.update(&t); + } + hmac.update(info); + hmac.update(&[block_index]); + + let mac = hmac.finalize().map_err(|_| HKDFState::HmacError)?; + t.copy_from_slice(mac.as_slice()); + + let copy_len = min(Self::HASH_LEN, length - okm_offset); + okm[okm_offset..okm_offset + copy_len].copy_from_slice(&t[..copy_len]); + + okm_offset += copy_len; + block_index += 1; + } + + // T 블록 강제 소거 + for byte in &mut t { + unsafe { + write_volatile(byte, 0); + } + } + + Ok(()) + } + + /// 단일 단계 키 유도 (Extract-then-Expand) + /// + /// IKM과 Salt를 사용해 내부적으로 PRK를 추출한 뒤, 즉시 Context(info)와 결합하여 + /// 원하는 길이의 출력 키 물질(OKM)을 생성합니다. + /// + /// # Security Rationale + /// 내부적으로 생성된 PRK(`SecureBuffer`)는 반환과 동시에 스코프를 벗어나며, + /// `Drop` 트레이트를 통해 즉시 하드웨어 수준에서 강제 소거(Zeroize)됩니다. + pub fn derive_key( + &self, + salt: Option<&[u8]>, + ikm: &[u8], + okm: &mut [u8], + info: &[u8], + length: usize, + ) -> Result<(), HKDFState> { + // Extract (PRK 생성 및 잠긴 메모리에 보관) + let prk_buffer = self.extract(salt, ikm)?; + + // Expand (PRK를 사용하여 OKM 생성) + // okm 버퍼의 크기 검증 등은 내부 expand 메소드의 Zero-Trust 로직에 위임 + self.expand(&prk_buffer, okm, info, length) + } + } + }; +} + +impl_hkdf!(HKDFSHA224, HMACSHA224, 28); +impl_hkdf!(HKDFSHA256, HMACSHA256, 32); +impl_hkdf!(HKDFSHA384, HMACSHA384, 48); +impl_hkdf!(HKDFSHA512, HMACSHA512, 64); + +impl_hkdf!(HKDFSHA3_224, HMACSHA3_224, 28); +impl_hkdf!(HKDFSHA3_256, HMACSHA3_256, 32); +impl_hkdf!(HKDFSHA3_384, HMACSHA3_384, 48); +impl_hkdf!(HKDFSHA3_512, HMACSHA3_512, 64); diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs new file mode 100644 index 0000000..8075057 --- /dev/null +++ b/crypto/hkdf/src/lib.rs @@ -0,0 +1,11 @@ +#![no_std] + +extern crate alloc; + +mod hkdf; + +// 외부(Java FFI 등) 및 사용자가 접근할 수 있는 구조체와 상태(에러) Enum만 공개합니다. +pub use hkdf::{ + HKDFSHA3_224, HKDFSHA3_256, HKDFSHA3_384, HKDFSHA3_512, HKDFSHA224, HKDFSHA256, HKDFSHA384, + HKDFSHA512, HKDFState, +}; diff --git a/crypto/hmac/Cargo.toml b/crypto/hmac/Cargo.toml new file mode 100644 index 0000000..dc28562 --- /dev/null +++ b/crypto/hmac/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "entlib-native-hmac" +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-sha2.workspace = true +entlib-native-sha3.workspace = true + +[dev-dependencies] +entlib-native-hex.workspace = true \ No newline at end of file diff --git a/crypto/hmac/src/hmac.rs b/crypto/hmac/src/hmac.rs new file mode 100644 index 0000000..14a47c2 --- /dev/null +++ b/crypto/hmac/src/hmac.rs @@ -0,0 +1,183 @@ +use crate::HmacError; +use entlib_native_constant_time::traits::ConstantTimeEq; +use entlib_native_secure_buffer::SecureBuffer; +use entlib_native_sha2::api::{SHA224, SHA256, SHA384, SHA512}; +use entlib_native_sha3::api::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + +const IPAD: u8 = 0x36; +const OPAD: u8 = 0x5c; +const SHA224_256_BLOCK_SIZE: usize = 64; // 512 bits +const SHA384_512_BLOCK_SIZE: usize = 128; // 1024 bits +// SHA3 HMAC 블록 크기 = rate (NIST FIPS 202 기준) +const SHA3_224_BLOCK_SIZE: usize = 144; // rate = 1152 bits +const SHA3_256_BLOCK_SIZE: usize = 136; // rate = 1088 bits +const SHA3_384_BLOCK_SIZE: usize = 104; // rate = 832 bits +const SHA3_512_BLOCK_SIZE: usize = 72; // rate = 576 bits +const MIN_KEY_LEN: usize = 14; // 112 bits (NIST SP 800-107r1) + +/// 생성된 MAC을 담는 래퍼 구조체입니다. +/// +/// 내부 필드는 [`SecureBuffer`]로 관리되어, `Drop` 시점에 MAC 바이트가 +/// 자동으로 0으로 소거되고 OS 레벨 메모리 잠금(mlock)이 해제됩니다. +pub struct MacResult(SecureBuffer); + +impl MacResult { + /// MAC 바이트를 읽기 전용 슬라이스로 반환합니다. + #[inline(always)] + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl PartialEq for MacResult { + /// 부채널 공격(Timing Attack) 방지를 위해 검증된 constant-time 크레이트 활용. + /// + /// MAC 길이(공개 정보)를 먼저 확인한 후 바이트를 상수-시간으로 비교합니다. + #[inline(never)] + fn eq(&self, other: &Self) -> bool { + let a = self.0.as_slice(); + let b = other.0.as_slice(); + + // MAC 길이는 공개 정보이므로 일반 분기 허용 + if a.len() != b.len() { + return false; + } + + let mut is_equal = 0xFFu8; + for (x, y) in a.iter().zip(b.iter()) { + is_equal &= x.ct_eq(y).unwrap_u8(); + } + + is_equal == 0xFF + } +} + +impl Eq for MacResult {} + +macro_rules! impl_hmac_sha { + ( + $struct_name:ident, + $hasher_type:ty, + $block_size:expr, + $mac_size:expr + ) => { + /// HMAC 구조체 + pub struct $struct_name { + i_key_pad: [u8; $block_size], + o_key_pad: [u8; $block_size], + hasher: $hasher_type, + } + + impl $struct_name { + /// HMAC 초기화 및 키 준비 함수입니다. + pub fn new(key: &[u8]) -> Result { + // [Security Control] NIST SP 800-107r1 5.3절: 112비트 미만의 키 거부 + if key.len() < MIN_KEY_LEN { + return Err(HmacError::WeakKeyLength); + } + + let mut k_block = [0u8; $block_size]; + + // 키 길이가 블록 크기보다 길 경우 해싱 (RFC 2104) + if key.len() > $block_size { + let mut key_hasher = <$hasher_type>::new(); + key_hasher.update(key); + let hashed_key = key_hasher + .finalize() + .map_err(HmacError::HashComputationError)?; + + let hash_slice = hashed_key.as_slice(); + k_block[..hash_slice.len()].copy_from_slice(hash_slice); + } else { + k_block[..key.len()].copy_from_slice(key); + } + + let mut i_key_pad = [0u8; $block_size]; + let mut o_key_pad = [0u8; $block_size]; + + for i in 0..$block_size { + i_key_pad[i] = k_block[i] ^ IPAD; + o_key_pad[i] = k_block[i] ^ OPAD; + } + + // H(K XOR ipad, text)의 첫 단계: H에 i_key_pad 주입 + let mut hasher = <$hasher_type>::new(); + hasher.update(&i_key_pad); + + // 사용이 끝난 원본 키 블록은 즉시 소거 + core::hint::black_box({ + k_block.fill(0); + }); + + Ok(Self { + i_key_pad, + o_key_pad, + hasher, + }) + } + + /// 스트리밍 방식을 지원하는 데이터 업데이트 함수입니다. + pub fn update(&mut self, data: &[u8]) { + self.hasher.update(data); + } + + /// 최종 MAC 계산 및 반환 함수입니다. + pub fn finalize(mut self) -> Result { + // Q. T. Felix NOTE: 왜 Option을 안 쓰냐 -> 분기생성 가능성 있음 -> 부채널 공격 위험해집니다. + // Drop 구현체의 부분 소유권 이동 금지 규칙을 우회하기 위해, + // 분기 없는 core::mem::replace를 사용하여 내부 Hasher의 소유권을 획득 + // 이 과정은 상수-시간으로 동작하며 해당 공격을 방지합니다. + let hasher = core::mem::replace(&mut self.hasher, <$hasher_type>::new()); + + // 소유권이 이전된 hasher를 통해 첫 번째 해시 결과 획득 + let inner_hash = hasher.finalize().map_err(HmacError::HashComputationError)?; + + // H(K XOR opad, H(K XOR ipad, text)) + let mut outer_hasher = <$hasher_type>::new(); + outer_hasher.update(&self.o_key_pad); + outer_hasher.update(inner_hash.as_slice()); + + // 최종 해시 결과 획득 + let outer_hash = outer_hasher + .finalize() + .map_err(HmacError::HashComputationError)?; + + // SecureBuffer에 MAC을 복사하여 mlock + 자동 소거 보장 + let mut mac_buf = + SecureBuffer::new_owned($mac_size).map_err(HmacError::AllocationError)?; + mac_buf + .as_mut_slice() + .copy_from_slice(outer_hash.as_slice()); + + Ok(MacResult(mac_buf)) + } + } + + /// 메모리 잔존 공격 방지를 위한 명시적 소거를 위한 Drop 트레이트 구현입니다. + impl Drop for $struct_name { + fn drop(&mut self) { + use core::ptr::write_volatile; + for byte in self.i_key_pad.iter_mut() { + unsafe { + write_volatile(byte, 0); + } + } + for byte in self.o_key_pad.iter_mut() { + unsafe { + write_volatile(byte, 0); + } + } + } + } + }; +} + +impl_hmac_sha!(HMACSHA224, SHA224, SHA224_256_BLOCK_SIZE, 28); +impl_hmac_sha!(HMACSHA256, SHA256, SHA224_256_BLOCK_SIZE, 32); +impl_hmac_sha!(HMACSHA384, SHA384, SHA384_512_BLOCK_SIZE, 48); +impl_hmac_sha!(HMACSHA512, SHA512, SHA384_512_BLOCK_SIZE, 64); + +impl_hmac_sha!(HMACSHA3_224, SHA3_224, SHA3_224_BLOCK_SIZE, 28); +impl_hmac_sha!(HMACSHA3_256, SHA3_256, SHA3_256_BLOCK_SIZE, 32); +impl_hmac_sha!(HMACSHA3_384, SHA3_384, SHA3_384_BLOCK_SIZE, 48); +impl_hmac_sha!(HMACSHA3_512, SHA3_512, SHA3_512_BLOCK_SIZE, 64); diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs new file mode 100644 index 0000000..055ea6a --- /dev/null +++ b/crypto/hmac/src/lib.rs @@ -0,0 +1,21 @@ +#![no_std] + +extern crate alloc; + +mod hmac; + +pub use hmac::{ + HMACSHA3_224, HMACSHA3_256, HMACSHA3_384, HMACSHA3_512, HMACSHA224, HMACSHA256, HMACSHA384, + HMACSHA512, MacResult, +}; + +/// HMAC 연산 중 발생할 수 있는 보안 오류 +#[derive(Debug)] +pub enum HmacError { + /// NIST SP 800-107r1에 따른 최소 키 길이(112 bits / 14 bytes) 미달 + WeakKeyLength, + /// 내부 해시 연산 중 발생한 오류 + HashComputationError(&'static str), + /// MAC 결과를 저장하기 위한 SecureBuffer 할당 실패 + AllocationError(&'static str), +} diff --git a/crypto/hmac/tests/hmac_kcmvp_cavp.rs b/crypto/hmac/tests/hmac_kcmvp_cavp.rs new file mode 100644 index 0000000..3b2ad38 --- /dev/null +++ b/crypto/hmac/tests/hmac_kcmvp_cavp.rs @@ -0,0 +1,361 @@ +#[cfg(test)] +mod kcmvp_cavp_hmac_sha2_test { + use std::fs::File; + use std::io::{BufRead, BufReader, BufWriter, Write}; + use std::path::Path; + + /// 헥스 문자열을 안전하게 바이트 벡터로 변환 (Zero-Trust 원칙 적용) + fn decode_hex(s: &str) -> Result, &'static str> { + if !s.len().is_multiple_of(2) { + return Err("Invalid hex string length"); + } + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| "Invalid hex character")) + .collect() + } + + /// 바이트 슬라이스를 헥스 문자열로 변환 + fn encode_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() + } + + /// 단일 알고리즘에 대한 CAVP 테스트 벡터 처리기 + /// `compute_mac` 클로저를 통해 구체적인 HMAC 알고리즘을 주입받습니다. + #[allow(unused_assignments)] + fn process_cavp_file(req_path: &str, rsp_path: &str, mut compute_mac: F) + where + // 입력: key, msg / 출력: Result<전체 MAC 바이트, 에러> + F: FnMut(&[u8], &[u8]) -> Result, &'static str>, + { + let req_file = File::open(Path::new(req_path)).expect("Failed to open .req file"); + let rsp_file = File::create(Path::new(rsp_path)).expect("Failed to create .rsp file"); + + let reader = BufReader::new(req_file); + let mut writer = BufWriter::new(rsp_file); + + let mut current_tlen = 0usize; + let mut current_key = Vec::new(); + let mut current_msg = Vec::new(); + + for line_result in reader.lines() { + let line = line_result.expect("Failed to read line"); + let trimmed = line.trim(); + + // 빈 줄이나 대괄호 표기(예: [L=32])는 그대로 출력 + if trimmed.is_empty() || trimmed.starts_with('[') { + writeln!(writer, "{}", line).unwrap(); + continue; + } + + // "Key = Value" 형태 파싱 + if let Some((k, v)) = trimmed.split_once('=') { + let key = k.trim(); + let val = v.trim(); + + // 기존 라인 출력 (요청에 맞게 원본 유지) + writeln!(writer, "{}", line).unwrap(); + + match key { + "Tlen" => { + current_tlen = val.parse::().expect("Invalid Tlen"); + } + "Key" => { + current_key = decode_hex(val).expect("Failed to decode Key"); + } + "Msg" => { + current_msg = decode_hex(val).expect("Failed to decode Msg"); + + // Msg 파싱이 완료되면 하나의 테스트 케이스 입력이 모두 준비된 것으로 간주하고 MAC 산출 + match compute_mac(¤t_key, ¤t_msg) { + Ok(full_mac) => { + // NIST SP 800-107r1 Tlen (Truncation) 처리 + // 전체 MAC에서 Tlen 바이트만큼만 잘라내 출력 + if current_tlen > full_mac.len() { + panic!( + "Tlen cannot be greater than the underlying hash output length" + ); + } + let truncated_mac = &full_mac[..current_tlen]; + writeln!(writer, "Mac = {}", encode_hex(truncated_mac)).unwrap(); + } + Err(e) => { + // 고보안 요구사항 미달 (112bit 미만 키와 같은 이유로) 시 CAVP 테스트에서는 + // 테스트 벡터 자체의 의도에 따라 처리가 달라질 수 있음 + // 에러 발생 시 로그를 남기고 스킵하거나 특정 포맷으로 출력 + writeln!(writer, "MacError = {}", e).unwrap(); + } + } + } + _ => { /* COUNT, Klen 등은 위에서 원본 라인으로 출력되었으므로 무시 */ + } + } + } else { + writeln!(writer, "{}", line).unwrap(); + } + } + } + + // + // 실제 테스트 실행부 + // + #[test] + fn cavp_hmac_sha2_test() { + use entlib_native_hmac::{HMACSHA224, HMACSHA256, HMACSHA384, HMACSHA512}; + + let dir = match std::env::var("KCMVP_CAVP_DIR") { + Ok(val) => val, + Err(_) => panic!("env"), + }; + + process_cavp_file( + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-224_KAT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-224_KAT.rsp", + dir + ) + .as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA224::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + + process_cavp_file( + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-256_KAT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-256_KAT.rsp", + dir + ) + .as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA256::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + + process_cavp_file( + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-384_KAT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-384_KAT.rsp", + dir + ) + .as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA384::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + + process_cavp_file( + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-512_KAT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_2_20260312205017/HMAC_SHA-512_KAT.rsp", + dir + ) + .as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA512::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + } +} + +#[cfg(test)] +mod kcmvp_cavp_hmac_sha3_test { + use std::fs::File; + use std::io::{BufRead, BufReader, BufWriter, Write}; + use std::path::Path; + + /// 헥스 문자열을 안전하게 바이트 벡터로 변환 (Zero-Trust 원칙 적용) + fn decode_hex(s: &str) -> Result, &'static str> { + if !s.len().is_multiple_of(2) { + return Err("Invalid hex string length"); + } + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| "Invalid hex character")) + .collect() + } + + /// 바이트 슬라이스를 헥스 문자열로 변환 + fn encode_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() + } + + /// 단일 알고리즘에 대한 CAVP 테스트 벡터 처리기 + /// `compute_mac` 클로저를 통해 구체적인 HMAC 알고리즘을 주입받습니다. + #[allow(unused_assignments)] + fn process_cavp_file(req_path: &str, rsp_path: &str, mut compute_mac: F) + where + // 입력: key, msg / 출력: Result<전체 MAC 바이트, 에러> + F: FnMut(&[u8], &[u8]) -> Result, &'static str>, + { + let req_file = File::open(Path::new(req_path)).expect("Failed to open .req file"); + let rsp_file = File::create(Path::new(rsp_path)).expect("Failed to create .rsp file"); + + let reader = BufReader::new(req_file); + let mut writer = BufWriter::new(rsp_file); + + let mut current_tlen = 0usize; + let mut current_key = Vec::new(); + let mut current_msg = Vec::new(); + + for line_result in reader.lines() { + let line = line_result.expect("Failed to read line"); + let trimmed = line.trim(); + + // 빈 줄이나 대괄호 표기(예: [L=32])는 그대로 출력 + if trimmed.is_empty() || trimmed.starts_with('[') { + writeln!(writer, "{}", line).unwrap(); + continue; + } + + // "Key = Value" 형태 파싱 + if let Some((k, v)) = trimmed.split_once('=') { + let key = k.trim(); + let val = v.trim(); + + // 기존 라인 출력 (요청에 맞게 원본 유지) + writeln!(writer, "{}", line).unwrap(); + + match key { + "Tlen" => { + current_tlen = val.parse::().expect("Invalid Tlen"); + } + "Key" => { + current_key = decode_hex(val).expect("Failed to decode Key"); + } + "Msg" => { + current_msg = decode_hex(val).expect("Failed to decode Msg"); + + // Msg 파싱이 완료되면 하나의 테스트 케이스 입력이 모두 준비된 것으로 간주하고 MAC 산출 + match compute_mac(¤t_key, ¤t_msg) { + Ok(full_mac) => { + // NIST SP 800-107r1 Tlen (Truncation) 처리 + // 전체 MAC에서 Tlen 바이트만큼만 잘라내 출력 + if current_tlen > full_mac.len() { + panic!( + "Tlen cannot be greater than the underlying hash output length" + ); + } + let truncated_mac = &full_mac[..current_tlen]; + writeln!(writer, "Mac = {}", encode_hex(truncated_mac)).unwrap(); + } + Err(e) => { + // 고보안 요구사항 미달 (112bit 미만 키와 같은 이유로) 시 CAVP 테스트에서는 + // 테스트 벡터 자체의 의도에 따라 처리가 달라질 수 있음 + // 에러 발생 시 로그를 남기고 스킵하거나 특정 포맷으로 출력 + writeln!(writer, "MacError = {}", e).unwrap(); + } + } + } + _ => { /* COUNT, Klen 등은 위에서 원본 라인으로 출력되었으므로 무시 */ + } + } + } else { + writeln!(writer, "{}", line).unwrap(); + } + } + } + + // + // 실제 테스트 실행부 + // + #[test] + fn cavp_hmac_sha3_test() { + use entlib_native_hmac::{HMACSHA3_224, HMACSHA3_256, HMACSHA3_384, HMACSHA3_512}; + + let dir = match std::env::var("KCMVP_CAVP_DIR") { + Ok(val) => val, + Err(_) => panic!("env"), + }; + + let more = format!("{}/entanglementlib__CAVP_5_20260315192802", dir); + + process_cavp_file( + format!("{}/HMAC_SHA3-224_KAT.req", more).as_str(), + format!("{}/HMAC_SHA3-224_KAT.rsp", more).as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA3_224::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + + process_cavp_file( + format!("{}/HMAC_SHA3-256_KAT.req", more).as_str(), + format!("{}/HMAC_SHA3-256_KAT.rsp", more).as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA3_256::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + + process_cavp_file( + format!("{}/HMAC_SHA3-384_KAT.req", more).as_str(), + format!("{}/HMAC_SHA3-384_KAT.rsp", more).as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA3_384::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + + process_cavp_file( + format!("{}/HMAC_SHA3-512_KAT.req", more).as_str(), + format!("{}/HMAC_SHA3-512_KAT.rsp", more).as_str(), + |key, msg| { + // 초기화 시 보안 정책(112bit 미만 차단 등)에 의해 에러가 발생할 수 있음 + let mut hmac = HMACSHA3_512::new(key).map_err(|_| "HmacInitError")?; + hmac.update(msg); + let result = hmac.finalize().map_err(|_| "HmacFinalizeError")?; + + Ok(result.as_slice().to_vec()) + }, + ); + } +} diff --git a/crypto/hmac/tests/hmac_test.rs b/crypto/hmac/tests/hmac_test.rs new file mode 100644 index 0000000..eaf23bb --- /dev/null +++ b/crypto/hmac/tests/hmac_test.rs @@ -0,0 +1,405 @@ +use entlib_native_hmac::{HMACSHA256, HMACSHA512, HmacError}; + +// +// 헬퍼 +// + +fn hmac256(key: &[u8], chunks: &[&[u8]]) -> Vec { + let mut h = HMACSHA256::new(key).expect("HMACSHA256 초기화 실패"); + for c in chunks { + h.update(c); + } + h.finalize() + .expect("HMACSHA256 finalize 실패") + .as_slice() + .to_vec() +} + +fn hmac512(key: &[u8], chunks: &[&[u8]]) -> Vec { + let mut h = HMACSHA512::new(key).expect("HMACSHA512 초기화 실패"); + for c in chunks { + h.update(c); + } + h.finalize() + .expect("HMACSHA512 finalize 실패") + .as_slice() + .to_vec() +} + +// +// RFC 4231 HMAC-SHA-256 +// + +/// TC1 — Key: 0x0b×20 / Data: "Hi There" +#[test] +fn sha256_tc1() { + assert_eq!( + hmac256(&[0x0bu8; 20], &[b"Hi There"]), + [ + 0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0x0b, + 0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x00, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c, + 0x2e, 0x32, 0xcf, 0xf7, + ] + ); +} + +/// TC3 — Key: 0xaa×20 / Data: 0xdd×50 +#[test] +fn sha256_tc3() { + assert_eq!( + hmac256(&[0xaau8; 20], &[&[0xddu8; 50]]), + [ + 0x77, 0x3e, 0xa9, 0x1e, 0x36, 0x80, 0x0e, 0x46, 0x85, 0x4d, 0xb8, 0xeb, 0xd0, 0x91, + 0x81, 0xa7, 0x29, 0x59, 0x09, 0x8b, 0x3e, 0xf8, 0xc1, 0x22, 0xd9, 0x63, 0x55, 0x14, + 0xce, 0xd5, 0x65, 0xfe, + ] + ); +} + +/// TC4 — Key: 0x01..0x19 (25 bytes) / Data: 0xcd×50 +#[test] +fn sha256_tc4() { + let key: [u8; 25] = [ + 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, + ]; + assert_eq!( + hmac256(&key, &[&[0xcdu8; 50]]), + [ + 0x82, 0x55, 0x8a, 0x38, 0x9a, 0x44, 0x3c, 0x0e, 0xa4, 0xcc, 0x81, 0x98, 0x99, 0xf2, + 0x08, 0x3a, 0x85, 0xf0, 0xfa, 0xa3, 0xe5, 0x78, 0xf8, 0x07, 0x7a, 0x2e, 0x3f, 0xf4, + 0x67, 0x29, 0x66, 0x5b, + ] + ); +} + +/// TC6 — Key: 0xaa×131 (블록 초과) / 키를 SHA-256으로 해싱 후 사용 +#[test] +fn sha256_tc6_key_longer_than_block() { + assert_eq!( + hmac256( + &[0xaau8; 131], + &[b"Test Using Larger Than Block-Size Key - Hash Key First"] + ), + [ + 0x60, 0xe4, 0x31, 0x59, 0x1e, 0xe0, 0xb6, 0x7f, 0x0d, 0x8a, 0x26, 0xaa, 0xcb, 0xf5, + 0xb7, 0x7f, 0x8e, 0x0b, 0xc6, 0x21, 0x37, 0x28, 0xc5, 0x14, 0x05, 0x46, 0x04, 0x0f, + 0x0e, 0xe3, 0x7f, 0x54, + ] + ); +} + +/// TC7 — Key: 0xaa×131 / 데이터도 블록 초과 +#[test] +fn sha256_tc7_key_and_data_longer_than_block() { + let data = b"This is a test using a larger than block-size key and a larger \ + than block-size data. The key needs to be hashed before being \ + used by the HMAC algorithm."; + assert_eq!( + hmac256(&[0xaau8; 131], &[data]), + [ + 0x9b, 0x09, 0xff, 0xa7, 0x1b, 0x94, 0x2f, 0xcb, 0x27, 0x63, 0x5f, 0xbc, 0xd5, 0xb0, + 0xe9, 0x44, 0xbf, 0xdc, 0x63, 0x64, 0x4f, 0x07, 0x13, 0x93, 0x8a, 0x7f, 0x51, 0x53, + 0x5c, 0x3a, 0x35, 0xe2, + ] + ); +} + +// +// RFC 4231 HMAC-SHA-512 +// + +/// TC1 — Key: 0x0b×20 / Data: "Hi There" +#[test] +fn sha512_tc1() { + assert_eq!( + hmac512(&[0x0bu8; 20], &[b"Hi There"]), + [ + 0x87, 0xaa, 0x7c, 0xde, 0xa5, 0xef, 0x61, 0x9d, 0x4f, 0xf0, 0xb4, 0x24, 0x1a, 0x1d, + 0x6c, 0xb0, 0x23, 0x79, 0xf4, 0xe2, 0xce, 0x4e, 0xc2, 0x78, 0x7a, 0xd0, 0xb3, 0x05, + 0x45, 0xe1, 0x7c, 0xde, 0xda, 0xa8, 0x33, 0xb7, 0xd6, 0xb8, 0xa7, 0x02, 0x03, 0x8b, + 0x27, 0x4e, 0xae, 0xa3, 0xf4, 0xe4, 0xbe, 0x9d, 0x91, 0x4e, 0xeb, 0x61, 0xf1, 0x70, + 0x2e, 0x69, 0x6c, 0x20, 0x3a, 0x12, 0x68, 0x54, + ] + ); +} + +/// TC3 — Key: 0xaa×20 / Data: 0xdd×50 +#[test] +fn sha512_tc3() { + assert_eq!( + hmac512(&[0xaau8; 20], &[&[0xddu8; 50]]), + [ + 0xfa, 0x73, 0xb0, 0x08, 0x9d, 0x56, 0xa2, 0x84, 0xef, 0xb0, 0xf0, 0x75, 0x6c, 0x89, + 0x0b, 0xe9, 0xb1, 0xb5, 0xdb, 0xdd, 0x8e, 0xe8, 0x1a, 0x36, 0x55, 0xf8, 0x3e, 0x33, + 0xb2, 0x27, 0x9d, 0x39, 0xbf, 0x3e, 0x84, 0x82, 0x79, 0xa7, 0x22, 0xc8, 0x06, 0xb4, + 0x85, 0xa4, 0x7e, 0x67, 0xc8, 0x07, 0xb9, 0x46, 0xa3, 0x37, 0xbe, 0xe8, 0x94, 0x26, + 0x74, 0x27, 0x88, 0x59, 0xe1, 0x32, 0x92, 0xfb, + ] + ); +} + +/// TC4 — Key: 0x01..0x19 (25 bytes) / Data: 0xcd×50 +#[test] +fn sha512_tc4() { + let key: [u8; 25] = [ + 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, + ]; + assert_eq!( + hmac512(&key, &[&[0xcdu8; 50]]), + [ + 0xb0, 0xba, 0x46, 0x56, 0x37, 0x45, 0x8c, 0x69, 0x90, 0xe5, 0xa8, 0xc5, 0xf6, 0x1d, + 0x4a, 0xf7, 0xe5, 0x76, 0xd9, 0x7f, 0xf9, 0x4b, 0x87, 0x2d, 0xe7, 0x6f, 0x80, 0x50, + 0x36, 0x1e, 0xe3, 0xdb, 0xa9, 0x1c, 0xa5, 0xc1, 0x1a, 0xa2, 0x5e, 0xb4, 0xd6, 0x79, + 0x27, 0x5c, 0xc5, 0x78, 0x80, 0x63, 0xa5, 0xf1, 0x97, 0x41, 0x12, 0x0c, 0x4f, 0x2d, + 0xe2, 0xad, 0xeb, 0xeb, 0x10, 0xa2, 0x98, 0xdd, + ] + ); +} + +/// TC6 — Key: 0xaa×131 (블록 128B 초과) / 키를 SHA-512로 해싱 후 사용 +#[test] +fn sha512_tc6_key_longer_than_block() { + assert_eq!( + hmac512( + &[0xaau8; 131], + &[b"Test Using Larger Than Block-Size Key - Hash Key First"] + ), + [ + 0x80, 0xb2, 0x42, 0x63, 0xc7, 0xc1, 0xa3, 0xeb, 0xb7, 0x14, 0x93, 0xc1, 0xdd, 0x7b, + 0xe8, 0xb4, 0x9b, 0x46, 0xd1, 0xf4, 0x1b, 0x4a, 0xee, 0xc1, 0x12, 0x1b, 0x01, 0x37, + 0x83, 0xf8, 0xf3, 0x52, 0x6b, 0x56, 0xd0, 0x37, 0xe0, 0x5f, 0x25, 0x98, 0xbd, 0x0f, + 0xd2, 0x21, 0x5d, 0x6a, 0x1e, 0x52, 0x95, 0xe6, 0x4f, 0x73, 0xf6, 0x3f, 0x0a, 0xec, + 0x8b, 0x91, 0x5a, 0x98, 0x5d, 0x78, 0x65, 0x98, + ] + ); +} + +/// TC7 — Key: 0xaa×131 / 데이터도 블록 초과 +#[test] +fn sha512_tc7_key_and_data_longer_than_block() { + let data = b"This is a test using a larger than block-size key and a larger \ + than block-size data. The key needs to be hashed before being \ + used by the HMAC algorithm."; + assert_eq!( + hmac512(&[0xaau8; 131], &[data]), + [ + 0xe3, 0x7b, 0x6a, 0x77, 0x5d, 0xc8, 0x7d, 0xba, 0xa4, 0xdf, 0xa9, 0xf9, 0x6e, 0x5e, + 0x3f, 0xfd, 0xde, 0xbd, 0x71, 0xf8, 0x86, 0x72, 0x89, 0x86, 0x5d, 0xf5, 0xa3, 0x2d, + 0x20, 0xcd, 0xc9, 0x44, 0xb6, 0x02, 0x2c, 0xac, 0x3c, 0x49, 0x82, 0xb1, 0x0d, 0x5e, + 0xeb, 0x55, 0xc3, 0xe4, 0xde, 0x15, 0x13, 0x46, 0x76, 0xfb, 0x6d, 0xe0, 0x44, 0x60, + 0x65, 0xc9, 0x74, 0x40, 0xfa, 0x8c, 0x6a, 0x58, + ] + ); +} + +// +// 보안 정책 (NIST SP 800-107r1) +// + +/// 112비트(14바이트) 미만 키: 두 알고리즘 모두 반드시 거부 +#[test] +fn rejects_weak_key_sha256() { + match HMACSHA256::new(&[0x42u8; 13]) { + Err(HmacError::WeakKeyLength) => {} + _ => panic!("HMACSHA256: 취약 키가 수락됨"), + } +} + +#[test] +fn rejects_weak_key_sha512() { + match HMACSHA512::new(&[0x42u8; 13]) { + Err(HmacError::WeakKeyLength) => {} + _ => panic!("HMACSHA512: 취약 키가 수락됨"), + } +} + +/// 빈 키: 두 알고리즘 모두 거부 +#[test] +fn rejects_empty_key_sha256() { + assert!(matches!( + HMACSHA256::new(&[]), + Err(HmacError::WeakKeyLength) + )); +} + +#[test] +fn rejects_empty_key_sha512() { + assert!(matches!( + HMACSHA512::new(&[]), + Err(HmacError::WeakKeyLength) + )); +} + +/// 경계값 — 정확히 14바이트(112비트): 두 알고리즘 모두 수락 +#[test] +fn accepts_minimum_key_sha256() { + assert!(HMACSHA256::new(&[0xabu8; 14]).is_ok()); +} + +#[test] +fn accepts_minimum_key_sha512() { + assert!(HMACSHA512::new(&[0xabu8; 14]).is_ok()); +} + +// +// 스트리밍 무결성 +// + +/// 분할 update는 단일 update와 동일한 MAC을 생성해야 함 (SHA-256) +#[test] +fn sha256_streaming_matches_single() { + let key = [0x0bu8; 20]; + let single = hmac256(&key, &[b"Hi There"]); + let chunked = hmac256(&key, &[b"Hi", b" ", b"There"]); + assert_eq!(single, chunked); +} + +/// 분할 update는 단일 update와 동일한 MAC을 생성해야 함 (SHA-512) +#[test] +fn sha512_streaming_matches_single() { + let key = [0x0bu8; 20]; + let single = hmac512(&key, &[b"Hi There"]); + let chunked = hmac512(&key, &[b"Hi", b" ", b"There"]); + assert_eq!(single, chunked); +} + +/// SHA-256 블록 경계(64B) 전후 분할도 동일 +#[test] +fn sha256_streaming_across_block_boundary() { + let key = [0x0bu8; 20]; + let data = [0x61u8; 200]; + let base = hmac256(&key, &[&data]); + for split in [63, 64, 65, 128] { + assert_eq!( + base, + hmac256(&key, &[&data[..split], &data[split..]]), + "SHA-256: {split}/{} 분할 불일치", + 200 - split + ); + } +} + +/// SHA-512 블록 경계(128B) 전후 분할도 동일 +#[test] +fn sha512_streaming_across_block_boundary() { + let key = [0x0bu8; 20]; + let data = [0x61u8; 400]; + let base = hmac512(&key, &[&data]); + for split in [127, 128, 129, 256] { + assert_eq!( + base, + hmac512(&key, &[&data[..split], &data[split..]]), + "SHA-512: {split}/{} 분할 불일치", + 400 - split + ); + } +} + +// +// 상수-시간 비교(MacResult) +// + +/// 동일 MAC끼리 CT 비교: true (SHA-256) +#[test] +fn sha256_mac_ct_eq_same() { + let mac1 = HMACSHA256::new(&[0x0bu8; 20]).unwrap().finalize().unwrap(); + let mac2 = HMACSHA256::new(&[0x0bu8; 20]).unwrap().finalize().unwrap(); + assert!(mac1 == mac2); +} + +/// 동일 MAC끼리 CT 비교: true (SHA-512) +#[test] +fn sha512_mac_ct_eq_same() { + let mac1 = HMACSHA512::new(&[0x0bu8; 20]).unwrap().finalize().unwrap(); + let mac2 = HMACSHA512::new(&[0x0bu8; 20]).unwrap().finalize().unwrap(); + assert!(mac1 == mac2); +} + +/// 다른 MAC CT 비교: false (SHA-256) +#[test] +fn sha256_mac_ct_ne_different() { + let key = [0x0bu8; 20]; + let mut h1 = HMACSHA256::new(&key).unwrap(); + h1.update(b"a"); + let m1 = h1.finalize().unwrap(); + let mut h2 = HMACSHA256::new(&key).unwrap(); + h2.update(b"b"); + let m2 = h2.finalize().unwrap(); + assert!(m1 != m2); +} + +/// 다른 MAC CT 비교: false (SHA-512) +#[test] +fn sha512_mac_ct_ne_different() { + let key = [0x0bu8; 20]; + let mut h1 = HMACSHA512::new(&key).unwrap(); + h1.update(b"a"); + let m1 = h1.finalize().unwrap(); + let mut h2 = HMACSHA512::new(&key).unwrap(); + h2.update(b"b"); + let m2 = h2.finalize().unwrap(); + assert!(m1 != m2); +} + +/// 1바이트 비트플립 탐지 (SHA-256): 메시지 1비트 차이는 다른 MAC을 생성해야 함 +#[test] +fn sha256_mac_single_bit_flip_detected() { + let key = [0x0bu8; 20]; + let mut h1 = HMACSHA256::new(&key).unwrap(); + h1.update(b"data\x00"); + let m1 = h1.finalize().unwrap(); + let mut h2 = HMACSHA256::new(&key).unwrap(); + h2.update(b"data\x01"); + let m2 = h2.finalize().unwrap(); + assert!(m1 != m2); +} + +/// 1바이트 비트플립 탐지 (SHA-512): 메시지 1비트 차이는 다른 MAC을 생성해야 함 +#[test] +fn sha512_mac_single_bit_flip_detected() { + let key = [0x0bu8; 20]; + let mut h1 = HMACSHA512::new(&key).unwrap(); + h1.update(b"data\x00"); + let m1 = h1.finalize().unwrap(); + let mut h2 = HMACSHA512::new(&key).unwrap(); + h2.update(b"data\x01"); + let m2 = h2.finalize().unwrap(); + assert!(m1 != m2); +} + +// +// 결정론적 특성 +// + +/// 동일 입력 → 동일 MAC (두 알고리즘 모두) +#[test] +fn macs_are_deterministic() { + let key = [0x5au8; 20]; + let data = b"deterministic test vector"; + assert_eq!(hmac256(&key, &[data]), hmac256(&key, &[data])); + assert_eq!(hmac512(&key, &[data]), hmac512(&key, &[data])); +} + +/// 키가 다르면 다른 MAC (두 알고리즘 모두) +#[test] +fn different_keys_different_macs() { + let data = b"same data"; + assert_ne!( + hmac256(&[0x01u8; 20], &[data]), + hmac256(&[0x02u8; 20], &[data]) + ); + assert_ne!( + hmac512(&[0x01u8; 20], &[data]), + hmac512(&[0x02u8; 20], &[data]) + ); +} + +/// 데이터가 다르면 다른 MAC (두 알고리즘 모두) +#[test] +fn different_data_different_macs() { + let key = [0x0bu8; 20]; + assert_ne!(hmac256(&key, &[b"msg_a"]), hmac256(&key, &[b"msg_b"])); + assert_ne!(hmac512(&key, &[b"msg_a"]), hmac512(&key, &[b"msg_b"])); +} diff --git a/crypto/key-establishment/Cargo.toml b/crypto/key-establishment/Cargo.toml new file mode 100644 index 0000000..d516c4e --- /dev/null +++ b/crypto/key-establishment/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "entlib-native-key-establishment" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] \ No newline at end of file diff --git a/crypto/key-establishment/src/lib.rs b/crypto/key-establishment/src/lib.rs new file mode 100644 index 0000000..0c9ac1a --- /dev/null +++ b/crypto/key-establishment/src/lib.rs @@ -0,0 +1 @@ +#![no_std] diff --git a/crypto/rng/Cargo.toml b/crypto/rng/Cargo.toml index 94e0eeb..083c1ff 100644 --- a/crypto/rng/Cargo.toml +++ b/crypto/rng/Cargo.toml @@ -6,13 +6,3 @@ authors.workspace = true license.workspace = true [dependencies] -entlib-native-core-secure.workspace = true -tokio.workspace = true - -[dev-dependencies] -criterion = { version = "0.8.2", features = ["html_reports"] } -tokio = { workspace = true, features = ["macros"] } - -[[bench]] -name = "anu_qrng_bench" -harness = false \ No newline at end of file diff --git a/crypto/rng/benches/anu_qrng_bench.rs b/crypto/rng/benches/anu_qrng_bench.rs deleted file mode 100644 index a786614..0000000 --- a/crypto/rng/benches/anu_qrng_bench.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! 베이스가 TLS 통신 처리라 기본적으로 그닥 성능이 좋지 못함 -//! 하지만 난수가 Java로 전달되기 전의 지연율 상한성을 파악할 수 있어서 -//! 혼합 rng 모듈이 양자 난수를 최초 1회만 받아야 하는 수학적 근거가 됌. - -use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; -use entlib_native_rng::anu_qrng::AnuQrngClient; -use std::hint::black_box; // Q. T. Felix NOTE: std::hint blackbox -use std::time::Duration; -use tokio::runtime::Builder; - -/// 섀넌 엔트로피(shannon entropy) 계산 함수 -/// -/// 주어진 바이트 시퀀스의 정보량을 측정합니다. -/// 대규모 및 군사급 보안 시스템에서는 난수 스트림의 엔트로피가 8.0에 수렴하는지 실시간으로 -/// 평가(security evaluation)해야해서 해당 연산의 처리량(throughput) 측정은 필수적입니다. -fn compute_shannon_entropy(data: &[u8]) -> f64 { - let mut counts = [0usize; 256]; - for &byte in data { - counts[byte as usize] += 1; - } - - let mut entropy = 0.0; - let len = data.len() as f64; - - for &count in &counts { - if count > 0 { - let probability = count as f64 / len; - entropy -= probability * probability.log2(); - } - } - - entropy -} - -/// 양자 난수 추출 처리량(throughput) 벤치마크 -/// -/// 외부 네트워크(curl) 호출 및 무의존성(zero-dependency) json 파서의 병목을 분석합니다. -fn bench_qrng_throughput(c: &mut Criterion) { - let mut group = c.benchmark_group("QRNG_Throughput"); - - // 네트워크 지연(network latency)으로 인한 벤치마크 타임아웃을 방지하기 위해 - // 샘플 크기(sample size)를 축소하고 측정 시간을 연장합니다. - group.sample_size(10); - group.measurement_time(Duration::from_secs(20)); - - // 전략 패턴(strategy pattern)의 초기 상태 구성에 필요한 32바이트(키) 및 최대 허용치 1024바이트 - let lengths = [32usize, 256, 1024]; - - for &len in &lengths { - group.throughput(Throughput::Bytes(len as u64)); - group.bench_with_input( - BenchmarkId::new("fetch_secure_bytes", len), - &len, - |b, &size| { - let rt = Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio 런타임 생성 실패"); - b.iter(|| { - // black_box를 통해 컴파일러의 데드 코드 제거(dead code elimination) 최적화 방지 - let result = rt.block_on(AnuQrngClient::fetch_secure_bytes(black_box(size))); - - // 네트워크 실패 시 벤치마크 패닉을 방지하고 에러 코드를 반환하도록 처리 - if let Ok(buffer) = result { - black_box(buffer); - } - }); - }, - ); - } - - group.finish(); -} - -/// 실시간 보안성 평가(security evaluation) 연산 오버헤드 벤치마크 -fn bench_qrng_security_evaluation(c: &mut Criterion) { - let mut group = c.benchmark_group("QRNG_Security_Evaluation"); - - let sizes = [32usize, 256, 1024]; - - for &size in &sizes { - // 실제 API 호출로 인한 네트워크 병목을 배제하고, 순수 엔트로피 연산의 - // CPU 처리량(cpu throughput)만을 정밀하게 평가하기 위해 더미(dummy) 데이터를 구성합니다. - let mock_data: Vec = (0..size).map(|i| (i % 256) as u8).collect(); - - group.throughput(Throughput::Bytes(size as u64)); - group.bench_with_input(BenchmarkId::new("shannon_entropy", size), &size, |b, _| { - b.iter(|| { - let entropy = compute_shannon_entropy(black_box(&mock_data)); - black_box(entropy); - }); - }); - } - - group.finish(); -} - -criterion_group!( - benches, - bench_qrng_throughput, - bench_qrng_security_evaluation -); -criterion_main!(benches); diff --git a/crypto/rng/src/anu_qrng.rs b/crypto/rng/src/anu_qrng.rs deleted file mode 100644 index c0e0b3e..0000000 --- a/crypto/rng/src/anu_qrng.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! ANU QRNG API(Streaming) -//! -//! # Secure Warning -//! TLS통신을 통해 작업을 수행하기 때문에 -//! 네트워크 지연 등의 문제가 있습니다. -//! -//! # Author -//! Q. T. Felix - -use crate::base_rng::RngError; -use entlib_native_core_secure::secure_buffer::SecureBuffer; -use std::process::Stdio; -use std::str; -use std::vec::Vec; -use tokio::process::Command; - -pub struct AnuQrngClient; - -impl AnuQrngClient { - pub async fn fetch_secure_bytes(length: usize) -> Result { - if !(1..=1024).contains(&length) { - return Err(RngError::ParseError); - } - - let url = format!( - "https://api.quantumnumbers.anu.edu.au?length={}&type=uint8", - length - ); - - let output = Command::new("curl") - .arg("-sS") // silent (진행률 숨김) + 오류 메시지 표시 - .arg("-f") // HTTP 오류 시 실패 - .arg("-L") // 리다이렉트 추적 - .arg("-X") - .arg("GET") - .arg("-H") - .arg(format!( - "x-api-key:{}", - std::env::var("QRNG_ANU_KEY").expect("QRNG_ANU_KEY must be set") - )) - .arg("--connect-timeout") - .arg("10") // 연결 타임아웃: 10초 - .arg("--max-time") - .arg("30") // 전체 요청 타임아웃: 30초 - .arg(url) - .stdin(Stdio::null()) // JVM FFI 컨텍스트에서의 stdin 상속 차단 - .output() - .await - .map_err(|e| RngError::NetworkFailure(format!("curl 실행 실패: {}", e)))?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(RngError::NetworkFailure(format!( - "HTTP 실패 (code: {}) | stderr: {}", - output.status, - stderr.trim() - ))); - } - - let response_str = str::from_utf8(&output.stdout).map_err(|_| RngError::ParseError)?; - - let bytes = Self::parse_json_data(response_str)?; - - Ok(SecureBuffer { inner: bytes }) - } - - /// 공백 및 포맷 변경에 강건하게 대응하는 개선된 슬라이싱 파서 - pub fn parse_json_data(json: &str) -> Result, RngError> { - let data_key = "\"data\""; - - // "data" 키의 시작 위치 탐색 - let key_idx = json.find(data_key).ok_or(RngError::ParseError)?; - - // 키 이후부터 첫 번째 '[' 탐색 - let start_bracket = json[key_idx..].find('[').ok_or(RngError::ParseError)? + key_idx + 1; - - // '[' 이후부터 첫 번째 ']' 탐색 - let end_bracket = json[start_bracket..] - .find(']') - .ok_or(RngError::ParseError)? - + start_bracket; - - let data_part = &json[start_bracket..end_bracket]; - let mut bytes = Vec::new(); - - for val_str in data_part.split(',') { - let trimmed = val_str.trim(); - if !trimmed.is_empty() { - let val = trimmed.parse::().map_err(|_| RngError::ParseError)?; - bytes.push(val); - } - } - - Ok(bytes) - } -} diff --git a/crypto/rng/src/base_rng.rs b/crypto/rng/src/base_rng.rs index 2603b5b..f2075f6 100644 --- a/crypto/rng/src/base_rng.rs +++ b/crypto/rng/src/base_rng.rs @@ -1,192 +1 @@ -//! 아키텍처 레벨 진난수 생성기 및 보안 버퍼 통합 모듈 -//! -//! # Author -//! Q. T. Felix -//! -//! # Security Warning -//! 본 구현체는 외부 크레이트 없이 프로세서의 하드웨어 난수 생성기에 의존합니다. -//! `Intel` 및 `ARM` 등 제조사의 하드웨어 백도어(hardware backdoor) 가능성을 -//! 배제할 수 없는 경우, 이 출력값을 시드(seed)로 삼아 스트림 암호(stream cipher) 또는 -//! 해시 기반 결정론적 난수 생성기(hash-drbg)와 결합하는 혼합 과정이 추가로 요구됩니다. - -use core::arch::asm; -use core::ptr::{copy_nonoverlapping, write_unaligned, write_volatile}; -use core::sync::atomic::compiler_fence; -use entlib_native_core_secure::secure_buffer::SecureBuffer; -use std::sync::atomic::Ordering; -use std::vec::Vec; - -/// 난수 생성 중 발생할 수 있는 시스템 및 보안 에러를 정의한 열거형입니다. -#[derive(Debug)] -pub enum RngError { - /// 프로세서가 필요한 하드웨어 난수 명령어를 지원하지 않습니다. - UnsupportedHardware, - /// 엔트로피 풀 고갈 또는 하드웨어 응답에 실패했습니다. - EntropyDepletion, - /// 양자 난수 네트워크 요청(curl)이 실패했습니다. - NetworkFailure(String), - /// 양자 난수 API 응답 데이터 파싱에 실패했습니다. - ParseError, - /// 잘못된 포인터를 참조합니다. - InvalidPointer, - /// 잘못된 파라미터를 전달했거나 받았습니다. - InvalidParameter, -} - -/// 요청한 길이만큼의 진난수를 포함하는 보안 버퍼를 반환합니다. -/// -/// 하드웨어 명령어를 통해 추출된 엔트로피를 직접 할당하며, -/// 지원되지 않는 아키텍처의 경우 컴파일 타임 에러를 발생시킵니다. -pub fn generate_hardware_random_bytes(len: usize) -> Result { - let mut buffer: Vec = Vec::with_capacity(len); - - unsafe { - let ptr: *mut _ = buffer.as_mut_ptr(); - let mut offset = 0; - - // 8바이트(u64) 단위로 난수를 채움 - while offset + 8 <= len { - let rand_val = get_hw_random_u64()?; - write_unaligned((ptr.add(offset)) as *mut u64, rand_val); - offset += 8; - } - - // 나머지 바이트 처리 - if offset < len { - let rand_val = get_hw_random_u64()?; - let bytes = rand_val.to_ne_bytes(); - copy_nonoverlapping(bytes.as_ptr(), ptr.add(offset), len - offset); - } - - // 모든 쓰기가 성공적으로 완료된 후 길이 설정 - buffer.set_len(len); - } - - Ok(SecureBuffer { inner: buffer }) -} - -/// `x86_64` 아키텍처에서 진난수(`u64`)를 추출합니다. -/// -/// NIST SP 800-90b 규격을 만족하는 `rdseed` 명령어를 우선적으로 시도하며, -/// 실패 시 `rdrand` 명령어로 폴백합니다. -#[cfg(target_arch = "x86_64")] -#[inline(always)] -fn get_hw_random_u64() -> Result { - // 런타임 기능 탐지 - if !std::arch::is_x86_feature_detected!("rdseed") { - return Err(RngError::UnsupportedHardware); - } - - let mut val: u64 = 0; - let mut success: u8; - let max_retries = 1000; // 무한 루프 방지 - - unsafe { - for _ in 0..max_retries { - asm!( - "rdseed {0}", - "setc {1}", - out(reg) val, - out(reg_byte) success, - options(nomem, nostack) - ); - if success != 0 { - return Ok(val); - } - core::hint::spin_loop(); - } - } - - // rdseed 실패 시 강등(rdrand)하지 않고 명시적 에러 반환 - Err(RngError::EntropyDepletion) -} - -/// 기존에 할당된 보안 버퍼(`secure_buffer`)에 하드웨어 진난수를 안전하게 주입합니다. -/// -/// ffi 계층을 통해 자바(java) 측에서 전달된 메모리 세그먼트(memory segment)를 -/// 재사용할 때 발생할 수 있는 힙 메모리 파편화를 방지하며, 난수 생성 중 -/// 스택에 복사된 임시 레지스터 값들을 함수 종료 전 강제로 소거(zeroize)합니다. -pub fn next_generate(buffer: &mut SecureBuffer) -> Result<(), RngError> { - let len = buffer.inner.len(); - if len == 0 { - return Ok(()); - } - - let ptr = buffer.inner.as_mut_ptr(); - let mut offset = 0; - - unsafe { - // 8바이트(u64) 단위 난수 주입 - while offset + 8 <= len { - let mut rand_val = get_hw_random_u64()?; - write_unaligned((ptr.add(offset)) as *mut u64, rand_val); - offset += 8; - - // 스택에 남은 rand_val 값을 즉각 소거하여 잔여 데이터 유출 방지 - write_volatile(&mut rand_val, 0); - } - - // 8바이트로 나누어떨어지지 않는 잔여 바이트 처리 - if offset < len { - let mut rand_val = get_hw_random_u64()?; - let mut bytes = rand_val.to_ne_bytes(); - let remain = len - offset; - - copy_nonoverlapping(bytes.as_ptr(), ptr.add(offset), remain); - - // 잔여 바이트 배열 및 원본 난수 변수 스택 소거 - for byte in bytes.iter_mut() { - write_volatile(byte, 0); - } - write_volatile(&mut rand_val, 0); - } - - // 컴파일러의 dce(dead code elimination) 및 명령어 재배치 최적화 방지 - compiler_fence(Ordering::SeqCst); - } - - Ok(()) -} - -/// `aarch64` 아키텍처에서 진난수(`u64`)를 추출합니다. -/// -/// `armv8.5-a` 이상에서 지원되는 `rndr` 레지스터를 통해 난수를 추출합니다. -#[cfg(target_arch = "aarch64")] -#[inline(always)] -fn get_hw_random_u64() -> Result { - // aarch64의 rndr 기능 탐지 (libc::getauxval 등을 통한 HWCAP 확인 권장) - // 현재 예시에서는 논리적 흐름을 보여줍니다. - /* if !has_rndr_feature() { - return Err(RngError::UnsupportedHardware); - } - */ - - let mut val: u64; - let mut success: u64; - let max_retries = 1000; // 무한 루프 방지 - - unsafe { - for _ in 0..max_retries { - asm!( - "mrs {0}, s3_3_c2_c4_0", - "cset {1}, ne", - out(reg) val, - out(reg) success, - options(nomem, nostack) - ); - if success != 0 { - return Ok(val); - } - core::hint::spin_loop(); - } - } - - Err(RngError::EntropyDepletion) -} - -/// 지원되지 않는 아키텍처에 대한 컴파일을 차단합니다. -#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] -#[inline(always)] -fn get_hw_random_u64() -> Result { - Err(RngError::UnsupportedHardware); -} +// TODO: 하드웨어 RNG (RDRAND/RDSEED 등) 기반 베어 RNG 구현 예정 diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 4bd6aa2..5980920 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -1,3 +1,2 @@ -pub mod anu_qrng; -pub mod base_rng; -pub mod mixed; +mod base_rng; +mod mixed; diff --git a/crypto/rng/src/mixed.rs b/crypto/rng/src/mixed.rs index 72dd4e7..5bed462 100644 --- a/crypto/rng/src/mixed.rs +++ b/crypto/rng/src/mixed.rs @@ -1,176 +1 @@ -//! 하드웨어 엔트로피 혼합 및 확장 모듈 -//! -//! # Author -//! Q. T. Felix -//! -//! # Security -//! 프로세서 레벨의 진난수 생성기(trng) 출력값을 직접 사용하지 않고, -//! `chacha20` 스트림 암호(stream cipher)의 코어 블록 함수를 통해 -//! 비선형적으로 혼합(mixing)합니다. -//! 내부 연산에 사용되는 모든 레지스터 및 상태 배열은 함수 종료 또는 -//! 객체 소멸 시 스택에서 강제 소거(zeroize)됩니다. -//! -//! # Note -//! 이 기능은 곧 `chacha20` 모듈로 차별화됩니다. - -use crate::anu_qrng::AnuQrngClient; -use crate::base_rng::{RngError, generate_hardware_random_bytes}; -use core::ptr::{copy_nonoverlapping, write_volatile}; -use core::sync::atomic::{Ordering, compiler_fence}; -use entlib_native_core_secure::secure_buffer::SecureBuffer; - -/// 상수 시간(constant-time) 연산을 보장하는 chacha20 쿼터 라운드(quarter round) -macro_rules! quarter_round { - ($a:expr, $b:expr, $c:expr, $d:expr) => { - $a = $a.wrapping_add($b); - $d ^= $a; - $d = $d.rotate_left(16); - $c = $c.wrapping_add($d); - $b ^= $c; - $b = $b.rotate_left(12); - $a = $a.wrapping_add($b); - $d ^= $a; - $d = $d.rotate_left(8); - $c = $c.wrapping_add($d); - $b ^= $c; - $b = $b.rotate_left(7); - }; -} - -/// 엔트로피 소스(entropy source)를 결정하는 전략(strategy) 열거형 -pub enum EntropyStrategy { - /// 로컬 프로세서의 하드웨어 난수 생성기(`rdseed`, `rndr` 등)를 사용합니다. - LocalHardware, - /// ANU 양자 난수 API를 통해 진공 양자 요동(quantum vacuum fluctuations) 데이터를 가져옵니다. - QuantumNetwork, -} - -/// 혼합 난수 생성기(mixed rng) 구조체 -/// -/// 하드웨어 진난수를 시드(seed)로 사용하여 512-비트(bit) 상태를 초기화합니다. -pub struct MixedRng { - state: [u32; 16], -} - -impl MixedRng { - /// 지정된 엔트로피 전략을 사용하여 새로운 혼합 난수 생성기를 인스턴스화합니다. - pub fn new(strategy: EntropyStrategy) -> Result { - // 전략 패턴(strategy pattern)에 따른 시드(seed) 추출 분기 - let (hw_key, hw_nonce) = match strategy { - EntropyStrategy::LocalHardware => ( - generate_hardware_random_bytes(32)?, - generate_hardware_random_bytes(12)?, - ), - EntropyStrategy::QuantumNetwork => { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| { - RngError::NetworkFailure(format!("tokio 런타임 빌드 실패: {}", e)) - })?; - rt.block_on(async { - let key = AnuQrngClient::fetch_secure_bytes(32).await?; - let nonce = AnuQrngClient::fetch_secure_bytes(12).await?; - Ok::<(SecureBuffer, SecureBuffer), RngError>((key, nonce)) - })? - } - }; - - let mut state = [0u32; 16]; - - // chacha20 상수(constants) 배열 초기화 - state[0] = 0x61707865; - state[1] = 0x3320646e; - state[2] = 0x79622d32; - state[3] = 0x6b206574; - - unsafe { - let key_ptr = hw_key.inner.as_ptr() as *const u32; - let nonce_ptr = hw_nonce.inner.as_ptr() as *const u32; - - for i in 0..8 { - state[4 + i] = core::ptr::read_unaligned(key_ptr.add(i)); - } - for i in 0..3 { - state[13 + i] = core::ptr::read_unaligned(nonce_ptr.add(i)); - } - } - - // 블록 카운터(block counter) - state[12] = 0; - - // hw_key와 hw_nonce는 SecureBuffer의 Drop 트레이트 구현에 의해 - // 스코프를 벗어날 때 자동으로 데이터 소거(zeroize)가 수행됨 - - Ok(Self { state }) - } - - /// 요청된 길이(len)만큼의 혼합 난수를 담은 보안 버퍼(secure buffer)를 반환합니다. - pub fn generate(&mut self, len: usize) -> Result { - let mut buffer: Vec = Vec::with_capacity(len); - let mut offset = 0; - let mut block = [0u32; 16]; - - unsafe { - let ptr: *mut _ = buffer.as_mut_ptr(); - - while offset < len { - self.chacha_block(&mut block); - - let remain = len - offset; - let copy_len = if remain < 64 { remain } else { 64 }; - - copy_nonoverlapping(block.as_ptr() as *const u8, ptr.add(offset), copy_len); - - offset += copy_len; - self.state[12] = self.state[12].wrapping_add(1); - } - - buffer.set_len(len); - - // 임시 블록 스택 소거(zeroize) - for word in block.iter_mut() { - write_volatile(word, 0); - } - compiler_fence(Ordering::SeqCst); - } - - Ok(SecureBuffer { inner: buffer }) - } - - /// 단일 chacha20 블록을 연산합니다. - #[inline(always)] - fn chacha_block(&self, out: &mut [u32; 16]) { - let mut x = self.state; - - // 20 라운드(10 번의 이중 라운드) - for _ in 0..10 { - // 열(column) 라운드 - quarter_round!(x[0], x[4], x[8], x[12]); - quarter_round!(x[1], x[5], x[9], x[13]); - quarter_round!(x[2], x[6], x[10], x[14]); - quarter_round!(x[3], x[7], x[11], x[15]); - // 대각선(diagonal) 라운드 - quarter_round!(x[0], x[5], x[10], x[15]); - quarter_round!(x[1], x[6], x[11], x[12]); - quarter_round!(x[2], x[7], x[8], x[13]); - quarter_round!(x[3], x[4], x[9], x[14]); - } - - for i in 0..16 { - out[i] = x[i].wrapping_add(self.state[i]); - } - } -} - -impl Drop for MixedRng { - fn drop(&mut self) { - // 객체 소멸 시 상태(state) 배열 강제 소거 - for word in self.state.iter_mut() { - unsafe { - write_volatile(word, 0); - } - } - compiler_fence(Ordering::SeqCst); - } -} +// TODO: 하드웨어 RNG + CSPRNG 혼합 방식 구현 예정 diff --git a/crypto/rng/tests/anu_qrng_test.rs b/crypto/rng/tests/anu_qrng_test.rs deleted file mode 100644 index ee4fbe2..0000000 --- a/crypto/rng/tests/anu_qrng_test.rs +++ /dev/null @@ -1,88 +0,0 @@ -#[cfg(test)] -mod tests { - use entlib_native_rng::anu_qrng::*; - use entlib_native_rng::base_rng::RngError; - use std::collections::HashSet; - - #[tokio::test] - async fn test_fetch_secure_bytes_valid() { - // 정상적인 길이의 난수 요청이 보안 버퍼(secure buffer)에 올바르게 적재되는지 검증 - let target_length = 32; - - match AnuQrngClient::fetch_secure_bytes(target_length).await { - Ok(buffer) => { - assert_eq!( - buffer.inner.len(), - target_length, - "요청한 길이와 반환된 버퍼의 크기가 일치하지 않습니다." - ); - } - Err(RngError::NetworkFailure(_)) | Err(RngError::ParseError) => { - // 폐쇄망 환경이거나 ANU 서버의 정책 변경으로 인한 통신 불가 시 테스트를 안전하게 우회 - println!( - "Skipping network test: ANU QRNG service is currently unreachable or format changed." - ); - } - Err(e) => { - panic!( - "예기치 않은 보안 모듈 에러(unexpected security module error): {:?}", - e - ); - } - } - } - - #[tokio::test] - async fn test_fetch_secure_bytes_invalid_bounds() { - // 버퍼 오버플로 및 비정상적 메모리 할당을 방지하기 위한 경계값 검증 - // api가 허용하는 1~1024 범위를 벗어난 요청 시 명시적 에러반환 - assert!(AnuQrngClient::fetch_secure_bytes(0).await.is_err()); - assert!(AnuQrngClient::fetch_secure_bytes(1025).await.is_err()); - } - - #[test] - fn test_parse_json_data_stability() { - // 외부 라이브러리 없이 구현된 슬라이싱(slicing) 기반 파서의 정상 작동 검증 - let mock_json = - r#"{"type":"uint8","length":5,"data":[12, 255, 0, 128, 42],"success":true}"#; - let result = AnuQrngClient::parse_json_data(mock_json); - - assert!(result.is_ok()); - let bytes = result.unwrap(); - assert_eq!(bytes, vec![12, 255, 0, 128, 42]); - } - - #[test] - fn test_parse_json_data_malformed() { - // 손상된 페이로드(malformed payload) 주입 시 시스템이 패닉(panic)에 빠지지 않고 - // 안전하게 에러(error)를 반환하는지 검증 - let missing_data_json = r#"{"type":"uint8","length":5,"success":true}"#; - assert!(AnuQrngClient::parse_json_data(missing_data_json).is_err()); - - let unclosed_bracket_json = r#"{"type":"uint8","length":5,"data":[12, 255, 0"#; - assert!(AnuQrngClient::parse_json_data(unclosed_bracket_json).is_err()); - - let invalid_type_json = r#"{"type":"uint8","length":1,"data":[NaN],"success":false}"#; - assert!(AnuQrngClient::parse_json_data(invalid_type_json).is_err()); - } - - #[tokio::test] - async fn test_randomness_basic_entropy() { - // 생성된 양자 난수(quantum random number)의 기본적인 무작위성(randomness)을 평가 - // 추출된 바이트 시퀀스 내의 고유값(unique values) 개수를 확인하여, 통신 오류로 인해 - // 0으로만 채워진 배열(zeroed array)이 반환되는 치명적 보안 결함을 방지함 - let len = 100; - if let Ok(buffer) = AnuQrngClient::fetch_secure_bytes(len).await { - let mut unique_values = HashSet::new(); - for &byte in &buffer.inner { - unique_values.insert(byte); - } - - // 100바이트 표본에서 이론적 엔트로피 $H(X)$가 0에 수렴하는 경우(동일한 값만 반복)를 실패로 간주합니다. - assert!( - unique_values.len() > 10, - "추출된 난수의 엔트로피가 보안 요구사항을 충족하지 못합니다." - ); - } - } -} diff --git a/crypto/rng/tests/hw_based_rng_test.rs b/crypto/rng/tests/hw_based_rng_test.rs deleted file mode 100644 index e0b1a3e..0000000 --- a/crypto/rng/tests/hw_based_rng_test.rs +++ /dev/null @@ -1,105 +0,0 @@ -#[cfg(test)] -mod tests { - use entlib_native_core_secure::secure_buffer::SecureBuffer; - use entlib_native_rng::base_rng::{RngError, generate_hardware_random_bytes, next_generate}; - - #[test] - fn test_standard_next_generate_random_bytes() { - let mut buffer = SecureBuffer { - inner: vec![0u8; 1024], - }; - match next_generate(&mut buffer) { - Ok(_r) => { - let res: String = buffer - .inner - .to_vec() - .iter() - .map(|b| format!("{:X?}", b)) - .collect(); - println!("{}", res); - } - Err(_e) => { - panic!("Failed"); - } - } - } - - #[test] - fn test_generate_hardware_random_bytes_length() { - // 다양한 길이의 난수 생성 요청 테스트 - let lengths = [0, 1, 8, 16, 32, 1024]; - - for &len in &lengths { - match generate_hardware_random_bytes(len) { - Ok(buffer) => { - // 생성된 버퍼의 길이가 요청한 길이와 일치하는지 확인 - assert_eq!( - buffer.inner.len(), - len, - "Buffer length mismatch for requested length: {}", - len - ); - } - Err(RngError::UnsupportedHardware) => { - // 하드웨어 미지원 환경에서는 테스트 스킵 (CI 환경 등) - println!( - "Skipping test for length {}: Hardware RNG not supported", - len - ); - } - Err(e) => { - panic!( - "Failed to generate random bytes for length {}: {:?}", - len, e - ); - } - } - } - } - - #[test] - fn test_generate_hardware_random_bytes_randomness() { - // 생성된 난수의 무작위성 (기본적인 중복 검사) - // NOTE: 엄밀한 통계적 검증은 아니며, 연속 호출 시 동일한 값이 나오지 않는지 확인하는겁니다 - let len = 32; - - let buf1 = match generate_hardware_random_bytes(len) { - Ok(b) => b, - Err(RngError::UnsupportedHardware) => return, // 하드웨어 미지원 시 스킵 - Err(e) => panic!("First generation failed: {:?}", e), - }; - - let buf2 = match generate_hardware_random_bytes(len) { - Ok(b) => b, - Err(RngError::UnsupportedHardware) => return, // 하드웨어 미지원 시 스킵 - Err(e) => panic!("Second generation failed: {:?}", e), - }; - - // 두 번의 호출 결과가 완전히 동일할 확률은 극히 낮음 - assert_ne!( - buf1.inner, buf2.inner, - "Two consecutive random generations produced identical output" - ); - } - - #[test] - fn test_generate_hardware_random_bytes_non_zero() { - // 생성된 난수가 모두 0이 아닌지 확인 (하드웨어 오류 감지) - let len = 1024; - match generate_hardware_random_bytes(len) { - Ok(buffer) => { - let is_all_zero = buffer.inner.iter().all(|&x| x == 0); - assert!( - !is_all_zero, - "Generated random bytes are all zero, possible hardware failure" - ); - } - Err(RngError::UnsupportedHardware) => { - println!("Skipping non-zero test: Hardware RNG not supported"); - } - Err(e) => { - panic!("Failed to generate random bytes: {:?}", e); - } - } - } -} diff --git a/crypto/rng/tests/mixed_rng_test.rs b/crypto/rng/tests/mixed_rng_test.rs deleted file mode 100644 index d210d3f..0000000 --- a/crypto/rng/tests/mixed_rng_test.rs +++ /dev/null @@ -1,157 +0,0 @@ -#[cfg(test)] -mod tests { - use entlib_native_rng::base_rng::RngError; - use entlib_native_rng::mixed::{EntropyStrategy, MixedRng}; - - /// 등록된 모든 엔트로피 전략(entropy strategy)에 대해 동일한 테스트를 수행하기 위한 헬퍼 함수입니다. - fn run_with_strategies(test_logic: F) - where - F: Fn(EntropyStrategy, &str), - { - // 로컬 프로세서 하드웨어 난수 기반(trng) - test_logic(EntropyStrategy::LocalHardware, "LocalHardware"); - // 외부 양자 난수 네트워크 통신 기반(qrng) - test_logic(EntropyStrategy::QuantumNetwork, "QuantumNetwork"); - } - - #[test] - fn test_mixed_rng_initialization() { - // MixedRng 초기화 및 의존성 주입 테스트 - run_with_strategies(|strategy, strategy_name| { - match MixedRng::new(strategy) { - Ok(mut rng) => { - // 초기화 성공 시, 32바이트(byte) 난수 생성을 통한 내부 상태 무결성 확인 - let result = rng.generate(32); - assert!( - result.is_ok(), - "[{}] Failed to generate random bytes after initialization", - strategy_name - ); - } - Err(RngError::UnsupportedHardware) => { - println!( - "[{}] Skipping initialization test: Hardware RNG not supported", - strategy_name - ); - } - Err(RngError::NetworkFailure(_)) | Err(RngError::ParseError) => { - // 오프라인 환경 또는 방화벽에 의한 네트워크 차단 시 테스트 우회(skip) - println!( - "[{}] Skipping initialization test: Network or parsing unavailable", - strategy_name - ); - } - Err(e) => { - panic!("[{}] Failed to initialize MixedRng: {:?}", strategy_name, e); - } - } - }); - } - - #[test] - fn test_mixed_rng_generation_length() { - // 다양한 길이의 블록(block) 및 잔여 바이트 생성 테스트 - let lengths = [0, 1, 16, 64, 128, 1024]; - - run_with_strategies(|strategy, strategy_name| match MixedRng::new(strategy) { - Ok(mut rng) => { - for &len in &lengths { - match rng.generate(len) { - Ok(buffer) => { - assert_eq!( - buffer.inner.len(), - len, - "[{}] Buffer length mismatch for requested length: {}", - strategy_name, - len - ); - } - Err(e) => { - panic!( - "[{}] Failed to generate random bytes for length {}: {:?}", - strategy_name, len, e - ); - } - } - } - } - Err(RngError::UnsupportedHardware) - | Err(RngError::NetworkFailure(_)) - | Err(RngError::ParseError) => { - println!( - "[{}] Skipping length test: Dependency unavailable", - strategy_name - ); - } - Err(e) => { - panic!("[{}] Failed to initialize MixedRng: {:?}", strategy_name, e); - } - }); - } - - #[test] - fn test_mixed_rng_randomness() { - // 생성된 난수 스트림(random stream)의 멱등성(idempotence) 및 중복 검사 - run_with_strategies(|strategy, strategy_name| { - match MixedRng::new(strategy) { - Ok(mut rng) => { - let len = 32; - let buf1 = rng.generate(len).expect("First generation failed"); - let buf2 = rng.generate(len).expect("Second generation failed"); - - // 연속 호출 시 동일한 값이 출력되지 않음을 증명(내부 상태가 정상 갱신됨을 의미) - assert_ne!( - buf1.inner, buf2.inner, - "[{}] Two consecutive random generations produced identical output", - strategy_name - ); - } - Err(RngError::UnsupportedHardware) - | Err(RngError::NetworkFailure(_)) - | Err(RngError::ParseError) => { - println!( - "[{}] Skipping randomness test: Dependency unavailable", - strategy_name - ); - } - Err(e) => { - panic!("[{}] Failed to initialize MixedRng: {:?}", strategy_name, e); - } - } - }); - } - - #[test] - fn test_mixed_rng_block_counter_increment() { - // chacha20 코어(core) 블록 카운터(block counter) 증가에 따른 출력 전이 검증 - run_with_strategies(|strategy, strategy_name| { - match MixedRng::new(strategy) { - Ok(mut rng) => { - // 128바이트 생성 (64바이트 단위 2개 블록) - let len = 128; - let buffer = rng.generate(len).expect("Generation failed"); - - let (first_block, second_block) = buffer.inner.split_at(64); - - // 블록 카운터가 올바르게 증가햇다면 두 블록의 암호학적 출력은 독립적이어야 함 - assert_ne!( - first_block, second_block, - "[{}] Consecutive blocks are identical, counter might not be incrementing", - strategy_name - ); - } - Err(RngError::UnsupportedHardware) - | Err(RngError::NetworkFailure(_)) - | Err(RngError::ParseError) => { - println!( - "[{}] Skipping block counter test: Dependency unavailable", - strategy_name - ); - } - Err(e) => { - panic!("[{}] Failed to initialize MixedRng: {:?}", strategy_name, e); - } - } - }); - } -} diff --git a/crypto/sha2/Cargo.toml b/crypto/sha2/Cargo.toml index 02e7a2b..815e9c6 100644 --- a/crypto/sha2/Cargo.toml +++ b/crypto/sha2/Cargo.toml @@ -9,12 +9,13 @@ license.workspace = true crate-type = ["cdylib", "rlib"] [dependencies] +entlib-native-secure-buffer.workspace = true +entlib-native-constant-time.workspace = true [dev-dependencies] -criterion = { version = "0.8.2", features = ["html_reports"] } -entlib-native-core-secure.workspace = true -entlib-native-rng.workspace = true +#criterion = { version = "0.8.2", features = ["html_reports"] } +#entlib-native-rng.workspace = true -[[bench]] -name = "sha2_bench" -harness = false \ No newline at end of file +#[[bench]] +#name = "sha2_bench" +#harness = false \ No newline at end of file diff --git a/crypto/sha2/benches/sha2_bench.rs b/crypto/sha2/benches/sha2_bench.rs deleted file mode 100644 index 7e6d2a5..0000000 --- a/crypto/sha2/benches/sha2_bench.rs +++ /dev/null @@ -1,123 +0,0 @@ -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; -use entlib_native_core_secure::secure_buffer::SecureBuffer; -use entlib_native_rng::base_rng::next_generate; -use entlib_native_sha2::api::*; -use std::hint::black_box; - -fn sha2_benchmark(c: &mut Criterion) { - // 메모리 할당 및 진난수 주입은 함수 진입점에서 단 1회만 수행하여 오버헤드를 차단합니다. - let mut data = SecureBuffer { - inner: vec![0u8; 1024], - }; - next_generate(&mut data).expect("hardware rng failure"); - let data_slice = data.inner.as_slice(); - - // 단일 벤치마크 그룹 생성 - let mut group = c.benchmark_group("throughput/sha2"); - - // - // SHA224 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA224 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA224::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA224 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA224::new(); - for _ in 0..1024 { - // 1MiB 페이로드 처리 - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - // - // SHA256 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA256 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA256::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA256 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA256::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - // - // SHA384 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA384 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA384::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA384 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA384::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - // - // SHA512 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA512 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA512::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA512 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA512::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.finish(); -} - -criterion_group!(benches, sha2_benchmark); -criterion_main!(benches); diff --git a/crypto/sha2/src/api.rs b/crypto/sha2/src/api.rs index e59fb2d..f058077 100644 --- a/crypto/sha2/src/api.rs +++ b/crypto/sha2/src/api.rs @@ -1,4 +1,5 @@ use crate::{Sha256State, Sha512State}; +use entlib_native_secure_buffer::SecureBuffer; // // SHA224 - start @@ -16,7 +17,7 @@ impl SHA224 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize() } } @@ -46,7 +47,7 @@ impl SHA256 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize() } } @@ -76,7 +77,7 @@ impl SHA384 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize() } } @@ -106,7 +107,7 @@ impl SHA512 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize() } } diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index bc602f8..74ec370 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -4,11 +4,12 @@ mod sha2_512; use core::ptr::write_volatile; use core::sync::atomic::{Ordering, compiler_fence}; +use entlib_native_secure_buffer::SecureBuffer; /// SHA-256 및 SHA-224를 위한 32비트 내부 상태 구조체(internal state structure) pub(crate) struct Sha256State { pub(crate) state: [u32; 8], - pub(crate) buffer: [u8; 64], + pub(crate) buffer: SecureBuffer, pub(crate) buffer_len: usize, pub(crate) total_len: u64, pub(crate) is_224: bool, @@ -17,7 +18,7 @@ pub(crate) struct Sha256State { /// SHA-512 및 SHA-384를 위한 64비트 내부 상태 구조체(internal state structure) pub(crate) struct Sha512State { pub(crate) state: [u64; 8], - pub(crate) buffer: [u8; 128], + pub(crate) buffer: SecureBuffer, pub(crate) buffer_len: usize, pub(crate) total_len: u128, pub(crate) is_384: bool, @@ -33,11 +34,8 @@ macro_rules! impl_zeroize_drop { write_volatile(&mut self.state[i], 0); } } - for i in 0..$buf_len { - unsafe { - write_volatile(&mut self.buffer[i], 0); - } - } + // Q. T. Felix NOTE: 이 Drop 트레이트가 호출되는 시점에 SecureBuffer 구조체의 Drop 트레이트도 호출되지 않나? + // 만약 그렇지 않다면, Drop 트레이트를 이 곳에서 수동으로 직접 호출해주어야 함. unsafe { write_volatile(&mut self.buffer_len, 0); write_volatile(&mut self.total_len, 0); diff --git a/crypto/sha2/src/sha2_256.rs b/crypto/sha2/src/sha2_256.rs index 420a406..dea9214 100644 --- a/crypto/sha2/src/sha2_256.rs +++ b/crypto/sha2/src/sha2_256.rs @@ -1,6 +1,8 @@ use crate::Sha256State; use core::ptr::write_volatile; use core::sync::atomic::{Ordering, compiler_fence}; +use entlib_native_constant_time::traits::{ConstantTimeEq, ConstantTimeSelect}; +use entlib_native_secure_buffer::SecureBuffer; const SHA_256_K: [u32; 64] = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, @@ -28,7 +30,7 @@ impl Sha256State { }; Self { state, - buffer: [0; 64], + buffer: SecureBuffer::new_owned(64).expect("SecureBuffer allocate failed"), buffer_len: 0, total_len: 0, is_224, @@ -113,9 +115,13 @@ impl Sha256State { while i < data.len() { let fill = 64 - self.buffer_len; - let chunk_len = core::cmp::min(data.len() - i, fill); - self.buffer[self.buffer_len..self.buffer_len + chunk_len] + let remain = data.len() - i; + let is_ge = remain.ct_is_ge(&fill); + let chunk_len = usize::ct_select(&fill, &remain, is_ge); + + let buf_slice = self.buffer.as_mut_slice(); + buf_slice[self.buffer_len..self.buffer_len + chunk_len] .copy_from_slice(&data[i..i + chunk_len]); self.buffer_len += chunk_len; @@ -123,47 +129,92 @@ impl Sha256State { if self.buffer_len == 64 { let mut block = [0u8; 64]; - block.copy_from_slice(&self.buffer); + block.copy_from_slice(buf_slice); self.process_block(&block); self.buffer_len = 0; } } } - pub(crate) fn finalize(mut self) -> Vec { - // 1비트 '1' 패딩 추가(append 1 bit) - self.buffer[self.buffer_len] = 0x80; + pub(crate) fn finalize(mut self) -> Result { + let len = self.buffer_len; + self.buffer.as_mut_slice()[len] = 0x80; self.buffer_len += 1; - // 길이가 56바이트를 초과하면 버퍼를 0으로 채우고 새로운 블록 처리 - if self.buffer_len > 56 { - self.buffer[self.buffer_len..64].fill(0); - let mut block = [0u8; 64]; - block.copy_from_slice(&self.buffer); - self.process_block(&block); - self.buffer_len = 0; - } + let needs_extra_block = self.buffer_len.ct_is_ge(&57usize); - // 남은 버퍼 공간을 0으로 채우기 - self.buffer[self.buffer_len..56].fill(0); + let mut block1 = [0u8; 64]; + let buf_slice = self.buffer.as_slice(); + for i in 0..64 { + let is_pad = i.ct_is_ge(&self.buffer_len); + block1[i] = u8::ct_select(&0u8, &buf_slice[i], is_pad); + } - // 최종 64비트 길이를 빅 엔디안 형식으로 추가(append length in big-endian) - self.buffer[56..64].copy_from_slice(&self.total_len.to_be_bytes()); + let total_len_bytes = self.total_len.to_be_bytes(); - let mut block = [0u8; 64]; - block.copy_from_slice(&self.buffer); - self.process_block(&block); + // 추가 블럭이 필요 없는 경우(False), 첫 번째 블럭 마지막에 길이 삽임 + for i in 56..64 { + let orig = block1[i]; + let len_byte = total_len_bytes[i - 56]; + // needs_extra_block이 True면 원래 값 0 유지, False면 len_byte 덮어쓰기 + block1[i] = u8::ct_select(&orig, &len_byte, needs_extra_block); + } + self.process_block(&block1); + let state_after_block1 = self.state; // 첫 번째 블럭 처리 후 상태 저장 + + let mut block2 = [0u8; 64]; + // 두 번째 블럭은 항상 마지막에 길이를 포함하도록 구성 (추가 블럭 필요 시 유효함) + block2[56..64].copy_from_slice(&total_len_bytes); + self.process_block(&block2); + + // 최종 상태 상수-시간 결정 + for (i, &saved) in state_after_block1.iter().enumerate() { + // needs_extra_block이 True면 두 블럭을 모두 거친 self.state 적용 + // 그렇지 않으면 첫 번째 블럭만 거침 state_after_block1 로 롤백 + self.state[i] = u32::ct_select(&self.state[i], &saved, needs_extra_block); + } - let mut digest = Vec::with_capacity(32); - for &s in &self.state { - digest.extend_from_slice(&s.to_be_bytes()); + // 스택에 할당된 임시 데이터 소거 + for b in &mut block1 { + unsafe { + write_volatile(b, 0); + } } + for b in &mut block2 { + unsafe { + write_volatile(b, 0); + } + } + compiler_fence(Ordering::SeqCst); + + // 다이제스트 생성을 위한 SecureBuffer 할당 + let digest_size = if self.is_224 { 28 } else { 32 }; + // 실패 시 &'static str 에러를 반환(?)하여 안전하게 에러 핸들링 + let mut digest_buf = SecureBuffer::new_owned(digest_size)?; + + // 가변 슬라이스를 통해 상태(state) 배열의 값을 다이제스트 버퍼로 전송 + let digest_slice = digest_buf.as_mut_slice(); + for (i, &s) in self.state.iter().enumerate() { + let start = i * 4; + // SHA-224 절단 시점을 넘어선 데이터는 복사하지 않음 + // 알고리즘 모드 선택은 공개 정보라서 분기 허용 + if start >= digest_size { + break; + } + + let bytes = s.to_be_bytes(); + + let max_end = start + 4; + let is_end_ge = max_end.ct_is_ge(&digest_size); + let end = usize::ct_select(&digest_size, &max_end, is_end_ge); + let copy_len = end - start; - if self.is_224 { - digest.truncate(28); + digest_slice[start..end].copy_from_slice(&bytes[..copy_len]); } - // self가 범위를 벗어나면서 Drop 트레이트에 의해 내부 상태가 자동 소거됨 - digest + // 함수 종료 시 `mut self`가 범위를 벗어나 다음 수행 + // 1. self.buffer (SecureBuffer)의 Drop 발동 -> 소거 및 OS 메모리 잠금 해제 + // 2. self.state 등 내부 데이터 파기 + Ok(digest_buf) } } diff --git a/crypto/sha2/src/sha2_512.rs b/crypto/sha2/src/sha2_512.rs index 684debc..61dd235 100644 --- a/crypto/sha2/src/sha2_512.rs +++ b/crypto/sha2/src/sha2_512.rs @@ -1,6 +1,8 @@ use crate::Sha512State; use core::ptr::write_volatile; use core::sync::atomic::{Ordering, compiler_fence}; +use entlib_native_constant_time::traits::{ConstantTimeEq, ConstantTimeSelect}; +use entlib_native_secure_buffer::SecureBuffer; const SHA_512_K: [u64; 80] = [ 0x428a2f98d728ae22, @@ -112,7 +114,7 @@ impl Sha512State { }; Self { state, - buffer: [0; 128], + buffer: SecureBuffer::new_owned(128).expect("SecureBuffer allocate failed"), buffer_len: 0, total_len: 0, is_384, @@ -204,9 +206,13 @@ impl Sha512State { while i < data.len() { let fill = 128 - self.buffer_len; - let chunk_len = core::cmp::min(data.len() - i, fill); - self.buffer[self.buffer_len..self.buffer_len + chunk_len] + let remain = data.len() - i; + let is_ge = remain.ct_is_ge(&fill); + let chunk_len = usize::ct_select(&fill, &remain, is_ge); + + let buf_slice = self.buffer.as_mut_slice(); + buf_slice[self.buffer_len..self.buffer_len + chunk_len] .copy_from_slice(&data[i..i + chunk_len]); self.buffer_len += chunk_len; @@ -214,7 +220,7 @@ impl Sha512State { if self.buffer_len == 128 { let mut block = [0u8; 128]; - block.copy_from_slice(&self.buffer); + block.copy_from_slice(self.buffer.as_slice()); self.process_block(&block); self.buffer_len = 0; } @@ -222,41 +228,80 @@ impl Sha512State { } /// 해시 연산 종료 및 다이제스트(digest) 반환 - pub(crate) fn finalize(mut self) -> Vec { - // 1비트 '1' 패딩 추가(append 1 bit) - self.buffer[self.buffer_len] = 0x80; + pub(crate) fn finalize(mut self) -> Result { + let len = self.buffer_len; + self.buffer.as_mut_slice()[len] = 0x80; self.buffer_len += 1; - // 길이가 112바이트를 초과하면 버퍼를 0으로 채우고 새로운 블록 처리 - if self.buffer_len > 112 { - self.buffer[self.buffer_len..128].fill(0); - let mut block = [0u8; 128]; - block.copy_from_slice(&self.buffer); - self.process_block(&block); - self.buffer_len = 0; + // 길이 누출 방지를 위한 더미 패딩 + let needs_extra_block = self.buffer_len.ct_is_ge(&113usize); + + let mut block1 = [0u8; 128]; + let buf_slice = self.buffer.as_slice(); + for i in 0..128 { + let is_pad = i.ct_is_ge(&self.buffer_len); + block1[i] = u8::ct_select(&0u8, &buf_slice[i], is_pad); } - // 남은 버퍼 공간을 0으로 채우기 - self.buffer[self.buffer_len..112].fill(0); + let total_len_bytes = self.total_len.to_be_bytes(); + + // 추가 블럭이 필요 없는 경우(False), 첫 번째 블럭 마지막에 길이 삽임 + for i in 112..128 { + let orig = block1[i]; + let len_byte = total_len_bytes[i - 112]; + // needs_extra_block이 True면 원래 값 0 유지, False면 len_byte 덮쓰기 + block1[i] = u8::ct_select(&orig, &len_byte, needs_extra_block); + } + self.process_block(&block1); + let state_after_block1 = self.state; // 첫 번째 블럭 처리 후 상태 저장 - // 최종 128비트 길이를 빅 엔디안 형식으로 추가(append length in big-endian) - self.buffer[112..128].copy_from_slice(&self.total_len.to_be_bytes()); + let mut block2 = [0u8; 128]; + // 두 번째 블럭은 항상 마지막에 길이를 포함하도록 구성 (추가 블럭 필요 시 유효함) + block2[112..128].copy_from_slice(&total_len_bytes); + self.process_block(&block2); - let mut block = [0u8; 128]; - block.copy_from_slice(&self.buffer); - self.process_block(&block); + // 최종 상태 상수-시간 결정 + for (i, &saved) in state_after_block1.iter().enumerate() { + // needs_extra_block이 True면 두 블럭을 모두 거친 self.state 적용 + // 그렇지 않으면 첫 번째 블럭만 거침 state_after_block1 로 롤백 + self.state[i] = u64::ct_select(&self.state[i], &saved, needs_extra_block); + } - let mut digest = Vec::with_capacity(64); - for &s in &self.state { - digest.extend_from_slice(&s.to_be_bytes()); + // 스택에 할당된 임시 데이터 소거 + for b in &mut block1 { + unsafe { + write_volatile(b, 0); + } + } + for b in &mut block2 { + unsafe { + write_volatile(b, 0); + } } + compiler_fence(Ordering::SeqCst); + + // 다이제스트 생성을 위한 SecureBuffer 할당 + let digest_size = if self.is_384 { 48 } else { 64 }; + // 실패 시 &'static str 에러를 반환(?)하여 안전하게 에러 핸들링 + let mut digest_buf = SecureBuffer::new_owned(digest_size)?; + + let digest_slice = digest_buf.as_mut_slice(); + for (i, &s) in self.state.iter().enumerate() { + let start = i * 8; + if start >= digest_size { + break; + } + + let bytes = s.to_be_bytes(); + + let max_end = start + 8; + let is_end_ge = max_end.ct_is_ge(&digest_size); + let end = usize::ct_select(&digest_size, &max_end, is_end_ge); + let copy_len = end - start; - // SHA-384의 경우 48바이트(384비트)로 절단 - if self.is_384 { - digest.truncate(48); + digest_slice[start..end].copy_from_slice(&bytes[..copy_len]); } - // 함수 종료 시 self가 범위를 벗어나며 Drop 트레이트에 의해 내부 상태가 완전 소거됨 - digest + Ok(digest_buf) } } diff --git a/crypto/sha2/tests/sha2_256_test.rs b/crypto/sha2/tests/sha2_256_test.rs index 9c21b0d..1ef4e23 100644 --- a/crypto/sha2/tests/sha2_256_test.rs +++ b/crypto/sha2/tests/sha2_256_test.rs @@ -11,7 +11,7 @@ mod tests { ($type:ty, $update:expr, $expected:expr) => {{ let mut hasher = <$type>::new(); hasher.update($update); - assert_eq!(hasher.finalize(), $expected); + assert_eq!(hasher.finalize().unwrap().as_slice(), $expected); }}; } @@ -36,23 +36,23 @@ mod tests { hasher_chunked.update(b"a"); hasher_chunked.update(b"b"); hasher_chunked.update(b"c"); - let digest_chunked = hasher_chunked.finalize(); + let digest_chunked = hasher_chunked.finalize().unwrap(); let mut hasher_single = SHA224::new(); hasher_single.update(b"abc"); - let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + let digest_single = hasher_single.finalize().unwrap(); + assert_eq!(digest_chunked.as_slice(), digest_single.as_slice()); // sha256 let mut hasher_chunked = SHA256::new(); hasher_chunked.update(b"a"); hasher_chunked.update(b"b"); hasher_chunked.update(b"c"); - let digest_chunked = hasher_chunked.finalize(); + let digest_chunked = hasher_chunked.finalize().unwrap(); let mut hasher_single = SHA256::new(); hasher_single.update(b"abc"); - let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + let digest_single = hasher_single.finalize().unwrap(); + assert_eq!(digest_chunked.as_slice(), digest_single.as_slice()); } } diff --git a/crypto/sha2/tests/sha2_512_test.rs b/crypto/sha2/tests/sha2_512_test.rs index 4e9ac1a..538ae8d 100644 --- a/crypto/sha2/tests/sha2_512_test.rs +++ b/crypto/sha2/tests/sha2_512_test.rs @@ -11,7 +11,7 @@ mod tests { ($type:ty, $update:expr, $expected:expr) => {{ let mut hasher = <$type>::new(); hasher.update($update); - assert_eq!(hasher.finalize(), $expected); + assert_eq!(hasher.finalize().unwrap().as_slice(), $expected); }}; } @@ -36,23 +36,23 @@ mod tests { hasher_chunked.update(b"a"); hasher_chunked.update(b"b"); hasher_chunked.update(b"c"); - let digest_chunked = hasher_chunked.finalize(); + let digest_chunked = hasher_chunked.finalize().unwrap(); let mut hasher_single = SHA384::new(); hasher_single.update(b"abc"); - let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + let digest_single = hasher_single.finalize().unwrap(); + assert_eq!(digest_chunked.as_slice(), digest_single.as_slice()); - // sha256 + // sha512 let mut hasher_chunked = SHA512::new(); hasher_chunked.update(b"a"); hasher_chunked.update(b"b"); hasher_chunked.update(b"c"); - let digest_chunked = hasher_chunked.finalize(); + let digest_chunked = hasher_chunked.finalize().unwrap(); let mut hasher_single = SHA512::new(); hasher_single.update(b"abc"); - let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + let digest_single = hasher_single.finalize().unwrap(); + assert_eq!(digest_chunked.as_slice(), digest_single.as_slice()); } } diff --git a/crypto/sha2/tests/sha2_kcvmp_cavp.rs b/crypto/sha2/tests/sha2_kcvmp_cavp.rs new file mode 100644 index 0000000..ea2ac8b --- /dev/null +++ b/crypto/sha2/tests/sha2_kcvmp_cavp.rs @@ -0,0 +1,233 @@ +mod kcmvp_cavp_test { + use std::fs::File; + use std::io::{BufRead, BufReader, BufWriter, Write}; + + use entlib_native_secure_buffer::SecureBuffer; + use entlib_native_sha2::api::{SHA224, SHA256, SHA384, SHA512}; + + pub trait CavpHash { + fn new() -> Self; + fn update(&mut self, data: &[u8]); + fn finalize(self) -> Result; + } + + macro_rules! impl_cavp_hash { + ($algo:ident) => { + impl CavpHash for $algo { + fn new() -> Self { + Self::new() + } + fn update(&mut self, data: &[u8]) { + self.update(data); + } + fn finalize(self) -> Result { + self.finalize() + } + } + }; + } + + impl_cavp_hash!(SHA224); + impl_cavp_hash!(SHA256); + impl_cavp_hash!(SHA384); + impl_cavp_hash!(SHA512); + + fn hex_to_bytes(hex: &str) -> Vec { + let hex = hex.trim(); + if hex.is_empty() { + return Vec::new(); + } + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap_or(0)) + .collect() + } + + fn bytes_to_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02X}", b)).collect() + } + + /// SMT / LMT 테스트 프로세서 + pub fn process_smt_lmt(req_path: &str, rsp_path: &str) -> std::io::Result<()> { + let req_file = File::open(req_path)?; + let mut rsp_file = BufWriter::new(File::create(rsp_path)?); + let reader = BufReader::new(req_file); + + let mut current_len: usize = 0; + + for line_result in reader.lines() { + let line = line_result?; + let trimmed = line.trim(); + + if trimmed.starts_with("Len =") { + current_len = trimmed.replace("Len =", "").trim().parse().unwrap_or(0); + writeln!(rsp_file, "{}", trimmed)?; + } else if trimmed.starts_with("Msg =") { + let msg_hex = trimmed.replace("Msg =", "").trim().to_string(); + let mut msg_bytes = hex_to_bytes(&msg_hex); + + // [수정점] Len 속성은 '비트(Bit) 길이'이므로 바이트 길이로 환산하여 정확히 절삭 + let byte_len = current_len / 8; + if msg_bytes.len() > byte_len { + msg_bytes.truncate(byte_len); + } + + let mut hasher = T::new(); + hasher.update(&msg_bytes); + let md_buffer = hasher.finalize().expect("Hash finalization failed"); + let md_hex = bytes_to_hex(md_buffer.as_slice()); + + writeln!(rsp_file, "{}", trimmed)?; + writeln!(rsp_file, "MD = {}", md_hex)?; + // writeln!(rsp_file)?; + } else { + writeln!(rsp_file, "{}", line)?; + } + } + rsp_file.flush()?; + Ok(()) + } + + /// MCT (Monte Carlo Test) 프로세서 + pub fn process_mct(req_path: &str, rsp_path: &str) -> std::io::Result<()> { + let req_file = File::open(req_path)?; + let mut rsp_file = BufWriter::new(File::create(rsp_path)?); + let reader = BufReader::new(req_file); + + for line_result in reader.lines() { + let line = line_result?; + let trimmed = line.trim(); + + if trimmed.starts_with("Seed =") { + writeln!(rsp_file, "{}", trimmed)?; + writeln!(rsp_file)?; + + let seed_hex = trimmed.replace("Seed =", "").trim().to_string(); + let mut current_seed = hex_to_bytes(&seed_hex); + + // 외부 루프 100회: j = 0 to 99 + for count in 0..100 { + writeln!(rsp_file, "COUNT = {}", count)?; + + // 초기화: MD_0 = MD_1 = MD_2 = Seed + let mut md_0 = current_seed.clone(); + let mut md_1 = current_seed.clone(); + let mut md_2 = current_seed.clone(); + + // 내부 루프 1000회: i = 3 to 1002 + for _ in 3..1003 { + // M_i = MD_{i-3} || MD_{i-2} || MD_{i-1} + // 세 개의 이전 해시값을 concatenation + let mut m_i = Vec::with_capacity(md_0.len() * 3); + m_i.extend_from_slice(&md_0); + m_i.extend_from_slice(&md_1); + m_i.extend_from_slice(&md_2); + + // MD_i = SHA2(M_i) + let mut hasher = T::new(); + hasher.update(&m_i); + let digest = hasher.finalize().expect("Hash finalization failed"); + let md_i = digest.as_slice().to_vec(); + + // 윈도우 시프트 (다음 루프를 위해 상태 업데이트) + md_0 = md_1; + md_1 = md_2; + md_2 = md_i; + } + + // 루프 종료 후 출력 및 Seed 업데이트: MD_j = Seed = MD_1002 + current_seed = md_2.clone(); + + writeln!(rsp_file, "MD = {}", bytes_to_hex(¤t_seed))?; + writeln!(rsp_file)?; + } + } else { + writeln!(rsp_file, "{}", line)?; + } + } + rsp_file.flush()?; + Ok(()) + } + + // + // 실제 테스트 실행부 + // + #[test] + fn cavp_sha2_test() { + let dir = match std::env::var("KCMVP_CAVP_DIR") { + Ok(val) => val, + Err(_) => panic!("env"), + }; + + // 공통 경로 추출로 가독성 및 유지보수성 향상 + let base_path = format!("{}/entanglementlib__CAVP_3_20260313003528", dir); + + // SHA-224 + process_smt_lmt::( + &format!("{}/SHA2-224_(Byte)_SMT.req", base_path), + &format!("{}/SHA2-224_(Byte)_SMT.rsp", base_path), + ) + .unwrap(); + process_smt_lmt::( + &format!("{}/SHA2-224_(Byte)_LMT.req", base_path), + &format!("{}/SHA2-224_(Byte)_LMT.rsp", base_path), + ) + .unwrap(); + process_mct::( + &format!("{}/SHA2-224_(Byte)_MCT.req", base_path), + &format!("{}/SHA2-224_(Byte)_MCT.rsp", base_path), + ) + .unwrap(); + + // SHA-256 + process_smt_lmt::( + &format!("{}/SHA2-256_(Byte)_SMT.req", base_path), + &format!("{}/SHA2-256_(Byte)_SMT.rsp", base_path), + ) + .unwrap(); + process_smt_lmt::( + &format!("{}/SHA2-256_(Byte)_LMT.req", base_path), + &format!("{}/SHA2-256_(Byte)_LMT.rsp", base_path), + ) + .unwrap(); + process_mct::( + &format!("{}/SHA2-256_(Byte)_MCT.req", base_path), + &format!("{}/SHA2-256_(Byte)_MCT.rsp", base_path), + ) + .unwrap(); + + // SHA-384 + process_smt_lmt::( + &format!("{}/SHA2-384_(Byte)_SMT.req", base_path), + &format!("{}/SHA2-384_(Byte)_SMT.rsp", base_path), + ) + .unwrap(); + process_smt_lmt::( + &format!("{}/SHA2-384_(Byte)_LMT.req", base_path), + &format!("{}/SHA2-384_(Byte)_LMT.rsp", base_path), + ) + .unwrap(); + process_mct::( + &format!("{}/SHA2-384_(Byte)_MCT.req", base_path), + &format!("{}/SHA2-384_(Byte)_MCT.rsp", base_path), + ) + .unwrap(); + + // SHA-512 + process_smt_lmt::( + &format!("{}/SHA2-512_(Byte)_SMT.req", base_path), + &format!("{}/SHA2-512_(Byte)_SMT.rsp", base_path), + ) + .unwrap(); + process_smt_lmt::( + &format!("{}/SHA2-512_(Byte)_LMT.req", base_path), + &format!("{}/SHA2-512_(Byte)_LMT.rsp", base_path), + ) + .unwrap(); + process_mct::( + &format!("{}/SHA2-512_(Byte)_MCT.req", base_path), + &format!("{}/SHA2-512_(Byte)_MCT.rsp", base_path), + ) + .unwrap(); + } +} diff --git a/crypto/sha3/Cargo.toml b/crypto/sha3/Cargo.toml index dc5e9a5..cc3ab25 100644 --- a/crypto/sha3/Cargo.toml +++ b/crypto/sha3/Cargo.toml @@ -9,33 +9,32 @@ license.workspace = true crate-type = ["cdylib", "rlib"] [dependencies] -hex = "0.4.3" # 다음 커밋에 제거 +entlib-native-constant-time.workspace = true +entlib-native-secure-buffer.workspace = true [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 +#criterion = { version = "0.8.2", features = ["html_reports"] } + +#[[bin]] +#name = "cavp_sha3_bit" +#path = "test-vectors/bin/cavp_sha3_bit.rs" +# +#[[bin]] +#name = "cavp_sha3_byte" +#path = "test-vectors/bin/cavp_sha3_byte.rs" +# +#[[bin]] +#name = "cavp_shake_xof_bit" +#path = "test-vectors/bin/cavp_shake_xof_bit.rs" +# +#[[bin]] +#name = "cavp_shake_xof_byte" +#path = "test-vectors/bin/cavp_shake_xof_byte.rs" +# +#[[bin]] +#name = "kcmvp_sha3_byte" +#path = "test-vectors/bin/kcmvp_sha3_byte.rs" + +#[[bench]] +#name = "sha3_bench" +#harness = false diff --git a/crypto/sha3/benches/sha3_bench.rs b/crypto/sha3/benches/sha3_bench.rs deleted file mode 100644 index 9f611da..0000000 --- a/crypto/sha3/benches/sha3_bench.rs +++ /dev/null @@ -1,172 +0,0 @@ -use criterion::{Criterion, Throughput, criterion_group, criterion_main}; -use entlib_native_core_secure::secure_buffer::SecureBuffer; -use entlib_native_rng::base_rng::next_generate; -use entlib_native_sha3::api::*; -use std::hint::black_box; - -fn sha3_benchmark(c: &mut Criterion) { - // 메모리 할당 및 진난수 주입은 함수 진입점에서 단 1회만 수행하여 오버헤드를 차단합니다. - let mut data = SecureBuffer { - inner: vec![0u8; 1024], - }; - next_generate(&mut data).expect("hardware rng failure"); - let data_slice = data.inner.as_slice(); - - // 단일 벤치마크 그룹 생성 - let mut group = c.benchmark_group("throughput/sha3"); - - // - // SHA3-224 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA3-224 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA3_224::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA3-224 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA3_224::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - // - // SHA3-256 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA3-256 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA3_256::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA3-256 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA3_256::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - // - // SHA3-384 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA3-384 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA3_384::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA3-384 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA3_384::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - // - // SHA3-512 Benchmarks - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHA3-512 16KiB", |b| { - b.iter(|| { - let mut hasher = SHA3_512::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHA3-512 1MiB", |b| { - b.iter(|| { - let mut hasher = SHA3_512::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize()); - }) - }); - - // - // SHAKE128 Benchmarks (32바이트 출력) - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHAKE128 16KiB", |b| { - b.iter(|| { - let mut hasher = SHAKE128::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize(32)); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHAKE128 1MiB", |b| { - b.iter(|| { - let mut hasher = SHAKE128::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize(32)); - }) - }); - - // - // SHAKE256 Benchmarks (64바이트 출력) - // - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("SHAKE256 16KiB", |b| { - b.iter(|| { - let mut hasher = SHAKE256::new(); - for _ in 0..16 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize(64)); - }) - }); - - group.throughput(Throughput::Bytes(1024 * 1024)); - group.bench_function("SHAKE256 1MiB", |b| { - b.iter(|| { - let mut hasher = SHAKE256::new(); - for _ in 0..1024 { - hasher.update(black_box(data_slice)); - } - black_box(hasher.finalize(64)); - }) - }); - - group.finish(); -} - -criterion_group!(benches, sha3_benchmark); -criterion_main!(benches); diff --git a/crypto/sha3/src/api.rs b/crypto/sha3/src/api.rs index f2e117b..8ec3b91 100644 --- a/crypto/sha3/src/api.rs +++ b/crypto/sha3/src/api.rs @@ -18,6 +18,7 @@ #![allow(non_camel_case_types)] use crate::KeccakState; +use entlib_native_secure_buffer::SecureBuffer; // // SHA3-224 - start @@ -35,12 +36,16 @@ impl SHA3_224 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize(28, None) } // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 - pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + pub fn finalize_bits( + self, + last_byte: u8, + valid_bits: usize, + ) -> Result { self.0.finalize(28, Some((last_byte, valid_bits))) } } @@ -70,12 +75,16 @@ impl SHA3_256 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize(32, None) } // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 - pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + pub fn finalize_bits( + self, + last_byte: u8, + valid_bits: usize, + ) -> Result { self.0.finalize(32, Some((last_byte, valid_bits))) } } @@ -105,12 +114,16 @@ impl SHA3_384 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize(48, None) } // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 - pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + pub fn finalize_bits( + self, + last_byte: u8, + valid_bits: usize, + ) -> Result { self.0.finalize(48, Some((last_byte, valid_bits))) } } @@ -140,12 +153,16 @@ impl SHA3_512 { } // 해시 연산 완료 및 다이제스트 반환 - pub fn finalize(self) -> Vec { + pub fn finalize(self) -> Result { self.0.finalize(64, None) } // last_bits: 0~7 사이의 값. 마지막 바이트의 유효 비트 개수 - pub fn finalize_bits(self, last_byte: u8, valid_bits: usize) -> Vec { + pub fn finalize_bits( + self, + last_byte: u8, + valid_bits: usize, + ) -> Result { self.0.finalize(64, Some((last_byte, valid_bits))) } } @@ -175,12 +192,17 @@ impl SHAKE128 { } // 바이트가 정확히 맞아떨어질 때 사용 (불완전 바이트 없음) - pub fn finalize(self, output_len: usize) -> Vec { + pub fn finalize(self, output_len: usize) -> Result { 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 { + pub fn finalize_bits( + self, + output_len: usize, + last_byte: u8, + valid_bits: usize, + ) -> Result { self.0.finalize(output_len, Some((last_byte, valid_bits))) } } @@ -210,12 +232,17 @@ impl SHAKE256 { } // 바이트가 정확히 맞아떨어질 때 사용 (불완전 바이트 없음) - pub fn finalize(self, output_len: usize) -> Vec { + pub fn finalize(self, output_len: usize) -> Result { 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 { + pub fn finalize_bits( + self, + output_len: usize, + last_byte: u8, + valid_bits: usize, + ) -> Result { 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 deleted file mode 100644 index fe14385..0000000 --- a/crypto/sha3/src/bin/cavp_sha3_bit.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! 얽힘 라이브러리(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 deleted file mode 100644 index 20dc34d..0000000 --- a/crypto/sha3/src/bin/cavp_sha3_byte.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! 얽힘 라이브러리(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 deleted file mode 100644 index 0133aa4..0000000 --- a/crypto/sha3/src/bin/cavp_shake_xof_bit.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! 얽힘 라이브러리(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 deleted file mode 100644 index b8a9df1..0000000 --- a/crypto/sha3/src/bin/cavp_shake_xof_byte.rs +++ /dev/null @@ -1,319 +0,0 @@ -//! 얽힘 라이브러리(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 deleted file mode 100644 index b1b7454..0000000 --- a/crypto/sha3/src/bin/kcmvp_sha3_byte.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! 얽힘 라이브러리(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 66582ae..f7116c5 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -1,7 +1,8 @@ use crate::KeccakState; -use core::cmp::min; use core::ptr::write_volatile; use core::sync::atomic::{Ordering, compiler_fence}; +use entlib_native_constant_time::traits::{ConstantTimeEq, ConstantTimeSelect}; +use entlib_native_secure_buffer::SecureBuffer; const KECCAK_ROUND_CONSTANTS: [u64; 24] = [ 0x0000000000000001, @@ -43,7 +44,7 @@ impl KeccakState { Self { state: [0; 25], rate_bytes: rate_bits / 8, - buffer: [0; 200], + buffer: SecureBuffer::new_owned(200).expect("SecureBuffer allocate failed"), buffer_len: 0, domain, } @@ -95,9 +96,8 @@ impl KeccakState { compiler_fence(Ordering::SeqCst); } - /// rate 바이트만큼 채워진 버퍼를 상태에 흡수(absorb)하고 순열 적용 - fn process_buffer(&mut self) { - for (i, chunk) in self.buffer[..self.rate_bytes].chunks(8).enumerate() { + fn process_buffer(&mut self, block: &[u8]) { + for (i, chunk) in block.chunks(8).enumerate() { let mut word_bytes = [0u8; 8]; word_bytes.copy_from_slice(chunk); self.state[i] ^= u64::from_le_bytes(word_bytes); @@ -109,14 +109,21 @@ impl KeccakState { pub(crate) fn update(&mut self, data: &[u8]) { let mut offset = 0; while offset < data.len() { - let take = min(self.rate_bytes - self.buffer_len, data.len() - offset); - self.buffer[self.buffer_len..self.buffer_len + take] + let fill = self.rate_bytes - self.buffer_len; + let remain = data.len() - offset; + let is_ge = remain.ct_is_ge(&fill); + let take = usize::ct_select(&fill, &remain, is_ge); + + self.buffer.as_mut_slice()[self.buffer_len..self.buffer_len + take] .copy_from_slice(&data[offset..offset + take]); self.buffer_len += take; offset += take; if self.buffer_len == self.rate_bytes { - self.process_buffer(); + let mut temp_block = [0u8; 200]; // rete_bytes의 최대 크기 넉넉히 수용 + temp_block[..self.rate_bytes] + .copy_from_slice(&self.buffer.as_slice()[..self.rate_bytes]); + self.process_buffer(&temp_block[..self.rate_bytes]); self.buffer_len = 0; } } @@ -127,46 +134,104 @@ impl KeccakState { /// # 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; - } + // Q. T. Felix NOTE: Option은 컴파일러 최적화에 따라 분기를 유발할 수 있음. + // 나중에 (b: u8, bits: usize) 형태의 명시적 인자 전달 구조로 리팩토링. + let (b, bits) = last_byte_opt.unwrap_or((0, 0)); + let valid_bits = bits; + + // 상수-시간 마스크로 불필요한 비트 제거 (오버플로 방지) + let mask = ((1u16 << valid_bits).wrapping_sub(1)) as u8; + let last_byte_val = b & mask; - // 도메인 구분자와 패딩 시작 비트(1) 병합 let padding = (self.domain as u16) << valid_bits; - self.buffer[self.buffer_len] |= (padding & 0xFF) as u8; - self.buffer_len += 1; + let p0 = (padding & 0xFF) as u8 | last_byte_val; + let p1 = (padding >> 8) as u8; - // 패딩이 바이트 경계를 넘어가는 경우 (오버플로) - 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; - } + // padding > 0xFF (padding >= 0x0100) + let has_p1 = padding.ct_is_ge(&0x0100u16); + + let rate = self.rate_bytes; + let len = self.buffer_len; + + // Keccak 최대 rate_bytes 수용할 수 있는 고정 버퍼 + let mut block1 = [0u8; 200]; + let mut block2 = [0u8; 200]; - // 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; + // 상수-시간 블럭 1,2 ㄹ데이터 구성 + let buf_slice = self.buffer.as_slice(); + let p1_to_block2 = len.ct_eq(&(rate - 1)) & has_p1; + + for i in 0..rate { + // i < len + let is_i_less_len = i.ct_is_ge(&len).choice_not(); + let is_i_eq_len = i.ct_eq(&len); + let is_i_eq_len_plus_1 = i.ct_eq(&(len + 1)); + + // i < len 이면 원래 버퍼, 아니면 0 + let mut byte = u8::ct_select(&buf_slice[i], &0, is_i_less_len); + + // i == len 위치에 p0 덮어쓰기 + byte = u8::ct_select(&p0, &byte, is_i_eq_len); + + // i == len + 1 이고 has_p1이 True인 위치에 p1 덮어쓰기 + let put_p1_here = is_i_eq_len_plus_1 & has_p1; + byte = u8::ct_select(&p1, &byte, put_p1_here); + + block1[i] = byte; } - // 남은 공간 0으로 채움 - self.buffer[self.buffer_len..self.rate_bytes].fill(0); + // p1이 블럭 경계를 넘어간 경우 block2의 첫 번째 바이트에 저장 + block2[0] = u8::ct_select(&p1, &0, p1_to_block2); + + // 충동 및 추가 블럭 필요 여부 판별 + let len_after_pad = len + 1 + usize::ct_select(&1, &0, has_p1); + + // 패딩이 경계를 넘었는가? + let spills_to_block2 = len_after_pad.ct_is_ge(&(rate + 1)); + // 블럭이 꽉 찼늗가? + let exactly_full = len_after_pad.ct_eq(&rate); + + // 블럭이 가득 찼는데 마지막 바이트에 0x80 비트가 이미 존재하는지 상수-시간으로 검사 (keccak 10*1 충돌대응) + let collision_bit = (block1[rate - 1] & 0x80).ct_eq(&0x80); + let has_collision = exactly_full & collision_bit; + + // 추가 블럭(block2) 연산 결과를 최종 state에 반영해야 하는지 여부 + let needs_block2 = spills_to_block2 | has_collision; - // 스펀지 구조의 최종 종료 패딩 비트(0x80) 설정 - self.buffer[self.rate_bytes - 1] |= 0x80; + // 최종 스펀지 패딩 비트 상수-시간 배치 + // needs_block2가 True면 block2 끝에, 그렇지 않으면 block1 끝에 0x80 적용 + block1[rate - 1] = + u8::ct_select(&block1[rate - 1], &(block1[rate - 1] | 0x80), needs_block2); + block2[rate - 1] = + u8::ct_select(&(block2[rate - 1] | 0x80), &block2[rate - 1], needs_block2); - self.process_buffer(); + // 상수-시간 압축 수행 + // 첫 번째 블럭 처리 및 keccak 상태 백업 + self.process_buffer(&block1[..rate]); + let state_after_block1 = self.state; + + // 두 번째 블럭 일괄 처리 + self.process_buffer(&block2[..rate]); + + // needs_block2가 False면 두 번째 블럭의 연산 결과를 폐기하고 첫 번째 결과로 상수-시간 롤백 + for (i, &saved) in state_after_block1.iter().enumerate() { + self.state[i] = u64::ct_select(&self.state[i], &saved, needs_block2); + } + + // 스택에 할당된 임시 패딩 데이터 완전 소거 + for b in &mut block1 { + unsafe { + write_volatile(b, 0); + } + } + for b in &mut block2 { + unsafe { + write_volatile(b, 0); + } + } + compiler_fence(Ordering::SeqCst); + + self.buffer_len = 0; } /// 해시 연산 종료 및 다이제스트(digest) 반환 @@ -177,23 +242,41 @@ impl KeccakState { mut self, output_len: usize, last_byte_opt: Option<(u8, usize)>, - ) -> Vec { + ) -> Result { self.pad(last_byte_opt); - let mut out = Vec::with_capacity(output_len); - while out.len() < output_len { - for i in 0..(self.rate_bytes / 8) { - if out.len() >= output_len { + let mut out_buf = SecureBuffer::new_owned(output_len)?; + if output_len == 0 { + return Ok(out_buf); + } + + let out_slice = out_buf.as_mut_slice(); + let mut out_idx = 0; + let rate_words = self.rate_bytes / 8; + + while out_idx < output_len { + for i in 0..rate_words { + // 출력 길이(output_len)는 스펙에 따른 공개 정보이라 + // 이를 기반으로 한 루프 탈출 분기문은 비밀 데이터의 타이밍을 누출하지 않음 + if out_idx >= output_len { break; } let word_bytes = self.state[i].to_le_bytes(); - let take = core::cmp::min(8, output_len - out.len()); - out.extend_from_slice(&word_bytes[..take]); + + let remain = output_len - out_idx; + let is_ge = remain.ct_is_ge(&8usize); + let take = usize::ct_select(&8, &remain, is_ge); + + // 계산될 길이만큼 버퍼에 복사 + out_slice[out_idx..out_idx + take].copy_from_slice(&word_bytes[..take]); + out_idx += take; } - if out.len() < output_len { + + // 추가 출력이 필요하면 keccak 상태 갱신 + if out_idx < output_len { Self::keccak_f1600(&mut self.state); } } - out + Ok(out_buf) } } diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index fc22248..b34f5e4 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -3,12 +3,13 @@ mod keccak; use core::ptr::write_volatile; use core::sync::atomic::{Ordering, compiler_fence}; +use entlib_native_secure_buffer::SecureBuffer; /// Keccak 스펀지 함수의 내부 상태 구조체(internal state structure) pub(crate) struct KeccakState { pub(crate) state: [u64; 25], pub(crate) rate_bytes: usize, - pub(crate) buffer: [u8; 200], + pub(crate) buffer: SecureBuffer, // 200 pub(crate) buffer_len: usize, pub(crate) domain: u8, } @@ -21,11 +22,6 @@ impl Drop for KeccakState { write_volatile(&mut self.state[i], 0); } } - for i in 0..200 { - unsafe { - write_volatile(&mut self.buffer[i], 0); - } - } unsafe { write_volatile(&mut self.buffer_len, 0); } diff --git a/crypto/sha3/tests/sha3_kcvmp_cavp.rs b/crypto/sha3/tests/sha3_kcvmp_cavp.rs new file mode 100644 index 0000000..8dc2d41 --- /dev/null +++ b/crypto/sha3/tests/sha3_kcvmp_cavp.rs @@ -0,0 +1,319 @@ +mod kcmvp_cavp_test { + use entlib_native_sha3::api::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + use std::fs::File; + use std::io::{BufRead, BufReader, BufWriter, Write}; + use std::path::Path; + + /// 외부 입력에 대한 엄격한 16진수 디코딩 (Zero-Trust 검증) + fn decode_hex(hex_str: &str) -> Result, &'static str> { + if !hex_str.len().is_multiple_of(2) { + return Err("Hex string length must be even"); + } + (0..hex_str.len()) + .step_by(2) + .map(|i| { + u8::from_str_radix(&hex_str[i..i + 2], 16).map_err(|_| "Invalid hex character") + }) + .collect() + } + + fn encode_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() + } + + /// SHA3 해시 연산을 추상화하는 매크로 + /// api.rs의 finalize 및 finalize_bits를 통해 비트 단위 해시를 완벽히 지원합니다. + macro_rules! compute_sha3 { + ($algo:ident, $msg:expr, $bit_len:expr) => {{ + let mut hasher = $algo::new(); + let byte_len = $bit_len / 8; + let rem_bits = $bit_len % 8; + + if byte_len > 0 { + hasher.update(&$msg[..byte_len]); + } + + // 불완전 바이트(valid_bits)가 존재하는 경우 finalize_bits 호출 + let secure_buf = if rem_bits == 0 { + hasher.finalize()? + } else { + hasher.finalize_bits($msg[byte_len], rem_bits)? + }; + + // SecureBuffer 내의 데이터를 복사한 후, + // secure_buf는 스코프 종료 시 Drop되어 물리적 소거(Zeroization) 수행 + secure_buf.as_slice().to_vec() + }}; + } + + pub enum Sha3Variant { + Sha3_224, + Sha3_256, + Sha3_384, + Sha3_512, + } + + impl Sha3Variant { + fn hash(&self, msg: &[u8], bit_len: usize) -> Result, &'static str> { + match self { + Sha3Variant::Sha3_224 => Ok(compute_sha3!(SHA3_224, msg, bit_len)), + Sha3Variant::Sha3_256 => Ok(compute_sha3!(SHA3_256, msg, bit_len)), + Sha3Variant::Sha3_384 => Ok(compute_sha3!(SHA3_384, msg, bit_len)), + Sha3Variant::Sha3_512 => Ok(compute_sha3!(SHA3_512, msg, bit_len)), + } + } + + /// KCMVP 규격에 따른 스펀지 구조 파라미터 (r, n) 반환 + fn parameters(&self) -> (usize, usize) { + match self { + Sha3Variant::Sha3_224 => (1152, 224), // + Sha3Variant::Sha3_256 => (1088, 256), // + Sha3Variant::Sha3_384 => (832, 384), // + Sha3Variant::Sha3_512 => (576, 512), // + } + } + } + + /// SMT 및 LMT 테스트 벡터 파싱 및 검증 수행 + #[allow(dead_code)] + fn process_smt_lmt( + req_path: &str, + rsp_path: &str, + variant: Sha3Variant, + ) -> Result<(), &'static str> { + let req_file = File::open(Path::new(req_path)).map_err(|_| "Failed to open .req file")?; + let rsp_file = + File::create(Path::new(rsp_path)).map_err(|_| "Failed to create .rsp file")?; + + let reader = BufReader::new(req_file); + let mut writer = BufWriter::new(rsp_file); + + let mut current_len: Option = None; + + for line in reader.lines() { + let line = line.map_err(|_| "IO Read Error")?; + let trimmed = line.trim(); + + if trimmed.starts_with('#') || trimmed.is_empty() { + writeln!(writer, "{}", line).map_err(|_| "IO Write Error")?; + continue; + } + + if let Some(len_str) = trimmed.strip_prefix("Len = ") { + current_len = Some(len_str.parse::().map_err(|_| "Invalid Len value")?); + writeln!(writer, "{}", trimmed).map_err(|_| "IO Write Error")?; + } else if let Some(msg_str) = trimmed.strip_prefix("Msg = ") { + writeln!(writer, "{}", trimmed).map_err(|_| "IO Write Error")?; + + let bit_len = current_len.take().ok_or("Msg found before Len")?; + + // Len이 0인 경우 Msg가 "00"으로 올 수 있으나, 빈 배열로 처리 + let msg_bytes = if bit_len == 0 { + vec![] + } else { + decode_hex(msg_str)? + }; + + let md = variant.hash(&msg_bytes, bit_len)?; + writeln!(writer, "MD = {}", encode_hex(&md)).map_err(|_| "IO Write Error")?; + } else { + // 그 외 헤더 정보 유지 + writeln!(writer, "{}", trimmed).map_err(|_| "IO Write Error")?; + } + } + + writer.flush().map_err(|_| "Failed to flush .rsp file")?; + Ok(()) + } + + /// MCT(Monte Carlo Test) 검증 로직 수행 + /// 100,000번의 해시 반복 수행 중 메모리 누수 및 타이밍 이슈가 없는지 철저히 격리합니다. + pub fn process_mct( + req_path: &str, + rsp_path: &str, + variant: Sha3Variant, + ) -> Result<(), &'static str> { + let req_file = File::open(Path::new(req_path)).map_err(|_| "Failed to open .req file")?; + let rsp_file = + File::create(Path::new(rsp_path)).map_err(|_| "Failed to create .rsp file")?; + + let reader = BufReader::new(req_file); + let mut writer = BufWriter::new(rsp_file); + + // KCMVP MCT 파라미터 계산 + let (r_bits, n_bits) = variant.parameters(); + let n_bytes = n_bits / 8; + let n_blocks = (r_bits / n_bits) + 1; // N = floor(r/n) + 1 + + for line in reader.lines() { + let line = line.map_err(|_| "IO Read Error")?; + let trimmed = line.trim(); + + if trimmed.starts_with("Seed = ") { + let seed_str = trimmed.strip_prefix("Seed = ").unwrap(); + let mut current_seed = decode_hex(seed_str)?; + + writeln!(writer, "{}", trimmed).map_err(|_| "IO Write Error")?; + + // 100개의 체크포인트(MD) 생성 루프 + for count in 0..100 { + writeln!(writer, "COUNT = {}", count).map_err(|_| "IO Write Error")?; + + // 초기 MD 배열 (크기 N)을 Seed로 모두 채움 + let mut md_history: Vec> = vec![current_seed.clone(); n_blocks]; + + // 내부 연쇄에 사용할 고정 크기 메시지 버퍼 미리 할당 (Zero-Allocation 전략) + let mut msg_buffer = vec![0u8; n_blocks * n_bytes]; + + // 1,000회 반복 해시 연산 + for _ in 0..1000 { + // Msg_i = MD_{k-N} || ... || MD_{k-2} || MD_{k-1} + // 고정 버퍼에 복사하여 힙(Heap) 오염 최소화 + for (idx, md) in md_history.iter().enumerate() { + let start = idx * n_bytes; + let end = start + n_bytes; + msg_buffer[start..end].copy_from_slice(md); + } + + // 해시 수행 + let bit_len = msg_buffer.len() * 8; + let md_k = variant.hash(&msg_buffer, bit_len)?; + + // 히스토리 상태 전이 (오래된 데이터 밀어내기) + for i in 0..(n_blocks - 1) { + md_history[i] = md_history[i + 1].clone(); + } + md_history[n_blocks - 1] = md_k; + } + + // 1,000회 완료 후의 최신 MD를 다음 루프의 Seed로 설정 + current_seed = md_history[n_blocks - 1].clone(); + + writeln!(writer, "MD = {}", encode_hex(¤t_seed)) + .map_err(|_| "IO Write Error")?; + writeln!(writer).map_err(|_| "IO Write Error")?; + } + } else if trimmed.starts_with('#') || trimmed.is_empty() || trimmed.starts_with('[') { + writeln!(writer, "{}", line).map_err(|_| "IO Write Error")?; + } + } + + writer.flush().map_err(|_| "Failed to flush .rsp file")?; + Ok(()) + } + + // + // 실제 테스트 실행부 + // + #[test] + fn cavp_sha3_test() { + let dir = match std::env::var("KCMVP_CAVP_DIR") { + Ok(val) => val, + Err(_) => panic!("env"), + }; + + // SHA3-224 + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-224_(Byte)_SMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-224_(Byte)_SMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_224 + // ).unwrap(); + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-224_(Byte)_LMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-224_(Byte)_LMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_224 + // ).unwrap(); + process_mct( + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-224_(Byte)_MCT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-224_(Byte)_MCT.rsp", + dir + ) + .as_str(), + Sha3Variant::Sha3_224, + ) + .unwrap(); + + // SHA3-256 + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-256_(Byte)_SMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-256_(Byte)_SMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_256 + // ).unwrap(); + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-256_(Byte)_LMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-256_(Byte)_LMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_256 + // ).unwrap(); + process_mct( + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-256_(Byte)_MCT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-256_(Byte)_MCT.rsp", + dir + ) + .as_str(), + Sha3Variant::Sha3_256, + ) + .unwrap(); + + // SHA3-384 + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-384_(Byte)_SMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-384_(Byte)_SMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_384 + // ).unwrap(); + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-384_(Byte)_LMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-384_(Byte)_LMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_384 + // ).unwrap(); + process_mct( + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-384_(Byte)_MCT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-384_(Byte)_MCT.rsp", + dir + ) + .as_str(), + Sha3Variant::Sha3_384, + ) + .unwrap(); + + // SHA3-512 + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-512_(Byte)_SMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-512_(Byte)_SMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_512 + // ).unwrap(); + // process_smt_lmt( + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-512_(Byte)_LMT.req", dir).as_str(), + // format!("{}/entanglementlib__CAVP_1_20260307173059/SHA3-512_(Byte)_LMT.rsp", dir).as_str(), + // Sha3Variant::Sha3_512 + // ).unwrap(); + process_mct( + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-512_(Byte)_MCT.req", + dir + ) + .as_str(), + format!( + "{}/entanglementlib__CAVP_1_20260307173059/SHA3-512_(Byte)_MCT.rsp", + dir + ) + .as_str(), + Sha3Variant::Sha3_512, + ) + .unwrap(); + } +} diff --git a/crypto/sha3/tests/sha3_test.rs b/crypto/sha3/tests/sha3_test.rs index c210a1a..2167532 100644 --- a/crypto/sha3/tests/sha3_test.rs +++ b/crypto/sha3/tests/sha3_test.rs @@ -11,7 +11,7 @@ mod tests { ($type:ty, $update:expr, $expected:expr) => {{ let mut hasher = <$type>::new(); hasher.update($update); - assert_eq!(hasher.finalize(), $expected); + assert_eq!(hasher.finalize().unwrap().as_slice(), $expected); }}; } @@ -58,7 +58,10 @@ mod tests { let mut hasher_single = SHA3_224::new(); hasher_single.update(b"abc"); let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + assert_eq!( + digest_chunked.unwrap().as_slice(), + digest_single.unwrap().as_slice() + ); // SHA3-256 let mut hasher_chunked = SHA3_256::new(); @@ -70,7 +73,10 @@ mod tests { let mut hasher_single = SHA3_256::new(); hasher_single.update(b"abc"); let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + assert_eq!( + digest_chunked.unwrap().as_slice(), + digest_single.unwrap().as_slice() + ); // SHA3-384 let mut hasher_chunked = SHA3_384::new(); @@ -82,7 +88,10 @@ mod tests { let mut hasher_single = SHA3_384::new(); hasher_single.update(b"abc"); let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + assert_eq!( + digest_chunked.unwrap().as_slice(), + digest_single.unwrap().as_slice() + ); // SHA3-512 let mut hasher_chunked = SHA3_512::new(); @@ -94,6 +103,9 @@ mod tests { let mut hasher_single = SHA3_512::new(); hasher_single.update(b"abc"); let digest_single = hasher_single.finalize(); - assert_eq!(digest_chunked, digest_single); + assert_eq!( + digest_chunked.unwrap().as_slice(), + digest_single.unwrap().as_slice() + ); } } diff --git a/crypto/sha3/tests/shake_test.rs b/crypto/sha3/tests/shake_test.rs index 495098e..2f130be 100644 --- a/crypto/sha3/tests/shake_test.rs +++ b/crypto/sha3/tests/shake_test.rs @@ -8,7 +8,10 @@ mod tests { ($type:ty, $update:expr, $output_len:expr, $expected:expr) => {{ let mut hasher = <$type>::new(); hasher.update($update); - assert_eq!(hasher.finalize($output_len), $expected); + assert_eq!( + hasher.finalize($output_len).unwrap().as_mut_slice(), + $expected + ); }}; } @@ -36,7 +39,10 @@ mod tests { let mut hasher_single = SHAKE128::new(); hasher_single.update(b"abc"); let digest_single = hasher_single.finalize(32); - assert_eq!(digest_chunked, digest_single); + assert_eq!( + digest_chunked.unwrap().as_slice(), + digest_single.unwrap().as_slice() + ); // SHAKE256 let mut hasher_chunked = SHAKE256::new(); @@ -48,7 +54,10 @@ mod tests { let mut hasher_single = SHAKE256::new(); hasher_single.update(b"abc"); let digest_single = hasher_single.finalize(64); - assert_eq!(digest_chunked, digest_single); + assert_eq!( + digest_chunked.unwrap().as_slice(), + digest_single.unwrap().as_slice() + ); } #[test] @@ -62,7 +71,7 @@ mod tests { let mut hasher_long = SHAKE128::new(); hasher_long.update(b"test"); let long = hasher_long.finalize(32); - assert_eq!(&long[..16], &short[..]); + assert_eq!(&long.unwrap().as_slice()[..16], short.unwrap().as_slice()); // SHAKE256 let mut hasher_short = SHAKE256::new(); @@ -72,6 +81,6 @@ mod tests { let mut hasher_long = SHAKE256::new(); hasher_long.update(b"test"); let long = hasher_long.finalize(64); - assert_eq!(&long[..32], &short[..]); + assert_eq!(&long.unwrap().as_slice()[..32], short.unwrap().as_slice()); } } diff --git a/crypto/tls/Cargo.toml b/crypto/tls/Cargo.toml new file mode 100644 index 0000000..13a5a1d --- /dev/null +++ b/crypto/tls/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "entlib-native-tls" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] diff --git a/crypto/tls/src/lib.rs b/crypto/tls/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/crypto/tls/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/internal/ffi/Cargo.toml b/internal/ffi/Cargo.toml index 239ef51..8963d53 100644 --- a/internal/ffi/Cargo.toml +++ b/internal/ffi/Cargo.toml @@ -8,34 +8,11 @@ license.workspace = true [lib] crate-type = ["cdylib", "lib"] -# 패닉 발생 시 스택 풀기를 비활성화하여 메모리 상태가 -# 외부에 노출되거나 조작될 가능성을 차단할 필요 있음 -[profile.release] -panic = "abort" -lto = true -codegen-units = 1 -strip = true - -[profile.dev] -panic = "abort" - [dependencies] -tokio.workspace = true +entlib-native-result.workspace = true entlib-native-base64.workspace = true -entlib-native-constant-time.workspace = true -entlib-native-core-secure.workspace = true -entlib-native-rng.workspace = true +entlib-native-secure-buffer.workspace = true +entlib-native-hex.workspace = true entlib-native-sha2.workspace = true entlib-native-sha3.workspace = true -entlib-native-chacha20.workspace = true - -[dev-dependencies] -#criterion = { version = "0.8.2", features = ["html_reports"] } -# -#[[bench]] -#name = "ffi_base64_bench" -#harness = false -# -#[[bench]] -#name = "ffi_wipe_bench" -#harness = false \ No newline at end of file +entlib-native-constant-time.workspace = true \ No newline at end of file diff --git a/internal/ffi/src/base64_ffi.rs b/internal/ffi/src/base64_ffi.rs index 4d4f18e..a8b9c69 100644 --- a/internal/ffi/src/base64_ffi.rs +++ b/internal/ffi/src/base64_ffi.rs @@ -1,219 +1,157 @@ //! 상수-시간(constant-time) Base64 인코딩·디코딩 FFI 모듈 //! -//! Java/Kotlin 측에서 호출자 할당(caller-alloc) 메모리에 직접 기록합니다. -//! 모든 연산은 분기 없는 상수 시간으로 side-channel 공격에 저항합니다. +//! 외부에서 호출자 할당(caller-alloc) 메모리에 직접 기록합니다. +//! 모든 연산은 `entlib-native-base64` 코어 크레이트에 위임되어 +//! 분기 없는 상수 시간으로 side-channel 공격에 저항합니다. +//! +//! # Security Note +//! 이 모듈의 함수는 반드시 외부에서 직접 호출되므로 연산 종료 즉시 소거 작업을 +//! 진행해선 안 됩니다. 소거 권한은 외부에 있으며, 반드시 소거 지시를 받고 +//! 작업을 수행해야 합니다. //! //! # Author //! Q. T. Felix -use entlib_native_base64::base64::{ct_b64_to_bin_u8, ct_bin_to_b64_u8}; -use entlib_native_constant_time::constant_time::ConstantTimeOps; -use std::slice; +use crate::FFIStandard; +use entlib_native_base64::{decode, encode}; +use entlib_native_result::EntLibResult; +use std::ptr::write_volatile; + +const TYPE_ID_BASE64: i8 = 1; /// Java 측에서 할당한 메모리에 `Base64` 인코딩 결과를 직접 기록하는 함수입니다. /// +/// 내부적으로 [`entlib_native_base64::encode`]에 위임합니다. +/// /// # Arguments -/// * `input_ptr` - 인코딩할 평문 배열의 포인터 -/// * `input_len` - 평문 배열의 길이 -/// * `out_ptr` - (호출자 할당) 인코딩된 결과를 기록할 메모리의 시작 포인터 -/// * `out_capacity` - 할당된 출력 메모리의 최대 바이트 크기 +/// * `input` - 인코딩할 평문 배열의 포인터 및 길이를 담은 [`FFIStandard`] 포인터 +/// * `output` - (호출자 할당) 인코딩된 결과를 기록할 메모리의 포인터 및 용량을 담은 [`FFIStandard`] 포인터 /// /// # Returns -/// * `>= 0`: 성공 시 인코딩된 결과의 실제 바이트 길이 반환 -/// * `< 0`: 에러 코드 반환 (-1: `Null`, -2: `Capacity` 부족) +/// * `status == 0`: 성공, `additional` 필드에 인코딩된 결과의 실제 바이트 길이 기록 +/// * `status == -1`: `Null` 포인터, 또는 입력 메모리 페이지 정렬 또는 잠금 실패 +/// * `status == -2`: 필요 버퍼 크기 산출 도중 오버플로우 발생, 또는 mlock 실패 +/// * `status == -3`: 호출자 할당 출력 용량 부족 /// /// # Safety /// 이 함수는 raw pointer를 직접 다루므로 unsafe입니다. 호출자는 다음을 **반드시** 보장해야 합니다. -/// -/// - `input_ptr`은 null이 아니며, `input_len` 바이트만큼 **읽기 유효**한 메모리를 가리켜야 합니다 -/// (정렬 요구사항 없음, u8 기준). -/// - `out_ptr`은 null이 아니며, `out_capacity` 바이트만큼 **쓰기 유효**한 메모리를 가리켜야 합니다. -/// - `input_ptr`과 `out_ptr`이 가리키는 메모리 영역은 서로 겹치지 않아야 합니다 -/// (aliasing violation → UB). -/// - `out_capacity`는 내부에서 계산된 `required_capacity` 이상이어야 합니다 -/// (함수가 -2를 반환하지만, 호출 전 미리 확인 권장). +/// - `input`은 null이 아니며, `input.ptr`은 `input.len` 바이트만큼 **읽기 유효**하고 +/// OS 페이지 크기에 맞게 정렬되어 있어야 합니다. +/// - `output`은 null이 아니며, `output.ptr`은 `output.len` 바이트만큼 **쓰기 유효**해야 합니다. +/// - `input.ptr`과 `output.ptr`이 가리키는 메모리 영역은 서로 겹치지 않아야 합니다. /// - 호출 기간 동안 두 메모리 영역이 해제되거나 재할당되지 않아야 합니다. /// - 단일 스레드에서 호출되며, concurrent 접근이 없어야 합니다. -/// -/// 함수 내부는 `write_volatile`과 constant-time 연산만 사용하므로 -/// timing attack 및 메모리 잔여 데이터 유출에 대한 군사급 보호를 제공합니다. #[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_b64_encode_caller_alloc( - input_ptr: *const u8, - input_len: usize, - out_ptr: *mut u8, - out_capacity: usize, -) -> isize { +unsafe extern "C" fn ffi_base64_encode( + input: *const FFIStandard, + output: *mut FFIStandard, +) -> EntLibResult { // 포인터 유효성 검증 - if input_ptr.is_null() || out_ptr.is_null() { - return -1; + if input.is_null() || output.is_null() { + return EntLibResult::new(TYPE_ID_BASE64, -1); } + // FFIStandard -> ManuallyDrop 변환 + // 내부적으로 페이지 정렬 검증 + OS 메모리 잠금(mlock) 수행 + let in_buffer = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new(TYPE_ID_BASE64, -6), + }; + let out_struct = unsafe { &mut *output }; + // 필요 버퍼 크기 산출 및 오버플로우 방지 - let required_capacity = match input_len.checked_add(2) { - Some(val) => (val / 3).checked_mul(4), - None => return -1, + let required_capacity = match in_buffer.len().checked_add(2) { + Some(val) => match (val / 3).checked_mul(4) { + Some(cap) => cap, + None => return EntLibResult::new(TYPE_ID_BASE64, -2), + }, + None => return EntLibResult::new(TYPE_ID_BASE64, -2), }; // 호출자가 할당한 용량(capacity) 검증 - if out_capacity < required_capacity.expect("entlib-native-ffi ERROR: overflow") { - return -2; + if out_struct.len < required_capacity { + return EntLibResult::new(TYPE_ID_BASE64, -3); } - let input = unsafe { slice::from_raw_parts(input_ptr, input_len) }; - let mut out_idx = 0; - let mut i = 0; - - // 상수 시간 인코딩 쓰기 루프 - while i < input_len { - let b0 = input[i]; - let b1 = if i + 1 < input_len { input[i + 1] } else { 0 }; - let b2 = if i + 2 < input_len { input[i + 2] } else { 0 }; - - let e0 = b0 >> 2; - let e1 = ((b0 & 0x03) << 4) | (b1 >> 4); - let e2 = ((b1 & 0x0F) << 2) | (b2 >> 6); - let e3 = b2 & 0x3F; - - unsafe { - core::ptr::write_volatile(out_ptr.add(out_idx), ct_bin_to_b64_u8(e0)); - core::ptr::write_volatile(out_ptr.add(out_idx + 1), ct_bin_to_b64_u8(e1)); - - // 패딩 처리 (상수 시간 마스킹으로 치환 권장) - let pad2 = if i + 1 < input_len { - ct_bin_to_b64_u8(e2) - } else { - b'=' - }; - let pad3 = if i + 2 < input_len { - ct_bin_to_b64_u8(e3) - } else { - b'=' - }; - - core::ptr::write_volatile(out_ptr.add(out_idx + 2), pad2); - core::ptr::write_volatile(out_ptr.add(out_idx + 3), pad3); + match encode(&in_buffer) { + Ok(encoded_buf) => { + let encoded = encoded_buf.as_slice(); + // write_volatile로 컴파일러 최적화에 의한 소거 방지 + unsafe { + for (i, &byte) in encoded.iter().enumerate() { + write_volatile(out_struct.ptr.add(i), byte); + } + } + EntLibResult::new(TYPE_ID_BASE64, 0).add_additional(required_capacity as isize) } - - out_idx += 4; - i += 3; + // encode 내부의 메모리 잠금 실패 등 비정상 경로 + Err(_) => EntLibResult::new(TYPE_ID_BASE64, -2), } - - out_idx as isize } /// Java 측에서 할당한 메모리에 분기 없는 상수-시간(constant-time)으로 `Base64` 디코딩을 수행합니다. /// +/// 내부적으로 [`entlib_native_base64::decode`]에 위임합니다. +/// /// # Arguments -/// * `input_ptr` - 디코딩할 `Base64` 문자열의 포인터 -/// * `input_len` - 문자열의 바이트 길이 -/// * `out_ptr` - (호출자 할당) 복원된 평문을 기록할 메모리의 시작 포인터 -/// * `out_capacity` - 할당된 출력 메모리의 최대 바이트 크기 +/// * `input` - 디코딩할 `Base64` 문자열의 포인터 및 길이를 담은 [`FFIStandard`] 포인터 +/// * `output` - (호출자 할당) 복원된 평문을 기록할 메모리의 포인터 및 용량을 담은 [`FFIStandard`] 포인터 /// /// # Returns -/// * `>= 0`: 성공 시 디코딩된 평문의 실제 바이트 길이 반환 -/// * `< 0`: 에러 코드 반환 (-1: `Null`, -2: `Capacity` 부족, -3: 디코딩 중 유효하지 않은 문자열 감지) +/// * `status == 0`: 성공, `additional` 필드에 디코딩된 평문의 실제 바이트 길이 기록 +/// * `status == -1`: `Null` 포인터, 또는 입력 메모리 페이지 정렬 또는 잠금 실패 +/// * `status == -2`: 호출자 할당 출력 용량 부족 +/// * `status == -3`: 유효하지 않은 `Base64` 문자열 (길이, 패딩, 문자 오류), 또는 mlock 실패 /// /// # Safety /// 이 함수는 raw pointer를 직접 다루므로 unsafe입니다. 호출자는 다음을 **반드시** 보장해야 합니다. -/// -/// - `input_ptr`은 null이 아니며, `input_len` 바이트만큼 **읽기 유효**한 메모리를 가리켜야 합니다. -/// - `out_ptr`은 null이 아니며, `out_capacity` 바이트만큼 **쓰기 유효**한 메모리를 가리켜야 합니다. -/// - `input_ptr`과 `out_ptr`이 가리키는 메모리 영역은 서로 겹치지 않아야 합니다 (aliasing UB 방지). -/// - `out_capacity`는 `(input_len / 4 + 1) * 3` 이상이어야 합니다 (함수가 -2 반환). +/// - `input`은 null이 아니며, `input.ptr`은 `input.len` 바이트만큼 **읽기 유효**하고 +/// OS 페이지 크기에 맞게 정렬되어 있어야 합니다. +/// - `output`은 null이 아니며, `output.ptr`은 `output.len` 바이트만큼 **쓰기 유효**해야 합니다. +/// - `input.ptr`과 `output.ptr`이 가리키는 메모리 영역은 서로 겹치지 않아야 합니다. +/// - `output.len`은 `(input.len / 4 + 1) * 3` 이상이어야 합니다. /// - 호출 기간 동안 메모리 영역이 유효해야 합니다. /// - 단일 스레드 호출, concurrent 접근 금지. -/// -/// 함수는 `ct_b64_to_bin_u8`와 [ConstantTimeOps::ct_select] 연산만 사용하며, -/// `write_volatile`로 메모리 잔여 데이터 유출을 방지합니다. -/// 잘못된 Base64 문자는 -3 에러로 안전하게 처리되므로 side-channel 누출이 없습니다. #[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_b64_decode_caller_alloc( - input_ptr: *const u8, - input_len: usize, - out_ptr: *mut u8, - out_capacity: usize, -) -> isize { +unsafe extern "C" fn ffi_base64_decode( + input: *const FFIStandard, + output: *mut FFIStandard, +) -> EntLibResult { // 포인터 유효성 검증 - if input_ptr.is_null() || out_ptr.is_null() { - return -1; + if input.is_null() || output.is_null() { + return EntLibResult::new(TYPE_ID_BASE64, -1); } + // FFIStandard -> ManuallyDrop 변환 + // 내부적으로 페이지 정렬 검증 + OS 메모리 잠금(mlock) 수행 + let in_buffer = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new(TYPE_ID_BASE64, -6), + }; + + let out_struct = unsafe { &mut *output }; + // 최대 필요 버퍼 크기 산출 (여유 공간 3바이트 포함) - let max_required_capacity = (input_len / 4 + 1) * 3; + let max_required_capacity = (in_buffer.len() / 4 + 1) * 3; // 호출자가 할당한 용량 검증 - if out_capacity < max_required_capacity { - return -2; + if out_struct.len < max_required_capacity { + return EntLibResult::new(TYPE_ID_BASE64, -4); } - let input = unsafe { slice::from_raw_parts(input_ptr, input_len) }; - - let mut error_accum = 0u8; - let mut acc = 0u32; - let mut buf_idx = 0usize; - let mut out_idx = 0usize; - - for &byte in input { - let decoded = ct_b64_to_bin_u8(byte); - - let is_err = decoded.ct_eq(0xFF); - let is_pad = decoded.ct_eq(0x81); - let is_ws = decoded.ct_eq(0x80); - - let is_valid = !is_err & !is_pad & !is_ws; - error_accum |= is_err & 0x01; - - let valid_mask_u32 = (is_valid as i8 as i32) as u32; - let valid_mask_usize = (is_valid as i8 as isize) as usize; - - let next_acc = (acc << 6) | (decoded as u32 & 0x3F); - acc = next_acc.ct_select(acc, valid_mask_u32); - - let next_buf_idx = buf_idx + 1; - buf_idx = next_buf_idx.ct_select(buf_idx, valid_mask_usize); - - let is_full = buf_idx.ct_eq(4); - - let b0 = (acc >> 16) as u8; - let b1 = (acc >> 8) as u8; - let b2 = acc as u8; - - unsafe { - core::ptr::write_volatile(out_ptr.add(out_idx), b0); - core::ptr::write_volatile(out_ptr.add(out_idx + 1), b1); - core::ptr::write_volatile(out_ptr.add(out_idx + 2), b2); + match decode(&in_buffer) { + Ok(decoded_buf) => { + let decoded = decoded_buf.as_slice(); + let decoded_len = decoded.len(); + // write_volatile로 컴파일러 최적화에 의한 소거 방지 + unsafe { + for (i, &byte) in decoded.iter().enumerate() { + write_volatile(out_struct.ptr.add(i), byte); + } + } + EntLibResult::new(TYPE_ID_BASE64, 0).add_additional(decoded_len as isize) } - - let next_out_idx = out_idx + 3; - out_idx = next_out_idx.ct_select(out_idx, is_full); - - acc = 0u32.ct_select(acc, is_full as u32); - buf_idx = 0usize.ct_select(buf_idx, is_full); - } - - let is_two = buf_idx.ct_eq(2); - let is_three = buf_idx.ct_eq(3); - - let b0_2 = (acc >> 4) as u8; - let b0_3 = (acc >> 10) as u8; - let b1_3 = (acc >> 2) as u8; - - let final_b0 = b0_2.ct_select(0, is_two as u8) | b0_3.ct_select(0, is_three as u8); - let final_b1 = b1_3.ct_select(0, is_three as u8); - - unsafe { - core::ptr::write_volatile(out_ptr.add(out_idx), final_b0); - core::ptr::write_volatile(out_ptr.add(out_idx + 1), final_b1); + // 유효하지 않은 Base64 문자열 또는 메모리 잠금 실패 + Err(_) => EntLibResult::new(TYPE_ID_BASE64, -5), } - - let add_len = 1usize.ct_select(0, is_two) | 2usize.ct_select(0, is_three); - out_idx += add_len; - - // 에러 플래그 누적 여부 확인 후 분기 반환 - if error_accum != 0 { - return -3; - } - - out_idx as isize } diff --git a/internal/ffi/src/chacha20_ffi.rs b/internal/ffi/src/chacha20_ffi.rs deleted file mode 100644 index 9054ef1..0000000 --- a/internal/ffi/src/chacha20_ffi.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! ChaCha20-Poly1305 AEAD 및 저수준 모듈 FFI 인터페이스 (Java JNI / FFM API 연동용) -//! -//! # Security -//! - 모든 출력은 Opaque Pointer(*mut SecureBuffer) 형태로 반환 (Callee-allocated 패턴) -//! - Java 측에서 반드시 작업 완료 후 `free_secure_buffer`를 호출해야 함 (Zeroize + Dealloc 보장) -//! - decrypt 실패 시 null 포인터 반환 -> 명확한 AuthenticationFailedException 유도 -//! - 입력 길이 검증 필수, heap allocation 최소화, constant-time 보장 유지 - -use core::ptr::null_mut; -use entlib_native_core_secure::secure_buffer::SecureBuffer; - -use entlib_native_chacha20::chacha20::{chacha20_poly1305_decrypt, chacha20_poly1305_encrypt}; -use entlib_native_chacha20::chacha20_state::process_chacha20; -use entlib_native_chacha20::poly1305::generate_poly1305; - -/// ChaCha20 단일 처리 FFI (저수준 모듈 노출) -#[unsafe(no_mangle)] -pub unsafe extern "C" fn process_chacha20_ffi( - key_ptr: *const u8, - key_len: usize, - nonce_ptr: *const u8, - nonce_len: usize, - counter: u32, - data_ptr: *const u8, - data_len: usize, -) -> *mut SecureBuffer { - if key_len != 32 || nonce_len != 12 { - return null_mut(); - } - - let key = unsafe { core::slice::from_raw_parts(key_ptr, 32) }; - let key_arr: &[u8; 32] = key.try_into().expect("key length already checked"); - let nonce = unsafe { core::slice::from_raw_parts(nonce_ptr, 12) }; - let nonce_arr: &[u8; 12] = nonce.try_into().expect("nonce length already checked"); - let data = unsafe { core::slice::from_raw_parts(data_ptr, data_len) }; - - let result = process_chacha20(key_arr, nonce_arr, counter, data); - - // SecureBuffer를 Box로 감싸 힙으로 이동시킨 후, 원시 포인터로 변환하여 Java로 전달 (Ownership transfer) - Box::into_raw(Box::new(result)) -} - -/// Poly1305 MAC 생성 FFI (저수준 모듈 노출) -#[unsafe(no_mangle)] -pub unsafe extern "C" fn generate_poly1305_ffi( - key_ptr: *const u8, - key_len: usize, - data_ptr: *const u8, - data_len: usize, -) -> *mut SecureBuffer { - if key_len != 32 { - return null_mut(); - } - - let key = unsafe { core::slice::from_raw_parts(key_ptr, 32) }; - let key_arr: &[u8; 32] = key.try_into().expect("key length already checked"); - let data = unsafe { core::slice::from_raw_parts(data_ptr, data_len) }; - - let result = generate_poly1305(key_arr, data); - - Box::into_raw(Box::new(result)) -} - -/// RFC 8439 ChaCha20-Poly1305 AEAD 암호화 FFI -#[unsafe(no_mangle)] -pub unsafe extern "C" fn chacha20_poly1305_encrypt_ffi( - key_ptr: *const u8, - key_len: usize, - nonce_ptr: *const u8, - nonce_len: usize, - aad_ptr: *const u8, - aad_len: usize, - plaintext_ptr: *const u8, - plaintext_len: usize, -) -> *mut SecureBuffer { - if key_len != 32 || nonce_len != 12 { - return null_mut(); - } - - let key = unsafe { core::slice::from_raw_parts(key_ptr, 32) }; - let key_arr: &[u8; 32] = key.try_into().expect("key length already checked"); - let nonce = unsafe { core::slice::from_raw_parts(nonce_ptr, 12) }; - let nonce_arr: &[u8; 12] = nonce.try_into().expect("nonce length already checked"); - let aad = unsafe { core::slice::from_raw_parts(aad_ptr, aad_len) }; - let plaintext = unsafe { core::slice::from_raw_parts(plaintext_ptr, plaintext_len) }; - - let result = chacha20_poly1305_encrypt(key_arr, nonce_arr, aad, plaintext); - - Box::into_raw(Box::new(result)) -} - -/// RFC 8439 ChaCha20-Poly1305 AEAD 복호화 FFI -/// 실패 시 null 반환 -> Java에서 AuthenticationFailedException 발생 -#[unsafe(no_mangle)] -pub unsafe extern "C" fn chacha20_poly1305_decrypt_ffi( - key_ptr: *const u8, - key_len: usize, - nonce_ptr: *const u8, - nonce_len: usize, - aad_ptr: *const u8, - aad_len: usize, - ciphertext_with_tag_ptr: *const u8, - ciphertext_with_tag_len: usize, -) -> *mut SecureBuffer { - if key_len != 32 || nonce_len != 12 { - return null_mut(); - } - - let key = unsafe { core::slice::from_raw_parts(key_ptr, 32) }; - let key_arr: &[u8; 32] = key.try_into().expect("key length already checked"); - let nonce = unsafe { core::slice::from_raw_parts(nonce_ptr, 12) }; - let nonce_arr: &[u8; 12] = nonce.try_into().expect("nonce length already checked"); - let aad = unsafe { core::slice::from_raw_parts(aad_ptr, aad_len) }; - let ct_with_tag = - unsafe { core::slice::from_raw_parts(ciphertext_with_tag_ptr, ciphertext_with_tag_len) }; - - if let Some(result) = chacha20_poly1305_decrypt(key_arr, nonce_arr, aad, ct_with_tag) { - Box::into_raw(Box::new(result)) - } else { - null_mut() - } -} diff --git a/internal/ffi/src/hex_ffi.rs b/internal/ffi/src/hex_ffi.rs new file mode 100644 index 0000000..b79db67 --- /dev/null +++ b/internal/ffi/src/hex_ffi.rs @@ -0,0 +1,94 @@ +use crate::FFIStandard; +use entlib_native_hex::{decode, encode}; +use entlib_native_result::EntLibResult; +use std::ptr::write_volatile; + +// 모듈 식별자 및 상태 코드 (예시) +const TYPE_ID_HEX: i8 = 2; + +/// Java FFM API를 통해 호출되는 FFI 전용 상수-시간 Hex 인코딩 함수입니다. +/// +/// # Security +/// - **Zero-Trust**: 전달받은 포인터의 Null 여부와 정렬(Alignment) 상태를 엄격히 검증합니다. +/// - **UCA 규정 준수**: 입력 버퍼의 생명주기 통제권을 존중하며, 새롭게 생성된 +/// 출력 버퍼의 소유권(is_rust_owned = true)을 Java 측에 명시적으로 전달합니다. +#[unsafe(no_mangle)] +pub extern "C" fn entlib_ffi_hex_encode( + input: *const FFIStandard, + output: *mut FFIStandard, +) -> EntLibResult { + // 포인터 유효성 검증 + if input.is_null() || output.is_null() { + return EntLibResult::new(TYPE_ID_HEX, -1); + } + + let input_buf = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new(TYPE_ID_HEX, -2), + }; + let out_struct = unsafe { &mut *output }; + + // 인코딩 길이 = 원본 길이 * 2 + let required_len = input_buf.len() * 2; + if out_struct.len < required_len { + return EntLibResult::new(TYPE_ID_HEX, -3); + } + + match encode(&input_buf) { + Ok(encoded_buf) => { + let encoded = encoded_buf.as_slice(); + + unsafe { + for (i, &byte) in encoded.iter().enumerate() { + write_volatile(out_struct.ptr.add(i), byte); + } + } + // 이 스코프 벗어나면서 encoded_buf -> 소거 -> 메모리 락 해제 + EntLibResult::new(TYPE_ID_HEX, 0).add_additional(required_len as isize) + } + // encode 내부의 메모리 잠금 실패 등 비정상 경로 + Err(_) => EntLibResult::new(TYPE_ID_HEX, -4), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn ffi_hex_decode( + input: *const FFIStandard, + output: *mut FFIStandard, +) -> EntLibResult { + if input.is_null() || output.is_null() { + return EntLibResult::new(TYPE_ID_HEX, -1); + } + + let input_buf = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new(TYPE_ID_HEX, -2), + }; + + // Hex 인코딩 데이터는 반드시 짝수 길이를 가짐 + // 길이는 비밀 데이터가 아님 -> 상수-시간 분기 필요 X + if !input_buf.len().is_multiple_of(2) { + return EntLibResult::new(TYPE_ID_HEX, -5); + } + + let out_struct = unsafe { &mut *output }; + + let required_len = input_buf.len() / 2; + if out_struct.len < required_len { + return EntLibResult::new(TYPE_ID_HEX, -3); + } + + match decode(&input_buf) { + Ok(encoded_buf) => { + let encoded = encoded_buf.as_slice(); + + unsafe { + for (i, &byte) in encoded.iter().enumerate() { + write_volatile(out_struct.ptr.add(i), byte); + } + } + EntLibResult::new(TYPE_ID_HEX, 0).add_additional(required_len as isize) + } + Err(_) => EntLibResult::new(TYPE_ID_HEX, -4), + } +} diff --git a/internal/ffi/src/lib.rs b/internal/ffi/src/lib.rs index 148d1ac..e4fc5b6 100644 --- a/internal/ffi/src/lib.rs +++ b/internal/ffi/src/lib.rs @@ -1,42 +1,72 @@ -pub mod base64_ffi; -mod chacha20_ffi; -mod rng_ffi; // todo; 보안강화 및 검증 -pub mod secure_buffer_ffi; -pub mod sha2_ffi; -pub mod sha3_ffi; - -/// ffi 작업 중 발생할 수 있는 상태 코드 (status code) +use entlib_native_result::EntLibResult; +use entlib_native_secure_buffer::SecureBuffer; +use std::mem::ManuallyDrop; + +pub(crate) const TYPE_ID: i8 = 1; + +mod base64_ffi; +mod hex_ffi; +mod sha_ffi; + +/// 얽힘 라이브러리 FFI 경계 통신 표준 구조체입니다. +/// +/// FFI 경계를 넘어온 브릿지 객체를 핸들링합니다. #[repr(C)] -pub enum FFIStatus { - Success = 0, - NullPointerError = -1, +pub struct FFIStandard { + pub ptr: *mut u8, + pub len: usize, + /// # Returns + /// `true` = Rust-Owned 패턴 (Rust가 할당 해제) + /// `false` = Java-Owned 패턴 (Java가 할당 해제) + pub is_rust_owned: bool, } -// -// no_std 유지 - start -// -// 아래 방법은 no_std를 유지하는 해결책이지만, Java 애플리케이션과 상호 작용하는 환경은 이미 -// 범용 운영체제의 스레드 모델과 가상 메모리 관리 시스템 위에서 동작함. 특수한 임베디드 하드웨어 -// 타겟팅이 목적이 아니라면, no_std를 해제하고 Rust의 표준 라이브러리(std)를 활용하는 것이 -// 메모리 효율 측면에서 시스템적 안정성을 극대화하는 데 유리함 -// -// #![no_std] -// extern crate alloc; -// -// // 스레드 안전과 메모리 안정성을 위해 libc 기반 글로벌 allocator 사용 -// use libc_alloc::LibcAlloc; -// -// #[global_allocator] -// static ALLOCATOR: LibcAlloc = LibcAlloc; -// -// #[cfg(not(test))] -// #[panic_handler] -// fn panic(_info: &internal::panic::PanicInfo) -> ! { -// // 민감 데이터 유출 방지를 위해 즉시 해제 -// unsafe { -// libc::abort(); -// } -// } -// -// no_std 유지 - end -// +impl FFIStandard { + /// FFI 경계를 넘어온 브릿지 객체를 Rust 도메인 객체인 [SecureBuffer]로 변환합니다. + /// + /// # Safety + /// 이 함수는 `ptr`이 유효하고 `len`만큼 접근 가능하며, + /// OS 페이지 크기(PAGE_SIZE)에 맞게 정렬되어 있음을 가정합니다. + pub unsafe fn into_domain_buffer(&self) -> Result, &'static str> { + // from_raw_parts를 통해 메모리 검증 및 래핑 + // 이 과정에서 정렬 검사 및 OS 메모리 잠금(lock_memory) 수행 + let buffer = unsafe { SecureBuffer::from_raw_parts(self.ptr, self.len)? }; + + // 자동 소거(Drop) 우회 및 소유권 제어 + if !self.is_rust_owned { + // JO 패턴 + // 함수 스코프가 끝나도 SecureBuffer의 Drop 트레이트가 실행되지 않도록 감쌈 + Ok(ManuallyDrop::new(buffer)) + } else { + // RO 패턴(is_rust_owned = true)이 이 구조체로 들어오는 경우는 거의 없는데 + // 만약 들어오더라도 브릿지를 통한 임시 뷰 역할이라서 자동 소거를 막음 + Ok(ManuallyDrop::new(buffer)) + } + } +} + +/// Java-Owned End Process order +/// +/// # Safety +/// - `target`은 유효한 `FFIStandard` 포인터여야 합니다. +/// - `target`이 가리키는 메모리는 Java FFM API에서 할당된 페이지-정렬 메모리여야 합니다. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn joep(target: *const FFIStandard) -> EntLibResult { + if target.is_null() { + return EntLibResult::new(TYPE_ID, -1); + } + + // ManuallyDrop으로 감싸지 않고 곧바로 SecureBuffer 생성 + // 내부적으로 PAGE_SIZE 검증 및 lock 통과 후 생성됨 + let result = unsafe { SecureBuffer::from_raw_parts((*target).ptr, (*target).len) }; + + if result.is_ok() { + // 블록이 종료되면서 변수가 스코프를 벗어남 + // -> SecureBuffer의 Drop 강제 발동 + // -> Zeroizer::zeroize_raw 실행 (완벽 소거) + // -> os_lock::unlock_memory 연쇄 실행 (잠금 해제) + EntLibResult::new(TYPE_ID, 0) + } else { + EntLibResult::new(TYPE_ID, -1) + } +} diff --git a/internal/ffi/src/rng_ffi.rs b/internal/ffi/src/rng_ffi.rs deleted file mode 100644 index b8e08c5..0000000 --- a/internal/ffi/src/rng_ffi.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! 난수 생성기 FFI 브릿지 모듈 -//! -//! [entlib_native_rng::base_rng], [entlib_native_rng::mixed], [entlib_native_rng::base_rng::anu_qrng] -//! 모듈을 모두 Java로 안전하게 노출합니다. -//! -//! # security -//! - 모든 민감 상태는 `MixedRng::Drop` + `SecureBuffer::Drop`에 의해 강제 zeroize -//! - anu qrng는 네트워크 호출이므로 `NetworkFailure`/`ParseError` 처리 필수 -//! - heap 메모리 누수를 방지하기 위한 버퍼 해제 함수 추가 -//! -//! # Author -//! Q. T. Felix - -use core::ptr; -use entlib_native_core_secure::secure_buffer::SecureBuffer; -use entlib_native_rng::anu_qrng::AnuQrngClient; -use entlib_native_rng::base_rng::{RngError, generate_hardware_random_bytes, next_generate}; -use entlib_native_rng::mixed::{EntropyStrategy, MixedRng}; -use std::boxed::Box; - -/// ffi 에러 코드 매핑 -#[inline(always)] -fn map_rng_error(err: RngError) -> u8 { - match err { - RngError::UnsupportedHardware => 1, - RngError::EntropyDepletion => 2, - RngError::InvalidPointer => 3, - RngError::NetworkFailure(msg) => { - eprintln!("[ENTLIB] RNG NetworkFailure: {}", msg); // 일단 네트워크 문제 부터 파악 - 4 - } - RngError::ParseError => 5, - RngError::InvalidParameter => 6, - } -} - -// -// 하드웨어 진난수 생성기 (hardware trng) ffi -// - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_rng_hw_generate( - len: usize, - err_flag: *mut u8, -) -> *mut SecureBuffer { - if !err_flag.is_null() { - unsafe { - *err_flag = 0; - } - } - - match generate_hardware_random_bytes(len) { - Ok(buffer) => Box::into_raw(Box::new(buffer)), - Err(e) => { - if !err_flag.is_null() { - unsafe { - *err_flag = map_rng_error(e); - } - } - ptr::null_mut() - } - } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_rng_hw_next_generate(buf: *mut SecureBuffer) -> u8 { - if buf.is_null() { - return map_rng_error(RngError::InvalidPointer); - } - - let buffer = unsafe { &mut *buf }; - match next_generate(buffer) { - Ok(_) => 0, - Err(e) => map_rng_error(e), - } -} - -// -// anu 양자 난수 생성기 (quantum rng) ffi -// - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_rng_anu_generate( - len: usize, - err_flag: *mut u8, -) -> *mut SecureBuffer { - if !err_flag.is_null() { - unsafe { - *err_flag = 0; - } - } - - let rt = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(rt) => rt, - Err(e) => { - if !err_flag.is_null() { - unsafe { - *err_flag = map_rng_error(RngError::NetworkFailure(format!( - "tokio 런타임 빌드 실패: {}", - e - ))); - } - } - return ptr::null_mut(); - } - }; - - match rt.block_on(AnuQrngClient::fetch_secure_bytes(len)) { - Ok(buffer) => Box::into_raw(Box::new(buffer)), - Err(e) => { - if !err_flag.is_null() { - unsafe { - *err_flag = map_rng_error(e); - } - } - ptr::null_mut() - } - } -} - -// -// 혼합 난수 생성기 (mixed rng with chacha20) ffi -// - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_rng_mixed_new_with_strategy( - strategy: u8, - err_flag: *mut u8, -) -> *mut MixedRng { - if !err_flag.is_null() { - unsafe { - *err_flag = 0; - } - } - - let entropy_strategy = match strategy { - 0 => EntropyStrategy::LocalHardware, - 1 => EntropyStrategy::QuantumNetwork, - _ => { - if !err_flag.is_null() { - unsafe { - *err_flag = map_rng_error(RngError::InvalidParameter); - } - } - return ptr::null_mut(); - } - }; - - match MixedRng::new(entropy_strategy) { - Ok(rng) => Box::into_raw(Box::new(rng)), - Err(e) => { - if !err_flag.is_null() { - unsafe { - *err_flag = map_rng_error(e); - } - } - ptr::null_mut() - } - } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_rng_mixed_new(err_flag: *mut u8) -> *mut MixedRng { - unsafe { entlib_rng_mixed_new_with_strategy(0, err_flag) } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_rng_mixed_generate( - rng_ptr: *mut MixedRng, - len: usize, - err_flag: *mut u8, -) -> *mut SecureBuffer { - if !err_flag.is_null() { - unsafe { - *err_flag = 0; - } - } - - if rng_ptr.is_null() { - if !err_flag.is_null() { - unsafe { - *err_flag = map_rng_error(RngError::InvalidPointer); - } - } - return ptr::null_mut(); - } - - let rng = unsafe { &mut *rng_ptr }; - match rng.generate(len) { - Ok(buffer) => Box::into_raw(Box::new(buffer)), - Err(e) => { - if !err_flag.is_null() { - unsafe { - *err_flag = map_rng_error(e); - } - } - ptr::null_mut() - } - } -} - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_rng_mixed_free(rng_ptr: *mut MixedRng) { - if !rng_ptr.is_null() { - // drop -> zeroize 보장 - let _ = unsafe { Box::from_raw(rng_ptr) }; - } -} diff --git a/internal/ffi/src/secure_buffer_ffi.rs b/internal/ffi/src/secure_buffer_ffi.rs deleted file mode 100644 index 6b863ad..0000000 --- a/internal/ffi/src/secure_buffer_ffi.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! 외부 함수 인터페이스(foreign function interface) 환경에서 민감 데이터의 안전한 교환 및 -//! 메모리 소거(zeroize)를 완벽하게 보장하기 위한 통제 모듈입니다. -//! -//! 얽힘 라이브러리의 Java 런타임과 네이티브 환경 간의 브리지 역할을 수행하며, -//! 가비지 컬렉터(garbage collector)에 의한 메모리 누수 및 잔류 데이터 노출을 원천 차단합니다. -//! 본 모듈은 대규모 엔터프라이즈 및 군사급 보안 요구사항을 충족하기 위해, 다음과 같이 -//! 두 가지 독립적이고 엄격한 메모리 소유권(memory ownership) 모델을 지원합니다. -//! -//! # 피호출자 할당 패턴 (callee-allocated, rust-owned memory) -//! 네이티브 환경에서 연산 결과의 크기를 Java 측이 사전에 알 수 없는 경우(가변 길이의 암호문 생성 등) -//! 사용되는 패턴입니다. Rust가 동적으로 할당한 [SecureBuffer]의 불투명 포인터(opaque pointer)가 -//! Java로 반환됩니다. -//! -//! Java 측은 획득한 포인터를 통해 다음 함수들을 순차적으로 호출해야 합니다. -//! - [entlib_secure_buffer_data]: 실제 데이터의 메모리 주소 매핑 -//! - [entlib_secure_buffer_len]: 데이터의 바이트 길이 확인 -//! - [entlib_secure_buffer_free]: 사용 완료 후 즉각적인 데이터 소거 및 메모리 할당 해제(deallocation) 지시 -//! -//! # 호출자 할당 패턴 (caller-allocated, java-owned memory) -//! Java 측의 보안 데이터 컨테이너(`SensitiveDataContainer`)가 `off-heap` 영역에 메모리를 -//! 선제적으로 확보하여 제공하는 경우 사용되는 패턴입니다. -//! -//! Java 스코프 컨텍스트(`SDCScopeContext`)가 종료될 때 호출되며, 네이티브는 데이터의 덮어쓰기만 수행합니다. -//! - [entanglement_secure_wipe]: Java가 소유한 메모리 영역을 임시로 `FFIExternalSecureBuffer`에 -//! 매핑하여 소거 로직을 실행하되, 할당 해제는 수행하지 않음 (해제 권한은 Java에 위임됨) -//! -//! # Authors -//! Q. T. Felix - -use core::ptr; -use entlib_native_core_secure::secure_buffer::{FFIExternalSecureBuffer, SecureBuffer}; - -/// 보안 버퍼 내 실제 데이터의 메모리 주소 반환 (get immutable data pointer) -/// -/// # Safety -/// - 반환된 원시 포인터(raw pointer)는 `SecureBuffer`가 `entlib_secure_buffer_free`를 통해 -/// 해제되기 전까지만 유효합니다. 해제 후 역참조 시 미정의 동작(undefined behavior)이 발생합니다. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_secure_buffer_data(buf: *const SecureBuffer) -> *const u8 { - if buf.is_null() { - return ptr::null(); - } - let buffer = unsafe { &*buf }; - buffer.inner.as_ptr() -} - -/// 보안 버퍼 내 데이터의 바이트 길이 반환 (get length of data) -/// -/// # Safety -/// - `buf`가 null이 아닌 경우, 유효한 `SecureBuffer` 인스턴스를 가리켜야 합니다. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_secure_buffer_len(buf: *const SecureBuffer) -> usize { - if buf.is_null() { - return 0; - } - let buffer = unsafe { &*buf }; - buffer.inner.len() -} - -/// 보안 버퍼 메모리 해제 및 데이터 소거 (free and zeroize) -/// -/// # Safety -/// - 호출 즉시 `SecureBuffer`의 `Drop` 트레이트가 실행되어 `write_volatile` 및 -/// `compiler_fence`를 통해 메모리가 안전하게 소거됩니다. -/// - Java 링커(java linker api)를 통해 반드시 한 번만 호출되어야 합니다 (double-free 방지). -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entlib_secure_buffer_free(buf: *mut SecureBuffer) { - if !buf.is_null() { - unsafe { - // Box::from_raw를 통해 소유권을 가져오며, 즉시 스코프를 벗어나 Drop 실행 - drop(Box::from_raw(buf)); - } - } -} - -/// 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 엔드포인트입니다. -/// -/// # Arguments -/// * `ptr` - 소거할 메모리 영역의 시작 포인터 (*mut u8) -/// * `len` - 소거할 메모리의 바이트 크기 (usize) -/// -/// # Safety -/// - `ptr`은 `len` 바이트만큼 할당된 유효한 메모리 영역을 가리켜야 합니다. -/// - Java의 제한된 아레나(confined arena) 수명 주기에 의해 유효성이 검증된 상태에서만 호출되어야 합니다. -/// - 이 함수는 호출자 할당 패턴으로, Java 측에서 할당 해제를 수행해야 합니다. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entanglement_secure_wipe(ptr: *mut u8, len: usize) { - // null 포인터 및 길이 검증 - if ptr.is_null() || len == 0 { - return; - } - - // 명시적 drop 호출로 RAII 소멸자를 즉시 실행 - // Q. T. Felix NOTE: FFIExternalSecureBuffer의 Drop 구현은 오직 write_volatile을 통한 소거만 수행해야 하며, - // 메모리 할당 해제(deallocate)는 Java 측에서 수행되어야 합니당 - drop(FFIExternalSecureBuffer { inner: ptr, len }); -} diff --git a/internal/ffi/src/sha2_ffi.rs b/internal/ffi/src/sha2_ffi.rs deleted file mode 100644 index 9e71c76..0000000 --- a/internal/ffi/src/sha2_ffi.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::FFIStatus; -use core::ptr; -use core::slice; -use entlib_native_core_secure::secure_buffer::SecureBuffer; -use entlib_native_sha2::api::{SHA224, SHA256, SHA384, SHA512}; - -macro_rules! generate_sha2_ffi { - ($struct_type:ty, $new_fn:ident, $update_fn:ident, $finalize_fn:ident, $free_fn:ident) => { - /// 해시 컨텍스트 초기화 (initialize hash context) - #[unsafe(no_mangle)] - pub extern "C" fn $new_fn() -> *mut $struct_type { - let instance = Box::new(<$struct_type>::new()); - Box::into_raw(instance) - } - - /// 데이터 주입 및 상태 업데이트 (update hash state) - /// - /// # Safety - /// - `ctx`는 `$new_fn`을 통해 할당된 유효한 포인터여야 합니다. - /// - `data` 포인터는 최소 `len` 바이트 크기의 읽기 가능한 메모리를 가리켜야 합니다. - /// - 동일한 컨텍스트에 대한 동시 접근 시 스레드-안전(thread-safe)을 보장하기 위해 호출 측에서 동기화해야 합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $update_fn( - ctx: *mut $struct_type, - data: *const u8, - len: usize, - ) -> i32 { - if ctx.is_null() { - return FFIStatus::NullPointerError as i32; - } - if data.is_null() && len > 0 { - return FFIStatus::NullPointerError as i32; - } - - let slice = unsafe { slice::from_raw_parts(data, len) }; - let hasher = unsafe { &mut *ctx }; - - hasher.update(slice); - - FFIStatus::Success as i32 - } - - /// 연산 완료 및 보안 버퍼 반환 (finalize and return secure buffer) - /// - /// 자바 영역으로 데이터를 복사하지 않고, 안전하게 소거되는 `SecureBuffer`의 포인터를 반환합니다. - /// - /// # Safety - /// - `ctx`는 유효한 포인터여야 하며, 호출 후 소유권이 소비(consume)되어 내부 상태가 자동 소거됩니다. - /// - 반환된 `SecureBuffer` 포인터는 자바 측에서 사용이 끝난 직후 반드시 `entlib_secure_buffer_free`를 통해 수동으로 해제되어야 합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $finalize_fn(ctx: *mut $struct_type) -> *mut SecureBuffer { - if ctx.is_null() { - return ptr::null_mut(); - } - - let hasher = unsafe { Box::from_raw(ctx) }; - let digest = hasher.finalize(); // Vec 반환 - - // 다이제스트를 SecureBuffer로 캡슐화하여 힙에 할당 - let secure_buffer = Box::new(SecureBuffer { inner: digest }); - Box::into_raw(secure_buffer) - } - - /// 예외 발생 시 해시 컨텍스트 조기 폐기 (early free on exception) - /// - /// # Safety - /// - `ctx`가 null이 아닐 경우 강제로 소유권을 가져와 메모리를 해제합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $free_fn(ctx: *mut $struct_type) { - if !ctx.is_null() { - unsafe { - drop(Box::from_raw(ctx)); - } - } - } - }; -} - -// SHA2 ffi 엔드포인트 자동 생성 -generate_sha2_ffi!( - SHA224, - entlib_sha224_new, - entlib_sha224_update, - entlib_sha224_finalize, - entlib_sha224_free -); -generate_sha2_ffi!( - SHA256, - entlib_sha256_new, - entlib_sha256_update, - entlib_sha256_finalize, - entlib_sha256_free -); -generate_sha2_ffi!( - SHA384, - entlib_sha384_new, - entlib_sha384_update, - entlib_sha384_finalize, - entlib_sha384_free -); -generate_sha2_ffi!( - SHA512, - entlib_sha512_new, - entlib_sha512_update, - entlib_sha512_finalize, - entlib_sha512_free -); diff --git a/internal/ffi/src/sha3_ffi.rs b/internal/ffi/src/sha3_ffi.rs deleted file mode 100644 index 5935507..0000000 --- a/internal/ffi/src/sha3_ffi.rs +++ /dev/null @@ -1,200 +0,0 @@ -use crate::FFIStatus; -use core::ptr; -use core::slice; -use entlib_native_core_secure::secure_buffer::SecureBuffer; -use entlib_native_sha3::api::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; - -macro_rules! generate_sha3_ffi { - ($struct_type:ty, $new_fn:ident, $update_fn:ident, $finalize_fn:ident, $free_fn:ident) => { - /// 해시 컨텍스트 초기화 (initialize hash context) - #[unsafe(no_mangle)] - pub extern "C" fn $new_fn() -> *mut $struct_type { - let instance = Box::new(<$struct_type>::new()); - Box::into_raw(instance) - } - - /// 데이터 주입 및 상태 업데이트 (update hash state) - /// - /// # Safety - /// - `ctx`는 `$new_fn`을 통해 할당된 유효한 포인터여야 합니다. - /// - `data` 포인터는 최소 `len` 바이트 크기의 읽기 가능한 메모리를 가리켜야 합니다. - /// - 동일한 컨텍스트에 대한 동시 접근 시 스레드-안전(thread-safe)을 보장하기 위해 호출 측에서 동기화해야 합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $update_fn( - ctx: *mut $struct_type, - data: *const u8, - len: usize, - ) -> i32 { - if ctx.is_null() { - return FFIStatus::NullPointerError as i32; - } - if data.is_null() && len > 0 { - return FFIStatus::NullPointerError as i32; - } - - let slice = unsafe { slice::from_raw_parts(data, len) }; - let hasher = unsafe { &mut *ctx }; - - hasher.update(slice); - - FFIStatus::Success as i32 - } - - /// 연산 완료 및 보안 버퍼 반환 (finalize and return secure buffer) - /// - /// 자바 영역으로 데이터를 복사하지 않고, 안전하게 소거되는 `SecureBuffer`의 포인터를 반환합니다. - /// - /// # Safety - /// - `ctx`는 유효한 포인터여야 하며, 호출 후 소유권이 소비(consume)되어 내부 상태가 자동 소거됩니다. - /// - 반환된 `SecureBuffer` 포인터는 자바 측에서 사용이 끝난 직후 반드시 `entlib_secure_buffer_free`를 통해 수동으로 해제되어야 합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $finalize_fn(ctx: *mut $struct_type) -> *mut SecureBuffer { - if ctx.is_null() { - return ptr::null_mut(); - } - - let hasher = unsafe { Box::from_raw(ctx) }; - let digest = hasher.finalize(); // Vec 반환 - - // 다이제스트를 SecureBuffer로 캡슐화하여 힙에 할당 - let secure_buffer = Box::new(SecureBuffer { inner: digest }); - Box::into_raw(secure_buffer) - } - - /// 예외 발생 시 해시 컨텍스트 조기 폐기 (early free on exception) - /// - /// # Safety - /// - `ctx`가 null이 아닐 경우 강제로 소유권을 가져와 메모리를 해제합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $free_fn(ctx: *mut $struct_type) { - if !ctx.is_null() { - unsafe { - drop(Box::from_raw(ctx)); - } - } - } - }; -} - -// SHA3 ffi 엔드포인트 자동 생성 -generate_sha3_ffi!( - SHA3_224, - entlib_sha3_224_new, - entlib_sha3_224_update, - entlib_sha3_224_finalize, - entlib_sha3_224_free -); -generate_sha3_ffi!( - SHA3_256, - entlib_sha3_256_new, - entlib_sha3_256_update, - entlib_sha3_256_finalize, - entlib_sha3_256_free -); -generate_sha3_ffi!( - SHA3_384, - entlib_sha3_384_new, - entlib_sha3_384_update, - entlib_sha3_384_finalize, - entlib_sha3_384_free -); -generate_sha3_ffi!( - SHA3_512, - entlib_sha3_512_new, - entlib_sha3_512_update, - entlib_sha3_512_finalize, - entlib_sha3_512_free -); - -macro_rules! generate_shake_ffi { - ($struct_type:ty, $new_fn:ident, $update_fn:ident, $finalize_fn:ident, $free_fn:ident) => { - /// 해시 컨텍스트 초기화 (initialize hash context) - #[unsafe(no_mangle)] - pub extern "C" fn $new_fn() -> *mut $struct_type { - let instance = Box::new(<$struct_type>::new()); - Box::into_raw(instance) - } - - /// 데이터 주입 및 상태 업데이트 (update hash state) - /// - /// # Safety - /// - `ctx`는 `$new_fn`을 통해 할당된 유효한 포인터여야 합니다. - /// - `data` 포인터는 최소 `len` 바이트 크기의 읽기 가능한 메모리를 가리켜야 합니다. - /// - 동일한 컨텍스트에 대한 동시 접근 시 스레드-안전(thread-safe)을 보장하기 위해 호출 측에서 동기화해야 합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $update_fn( - ctx: *mut $struct_type, - data: *const u8, - len: usize, - ) -> i32 { - if ctx.is_null() { - return FFIStatus::NullPointerError as i32; - } - if data.is_null() && len > 0 { - return FFIStatus::NullPointerError as i32; - } - - let slice = unsafe { slice::from_raw_parts(data, len) }; - let hasher = unsafe { &mut *ctx }; - - hasher.update(slice); - - FFIStatus::Success as i32 - } - - /// XOF 연산 완료 및 보안 버퍼 반환 (finalize and return secure buffer) - /// - /// # Arguments - /// * `ctx` - 해시 컨텍스트 포인터 - /// * `output_len` - 자바 측에서 요구하는 가변 다이제스트의 바이트 길이 (variable output length) - /// - /// # Safety - /// - `ctx`는 유효한 포인터여야 하며, 호출 후 소유권이 소비(consume)되어 내부 상태가 자동 소거됩니다. - /// - 반환된 `SecureBuffer` 포인터는 자바 측에서 사용이 끝난 직후 반드시 `entlib_secure_buffer_free`를 통해 수동으로 해제되어야 합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $finalize_fn( - ctx: *mut $struct_type, - output_len: usize, // XOF를 위한 추가 파라미터 - ) -> *mut SecureBuffer { - if ctx.is_null() { - return ptr::null_mut(); - } - - let hasher = unsafe { Box::from_raw(ctx) }; - - // output_len을 전달하여 원하는 길이만큼 다이제스트 추출 - let digest = hasher.finalize(output_len); - - let secure_buffer = Box::new(SecureBuffer { inner: digest }); - Box::into_raw(secure_buffer) - } - - /// 예외 발생 시 해시 컨텍스트 조기 폐기 (early free on exception) - /// - /// # Safety - /// - `ctx`가 null이 아닐 경우 강제로 소유권을 가져와 메모리를 해제합니다. - #[unsafe(no_mangle)] - pub unsafe extern "C" fn $free_fn(ctx: *mut $struct_type) { - if !ctx.is_null() { - unsafe { - drop(Box::from_raw(ctx)); - } - } - } - }; -} - -generate_shake_ffi!( - SHAKE128, - entlib_sha3_shake128_new, - entlib_sha3_shake128_update, - entlib_sha3_shake128_finalize, - entlib_sha3_shake128_free -); -generate_shake_ffi!( - SHAKE256, - entlib_sha3_shake256_new, - entlib_sha3_shake256_update, - entlib_sha3_shake256_finalize, - entlib_sha3_shake256_free -); diff --git a/internal/ffi/src/sha_ffi.rs b/internal/ffi/src/sha_ffi.rs new file mode 100644 index 0000000..afc07cc --- /dev/null +++ b/internal/ffi/src/sha_ffi.rs @@ -0,0 +1,281 @@ +use crate::FFIStandard; +use entlib_native_constant_time::traits::{ConstantTimeEq, ConstantTimeSelect}; +use entlib_native_result::EntLibResult; +use entlib_native_sha2::api::*; +use entlib_native_sha3::api::*; +use std::ptr::write_volatile; +use std::sync::atomic::{Ordering, compiler_fence}; + +const TYPE_ID_SHA2: i8 = 3; +const TYPE_ID_SHA3: i8 = 4; + +macro_rules! impl_ffi_hash_func { + ( + $fn_name:ident, // 생성할 FFI 함수명 + $hasher_type:ty, // 해시 엔진 타입 + $digest_size:expr, // 다이제스트 출력 크기 (바이트 단위) + $type_id:expr // EntLibResult에 사용할 크레이트 식별자 + ) => { + #[unsafe(no_mangle)] + pub unsafe extern "C" fn $fn_name( + input: *const FFIStandard, + output: *mut FFIStandard, + ) -> EntLibResult { + // 포인터 유효성 검증 + if input.is_null() || output.is_null() { + return EntLibResult::new($type_id, -1); + } + + // FFIStandard -> ManuallyDrop 변환 + let in_buffer = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new($type_id, -2), + }; + let out_struct = unsafe { &mut *output }; + + // 출력 버퍼 용량 검증 (공개 정보이므로 분기 허용) + let required_capacity = $digest_size; + if out_struct.len < required_capacity { + return EntLibResult::new($type_id, -3); + } + + // 해시 엔진 인스턴스화 및 연산 수행 + let mut hasher = <$hasher_type>::new(); + hasher.update(in_buffer.as_slice()); + match hasher.finalize() { + Ok(result_buf) => { + let result = result_buf.as_slice(); + unsafe { + let out_len = out_struct.len; + + // 상수-시간 FFI 패딩 복사 + for i in 0..out_len { + // i < required_capacity 반별 (i >= required_capacity 의 NOT) + let is_valid = i.ct_is_ge(&required_capacity).choice_not(); + + // out-of-bounds 접근 방지를 위한 안전한 인덱스 선택 + let safe_idx = usize::ct_select(&0, &i, is_valid); + let valid_byte = result[safe_idx]; + + // 범위를 벗어난 공간은 GC 데이터 유출 막기 위해 상수-시간 0x00 처리 + let byte_to_write = u8::ct_select(&0x00, &valid_byte, is_valid); + write_volatile(out_struct.ptr.add(i), byte_to_write); + } + compiler_fence(Ordering::SeqCst); + } + EntLibResult::new($type_id, 0).add_additional(required_capacity as isize) + } + Err(_) => EntLibResult::new($type_id, -4), + } + } + }; +} + +/// 통합 제어 아키텍처(UCA) JO 패턴에 기반하여, +/// 불완전한 마지막 바이트(last_byte)와 유효 비트 수(valid_bits)를 처리하는 +/// 고정 길이 다이제스트 해시 엔진용 상수-시간 FFI 함수 생성 매크로입니다. +macro_rules! impl_ffi_hash_bits_func { + ( + $fn_name:ident, // 생성할 FFI 함수명 + $hasher_type:ty, // 해시 엔진 타입 + $digest_size:expr, // 다이제스트 출력 크기 (바이트 단위) + $type_id:expr // EntLibResult에 사용할 크레이트 식별자 + ) => { + #[unsafe(no_mangle)] + pub unsafe extern "C" fn $fn_name( + input: *const FFIStandard, + output: *mut FFIStandard, + last_byte: u8, // Q. T. Felix TODO: https://github.com/Quant-Off/entlib-native/pull/9#issuecomment-4059961999 + valid_bits: usize, + ) -> EntLibResult { + // 포인터 유효성 검증 + if input.is_null() || output.is_null() { + return EntLibResult::new($type_id, -1); + } + + // 입력 비트 유효성 검증 (0~7 범위를 벗어난 악의적 입력 차단) + if valid_bits > 7 { + return EntLibResult::new($type_id, -5); + } + + // FFIStandard -> ManuallyDrop 변환 + let in_buffer = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new($type_id, -2), + }; + let out_struct = unsafe { &mut *output }; + + // 출력 버퍼 용량 검증 (고정 크기 다이제스트 기준) + let required_capacity = $digest_size; + if out_struct.len < required_capacity { + return EntLibResult::new($type_id, -3); + } + + // 해시 엔진 인스턴스화 및 데이터 업데이트 + let mut hasher = <$hasher_type>::new(); + hasher.update(in_buffer.as_slice()); + match hasher.finalize_bits(last_byte, valid_bits) { + Ok(result_buf) => { + let result = result_buf.as_slice(); + unsafe { + let out_len = out_struct.len; + + // 상수-시간 FFI 패딩 복사 + // 다이제스트 크기를 초과하는 버퍼 영역은 물리적으로 0x00 완전 소거 + for i in 0..out_len { + let is_valid = i.ct_is_ge(&required_capacity).choice_not(); + + // Out-of-bounds 방지를 위해 인덱스를 상수-시간으로 0으로 폴백(Fallback) + let safe_idx = usize::ct_select(&0, &i, is_valid); + let valid_byte = result[safe_idx]; + + // is_valid가 참이면 valid_byte, 그렇지 않으면 0x00 + let byte_to_write = u8::ct_select(&0x00, &valid_byte, is_valid); + + write_volatile(out_struct.ptr.add(i), byte_to_write); + } + + // JNI 경계 이탈 전 완벽한 메모리 가시성 보장 + compiler_fence(Ordering::SeqCst); + } + EntLibResult::new($type_id, 0).add_additional(required_capacity as isize) + } + Err(_) => EntLibResult::new($type_id, -4), + } + } + }; +} + +/// 바이트가 정확히 맞아떨어지는 일반적인 XOF 연산을 위한 FFI 함수 생성 매크로입니다. +macro_rules! impl_ffi_xof_func { + ( + $fn_name:ident, // 생성할 FFI 함수명 + $hasher_type:ty, // 해시 엔진 타입 + $type_id:expr // EntLibResult에 사용할 크레이트 식별자 + ) => { + #[unsafe(no_mangle)] + pub unsafe extern "C" fn $fn_name( + input: *const FFIStandard, + output: *mut FFIStandard, + ) -> EntLibResult { + if input.is_null() || output.is_null() { + return EntLibResult::new($type_id, -1); + } + + let in_buffer = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new($type_id, -2), + }; + let out_struct = unsafe { &mut *output }; + + let requested_out_len = out_struct.len; + + // 비정상적으로 거대한 메모리 할당 요청 차단 + if requested_out_len == 0 || requested_out_len > 16_777_216 { + return EntLibResult::new($type_id, -3); + } + + let mut hasher = <$hasher_type>::new(); + hasher.update(in_buffer.as_slice()); + + match hasher.finalize(requested_out_len) { + Ok(result_buf) => { + let result = result_buf.as_slice(); + unsafe { + // XOF는 요청한 길이(requested_out_len)만큼 정확히 버퍼를 생성 + // 고정 길이 SHA-2처럼 초과분에 대한 상수-시간 패딩(0x00)이 필요없음 + for i in 0..requested_out_len { + write_volatile(out_struct.ptr.add(i), result[i]); + } + + // JNI 경계를 넘기 전 메모리 가시성(Visibility) 보장 + compiler_fence(Ordering::SeqCst); + } + EntLibResult::new($type_id, 0).add_additional(requested_out_len as isize) + } + Err(_) => EntLibResult::new($type_id, -4), + } + } + }; +} + +/// 불완전한 마지막 바이트(last_byte)와 유효 비트 수(valid_bits) 처리가 필요한 +/// 고급 XOF(cSHAKE, KMAC 등 파생 알고리즘) FFI 함수 생성 매크로입니다. +macro_rules! impl_ffi_xof_bits_func { + ( + $fn_name:ident, + $hasher_type:ty, + $type_id:expr + ) => { + #[unsafe(no_mangle)] + pub unsafe extern "C" fn $fn_name( + input: *const FFIStandard, + output: *mut FFIStandard, + last_byte: u8, + valid_bits: usize, + ) -> EntLibResult { + if input.is_null() || output.is_null() { + return EntLibResult::new($type_id, -1); + } + + // 비트 수 유효성 검증 (0~7 범위 이탈 시 에러 반환) + if valid_bits > 7 { + return EntLibResult::new($type_id, -5); + } + + let in_buffer = match unsafe { (*input).into_domain_buffer() } { + Ok(buf) => buf, + Err(_) => return EntLibResult::new($type_id, -2), + }; + let out_struct = unsafe { &mut *output }; + + let requested_out_len = out_struct.len; + + if requested_out_len == 0 || requested_out_len > 16_777_216 { + return EntLibResult::new($type_id, -3); + } + + let mut hasher = <$hasher_type>::new(); + hasher.update(in_buffer.as_slice()); + match hasher.finalize_bits(requested_out_len, last_byte, valid_bits) { + Ok(result_buf) => { + let result = result_buf.as_slice(); + unsafe { + for i in 0..requested_out_len { + write_volatile(out_struct.ptr.add(i), result[i]); + } + compiler_fence(Ordering::SeqCst); + } + EntLibResult::new($type_id, 0).add_additional(requested_out_len as isize) + } + Err(_) => EntLibResult::new($type_id, -4), + } + } + }; +} + +// SHA2 ffi 엔드포인트 생성 +impl_ffi_hash_func!(ffi_sha2_224, SHA224, 28, TYPE_ID_SHA2); +impl_ffi_hash_func!(ffi_sha2_256, SHA256, 32, TYPE_ID_SHA2); +impl_ffi_hash_func!(ffi_sha2_384, SHA384, 48, TYPE_ID_SHA2); +impl_ffi_hash_func!(ffi_sha2_512, SHA512, 64, TYPE_ID_SHA2); + +// SHA3 ffi 엔드포인트 생성 +impl_ffi_hash_func!(ffi_sha3_224, SHA3_224, 28, TYPE_ID_SHA3); +impl_ffi_hash_func!(ffi_sha3_256, SHA3_256, 32, TYPE_ID_SHA3); +impl_ffi_hash_func!(ffi_sha3_384, SHA3_384, 48, TYPE_ID_SHA3); +impl_ffi_hash_func!(ffi_sha3_512, SHA3_512, 64, TYPE_ID_SHA3); + +// SHA3 계열 부분 바이트 FFI 인터페이스 생성 +impl_ffi_hash_bits_func!(ffi_sha3_224_bits, SHA3_256, 28, TYPE_ID_SHA3); +impl_ffi_hash_bits_func!(ffi_sha3_256_bits, SHA3_512, 32, TYPE_ID_SHA3); +impl_ffi_hash_bits_func!(ffi_sha3_384_bits, SHA3_256, 48, TYPE_ID_SHA3); +impl_ffi_hash_bits_func!(ffi_sha3_512_bits, SHA3_512, 64, TYPE_ID_SHA3); + +// 일반적인 바이트 정렬 SHAKE 인터페이스 생성 +impl_ffi_xof_func!(ffi_shake128, SHAKE128, TYPE_ID_SHA3); +impl_ffi_xof_func!(ffi_shake256, SHAKE256, TYPE_ID_SHA3); + +// 비트 단위 패딩이 필요한 경우를 위한 인터페이스 생성 +impl_ffi_xof_bits_func!(ffi_shake128_bits, SHAKE128, TYPE_ID_SHA3); +impl_ffi_xof_bits_func!(ffi_shake256_bits, SHAKE256, TYPE_ID_SHA3); diff --git a/scripts/catch_hex_dudect_audit.sh b/scripts/catch_hex_dudect_audit.sh new file mode 100755 index 0000000..6cfd05d --- /dev/null +++ b/scripts/catch_hex_dudect_audit.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -euo pipefail + +cargo +nightly build --release -p entlib-native-constant-time --bench dudect_audit + +BIN=$(find ./target/release/deps/ -maxdepth 1 -type f -name "dudect_audit-*" -perm +111 | head -n 1) + +OUT=$("$BIN") +echo "$OUT" + +FAIL=0 + +echo "$OUT" | awk -F'max t = ' '/max t =/ { + split($2,a,",") + t=a[1]+0 + if (t<0) t=-t + if (t>=4.5) { + printf("FAIL: |max t| = %f >= 4.5\n", t) + exit 1 + } else { + printf("PASS: |max t| = %f < 4.5\n", t) + } +} +' \ No newline at end of file diff --git a/scripts/extract_ct_symbols.sh b/scripts/extract_ct_symbols.sh new file mode 100755 index 0000000..2d697c0 --- /dev/null +++ b/scripts/extract_ct_symbols.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# 컴파일된 바이너리에서 검증 대상 상수-시간 함수를 Zero-Trust 기반으로 자동 추출하기 위해 만듦 + +set -euo pipefail + +TARGET_ARCH=$1 +PACKAGE_NAME="entlib-native-constant-time" +CRATE_NAME="entlib_native_constant_time" + +echo "[INFO] $TARGET_ARCH 아키텍처 바이너리 심볼 추출 시작" + +# 라이브러리 내의 모든 심볼 목록 추출 +# stderr 노이즈를 제거하여 순수 심볼 목록만 ALL_SYMBOLS에 담음 +# cargo asm 자체가 실패할 경우 명시적인 에러 메시지 던짐 +ALL_SYMBOLS=$(cargo asm -p "$PACKAGE_NAME" --features audit_mode --target "$TARGET_ARCH" --release --lib 2>&1 || true) +if [ -z "$ALL_SYMBOLS" ]; then + echo "[CRITICAL] cargo asm 실행에 실패하여 심볼을 추출할 수 없습니다!" + exit 1 +fi + +# 대상 크레이트 네임스페이스 및 핵심 보안 트레이트 메소드 패턴 정의 +# 명시된 모든 ConstantTime 연산 포함 +PATTERN_PREFIX="audit_verify_" +SECURE_PATTERNS="${PATTERN_PREFIX}(choice_from_mask_normalized|choice_not|choice_unwrap_u8|u64_ct_eq|u64_ct_is_ge|u64_ct_is_negative|u64_ct_is_zero|u64_ct_ne|u64_ct_select|u64_ct_swap)" + +# 심볼 필터링 및 배열 저장 +# - 첫 번째 안내 문구 제거 및 공백으로 분리된 항목을 줄 단위로 변환 +# - grep -E: 보안 패턴 메소드 필터링 +# - sed: 쌍따옴표, [bytes] 부분, 숫자 인덱스 제거 및 불필요한 공백 정리 +mapfile -t TARGET_FUNCTIONS < <(echo "$ALL_SYMBOLS" \ + | sed 's/Try one of those by name or a sequence number //' \ + | tr ' ' '\n' \ + | grep -E "$SECURE_PATTERNS" \ + | sed -E 's/"//g' \ + | sed -E 's/\[[0-9]+\]//g') + +# 검증 실패 통제 +if [ ${#TARGET_FUNCTIONS[@]} -eq 0 ]; then + echo "[CRITICAL] 추출된 타겟 함수가 없습니다! 심볼 테이블이 손상되었거나 컴파일러에 의해 모두 최적화(DCE)되었을 가능성이 있습니다." + echo "--- [디버그: 원본 심볼 목록 (상위 20개)] ---" + echo "$ALL_SYMBOLS" | head -n 20 + echo "--------------------------------------------------------" + exit 1 +fi + +echo "--- [자동 추출된 검증 대상 함수 목록: ${#TARGET_FUNCTIONS[@]}개] ---" +for FUNC in "${TARGET_FUNCTIONS[@]}"; do + echo "$FUNC" +done +echo "--------------------------------------------------------" + +# 환경 변수로 내보내어 CI 파이프라인의 다음 단계에서 사용 가능하도록 조치 (JSON 형식 변환) +# jq를 사용하여 쉘 배열을 JSON 배열 문자열로 안전히 직렬화 +printf '%s\n' "${TARGET_FUNCTIONS[@]}" | jq -R . | jq -s . > target_functions.json \ No newline at end of file diff --git a/scripts/verify_ct_asm.sh b/scripts/verify_ct_asm.sh old mode 100755 new mode 100644