diff --git a/.github/workflows/secure_release.yml b/.github/workflows/secure_release.yml
new file mode 100644
index 0000000..8294193
--- /dev/null
+++ b/.github/workflows/secure_release.yml
@@ -0,0 +1,191 @@
+name: Secure Release & Publish Pipeline
+
+on:
+ pull_request:
+ branches: [ master ]
+ push:
+ tags:
+ - 'v*.*.*'
+
+permissions:
+ contents: write
+ pages: write
+ id-token: write
+
+jobs:
+ verify-integrity:
+ name: Source Integrity Verification
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Build entlib-native
+ run: cargo build --release
+
+ - name: Generate Source Archives
+ run: |
+ VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "dev")
+ ARCHIVE_NAME="entlib-native-${VERSION}-source"
+ mkdir -p target/dist
+ git archive \
+ --format=tar.gz \
+ --prefix="${ARCHIVE_NAME}/" \
+ -o "target/dist/${ARCHIVE_NAME}.tar.gz" \
+ HEAD
+ git archive \
+ --format=zip \
+ --prefix="${ARCHIVE_NAME}/" \
+ -o "target/dist/${ARCHIVE_NAME}.zip" \
+ HEAD
+ cp ./public/public-key.asc target/dist/public-key.asc
+ echo "[+] Source archives generated"
+ ls -lh target/dist/
+
+ - name: Import Public Key (Trust Anchor)
+ run: gpg --import ./public/public-key.asc
+
+ - name: Verify PGP Signature
+ run: |
+ echo "[*] Verifying PGP signature of RELEASE_HASHES.txt..."
+ gpg --verify ./public/RELEASE_HASHES.txt.asc ./public/RELEASE_HASHES.txt
+ echo "[+] PGP signature verification passed"
+
+ create-release:
+ name: Create GitHub Release
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs: verify-integrity
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Build entlib-native
+ run: cargo build --release
+
+ - name: Prepare Release Artifacts
+ env:
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ run: |
+ VERSION=$(git describe --tags --exact-match)
+ ARCHIVE_NAME="entlib-native-${VERSION}-source"
+ mkdir -p target/dist
+ git archive \
+ --format=tar.gz \
+ --prefix="${ARCHIVE_NAME}/" \
+ -o "target/dist/${ARCHIVE_NAME}.tar.gz" \
+ HEAD
+ git archive \
+ --format=zip \
+ --prefix="${ARCHIVE_NAME}/" \
+ -o "target/dist/${ARCHIVE_NAME}.zip" \
+ HEAD
+ cp ./public/public-key.asc target/dist/public-key.asc
+ cp ./public/RELEASE_HASHES.txt target/dist/RELEASE_HASHES.txt
+ cp ./public/RELEASE_HASHES.txt.asc target/dist/RELEASE_HASHES.txt.asc
+
+ - name: Extract Tag Version
+ id: version
+ run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ name: "entlib-native ${{ steps.version.outputs.tag }}"
+ body: |
+ ## Source Distribution
+
+ | File | Description |
+ |------|-------------|
+ | `entlib-native-${{ steps.version.outputs.tag }}-source.tar.gz` | Source archive (tar.gz) |
+ | `entlib-native-${{ steps.version.outputs.tag }}-source.zip` | Source archive (zip) |
+ | `RELEASE_HASHES.txt` | SHA3-512 + BLAKE3 checksums |
+ | `RELEASE_HASHES.txt.asc` | PGP detached signature |
+ | `public-key.asc` | PGP public key (Trust Anchor) |
+
+ ## Verify Integrity
+
+ ```bash
+ gpg --import public-key.asc
+ gpg --verify RELEASE_HASHES.txt.asc RELEASE_HASHES.txt
+ ```
+ files: |
+ ./target/dist/entlib-native-*-source.tar.gz
+ ./target/dist/entlib-native-*-source.zip
+ ./target/dist/RELEASE_HASHES.txt
+ ./target/dist/RELEASE_HASHES.txt.asc
+ ./target/dist/public-key.asc
+
+ publish-trust-anchor:
+ name: Publish Trust Anchor to GitHub Pages
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs: create-release
+ runs-on: ubuntu-latest
+
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Extract Tag Version
+ id: version
+ run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
+
+ - name: Build Pages Site
+ env:
+ RELEASE_TAG: ${{ steps.version.outputs.tag }}
+ run: |
+ mkdir -p _site/keys "_site/releases/${RELEASE_TAG}"
+
+ cp ./public/public-key.asc _site/keys/public-key.asc
+
+ cp ./public/RELEASE_HASHES.txt \
+ "_site/releases/${RELEASE_TAG}/RELEASE_HASHES.txt"
+ cp ./public/RELEASE_HASHES.txt.asc \
+ "_site/releases/${RELEASE_TAG}/RELEASE_HASHES.txt.asc"
+
+ cat > _site/index.html << 'PAGE'
+
+
+
entlib-native Trust Anchor
+
+ entlib-native Trust Anchor
+ PGP Public Key
+ public-key.asc
+ Verification
+
+ # 1. Import the public key
+ curl -sO https://quant-off.github.io/entlib-native/keys/public-key.asc
+ gpg --import public-key.asc
+
+ # 2. Download the release hashes and signature
+ VERSION="vX.Y.Z"
+ curl -sO "https://quant-off.github.io/entlib-native/releases/${VERSION}/RELEASE_HASHES.txt"
+ curl -sO "https://quant-off.github.io/entlib-native/releases/${VERSION}/RELEASE_HASHES.txt.asc"
+
+ # 3. Verify the signature
+ gpg --verify RELEASE_HASHES.txt.asc RELEASE_HASHES.txt
+
+
+
+ PAGE
+
+ - name: Upload Pages Artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: _site
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md
index e44001b..0987025 100644
--- a/CONTRIBUTION.md
+++ b/CONTRIBUTION.md
@@ -64,9 +64,12 @@ _안녕하세요. 저희는 팀 퀀트(Quant)이며, 저는 Quant Theodore Felix
- 공통
- **올바른 오류 전파 방법**: 많은 크레이트의 핵심 기능은 `Result` 열거형을 통해 `SecureBuffer` 구조체와 문자열 참조를 반환합니다. 이는 오류 전파에 부적절합니다.
- **컴플라이언스 문제**: 암호 모듈 구현에 있어 국제적 인증 및 규정을 준수하지 않은 부분을 발견했다면, 즉시 연락주세요.
+ - **오류 메시지**: 오류 메시지는 기본적으로 모호해야 하지만 알아차리기 애-매한 정도로 진실성이 있어야 합니다. 현재 오류 메시지는 어때 보이시나요?
+ - **보안 논의**: [`entlib-native`의 보안성에 대해 논의하는 문서](SECURITY_DISCUSSION.md)에 대해 의견이 있으신가요?
- 보안 버퍼 크레이트 `entlib-native-secure-buffer`
- **베어메탈 캐시 플러시 문제**: `zeroizer.rs` 내 no_std 폐쇄 환경을 위한 Fall-back 시, 해당 환경의 하드웨어(CPU) 특성에 따라 캐시 라인 플러시가 보장되지 않을 수 있다고 합니다. 이 부분에 대해 섬세한 평가검증이 필요합니다.
- **이중 잠금**: JO(Java-Owned) 패턴을 통해 상호 작용 시 메모리 lock 수행 후 전달됩니다. Rust 측 `SecureMemoryBlock` 구조체는 이 데이터에 대해 한 번 더 lock을 수행합니다. 이 작업에 대해 어떻게 생각하시나요?
+ - **베어메탈 대응**: 최신 IoT, HSM, 자동차 천장 시스템(Automotive) 등은 대부분 ARM 기반의 베어메 또는 RTOS 환경에서 구동됩니다. 현재 보안 버퍼는 `mlock` 등의 시스템 콜을 이용해 메모리를 잠그고 있는데, 베어메탈에선 이러한 대응이 불가능합니다. 소프트웨어 레벨에서 '가능한 대응'에 대한 아이디어가 필요합니다.
- CI 워크플로
- **엄격한 상수-시간 검사**: 현재 구현된 상수-시간 연산이 부족해 보이시거나, 엄격한 검증을 위해서는 어떻게 해야 한다고 생각하시나요?
- **메모리 오염 추적 방법**: CC 상수-시간 감사 워크플로의 Level 3(바이너리 메모리 오염 추적)은 Unix 환경에서 Valgrind를 사용하여 테스트를 수행합니다. 하지만 저는 아직 이 부분에 대해 큰 아이디어가 없어 임시 비활성화해둔 상태입니다. 이 부분에 대해 좋은 아이디어를 가지고 있다면 알려주세요.
diff --git a/CONTRIBUTION_EN.md b/CONTRIBUTION_EN.md
index a28396f..c36067e 100644
--- a/CONTRIBUTION_EN.md
+++ b/CONTRIBUTION_EN.md
@@ -64,9 +64,12 @@ Contributions corresponding to the following items for this project are classifi
- Common
- **Correct error propagation method**: The core function of many crates returns a `SecureBuffer` struct and a string reference through a `Result` enum. This is inappropriate for error propagation.
- **Compliance issues**: If you find any parts that do not comply with international certifications and regulations in the implementation of the cryptographic module, please contact us immediately.
+ - **Error messages**: Error messages should be ambiguous by default, but they must be truthful enough to be subtly recognizable. What do you think of the current error messages?
+ - **Security Discussion**: Do you have any opinions on the [document discussing the security of `entlib-native`](SECURITY_DISCUSSION.md)?
- Secure buffer crate `entlib-native-secure-buffer`
- **Bare-metal cache flush issue**: When falling back for a no_std closed environment in `zeroizer.rs`, it is said that cache line flushing may not be guaranteed depending on the hardware (CPU) characteristics of the environment. Delicate evaluation and verification are needed for this part.
- **Double lock**: When interacting through the JO (Java-Owned) pattern, the memory is locked and then transmitted. The `SecureMemoryBlock` struct on the Rust side performs another lock on this data. What do you think about this operation?
+ - **Bare-metal support**: Most modern IoT, HSM, and automotive systems run on ARM-based bare-metal or RTOS environments. Currently, the secure buffer uses system calls like `mlock` to lock memory, but such responses are impossible in bare-metal environments. We need ideas for "possible responses" at the software level.
- CI workflow
- **Strict constant-time check**: Do you think the currently implemented constant-time operation is insufficient, or what do you think should be done for strict verification?
- **How to track memory corruption**: Level 3 (binary memory corruption tracking) of the CC constant-time audit workflow uses Valgrind to perform tests in a Unix environment. However, I have temporarily disabled it because I don't have a big idea about this part yet. Please let me know if you have a good idea about this.
diff --git a/Cargo.toml b/Cargo.toml
index 20933ce..bfd416c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[workspace]
-members = ["internal/*", "crypto/*", "core/*"]
+members = ["internal/*", "crypto/*", "core/*", "cli", "canary"]
resolver = "2"
[workspace.package]
@@ -27,17 +27,25 @@ entlib-native-quantum-util = { path = "internal/quantum-util", version = "2.0.0"
### CORE DEPENDENCIES ###
entlib-native-rng = { path = "core/rng", version = "2.0.0" }
entlib-native-hex = { path = "core/hex", version = "2.0.0" }
+entlib-native-base = { path = "core/base", version = "2.0.0" }
entlib-native-result = { path = "core/result", version = "2.0.0" }
entlib-native-base64 = { path = "core/base64", version = "2.0.0" }
entlib-native-secure-buffer = { path = "core/secure-buffer", version = "2.0.0" }
entlib-native-constant-time = { path = "core/constant-time", version = "2.0.0" }
### INTERNAL CRYPTO DEPENDENCIES ###
entlib-native-tls = { path = "crypto/tls", version = "2.0.0" }
+entlib-native-aes = { path = "crypto/aes", version = "2.0.0" }
entlib-native-hkdf = { path = "crypto/hkdf", version = "2.0.0" }
entlib-native-hmac = { path = "crypto/hmac", version = "2.0.0" }
entlib-native-sha2 = { path = "crypto/sha2", version = "2.0.0" }
entlib-native-sha3 = { path = "crypto/sha3", version = "2.0.0" }
+entlib-native-blake = { path = "crypto/blake", version = "2.0.0" }
+entlib-native-pkcs8 = { path = "crypto/pkcs8", version = "2.0.0" }
+entlib-native-armor = { path = "crypto/armor", version = "2.0.0" }
+entlib-native-mlkem = { path = "crypto/mlkem", version = "2.0.0" }
entlib-native-mldsa = { path = "crypto/mldsa", version = "2.0.0" }
+entlib-native-pbkdf2 = { path = "crypto/pbkdf2", version = "2.0.0" }
+entlib-native-argon2id = { path = "crypto/argon2id", version = "2.0.0" }
entlib-native-chacha20 = { path = "crypto/chacha20", version = "2.0.0" }
entlib-native-key-establishment = { path = "crypto/key-establishment", version = "2.0.0" }
entlib-native-digital-signature = { path = "crypto/digital-signature", version = "2.0.0" }
diff --git a/README.md b/README.md
index ba6bfa7..85f7504 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://github.com/Quant-Off/entlib-native)
[](LICENSE)
-[](https://github.com/Quant-Off/entlib-native)
+[](https://github.com/Quant-Off/entlib-native)

@@ -10,7 +10,7 @@
> [English README](README_EN.md)
-EntanglementLib의 보안 기능을 완벽히 수행하기 위한 네이티브 베이스 언어는 Rust가 가장 잘 어울립니다. 이 언어의 가장 큰 장점은 성능 저하 없이 메모리 안정성을 보장하는 거예요. 세부적으로 [소유권 개념(Ownership)](https://doc.rust-kr.org/ch04-00-understanding-ownership.html)은 자원 관리를 용이하게 하고, **데이터 경쟁 없는 동시성 기능**은 통해 멀티 스레드 환경에서도 보안성을 강화해줍니다.
+EntanglementLib의 보안 기능을 완벽히 수행하기 위한 네이티브 베이스 언어는 Rust가 가장 잘 어울립니다. 이 언어의 가장 큰 장점은 성능 저하 없이 메모리 안정성을 보장하는 것입니다. 세부적으로 [소유권 개념(Ownership)](https://doc.rust-kr.org/ch04-00-understanding-ownership.html)은 자원 관리를 용이하게 하고, **데이터 경쟁 없는 동시성 기능**은 멀티 스레드 환경에서도 보안성을 강화해줍니다.
Python이나 JPMS(Java Platform Module System)와 일관된 모듈 관리, 캡슐화가 간편한 등, 언어 자체가 유연한 특성을 가지고 있으며 FFI(Foreign Function Interface)로 Java와 간편히 연결되는 것은 충분히 매력으로 다가옵니다.
@@ -38,6 +38,9 @@ Python이나 JPMS(Java Platform Module System)와 일관된 모듈 관리, 캡
얽힘 라이브러리의 최종 보안 목표는 CC EAL5+ 이상(EAL7)의 등급을 취득하는 것입니다. 이를 위해서는 하드웨어 레벨에서의 엄격한 설계, 정형적 명세 등의 까다롭고 복잡한 준비가 필요하지만 향후 군사급 보안에 다다를 예정입니다. 저는 이를 위한 아키텍처 설계 중에 있습니다.
+> [!NOTE]
+> 저희는 보안에 관해 더 깊게 논의해보고자 합니다. 이를 위해 [보안 논의 문서](SECURITY_DISCUSSION.md)를 참고해주세요.
+
## 향후 계획
지원되는 고전적 암호화 알고리즘 모듈을 다양하게 구현해야 합니다.
@@ -45,27 +48,34 @@ Python이나 JPMS(Java Platform Module System)와 일관된 모듈 관리, 캡
- AEAD
- [ ] ChaCha20
- BlockCipher
- - [ ] AES(128, 192, 256)
- - [ ] ARIA(128, 192, 256)
+ - [X] AES-256 (GCM, CBC-HMAC)
+- KDF
+ - [X] PBKDF2
+ - [X] Argon2id
- Digital Signature
- [ ] RSA(2048, 4096, 8192)
- [ ] ED25519, ED448 서명
- [ ] X25519, X448 키 합의
-
-이 뿐만 아니라 HMAC, HKDF 등의 암호학적 필수 기능도 제공되어야 합니다.
+- Serializer / Encode Pipeline
+ - [X] DER
+ - [X] PEM
+- PKC Standard Pipeline
+ - [X] PKCS #8
+ - [PKCS #11](https://docs.oasis-open.org/pkcs11/pkcs11-base/v2.40/os/pkcs11-base-v2.40-os.html)
+ - [ ] C-API FFI 매핑
+ - [ ] Dyn Loader (시스템 콜 방식)
양자-내성 암호화(Post-Quantum Cryptography, PQC) 알고리즘은 다음의 목표를 가집니다.
-- [ ] [FIPS 203(Module Lattice-based Key Encapsulate Mechanism, ML-KEM)](https://csrc.nist.gov/pubs/fips/203/final)
+- [X] [FIPS 203(Module Lattice-based Key Encapsulate Mechanism, ML-KEM)](https://csrc.nist.gov/pubs/fips/203/final)
- [X] [FIPS 204(Module Lattice-based Digital Signature Algorithm, ML-DSA)](https://csrc.nist.gov/pubs/fips/204/final)
- [ ] [FIPS 205(Stateless Hash-based Digital Signature Algorithm, SLH-DSA)](https://csrc.nist.gov/pubs/fips/205/final)
-위 PQC 알고리즘이 구현되면 다음의 TLS 기능도 제공되어야 합니다.
+그리고 다음의 TLS 기능도 제공되어야 합니다.
- [ ] TLS 1.3
- [ ] [`draft-ietf-tls-ecdhe-mlkem`](https://datatracker.ietf.org/doc/draft-ietf-tls-ecdhe-mlkem/)에 따른 X25519MLKEM768
-
-PKIX나 JWT 및 CWT, OTP 등, 아직 갈 길이 멀다는 것이 실감됩니다.
+- [ ] X9.146 QTLS 확장 표준
## 인증 및 규정 준수 필요
diff --git a/README_EN.md b/README_EN.md
index bb6ea05..d4a628c 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -38,34 +38,44 @@ Of course, this is not a formal verification, but only an internal evaluation. T
The final security goal of the Entanglement Library is to obtain a grade of CC EAL5+ or higher (EAL7). This requires difficult and complex preparations such as strict design at the hardware level and formal specifications, but it is planned to reach military-grade security in the future. I am in the process of designing the architecture for this.
+> [!NOTE]
+> We would like to discuss security in more depth. For this, please refer to the [Security Discussion Document](SECURITY_DISCUSSION.md).
+
## Future Plans
We need to implement a variety of supported classic cryptographic algorithm modules.
- AEAD
- - [ ] ChaCha20
-- BlockCipher
- - [ ] AES(128, 192, 256)
- - [ ] ARIA(128, 192, 256)
+ - [ ] ChaCha20
+- Block Cipher
+ - [X] AES-256 (GCM, CBC-HMAC)
+- KDF (Key Derivation Function)
+ - [X] PBKDF2
+ - [X] Argon2id
- Digital Signature
- - [ ] RSA(2048, 4096, 8192)
- - [ ] ED25519, ED448 signature
- - [ ] X25519, X448 key agreement
-
-In addition, cryptographic essential functions such as HMAC and HKDF must also be provided.
-
-The Post-Quantum Cryptography (PQC) algorithm has the following goals.
-
-- [ ] [FIPS 203 (Module Lattice-based Key Encapsulate Mechanism, ML-KEM)](https://csrc.nist.gov/pubs/fips/203/final)
-- [X] [FIPS 204(Module Lattice-based Digital Signature Algorithm, ML-DSA)](https://csrc.nist.gov/pubs/fips/204/final)
-- [ ] [FIPS 205 (Stateless Hash-based Digital Signature Algorithm, SLH-DSA)](https://csrc.nist.gov/pubs/fips/205/final)
-
-Once the above PQC algorithm is implemented, the following TLS features must also be provided.
+ - [ ] RSA (2048, 4096, 8192)
+ - [ ] ED25519, ED448 Signatures
+ - [ ] X25519, X448 Key Agreement
+- Serializer / Encode Pipeline
+ - [X] DER
+ - [X] PEM
+- PKC Standard Pipeline
+ - [X] PKCS #8
+ - [PKCS #11](https://docs.oasis-open.org/pkcs11/pkcs11-base/v2.40/os/pkcs11-base-v2.40-os.html)
+ - [ ] C-API FFI Mapping
+ - [ ] Dyn Loader (System Call-based)
+
+Post-Quantum Cryptography (PQC) algorithms aim to achieve the following goals.
+
+- [X] FIPS 203 (Module Lattice-based Key Encapsulation Mechanism, ML-KEM)
+- [x] FIPS 204 (Module Lattice-based Digital Signature Algorithm, ML-DSA)
+- [ ] FIPS 205 (Stateless Hash-based Digital Signature Algorithm, SLH-DSA)
+
+Additionally, the following TLS features must be supported.
- [ ] TLS 1.3
-- [ ] X25519MLKEM768 according to [`draft-ietf-tls-ecdhe-mlkem`](https://datatracker.ietf.org/doc/draft-ietf-tls-ecdhe-mlkem/)
-
-I realize that there is still a long way to go, such as PKIX, JWT and CWT, and OTP.
+- [ ] X25519MLKEM768 in accordance with `draft-ietf-tls-ecdhe-mlkem`
+- [ ] X9.146 QTLS Extension Standard
## Certification and Compliance Required
diff --git a/SECURITY.md b/SECURITY.md
index 015124a..611b896 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -22,15 +22,6 @@
> [!NOTE]
> 보안 통신을 위해 PGP 키가 필요한 경우, 저장소 내의 [KEYS](KEYS) 파일을 확인하거나 요청해 주십시오.
-### 처리 절차
-
-제보된 취약점은 다음과 같은 절차로 처리됩니다.
-
-1. **접수 확인:** 48시간 이내에 제보자에게 접수 확인 메일을 발송합니다.
-2. **분석 및 검증:** 퀀트 팀 내부에서 취약점의 영향도와 재현 가능성을 세밀히 분석합니다.
-3. **패치 개발:** 문제가 확인되면 `entlib-native` 또는 `entanglementlib`의 핫픽스를 개발합니다.
-4. **공개 및 배포:** 패치가 완료되고 릴리즈된 후, 제보자와 협의하여 적절한 시점에 취약점 정보를 공개(Disclosure)합니다.
-
## 보안 중점 사항 (Security Focus Areas)
이 프로젝트는 특히 다음 영역의 보안을 중요하게 다룹니다.
@@ -51,7 +42,8 @@
* **실험적 기능:** 명시적으로 "실험적(Experimental)"이라고 표시된 기능의 버그
* **사용자 환경 문제:** 사용자의 OS나 하드웨어 자체의 결함으로 인한 문제
-[기여 문서](CONTRIBUTION.md)에서 자세한 사항을 확인할 수 있습니다.
+> [!TIP]
+> [기여 문서](CONTRIBUTION.md)에서 자세한 사항을 확인할 수 있습니다.
## 감사의 말
diff --git a/SECURITY_DISCUSSION.md b/SECURITY_DISCUSSION.md
new file mode 100644
index 0000000..1d61e0e
--- /dev/null
+++ b/SECURITY_DISCUSSION.md
@@ -0,0 +1,31 @@
+# 보안성 논의
+
+> [English SECURITY DISCUSSION](SECURITY_DISCUSSION_EN.md)
+
+`entlib-native`에는 분명 데이터 관리, 상수-시간에 대한 안전한 연산을 제공합니다만, 이것을 '안전하다' 라고 말하기 전 LLVM 컴파일러에 대한 공격적 최적화 문제에 대해 논의해볼 필요가 있습니다.
+
+## LLVM 컴파일러의 고질적인 문제
+
+기술적으로 Rust의 백엔드인 LLVM은 코드의 평균 실행 속도 향상을 최우선으로 최적화를 수행합니다. 이 과정에서 개발자가 부채널 공격(Timing Side-Channel Attack)을 방어하기 위해 의도적으로 작성한 상수-시간 비트 연산이나 수학적 트릭을 LLVM의 `SimplifyCFG` 패스 등이 임의로 조건부 분기(예로, 어셈블리 상에서 `cmp` 후 `jmp`)로 변환해버리는 사례가 지속적으로 보고되고 있습니다.
+
+`entlib-native`와 같은 고보안 원칙에 의거한 암호 기능을 제공하는 프레임워크, 라이브러리 등은 외부 의존성 없이 모든 핵심 보안 모듈을 직접 캡슐화하여 구현해야 함으로써, FIPS 140-2/3 및 CC EAL2(또는 EAL3) 수준의 엄격한 검증을 통과해야 하는 환경이라면 이를 방지하기 위한 대책을 엄격히 마련해야 합니다.
+
+결론만 빠르게 이야기하자면 저희가 생각한 방안으로는, 인-라인 어셈블리를 활용하는 것입니다. 이는 LLVM 최적화기가 절대 개입할 수 없는 블랙박스 구간을 만드는 가장 확실한 방법입니다. 조건부 로직이 필요한 부분에서 소프트웨어적인 비트 연산 대신, 하드웨어가 지원하는 상수-시간 조건부 이동 명령어를 직접 호출해야 합니다. 예를 들어 `x86_64` 이키텍처에선 조건부 이동(`cmov`) 명령어를 사용할 수 있고, `aarch64` 아키텍처에선 조건부 선택(`csel`) 명령어를 사용할 수 있습니다. 이러한 방식은 컴파일러의 명령어 선택 단계를 우회하므로 컴파일러 버전이 업데이트되더라도 분기문이 삽입되는 것을 완벽하게 차단할 수 있습니다.
+
+컴파일러 베리어와 휘발성(Volatile) 연산 및 메모리 베리어에 대해 좀 더 논의할 수 있습니다. 컴파일러 베리어(`core::hint::black_box`)는 컴파일러에게 특정 값을 최적화 분석 과정에서 무시하도록 지시합니다. 이를 통해 컴파일러가 입력값을 예측하여 상수 폴딩을 하거나, 데드 코드 제거(Dead Code Elimination, DCE)를 수행하는 것을 막을 수 있습니다.
+
+하지만 Rust 공식 문서에서도 명시하듯 `black_box`는 최적화를 억제할 뿐, 암호학적 상수-시간 실행을 절대적으로 보장하지 않습니다. 즉, 변수 값이 최적화되는 것은 막아도, 그 변수를 처리하는 연산 자체를 컴파일러가 분기문으로 컴파일하는 것까지 완벽히 통제할 수는 없습니다. 따라서 메인 방어 수단이 아닌 보조 수단으로만 접근해야 합니다.
+
+휘발성 연산 및 메모리 베리어의 경우는 어떨까요? 이 방식에 따라 데이터의 생명 주기(할당부터 소거까지) 전반에 걸쳐 최적화를 막으려면 `core::ptr::read_volatile` 및 `write_volatile`을 사용할 수 있습니다. 포렌식 불가능한 수준의 메모리 소거를 구현할 때, 일반적인 메모리 쓰기를 사용하면 LLVM은 "이후에 사용되지 않는 메모리 쓰기"로 판단하여 소거 로직 자체를 삭제(Dead Store Elimination, DSE)해버립니다. 휘발성 연산은 컴파일러가 메모리 접근을 생략하거나 순서를 재배치하는 것을 막아줍니다.
+
+`entlib-native`의 모든 연산에서는 분명 상수-시간 연산을 지원합니다. 결과적으로 인-라인 어셈블리 및 컴파일러 베리어의 제한적 활용, 휘발성 연산과 메모리 베리어를 사용하여 LLVM 측 최적화를 통제하는 기술적 모멘트를 보여주기도 합니다.
+
+## 안전하지 않은 연산에 대해
+
+하지만 우리는 `entlib-native`가 LLVM 공격적 최적화 문제를 해결하기 위해 기본적으로 "안전하지 않은 연산을 사용한다." 라는 맥락에서 논의를 더 이어갈 수 있습니다. 암호학적인 안정성을 달성하기 위해선 컴파일러의 통제를 벗어나야 하지만, 이는 Rust의 핵심 가치인 "메모리 안정성"을 스스로 해제해야 하는 딜레마로 이어지기 떄문입니다.
+
+`entlib-native`는 엄격한 보안 인증을 통과해야 하며, 이 프로젝트에서 `unsafe`의 사용은 컴파일러를 대신해 보안과 수학/논리적 무결성을 증명하고 책임진다는 명시적 선언입니다.
+
+Rust의 "Safe"는 컴파일 타임에 소유권(Ownership), 빌림(Borrowing), 수명(Lifetime) 규칙을 강제하는 것이며, '미정의 동작(Undefined Behavior)이 없음'을 의미하지만, 타이밍 공격이나 잔류 메모리 탈취에 대해서는 무방비 상태입니다. 말인 즉슨 메모리 안전성 규칙이 오히려 부채널, DSE 등의 보안 취약점을 야기하므로, 이를 방어하기 위해 의도적으로 하드웨어 종속적인 제어권(unsafe)을 획득해야 한다고 생각됩니다. 이를 암호학적 도메인과 메모리 도메인의 분리라고 정의하겠습니다.
+
+안전한 추상화와 완전한 캡슐화 원칙을 지지합니다. 즉, `unsafe` 코드는 절대 전역적으로 퍼져 있어서는 안 되며, 공개 가능한 API 시그니처 뒤에 완벽하게 캡슐화되어야 합니다. 인-라인 어셈블리나 `volatile` 연산을 수행하는 코드는 최소 단위의 함수로 격리해야 하며, `unsafe` 블록에 진입하기 전, 외부로부터 들여온 데이터에 대한 경계 검사와 `null` 포인터 검사 등을 반드시 Safe 영역에서 완료해야 합니다.
\ No newline at end of file
diff --git a/SECURITY_DISCUSSION_EN.md b/SECURITY_DISCUSSION_EN.md
new file mode 100644
index 0000000..e94fc9b
--- /dev/null
+++ b/SECURITY_DISCUSSION_EN.md
@@ -0,0 +1,31 @@
+# Security Discussion
+
+> [Korean SECURITY DISCUSSION](SECURITY_DISCUSSION.md)
+
+`entlib-native` clearly provides secure operations for data management and constant-time, but before we can call it 'secure', we need to discuss the problem of aggressive optimization in the LLVM compiler.
+
+## The Chronic Problem of the LLVM Compiler
+
+Technically, LLVM, the backend of Rust, performs optimizations with the top priority of improving the average execution speed of the code. In this process, there are continuous reports of cases where constant-time bit operations or mathematical tricks intentionally written by developers to defend against side-channel attacks (Timing Side-Channel Attacks) are arbitrarily converted into conditional branches (e.g., `cmp` followed by `jmp` in assembly) by LLVM's `SimplifyCFG` pass, etc.
+
+Frameworks, libraries, etc. that provide cryptographic functions based on high-security principles like `entlib-native` must implement all core security modules by encapsulating them directly without external dependencies. In an environment that must pass strict verification at the level of FIPS 140-2/3 and CC EAL2 (or EAL3), strict measures must be taken to prevent this.
+
+To get straight to the point, the solution we came up with is to use inline assembly. This is the most reliable way to create a black-box section where the LLVM optimizer can never intervene. In parts where conditional logic is needed, instead of software-based bit operations, we should directly call the constant-time conditional move instructions supported by the hardware. For example, the `x86_64` architecture can use the conditional move (`cmov`) instruction, and the `aarch64` architecture can use the conditional select (`csel`) instruction. This approach bypasses the compiler's instruction selection stage, so it can completely prevent the insertion of branch statements even if the compiler version is updated.
+
+We can discuss compiler barriers and volatile operations and memory barriers further. A compiler barrier (`core::hint::black_box`) instructs the compiler to ignore a specific value during the optimization analysis process. This can prevent the compiler from performing constant folding by predicting the input value or performing Dead Code Elimination (DCE).
+
+However, as the official Rust documentation also states, `black_box` only suppresses optimization and does not absolutely guarantee cryptographic constant-time execution. In other words, while it can prevent the value of a variable from being optimized, it cannot completely control the compiler from compiling the operation that processes that variable into a branch statement. Therefore, it should only be approached as a supplementary measure, not the main defense.
+
+What about volatile operations and memory barriers? According to this approach, `core::ptr::read_volatile` and `write_volatile` can be used to prevent optimization throughout the entire lifecycle of data (from allocation to erasure). When implementing memory erasure that is impossible to forensics, if you use normal memory writes, LLVM will judge it as "a memory write that is not used afterwards" and delete the erasure logic itself (Dead Store Elimination, DSE). Volatile operations prevent the compiler from omitting or reordering memory accesses.
+
+All operations in `entlib-native` clearly support constant-time operations. As a result, it also shows the technical moment of controlling LLVM-side optimization by using limited use of inline assembly and compiler barriers, volatile operations, and memory barriers.
+
+## Regarding Unsafe Operations
+
+However, we can continue the discussion in the context that `entlib-native` basically "uses unsafe operations" to solve the problem of aggressive LLVM optimization. This is because to achieve cryptographic stability, we must escape the control of the compiler, but this leads to the dilemma of having to release Rust's core value of "memory stability" ourselves.
+
+`entlib-native` must pass strict security certification, and the use of `unsafe` in this project is an explicit declaration that it proves and takes responsibility for security and mathematical/logical integrity on behalf of the compiler.
+
+"Safe" in Rust is to enforce the rules of Ownership, Borrowing, and Lifetime at compile time, and it means 'no Undefined Behavior', but it is defenseless against timing attacks or residual memory theft. In other words, the memory safety rules rather cause security vulnerabilities such as side channels and DSE, so it is thought that we must intentionally acquire hardware-dependent control (unsafe) to defend against them. We will define this as the separation of the cryptographic domain and the memory domain.
+
+We support the principles of safe abstraction and complete encapsulation. That is, `unsafe` code should never be spread globally, but should be completely encapsulated behind a publicly available API signature. Code that performs inline assembly or `volatile` operations should be isolated into the smallest unit of function, and before entering an `unsafe` block, boundary checks and `null` pointer checks for data brought in from the outside must be completed in the Safe area.
\ No newline at end of file
diff --git a/SECURITY_EN.md b/SECURITY_EN.md
index 5dd536d..8c027c3 100644
--- a/SECURITY_EN.md
+++ b/SECURITY_EN.md
@@ -22,15 +22,6 @@ If you discover a complex security vulnerability, data residue issue, or memory-
> [!NOTE]
> If you need a PGP key for secure communication, please check the [KEYS](KEYS) file in the repository or request it.
-### Processing Procedure
-
-Reported vulnerabilities are handled through the following procedure:
-
-1. **Receipt Confirmation:** We will send a confirmation email to the reporter within 48 hours.
-2. **Analysis and Verification:** The Quant team will analyze the impact and reproducibility of the vulnerability in detail.
-3. **Patch Development:** If the problem is confirmed, we will develop a hotfix for `entlib-native` or `entanglementlib`.
-4. **Disclosure and Distribution:** After the patch is completed and released, we will disclose the vulnerability information at an appropriate time in consultation with the reporter.
-
## Security Focus Areas
This project places particular importance on security in the following areas:
@@ -51,7 +42,8 @@ The following items are generally excluded from security vulnerability reports,
* **Experimental Features:** Bugs in features explicitly marked as "Experimental".
* **User Environment Issues:** Problems caused by defects in the user's OS or hardware itself.
-You can find more details in the [Contribution Document](CONTRIBUTION_EN.md).
+> [!TIP]
+> You can find more details in the [Contribution Document](CONTRIBUTION_EN.md).
## Acknowledgments
diff --git a/canary/Cargo.toml b/canary/Cargo.toml
new file mode 100644
index 0000000..9a37a7a
--- /dev/null
+++ b/canary/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "canary"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[[bin]]
+name = "entlib-canary"
+path = "src/main.rs"
+
+[dependencies]
+entlib-native-secure-buffer.workspace = true
\ No newline at end of file
diff --git a/canary/README.md b/canary/README.md
new file mode 100644
index 0000000..00fa432
--- /dev/null
+++ b/canary/README.md
@@ -0,0 +1,175 @@
+# SecureBuffer Zeroization Verification
+
+FIPS 140-3 IG 3.1 및 CC FCS_CKM.4에서 요구하는 민감 데이터 소거(Zeroization)를 수학적으로 정형화하고, 해당 속성이 테스트 스위트에 의해 검증됨을 증명함.
+
+---
+
+## 1. 정형 모델
+
+### 1.1 메모리 모델
+
+메모리 영역 $`M`$을 비트의 유한 시퀀스로 정의함.
+
+```math
+M = (b_0, b_1, \ldots, b_{n-1}), \quad b_i \in \{0, 1\}, \quad n = \texttt{capacity} \times 8
+```
+
+$`\texttt{capacity}`$는 `SecureBuffer`가 관리하는 전체 할당 크기(페이지 정렬됨)이며, 사용자 데이터 영역($`\texttt{len}`$)뿐 아니라 패딩 갭($`\texttt{len}..\texttt{capacity}`$)을 포함함.
+
+### 1.2 소거 속성 (Zeroization Property)
+
+**정의 (ZP)**. 소거 함수 $`Z: \{0,1\}^n \to \{0,1\}^n`$은 다음을 만족할 때 **완전**하다고 함:
+
+```math
+\forall\, M \in \{0,1\}^n : Z(M) = 0^n
+```
+
+즉, 임의의 초기 메모리 상태에 대해 소거 후 모든 비트가 0이어야 함.
+
+### 1.3 비트 독립성 (Bit Independence)
+
+소거가 완전함을 증명하려면, 각 비트 위치 $`i`$에 대해 $`1 \to 0`$ 전환이 독립적으로 발생함을 보여야 함.
+
+**보조정리 1 (비트별 소거)**. 비트 위치 $`i`$ ($`0 \le i < n`$)에 대해:
+
+```math
+\forall\, i : \exists\, M \text{ s.t. } M[i] = 1 \;\land\; Z(M)[i] = 0
+```
+
+이는 "모든 비트 위치에서 1이 0으로 전환될 수 있다"는 것을 의미함.
+
+**보조정리 2 (보수 완전성)**. 두 메모리 상태 $`M_a`$, $`M_b`$가 비트 보수(bitwise complement)이면:
+
+```math
+M_a \oplus M_b = 1^n
+```
+
+$`M_a`$와 $`M_b`$ 모두에 대해 $`Z(M_a) = Z(M_b) = 0^n`$이 성립하면, 모든 비트 위치에서 $`1 \to 0`$ 전환이 독립적으로 검증됨. $`M_a[i] = 1`$인 위치와 $`M_b[i] = 1`$인 위치의 합집합이 전체 비트 집합이기 때문임.
+
+```math
+\{i \mid M_a[i] = 1\} \cup \{i \mid M_b[i] = 1\} = \{0, 1, \ldots, n-1\}
+```
+
+---
+
+## 2. 테스트에 의한 증명
+
+### 2.1 JO(Java-Owned) 패턴 검증 (`zeroize_jo.rs`)
+
+| 테스트 | 포이즌 | 비트 패턴 | 검증 대상 |
+|---|---|---|---|
+| `jo_zeroize_0xff_all_bits_set` | $`\texttt{0xFF}`$ | $`11111111_2`$ | 보조정리 1의 충분조건 |
+| `jo_zeroize_0xaa_even_bits` | $`\texttt{0xAA}`$ | $`10101010_2`$ | 짝수 비트 위치 소거 |
+| `jo_zeroize_0x55_odd_bits` | $`\texttt{0x55}`$ | $`01010101_2`$ | 홀수 비트 위치 소거 |
+| `jo_zeroize_complement_pair` | $`\texttt{0xAA} + \texttt{0x55}`$ | 보수 쌍 | 보조정리 2 적용 |
+| `jo_zeroize_sequential_all_byte_values` | $`\texttt{0x00}..\texttt{0xFF}`$ | 전체 바이트 공간 | $`2^8`$가지 바이트 값의 소거 가능성 |
+
+**증명 (비트 독립성)**.
+
+$`\texttt{0xAA} = 10101010_2`$에 대해 $`Z`$ 적용 후 0이면, 비트 위치 $`\{1, 3, 5, 7\}`$ (LSB 기준)에서 $`1 \to 0`$ 전환이 확인됨.
+
+$`\texttt{0x55} = 01010101_2`$에 대해 $`Z`$ 적용 후 0이면, 비트 위치 $`\{0, 2, 4, 6\}`$에서 $`1 \to 0`$ 전환이 확인됨.
+
+```math
+\{1,3,5,7\} \cup \{0,2,4,6\} = \{0,1,2,3,4,5,6,7\}
+```
+
+이므로 바이트 내 모든 비트 위치에서 독립 소거가 검증됨. 이를 $`\texttt{capacity}`$ 전체(4096바이트 = 32768비트)에 대해 반복하므로, 전체 메모리 영역의 비트 독립성이 증명됨.
+
+### 2.2 RO(Rust-Owned) 패턴 검증 (`zeroize_ro.rs`)
+
+RO 패턴에서는 $`\texttt{len} < \texttt{capacity}`$인 패딩 갭이 존재함.
+
+
+
+| 테스트 | 검증 대상 |
+|--------------------------------------------------|-------------------------------------------------------------------------|
+| `ro_full_capacity_zeroed_including_padding_gap` | 전체 $`\texttt{capacity}`$($`\texttt{len}`$ + padding) 소거 |
+| `ro_padding_gap_explicitly_poisoned_then_zeroed` | 패딩 갭에 명시적 포이즌 주입 후 소거 확인 |
+| `ro_complement_patterns` | RO 패턴에서 비트 독립성($`\texttt{0xAA}`$, $`\texttt{0x55}`$, $`\texttt{0xFF}`$) |
+
+**증명 (패딩 갭 소거)**. `SecureBuffer::drop()`은 `Zeroizer::zeroize_raw(ptr, capacity)`를 호출하며, $`\texttt{capacity} \ge \texttt{len}`$이 항상 성립함. 테스트에서 $`\texttt{len}=100`$, $`\texttt{capacity}=4096`$으로 설정하고 전체 4096바이트를 포이즌한 뒤 `Drop` 후 전수 스캔함. 바이트 $`[100, 4095]`$(패딩 갭)이 0임이 확인되면, 패딩 영역의 소거가 보장됨.
+
+### 2.3 다중 페이지 검증 (`zeroize_multi_page.rs`)
+
+| 테스트 | 페이지 수 | 검증 대상 |
+|-------------------------------------------|-------------|--------------|
+| `multi_page_3_pages` | 3 (12288B) | 다중 페이지 소거 |
+| `multi_page_10_pages` | 10 (40960B) | 대용량 소거 |
+| `page_boundary_bytes_explicitly_verified` | 3 | 경계 오프바이원 |
+| `ro_multi_page_padding_gap` | $`\ge 2`$ | RO 다중 페이지 패딩 |
+
+**증명 (페이지 경계 안전성)**. 캐시 라인 플러시 루프가 페이지 경계에서 오프바이원 결함을 가질 수 있음:
+
+```asm
+.loop:
+ clflush [flush_ptr]
+ add flush_ptr, cache_line_size
+ cmp flush_ptr, end_ptr
+ jb .loop
+```
+
+`page_boundary_bytes_explicitly_verified`는 오프셋 $`\{0,\; 4095,\; 4096,\; 4097,\; 8191,\; 8192,\; 8193,\; 12287\}`$을 명시적으로 검사하여, 페이지 경계 전후의 바이트가 누락 없이 소거됨을 확인함.
+
+### 2.4 패닉 경로 검증 (`absolute.rs`)
+
+Rust의 스택 해제(stack unwinding)에서 `Drop` 트레이트가 올바르게 호출되는지 검증함.
+
+| 테스트 | 검증 대상 |
+|----------------------------------|-------------------------|
+| `panic_survival_0xff` | 패닉 후 RAII `Drop`에 의한 소거 |
+| `panic_survival_0xaa` | 패닉 + 짝수 비트 |
+| `panic_survival_0x55` | 패닉 + 홀수 비트 |
+| `panic_survival_complement_pair` | 패닉 경로 비트 독립성 |
+
+**증명 (패닉 안전성)**. `catch_unwind` 내에서 `SecureBuffer` 생성 후 `panic!()` 발생 시, Rust의 unwind 메커니즘이 스택 프레임을 역순으로 정리하며 `SecureBuffer::drop()`을 호출함. 테스트 프로필은 $`\texttt{panic} = \texttt{"unwind"}`$(Cargo 기본값)를 사용하므로 `catch_unwind`가 정상 동작함.
+
+---
+
+## 3. 하드웨어 수준 보장
+
+소프트웨어 테스트로 검증할 수 없는 하드웨어 수준의 보장은 구현 코드의 정적 분석으로 확인함.
+
+### 3.1 컴파일러 DSE(Dead Store Elimination) 방지
+
+| 아키텍처 | 메커니즘 | 근거 |
+|-------------|------------------------------------------|---------------------------|
+| x86_64 | `rep stosb` (인라인 어셈블리) | 컴파일러가 `asm!` 블록을 제거할 수 없음 |
+| AArch64 | `write_volatile` 루프 | volatile 시맨틱이 DSE를 원천 차단함 |
+| 기타 (std) | `explicit_bzero` / `RtlSecureZeroMemory` | OS 커널이 보장하는 소거 API |
+| 기타 (no_std) | `write_volatile` 루프 | volatile 시맨틱 |
+
+모든 경로에서 `compiler_fence(SeqCst)` + `fence(SeqCst)`가 후속하여 컴파일러 및 하드웨어 파이프라인 재배치를 방지함.
+
+### 3.2 캐시 라인 플러시
+
+| 아키텍처 | 명령어 | 보장 |
+|---------|-----------------------|-----------------------------|
+| x86_64 | `clflush` + `mfence` | 캐시에서 메인 메모리로 즉시 기록 후 무효화함 |
+| AArch64 | `dc civac` + `dsb sy` | 데이터 캐시 클린 + 무효화, 전체 시스템 배리어 |
+
+이는 소거된 0이 CPU 캐시에만 머물지 않고 물리 메모리(DRAM)에 반영됨을 보장함.
+
+---
+
+## 4. 커버리지 매트릭스
+
+| 구분 | JO | RO | Panic | Multi-Page |
+|:-----------------|:---:|:--:|:-----:|:----------:|
+| `0xFF` 전체 비트 | O | O | O | O |
+| `0xAA` 짝수 비트 | O | O | O | - |
+| `0x55` 홀수 비트 | O | O | O | - |
+| 보수 쌍 독립성 | O | O | O | - |
+| 숫자 카운터 | O | - | - | - |
+| 패딩 갭 | N/A | O | N/A | O |
+| 전체 `capacity` 스캔 | O | O | O | O |
+
+---
+
+## 5. 실행
+
+```bash
+cargo test -p canary
+```
+
+16개 테스트가 모두 통과하면, 위 정형 모델에서 정의한 소거 속성(ZP)이 소프트웨어 수준에서 검증된 것으로 판단함.
diff --git a/canary/check_zeroize.sh b/canary/check_zeroize.sh
new file mode 100755
index 0000000..44bb143
--- /dev/null
+++ b/canary/check_zeroize.sh
@@ -0,0 +1,56 @@
+#!/bin/zsh
+
+# 설정 변수
+CANARY_PATTERN="ENTLIB_FORENSIC_CANARY_PATTERN__"
+CORE_DIR="/cores"
+TEST_BINARY="../target/debug/entlib-canary"
+
+echo "[*] EntanglementLib EAL2+ Zeroization Verification Test"
+echo "[*] Setting up core dump limits..."
+ulimit -c unlimited
+
+echo "[*] Cleaning up previous core dumps in $CORE_DIR..."
+sudo rm -f $CORE_DIR/core.*
+
+echo "[*] Executing test binary..."
+# 바이너리 실행 (내부에서 process::abort() 호출로 인해 abort 발생)
+$TEST_BINARY &
+PID=$!
+
+# 프로세스 종료 대기 (SIGABRT로 비정상 종료되므로 에러 메시지 억제)
+wait $PID 2>/dev/null
+EXIT_CODE=$?
+
+echo "[*] Process exited with code: $EXIT_CODE (Expected 134 for SIGABRT)"
+
+# 가장 최근에 생성된 코어 덤프 파일 찾기
+CORE_FILE=$(ls -t $CORE_DIR/core.* 2>/dev/null | head -n 1)
+
+if [[ -z "$CORE_FILE" ]]; then
+ echo "[-] FAILED: Core dump was not generated."
+ echo " Please check 'ulimit -c unlimited' and SIP settings."
+ exit 1
+fi
+
+echo "[*] Core dump generated successfully: $CORE_FILE"
+echo "[*] Scanning core dump for forensic canary. This may take a moment depending on dump size..."
+
+# 1. strings 명령어로 바이너리 내의 텍스트만 초고속으로 추출
+# 2. 파이프를 통해 grep -c 로 카나리아 패턴이 포함된 라인 수만 카운트
+MATCH_COUNT=$(strings "$CORE_FILE" | grep -c "$CANARY_PATTERN" | tr -d ' ')
+
+if [[ "$MATCH_COUNT" -gt 0 ]]; then
+ echo "=========================================================="
+ echo "[!] CRITICAL FAILURE: Zeroization Failed!"
+ echo "[!] Forensic Canary found $MATCH_COUNT time(s) in memory."
+ echo "[!] The chokepoint logic or compiler optimization defense has failed."
+ echo "=========================================================="
+ exit 1
+else
+ echo "=========================================================="
+ echo "[+] SUCCESS: Forensic-proof Zeroization Verified!"
+ echo "[+] Canary pattern was NOT found in the core dump."
+ echo "[+] FIPS 140-3 / CC EAL2+ erasure requirements met."
+ echo "=========================================================="
+ exit 0
+fi
\ No newline at end of file
diff --git a/canary/src/main.rs b/canary/src/main.rs
new file mode 100644
index 0000000..31f8913
--- /dev/null
+++ b/canary/src/main.rs
@@ -0,0 +1,88 @@
+use std::alloc::{alloc, dealloc, Layout};
+use std::ptr;
+use entlib_native_secure_buffer::SecureBuffer;
+
+const CANARY: &[u8; 32] = b"ENTLIB_FORENSIC_CANARY_PATTERN__";
+
+// fn test_secure_buffer_zeroization_and_abort() { // 이거 개무거움
+// // 스코프 블록을 생성하여 명시적인 Drop(소거) 유도
+// {
+// // 1. Rust-Owned (RO) 패턴으로 안전한 버퍼 할당
+// // 내부적으로 SecureMemoryBlock::allocate_locked를 호출하며 페이지 정렬됨
+// let mut buffer = SecureBuffer::new_owned(32).expect("Failed to allocate SecureBuffer");
+// let slice = buffer.as_mut_slice();
+//
+// unsafe {
+// // 2. 카나리아 데이터를 메모리에 주입
+// ptr::copy_nonoverlapping(CANARY.as_ptr(), slice.as_mut_ptr(), 32);
+//
+// // 컴파일러 최적화 방지 및 메모리 쓰기 강제화
+// let _ = ptr::read_volatile(slice.as_ptr());
+// }
+// } // 스코프 종료
+//
+// // 3. 소거 직후 OS에 시그널을 보내 강제 크래시 및 코어 덤프 생성 유도
+// process::abort();
+// }
+
+fn test_zeroization_in_memory_forensic() {
+ let page_size = 4096; // macOS 기본 페이지 크기
+ // 1. 페이지 크기에 맞게 정렬된 메모리를 수동으로 할당 (Java FFM API 오프힙 모사)
+ let layout = Layout::from_size_align(page_size, page_size).unwrap();
+
+ unsafe {
+ let raw_ptr = alloc(layout);
+ assert!(!raw_ptr.is_null(), "Failed to allocate memory");
+
+ // 2. 카나리아 데이터 주입
+ ptr::copy_nonoverlapping(CANARY.as_ptr(), raw_ptr, CANARY.len());
+
+ // 3. 단일 병목점 스코 프생성
+ {
+ // JO 패턴으로 메모리 래핑
+ let buffer = SecureBuffer::from_raw_parts(raw_ptr, page_size)
+ .expect("Failed to create SecureBuffer");
+
+ // 컴파일러 최적화 방지용 volatile read
+ let _ = ptr::read_volatile(buffer.as_slice().as_ptr());
+
+ }
+
+ // 4. 인메모리 포렌식 전수 스캔 (코어 덤프 대체ㅇ)
+ // SecureBuffer는 파괴되었지만, raw_ptr은 아직 OS에 반환되지 않았으므로 안전하게 읽을 수 있음
+ let dumped_slice = core::slice::from_raw_parts(raw_ptr, page_size);
+
+ let mut unclean_bytes = 0usize;
+ let mut first_unclean_index = None;
+
+ for (i, &byte) in dumped_slice.iter().enumerate() {
+ if byte != 0x00 {
+ unclean_bytes += 1;
+ if first_unclean_index.is_none() {
+ first_unclean_index = Some(i);
+ }
+ }
+ }
+
+ dealloc(raw_ptr, layout);
+
+ // 5. 검증 단언 (Assert)
+ if unclean_bytes > 0 {
+ let idx = first_unclean_index.unwrap();
+ panic!(
+ "CRITICAL FAILURE: Zeroization NOT 100% complete!\n\
+ Found {}/{} uncleared bytes. First at offset [{}].",
+ unclean_bytes, page_size, idx
+ );
+ } else {
+ println!("==========================================================");
+ println!("[+] SUCCESS: Full-capacity ({} bytes) Forensic Zeroization Verified!", page_size);
+ println!("[+] The compiler did NOT optimize away the zeroization logic.");
+ println!("==========================================================");
+ }
+ }
+}
+
+fn main() {
+ test_zeroization_in_memory_forensic();
+}
\ No newline at end of file
diff --git a/canary/tests/absolute.rs b/canary/tests/absolute.rs
new file mode 100644
index 0000000..35da441
--- /dev/null
+++ b/canary/tests/absolute.rs
@@ -0,0 +1,66 @@
+use std::alloc::{Layout, alloc, dealloc};
+use std::panic;
+use std::ptr;
+
+use entlib_native_secure_buffer::SecureBuffer;
+
+const PAGE_SIZE: usize = 4096;
+
+fn verify_panic_survival(poison: u8) {
+ let layout = Layout::from_size_align(PAGE_SIZE, PAGE_SIZE).unwrap();
+ let raw_ptr = unsafe { alloc(layout) };
+ assert!(!raw_ptr.is_null());
+
+ unsafe { ptr::write_bytes(raw_ptr, poison, PAGE_SIZE) };
+
+ let result = panic::catch_unwind(|| {
+ unsafe {
+ let _buffer = SecureBuffer::from_raw_parts(raw_ptr, PAGE_SIZE).unwrap();
+ panic!("Simulated cryptographic operation failure");
+ }
+ });
+
+ assert!(result.is_err(), "Panic must occur for Drop to be invoked via stack unwinding");
+
+ let slice = unsafe { core::slice::from_raw_parts(raw_ptr, PAGE_SIZE) };
+ let mut unclean = 0usize;
+ let mut first_idx = None;
+ for (i, &byte) in slice.iter().enumerate() {
+ if byte != 0x00 {
+ unclean += 1;
+ if first_idx.is_none() {
+ first_idx = Some(i);
+ }
+ }
+ }
+
+ unsafe { dealloc(raw_ptr, layout) };
+
+ if unclean > 0 {
+ panic!(
+ "Panic-path zeroization failed: poison=0x{:02X}, {}/{} bytes unclean, first at offset {}",
+ poison, unclean, PAGE_SIZE, first_idx.unwrap()
+ );
+ }
+}
+
+#[test]
+fn panic_survival_0xff() {
+ verify_panic_survival(0xFF);
+}
+
+#[test]
+fn panic_survival_0xaa() {
+ verify_panic_survival(0xAA);
+}
+
+#[test]
+fn panic_survival_0x55() {
+ verify_panic_survival(0x55);
+}
+
+#[test]
+fn panic_survival_complement_pair() {
+ verify_panic_survival(0xAA);
+ verify_panic_survival(0x55);
+}
diff --git a/canary/tests/zeroize_jo.rs b/canary/tests/zeroize_jo.rs
new file mode 100644
index 0000000..8d18452
--- /dev/null
+++ b/canary/tests/zeroize_jo.rs
@@ -0,0 +1,83 @@
+use std::alloc::{Layout, alloc, dealloc};
+use std::ptr;
+
+use entlib_native_secure_buffer::SecureBuffer;
+
+const PAGE_SIZE: usize = 4096;
+
+fn verify_jo_zeroization(poison: u8) {
+ let layout = Layout::from_size_align(PAGE_SIZE, PAGE_SIZE).unwrap();
+ let raw_ptr = unsafe { alloc(layout) };
+ assert!(!raw_ptr.is_null());
+
+ unsafe { ptr::write_bytes(raw_ptr, poison, PAGE_SIZE) };
+
+ {
+ let _buffer = unsafe { SecureBuffer::from_raw_parts(raw_ptr, PAGE_SIZE).unwrap() };
+ }
+
+ let slice = unsafe { core::slice::from_raw_parts(raw_ptr, PAGE_SIZE) };
+ let mut unclean = 0usize;
+ let mut first_idx = None;
+ for (i, &byte) in slice.iter().enumerate() {
+ if byte != 0x00 {
+ unclean += 1;
+ if first_idx.is_none() {
+ first_idx = Some(i);
+ }
+ }
+ }
+
+ unsafe { dealloc(raw_ptr, layout) };
+
+ if unclean > 0 {
+ panic!(
+ "JO zeroization failed: poison=0x{:02X}, {}/{} bytes unclean, first at offset {}",
+ poison, unclean, PAGE_SIZE, first_idx.unwrap()
+ );
+ }
+}
+
+#[test]
+fn jo_zeroize_0xff_all_bits_set() {
+ verify_jo_zeroization(0xFF);
+}
+
+#[test]
+fn jo_zeroize_0xaa_even_bits() {
+ verify_jo_zeroization(0xAA);
+}
+
+#[test]
+fn jo_zeroize_0x55_odd_bits() {
+ verify_jo_zeroization(0x55);
+}
+
+#[test]
+fn jo_zeroize_complement_pair_proves_bitwise_independence() {
+ verify_jo_zeroization(0xAA);
+ verify_jo_zeroization(0x55);
+}
+
+#[test]
+fn jo_zeroize_sequential_all_byte_values() {
+ let layout = Layout::from_size_align(PAGE_SIZE, PAGE_SIZE).unwrap();
+ let raw_ptr = unsafe { alloc(layout) };
+ assert!(!raw_ptr.is_null());
+
+ let slice = unsafe { core::slice::from_raw_parts_mut(raw_ptr, PAGE_SIZE) };
+ for (i, byte) in slice.iter_mut().enumerate() {
+ *byte = (i % 256) as u8;
+ }
+
+ {
+ let _buffer = unsafe { SecureBuffer::from_raw_parts(raw_ptr, PAGE_SIZE).unwrap() };
+ }
+
+ let slice = unsafe { core::slice::from_raw_parts(raw_ptr, PAGE_SIZE) };
+ for (i, &byte) in slice.iter().enumerate() {
+ assert_eq!(byte, 0x00, "Sequential pattern: byte at offset {} not zeroed (was 0x{:02X})", i, byte);
+ }
+
+ unsafe { dealloc(raw_ptr, layout) };
+}
diff --git a/canary/tests/zeroize_multi_page.rs b/canary/tests/zeroize_multi_page.rs
new file mode 100644
index 0000000..182871b
--- /dev/null
+++ b/canary/tests/zeroize_multi_page.rs
@@ -0,0 +1,110 @@
+use std::alloc::{Layout, alloc, dealloc};
+use std::ptr;
+
+use entlib_native_secure_buffer::SecureBuffer;
+
+const PAGE_SIZE: usize = 4096;
+
+fn verify_jo_multi_page(num_pages: usize) {
+ let total = PAGE_SIZE * num_pages;
+ let layout = Layout::from_size_align(total, PAGE_SIZE).unwrap();
+ let raw_ptr = unsafe { alloc(layout) };
+ assert!(!raw_ptr.is_null());
+
+ unsafe { ptr::write_bytes(raw_ptr, 0xFF, total) };
+
+ {
+ let _buffer = unsafe { SecureBuffer::from_raw_parts(raw_ptr, total).unwrap() };
+ }
+
+ let slice = unsafe { core::slice::from_raw_parts(raw_ptr, total) };
+ let mut unclean = 0usize;
+ let mut first_idx = None;
+ for (i, &byte) in slice.iter().enumerate() {
+ if byte != 0x00 {
+ unclean += 1;
+ if first_idx.is_none() {
+ first_idx = Some(i);
+ }
+ }
+ }
+
+ unsafe { dealloc(raw_ptr, layout) };
+
+ if unclean > 0 {
+ panic!(
+ "Multi-page zeroization failed: pages={}, {}/{} bytes unclean, first at offset {}",
+ num_pages, unclean, total, first_idx.unwrap()
+ );
+ }
+}
+
+#[test]
+fn multi_page_3_pages() {
+ verify_jo_multi_page(3);
+}
+
+#[test]
+fn multi_page_10_pages() {
+ verify_jo_multi_page(10);
+}
+
+#[test]
+fn page_boundary_bytes_explicitly_verified() {
+ let num_pages = 3;
+ let total = PAGE_SIZE * num_pages;
+ let layout = Layout::from_size_align(total, PAGE_SIZE).unwrap();
+ let raw_ptr = unsafe { alloc(layout) };
+ assert!(!raw_ptr.is_null());
+
+ unsafe { ptr::write_bytes(raw_ptr, 0xCC, total) };
+
+ {
+ let _buffer = unsafe { SecureBuffer::from_raw_parts(raw_ptr, total).unwrap() };
+ }
+
+ let slice = unsafe { core::slice::from_raw_parts(raw_ptr, total) };
+
+ let boundary_offsets = [
+ 0,
+ PAGE_SIZE - 1,
+ PAGE_SIZE,
+ PAGE_SIZE + 1,
+ 2 * PAGE_SIZE - 1,
+ 2 * PAGE_SIZE,
+ 2 * PAGE_SIZE + 1,
+ total - 1,
+ ];
+
+ for &offset in &boundary_offsets {
+ assert_eq!(
+ slice[offset], 0x00,
+ "Page boundary byte at offset {} not zeroed (was 0x{:02X})",
+ offset, slice[offset]
+ );
+ }
+
+ unsafe { dealloc(raw_ptr, layout) };
+}
+
+#[test]
+fn ro_multi_page_padding_gap() {
+ let size = 5000;
+ let mut buffer = SecureBuffer::new_owned(size).unwrap();
+ let ptr = buffer.as_mut_slice().as_mut_ptr();
+ let capacity = buffer.capacity();
+ assert!(capacity >= 2 * PAGE_SIZE, "5000 bytes should span 2 pages");
+
+ unsafe { ptr::write_bytes(ptr, 0xBB, capacity) };
+
+ drop(buffer);
+
+ let slice = unsafe { core::slice::from_raw_parts(ptr, capacity) };
+ for (i, &byte) in slice.iter().enumerate() {
+ assert_eq!(
+ byte, 0x00,
+ "RO multi-page: byte at offset {} not zeroed (capacity={})",
+ i, capacity
+ );
+ }
+}
diff --git a/canary/tests/zeroize_ro.rs b/canary/tests/zeroize_ro.rs
new file mode 100644
index 0000000..87ade6e
--- /dev/null
+++ b/canary/tests/zeroize_ro.rs
@@ -0,0 +1,83 @@
+use std::ptr;
+
+use entlib_native_secure_buffer::SecureBuffer;
+
+#[test]
+fn ro_full_capacity_zeroed_including_padding_gap() {
+ let size = 100;
+ let mut buffer = SecureBuffer::new_owned(size).unwrap();
+ let ptr = buffer.as_mut_slice().as_mut_ptr();
+ let capacity = buffer.capacity();
+ assert!(capacity > size, "capacity({}) must exceed len({})", capacity, size);
+
+ unsafe { ptr::write_bytes(ptr, 0xAA, capacity) };
+
+ drop(buffer);
+
+ let slice = unsafe { core::slice::from_raw_parts(ptr, capacity) };
+ let mut unclean = 0usize;
+ let mut first_idx = None;
+ for (i, &byte) in slice.iter().enumerate() {
+ if byte != 0x00 {
+ unclean += 1;
+ if first_idx.is_none() {
+ first_idx = Some(i);
+ }
+ }
+ }
+
+ if unclean > 0 {
+ panic!(
+ "RO zeroization failed: {}/{} bytes unclean (len={}, capacity={}), first at offset {}",
+ unclean, capacity, size, capacity, first_idx.unwrap()
+ );
+ }
+}
+
+#[test]
+fn ro_padding_gap_explicitly_poisoned_then_zeroed() {
+ let size = 64;
+ let mut buffer = SecureBuffer::new_owned(size).unwrap();
+ let ptr = buffer.as_mut_slice().as_mut_ptr();
+ let capacity = buffer.capacity();
+
+ unsafe {
+ ptr::write_bytes(ptr, 0x42, size);
+ ptr::write_bytes(ptr.add(size), 0xFF, capacity - size);
+ };
+
+ drop(buffer);
+
+ let slice = unsafe { core::slice::from_raw_parts(ptr, capacity) };
+
+ for (i, &byte) in slice[..size].iter().enumerate() {
+ assert_eq!(byte, 0x00, "RO data region: byte at offset {} not zeroed", i);
+ }
+
+ for (i, &byte) in slice[size..].iter().enumerate() {
+ assert_eq!(byte, 0x00, "RO padding gap: byte at offset {} not zeroed", size + i);
+ }
+}
+
+#[test]
+fn ro_complement_patterns() {
+ for poison in [0xAA_u8, 0x55, 0xFF] {
+ let size = 200;
+ let mut buffer = SecureBuffer::new_owned(size).unwrap();
+ let ptr = buffer.as_mut_slice().as_mut_ptr();
+ let capacity = buffer.capacity();
+
+ unsafe { ptr::write_bytes(ptr, poison, capacity) };
+
+ drop(buffer);
+
+ let slice = unsafe { core::slice::from_raw_parts(ptr, capacity) };
+ for (i, &byte) in slice.iter().enumerate() {
+ assert_eq!(
+ byte, 0x00,
+ "RO poison=0x{:02X}: byte at offset {} not zeroed",
+ poison, i
+ );
+ }
+ }
+}
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
new file mode 100644
index 0000000..4c6f290
--- /dev/null
+++ b/cli/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "entlib-native-cli"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[[bin]]
+name = "entlib-cli"
+path = "src/main.rs"
+
+[dependencies]
+clap = { version = "4.5.51", features = ["derive"] }
+entlib-native-argon2id.workspace = true
+entlib-native-base64.workspace = true
+entlib-native-blake.workspace = true
+entlib-native-hex.workspace = true
+entlib-native-sha2.workspace = true
+entlib-native-sha3.workspace = true
+entlib-native-mldsa.workspace = true
+entlib-native-mlkem.workspace = true
+entlib-native-pkcs8.workspace = true
+entlib-native-rng.workspace = true
+entlib-native-secure-buffer.workspace = true
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
\ No newline at end of file
diff --git a/cli/src/cmd/argon2id.rs b/cli/src/cmd/argon2id.rs
new file mode 100644
index 0000000..a3b4a66
--- /dev/null
+++ b/cli/src/cmd/argon2id.rs
@@ -0,0 +1,145 @@
+use super::hex_encode;
+use crate::input;
+use clap::Subcommand;
+use entlib_native_argon2id::Argon2id;
+use entlib_native_secure_buffer::SecureBuffer;
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// Argon2id 해시 생성
+ Hash {
+ /// 비밀번호 파일 (생략 시 stdin)
+ #[arg(long)]
+ in_file: Option,
+ /// 솔트 (hex 문자열)
+ #[arg(long, group = "salt_src")]
+ salt: Option,
+ /// 솔트 파일 (raw 바이너리)
+ #[arg(long, group = "salt_src")]
+ salt_file: Option,
+ /// 시간 비용 (기본: 2)
+ #[arg(long, default_value_t = 2)]
+ time_cost: u32,
+ /// 메모리 비용 KiB (기본: 19456)
+ #[arg(long, default_value_t = 19456)]
+ memory_cost: u32,
+ /// 병렬성 (기본: 1)
+ #[arg(long, default_value_t = 1)]
+ parallelism: u32,
+ /// 출력 태그 길이 바이트 (기본: 32)
+ #[arg(long, default_value_t = 32)]
+ tag_length: u32,
+ /// 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ out_file: Option,
+ /// raw 바이너리 출력 (기본: hex)
+ #[arg(long)]
+ raw: bool,
+ },
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Hash {
+ in_file,
+ salt,
+ salt_file,
+ time_cost,
+ memory_cost,
+ parallelism,
+ tag_length,
+ out_file,
+ raw,
+ } => run_hash(
+ in_file,
+ salt,
+ salt_file,
+ time_cost,
+ memory_cost,
+ parallelism,
+ tag_length,
+ out_file,
+ raw,
+ ),
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+fn run_hash(
+ in_file: Option,
+ salt_hex: Option,
+ salt_file: Option,
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+ tag_length: u32,
+ out_file: Option,
+ raw: bool,
+) {
+ let interactive = in_file.is_none();
+
+ let password = match in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("비밀번호 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let salt_bytes = load_salt(salt_hex, salt_file);
+
+ let argon = match Argon2id::new(time_cost, memory_cost, parallelism, tag_length) {
+ Ok(a) => a,
+ Err(e) => {
+ eprintln!("Argon2id 파라미터 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let tag = match argon.hash(password.as_slice(), &salt_bytes, &[], &[]) {
+ Ok(t) => t,
+ Err(e) => {
+ eprintln!("Argon2id 해시 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let result = if raw { tag } else { hex_encode(tag) };
+ input::write_output(result, out_file.as_deref(), interactive);
+}
+
+fn load_salt(salt_hex: Option, salt_file: Option) -> Vec {
+ if let Some(hex) = salt_hex {
+ let hex_bytes = hex.as_bytes();
+ let mut hex_buf = match SecureBuffer::new_owned(hex_bytes.len()) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("메모리 할당 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ hex_buf.as_mut_slice().copy_from_slice(hex_bytes);
+ match entlib_native_hex::decode(&hex_buf) {
+ Ok(b) => return b.as_slice().to_vec(),
+ Err(e) => {
+ eprintln!("솔트 hex 디코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ if let Some(path) = salt_file {
+ match input::read_file(&path) {
+ Ok(b) => return b.as_slice().to_vec(),
+ Err(e) => {
+ eprintln!("솔트 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ eprintln!("오류: --salt 또는 --salt-file 중 하나를 지정해야 합니다");
+ std::process::exit(1);
+}
diff --git a/cli/src/cmd/base64.rs b/cli/src/cmd/base64.rs
new file mode 100644
index 0000000..70f62ae
--- /dev/null
+++ b/cli/src/cmd/base64.rs
@@ -0,0 +1,68 @@
+use crate::input;
+use clap::Subcommand;
+use entlib_native_base64::{decode, encode};
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// Base64 인코딩
+ Encode {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ },
+ /// Base64 디코딩
+ Decode {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ },
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Encode { in_file, out_file } => {
+ let interactive = in_file.is_none();
+ let buf = match in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ match encode(&buf) {
+ Ok(result) => input::write_output(result, out_file.as_deref(), interactive),
+ Err(e) => {
+ eprintln!("인코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ Ops::Decode { in_file, out_file } => {
+ let interactive = in_file.is_none();
+ let buf = match in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ match decode(&buf) {
+ Ok(result) => input::write_output(result, out_file.as_deref(), interactive),
+ Err(e) => {
+ eprintln!("디코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ }
+}
diff --git a/cli/src/cmd/blake.rs b/cli/src/cmd/blake.rs
new file mode 100644
index 0000000..a9f504c
--- /dev/null
+++ b/cli/src/cmd/blake.rs
@@ -0,0 +1,178 @@
+use super::hex_encode;
+use crate::input;
+use clap::Subcommand;
+use entlib_native_blake::file::{blake2b as blake2b_file, blake3 as blake3_file};
+use entlib_native_blake::{Blake2b, Blake3};
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// BLAKE2b (RFC 7693, 최대 512-bit 다이제스트)
+ #[command(name = "2b")]
+ Blake2b {
+ /// 출력 바이트 수 (1–64, 기본: 32)
+ #[arg(long, default_value_t = 32)]
+ output_len: usize,
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ /// raw 바이너리 출력 (기본: hex)
+ #[arg(long)]
+ raw: bool,
+ },
+ /// BLAKE2b 파일 스트리밍 해시
+ #[command(name = "2b-file")]
+ Blake2bFile {
+ /// 출력 바이트 수 (1-64, 기본: 32)
+ #[arg(long, default_value_t = 32)]
+ output_len: usize,
+ /// 해시할 파일 경로
+ file: String,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// BLAKE3 파일 스트리밍 해시
+ #[command(name = "3-file")]
+ Blake3File {
+ /// 출력 바이트 수 (기본: 32)
+ #[arg(long, default_value_t = 32)]
+ output_len: usize,
+ /// 해시할 파일 경로
+ file: String,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// BLAKE3 (32-byte 기본 출력, XOF 지원)
+ #[command(name = "3")]
+ Blake3 {
+ /// 출력 바이트 수 (기본: 32)
+ #[arg(long, default_value_t = 32)]
+ output_len: usize,
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ /// raw 바이너리 출력 (기본: hex)
+ #[arg(long)]
+ raw: bool,
+ },
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Blake2bFile {
+ output_len,
+ file,
+ out_file,
+ raw,
+ } => {
+ if !(1..=64).contains(&output_len) {
+ eprintln!("output_len은 1-64 범위여야 합니다");
+ std::process::exit(1);
+ }
+ match blake2b_file::hash_file(&file, output_len) {
+ Ok(d) => {
+ let result = if raw { d } else { hex_encode(d) };
+ input::write_output(result, out_file.as_deref(), false);
+ }
+ Err(e) => {
+ eprintln!("파일 해시 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ Ops::Blake3File {
+ output_len,
+ file,
+ out_file,
+ raw,
+ } => {
+ if output_len == 0 {
+ eprintln!("output_len은 1 이상이어야 합니다");
+ std::process::exit(1);
+ }
+ match blake3_file::hash_file(&file, output_len) {
+ Ok(d) => {
+ let result = if raw { d } else { hex_encode(d) };
+ input::write_output(result, out_file.as_deref(), false);
+ }
+ Err(e) => {
+ eprintln!("파일 해시 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ Ops::Blake2b {
+ output_len,
+ in_file,
+ out_file,
+ raw,
+ } => {
+ if !(1..=64).contains(&output_len) {
+ eprintln!("오류: output_len은 1–64 범위여야 합니다");
+ std::process::exit(1);
+ }
+ let interactive = in_file.is_none();
+ let buf = match in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let mut hasher = Blake2b::new(output_len);
+ hasher.update(buf.as_slice());
+ let digest = match hasher.finalize() {
+ Ok(d) => d,
+ Err(e) => {
+ eprintln!("해시 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let result = if raw { digest } else { hex_encode(digest) };
+ input::write_output(result, out_file.as_deref(), interactive);
+ }
+ Ops::Blake3 {
+ output_len,
+ in_file,
+ out_file,
+ raw,
+ } => {
+ if output_len == 0 {
+ eprintln!("오류: output_len은 1 이상이어야 합니다");
+ std::process::exit(1);
+ }
+ let interactive = in_file.is_none();
+ let buf = match in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let mut hasher = Blake3::new();
+ hasher.update(buf.as_slice());
+ let digest = match hasher.finalize_xof(output_len) {
+ Ok(d) => d,
+ Err(e) => {
+ eprintln!("해시 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let result = if raw { digest } else { hex_encode(digest) };
+ input::write_output(result, out_file.as_deref(), interactive);
+ }
+ }
+}
diff --git a/cli/src/cmd/hex.rs b/cli/src/cmd/hex.rs
new file mode 100644
index 0000000..f1264ea
--- /dev/null
+++ b/cli/src/cmd/hex.rs
@@ -0,0 +1,68 @@
+use crate::input;
+use clap::Subcommand;
+use entlib_native_hex::{decode, encode};
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// Hex 인코딩
+ Encode {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ },
+ /// Hex 디코딩
+ Decode {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ },
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Encode { in_file, out_file } => {
+ let interactive = in_file.is_none();
+ let buf = match in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ match encode(&buf) {
+ Ok(result) => input::write_output(result, out_file.as_deref(), interactive),
+ Err(e) => {
+ eprintln!("인코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ Ops::Decode { in_file, out_file } => {
+ let interactive = in_file.is_none();
+ let buf = match in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ match decode(&buf) {
+ Ok(result) => input::write_output(result, out_file.as_deref(), interactive),
+ Err(e) => {
+ eprintln!("디코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ }
+}
diff --git a/cli/src/cmd/mldsa.rs b/cli/src/cmd/mldsa.rs
new file mode 100644
index 0000000..9112263
--- /dev/null
+++ b/cli/src/cmd/mldsa.rs
@@ -0,0 +1,378 @@
+use crate::input;
+use clap::Subcommand;
+use entlib_native_mldsa::{HashDRBGRng, MLDSA, MLDSAParameter, MLDSAPrivateKey, MLDSAPublicKey};
+use entlib_native_pkcs8::{
+ Algorithm, DEFAULT_MEMORY_COST, DEFAULT_PARALLELISM, DEFAULT_TIME_COST, Pkcs8Params,
+};
+use entlib_native_rng::{DrbgError, HashDRBGSHA256};
+
+fn parse_param(s: &str) -> Result {
+ match s {
+ "ml-dsa-44" | "mldsa44" | "ML-DSA-44" => Ok(MLDSAParameter::MLDSA44),
+ "ml-dsa-65" | "mldsa65" | "ML-DSA-65" => Ok(MLDSAParameter::MLDSA65),
+ "ml-dsa-87" | "mldsa87" | "ML-DSA-87" => Ok(MLDSAParameter::MLDSA87),
+ _ => Err(format!("알 수 없는 알고리즘: {s}")),
+ }
+}
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// ML-DSA 키 쌍 생성 및 추출
+ Keygen {
+ /// 파라미터 셋 (ml-dsa-44 | ml-dsa-65 | ml-dsa-87)
+ #[arg(long, short = 'a')]
+ algorithm: String,
+ /// 공개 키 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ pk_out: Option,
+ /// 비밀 키 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ sk_out: Option,
+ /// PKCS#8 PEM 형식으로 출력 (비밀 키: EncryptedPrivateKeyInfo, 공개 키: SubjectPublicKeyInfo)
+ #[arg(long)]
+ pkcs8: bool,
+ /// Argon2id 시간 비용 (기본: 2, --pkcs8 활성화 시에만 적용)
+ #[arg(long, default_value_t = DEFAULT_TIME_COST)]
+ time_cost: u32,
+ /// Argon2id 메모리 비용 KiB (기본: 19456, --pkcs8 활성화 시에만 적용)
+ #[arg(long, default_value_t = DEFAULT_MEMORY_COST)]
+ memory_cost: u32,
+ /// Argon2id 병렬성 (기본: 1, --pkcs8 활성화 시에만 적용)
+ #[arg(long, default_value_t = DEFAULT_PARALLELISM)]
+ parallelism: u32,
+ },
+ /// ML-DSA 서명 생성
+ Sign {
+ /// 파라미터 셋 (ml-dsa-44 | ml-dsa-65 | ml-dsa-87)
+ #[arg(long, short = 'a')]
+ algorithm: String,
+ /// 비밀 키 파일
+ #[arg(long)]
+ sk_file: String,
+ /// 서명할 메시지 파일 (생략 시 stdin)
+ #[arg(long)]
+ msg_file: Option,
+ /// 컨텍스트 문자열 (기본: 빈 문자열)
+ #[arg(long, default_value = "")]
+ ctx: String,
+ /// 서명 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ out_file: Option,
+ },
+ /// ML-DSA 서명 검증
+ Verify {
+ /// 파라미터 셋 (ml-dsa-44 | ml-dsa-65 | ml-dsa-87)
+ #[arg(long, short = 'a')]
+ algorithm: String,
+ /// 공개 키 파일
+ #[arg(long)]
+ pk_file: String,
+ /// 서명 파일
+ #[arg(long)]
+ sig_file: String,
+ /// 검증할 메시지 파일 (생략 시 stdin)
+ #[arg(long)]
+ msg_file: Option,
+ /// 서명 시 사용한 컨텍스트 문자열 (기본: 빈 문자열)
+ #[arg(long, default_value = "")]
+ ctx: String,
+ },
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Keygen {
+ algorithm,
+ pk_out,
+ sk_out,
+ pkcs8,
+ time_cost,
+ memory_cost,
+ parallelism,
+ } => run_keygen(
+ algorithm,
+ pk_out,
+ sk_out,
+ pkcs8,
+ time_cost,
+ memory_cost,
+ parallelism,
+ ),
+ Ops::Sign {
+ algorithm,
+ sk_file,
+ msg_file,
+ ctx,
+ out_file,
+ } => run_sign(algorithm, sk_file, msg_file, ctx, out_file),
+ Ops::Verify {
+ algorithm,
+ pk_file,
+ sig_file,
+ msg_file,
+ ctx,
+ } => run_verify(algorithm, pk_file, sig_file, msg_file, ctx),
+ }
+}
+
+fn run_keygen(
+ algorithm: String,
+ pk_out: Option,
+ sk_out: Option,
+ pkcs8: bool,
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+) {
+ let param = match parse_param(&algorithm) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("오류: {e} — 지원 알고리즘: ml-dsa-44, ml-dsa-65, ml-dsa-87");
+ std::process::exit(1);
+ }
+ };
+
+ let mut rng = match HashDRBGRng::new_from_os(Some(b"entlib-mldsa-keygen")) {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("RNG 초기화 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let (pk, sk) = match MLDSA::key_gen(param, &mut rng) {
+ Ok(kp) => kp,
+ Err(e) => {
+ eprintln!("키 생성 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ if pkcs8 {
+ run_keygen_pkcs8(
+ param,
+ pk.as_bytes(),
+ sk.as_bytes(),
+ pk_out,
+ sk_out,
+ time_cost,
+ memory_cost,
+ parallelism,
+ );
+ return;
+ }
+
+ // 공개 키 출력
+ write_bytes(pk.as_bytes(), pk_out.as_deref(), "공개 키");
+
+ // 비밀 키 출력
+ write_bytes(sk.as_bytes(), sk_out.as_deref(), "비밀 키");
+}
+
+fn param_to_algorithm(param: MLDSAParameter) -> Algorithm {
+ match param {
+ MLDSAParameter::MLDSA44 => Algorithm::MLDSA44,
+ MLDSAParameter::MLDSA65 => Algorithm::MLDSA65,
+ MLDSAParameter::MLDSA87 => Algorithm::MLDSA87,
+ }
+}
+
+fn generate_salt_nonce() -> Result<([u8; 16], [u8; 12]), DrbgError> {
+ let mut drbg = HashDRBGSHA256::new_from_os(Some(b"entlib-mldsa-pkcs8"))?;
+ let mut salt = [0u8; 16];
+ let mut nonce = [0u8; 12];
+ drbg.generate(&mut salt, None)?;
+ drbg.generate(&mut nonce, None)?;
+ Ok((salt, nonce))
+}
+
+#[allow(clippy::too_many_arguments)]
+fn run_keygen_pkcs8(
+ param: MLDSAParameter,
+ pk_bytes: &[u8],
+ sk_bytes: &[u8],
+ pk_out: Option,
+ sk_out: Option,
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+) {
+ let algo = param_to_algorithm(param);
+
+ let passphrase = match input::read_passphrase("패스프레이즈: ") {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("패스프레이즈 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let (salt, nonce) = match generate_salt_nonce() {
+ Ok(v) => v,
+ Err(e) => {
+ eprintln!("난수 생성 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let params = Pkcs8Params::new(time_cost, memory_cost, parallelism, salt, nonce);
+
+ let sk_pem =
+ match entlib_native_pkcs8::encrypt_pem(algo, sk_bytes, passphrase.as_slice(), ¶ms) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("비밀 키 암호화 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let pk_pem = match entlib_native_pkcs8::encode_spki_pem(algo, pk_bytes) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("공개 키 인코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ write_bytes(pk_pem.as_slice(), pk_out.as_deref(), "공개 키 PEM");
+ write_bytes(sk_pem.as_slice(), sk_out.as_deref(), "비밀 키 PEM");
+}
+
+fn run_sign(
+ algorithm: String,
+ sk_file: String,
+ msg_file: Option,
+ ctx: String,
+ out_file: Option,
+) {
+ let param = match parse_param(&algorithm) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let sk_bytes = match input::read_file(&sk_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("비밀 키 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let sk = match MLDSAPrivateKey::from_bytes(param, sk_bytes.as_slice()) {
+ Ok(k) => k,
+ Err(e) => {
+ eprintln!("비밀 키 파싱 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let msg = match msg_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("메시지 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let mut rng = match HashDRBGRng::new_from_os(Some(b"entlib-mldsa-sign")) {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("RNG 초기화 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let sig = match MLDSA::sign(&sk, msg.as_slice(), ctx.as_bytes(), &mut rng) {
+ Ok(s) => s,
+ Err(e) => {
+ eprintln!("서명 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ write_bytes(sig.as_slice(), out_file.as_deref(), "서명");
+}
+
+fn run_verify(
+ algorithm: String,
+ pk_file: String,
+ sig_file: String,
+ msg_file: Option,
+ ctx: String,
+) {
+ let param = match parse_param(&algorithm) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let pk_bytes = match input::read_file(&pk_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("공개 키 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let pk = match MLDSAPublicKey::from_bytes(param, pk_bytes.as_slice().to_vec()) {
+ Ok(k) => k,
+ Err(e) => {
+ eprintln!("공개 키 파싱 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let sig = match input::read_file(&sig_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("서명 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let msg = match msg_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("메시지 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ match MLDSA::verify(&pk, msg.as_slice(), sig.as_slice(), ctx.as_bytes()) {
+ Ok(true) => eprintln!("서명 유효"),
+ Ok(false) => {
+ eprintln!("서명 무효");
+ std::process::exit(1);
+ }
+ Err(e) => {
+ eprintln!("검증 오류: {e:?}");
+ std::process::exit(1);
+ }
+ }
+}
+
+fn write_bytes(data: &[u8], path: Option<&str>, label: &str) {
+ use std::io::Write;
+ if let Some(p) = path {
+ if let Err(e) = std::fs::write(p, data) {
+ eprintln!("{label} 파일 쓰기 오류: {e}");
+ std::process::exit(1);
+ }
+ } else if let Err(e) = std::io::stdout().write_all(data) {
+ eprintln!("{label} 출력 오류: {e}");
+ std::process::exit(1);
+ }
+}
diff --git a/cli/src/cmd/mlkem.rs b/cli/src/cmd/mlkem.rs
new file mode 100644
index 0000000..7e88998
--- /dev/null
+++ b/cli/src/cmd/mlkem.rs
@@ -0,0 +1,332 @@
+use crate::input;
+use clap::Subcommand;
+use entlib_native_mlkem::{
+ HashDRBGRng, MLKEM, MLKEMDecapsulationKey, MLKEMEncapsulationKey, MLKEMParameter,
+};
+use entlib_native_pkcs8::{
+ Algorithm, DEFAULT_MEMORY_COST, DEFAULT_PARALLELISM, DEFAULT_TIME_COST, Pkcs8Params,
+};
+use entlib_native_rng::{DrbgError, HashDRBGSHA256};
+
+fn parse_param(s: &str) -> Result {
+ match s {
+ "ml-kem-512" | "mlkem512" | "ML-KEM-512" => Ok(MLKEMParameter::MLKEM512),
+ "ml-kem-768" | "mlkem768" | "ML-KEM-768" => Ok(MLKEMParameter::MLKEM768),
+ "ml-kem-1024" | "mlkem1024" | "ML-KEM-1024" => Ok(MLKEMParameter::MLKEM1024),
+ _ => Err(format!("알 수 없는 알고리즘: {s}")),
+ }
+}
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// ML-KEM 키 쌍 생성 (캡슐화 키 + 역캡슐화 키)
+ Keygen {
+ /// 파라미터 셋 (ml-kem-512 | ml-kem-768 | ml-kem-1024)
+ #[arg(long, short = 'a')]
+ algorithm: String,
+ /// 캡슐화 키 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ ek_out: Option,
+ /// 역캡슐화 키 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ dk_out: Option,
+ /// PKCS#8 PEM 형식으로 출력 (역캡슐화 키: EncryptedPrivateKeyInfo, 캡슐화 키: SubjectPublicKeyInfo)
+ #[arg(long)]
+ pkcs8: bool,
+ /// Argon2id 시간 비용 (기본: 2, --pkcs8 활성화 시에만 적용)
+ #[arg(long, default_value_t = DEFAULT_TIME_COST)]
+ time_cost: u32,
+ /// Argon2id 메모리 비용 KiB (기본: 19456, --pkcs8 활성화 시에만 적용)
+ #[arg(long, default_value_t = DEFAULT_MEMORY_COST)]
+ memory_cost: u32,
+ /// Argon2id 병렬성 (기본: 1, --pkcs8 활성화 시에만 적용)
+ #[arg(long, default_value_t = DEFAULT_PARALLELISM)]
+ parallelism: u32,
+ },
+ /// ML-KEM 캡슐화 (공유 비밀 + 암호문 생성)
+ Encaps {
+ /// 파라미터 셋 (ml-kem-512 | ml-kem-768 | ml-kem-1024)
+ #[arg(long, short = 'a')]
+ algorithm: String,
+ /// 캡슐화 키 파일
+ #[arg(long)]
+ ek_file: String,
+ /// 공유 비밀 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ ss_out: Option,
+ /// 암호문 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ ct_out: Option,
+ },
+ /// ML-KEM 역캡슐화 (공유 비밀 복원)
+ Decaps {
+ /// 파라미터 셋 (ml-kem-512 | ml-kem-768 | ml-kem-1024)
+ #[arg(long, short = 'a')]
+ algorithm: String,
+ /// 역캡슐화 키 파일
+ #[arg(long)]
+ dk_file: String,
+ /// 암호문 파일
+ #[arg(long)]
+ ct_file: String,
+ /// 공유 비밀 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ ss_out: Option,
+ },
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Keygen {
+ algorithm,
+ ek_out,
+ dk_out,
+ pkcs8,
+ time_cost,
+ memory_cost,
+ parallelism,
+ } => run_keygen(
+ algorithm,
+ ek_out,
+ dk_out,
+ pkcs8,
+ time_cost,
+ memory_cost,
+ parallelism,
+ ),
+ Ops::Encaps {
+ algorithm,
+ ek_file,
+ ss_out,
+ ct_out,
+ } => run_encaps(algorithm, ek_file, ss_out, ct_out),
+ Ops::Decaps {
+ algorithm,
+ dk_file,
+ ct_file,
+ ss_out,
+ } => run_decaps(algorithm, dk_file, ct_file, ss_out),
+ }
+}
+
+fn run_keygen(
+ algorithm: String,
+ ek_out: Option,
+ dk_out: Option,
+ pkcs8: bool,
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+) {
+ let param = match parse_param(&algorithm) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("오류: {e} — 지원 알고리즘: ml-kem-512, ml-kem-768, ml-kem-1024");
+ std::process::exit(1);
+ }
+ };
+
+ let mut rng = match HashDRBGRng::new_from_os(Some(b"entlib-mlkem-keygen")) {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("RNG 초기화 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let (ek, dk) = match MLKEM::key_gen(param, &mut rng) {
+ Ok(kp) => kp,
+ Err(e) => {
+ eprintln!("키 생성 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ if pkcs8 {
+ run_keygen_pkcs8(
+ param,
+ ek.as_bytes(),
+ dk.as_bytes(),
+ ek_out,
+ dk_out,
+ time_cost,
+ memory_cost,
+ parallelism,
+ );
+ return;
+ }
+
+ write_bytes(ek.as_bytes(), ek_out.as_deref(), "캡슐화 키");
+ write_bytes(dk.as_bytes(), dk_out.as_deref(), "역캡슐화 키");
+}
+
+fn param_to_algorithm(param: MLKEMParameter) -> Algorithm {
+ match param {
+ MLKEMParameter::MLKEM512 => Algorithm::MLKEM512,
+ MLKEMParameter::MLKEM768 => Algorithm::MLKEM768,
+ MLKEMParameter::MLKEM1024 => Algorithm::MLKEM1024,
+ }
+}
+
+fn generate_salt_nonce() -> Result<([u8; 16], [u8; 12]), DrbgError> {
+ let mut drbg = HashDRBGSHA256::new_from_os(Some(b"entlib-mlkem-pkcs8"))?;
+ let mut salt = [0u8; 16];
+ let mut nonce = [0u8; 12];
+ drbg.generate(&mut salt, None)?;
+ drbg.generate(&mut nonce, None)?;
+ Ok((salt, nonce))
+}
+
+#[allow(clippy::too_many_arguments)]
+fn run_keygen_pkcs8(
+ param: MLKEMParameter,
+ ek_bytes: &[u8],
+ dk_bytes: &[u8],
+ ek_out: Option,
+ dk_out: Option,
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+) {
+ let algo = param_to_algorithm(param);
+
+ let passphrase = match input::read_passphrase("패스프레이즈: ") {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("패스프레이즈 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let (salt, nonce) = match generate_salt_nonce() {
+ Ok(v) => v,
+ Err(e) => {
+ eprintln!("난수 생성 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let params = Pkcs8Params::new(time_cost, memory_cost, parallelism, salt, nonce);
+
+ let dk_pem =
+ match entlib_native_pkcs8::encrypt_pem(algo, dk_bytes, passphrase.as_slice(), ¶ms) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("역캡슐화 키 암호화 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let ek_pem = match entlib_native_pkcs8::encode_spki_pem(algo, ek_bytes) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("캡슐화 키 인코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ write_bytes(ek_pem.as_slice(), ek_out.as_deref(), "캡슐화 키 PEM");
+ write_bytes(dk_pem.as_slice(), dk_out.as_deref(), "역캡슐화 키 PEM");
+}
+
+fn run_encaps(algorithm: String, ek_file: String, ss_out: Option, ct_out: Option) {
+ let param = match parse_param(&algorithm) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let ek_bytes = match input::read_file(&ek_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("캡슐화 키 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let ek = match MLKEMEncapsulationKey::from_bytes(param, ek_bytes.as_slice().to_vec()) {
+ Ok(k) => k,
+ Err(e) => {
+ eprintln!("캡슐화 키 파싱 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let mut rng = match HashDRBGRng::new_from_os(Some(b"entlib-mlkem-encaps")) {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("RNG 초기화 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let (ss, ct) = match MLKEM::encaps(&ek, &mut rng) {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("캡슐화 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ write_bytes(ss.as_slice(), ss_out.as_deref(), "공유 비밀");
+ write_bytes(&ct, ct_out.as_deref(), "암호문");
+}
+
+fn run_decaps(algorithm: String, dk_file: String, ct_file: String, ss_out: Option) {
+ let param = match parse_param(&algorithm) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let dk_bytes = match input::read_file(&dk_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("역캡슐화 키 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let dk = match MLKEMDecapsulationKey::from_bytes(param, dk_bytes.as_slice()) {
+ Ok(k) => k,
+ Err(e) => {
+ eprintln!("역캡슐화 키 파싱 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let ct_bytes = match input::read_file(&ct_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("암호문 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let ss = match MLKEM::decaps(&dk, ct_bytes.as_slice()) {
+ Ok(s) => s,
+ Err(e) => {
+ eprintln!("역캡슐화 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ write_bytes(ss.as_slice(), ss_out.as_deref(), "공유 비밀");
+}
+
+fn write_bytes(data: &[u8], path: Option<&str>, label: &str) {
+ use std::io::Write;
+ if let Some(p) = path {
+ if let Err(e) = std::fs::write(p, data) {
+ eprintln!("{label} 파일 쓰기 오류: {e}");
+ std::process::exit(1);
+ }
+ } else if let Err(e) = std::io::stdout().write_all(data) {
+ eprintln!("{label} 출력 오류: {e}");
+ std::process::exit(1);
+ }
+}
diff --git a/cli/src/cmd/mod.rs b/cli/src/cmd/mod.rs
new file mode 100644
index 0000000..137cf5c
--- /dev/null
+++ b/cli/src/cmd/mod.rs
@@ -0,0 +1,21 @@
+use entlib_native_secure_buffer::SecureBuffer;
+
+pub mod argon2id;
+pub mod base64;
+pub mod blake;
+pub mod hex;
+pub mod mldsa;
+pub mod mlkem;
+pub mod pkcs8;
+pub mod sha2;
+pub mod sha3;
+
+pub(crate) fn hex_encode(digest: SecureBuffer) -> SecureBuffer {
+ match entlib_native_hex::encode(&digest) {
+ Ok(h) => h,
+ Err(e) => {
+ eprintln!("hex 인코딩 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+}
diff --git a/cli/src/cmd/pkcs8.rs b/cli/src/cmd/pkcs8.rs
new file mode 100644
index 0000000..f1c8852
--- /dev/null
+++ b/cli/src/cmd/pkcs8.rs
@@ -0,0 +1,158 @@
+use crate::input;
+use clap::Subcommand;
+use entlib_native_pkcs8::{
+ Algorithm, DEFAULT_MEMORY_COST, DEFAULT_PARALLELISM, DEFAULT_TIME_COST, Pkcs8Params,
+};
+use entlib_native_rng::{DrbgError, HashDRBGSHA256};
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// 개인 키를 PKCS#8 EncryptedPrivateKeyInfo PEM으로 암호화
+ Encrypt {
+ /// 알고리즘 (ml-dsa-44 | ml-dsa-65 | ml-dsa-87)
+ #[arg(long, short = 'a')]
+ algorithm: String,
+ /// 키 바이트 입력 파일 (필수)
+ #[arg(long)]
+ key_file: String,
+ /// PEM 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ out_file: Option,
+ /// Argon2id 시간 비용 (기본: 2)
+ #[arg(long, default_value_t = DEFAULT_TIME_COST)]
+ time_cost: u32,
+ /// Argon2id 메모리 비용 KiB (기본: 19456)
+ #[arg(long, default_value_t = DEFAULT_MEMORY_COST)]
+ memory_cost: u32,
+ /// Argon2id 병렬성 (기본: 1)
+ #[arg(long, default_value_t = DEFAULT_PARALLELISM)]
+ parallelism: u32,
+ },
+ /// PKCS#8 EncryptedPrivateKeyInfo PEM에서 개인 키 복호화
+ Decrypt {
+ /// 암호화된 PEM 입력 파일 (필수)
+ #[arg(long)]
+ in_file: String,
+ /// 키 바이트 출력 파일 (생략 시 stdout)
+ #[arg(long)]
+ out_file: Option,
+ },
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Encrypt {
+ algorithm,
+ key_file,
+ out_file,
+ time_cost,
+ memory_cost,
+ parallelism,
+ } => run_encrypt(
+ algorithm,
+ key_file,
+ out_file,
+ time_cost,
+ memory_cost,
+ parallelism,
+ ),
+ Ops::Decrypt { in_file, out_file } => run_decrypt(in_file, out_file),
+ }
+}
+
+fn run_encrypt(
+ algorithm: String,
+ key_file: String,
+ out_file: Option,
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+) {
+ let algo = match Algorithm::from_name(&algorithm) {
+ Ok(a) => a,
+ Err(e) => {
+ eprintln!("오류: {e} — 지원 알고리즘: ml-dsa-44, ml-dsa-65, ml-dsa-87");
+ std::process::exit(1);
+ }
+ };
+
+ let key_bytes = match input::read_file(&key_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("키 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let passphrase = match input::read_passphrase("패스프레이즈: ") {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("패스프레이즈 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let (salt, nonce) = match generate_salt_nonce() {
+ Ok(v) => v,
+ Err(e) => {
+ eprintln!("난수 생성 오류: {e:?}");
+ std::process::exit(1);
+ }
+ };
+
+ let params = Pkcs8Params::new(time_cost, memory_cost, parallelism, salt, nonce);
+
+ let pem = match entlib_native_pkcs8::encrypt_pem(
+ algo,
+ key_bytes.as_slice(),
+ passphrase.as_slice(),
+ ¶ms,
+ ) {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("암호화 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ input::write_output(pem, out_file.as_deref(), false);
+}
+
+fn run_decrypt(in_file: String, out_file: Option) {
+ let pem = match input::read_file(&in_file) {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("입력 파일 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let passphrase = match input::read_passphrase("패스프레이즈: ") {
+ Ok(p) => p,
+ Err(e) => {
+ eprintln!("패스프레이즈 읽기 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ let (algo, key_buf) =
+ match entlib_native_pkcs8::decrypt_pem(pem.as_slice(), passphrase.as_slice()) {
+ Ok(r) => r,
+ Err(e) => {
+ eprintln!("복호화 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+
+ eprintln!("알고리즘: {}", algo.name());
+ input::write_output(key_buf, out_file.as_deref(), false);
+}
+
+fn generate_salt_nonce() -> Result<([u8; 16], [u8; 12]), DrbgError> {
+ let mut drbg = HashDRBGSHA256::new_from_os(Some(b"entlib-pkcs8"))?;
+ let mut salt = [0u8; 16];
+ let mut nonce = [0u8; 12];
+ drbg.generate(&mut salt, None)?;
+ drbg.generate(&mut nonce, None)?;
+ Ok((salt, nonce))
+}
diff --git a/cli/src/cmd/sha2.rs b/cli/src/cmd/sha2.rs
new file mode 100644
index 0000000..bade449
--- /dev/null
+++ b/cli/src/cmd/sha2.rs
@@ -0,0 +1,98 @@
+use super::hex_encode;
+use crate::input;
+use clap::Subcommand;
+use entlib_native_sha2::api::{SHA224, SHA256, SHA384, SHA512};
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// SHA-224 (112-bit security)
+ Sha224 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ /// raw 바이너리 출력 (기본: hex)
+ #[arg(long)]
+ raw: bool,
+ },
+ /// SHA-256 (128-bit security)
+ Sha256 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// SHA-384 (192-bit security)
+ Sha384 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// SHA-512 (256-bit security)
+ Sha512 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+}
+
+macro_rules! run_hash {
+ ($hasher:expr, $in_file:expr, $out_file:expr, $raw:expr) => {{
+ let interactive = $in_file.is_none();
+ let buf = match $in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let mut hasher = $hasher;
+ hasher.update(buf.as_slice());
+ let digest = match hasher.finalize() {
+ Ok(d) => d,
+ Err(e) => {
+ eprintln!("해시 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let result = if $raw { digest } else { hex_encode(digest) };
+ input::write_output(result, $out_file.as_deref(), interactive);
+ }};
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::Sha224 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA224::new(), in_file, out_file, raw),
+ Ops::Sha256 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA256::new(), in_file, out_file, raw),
+ Ops::Sha384 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA384::new(), in_file, out_file, raw),
+ Ops::Sha512 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA512::new(), in_file, out_file, raw),
+ }
+}
diff --git a/cli/src/cmd/sha3.rs b/cli/src/cmd/sha3.rs
new file mode 100644
index 0000000..d3404b3
--- /dev/null
+++ b/cli/src/cmd/sha3.rs
@@ -0,0 +1,202 @@
+use super::hex_encode;
+use crate::input;
+use clap::Subcommand;
+use entlib_native_sha3::api::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256};
+use entlib_native_sha3::file::{sha3_224 as sha3_224_file, sha3_256 as sha3_256_file, sha3_384 as sha3_384_file, sha3_512 as sha3_512_file};
+
+#[derive(Subcommand)]
+pub(crate) enum Ops {
+ /// SHA3-224 (112-bit security)
+ Sha3_224 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ /// raw 바이너리 출력 (기본: hex)
+ #[arg(long)]
+ raw: bool,
+ },
+ /// SHA3-256 (128-bit security)
+ Sha3_256 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// SHA3-384 (192-bit security)
+ Sha3_384 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// SHA3-512 (256-bit security)
+ Sha3_512 {
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// 파일 스트리밍 해시 (SHA3-224/256/384/512)
+ HashFile {
+ /// SHA3 변형 (224, 256, 384, 512)
+ #[arg(long, default_value_t = 256)]
+ bits: u16,
+ /// 해시할 파일 경로
+ file: String,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// XOF SHAKE128 (128-bit security, 가변 출력 길이)
+ Shake128 {
+ /// 출력 바이트 수
+ #[arg(long)]
+ output_len: usize,
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+ /// XOF SHAKE256 (256-bit security, 가변 출력 길이)
+ Shake256 {
+ /// 출력 바이트 수
+ #[arg(long)]
+ output_len: usize,
+ #[arg(long)]
+ in_file: Option,
+ #[arg(long)]
+ out_file: Option,
+ #[arg(long)]
+ raw: bool,
+ },
+}
+
+macro_rules! run_hash {
+ ($hasher:expr, $in_file:expr, $out_file:expr, $raw:expr) => {{
+ let interactive = $in_file.is_none();
+ let buf = match $in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let mut hasher = $hasher;
+ hasher.update(buf.as_slice());
+ let digest = match hasher.finalize() {
+ Ok(d) => d,
+ Err(e) => {
+ eprintln!("해시 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let result = if $raw { digest } else { hex_encode(digest) };
+ input::write_output(result, $out_file.as_deref(), interactive);
+ }};
+}
+
+macro_rules! run_xof {
+ ($hasher:expr, $output_len:expr, $in_file:expr, $out_file:expr, $raw:expr) => {{
+ let interactive = $in_file.is_none();
+ let buf = match $in_file
+ .as_deref()
+ .map(input::read_file)
+ .unwrap_or_else(input::read_stdin)
+ {
+ Ok(b) => b,
+ Err(e) => {
+ eprintln!("오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let mut hasher = $hasher;
+ hasher.update(buf.as_slice());
+ let digest = match hasher.finalize($output_len) {
+ Ok(d) => d,
+ Err(e) => {
+ eprintln!("해시 오류: {e}");
+ std::process::exit(1);
+ }
+ };
+ let result = if $raw { digest } else { hex_encode(digest) };
+ input::write_output(result, $out_file.as_deref(), interactive);
+ }};
+}
+
+pub(crate) fn run(op: Ops) {
+ match op {
+ Ops::HashFile {
+ bits,
+ file,
+ out_file,
+ raw,
+ } => {
+ let digest = match bits {
+ 224 => sha3_224_file::hash_file(&file),
+ 256 => sha3_256_file::hash_file(&file),
+ 384 => sha3_384_file::hash_file(&file),
+ 512 => sha3_512_file::hash_file(&file),
+ _ => {
+ eprintln!("지원하지 않는 비트 길이: {bits} (224, 256, 384, 512 중 선택)");
+ std::process::exit(1);
+ }
+ };
+ match digest {
+ Ok(d) => {
+ let result = if raw { d } else { hex_encode(d) };
+ input::write_output(result, out_file.as_deref(), false);
+ }
+ Err(e) => {
+ eprintln!("파일 해시 오류: {e}");
+ std::process::exit(1);
+ }
+ }
+ }
+ Ops::Sha3_224 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA3_224::new(), in_file, out_file, raw),
+ Ops::Sha3_256 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA3_256::new(), in_file, out_file, raw),
+ Ops::Sha3_384 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA3_384::new(), in_file, out_file, raw),
+ Ops::Sha3_512 {
+ in_file,
+ out_file,
+ raw,
+ } => run_hash!(SHA3_512::new(), in_file, out_file, raw),
+ Ops::Shake128 {
+ output_len,
+ in_file,
+ out_file,
+ raw,
+ } => run_xof!(SHAKE128::new(), output_len, in_file, out_file, raw),
+ Ops::Shake256 {
+ output_len,
+ in_file,
+ out_file,
+ raw,
+ } => run_xof!(SHAKE256::new(), output_len, in_file, out_file, raw),
+ }
+}
diff --git a/cli/src/input.rs b/cli/src/input.rs
new file mode 100644
index 0000000..0f01ad4
--- /dev/null
+++ b/cli/src/input.rs
@@ -0,0 +1,190 @@
+use core::ptr::write_volatile;
+use entlib_native_secure_buffer::SecureBuffer;
+use std::io::{self, Write};
+
+pub(crate) fn read_stdin() -> Result {
+ #[cfg(unix)]
+ {
+ use std::os::fd::AsRawFd;
+ let fd = io::stdin().as_raw_fd();
+ if unsafe { libc::isatty(fd) == 1 } {
+ eprint!(">>>> 입력 모드 활성화.\n입력: ");
+ io::stderr().flush().ok();
+ return read_no_echo(fd);
+ }
+ }
+
+ use std::io::Read;
+ let mut raw: Vec = Vec::new();
+ io::stdin()
+ .read_to_end(&mut raw)
+ .map_err(|e| e.to_string())?;
+ // 파이프 경유 시 상류 명령이 추가한 개행 문자 제거
+ if raw.last() == Some(&b'\n') {
+ raw.pop();
+ }
+ if raw.last() == Some(&b'\r') {
+ raw.pop();
+ }
+ vec_to_secure(raw)
+}
+
+#[cfg(unix)]
+fn read_no_echo(fd: i32) -> Result {
+ let mut old: libc::termios = unsafe { core::mem::zeroed() };
+ if unsafe { libc::tcgetattr(fd, &mut old) } != 0 {
+ return Err("터미널 속성 획득 실패".into());
+ }
+
+ let mut raw = old;
+ raw.c_lflag &= !(libc::ECHO | libc::ECHOE | libc::ECHOK | libc::ECHONL);
+ if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 {
+ return Err("에코 비활성화 실패".into());
+ }
+
+ let result = read_line_bytes();
+ unsafe { libc::tcsetattr(fd, libc::TCSANOW, &old) };
+ eprintln!();
+ result
+}
+
+fn read_line_bytes() -> Result {
+ use std::io::Read;
+ // Q. T. Felix NOTE: with_capacity로 재할당 최소화
+ // 재할당 시 이전 heap 영역은 소거 불가
+ let mut raw: Vec = Vec::with_capacity(4096);
+ let mut byte = [0u8; 1];
+ loop {
+ match io::stdin().read(&mut byte) {
+ Ok(0) => break,
+ Ok(_) => {
+ if byte[0] == b'\n' {
+ break;
+ }
+ if byte[0] != b'\r' {
+ raw.push(byte[0]);
+ }
+ }
+ Err(e) => return Err(e.to_string()),
+ }
+ }
+ vec_to_secure(raw)
+}
+
+/// 패스프레이즈를 TTY에서 에코 없이 읽습니다.
+/// stdin이 파이프이더라도 /dev/tty에서 직접 읽어 충돌을 방지합니다.
+pub(crate) fn read_passphrase(prompt: &str) -> Result {
+ #[cfg(unix)]
+ {
+ use std::ffi::CString;
+ let path = CString::new("/dev/tty").unwrap();
+ let fd = unsafe { libc::open(path.as_ptr(), libc::O_RDWR) };
+ if fd >= 0 {
+ eprint!("{prompt}");
+ io::stderr().flush().ok();
+ let result = read_no_echo_fd(fd);
+ unsafe { libc::close(fd) };
+ eprintln!();
+ return result;
+ }
+ }
+ // TTY 열기 실패 시 stdin 폴백
+ eprint!("{prompt}");
+ io::stderr().flush().ok();
+ read_stdin()
+}
+
+#[cfg(unix)]
+fn read_no_echo_fd(fd: i32) -> Result {
+ let mut old: libc::termios = unsafe { core::mem::zeroed() };
+ if unsafe { libc::tcgetattr(fd, &mut old) } != 0 {
+ return Err("터미널 속성 획득 실패".into());
+ }
+ let mut raw = old;
+ raw.c_lflag &= !(libc::ECHO | libc::ECHOE | libc::ECHOK | libc::ECHONL);
+ if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) } != 0 {
+ return Err("에코 비활성화 실패".into());
+ }
+ let result = read_line_from_fd(fd);
+ unsafe { libc::tcsetattr(fd, libc::TCSANOW, &old) };
+ result
+}
+
+#[cfg(unix)]
+fn read_line_from_fd(fd: i32) -> Result {
+ let mut raw: Vec = Vec::with_capacity(4096);
+ let mut byte = [0u8; 1];
+ loop {
+ let n = unsafe { libc::read(fd, byte.as_mut_ptr() as *mut libc::c_void, 1) };
+ match n {
+ 0 => break,
+ n if n < 0 => return Err("패스프레이즈 읽기 오류".into()),
+ _ => {
+ if byte[0] == b'\n' {
+ break;
+ }
+ if byte[0] != b'\r' {
+ raw.push(byte[0]);
+ }
+ }
+ }
+ }
+ vec_to_secure(raw)
+}
+
+pub(crate) fn read_file(path: &str) -> Result {
+ let mut raw = std::fs::read(path).map_err(|e| format!("파일 읽기 오류: {e}"))?;
+ let mut buf =
+ SecureBuffer::new_owned(raw.len()).map_err(|e| format!("메모리 할당 오류: {e}"))?;
+ buf.as_mut_slice().copy_from_slice(&raw);
+ for b in raw.iter_mut() {
+ unsafe { write_volatile(b as *mut u8, 0) };
+ }
+ Ok(buf)
+}
+
+fn vec_to_secure(mut raw: Vec) -> Result {
+ let mut buf =
+ SecureBuffer::new_owned(raw.len()).map_err(|e| format!("메모리 할당 오류: {e}"))?;
+ buf.as_mut_slice().copy_from_slice(&raw);
+ for b in raw.iter_mut() {
+ unsafe { write_volatile(b as *mut u8, 0) };
+ }
+ Ok(buf)
+}
+
+pub(crate) fn write_output(result: SecureBuffer, out_file: Option<&str>, interactive: bool) {
+ if let Some(path) = out_file {
+ if let Err(e) = std::fs::write(path, result.as_slice()) {
+ eprintln!("파일 쓰기 오류: {e}");
+ std::process::exit(1);
+ }
+ // SecureBuffer 소거부
+ return;
+ }
+
+ let stdout = io::stdout();
+ let mut out = stdout.lock();
+ // stdout이 TTY일 때만 사람 친화적 접두사 표시 — 파이프 경유 시 raw 출력 유지
+ let tty_out = is_tty_stdout();
+ if interactive && tty_out {
+ let _ = write!(out, "결과: ");
+ }
+ if let Err(e) = out.write_all(result.as_slice()) {
+ eprintln!("출력 오류: {e}");
+ std::process::exit(1);
+ }
+ let _ = writeln!(out);
+ // SecureBuffer 소거부
+}
+
+#[cfg(unix)]
+fn is_tty_stdout() -> bool {
+ use std::os::fd::AsRawFd;
+ unsafe { libc::isatty(io::stdout().as_raw_fd()) == 1 }
+}
+
+#[cfg(not(unix))]
+fn is_tty_stdout() -> bool {
+ false
+}
diff --git a/cli/src/main.rs b/cli/src/main.rs
new file mode 100644
index 0000000..fcc762b
--- /dev/null
+++ b/cli/src/main.rs
@@ -0,0 +1,75 @@
+use clap::{Parser, Subcommand};
+
+mod cmd;
+mod input;
+
+#[derive(Parser)]
+#[command(name = "entlib-cli")]
+struct Cli {
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+ /// Base64 인코딩/디코딩
+ Base64 {
+ #[command(subcommand)]
+ op: cmd::base64::Ops,
+ },
+ /// Hex 인코딩/디코딩
+ Hex {
+ #[command(subcommand)]
+ op: cmd::hex::Ops,
+ },
+ /// SHA-2 해시 (SHA-224/256/384/512)
+ Sha2 {
+ #[command(subcommand)]
+ op: cmd::sha2::Ops,
+ },
+ /// SHA-3 해시 (SHA3-224/256/384/512, SHAKE128/256)
+ Sha3 {
+ #[command(subcommand)]
+ op: cmd::sha3::Ops,
+ },
+ /// PKCS#8 EncryptedPrivateKeyInfo 암호화/복호화
+ Pkcs8 {
+ #[command(subcommand)]
+ op: cmd::pkcs8::Ops,
+ },
+ /// ML-DSA 전자 서명 (키 생성 / 서명 / 검증)
+ MlDsa {
+ #[command(subcommand)]
+ op: cmd::mldsa::Ops,
+ },
+ /// ML-KEM 키 캡슐화 (키 생성 / 캡슐화 / 역캡슐화)
+ MlKem {
+ #[command(subcommand)]
+ op: cmd::mlkem::Ops,
+ },
+ /// Argon2id 비밀번호 해시 (RFC 9106)
+ Argon2id {
+ #[command(subcommand)]
+ op: cmd::argon2id::Ops,
+ },
+ /// BLAKE 해시 (BLAKE2b / BLAKE3)
+ Blake {
+ #[command(subcommand)]
+ op: cmd::blake::Ops,
+ },
+}
+
+fn main() {
+ let cli = Cli::parse();
+ match cli.command {
+ Commands::Base64 { op } => cmd::base64::run(op),
+ Commands::Hex { op } => cmd::hex::run(op),
+ Commands::Sha2 { op } => cmd::sha2::run(op),
+ Commands::Sha3 { op } => cmd::sha3::run(op),
+ Commands::Pkcs8 { op } => cmd::pkcs8::run(op),
+ Commands::MlDsa { op } => cmd::mldsa::run(op),
+ Commands::MlKem { op } => cmd::mlkem::run(op),
+ Commands::Argon2id { op } => cmd::argon2id::run(op),
+ Commands::Blake { op } => cmd::blake::run(op),
+ }
+}
diff --git a/crypto/rng/Cargo.toml b/core/base/Cargo.toml
similarity index 81%
rename from crypto/rng/Cargo.toml
rename to core/base/Cargo.toml
index 083c1ff..b773cd2 100644
--- a/crypto/rng/Cargo.toml
+++ b/core/base/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "entlib-native-rng"
+name = "entlib-native-base"
version.workspace = true
edition.workspace = true
authors.workspace = true
diff --git a/core/base/src/error.rs b/core/base/src/error.rs
new file mode 100644
index 0000000..ef8a94f
--- /dev/null
+++ b/core/base/src/error.rs
@@ -0,0 +1,339 @@
+/// FFI 경계를 넘어 전달될 수 있는 C 호환 에러 코드 열거형입니다.
+/// 메시지는 모호하게 유지되며, 구체적인 실패 원인은 내부 로그(보안 감사용)로만 남겨야 합니다.
+#[repr(C)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum EntLibState {
+ /// 과정 또는 결과 표현이 성공했습니다.
+ Success = 0,
+
+ /// 모듈 상태 오류 (Module State Error)
+ /// 모듈이 초기화되지 않았거나, POST(자가 진단)가 실패하여
+ /// '에러 상태(Error State)'에 진입했을 때 발생합니다. 이 상태에서는 모든 암호 연산이 차단되어야 합니다.
+ StateError = 1,
+
+ /// 유효하지 않은 입력 (Invalid Input)
+ /// 키 길이 불일치, 포맷 오류, 범위를 벗어난 매개변수 등 입력값 검증 실패 시 반환됩니다.
+ /// "어떤" 값이 "왜" 틀렸는지는 절대 반환하지 않습니다.
+ InvalidInput = 2,
+
+ /// 암호 연산 실패 (Cryptographic Operation Failed)
+ /// 서명 검증 실패, MAC 불일치, 복호화 실패 등 알고리즘 수행 중 발생한 논리적 오류입니다.
+ /// 타이밍 공격을 막기 위해 상수-시간(Constant-Time) 검증이 끝난 후 일괄적으로 이 에러를 반환해야 합니다.
+ OperationFailed = 3,
+
+ /// 리소스 및 환경 오류 (Resource/Environment Error)
+ /// OS 메모리 할당 실패, 난수 발생기(RNG) 엔트로피 부족,
+ /// 또는 외부 주입 메모리의 페이지 정렬(Alignment) 검증 실패 시 발생합니다.
+ ResourceError = 4,
+
+ /// 내부 패닉 및 치명적 예외 (Fatal Error)
+ /// Rust 내부에서 `panic!`이 발생했거나 복구할 수 없는 하드웨어 결함이 감지되었을 때 반환됩니다.
+ FatalError = 5,
+}
+
+/// 개별 크레이트의 상세 에러를 FFI 경계용 모호한 에러로 변환하는 트레이트입니다.
+pub trait ToExternalError {
+ /// 상세 에러를 FIPS 요구사항에 맞는 안전한 에러 코드로 변환합니다.
+ fn to_fips_error(&self) -> EntLibState;
+
+ /// (선택적) CC EAL4+ 인증을 위해 내부 보안 감사 로그(Audit Log)에
+ /// 상세 에러 원인을 기록하는 기본 메서드를 제공할 수 있습니다.
+ fn log_security_audit(&self) {
+ // TODO: 내부 로깅 시스템 호출 (예: tracing, log 크레이트 연동)
+ // 외부(Java)로는 절대 전달되지 않는 안전한 영역임.
+ }
+}
+
+pub mod hash {
+ use crate::error::secure_buffer::SecureBufferError;
+ use crate::error::{EntLibState, ToExternalError};
+
+ #[derive(Debug)]
+ pub enum HashError {
+ InvalidOutputLength,
+ Buffer(SecureBufferError),
+ }
+
+ impl core::fmt::Display for HashError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ HashError::InvalidOutputLength => f.write_str("invalid output length"),
+ HashError::Buffer(e) => write!(f, "{}", e),
+ }
+ }
+ }
+
+ impl core::error::Error for HashError {}
+
+ impl From for HashError {
+ fn from(e: SecureBufferError) -> Self {
+ HashError::Buffer(e)
+ }
+ }
+
+ impl ToExternalError for HashError {
+ fn to_fips_error(&self) -> EntLibState {
+ match self {
+ HashError::InvalidOutputLength => EntLibState::InvalidInput,
+ HashError::Buffer(e) => e.to_fips_error(),
+ }
+ }
+ }
+}
+
+pub mod argon2id {
+ use crate::error::hash::HashError;
+ use crate::error::secure_buffer::SecureBufferError;
+ use crate::error::{EntLibState, ToExternalError};
+
+ #[derive(Debug)]
+ pub enum Argon2idError {
+ InvalidParameter,
+ Hash(HashError),
+ Buffer(SecureBufferError),
+ }
+
+ impl core::fmt::Display for Argon2idError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Argon2idError::InvalidParameter => f.write_str("invalid parameter"),
+ Argon2idError::Hash(e) => write!(f, "{}", e),
+ Argon2idError::Buffer(e) => write!(f, "{}", e),
+ }
+ }
+ }
+
+ impl core::error::Error for Argon2idError {}
+
+ impl From for Argon2idError {
+ fn from(e: HashError) -> Self {
+ Argon2idError::Hash(e)
+ }
+ }
+
+ impl From for Argon2idError {
+ fn from(e: SecureBufferError) -> Self {
+ Argon2idError::Buffer(e)
+ }
+ }
+
+ impl ToExternalError for Argon2idError {
+ fn to_fips_error(&self) -> EntLibState {
+ match self {
+ Argon2idError::InvalidParameter => EntLibState::InvalidInput,
+ Argon2idError::Hash(e) => e.to_fips_error(),
+ Argon2idError::Buffer(e) => e.to_fips_error(),
+ }
+ }
+ }
+}
+
+pub mod secure_buffer {
+ use crate::error::{EntLibState, ToExternalError};
+
+ #[derive(Debug)]
+ pub enum SecureBufferError {
+ AllocationFailed,
+ InvalidLayout,
+ MemoryLockFailed,
+ PageAlignmentViolation,
+ }
+
+ impl core::fmt::Display for SecureBufferError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ SecureBufferError::AllocationFailed => f.write_str("allocation failed"),
+ SecureBufferError::InvalidLayout => f.write_str("invalid layout"),
+ SecureBufferError::MemoryLockFailed => f.write_str("memory lock failed"),
+ SecureBufferError::PageAlignmentViolation => {
+ f.write_str("page alignment violation")
+ }
+ }
+ }
+ }
+
+ impl core::error::Error for SecureBufferError {}
+
+ impl ToExternalError for SecureBufferError {
+ fn to_fips_error(&self) -> EntLibState {
+ match self {
+ SecureBufferError::AllocationFailed
+ | SecureBufferError::InvalidLayout
+ | SecureBufferError::MemoryLockFailed => EntLibState::ResourceError,
+ SecureBufferError::PageAlignmentViolation => EntLibState::InvalidInput,
+ }
+ }
+ }
+}
+
+pub mod rng {
+ use crate::error::secure_buffer::SecureBufferError;
+ use crate::error::{EntLibState, ToExternalError};
+
+ #[derive(Debug)]
+ pub enum RngError {
+ OsKernelError,
+ EntropySourceEof,
+ SizeLimitExceeded,
+ InvalidAlignment,
+ HardwareEntropyExhausted,
+ InsufficientEntropy,
+ Buffer(SecureBufferError),
+ }
+
+ impl From for RngError {
+ fn from(e: SecureBufferError) -> Self {
+ RngError::Buffer(e)
+ }
+ }
+
+ impl ToExternalError for RngError {
+ fn to_fips_error(&self) -> EntLibState {
+ match self {
+ RngError::SizeLimitExceeded
+ | RngError::InvalidAlignment
+ | RngError::InsufficientEntropy => EntLibState::InvalidInput,
+ RngError::OsKernelError
+ | RngError::EntropySourceEof
+ | RngError::HardwareEntropyExhausted => EntLibState::ResourceError,
+ RngError::Buffer(e) => e.to_fips_error(),
+ }
+ }
+ }
+}
+
+pub mod hex {
+ use crate::error::secure_buffer::SecureBufferError;
+ use crate::error::{EntLibState, ToExternalError};
+
+ #[derive(Debug)]
+ pub enum HexError {
+ IllegalCharacter,
+ Buffer(SecureBufferError),
+ }
+
+ impl core::fmt::Display for HexError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ HexError::IllegalCharacter => f.write_str("illegal character"),
+ HexError::Buffer(e) => write!(f, "{}", e),
+ }
+ }
+ }
+
+ impl core::error::Error for HexError {}
+
+ impl From for HexError {
+ fn from(e: SecureBufferError) -> Self {
+ HexError::Buffer(e)
+ }
+ }
+
+ impl ToExternalError for HexError {
+ fn to_fips_error(&self) -> EntLibState {
+ match self {
+ HexError::IllegalCharacter => EntLibState::InvalidInput,
+ HexError::Buffer(e) => e.to_fips_error(),
+ }
+ }
+ }
+}
+
+pub mod base64 {
+ use crate::error::secure_buffer::SecureBufferError;
+ use crate::error::{EntLibState, ToExternalError};
+
+ #[derive(Debug)]
+ pub enum Base64Error {
+ InvalidLength,
+ IllegalCharacterOrPadding,
+ Buffer(SecureBufferError),
+ }
+
+ impl core::fmt::Display for Base64Error {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ Base64Error::InvalidLength => f.write_str("invalid length"),
+ Base64Error::IllegalCharacterOrPadding => {
+ f.write_str("illegal character or padding")
+ }
+ Base64Error::Buffer(e) => write!(f, "{}", e),
+ }
+ }
+ }
+
+ impl core::error::Error for Base64Error {}
+
+ impl From for Base64Error {
+ fn from(e: SecureBufferError) -> Self {
+ Base64Error::Buffer(e)
+ }
+ }
+
+ impl ToExternalError for Base64Error {
+ fn to_fips_error(&self) -> EntLibState {
+ match self {
+ Base64Error::InvalidLength | Base64Error::IllegalCharacterOrPadding => {
+ EntLibState::InvalidInput
+ }
+ Base64Error::Buffer(e) => e.to_fips_error(),
+ }
+ }
+ }
+}
+
+pub mod mldsa {
+ use crate::error::hash::HashError;
+ use crate::error::secure_buffer::SecureBufferError;
+ use crate::error::{EntLibState, ToExternalError};
+
+ #[derive(Debug)]
+ pub enum MLDSAError {
+ /// 입력 바이트 슬라이스의 길이가 요구사항과 일치하지 않습니다.
+ InvalidLength,
+ /// 내부 연산 실패 (예: 해시 함수 오류, 메모리 할당 실패)
+ InternalError,
+ /// 난수 생성기(RNG) 오류
+ RngError,
+ /// ctx(컨텍스트) 길이가 FIPS 204 제한(255바이트)을 초과합니다.
+ ContextTooLong,
+ /// 서명 시도가 최대 반복 횟수를 초과하였습니다 (극히 희박한 경우).
+ SigningFailed,
+ /// 서명 검증 실패
+ InvalidSignature,
+ /// 아직 구현되지 않은 기능입니다.
+ NotImplemented,
+ /// 내부 해시 연산 실패
+ Hash(HashError),
+ /// SecureBuffer 할당 실패
+ Buffer(SecureBufferError),
+ }
+
+ impl ToExternalError for MLDSAError {
+ fn to_fips_error(&self) -> EntLibState {
+ match self {
+ MLDSAError::InvalidLength | MLDSAError::ContextTooLong => EntLibState::InvalidInput,
+
+ MLDSAError::InvalidSignature | MLDSAError::SigningFailed | MLDSAError::Hash(_) => {
+ EntLibState::OperationFailed
+ }
+
+ MLDSAError::RngError | MLDSAError::Buffer(_) => EntLibState::ResourceError,
+
+ MLDSAError::InternalError | MLDSAError::NotImplemented => EntLibState::FatalError,
+ }
+ }
+ }
+
+ impl From for MLDSAError {
+ fn from(e: HashError) -> Self {
+ MLDSAError::Hash(e)
+ }
+ }
+
+ impl From for MLDSAError {
+ fn from(e: SecureBufferError) -> Self {
+ MLDSAError::Buffer(e)
+ }
+ }
+}
diff --git a/core/base/src/lib.rs b/core/base/src/lib.rs
new file mode 100644
index 0000000..fa02ef4
--- /dev/null
+++ b/core/base/src/lib.rs
@@ -0,0 +1,3 @@
+#![no_std]
+
+pub mod error;
diff --git a/core/base64/Cargo.toml b/core/base64/Cargo.toml
index 687883e..21382d0 100644
--- a/core/base64/Cargo.toml
+++ b/core/base64/Cargo.toml
@@ -8,10 +8,4 @@ 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"] }
-
-#[[bench]]
-#name = "base64_bench"
-#harness = false
\ No newline at end of file
+entlib-native-base.workspace = true
\ No newline at end of file
diff --git a/core/base64/src/lib.rs b/core/base64/src/lib.rs
index 35e89ae..4ca286d 100644
--- a/core/base64/src/lib.rs
+++ b/core/base64/src/lib.rs
@@ -1,6 +1,7 @@
pub mod base64;
use base64::{ct_b64_to_bin_u8, ct_bin_to_b64_u8};
+use entlib_native_base::error::base64::Base64Error;
use entlib_native_secure_buffer::SecureBuffer;
/// RFC 4648 표준 Base64 인코딩 함수입니다.
@@ -31,7 +32,7 @@ use entlib_native_secure_buffer::SecureBuffer;
/// assert_eq!(encoded.as_slice(), b"TWFu");
/// // input, encoded 모두 여기서 Drop되면서 내용이 자동 소거됨
/// ```
-pub fn encode(input: &SecureBuffer) -> Result {
+pub fn encode(input: &SecureBuffer) -> Result {
let input = input.as_slice();
let full_groups = input.len() / 3;
@@ -106,14 +107,14 @@ pub fn encode(input: &SecureBuffer) -> Result {
/// invalid.as_mut_slice().copy_from_slice(b"!!!!");
/// assert!(decode(&invalid).is_err());
/// ```
-pub fn decode(input: &SecureBuffer) -> Result {
+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");
+ return Err(Base64Error::InvalidLength);
}
if input.is_empty() {
- return SecureBuffer::new_owned(0);
+ return Ok(SecureBuffer::new_owned(0)?);
}
let num_groups = input.len() / 4;
@@ -190,7 +191,7 @@ pub fn decode(input: &SecureBuffer) -> Result {
if invalid != 0 {
// buf는 여기서 Drop되며 중간값을 포함한 내용 자동 소거
- Err("invalid base64: illegal character or padding")
+ Err(Base64Error::IllegalCharacterOrPadding)
} else {
Ok(buf)
}
diff --git a/core/hex/Cargo.toml b/core/hex/Cargo.toml
index ac7c225..424758e 100644
--- a/core/hex/Cargo.toml
+++ b/core/hex/Cargo.toml
@@ -8,3 +8,4 @@ license.workspace = true
[dependencies]
entlib-native-secure-buffer.workspace = true
entlib-native-constant-time.workspace = true
+entlib-native-base.workspace = true
diff --git a/core/hex/src/hex.rs b/core/hex/src/hex.rs
index 3457f73..292e4eb 100644
--- a/core/hex/src/hex.rs
+++ b/core/hex/src/hex.rs
@@ -81,10 +81,11 @@ fn decode_nibble_ct(c: u8) -> (u8, Choice) {
// 6. 상수 시간 선택 (Constant-Time Select)
// 기본값 0에서 시작하여 조건이 참(0xFF)일 때만 해당 값을 덮어씁니다.
+ // ct_select(a, b, choice): choice=0xFF → a, choice=0x00 → b
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 = u8::ct_select(&val_digit, &result, is_digit);
+ result = u8::ct_select(&val_lower, &result, is_lower_hex);
+ result = u8::ct_select(&val_upper, &result, is_upper_hex);
(result, is_valid)
}
@@ -124,7 +125,7 @@ pub(crate) fn decode_hex_core_ct(input: &[u8], output: &mut [u8]) -> Choice {
let decoded_byte = (high_nibble << 4) | low_nibble;
// 유효하지 않은 바이트인 경우 출력 버퍼에 0을 기록하여 쓰레기값 생성을 방지합니다.
- output[i] = u8::ct_select(&0, &decoded_byte, byte_valid);
+ output[i] = u8::ct_select(&decoded_byte, &0, byte_valid);
}
all_valid
diff --git a/core/hex/src/lib.rs b/core/hex/src/lib.rs
index 5f01420..4be567e 100644
--- a/core/hex/src/lib.rs
+++ b/core/hex/src/lib.rs
@@ -1,6 +1,7 @@
mod hex;
use crate::hex::{decode_hex_core_ct, encode_hex_core_ct};
+use entlib_native_base::error::hex::HexError;
use entlib_native_secure_buffer::SecureBuffer;
/// 군사급 보안 요구사항을 충족하는 상수 시간 Hex 인코딩 함수입니다.
@@ -13,7 +14,7 @@ use entlib_native_secure_buffer::SecureBuffer;
/// # Returns
/// - `Ok(SecureBuffer)` - Hex 인코딩이 완료된 새 버퍼 (OS 레벨 잠금 완료)
/// - `Err` - 메모리 할당 실패 시
-pub fn encode(input: &SecureBuffer) -> Result {
+pub fn encode(input: &SecureBuffer) -> Result {
// 1. 읽기 전용 및 쓰기 전용 슬라이스 확보
// 내부 데이터를 다룰 때 as_slice 및 as_mut_slice를 통해 반환된 슬라이스는 SecureBuffer의 수명에 묶여 있습니다.
let input_slice = input.as_slice();
@@ -42,7 +43,7 @@ pub fn encode(input: &SecureBuffer) -> Result {
/// # Returns
/// - `Ok(SecureBuffer)` - 디코딩이 완료된 새 버퍼 (OS 레벨 잠금 완료)
/// - `Err` - 메모리 할당 실패 또는 유효하지 않은 Hex 문자열 입력 시
-pub fn decode(input: &SecureBuffer) -> Result {
+pub fn decode(input: &SecureBuffer) -> Result {
// 1. 읽기 전용 슬라이스 확보
let input_slice = input.as_slice();
@@ -67,6 +68,6 @@ pub fn decode(input: &SecureBuffer) -> Result {
// 이때 할당된 전체 capacity에 대해 Zeroizer::zeroize_raw가 수행되어 불완전한 데이터가 물리적으로 소거됩니다.
// 타이밍/패딩 오라클 공격 방지를 위해 에러 원인(위치, 발생한 문자 등)을 상세히 밝히지 않고 균일한 메시지를 반환합니다.
- Err("Security Violation: Invalid hex encoding detected.")
+ Err(HexError::IllegalCharacter)
}
}
diff --git a/core/rng/Cargo.toml b/core/rng/Cargo.toml
new file mode 100644
index 0000000..35980d3
--- /dev/null
+++ b/core/rng/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "entlib-native-rng"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+entlib-native-secure-buffer.workspace = true
+entlib-native-sha2.workspace = true
+entlib-native-sha3.workspace = true
+entlib-native-base.workspace = true
\ No newline at end of file
diff --git a/core/rng/README.md b/core/rng/README.md
new file mode 100644
index 0000000..6f6e407
--- /dev/null
+++ b/core/rng/README.md
@@ -0,0 +1,205 @@
+# Hash_DRBG 크레이트 (entlib-native-rng)
+
+> Q. T. Felix (수정: 26.03.21 UTC+9)
+>
+> [English README](README_EN.md)
+
+`entlib-native-rng`은 NIST SP 800-90A Rev. 1 표준의 Section 10.1.1에 명시된 Hash_DRBG(Hash-based Deterministic Random Bit Generator)를 구현하는 `no_std` 호환 크레이트입니다. 본 크레이트는 FIPS 140-3 승인 알고리즘 요건을 기준으로 설계되었으며, 내부 상태를 `SecureBuffer`로 관리하여 메모리 덤프·콜드 부트 공격에 대한 방어를 내장합니다.
+
+## 보안 위협 모델
+
+DRBG의 보안은 세 가지 핵심 속성에 의존합니다.
+
+**예측 불가능성(Unpredictability)**: 공격자가 이전 출력을 전부 관찰하더라도 다음 출력을 예측할 수 없어야 합니다. Hash_DRBG는 내부 상태 $V$와 $C$를 출력에 직접 노출하지 않고, 단방향 해시 함수를 통해서만 출력을 유도함으로써 이를 보장합니다.
+
+**상태 복원 공격 저항(State Recovery Resistance)**: 내부 상태 $V$와 $C$가 노출되더라도 이전 출력을 역산할 수 없어야 합니다. 두 값은 `SecureBuffer`에 격리되어 OS 레벨 메모리 잠금(`mlock`)과 Drop 시점의 강제 소거가 적용됩니다.
+
+**재시드 강제(Mandatory Reseed)**: reseed 카운터가 $2^{48}$을 초과하면 `generate`가 즉시 `ReseedRequired`를 반환합니다. 이는 동일한 상태에서 과도하게 많은 출력을 생성하는 것을 구조적으로 차단합니다.
+
+## 아키텍처
+
+```
+entlib-native-rng
+├── os_entropy (내부 모듈) — 플랫폼별 OS 엔트로피 추출
+└── hash_drbg (내부 모듈) — NIST SP 800-90A Hash_DRBG 구현
+ ├── HashDRBGSHA224 (security_strength = 112 bits)
+ ├── HashDRBGSHA256 (security_strength = 128 bits)
+ ├── HashDRBGSHA384 (security_strength = 192 bits)
+ └── HashDRBGSHA512 (security_strength = 256 bits)
+```
+
+유일하게 공개되는 초기화 경로는 `new_from_os`입니다. 사용자가 엔트로피를 직접 주입하는 `instantiate`는 `pub(crate)`로 제한되어 예측 가능한 시드 주입 위험을 원천 차단합니다.
+
+## OS 엔트로피 소스
+
+`extract_os_entropy`는 플랫폼별 직접 syscall 또는 검증된 라이브러리 함수를 통해 원시 엔트로피를 수집합니다. `getrandom` 등 외부 크레이트에 의존하지 않습니다.
+
+| 타겟 | 방식 |
+|----------------------------|--------------------------------------------|
+| `linux + x86_64` | `SYS_getrandom` (318), `syscall` 명령어 직접 호출 |
+| `linux + aarch64` | `SYS_getrandom` (278), `svc #0` 명령어 직접 호출 |
+| `macos` (x86_64 / aarch64) | `getentropy(2)` — libSystem FFI |
+
+Linux 구현은 `EINTR` 재시도 및 부분 읽기 루프를 포함하여 항상 `size` 바이트를 완전히 채웁니다. macOS의 `getentropy`는 단일 호출로 완전 채움을 보장하며 최대 256 바이트 제한이 있으나, `new_from_os`가 요청하는 최대 크기($2 \times 32 = 64$ bytes for SHA-512)는 이를 초과하지 않습니다.
+
+수집된 엔트로피와 nonce는 `SecureBuffer`로 반환되어 `instantiate` 완료 후 Drop 시점에 자동 소거됩니다.
+
+## Hash_DRBG 명세
+
+### NIST SP 800-90A Rev. 1, Table 2 파라미터
+
+| 인스턴스 | 해시 | outlen | seedlen | security_strength | 최소 엔트로피 |
+|------------------|---------|--------|---------|-------------------|---------|
+| `HashDRBGSHA224` | SHA-224 | 28 B | 55 B | 112 bits | 14 B |
+| `HashDRBGSHA256` | SHA-256 | 32 B | 55 B | 128 bits | 16 B |
+| `HashDRBGSHA384` | SHA-384 | 48 B | 111 B | 192 bits | 24 B |
+| `HashDRBGSHA512` | SHA-512 | 64 B | 111 B | 256 bits | 32 B |
+
+### Hash_df (Section 10.3.1)
+
+Hash 유도 함수(Hash Derivation Function)는 임의 길이의 입력 연접(concatenation)으로부터 정확히 `no_of_bytes_to_return` 바이트를 유도합니다.
+
+```math
+V = \text{Hash\_df}(\text{entropy\_input} \| \text{nonce} \| \text{personalization\_string} \text{seedlen})
+```
+
+내부적으로 $`m = \lceil \text{seedlen} / \text{outlen} \rceil`$회 반복하며, 각 반복에서 카운터 바이트와 비트 수(big-endian 4 바이트)를 prefix로 해시를 계산합니다.
+
+```math
+\text{Hash\_df}[i] = H(\text{counter}_i \| \text{no\_of\_bits\_to\_return} \| \text{input\_string})
+```
+
+### Instantiate (Section 10.1.1.2)
+
+엔트로피 입력, nonce, personalization string으로 내부 상태 $`V`$와 $`C`$를 초기화합니다.
+
+```math
+V = \text{Hash\_df}(\text{entropy\_input} \| \text{nonce} \| \text{personalization\_string},\ \text{seedlen})
+```
+
+```math
+C = \text{Hash\_df}(\texttt{0x00} \| V,\ \text{seedlen})
+```
+
+`new_from_os`는 entropy_input으로 $`2 \times \text{security\_strength}`$ 바이트, nonce로 $`\text{security\_strength}`$ 바이트를 OS에 대한 **별개의 두 호출**로 수집하여 nonce 독립성을 보장합니다.
+
+### Reseed (Section 10.1.1.3)
+
+새로운 엔트로피로 내부 상태를 갱신합니다.
+
+```math
+V' = \text{Hash\_df}(\texttt{0x01} \| V \| \text{entropy\_input} \| \text{additional\_input},\ \text{seedlen})
+```
+
+```math
+C' = \text{Hash\_df}(\texttt{0x00} \| V',\ \text{seedlen})
+```
+
+중간 스택 버퍼(`new_v`, `new_c`)는 연산 완료 후 `write_volatile`로 강제 소거됩니다.
+
+### Generate (Section 10.1.1.4)
+
+요청당 최대 $2^{19}$ bits(65,536 bytes)의 의사난수를 생성합니다. `additional_input`이 주어지면 먼저 내부 상태를 갱신합니다.
+
+**additional_input 처리**:
+
+```math
+w = H(\texttt{0x02} \| V \| \text{additional\_input})
+```
+```math
+V \leftarrow (V + w_{\text{padded}}) \bmod 2^{\text{seedlen} \times 8}
+```
+
+**출력 생성 (Hashgen)**:
+
+내부 카운터 $`\text{data} = V`$를 복사하여 $`\lceil \text{requested\_bytes} / \text{outlen} \rceil`$회 해시합니다.
+
+```math
+W_i = H(\text{data} + i - 1),\quad \text{data 시작값} = V
+```
+
+**상태 갱신**:
+
+```math
+H = H(\texttt{0x03} \| V)
+```
+```math
+V \leftarrow (V + H_{\text{padded}} + C + \text{reseed\_counter}) \bmod 2^{\text{seedlen} \times 8}
+```
+
+### 모듈러 덧셈 (add_mod / add_u64_mod)
+
+내부 상태 $`V`$는 big-endian 바이트 배열로 표현됩니다. `add_mod`는 낮은 인덱스(상위 바이트)부터 올림수(carry)를 전파하는 순수 산술 연산으로 구현되어 **비밀 데이터의 값에 의존하는 분기가 존재하지 않습니다**.
+
+> [!NOTE]
+> **상수-시간 불변식**: 반복 횟수는 항상 `seedlen`(공개 상수)에 고정됩니다.
+>
+> carry는 `u16` 산술 마스킹으로만 처리되어 조건 분기를 유발하지 않습니다.
+
+## 메모리 보안
+
+내부 상태 $`V`$, $`C`$는 `SecureBuffer`로 할당됩니다. `SecureBuffer`는 OS `mlock`으로 해당 페이지를 스왑 불가 영역에 고정하고, Drop 시 `write_volatile` 기반 소거를 수행합니다. `reseed_counter`는 `Drop` 구현 내에서 `write_volatile`로 별도 소거됩니다.
+
+스택에 복사된 중간값(`new_v`, `new_c`, `c_copy`, `h_padded`, `w_padded`, `data`)은 모두 연산 완료 즉시 `write_volatile` 루프로 소거되어 스택 잔존 데이터 공격을 방지합니다.
+
+## 오류 열거형 (DrbgError)
+
+| 변형 | 발생 조건 |
+|---------------------|-----------------------------------------|
+| `EntropyTooShort` | entropy_input < security_strength bytes |
+| `EntropyTooLong` | 입력 길이 > $`2^{32}`$ bytes |
+| `NonceTooShort` | nonce < security_strength / 2 bytes |
+| `InputTooLong` | additional_input > $`2^{32}`$ bytes |
+| `InvalidArgument` | no_of_bits 산출 오버플로우 |
+| `ReseedRequired` | reseed_counter > $`2^{48}`$ |
+| `AllocationFailed` | SecureBuffer 할당 또는 mlock 실패 |
+| `InternalHashError` | 해시 함수 내부 오류 |
+| `RequestTooLarge` | 요청 크기 > 65,536 bytes |
+| `OsEntropyFailed` | OS 엔트로피 소스 접근 실패 |
+
+## 사용 예시
+
+```rust
+use entlib_native_rng::{HashDRBGSHA256, DrbgError};
+
+fn generate_key() -> Result<[u8; 32], DrbgError> {
+ // OS 엔트로피로 초기화 — 유일하게 허용되는 외부 초기화 경로
+ let mut drbg = HashDRBGSHA256::new_from_os(Some(b"myapp-keygen-v1"))?;
+
+ let mut key = [0u8; 32];
+ drbg.generate(&mut key, None)?;
+ Ok(key)
+}
+```
+
+재시드 수행 예시
+
+```rust
+use entlib_native_rng::{HashDRBGSHA512, DrbgError};
+
+fn generate_with_reseed() -> Result<(), DrbgError> {
+ let mut drbg = HashDRBGSHA512::new_from_os(None)?;
+ let mut buf = [0u8; 64];
+
+ loop {
+ match drbg.generate(&mut buf, None) {
+ Ok(()) => break,
+ Err(DrbgError::ReseedRequired) => {
+ // OS 엔트로피로 재시드 후 재시도
+ let entropy = [0u8; 32]; // 실제 구현에서는 OS 엔트로피 사용
+ drbg.reseed(&entropy, None)?;
+ }
+ Err(e) => return Err(e),
+ }
+ }
+ Ok(())
+}
+```
+
+## 설계 원칙 요약
+
+본 크레이트는 세 가지 수준의 보안 설계를 적용합니다.
+
+1. **표준 준수**: NIST SP 800-90A Rev. 1의 Hash_DRBG 알고리즘을 명세 단계별로 정확히 구현하며, Table 2의 파라미터를 매크로 단위로 강제합니다.
+2. **메모리 격리**: 비밀 내부 상태 $`V`$, $`C`$를 `SecureBuffer`에 격리하고, 스택 복사본은 `write_volatile`로 즉시 소거하여 메모리 잔존 공격 표면을 최소화합니다.
+3. **엔트로피 무결성**: 유일한 초기화 경로를 `new_from_os`로 제한하고, entropy_input과 nonce를 별개 OS 호출로 수집하여 외부 공격자가 시드를 제어할 수 없도록 합니다.
diff --git a/core/rng/README_EN.md b/core/rng/README_EN.md
new file mode 100644
index 0000000..ab8909a
--- /dev/null
+++ b/core/rng/README_EN.md
@@ -0,0 +1,205 @@
+# Hash_DRBG Crate (entlib-native-rng)
+
+> Q. T. Felix (Modified: 26.03.21 UTC+9)
+>
+> [Korean README](README.md)
+
+`entlib-native-rng` is a `no_std` compatible crate that implements the Hash_DRBG (Hash-based Deterministic Random Bit Generator) specified in Section 10.1.1 of NIST SP 800-90A Rev. 1. This crate is designed based on the FIPS 140-3 approved algorithm requirements and has built-in defense against memory dump and cold boot attacks by managing its internal state with `SecureBuffer`.
+
+## Security Threat Model
+
+The security of a DRBG depends on three key properties.
+
+**Unpredictability**: An attacker should not be able to predict the next output even if they have observed all previous outputs. Hash_DRBG ensures this by not directly exposing the internal states $V$ and $C$ in the output, but only deriving the output through a one-way hash function.
+
+**State Recovery Resistance**: Even if the internal states $V$ and $C$ are exposed, it should not be possible to reverse-calculate the previous outputs. Both values are isolated in `SecureBuffer`, to which OS-level memory locking (`mlock`) and forced erasure at the time of Drop are applied.
+
+**Mandatory Reseed**: If the reseed counter exceeds $2^{48}$, `generate` immediately returns `ReseedRequired`. This structurally blocks the generation of an excessive amount of output from the same state.
+
+## Architecture
+
+```
+entlib-native-rng
+├── os_entropy (internal module) — Platform-specific OS entropy extraction
+└── hash_drbg (internal module) — NIST SP 800-90A Hash_DRBG implementation
+ ├── HashDRBGSHA224 (security_strength = 112 bits)
+ ├── HashDRBGSHA256 (security_strength = 128 bits)
+ ├── HashDRBGSHA384 (security_strength = 192 bits)
+ └── HashDRBGSHA512 (security_strength = 256 bits)
+```
+
+The only public initialization path is `new_from_os`. `instantiate`, which allows the user to inject entropy directly, is restricted to `pub(crate)` to fundamentally block the risk of predictable seed injection.
+
+## OS Entropy Source
+
+`extract_os_entropy` collects raw entropy through platform-specific direct syscalls or verified library functions. It does not depend on external crates such as `getrandom`.
+
+| Target | Method |
+|----------------------------|-------------------------------------------------------------|
+| `linux + x86_64` | `SYS_getrandom` (318), direct call to `syscall` instruction |
+| `linux + aarch64` | `SYS_getrandom` (278), direct call to `svc #0` instruction |
+| `macos` (x86_64 / aarch64) | `getentropy(2)` — libSystem FFI |
+
+The Linux implementation always completely fills `size` bytes, including `EINTR` retries and partial read loops. macOS's `getentropy` guarantees a complete fill with a single call and has a maximum limit of 256 bytes, but the maximum size requested by `new_from_os` ($2 \times 32 = 64$ bytes for SHA-512) does not exceed this.
+
+The collected entropy and nonce are returned as `SecureBuffer` and are automatically erased at the time of Drop after `instantiate` is complete.
+
+## Hash_DRBG Specification
+
+### NIST SP 800-90A Rev. 1, Table 2 Parameters
+
+| Instance | Hash | outlen | seedlen | security_strength | Minimum Entropy |
+|------------------|---------|--------|---------|-------------------|-----------------|
+| `HashDRBGSHA224` | SHA-224 | 28 B | 55 B | 112 bits | 14 B |
+| `HashDRBGSHA256` | SHA-256 | 32 B | 55 B | 128 bits | 16 B |
+| `HashDRBGSHA384` | SHA-384 | 48 B | 111 B | 192 bits | 24 B |
+| `HashDRBGSHA512` | SHA-512 | 64 B | 111 B | 256 bits | 32 B |
+
+### Hash_df (Section 10.3.1)
+
+The Hash Derivation Function derives exactly `no_of_bytes_to_return` bytes from a concatenation of inputs of arbitrary length.
+
+```math
+V = \text{Hash\_df}(\text{entropy\_input} \| \text{nonce} \| \text{personalization\_string} \text{seedlen})
+```
+
+Internally, it repeats $m = \lceil \text{seedlen} / \text{outlen} \rceil$ times, and in each iteration, it calculates the hash with a counter byte and the number of bits (big-endian 4 bytes) as a prefix.
+
+```math
+\text{Hash\_df}[i] = H(\text{counter}_i \| \text{no\_of\_bits\_to\_return} \| \text{input\_string})
+```
+
+### Instantiate (Section 10.1.1.2)
+
+Initializes the internal states $V$ and $C$ with the entropy input, nonce, and personalization string.
+
+```math
+V = \text{Hash\_df}(\text{entropy\_input} \| \text{nonce} \| \text{personalization\_string},\ \text{seedlen})
+```
+
+```math
+C = \text{Hash\_df}(\texttt{0x00} \| V,\ \text{seedlen})
+```
+
+`new_from_os` collects $2 \times \text{security\_strength}$ bytes for entropy_input and $\text{security\_strength}$ bytes for nonce with **two separate calls** to the OS to ensure nonce independence.
+
+### Reseed (Section 10.1.1.3)
+
+Updates the internal state with new entropy.
+
+```math
+V' = \text{Hash\_df}(\texttt{0x01} \| V \| \text{entropy\_input} \| \text{additional\_input},\ \text{seedlen})
+```
+
+```math
+C' = \text{Hash\_df}(\texttt{0x00} \| V',\ \text{seedlen})
+```
+
+The intermediate stack buffers (`new_v`, `new_c`) are forcibly erased with `write_volatile` after the operation is complete.
+
+### Generate (Section 10.1.1.4)
+
+Generates up to $2^{19}$ bits (65,536 bytes) of pseudorandom numbers per request. If `additional_input` is given, the internal state is updated first.
+
+**additional_input processing**:
+
+```math
+w = H(\texttt{0x02} \| V \| \text{additional\_input})
+```
+```math
+V \leftarrow (V + w_{\text{padded}}) \bmod 2^{\text{seedlen} \times 8}
+```
+
+**Output generation (Hashgen)**:
+
+Copies the internal counter $\text{data} = V$ and hashes it $\lceil \text{requested\_bytes} / \text{outlen} \rceil$ times.
+
+```math
+W_i = H(\text{data} + i - 1),\quad \text{starting value of data} = V
+```
+
+**State update**:
+
+```math
+H = H(\texttt{0x03} \| V)
+```
+```math
+V \leftarrow (V + H_{\text{padded}} + C + \text{reseed\_counter}) \bmod 2^{\text{seedlen} \times 8}
+```
+
+### Modular Addition (add_mod / add_u64_mod)
+
+The internal state $V$ is represented as a big-endian byte array. `add_mod` is implemented as a pure arithmetic operation that propagates the carry from the low index (high byte), so **there are no branches that depend on the value of the secret data**.
+
+> [!NOTE]
+> **Constant-Time Invariant**: The number of iterations is always fixed to `seedlen` (a public constant).
+>
+> The carry is only handled by `u16` arithmetic masking and does not cause conditional branches.
+
+## Memory Security
+
+The internal states $V$ and $C$ are allocated as `SecureBuffer`. `SecureBuffer` pins the corresponding page to a non-swappable area with OS `mlock` and performs `write_volatile`-based erasure at the time of Drop. `reseed_counter` is separately erased with `write_volatile` within the `Drop` implementation.
+
+Intermediate values copied to the stack (`new_v`, `new_c`, `c_copy`, `h_padded`, `w_padded`, `data`) are all erased with a `write_volatile` loop immediately after the operation is complete to prevent stack residue data attacks.
+
+## Error Enum (DrbgError)
+
+| Variant | Condition of Occurrence |
+|---------------------|------------------------------------------|
+| `EntropyTooShort` | entropy_input < security_strength bytes |
+| `EntropyTooLong` | input length > $2^{32}$ bytes |
+| `NonceTooShort` | nonce < security_strength / 2 bytes |
+| `InputTooLong` | additional_input > $2^{32}$ bytes |
+| `InvalidArgument` | no_of_bits calculation overflow |
+| `ReseedRequired` | reseed_counter > $2^{48}$ |
+| `AllocationFailed` | SecureBuffer allocation or mlock failure |
+| `InternalHashError` | Internal error in the hash function |
+| `RequestTooLarge` | Request size > 65,536 bytes |
+| `OsEntropyFailed` | Failed to access OS entropy source |
+
+## Usage Example
+
+```rust
+use entlib_native_rng::{HashDRBGSHA256, DrbgError};
+
+fn generate_key() -> Result<[u8; 32], DrbgError> {
+ // Initialize with OS entropy — the only allowed external initialization path
+ let mut drbg = HashDRBGSHA256::new_from_os(Some(b"myapp-keygen-v1"))?;
+
+ let mut key = [0u8; 32];
+ drbg.generate(&mut key, None)?;
+ Ok(key)
+}
+```
+
+Reseed example
+
+```rust
+use entlib_native_rng::{HashDRBGSHA512, DrbgError};
+
+fn generate_with_reseed() -> Result<(), DrbgError> {
+ let mut drbg = HashDRBGSHA512::new_from_os(None)?;
+ let mut buf = [0u8; 64];
+
+ loop {
+ match drbg.generate(&mut buf, None) {
+ Ok(()) => break,
+ Err(DrbgError::ReseedRequired) => {
+ // Reseed with OS entropy and retry
+ let entropy = [0u8; 32]; // In a real implementation, use OS entropy
+ drbg.reseed(&entropy, None)?;
+ }
+ Err(e) => return Err(e),
+ }
+ }
+ Ok(())
+}
+```
+
+## Summary of Design Principles
+
+This crate applies a three-level security design.
+
+1. **Standard Compliance**: It accurately implements the Hash_DRBG algorithm of NIST SP 800-90A Rev. 1 at each specification step and enforces the parameters of Table 2 at the macro level.
+2. **Memory Isolation**: It isolates the secret internal states $V$ and $C$ in `SecureBuffer` and immediately erases stack copies with `write_volatile` to minimize the memory residue attack surface.
+3. **Entropy Integrity**: It restricts the only initialization path to `new_from_os` and collects entropy_input and nonce with separate OS calls to prevent external attackers from controlling the seed.
diff --git a/core/rng/src/hash_drbg.rs b/core/rng/src/hash_drbg.rs
new file mode 100644
index 0000000..f918ca0
--- /dev/null
+++ b/core/rng/src/hash_drbg.rs
@@ -0,0 +1,1007 @@
+//! NIST SP 800-90A Rev. 1에 따른 Hash_DRBG 구현 모듈입니다.
+//!
+//! 이 모듈은 NIST SP 800-90A Rev. 1 표준의 10.1.1 섹션에 명시된 해시 기반 결정론적 난수 비트 생성기(Hash_DRBG)를 구현합니다.
+//!
+//! # Features
+//! - **NIST 표준 준수**: `Instantiate`, `Reseed`, `Generate` 알고리즘을 표준 명세에 따라 구현합니다.
+//! - **다양한 해시 함수 지원**: `SHA-224`, `SHA-256`, `SHA-384`, `SHA-512`를 기반으로 하는 DRBG 인스턴스를 제공합니다.
+//! - [`HashDRBGSHA224`] (Security Strength: 112 bits)
+//! - [`HashDRBGSHA256`] (Security Strength: 128 bits)
+//! - [`HashDRBGSHA384`] (Security Strength: 192 bits)
+//! - [`HashDRBGSHA512`] (Security Strength: 256 bits)
+//! - **메모리 보안**: 내부 상태 `V`와 `C`를 [`SecureBuffer`]를 사용하여 관리합니다. 이를 통해 OS 레벨의 메모리 잠금(`mlock`)과 Drop 시점의 자동 소거를 보장하여, 메모리 덤프나 콜드 부트 공격으로부터 내부 상태를 보호합니다.
+//! - **Reseed 강제**: 표준에 따라 최대 reseed 간격(`RESEED_INTERVAL`)을 초과하면 [`generate`] 함수가 [`ReseedRequired`] 에러를 반환하여 주기적인 엔트로피 갱신을 강제합니다.
+//! - **유연한 입력 처리**: `instantiate`, `reseed`, `generate` 함수에서 `additional_input`과 `personalization_string`을 지원합니다.
+//!
+//! # Examples
+//! ```rust
+//! use entlib_native_rng::{HashDRBGSHA256, DrbgError};
+//!
+//! fn main() -> Result<(), DrbgError> {
+//! // 1. 초기화 — OS 엔트로피 소스 사용 (임의 엔트로피 주입 불가)
+//! let personalization = Some(b"my-app-specific-string" as &[u8]);
+//! let mut drbg = HashDRBGSHA256::new_from_os(personalization)?;
+//!
+//! // 2. 난수 생성 (Generate)
+//! let mut random_bytes = [0u8; 128];
+//! drbg.generate(&mut random_bytes, None)?;
+//!
+//! // 3. reseed — ReseedRequired 수신 시 호출
+//! let new_entropy = &[1u8; 16]; // 실제로는 OS 엔트로피 소스에서 획득
+//! drbg.reseed(new_entropy, None)?;
+//!
+//! // 4. 추가 난수 생성
+//! let mut more_random_bytes = [0u8; 64];
+//! drbg.generate(&mut more_random_bytes, None)?;
+//!
+//! Ok(())
+//! }
+//! ```
+//!
+//! # Security Note
+//! - `impl_hash_drbg!` 매크로를 사용하여 각 해시 함수에 대한 DRBG 구조체와 구현을 생성합니다. 이는 코드 중복을 최소화하고 일관성을 유지합니다.
+//! - 내부 상태 덧셈 연산(`add_mod`, `add_u64_mod`)은 Big-endian 모듈러 덧셈으로 구현되어 표준을 정확히 따릅니다.
+//! - 중간 계산값이나 스택에 복사된 민감한 데이터는 `write_volatile`을 사용하여 명시적으로 소거합니다.
+//!
+//! # Authors
+//! Q. T. Felix
+
+use crate::DrbgError;
+use core::cmp::min;
+use core::ptr::write_volatile;
+use entlib_native_secure_buffer::SecureBuffer;
+use entlib_native_sha2::api::{SHA224, SHA256, SHA384, SHA512};
+
+/// 최대 reseed 간격
+const RESEED_INTERVAL: u64 = 1 << 48;
+
+/// 요청당 최대 출력 바이트 (2^19 bits = 65536 bytes)
+const MAX_BYTES_PER_REQUEST: usize = 65536;
+
+/// NIST SP 800-90A Rev. 1, Table 2: entropy_input / nonce / personalization_string 최대 길이
+/// 2^35 bits = 2^32 bytes. usize가 32-bit인 환경에서도 안전하게 비교하기 위해 u64 사용.
+const MAX_LENGTH: u64 = 1u64 << 32;
+
+/// NIST SP 800-90A Rev. 1, Table 2: additional_input 최대 길이 (2^35 bits = 2^32 bytes)
+const MAX_ADDITIONAL_INPUT: u64 = 1u64 << 32;
+
+/// Hash_DRBG 함수 일관 구현을 위한 매크로입니다.
+///
+/// NIST SP 800-90A Rev. 1에 따른 Hash_DRBG 변형을 생성합니다.
+///
+/// # Arguments
+/// - `$struct_name` : 생성할 구조체 이름
+/// - `$hasher_type` : 사용할 해시 함수 타입 (예: SHA256)
+/// - `$outlen` : 해시 출력 크기 (bytes, NIST Table 2 outlen)
+/// - `$seedlen` : 시드 길이 (bytes, NIST Table 2 seedlen)
+/// - `$min_entropy` : 최소 엔트로피/보안 강도 (bytes, security_strength / 8)
+macro_rules! impl_hash_drbg {
+ (
+ $struct_name:ident,
+ $hasher_type:ty,
+ $outlen:expr,
+ $seedlen:expr,
+ $min_entropy:expr
+ ) => {
+ /// Hash_DRBG 인스턴스입니다.
+ ///
+ /// 내부 상태 V, C는 [`SecureBuffer`]로 관리되어 OS 레벨 메모리 잠금(lock)과
+ /// [Drop] 시점의 강제 소거([`Zeroize`])가 보장됩니다.
+ pub struct $struct_name {
+ /// 내부 상태 V — seedlen bytes
+ v: SecureBuffer,
+ /// 내부 상태 C — seedlen bytes
+ c: SecureBuffer,
+ /// reseed 카운터 (1부터 시작, RESEED_INTERVAL 초과 시 ReseedRequired 반환)
+ reseed_counter: u64,
+ }
+
+ impl $struct_name {
+ /// NIST SP 800-90A Rev. 1의 Section 10.3.1의 Hash_df
+ ///
+ /// inputs 슬라이스 배열을 순서대로 연결(concatenate)한 것으로 간주하여
+ /// `no_of_bytes_to_return` 길이의 바이트를 유도합니다.
+ ///
+ /// `output.len() == no_of_bytes_to_return` 이어야 합니다.
+ fn hash_df(
+ inputs: &[&[u8]],
+ no_of_bytes_to_return: usize,
+ output: &mut [u8],
+ ) -> Result<(), DrbgError> {
+ // Hash_df 명세: no_of_bits_to_return을 4바이트 big-endian 정수로 인코딩
+ // seedlen_bits(max=888) < 2^32 이므로 u32으로 충분
+ let no_of_bits = (no_of_bytes_to_return as u32)
+ .checked_mul(8)
+ .ok_or(DrbgError::InvalidArgument)?;
+ let no_of_bits_be = no_of_bits.to_be_bytes();
+
+ let m = no_of_bytes_to_return.div_ceil($outlen);
+ let mut written = 0usize;
+
+ // counter in [1, m], m ≤ ceil(seedlen / outlen) ≤ 4 — u8 오버플로 없음
+ for counter in 1u8..=(m as u8) {
+ let mut hasher = <$hasher_type>::new();
+ hasher.update(&[counter]);
+ hasher.update(&no_of_bits_be);
+ for chunk in inputs {
+ hasher.update(chunk);
+ }
+ let hash = hasher
+ .finalize()
+ .map_err(|_| DrbgError::InternalHashError)?;
+ let hash_bytes = hash.as_slice();
+
+ let copy_len = min($outlen, no_of_bytes_to_return - written);
+ output[written..written + copy_len].copy_from_slice(&hash_bytes[..copy_len]);
+ written += copy_len;
+ }
+
+ Ok(())
+ }
+
+ /// Big-endian 모듈식 덧셈: `dst = (dst + src) mod 2^(dst.len() * 8)`
+ ///
+ /// dst와 src는 같은 길이여야 합니다.
+ ///
+ /// # 상수-시간(Constant-Time) 불변식
+ ///
+ /// 이 함수는 `dst`와 `src`의 **값**에 대해 데이터 의존적 분기(branch)가 없습니다.
+ /// - 반복 횟수: 항상 `dst.len()` (고정, 비밀 데이터에 무관)
+ /// - 조건 분기: 없음 — carry는 산술 연산(`u16` 오버플로 마스킹)으로만 처리됨
+ /// - 캐시 접근 패턴: 인덱스가 단순 증가(선형) — 캐시-타이밍 공격 면역
+ ///
+ /// **주의**: 컴파일러가 루프를 언롤하거나 SIMD로 변환해도 CT 보장은 유지됩니다.
+ /// 단, 이 함수의 결과를 외부에서 비교(`==`)할 때는 반드시 상수-시간 비교를 사용하세요.
+ #[inline]
+ fn add_mod(dst: &mut [u8], src: &[u8]) {
+ let mut carry: u16 = 0;
+ // big-endian: 낮은 인덱스 = 상위 바이트 -> 오른쪽(낮은 유효 바이트)부터 덧셈
+ for (d, s) in dst.iter_mut().rev().zip(src.iter().rev()) {
+ let sum = *d as u16 + *s as u16 + carry;
+ *d = sum as u8;
+ carry = sum >> 8;
+ }
+ // 최종 carry는 mod 2^(seedlen_bits)에 의해 버림
+ }
+
+ /// Big-endian 모듈식 u64 덧셈: `dst = (dst + val) mod 2^(dst.len() * 8)`
+ ///
+ /// `val`을 big-endian 8바이트로 해석하여 `dst`의 최하위 바이트부터 더합니다.
+ ///
+ /// # 상수-시간(Constant-Time) 불변식
+ ///
+ /// - 반복 횟수: 항상 `dst.len()` (고정)
+ /// - 조건 분기: `if i < 8`은 *인덱스*(공개 상수)에 의존하며, `dst`나 `val`의
+ /// **값**에 의존하지 않습니다.
+ /// - `val`(= reseed_counter)은 비밀 데이터가 아닌 단조 증가 카운터이므로
+ /// 이 경로의 타이밍 관찰은 보안 위협이 되지 않습니다.
+ /// - `dst`(= 내부 상태 V)의 값은 분기 조건에 관여하지 않습니다.
+ #[inline]
+ fn add_u64_mod(dst: &mut [u8], val: u64) {
+ let val_be = val.to_be_bytes(); // [u8; 8]
+ let mut carry: u16 = 0;
+ let dst_len = dst.len();
+
+ for i in 0..dst_len {
+ let dst_idx = dst_len - 1 - i;
+ // val_be의 최하위 바이트는 val_be[7], i=0에서 사용
+ let val_byte = if i < 8 { val_be[7 - i] } else { 0u8 };
+ let sum = dst[dst_idx] as u16 + val_byte as u16 + carry;
+ dst[dst_idx] = sum as u8;
+ carry = sum >> 8;
+ }
+ }
+
+ /// NIST SP 800-90A Rev. 1, Section 10.1.1.4: Hashgen
+ ///
+ /// 내부 상태 V를 기반으로 `requested_bytes` 길이의 출력 바이트를 생성합니다.
+ ///
+ /// # 상수-시간(Constant-Time) 불변식
+ ///
+ /// - 루프 횟수: `ceil(requested_bytes / outlen)` — `requested_bytes`(공개)에 의존,
+ /// 비밀 상태 V의 **값**에 무관
+ /// - 내부 상태 `V`의 복사본 `data`는 값에 무관한 순차 증가(`add_u64_mod`)만 수행
+ /// - 해시 입력 크기 고정 -> 해시 연산 자체의 타이밍은 V 값에 무관
+ /// - 스택 복사본 `data`는 함수 종료 시 `write_volatile`로 소거 (메모리 잔존 방지)
+ ///
+ /// **CT 위협 모델**: Hashgen의 출력은 공개(반환값)이므로 출력 자체의 CT 보호는
+ /// 불필요합니다. 보호 대상은 내부 상태 V이며, V는 외부에 직접 노출되지 않습니다.
+ fn hashgen(&self, requested_bytes: usize, output: &mut [u8]) -> Result<(), DrbgError> {
+ // data = V (스택 복사 — Drop 후 write_volatile로 소거)
+ let mut data = [0u8; $seedlen];
+ data.copy_from_slice(self.v.as_slice());
+
+ let m = requested_bytes.div_ceil($outlen);
+ let mut written = 0usize;
+
+ for _ in 0..m {
+ let mut hasher = <$hasher_type>::new();
+ hasher.update(&data);
+ let hash = hasher
+ .finalize()
+ .map_err(|_| DrbgError::InternalHashError)?;
+ let hash_bytes = hash.as_slice();
+
+ let copy_len = min($outlen, requested_bytes - written);
+ output[written..written + copy_len].copy_from_slice(&hash_bytes[..copy_len]);
+ written += copy_len;
+
+ // data = (data + 1) mod 2^seedlen (NIST 명세)
+ Self::add_u64_mod(&mut data, 1);
+ }
+
+ // data(= V 파생본) 강제 소거
+ for byte in &mut data {
+ unsafe {
+ write_volatile(byte, 0);
+ }
+ }
+
+ Ok(())
+ }
+
+ //
+ // 공개 API
+ //
+
+ /// OS 엔트로피 소스로부터 Hash_DRBG를 안전하게 초기화합니다.
+ ///
+ /// 이것이 **권장되는 유일한 초기화 경로**입니다. 내부 `instantiate`와 달리
+ /// 사용자가 엔트로피를 직접 주입할 수 없어, 예측 가능한 시드 사용 위험을 차단합니다.
+ ///
+ /// # 엔트로피 수집 전략 (NIST SP 800-90A Rev.1 Section 8.6.7)
+ ///
+ /// | 입력 | 수집 크기 | 최솟값 대비 |
+ /// |------------------|---------------------------------|--------------|
+ /// | `entropy_input` | `2 × security_strength` bytes | 2배 여유 |
+ /// | `nonce` | `security_strength` bytes | 2배 여유 |
+ ///
+ /// 두 값은 OS에 대한 **별개의 호출**로 수집되어 nonce의 독립성을 보장합니다.
+ ///
+ /// # 엔트로피 소스
+ /// - Linux x86_64: `getrandom(2)` 직접 syscall (GRND_RANDOM 플래그 없음)
+ /// - macOS aarch64: `getentropy(2)` 직접 syscall
+ ///
+ /// # 메모리 보안
+ /// 수집된 엔트로피·nonce는 [`SecureBuffer`]로 관리되어 Drop 시 자동 소거됩니다.
+ ///
+ /// # Errors
+ /// - `DrbgError::OsEntropyFailed`: OS 엔트로피 소스 접근 실패
+ pub fn new_from_os(personalization_string: Option<&[u8]>) -> Result {
+ // entropy_input: 2 × security_strength 바이트 (별개 호출로 독립성 보장)
+ let entropy = crate::os_entropy::extract_os_entropy($min_entropy * 2)
+ .map_err(|_| DrbgError::OsEntropyFailed)?;
+
+ // nonce: security_strength 바이트 (entropy_input과 별개 호출)
+ let nonce = crate::os_entropy::extract_os_entropy($min_entropy)
+ .map_err(|_| DrbgError::OsEntropyFailed)?;
+
+ // SecureBuffer는 Drop 시 자동 소거 — 별도 write_volatile 루프 불필요
+ Self::instantiate(entropy.as_slice(), nonce.as_slice(), personalization_string)
+ }
+
+ /// NIST SP 800-90A Rev. 1, Section 10.1.1.2: Hash_DRBG_Instantiate_algorithm
+ ///
+ /// 사용자가 엔트로피를 직접 주입하는 내부 초기화 함수입니다.
+ ///
+ /// # 보안 요구사항
+ /// - `entropy_input`: `security_strength` ~ 125 bytes (충분한 무작위성 필수)
+ /// - `nonce`: `security_strength / 2` bytes 이상 (재사용 금지)
+ /// - `personalization_string`: 선택적 (최대 125 bytes 권장)
+ ///
+ /// # 주의 (보안)
+ /// 이 함수는 **크레이트 내부 전용**입니다. 외부에서 임의 엔트로피를 주입하면
+ /// DRBG 출력의 무작위성이 공격자에 의해 제어될 수 있습니다.
+ /// 외부 코드는 반드시 [`new_from_os`]를 통해 OS 엔트로피로 초기화하세요.
+ pub(crate) fn instantiate(
+ entropy_input: &[u8],
+ nonce: &[u8],
+ personalization_string: Option<&[u8]>,
+ ) -> Result {
+ // NIST SP 800-90A Rev. 1, Section 8.6.7 검증
+ if entropy_input.len() < $min_entropy {
+ return Err(DrbgError::EntropyTooShort);
+ }
+ if (entropy_input.len() as u64) > MAX_LENGTH {
+ return Err(DrbgError::EntropyTooLong);
+ }
+ // nonce 최소 길이: security_strength / 2
+ if nonce.len() < ($min_entropy / 2) {
+ return Err(DrbgError::NonceTooShort);
+ }
+ if (nonce.len() as u64) > MAX_LENGTH {
+ return Err(DrbgError::EntropyTooLong);
+ }
+
+ let ps = personalization_string.unwrap_or(&[]);
+ if (ps.len() as u64) > MAX_ADDITIONAL_INPUT {
+ return Err(DrbgError::InputTooLong);
+ }
+
+ // V = Hash_df(entropy_input || nonce || personalization_string, seedlen)
+ let mut v_buf =
+ SecureBuffer::new_owned($seedlen).map_err(|_| DrbgError::AllocationFailed)?;
+ Self::hash_df(&[entropy_input, nonce, ps], $seedlen, v_buf.as_mut_slice())?;
+
+ // C = Hash_df(0x00 || V, seedlen)
+ let mut c_buf =
+ SecureBuffer::new_owned($seedlen).map_err(|_| DrbgError::AllocationFailed)?;
+ Self::hash_df(
+ &[&[0x00u8], v_buf.as_slice()],
+ $seedlen,
+ c_buf.as_mut_slice(),
+ )?;
+
+ Ok(Self {
+ v: v_buf,
+ c: c_buf,
+ reseed_counter: 1,
+ })
+ }
+
+ /// NIST SP 800-90A Rev. 1, Section 10.1.1.3: Hash_DRBG_Reseed_algorithm
+ ///
+ /// 새로운 엔트로피로 내부 상태를 갱신합니다.
+ /// `ReseedRequired` 에러 수신 후 반드시 호출해야 합니다.
+ pub fn reseed(
+ &mut self,
+ entropy_input: &[u8],
+ additional_input: Option<&[u8]>,
+ ) -> Result<(), DrbgError> {
+ if entropy_input.len() < $min_entropy {
+ return Err(DrbgError::EntropyTooShort);
+ }
+ if (entropy_input.len() as u64) > MAX_LENGTH {
+ return Err(DrbgError::EntropyTooLong);
+ }
+
+ let ai = additional_input.unwrap_or(&[]);
+ if (ai.len() as u64) > MAX_ADDITIONAL_INPUT {
+ return Err(DrbgError::InputTooLong);
+ }
+
+ // new_V = Hash_df(0x01 || V || entropy_input || additional_input, seedlen)
+ // 스택 버퍼에 먼저 계산 후 SecureBuffer에 복사
+ let mut new_v = [0u8; $seedlen];
+ Self::hash_df(
+ &[&[0x01u8], self.v.as_slice(), entropy_input, ai],
+ $seedlen,
+ &mut new_v,
+ )?;
+ self.v.as_mut_slice().copy_from_slice(&new_v);
+
+ // new_v 스택 버퍼 강제 소거
+ for byte in &mut new_v {
+ unsafe {
+ write_volatile(byte, 0);
+ }
+ }
+
+ // new_C = Hash_df(0x00 || new_V, seedlen)
+ // self.c를 직접 출력 버퍼로 사용 (self.v 불변 대여 -> 가변 대여 순서 주의)
+ let mut new_c = [0u8; $seedlen];
+ Self::hash_df(&[&[0x00u8], self.v.as_slice()], $seedlen, &mut new_c)?;
+ self.c.as_mut_slice().copy_from_slice(&new_c);
+
+ for byte in &mut new_c {
+ unsafe {
+ write_volatile(byte, 0);
+ }
+ }
+
+ self.reseed_counter = 1;
+ Ok(())
+ }
+
+ /// NIST SP 800-90A Rev. 1, Section 10.1.1.4: Hash_DRBG_Generate_algorithm
+ ///
+ /// `output.len()` 바이트의 의사난수를 생성합니다.
+ ///
+ /// # 에러
+ /// - `ReseedRequired`: reseed 간격(2^48) 초과 — `reseed()` 후 재호출
+ /// - `RequestTooLarge`: 요청 크기가 65536 bytes 초과
+ pub fn generate(
+ &mut self,
+ output: &mut [u8],
+ additional_input: Option<&[u8]>,
+ ) -> Result<(), DrbgError> {
+ if output.len() > MAX_BYTES_PER_REQUEST {
+ return Err(DrbgError::RequestTooLarge);
+ }
+ // reseed 간격 강제 검사
+ if self.reseed_counter > RESEED_INTERVAL {
+ return Err(DrbgError::ReseedRequired);
+ }
+
+ // additional_input 처리
+ if let Some(ai) = additional_input {
+ if (ai.len() as u64) > MAX_ADDITIONAL_INPUT {
+ return Err(DrbgError::InputTooLong);
+ }
+ if !ai.is_empty() {
+ // w = Hash(0x02 || V || additional_input)
+ let mut hasher = <$hasher_type>::new();
+ hasher.update(&[0x02u8]);
+ hasher.update(self.v.as_slice());
+ hasher.update(ai);
+ let w = hasher
+ .finalize()
+ .map_err(|_| DrbgError::InternalHashError)?;
+
+ // w(outlen bytes)를 seedlen bytes로 오른쪽 정렬 (big-endian MSB=0 패딩)
+ // V = (V + w) mod 2^seedlen
+ let mut w_padded = [0u8; $seedlen];
+ w_padded[$seedlen - $outlen..].copy_from_slice(w.as_slice());
+ Self::add_mod(self.v.as_mut_slice(), &w_padded);
+
+ for byte in &mut w_padded {
+ unsafe {
+ write_volatile(byte, 0);
+ }
+ }
+ }
+ }
+
+ // returned_bits = Hashgen(requested_bytes, V)
+ self.hashgen(output.len(), output)?;
+
+ // H = Hash(0x03 || V)
+ let mut hasher = <$hasher_type>::new();
+ hasher.update(&[0x03u8]);
+ hasher.update(self.v.as_slice());
+ let h = hasher
+ .finalize()
+ .map_err(|_| DrbgError::InternalHashError)?;
+
+ // V = (V + H + C + reseed_counter) mod 2^seedlen
+ // H(outlen bytes)를 seedlen bytes로 오른쪽 정렬 후 덧셈
+ let mut h_padded = [0u8; $seedlen];
+ h_padded[$seedlen - $outlen..].copy_from_slice(h.as_slice());
+ Self::add_mod(self.v.as_mut_slice(), &h_padded);
+
+ for byte in &mut h_padded {
+ unsafe {
+ write_volatile(byte, 0);
+ }
+ }
+
+ // C를 스택에 복사 후 V에 덧셈 (self.v와 self.c 동시 대여 회피)
+ let mut c_copy = [0u8; $seedlen];
+ c_copy.copy_from_slice(self.c.as_slice());
+ Self::add_mod(self.v.as_mut_slice(), &c_copy);
+
+ for byte in &mut c_copy {
+ unsafe {
+ write_volatile(byte, 0);
+ }
+ }
+
+ // reseed_counter를 V에 덧셈
+ Self::add_u64_mod(self.v.as_mut_slice(), self.reseed_counter);
+ self.reseed_counter += 1;
+
+ Ok(())
+ }
+ }
+
+ /// 메모리 잔존 공격 방지: reseed_counter 강제 소거
+ ///
+ /// SecureBuffer(V, C)는 자체 Drop에서 자동 소거됩니다.
+ impl Drop for $struct_name {
+ fn drop(&mut self) {
+ unsafe {
+ write_volatile(&mut self.reseed_counter, 0u64);
+ }
+ }
+ }
+ };
+}
+
+// NIST SP 800-90A Rev. 1, Table 2 파라미터
+// 구조체, 해셔, 출력길이, 시드길이, 최소엔트로피
+impl_hash_drbg!(HashDRBGSHA224, SHA224, 28, 55, 14); // security_strength=112 bits
+impl_hash_drbg!(HashDRBGSHA256, SHA256, 32, 55, 16); // security_strength=128 bits
+impl_hash_drbg!(HashDRBGSHA384, SHA384, 48, 111, 24); // security_strength=192 bits
+impl_hash_drbg!(HashDRBGSHA512, SHA512, 64, 111, 32); // security_strength=256 bits !Recommended!
+
+//
+// 단위 테스트 (pub(crate) instantiate 접근을 위해 src/ 내부에 위치)
+//
+
+#[cfg(test)]
+mod tests {
+ use super::{HashDRBGSHA224, HashDRBGSHA256, HashDRBGSHA384, HashDRBGSHA512};
+ use crate::DrbgError;
+
+ //
+ // 헬퍼
+ //
+
+ fn hex(s: &str) -> alloc::vec::Vec {
+ (0..s.len())
+ .step_by(2)
+ .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
+ .collect()
+ }
+
+ //
+ // 1. 입력 검증: instantiate
+ //
+
+ #[test]
+ fn sha224_instantiate_rejects_entropy_too_short() {
+ assert!(matches!(
+ HashDRBGSHA224::instantiate(&[0xabu8; 13], &[0xcdu8; 7], None),
+ Err(DrbgError::EntropyTooShort)
+ ));
+ }
+
+ #[test]
+ fn sha256_instantiate_rejects_entropy_too_short() {
+ assert!(matches!(
+ HashDRBGSHA256::instantiate(&[0xabu8; 15], &[0xcdu8; 8], None),
+ Err(DrbgError::EntropyTooShort)
+ ));
+ }
+
+ #[test]
+ fn sha384_instantiate_rejects_entropy_too_short() {
+ assert!(matches!(
+ HashDRBGSHA384::instantiate(&[0xabu8; 23], &[0xcdu8; 12], None),
+ Err(DrbgError::EntropyTooShort)
+ ));
+ }
+
+ #[test]
+ fn sha512_instantiate_rejects_entropy_too_short() {
+ assert!(matches!(
+ HashDRBGSHA512::instantiate(&[0xabu8; 31], &[0xcdu8; 16], None),
+ Err(DrbgError::EntropyTooShort)
+ ));
+ }
+
+ #[test]
+ fn sha256_instantiate_rejects_nonce_too_short() {
+ assert!(matches!(
+ HashDRBGSHA256::instantiate(&[0xabu8; 32], &[0xcdu8; 7], None),
+ Err(DrbgError::NonceTooShort)
+ ));
+ }
+
+ #[test]
+ fn sha512_instantiate_rejects_nonce_too_short() {
+ assert!(matches!(
+ HashDRBGSHA512::instantiate(&[0xabu8; 32], &[0xcdu8; 15], None),
+ Err(DrbgError::NonceTooShort)
+ ));
+ }
+
+ #[test]
+ fn sha256_instantiate_accepts_minimum_inputs() {
+ assert!(HashDRBGSHA256::instantiate(&[0xabu8; 16], &[0xcdu8; 8], None).is_ok());
+ }
+
+ #[test]
+ fn sha512_instantiate_accepts_minimum_inputs() {
+ assert!(HashDRBGSHA512::instantiate(&[0xabu8; 32], &[0xcdu8; 16], None).is_ok());
+ }
+
+ //
+ // 2. 입력 검증: reseed
+ //
+
+ #[test]
+ fn sha256_reseed_rejects_entropy_too_short() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0xabu8; 32], &[0xcdu8; 16], None).unwrap();
+ assert!(matches!(
+ drbg.reseed(&[0xefu8; 15], None),
+ Err(DrbgError::EntropyTooShort)
+ ));
+ }
+
+ #[test]
+ fn sha256_reseed_accepts_minimum_entropy() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0xabu8; 32], &[0xcdu8; 16], None).unwrap();
+ assert!(drbg.reseed(&[0xefu8; 16], None).is_ok());
+ }
+
+ //
+ // 3. 입력 검증: generate
+ //
+
+ #[test]
+ fn sha256_generate_rejects_request_too_large() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0xabu8; 32], &[0xcdu8; 16], None).unwrap();
+ let mut buf = alloc::vec![0u8; 65537];
+ assert!(matches!(
+ drbg.generate(&mut buf, None),
+ Err(DrbgError::RequestTooLarge)
+ ));
+ }
+
+ #[test]
+ fn sha256_generate_accepts_maximum_request_size() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0xabu8; 32], &[0xcdu8; 16], None).unwrap();
+ let mut buf = alloc::vec![0u8; 65536];
+ assert!(drbg.generate(&mut buf, None).is_ok());
+ }
+
+ #[test]
+ fn sha256_generate_empty_output_is_valid() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0xabu8; 32], &[0xcdu8; 16], None).unwrap();
+ let mut buf = [];
+ assert!(drbg.generate(&mut buf, None).is_ok());
+ }
+
+ //
+ // 4. 결정론성
+ //
+
+ #[test]
+ fn sha256_is_deterministic() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_eq!(out1, out2);
+ }
+
+ #[test]
+ fn sha512_is_deterministic() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d1 = HashDRBGSHA512::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA512::instantiate(&entropy, &nonce, None).unwrap();
+ let mut out1 = [0u8; 128];
+ let mut out2 = [0u8; 128];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_eq!(out1, out2);
+ }
+
+ #[test]
+ fn sha256_two_generates_are_deterministic() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut buf1a = [0u8; 32];
+ let mut buf1b = [0u8; 32];
+ d1.generate(&mut buf1a, None).unwrap();
+ d1.generate(&mut buf1b, None).unwrap();
+ let mut buf2a = [0u8; 32];
+ let mut buf2b = [0u8; 32];
+ d2.generate(&mut buf2a, None).unwrap();
+ d2.generate(&mut buf2b, None).unwrap();
+ assert_eq!(buf1a, buf2a);
+ assert_eq!(buf1b, buf2b);
+ }
+
+ //
+ // 5. 독립성
+ //
+
+ #[test]
+ fn sha256_different_entropy_produces_different_output() {
+ let nonce = [0x7bu8; 16];
+ let mut d1 = HashDRBGSHA256::instantiate(&[0x01u8; 32], &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&[0x02u8; 32], &nonce, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_ne!(out1, out2);
+ }
+
+ #[test]
+ fn sha256_different_nonce_produces_different_output() {
+ let entropy = [0x5au8; 32];
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &[0x01u8; 16], None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &[0x02u8; 16], None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_ne!(out1, out2);
+ }
+
+ #[test]
+ fn sha256_personalization_string_changes_output() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d_no_ps = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d_with_ps =
+ HashDRBGSHA256::instantiate(&entropy, &nonce, Some(b"my-app-context")).unwrap();
+ let mut out_no = [0u8; 64];
+ let mut out_with = [0u8; 64];
+ d_no_ps.generate(&mut out_no, None).unwrap();
+ d_with_ps.generate(&mut out_with, None).unwrap();
+ assert_ne!(out_no, out_with);
+ }
+
+ #[test]
+ fn sha256_additional_input_changes_output() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, Some(b"additional-context")).unwrap();
+ assert_ne!(out1, out2);
+ }
+
+ #[test]
+ fn sha256_different_additional_inputs_produce_different_output() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, Some(b"context-A")).unwrap();
+ d2.generate(&mut out2, Some(b"context-B")).unwrap();
+ assert_ne!(out1, out2);
+ }
+
+ //
+ // 6. 순차 출력
+ //
+
+ #[test]
+ fn sha256_sequential_generates_produce_different_output() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0x5au8; 32], &[0x7bu8; 16], None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ drbg.generate(&mut out1, None).unwrap();
+ drbg.generate(&mut out2, None).unwrap();
+ assert_ne!(out1, out2);
+ }
+
+ #[test]
+ fn sha512_sequential_generates_produce_different_output() {
+ let mut drbg = HashDRBGSHA512::instantiate(&[0x5au8; 32], &[0x7bu8; 16], None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ drbg.generate(&mut out1, None).unwrap();
+ drbg.generate(&mut out2, None).unwrap();
+ assert_ne!(out1, out2);
+ }
+
+ #[test]
+ fn sha256_output_is_not_all_zeros() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0x5au8; 32], &[0x7bu8; 16], None).unwrap();
+ let mut out = [0u8; 64];
+ drbg.generate(&mut out, None).unwrap();
+ assert!(out.iter().any(|&b| b != 0));
+ }
+
+ // 7. Reseed 동작
+
+ #[test]
+ fn sha256_reseed_changes_subsequent_output() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d_no_reseed = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d_reseeded = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ d_reseeded.reseed(&[0xffu8; 32], None).unwrap();
+ let mut out_no = [0u8; 64];
+ let mut out_re = [0u8; 64];
+ d_no_reseed.generate(&mut out_no, None).unwrap();
+ d_reseeded.generate(&mut out_re, None).unwrap();
+ assert_ne!(out_no, out_re);
+ }
+
+ #[test]
+ fn sha256_reseed_is_deterministic() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let reseed_entropy = [0xddu8; 32];
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ d1.reseed(&reseed_entropy, None).unwrap();
+ d2.reseed(&reseed_entropy, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_eq!(out1, out2);
+ }
+
+ #[test]
+ fn sha256_reseed_additional_input_changes_output() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let reseed_entropy = [0xddu8; 32];
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ d1.reseed(&reseed_entropy, None).unwrap();
+ d2.reseed(&reseed_entropy, Some(b"reseed-context")).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_ne!(out1, out2);
+ }
+
+ // 8. 알고리즘 간 독립성
+
+ #[test]
+ fn sha256_and_sha512_produce_different_output() {
+ let entropy = [0x5au8; 32];
+ let nonce = [0x7bu8; 16];
+ let mut d256 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d512 = HashDRBGSHA512::instantiate(&entropy, &nonce, None).unwrap();
+ let mut out256 = [0u8; 32];
+ let mut out512 = [0u8; 32];
+ d256.generate(&mut out256, None).unwrap();
+ d512.generate(&mut out512, None).unwrap();
+ assert_ne!(out256, out512);
+ }
+
+ #[test]
+ fn sha224_and_sha384_produce_different_output() {
+ let entropy32 = [0x5au8; 32];
+ let nonce16 = [0x7bu8; 16];
+ let mut d224 = HashDRBGSHA224::instantiate(&entropy32[..14], &nonce16[..7], None).unwrap();
+ let mut d384 = HashDRBGSHA384::instantiate(&entropy32, &nonce16, None).unwrap();
+ let mut out224 = [0u8; 28];
+ let mut out384 = [0u8; 28];
+ d224.generate(&mut out224, None).unwrap();
+ d384.generate(&mut out384, None).unwrap();
+ assert_ne!(out224, out384);
+ }
+
+ // 9. NIST KAT (Known Answer Tests)
+
+ /// SHA-256, No PR, No Additional Input — 결정론성 검증
+ #[test]
+ fn sha256_kat_count0_deterministic_cross_instance() {
+ let entropy = hex("06032cd5eed33f39265f49ecb142c511da9aff2af71203bffaf34a9ca5bd9c0d");
+ let nonce = hex("0e66f71edc43e42a45ad3c6fc6cdc4df");
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut discard = [0u8; 64];
+ d1.generate(&mut discard, None).unwrap();
+ d2.generate(&mut discard, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_eq!(out1, out2);
+ assert!(out1.iter().any(|&b| b != 0));
+ }
+
+ /// SHA-512, No PR, No Additional Input — 결정론성 검증
+ #[test]
+ fn sha512_kat_count0_deterministic_cross_instance() {
+ let entropy = hex("8d1d45bcba43f5ca54c7b57d08f8e3ff72b6df8e9e43cde6f2ad99db8a1e5478");
+ let nonce = hex("a0bc70ef9fe7c2da67b2a58e93dd5e3c");
+ let mut d1 = HashDRBGSHA512::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA512::instantiate(&entropy, &nonce, None).unwrap();
+ let mut discard = [0u8; 64];
+ d1.generate(&mut discard, None).unwrap();
+ d2.generate(&mut discard, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_eq!(out1, out2);
+ assert!(out1.iter().any(|&b| b != 0));
+ }
+
+ /// SHA-256 with Personalization String — 결정론성 유지
+ #[test]
+ fn sha256_kat_with_personalization_deterministic() {
+ let entropy = hex("06032cd5eed33f39265f49ecb142c511da9aff2af71203bffaf34a9ca5bd9c0d");
+ let nonce = hex("0e66f71edc43e42a45ad3c6fc6cdc4df");
+ let ps = b"entanglementlib-test-personalization";
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, Some(ps)).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, Some(ps)).unwrap();
+ let mut discard = [0u8; 64];
+ d1.generate(&mut discard, None).unwrap();
+ d2.generate(&mut discard, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_eq!(out1, out2);
+ }
+
+ /// SHA-256 with Reseed — reseed 후 결정론성 확인
+ #[test]
+ fn sha256_kat_after_reseed_deterministic() {
+ let entropy = hex("06032cd5eed33f39265f49ecb142c511da9aff2af71203bffaf34a9ca5bd9c0d");
+ let nonce = hex("0e66f71edc43e42a45ad3c6fc6cdc4df");
+ let reseed_entropy =
+ hex("a4f0cdce12f1a7c12b3b5fba2c2a23b5f1eeb0e4a12f3c9d1b8e4f5a7c6d0e2b");
+ let mut d1 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ let mut d2 = HashDRBGSHA256::instantiate(&entropy, &nonce, None).unwrap();
+ d1.reseed(&reseed_entropy, None).unwrap();
+ d2.reseed(&reseed_entropy, None).unwrap();
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_eq!(out1, out2);
+ assert!(out1.iter().any(|&b| b != 0));
+ }
+
+ // 10. 통계적 건전성
+
+ #[test]
+ fn sha256_output_has_reasonable_byte_distribution() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0x5au8; 32], &[0x7bu8; 16], None).unwrap();
+ let mut buf = alloc::vec![0u8; 1024];
+ drbg.generate(&mut buf, None).unwrap();
+ let mut freq = [0u32; 256];
+ for &b in &buf {
+ freq[b as usize] += 1;
+ }
+ let max_freq = *freq.iter().max().unwrap();
+ assert!(
+ max_freq <= 25,
+ "과도한 바이트 빈도 편향 감지: max_freq={max_freq}"
+ );
+ }
+
+ #[test]
+ fn sha256_large_output_blocks_are_distinct() {
+ let mut drbg = HashDRBGSHA256::instantiate(&[0x5au8; 32], &[0x7bu8; 16], None).unwrap();
+ let mut blocks: alloc::vec::Vec<[u8; 32]> = alloc::vec::Vec::new();
+ for _ in 0..8 {
+ let mut block = [0u8; 32];
+ drbg.generate(&mut block, None).unwrap();
+ blocks.push(block);
+ }
+ for i in 0..blocks.len() {
+ for j in (i + 1)..blocks.len() {
+ assert_ne!(
+ blocks[i], blocks[j],
+ "블록 {i}와 {j}가 동일: {:?}",
+ blocks[i]
+ );
+ }
+ }
+ }
+
+ // 11. new_from_os 스모크 테스트
+
+ /// OS 엔트로피로 초기화된 두 인스턴스의 출력이 서로 다름 (독립성)
+ #[test]
+ fn new_from_os_produces_unique_outputs() {
+ let mut d1 = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut d2 = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ // 독립적인 OS 엔트로피로 초기화되었으므로 출력이 달라야 함
+ assert_ne!(
+ out1, out2,
+ "서로 다른 OS 엔트로피 인스턴스의 출력이 동일합니다"
+ );
+ }
+
+ /// new_from_os 출력이 전부 0이 아님
+ #[test]
+ fn new_from_os_output_is_not_all_zeros() {
+ let mut drbg = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut out = [0u8; 64];
+ drbg.generate(&mut out, None).unwrap();
+ assert!(out.iter().any(|&b| b != 0));
+ }
+
+ /// personalization_string을 포함한 new_from_os 초기화
+ #[test]
+ fn new_from_os_with_personalization_succeeds() {
+ let ps = b"entlib-native-mldsa-v2";
+ assert!(HashDRBGSHA512::new_from_os(Some(ps)).is_ok());
+ assert!(HashDRBGSHA256::new_from_os(Some(ps)).is_ok());
+ }
+}
diff --git a/core/rng/src/lib.rs b/core/rng/src/lib.rs
new file mode 100644
index 0000000..77c00d7
--- /dev/null
+++ b/core/rng/src/lib.rs
@@ -0,0 +1,39 @@
+#![no_std]
+
+extern crate alloc;
+
+mod hash_drbg;
+mod os_entropy;
+
+/// DRBG 연산 중 발생할 수 있는 오류에 대한 열거형입니다.
+///
+/// 모든 DRBG 구현에서 공유됩니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DrbgError {
+ /// 엔트로피 입력이 최소 보안 강도 요구사항(security_strength bytes) 미달
+ EntropyTooShort,
+ /// 엔트로피 입력 또는 Nonce가 최대 허용 길이(2^35 bits = 2^32 bytes) 초과
+ EntropyTooLong,
+ /// additional_input 또는 personalization_string이 최대 허용 길이(2^35 bits = 2^32 bytes) 초과
+ InputTooLong,
+ /// Nonce가 최소 길이(security_strength / 2 bytes) 미달
+ NonceTooShort,
+ /// 잘못된 인수 (예: no_of_bits 오버플로우)
+ InvalidArgument,
+ /// 재시드 간격(2^48) 초과 — 즉시 reseed() 호출 필요
+ ReseedRequired,
+ /// SecureBuffer 메모리 할당 실패 또는 OS mlock 실패
+ AllocationFailed,
+ /// 내부 해시 연산 실패
+ InternalHashError,
+ /// 요청한 출력 크기가 최대 허용치(65536 bytes = 2^19 bits) 초과
+ RequestTooLarge,
+ /// OS 엔트로피 소스 접근 실패
+ ///
+ /// 발생 원인:
+ /// - 지원되지 않는 플랫폼: `os_entropy::extract_os_entropy` cfg 조건 미충족
+ /// - VM 환경: 엔트로피 풀 초기화 미완료 (부팅 직후)
+ OsEntropyFailed,
+}
+
+pub use hash_drbg::{HashDRBGSHA224, HashDRBGSHA256, HashDRBGSHA384, HashDRBGSHA512};
diff --git a/core/rng/src/os_entropy.rs b/core/rng/src/os_entropy.rs
new file mode 100644
index 0000000..ea32776
--- /dev/null
+++ b/core/rng/src/os_entropy.rs
@@ -0,0 +1,195 @@
+use core::arch::asm;
+use entlib_native_base::error::rng::RngError;
+use entlib_native_secure_buffer::SecureBuffer;
+use entlib_native_sha3::api::SHA3_256;
+
+#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
+pub fn extract_os_entropy(size: usize) -> Result {
+ let mut buffer = SecureBuffer::new_owned(size)?;
+ let buf_slice = buffer.as_mut_slice();
+ let mut read_bytes = 0;
+
+ // SYS_getrandom = 318, flags = 0 (/dev/urandom 동작)
+ while read_bytes < size {
+ let ret: isize;
+ unsafe {
+ asm!(
+ "syscall",
+ in("rax") 318usize,
+ in("rdi") buf_slice[read_bytes..].as_mut_ptr(),
+ in("rsi") size - read_bytes,
+ in("rdx") 0usize,
+ lateout("rcx") _,
+ lateout("r11") _,
+ lateout("rax") ret,
+ );
+ }
+ if ret < 0 {
+ let errno = -ret;
+ if errno == 4 {
+ // EINTR
+ continue;
+ }
+ return Err(RngError::OsKernelError);
+ }
+ if ret == 0 {
+ return Err(RngError::EntropySourceEof);
+ }
+ read_bytes += ret as usize;
+ }
+
+ Ok(buffer)
+}
+
+#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
+pub fn extract_os_entropy(size: usize) -> Result {
+ let mut buffer = SecureBuffer::new_owned(size)?;
+ let buf_slice = buffer.as_mut_slice();
+ let mut read_bytes = 0;
+
+ // SYS_getrandom = 278 (aarch64 Linux), flags = 0 (/dev/urandom 동작)
+ while read_bytes < size {
+ let ret: isize;
+ unsafe {
+ asm!(
+ "svc #0",
+ in("x8") 278usize,
+ in("x0") buf_slice[read_bytes..].as_mut_ptr(),
+ in("x1") size - read_bytes,
+ in("x2") 0usize,
+ lateout("x0") ret,
+ options(nostack),
+ );
+ }
+ if ret < 0 {
+ if -ret == 4 {
+ // EINTR
+ continue;
+ }
+ return Err(RngError::OsKernelError);
+ }
+ if ret == 0 {
+ return Err(RngError::EntropySourceEof);
+ }
+ read_bytes += ret as usize;
+ }
+
+ Ok(buffer)
+}
+
+#[cfg(target_os = "macos")]
+pub fn extract_os_entropy(size: usize) -> Result {
+ if size > 256 {
+ return Err(RngError::SizeLimitExceeded);
+ }
+ let mut buffer = SecureBuffer::new_owned(size)?;
+ unsafe extern "C" {
+ fn getentropy(buf: *mut u8, buflen: usize) -> i32;
+ }
+ let ret = unsafe { getentropy(buffer.as_mut_slice().as_mut_ptr(), size) };
+ if ret != 0 {
+ return Err(RngError::OsKernelError);
+ }
+ Ok(buffer)
+}
+
+#[allow(dead_code)]
+#[cfg(target_arch = "x86_64")]
+pub fn extract_hardware_entropy(size: usize) -> Result {
+ let mut buffer = SecureBuffer::new_owned(size)?;
+ let buf_slice = buffer.as_mut_slice();
+
+ if !size.is_multiple_of(8) {
+ return Err(RngError::InvalidAlignment);
+ }
+
+ let qwords = size / 8;
+ for i in 0..qwords {
+ let mut seed: u64 = 0;
+ let mut success: u8 = 0;
+
+ let mut retries = 100;
+ while retries > 0 {
+ unsafe {
+ asm!(
+ "rdseed {0}",
+ "setc {1}",
+ out(reg) seed,
+ out(reg_byte) success,
+ );
+ }
+
+ if success == 1 {
+ break;
+ }
+
+ // CPU 파이프라인 지연을 위한 pause
+ unsafe { asm!("pause") };
+ retries -= 1;
+ }
+
+ if success == 0 {
+ return Err(RngError::HardwareEntropyExhausted);
+ }
+
+ buf_slice[i * 8..(i + 1) * 8].copy_from_slice(&seed.to_ne_bytes());
+ }
+
+ Ok(buffer)
+}
+
+#[cfg(target_arch = "aarch64")]
+pub fn extract_hardware_entropy_arm(size: usize) -> Result {
+ let mut buffer = SecureBuffer::new_owned(size)?;
+ let buf_slice = buffer.as_mut_slice();
+
+ if !size.is_multiple_of(8) {
+ return Err(RngError::InvalidAlignment);
+ }
+
+ let qwords = size / 8;
+ for i in 0..qwords {
+ let mut seed: u64 = 0;
+ let mut success: u64; // ARM 상태 플래그 확인용
+
+ // RNDRRS 레지스터에서 값을 읽음
+ // 실패 시 Z 플래그(Zero)가 1로 설정
+ unsafe {
+ asm!(
+ "mrs {0}, s3_3_c2_c4_1", // RNDRRS의 시스템 레지스터 인코딩
+ "cset {1}, ne", // Z 플래그가 0이면(ne) 성공(1)
+ out(reg) seed,
+ out(reg) success,
+ );
+ }
+
+ if success == 0 {
+ return Err(RngError::HardwareEntropyExhausted);
+ }
+
+ buf_slice[i * 8..(i + 1) * 8].copy_from_slice(&seed.to_ne_bytes());
+ }
+
+ Ok(buffer)
+}
+
+// Q. T. Felix TODO: x86_64, ARM64 베어메탈 std/no_std 환경 대응하는 엔트로피 소스 추출 기능
+
+/// OS 시스템 콜이나 CPU 명령어에서 얻은 원시 엔트로피(Raw Entropy)는 즉시 암호 키로 사용되어서는
+/// 안 됩니다. NIST SP 800-90C 지침에 따라 검증된 암호학적 해시 함수를 통해 컨디셔닝(Conditioning)
+/// 과정을 거쳐야 합니다.
+#[allow(unused)]
+pub fn condition_entropy(raw_entropy: &SecureBuffer) -> Result {
+ if raw_entropy.len() < 32 {
+ return Err(RngError::InsufficientEntropy);
+ }
+
+ let mut hasher = SHA3_256::new();
+ hasher.update(raw_entropy.as_slice());
+ hasher.finalize().map_err(|e| match e {
+ entlib_native_base::error::hash::HashError::Buffer(buf_err) => RngError::Buffer(buf_err),
+ _ => RngError::Buffer(
+ entlib_native_base::error::secure_buffer::SecureBufferError::AllocationFailed,
+ ),
+ })
+}
diff --git a/core/rng/tests/hash_drbg_test.rs b/core/rng/tests/hash_drbg_test.rs
new file mode 100644
index 0000000..c5a86dc
--- /dev/null
+++ b/core/rng/tests/hash_drbg_test.rs
@@ -0,0 +1,136 @@
+//! Hash_DRBG 공개 API 통합 테스트.
+//!
+//! `instantiate`는 `pub(crate)` 내부 전용 함수로, 이 파일에서는 접근할 수 없습니다.
+//! 입력 검증, 결정론성, KAT 테스트는 `src/hash_drbg.rs` 내부 단위 테스트에서 수행됩니다.
+//!
+//! 이 파일은 공개 API인 `new_from_os`, `reseed`, `generate`의 통합 동작을 검증합니다.
+
+use entlib_native_rng::{DrbgError, HashDRBGSHA256, HashDRBGSHA512};
+
+//
+// generate 입력 검증
+//
+
+#[test]
+fn generate_rejects_request_too_large() {
+ let mut drbg = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut buf = vec![0u8; 65537];
+ assert!(matches!(
+ drbg.generate(&mut buf, None),
+ Err(DrbgError::RequestTooLarge)
+ ));
+}
+
+#[test]
+fn generate_accepts_maximum_request_size() {
+ let mut drbg = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut buf = vec![0u8; 65536];
+ assert!(drbg.generate(&mut buf, None).is_ok());
+}
+
+#[test]
+fn generate_empty_output_is_valid() {
+ let mut drbg = HashDRBGSHA256::new_from_os(None).expect("new_from_os failed");
+ let mut buf = [];
+ assert!(drbg.generate(&mut buf, None).is_ok());
+}
+
+//
+// reseed 입력 검증
+//
+
+#[test]
+fn reseed_rejects_entropy_too_short() {
+ let mut drbg = HashDRBGSHA256::new_from_os(None).expect("new_from_os failed");
+ assert!(matches!(
+ drbg.reseed(&[0xefu8; 15], None),
+ Err(DrbgError::EntropyTooShort)
+ ));
+}
+
+#[test]
+fn reseed_accepts_minimum_entropy() {
+ let mut drbg = HashDRBGSHA256::new_from_os(None).expect("new_from_os failed");
+ assert!(drbg.reseed(&[0xefu8; 16], None).is_ok());
+}
+
+//
+// new_from_os 스모크 테스트
+//
+
+#[test]
+fn new_from_os_sha256_succeeds() {
+ assert!(HashDRBGSHA256::new_from_os(None).is_ok());
+}
+
+#[test]
+fn new_from_os_sha512_succeeds() {
+ assert!(HashDRBGSHA512::new_from_os(None).is_ok());
+}
+
+#[test]
+fn new_from_os_with_personalization_succeeds() {
+ let ps = b"entlib-native-integration-test";
+ assert!(HashDRBGSHA512::new_from_os(Some(ps)).is_ok());
+}
+
+/// OS 엔트로피로 초기화된 두 인스턴스의 출력이 서로 다름 (독립성)
+#[test]
+fn two_os_instances_produce_different_output() {
+ let mut d1 = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut d2 = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, None).unwrap();
+ assert_ne!(out1, out2, "독립 인스턴스의 출력이 동일합니다");
+}
+
+/// generate 출력이 전부 0이 아님
+#[test]
+fn output_is_not_all_zeros() {
+ let mut drbg = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut out = [0u8; 64];
+ drbg.generate(&mut out, None).unwrap();
+ assert!(out.iter().any(|&b| b != 0));
+}
+
+/// 연속 두 번 generate → 서로 다른 출력
+#[test]
+fn sequential_generates_produce_different_output() {
+ let mut drbg = HashDRBGSHA512::new_from_os(None).expect("new_from_os failed");
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ drbg.generate(&mut out1, None).unwrap();
+ drbg.generate(&mut out2, None).unwrap();
+ assert_ne!(out1, out2);
+}
+
+/// reseed 후 출력이 변경됨
+#[test]
+fn reseed_changes_subsequent_output() {
+ let mut drbg = HashDRBGSHA256::new_from_os(None).expect("new_from_os failed");
+ let mut before = [0u8; 64];
+ drbg.generate(&mut before, None).unwrap();
+ // 다른 엔트로피로 reseed
+ drbg.reseed(&[0xffu8; 32], None).unwrap();
+ let mut after = [0u8; 64];
+ drbg.generate(&mut after, None).unwrap();
+ assert_ne!(before, after);
+}
+
+/// additional_input 유무 → 다른 출력
+#[test]
+fn additional_input_changes_output() {
+ let mut d1 = HashDRBGSHA256::new_from_os(Some(b"fixed-personalization")).expect("failed");
+ let mut d2 = HashDRBGSHA256::new_from_os(Some(b"fixed-personalization")).expect("failed");
+ // 두 인스턴스는 서로 다른 OS 엔트로피로 초기화되므로, additional_input 효과를
+ // 단독으로 검증하기 어렵습니다. 대신 additional_input 제공 시 에러가 없음을 확인합니다.
+ let mut out1 = [0u8; 64];
+ let mut out2 = [0u8; 64];
+ d1.generate(&mut out1, None).unwrap();
+ d2.generate(&mut out2, Some(b"context")).unwrap();
+ // 두 출력이 다를 수 있고 (다른 OS 엔트로피), generate 자체가 성공해야 함
+ let _ = out1;
+ let _ = out2;
+}
diff --git a/core/secure-buffer/Cargo.toml b/core/secure-buffer/Cargo.toml
index 7ed8e7f..a024089 100644
--- a/core/secure-buffer/Cargo.toml
+++ b/core/secure-buffer/Cargo.toml
@@ -9,4 +9,5 @@ license.workspace = true
std = []
[dependencies]
-entlib-native-result.workspace = true
\ No newline at end of file
+entlib-native-result.workspace = true
+entlib-native-base.workspace = true
\ No newline at end of file
diff --git a/core/secure-buffer/src/buffer.rs b/core/secure-buffer/src/buffer.rs
index 433ac89..91302c7 100644
--- a/core/secure-buffer/src/buffer.rs
+++ b/core/secure-buffer/src/buffer.rs
@@ -1,5 +1,6 @@
use crate::memory::SecureMemoryBlock;
use crate::zeroize::{SecureZeroize, Zeroizer};
+use entlib_native_base::error::secure_buffer::SecureBufferError;
/// 군사급 보안 요구사항을 충족하는 고수준 보안 버퍼입니다.
///
@@ -31,8 +32,8 @@ impl SecureBuffer {
///
/// # Returns
/// - `Ok(SecureBuffer)` - 할당 및 잠금 성공 시
- /// - `Err(&'static str)` - 메모리 할당 실패 또는 OS 리소스 제한 도달 시
- pub fn new_owned(size: usize) -> Result {
+ /// - `Err(SecureBufferError)` - 메모리 할당 실패 또는 OS 리소스 제한 도달 시
+ pub fn new_owned(size: usize) -> Result {
let block = SecureMemoryBlock::allocate_locked(size)?;
Ok(Self {
@@ -56,23 +57,20 @@ impl SecureBuffer {
/// - `ptr`은 유효한 메모리 주소를 가리켜야 합니다.
/// - `len`은 해당 메모리 영역의 올바른 크기여야 합니다.
/// - 호출자는 `ptr`이 가리키는 메모리가 `len`만큼 유효함을 보장해야 합니다.
- pub unsafe fn from_raw_parts(ptr: *mut u8, len: usize) -> Result {
+ 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.");
+ return Err(SecureBufferError::PageAlignmentViolation);
}
if !len.is_multiple_of(ps) {
- return Err(
- "Security Violation: External memory length is not a multiple of PAGE_SIZE.",
- );
+ return Err(SecureBufferError::PageAlignmentViolation);
}
#[cfg(feature = "std")]
unsafe {
- // 외부 메모리라도 Rust 쪽에서 사용 중에는 스왑되지 않도록 잠금 시도
+ // Q. T. Felix TODO: 베어메탈 std 환경에서 lock_memory는 사용할 수 없습니다.
if !crate::memory::os_lock::lock_memory(ptr, len) {
- return Err("Failed to lock external memory segment.");
+ return Err(SecureBufferError::MemoryLockFailed);
}
}
@@ -96,11 +94,16 @@ impl SecureBuffer {
self.len == 0
}
+ #[inline(always)]
+ pub fn capacity(&self) -> usize {
+ self.capacity
+ }
+
/// 버퍼의 내용을 읽기 전용 슬라이스로 반환합니다.
///
/// # Security Note
/// 반환된 슬라이스는 `SecureBuffer`의 수명에 묶여 있습니다.
- /// 슬라이스를 통해 얻은 데이터는 별도로 복사하지 말고 제자리에서 사용하십시오.
+ /// 슬라이스를 통해 얻은 데이터는 별도로 복사하지 말고 제자리에서 사용하세요.
#[inline(always)]
pub fn as_slice(&self) -> &[u8] {
unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
@@ -139,6 +142,7 @@ impl Drop for SecureBuffer {
// 외부가 소유한 메모리: 잠금만 해제하고, 메모리 반환은 Java Arena 등에 위임
#[cfg(feature = "std")]
unsafe {
+ // Q. T. Felix TODO: 베어메탈 std 환경에서 unlock_memory는 사용할 수 없습니다.
crate::memory::os_lock::unlock_memory(self.ptr, self.capacity);
}
}
diff --git a/core/secure-buffer/src/memory.rs b/core/secure-buffer/src/memory.rs
index 1f73900..6dba1c9 100644
--- a/core/secure-buffer/src/memory.rs
+++ b/core/secure-buffer/src/memory.rs
@@ -1,4 +1,5 @@
use alloc::alloc::{Layout, alloc_zeroed, dealloc};
+use entlib_native_base::error::secure_buffer::SecureBufferError;
#[cfg(feature = "std")]
use std::sync::OnceLock;
@@ -8,9 +9,15 @@ pub(crate) fn page_size() -> usize {
#[cfg(feature = "std")]
{
static OS_PAGE_SIZE: OnceLock = OnceLock::new();
- *OS_PAGE_SIZE.get_or_init(|| unsafe {
+ *OS_PAGE_SIZE.get_or_init(|| {
#[cfg(unix)]
+ {
// 커널 계층과 직접 통신하여 페이지 크기 획득
+ // Linux: fetch_os_page_size()는 raw syscall을 사용하는 unsafe fn
+ // 그 외 Unix(macOS 등): POSIX getpagesize() 래퍼이므로 safe fn
+ #[cfg(target_os = "linux")]
+ let size = unsafe { fetch_os_page_size() };
+ #[cfg(not(target_os = "linux"))]
let size = fetch_os_page_size();
// 변조된 커널 응답 방어 (최소 4kb 및 2배수 확인)
@@ -18,7 +25,8 @@ pub(crate) fn page_size() -> usize {
panic!("Security Violation: 안전하지 않거나 변조된 OS 페이지 크기가 감지되었습니다! ({})", size);
}
size
- })
+ }
+ }) // Q. T. Felix TODO: 베어메탈 환경 대응이 필요합니다.
}
#[cfg(not(feature = "std"))]
@@ -153,10 +161,10 @@ unsafe fn fetch_os_page_size() -> usize {
#[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;
+ fn getpagesize() -> core::ffi::c_int;
}
- let size = unsafe { get_pagesize() };
+ let size = unsafe { getpagesize() };
// 비정상적인 OS 응답 방어
if size <= 0 {
@@ -204,32 +212,29 @@ impl SecureMemoryBlock {
///
/// # Returns
/// - `Ok(SecureMemoryBlock)` - 할당 및 잠금 성공 시
- /// - `Err(&'static str)` - 메모리 할당 실패 또는 잠금 실패(리소스 제한 등) 시
+ /// - `Err(SecureBufferError)` - 메모리 할당 실패 또는 잠금 실패(리소스 제한 등) 시
///
/// # Safety
/// 내부적으로 `alloc_zeroed`를 사용하여 초기화되지 않은 메모리 접근(UB)을 방지합니다.
/// 하지만 OS의 메모리 잠금 제한(RLIMIT_MEMLOCK 등)에 걸릴 경우 실패할 수 있습니다.
- pub fn allocate_locked(size: usize) -> Result {
+ 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")?;
+ let layout =
+ Layout::from_size_align(capacity, ps).map_err(|_| SecureBufferError::InvalidLayout)?;
- // 할당 시 남는 패딩 영역의 기존 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");
+ return Err(SecureBufferError::AllocationFailed);
}
#[cfg(feature = "std")]
unsafe {
- // OS별 메모리 잠금 수행
+ // Q. T. Felix TODO: 베어메탈 std 환경에서 lock_memory는 사용할 수 없습니다.
if !os_lock::lock_memory(ptr, capacity) {
- // 잠금 실패 시, 할당했던 메모리를 즉시 해제하고 에러 반환
dealloc(ptr, layout);
- return Err("OS memory lock (mlock/VirtualLock) failed. Resource limit reached.");
+ return Err(SecureBufferError::MemoryLockFailed);
}
}
@@ -250,6 +255,7 @@ impl SecureMemoryBlock {
#[cfg(feature = "std")]
// 메모리 잠금 해제 (페이지 아웃 허용)
unsafe {
+ // Q. T. Felix TODO: 베어메탈 std 환경에서 unlock_memory는 사용할 수 없습니다.
os_lock::unlock_memory(self.ptr, self.capacity);
}
@@ -299,8 +305,8 @@ pub(crate) mod os_lock {
}
unsafe extern "C" {
- fn get_rlimit(resource: i32, rlim: *mut Rlimit) -> i32;
- fn set_rlimit(resource: i32, rlim: *const Rlimit) -> i32;
+ fn getrlimit(resource: i32, rlim: *mut Rlimit) -> i32;
+ fn setrlimit(resource: i32, rlim: *const Rlimit) -> i32;
}
const RLIMIT_MEMLOCK: i32 = 8;
@@ -312,12 +318,12 @@ pub(crate) mod os_lock {
};
unsafe {
- if get_rlimit(RLIMIT_MEMLOCK, &mut rlim) == 0 {
+ if getrlimit(RLIMIT_MEMLOCK, &mut rlim) == 0 {
rlim.rlim_cur = RLIM_INFINITY;
rlim.rlim_max = RLIM_INFINITY;
// 한도 상향 성공 시 2차 잠금 재시도
- if set_rlimit(RLIMIT_MEMLOCK, &rlim) == 0 {
+ if setrlimit(RLIMIT_MEMLOCK, &rlim) == 0 {
return mlock(ptr as *const c_void, len) == 0;
}
}
diff --git a/crypto/aes/Cargo.toml b/crypto/aes/Cargo.toml
new file mode 100644
index 0000000..bd51dea
--- /dev/null
+++ b/crypto/aes/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "entlib-native-aes"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+entlib-native-secure-buffer.workspace = true
+entlib-native-constant-time.workspace = true
+entlib-native-hmac.workspace = true
+
+[dev-dependencies]
diff --git a/crypto/aes/README.md b/crypto/aes/README.md
new file mode 100644
index 0000000..8c8131d
--- /dev/null
+++ b/crypto/aes/README.md
@@ -0,0 +1,272 @@
+# AES-256 크레이트 (entlib-native-aes)
+
+> Q. T. Felix (수정: 26.03.22 UTC+9)
+>
+> [English README](README_EN.md)
+
+`entlib-native-aes`는 NIST FIPS 140-3 및 Common Criteria EAL4+ 인증 요구사항을 충족하도록 설계된 AES-256 암호화 모듈입니다. **256비트 키만 지원**하며, 기밀성과 무결성을 동시에 제공하는 두 가지 승인된 운용 모드를 구현합니다.
+
+- **AES-256-GCM** — NIST SP 800-38D 준거 AEAD (Authenticated Encryption with Associated Data)
+- **AES-256-CBC-HMAC-SHA256** — NIST SP 800-38A + Encrypt-then-MAC 구성 (CBC 단독 사용 금지)
+
+**이 알고리즘은 128, 192 키 길이는 의도적으로 대응하지 않습니다.** FIPS 140-3은 AES-256 사용을 권고하며, 단일 키 크기만 노출하여 잘못된 키 길이 선택으로 인한 보안 약화를 사전에 차단합니다.
+
+## 보안 위협 모델
+
+### 캐시 타이밍 공격 (Cache-Timing Attack)
+
+AES의 표준 소프트웨어 구현은 SubBytes 연산을 위해 256바이트 SBox 룩업 테이블을 사용합니다. 이 접근 방식은 치명적인 취약점을 내포합니다. 공격자가 동일한 CPU 캐시를 공유하는 환경(VPS, 클라우드)에서 캐시 히트·미스 패턴으로부터 접근된 테이블 인덱스, 즉 비밀 키 바이트를 통계적으로 복원할 수 있습니다. 다니엘 번스타인(D. J. Bernsteint)의 2005년 AES 타이밍 공격은 이를 실증적으로 증명하였습니다.
+
+본 크레이트는 룩업 테이블을 일체 사용하지 않습니다. SubBytes는 GF(2^8) 역원 계산과 아핀 변환을 순수 산술 비트 연산으로 수행하며, 모든 연산의 실행 시간은 비밀 키 및 평문 값과 완전히 독립적입니다.
+
+### 패딩 오라클 공격 (Padding Oracle Attack)
+
+CBC 모드에서 복호화 오류 응답이 패딩 유효성에 따라 달라지면, 공격자는 적응적 선택 암호문 공격(ACCA)으로 임의 암호문을 완전히 복호화할 수 있습니다(POODLE, Lucky 13 변종). 본 구현은 **Encrypt-then-MAC** 구성을 강제하여 이 공격 벡터를 원천 차단합니다. MAC 검증이 선행되며, MAC 실패 시 복호화 연산 자체를 수행하지 않습니다.
+
+### GCM Nonce 재사용 (Nonce Reuse)
+
+GCM에서 동일한 (키, nonce) 쌍이 두 번이라도 사용되면 두 암호문의 XOR로부터 평문의 XOR이 노출되어 기밀성이 완전히 붕괴됩니다. 나아가 GHASH 다항식 방정식 풀기를 통해 인증 키 H가 복원되어 무결성도 위협받습니다. 본 크레이트는 nonce 생성 정책을 호출자에 위임하며, API 문서에 명시적 경고를 부착합니다. 프로덕션 환경에서는 `entlib-native-rng`의 `HashDRBGSHA256`로 nonce를 생성하거나, 충돌 없음이 보장된 카운터 기반 구성을 사용하십시오.
+
+## 보안 핵심: 상수-시간 AES 코어
+
+### GF(2^8) 산술
+
+AES SubBytes는 유한체 GF(2^8) = GF(2)[x] / (x^8 + x^4 + x^3 + x + 1) 위의 역원을 계산한 후 아핀 변환(Affine Transformation)을 적용합니다.
+
+#### xtime: GF(2^8) 에서 x 를 곱함
+
+$$ \text{xtime}(a) = \begin{cases} a \ll 1 & \text{if MSB}(a) = 0 \\ (a \ll 1) \oplus \texttt{0x1b} & \text{if MSB}(a) = 1 \end{cases} $$
+
+분기문 없이 구현합니다.
+
+$$\text{mask} = -(a \gg 7), \quad \text{xtime}(a) = (a \ll 1) \oplus (\texttt{0x1b} \land \text{mask})$$
+
+`mask`는 MSB가 1이면 `0xFF`, 0이면 `0x00`이므로, 단일 `SHR`, `NEG`, `AND`, `XOR` 명령어 4개로 컴파일됩니다.
+
+#### gmul: GF(2^8) 곱셈 — 고정 8회 반복
+
+$$\text{gmul}(a, b) = \bigoplus_{i=0}^{7} \left( a \cdot x^i \land -(b_i) \right)$$
+
+여기서 $b_i$는 $b$의 $i$번째 비트입니다. `-(b & 1).wrapping_neg()`로 비트를 마스크로 변환하여 분기 없이 조건부 XOR를 수행합니다. 반복 횟수는 비밀 데이터와 무관한 고정 값 8이므로 타이밍이 일정합니다.
+
+#### gf_inv: GF(2^8) 역원 — 페르마의 소정리
+
+유한체에서 $a \ne 0$이면 $a^{-1} = a^{2^8 - 2} = a^{254}$입니다. $a = 0$이면 $0^{254} = 0$이 자연히 반환되므로 분기가 필요하지 않습니다.
+
+> [!NOTE]
+> **Square-and-Multiply 전개**: $254 = \texttt{11111110}_2$이므로
+>
+> $$a^{254} = a^{128} \cdot a^{64} \cdot a^{32} \cdot a^{16} \cdot a^8 \cdot a^4 \cdot a^2$$
+>
+> 7번의 제곱(squaring)과 6번의 곱셈으로 총 13회의 `gmul` 호출로 계산됩니다. 테이블 접근이 전혀 없으므로 캐시 타이밍 채널이 존재하지 않습니다.
+
+#### sub_byte: SubBytes 아핀 변환
+
+역원 $a^{-1}$에 아핀 변환 $M \cdot a^{-1} + c$를 적용합니다.
+
+$$b_i = a^{-1}_i \oplus a^{-1}_{(i+4) \bmod 8} \oplus a^{-1}_{(i+5) \bmod 8} \oplus a^{-1}_{(i+6) \bmod 8} \oplus a^{-1}_{(i+7) \bmod 8} \oplus c_i$$
+
+비트 회전으로 동치 표현합니다 ($c = \texttt{0x63}$).
+
+```math
+\text{sub\_byte}(a) = a^{-1} \oplus \text{ROL}(a^{-1}, 1) \oplus \text{ROL}(a^{-1}, 2) \oplus \text{ROL}(a^{-1}, 3) \oplus \text{ROL}(a^{-1}, 4) \oplus \texttt{0x63}
+```
+
+역 SubBytes (`inv_sub_byte`)는 역 아핀 변환 후 역원을 계산합니다.
+
+```math
+\text{inv\_sub\_byte}(a) = \text{gf\_inv}\!\left(\text{ROL}(a,1) \oplus \text{ROL}(a,3) \oplus \text{ROL}(a,6) \oplus \texttt{0x05}\right)
+```
+
+### 키 스케줄 (Key Schedule)
+
+AES-256은 32바이트 마스터 키로부터 15개의 라운드 키(각 16바이트)를 생성합니다. 키 확장에 사용되는 중간 배열 `w: [u32; 60]`은 라운드 키 추출 직후 `write_volatile`로 소거됩니다. `KeySchedule` 구조체는 `Drop` 트레이트를 구현하여, 스코프 이탈 시 240바이트 라운드 키 전체를 자동으로 강제 소거합니다.
+
+```rust
+impl Drop for KeySchedule {
+ fn drop(&mut self) {
+ for rk in &mut self.round_keys {
+ for b in rk {
+ unsafe { write_volatile(b, 0) };
+ }
+ }
+ }
+}
+```
+
+## AES-256-GCM
+
+NIST SP 800-38D §7.1에 따른 구현입니다. 96비트(12 bytes) nonce만 지원합니다. 임의 길이의 IV를 허용하는 일반화 경로(GHASH를 이용한 IV 파생)는 nonce 충돌 위험을 증가시키므로 의도적으로 제외하였습니다.
+
+### 내부 동작
+
+1. **해시 부키 생성**
+ - $`H = E_K(0^{128})`$
+2. **초기 카운터 블록**
+ - $`J_0 = \text{nonce}_{96} \| \texttt{0x00000001}_{32}`$
+3. **암호화 (GCTR)**
+ - $`C = \text{GCTR}_K(\text{inc}_{32}(J_0),\ P)`$
+ - $`\text{inc}_{32}`$는 하위 32비트를 빅엔디안으로 1 증가시킵니다.
+4. **인증 태그**
+ - $`T = E_K(J_0) \oplus \text{GHASH}_H(A,\ C)`$
+
+여기서 GHASH는 AAD, 암호문, 길이 블록 $`[\text{len}(A)]_{64} \| [\text{len}(C)]_{64}`$를 순서대로 처리합니다.
+
+### GHASH: GF(2^128) 곱셈 — 상수-시간 보장
+
+GCM 인증은 $\text{GF}(2^{128})$ 위에서 이루어집니다. 환원 다항식은 $f(x) = x^{128} + x^7 + x^2 + x + 1$이며, 이는 비트열 `0xE1000...0` (128비트, MSB 우선)으로 표현됩니다.
+
+> [!NOTE]
+> **상수-시간 GF(2^128) 곱셈**: NIST SP 800-38D Algorithm 1의 표준 구현은 비밀 값에 의존하는 조건 분기를 포함합니다. 본 구현은 고정 128회 반복과 비트 마스크 트릭으로 이를 제거합니다.
+>
+> 각 반복에서 $X$의 $i$번째 비트 $X_i$를 마스크로 변환하여 분기 없이 누산합니다.
+>
+> $$\text{mask} = -(X_i), \quad Z \mathrel{⊕}= V \land \text{mask}$$
+>
+> $V$의 우측 시프트 후 조건부 환원도 동일한 방식으로 처리됩니다.
+>
+> ```math
+> \text{lsb\_mask} = -(V_{127}), \quad V_{\text{high}} \mathrel{⊕}= \texttt{0xE100...00} \land \text{lsb\_mask}
+> ```
+
+`GHashState`는 `Drop` 트레이트를 구현하여, 내부 상태 $Z$와 해시 부키 $H$를 `write_volatile`로 소거합니다.
+
+### 복호화 검증 원칙
+
+복호화 시 태그를 먼저 재계산하고, `ConstantTimeEq::ct_eq()`를 사용하여 16바이트를 상수-시간으로 비교합니다. 검증에 실패하면 `AESError::AuthenticationFailed`를 반환하고 평문 출력을 일체 수행하지 않습니다.
+
+```rust
+// 상수-시간 16바이트 비교
+let mut r = 0xFFu8;
+for i in 0..16 {
+r &= expected_tag[i].ct_eq(&received_tag[i]).unwrap_u8();
+}
+if r != 0xFF { return Err(AESError::AuthenticationFailed); }
+// 검증 통과 후에만 복호화 수행
+```
+
+### API
+
+```rust
+AES256GCM::encrypt(
+key: &SecureBuffer, // 256비트 AES 키
+nonce: &[u8; 12], // 96비트 nonce (반드시 유일해야 함)
+aad: &[u8], // 추가 인증 데이터
+plaintext: &[u8],
+ciphertext_out: &mut [u8], // plaintext.len() bytes
+tag_out: &mut [u8; 16], // 인증 태그 출력
+) -> Result<(), AESError>
+
+AES256GCM::decrypt(
+key: &SecureBuffer,
+nonce: &[u8; 12],
+aad: &[u8],
+ciphertext: &[u8],
+tag: &[u8; 16], // 수신한 인증 태그
+plaintext_out: &mut [u8], // ciphertext.len() bytes
+) -> Result<(), AESError> // 태그 불일치 시 AuthenticationFailed
+```
+
+> [!WARNING]
+> 동일한 `(key, nonce)` 쌍을 두 번 이상 사용하면 기밀성과 무결성이 모두 파괴됩니다. nonce는 `entlib-native-rng`의 `HashDRBGSHA256`를 통해 생성하거나, 단조 증가 카운터로 관리하십시오.
+
+## AES-256-CBC-HMAC-SHA256
+
+NIST SP 800-38A의 CBC 모드 단독 사용은 기밀성만 보장하고 무결성을 제공하지 않습니다. 본 구현은 **Encrypt-then-MAC** 구성을 강제합니다. 암호화 후 `IV || 암호문`에 HMAC-SHA256 태그를 생성하여 출력에 부착합니다.
+
+### 출력 형식
+
+```
+┌─────────────────┬────────────────────────────────────────┬───────────────────────────────┐
+│ IV (16 B) │ Ciphertext + PKCS7 Padding (N×16 B) │ HMAC-SHA256(IV||CT) (32 B) │
+└─────────────────┴────────────────────────────────────────┴───────────────────────────────┘
+```
+
+PKCS7 패딩은 항상 추가됩니다. 평문이 블록 경계에 정확히 맞아도 16바이트(`0x10` × 16)의 완전한 패딩 블록이 추가되므로, 출력 암호문의 길이는 항상 $`\lceil P / 16 \rceil + 1`$블록입니다.
+
+> [!NOTE]
+> **PKCS7 상수-시간 검증**: 복호화 시 패딩 바이트 검증은 XOR와 비트 마스크로 수행합니다.
+>
+> ```math
+> \begin{align}
+> \text{diff}_i &= \text{data}[i] \oplus \text{pad\_byte}, \quad \text{not\_zero}_i = \frac{\text{diff}_i \mathbin{|} (-\text{diff}_i)}{2^7} \\
+> \text{valid} &= \bigwedge_{i} \overline{(\text{not\_zero}_i - 1)} \quad (\text{0xFF이면 유효})
+> \end{align}
+>```
+>
+> MAC 검증 통과 후에만 패딩 검증이 수행되므로, 공격자가 유효한 MAC 없이 패딩 오라클을 이용하는 것은 불가능합니다.
+
+### 복호화 순서
+
+1. 입력 형식 검증 (최소 64바이트, 블록 크기 정렬)
+2. HMAC-SHA256 재계산 → `ct_eq_32`로 상수-시간 비교 (`AESError::AuthenticationFailed` 또는 통과)
+3. MAC 검증 통과 후에만 AES-256-CBC 복호화 수행
+4. PKCS7 패딩 검증 및 제거
+
+### API
+
+```rust
+AES256CBCHmac::encrypt(
+ enc_key: &SecureBuffer, // 256비트 AES 암호화 키
+ mac_key: &SecureBuffer, // HMAC-SHA256 키 (최소 14 bytes, 권장 32 bytes)
+ iv: &[u8; 16], // 128비트 IV (메시지마다 고유해야 함)
+ plaintext: &[u8],
+ output: &mut [u8], // 최소 cbc_output_len(plaintext.len()) bytes
+) -> Result // 출력에 쓰인 바이트 수
+
+AES256CBCHmac::decrypt(
+ enc_key: &SecureBuffer,
+ mac_key: &SecureBuffer,
+ input: &[u8], // IV(16) || CT || HMAC(32) 형식
+ output: &mut [u8],
+) -> Result // 복호화된 평문 바이트 수
+
+// 버퍼 크기 계산 헬퍼
+cbc_output_len(plaintext_len: usize) -> usize
+cbc_plaintext_max_len(input_len: usize) -> Option
+```
+
+> [!IMPORTANT]
+> `enc_key`와 `mac_key`는 반드시 독립적인 별개의 키를 사용해야 합니다. 동일한 키를 두 용도에 재사용하면 암호화 스킴의 안전성 증명이 무효가 됩니다. 키 파생이 필요한 경우 `entlib-native-hkdf`를 사용하여 마스터 키로부터 두 개의 독립적인 서브키를 파생하십시오.
+
+## 키 관리 요구사항
+
+| 파라미터 | 요구사항 | 근거 |
+|-----------|------------------------------|-----------------------------|
+| AES 키 | 정확히 256비트 (32 bytes) | FIPS 140-3, NIST SP 800-38D |
+| GCM nonce | 96비트 (12 bytes), 유일 | NIST SP 800-38D §8.2 |
+| CBC IV | 128비트 (16 bytes), 각 메시지마다 고유 | NIST SP 800-38A §6.2 |
+| CBC MAC 키 | AES 키와 독립, 최소 112비트 | NIST SP 800-107r1 |
+
+모든 키는 반드시 `entlib-native-secure-buffer`의 `SecureBuffer`로 관리하여 mlock 기반 메모리 잠금과 Drop 시 자동 소거를 보장해야 합니다.
+
+## 검증
+
+### NIST CAVP 테스트 벡터
+
+| 테스트 | 출처 | 결과 |
+|--------------------|---------------------------------------|----|
+| AES-256 ECB 블록 암호화 | NIST FIPS 197 Appendix B | O |
+| AES-256-GCM 암호화 | NIST CAVP (OpenSSL 교차 검증) | O |
+| AES-256-GCM 복호화 | 역방향 라운드트립 | O |
+| AES-256-CBC 암호문 | NIST SP 800-38A F.2.5 (OpenSSL 교차 검증) | O |
+| GCM 태그 1비트 변조 | 조작된 태그 → `AuthenticationFailed` | O |
+| CBC MAC 1비트 변조 | 조작된 MAC → `AuthenticationFailed` | O |
+
+```bash
+cargo test -p entlib-native-aes
+```
+
+> [!WARNING]
+> KAT(Known Answer Test) 테스트 벡터를 엄밀하게 통과하기 위한 준비 중에 있습니다.
+>
+> 위 표의 근거는 테스트 벡터의 개별 테스트 블럭의 일치 여부를 검증하는 테스트 모듈 `aes_test.rs`입니다.
+
+## 설계 원칙 요약
+
+1. **256비트 단일 키 강제** — 키 크기 선택 오류로 인한 보안 약화를 API 수준에서 차단합니다.
+2. **룩업 테이블 완전 배제** — SBox를 포함한 모든 연산이 순수 산술 비트 연산으로 수행되어 캐시 타이밍 채널이 존재하지 않습니다.
+3. **고정 반복 횟수** — `gmul`(8회), `gf128_mul`(128회) 등 모든 내부 루프는 비밀 데이터와 무관한 상수로 고정됩니다.
+4. **Encrypt-then-MAC 강제** — CBC 단독 사용 API를 노출하지 않아 패딩 오라클 공격을 구조적으로 차단합니다.
+5. **검증 후 복호화 원칙** — GCM 태그와 CBC HMAC 모두 상수-시간 검증 통과 전에 평문을 출력하지 않습니다.
+6. **키 소재 즉시 소거** — `KeySchedule`, `GHashState`, 블록 연산 중간값 모두 `write_volatile`로 사용 직후 소거됩니다.
diff --git a/crypto/aes/README_EN.md b/crypto/aes/README_EN.md
new file mode 100644
index 0000000..22f9f3d
--- /dev/null
+++ b/crypto/aes/README_EN.md
@@ -0,0 +1,272 @@
+# AES-256 Crate (entlib-native-aes)
+
+> Q. T. Felix (Modified: 26.03.22 UTC+9)
+>
+> [Korean README](README.md)
+
+`entlib-native-aes` is an AES-256 encryption module designed to meet the requirements of NIST FIPS 140-3 and Common Criteria EAL4+ certification. It **only supports 256-bit keys** and implements two approved modes of operation that provide both confidentiality and integrity.
+
+- **AES-256-GCM** — AEAD (Authenticated Encryption with Associated Data) compliant with NIST SP 800-38D
+- **AES-256-CBC-HMAC-SHA256** — Encrypt-then-MAC configuration with NIST SP 800-38A (CBC is not used alone)
+
+**This algorithm intentionally does not support 128 and 192 key lengths.** FIPS 140-3 recommends the use of AES-256, and by exposing only a single key size, it prevents security weaknesses caused by incorrect key length selection in advance.
+
+## Security Threat Model
+
+### Cache-Timing Attack
+
+Standard software implementations of AES use a 256-byte S-box lookup table for the SubBytes operation. This approach has a fatal vulnerability. An attacker in an environment that shares the same CPU cache (VPS, cloud) can statistically recover the accessed table index, i.e., the secret key byte, from the cache hit/miss pattern. Daniel Bernstein's (D. J. Bernstein) 2005 AES timing attack demonstrated this empirically.
+
+This crate does not use any lookup tables. SubBytes performs the GF(2^8) inverse calculation and affine transformation as pure arithmetic bit operations, and the execution time of all operations is completely independent of the secret key and plaintext values.
+
+### Padding Oracle Attack
+
+In CBC mode, if the decryption error response depends on the validity of the padding, an attacker can completely decrypt an arbitrary ciphertext with an adaptive chosen-ciphertext attack (ACCA) (POODLE, Lucky 13 variants). This implementation fundamentally blocks this attack vector by forcing the **Encrypt-then-MAC** configuration. MAC verification is performed first, and if the MAC fails, the decryption operation itself is not performed.
+
+### GCM Nonce Reuse
+
+In GCM, if the same (key, nonce) pair is used even twice, the XOR of the plaintexts is exposed from the XOR of the two ciphertexts, completely breaking confidentiality. Furthermore, the authentication key H is recovered by solving the GHASH polynomial equation, which also threatens integrity. This crate delegates the nonce generation policy to the caller and attaches an explicit warning to the API documentation. In a production environment, generate the nonce with `HashDRBGSHA256` from `entlib-native-rng` or use a counter-based configuration that guarantees no collisions.
+
+## Security Core: Constant-Time AES Core
+
+### GF(2^8) Arithmetic
+
+AES SubBytes calculates the inverse on the finite field GF(2^8) = GF(2)[x] / (x^8 + x^4 + x^3 + x + 1) and then applies an affine transformation.
+
+#### xtime: Multiply by x in GF(2^8)
+
+$$ \text{xtime}(a) = \begin{cases} a \ll 1 & \text{if MSB}(a) = 0 \\ (a \ll 1) \oplus \texttt{0x1b} & \text{if MSB}(a) = 1 \end{cases} $$
+
+Implemented without a branch statement.
+
+$$\text{mask} = -(a \gg 7), \quad \text{xtime}(a) = (a \ll 1) \oplus (\texttt{0x1b} \land \text{mask})$$
+
+Since `mask` is `0xFF` if the MSB is 1 and `0x00` if it is 0, it is compiled into four instructions: a single `SHR`, `NEG`, `AND`, and `XOR`.
+
+#### gmul: GF(2^8) Multiplication — Fixed 8 Iterations
+
+$$\text{gmul}(a, b) = \bigoplus_{i=0}^{7} \left( a \cdot x^i \land -(b_i) \right)$$
+
+Here, $b_i$ is the $i$-th bit of $b$. It performs a conditional XOR without branching by converting the bit to a mask with `-(b & 1).wrapping_neg()`. The number of iterations is a fixed value of 8, which is independent of the secret data, so the timing is constant.
+
+#### gf_inv: GF(2^8) Inverse — Fermat's Little Theorem
+
+In a finite field, if $a \ne 0$, then $a^{-1} = a^{2^8 - 2} = a^{254}$. If $a = 0$, then $0^{254} = 0$ is naturally returned, so no branch is needed.
+
+> [!NOTE]
+> **Square-and-Multiply Expansion**: Since $254 = \texttt{11111110}_2$,
+>
+> $$a^{254} = a^{128} \cdot a^{64} \cdot a^{32} \cdot a^{16} \cdot a^8 \cdot a^4 \cdot a^2$$
+>
+> It is calculated with a total of 13 `gmul` calls, with 7 squarings and 6 multiplications. Since there is no table access at all, there is no cache timing channel.
+
+#### sub_byte: SubBytes Affine Transformation
+
+Applies the affine transformation $M \cdot a^{-1} + c$ to the inverse $a^{-1}$.
+
+$$b_i = a^{-1}_i \oplus a^{-1}_{(i+4) \bmod 8} \oplus a^{-1}_{(i+5) \bmod 8} \oplus a^{-1}_{(i+6) \bmod 8} \oplus a^{-1}_{(i+7) \bmod 8} \oplus c_i$$
+
+Expressed equivalently by bit rotation ($c = \texttt{0x63}$).
+
+```math
+\text{sub\_byte}(a) = a^{-1} \oplus \text{ROL}(a^{-1}, 1) \oplus \text{ROL}(a^{-1}, 2) \oplus \text{ROL}(a^{-1}, 3) \oplus \text{ROL}(a^{-1}, 4) \oplus \texttt{0x63}
+```
+
+Inverse SubBytes (`inv_sub_byte`) calculates the inverse after the inverse affine transformation.
+
+```math
+\text{inv\_sub\_byte}(a) = \text{gf\_inv}\!\left(\text{ROL}(a,1) \oplus \text{ROL}(a,3) \oplus \text{ROL}(a,6) \oplus \texttt{0x05}\right)
+```
+
+### Key Schedule
+
+AES-256 generates 15 round keys (16 bytes each) from a 32-byte master key. The intermediate array `w: [u32; 60]` used for key expansion is erased with `write_volatile` immediately after the round keys are extracted. The `KeySchedule` struct implements the `Drop` trait to automatically force the erasure of the entire 240-byte round key when it goes out of scope.
+
+```rust
+impl Drop for KeySchedule {
+ fn drop(&mut self) {
+ for rk in &mut self.round_keys {
+ for b in rk {
+ unsafe { write_volatile(b, 0) };
+ }
+ }
+ }
+}
+```
+
+## AES-256-GCM
+
+Implementation according to NIST SP 800-38D §7.1. Only 96-bit (12 bytes) nonces are supported. The generalized path that allows for arbitrary length IVs (IV derivation using GHASH) is intentionally excluded as it increases the risk of nonce collisions.
+
+### Internal Operation
+
+1. **Hash Subkey Generation**
+ - $H = E_K(0^{128})$
+2. **Initial Counter Block**
+ - $J_0 = \text{nonce}_{96} \| \texttt{0x00000001}_{32}$
+3. **Encryption (GCTR)**
+ - $C = \text{GCTR}_K(\text{inc}_{32}(J_0),\ P)$
+ - $\text{inc}_{32}$ increments the lower 32 bits by 1 in big-endian.
+4. **Authentication Tag**
+ - $T = E_K(J_0) \oplus \text{GHASH}_H(A,\ C)$
+
+Here, GHASH processes the AAD, the ciphertext, and the length block $[\text{len}(A)]_{64} \| [\text{len}(C)]_{64}$ in order.
+
+### GHASH: GF(2^128) Multiplication — Constant-Time Guarantee
+
+GCM authentication is performed over $\text{GF}(2^{128})$. The reduction polynomial is $f(x) = x^{128} + x^7 + x^2 + x + 1$, which is represented by the bit string `0xE1000...0` (128 bits, MSB first).
+
+> [!NOTE]
+> **Constant-Time GF(2^128) Multiplication**: The standard implementation of NIST SP 800-38D Algorithm 1 includes a conditional branch that depends on a secret value. This implementation removes it with a fixed 128 iterations and a bit mask trick.
+>
+> In each iteration, the $i$-th bit of $X$, $X_i$, is converted to a mask to accumulate without branching.
+>
+> $$\text{mask} = -(X_i), \quad Z \mathrel{⊕}= V \land \text{mask}$$
+>
+> The conditional reduction after the right shift of $V$ is also handled in the same way.
+>
+> ```math
+> \text{lsb\_mask} = -(V_{127}), \quad V_{\text{high}} \mathrel{⊕}= \texttt{0xE100...00} \land \text{lsb\_mask}
+> ```
+
+`GHashState` implements the `Drop` trait to erase the internal state $Z$ and the hash subkey $H$ with `write_volatile`.
+
+### Decryption Verification Principle
+
+When decrypting, the tag is first recalculated, and the 16 bytes are compared in constant time using `ConstantTimeEq::ct_eq()`. If the verification fails, `AESError::AuthenticationFailed` is returned and no plaintext is output at all.
+
+```rust
+// Constant-time 16-byte comparison
+let mut r = 0xFFu8;
+for i in 0..16 {
+r &= expected_tag[i].ct_eq(&received_tag[i]).unwrap_u8();
+}
+if r != 0xFF { return Err(AESError::AuthenticationFailed); }
+// Decryption is performed only after verification passes
+```
+
+### API
+
+```rust
+AES256GCM::encrypt(
+key: &SecureBuffer, // 256-bit AES key
+nonce: &[u8; 12], // 96-bit nonce (must be unique)
+aad: &[u8], // Additional authenticated data
+plaintext: &[u8],
+ciphertext_out: &mut [u8], // plaintext.len() bytes
+tag_out: &mut [u8; 16], // Authentication tag output
+) -> Result<(), AESError>
+
+AES256GCM::decrypt(
+key: &SecureBuffer,
+nonce: &[u8; 12],
+aad: &[u8],
+ciphertext: &[u8],
+tag: &[u8; 16], // Received authentication tag
+plaintext_out: &mut [u8], // ciphertext.len() bytes
+) -> Result<(), AESError> // AuthenticationFailed if tag does not match
+```
+
+> [!WARNING]
+> Using the same `(key, nonce)` pair more than once will destroy both confidentiality and integrity. Generate the nonce via `HashDRBGSHA256` from `entlib-native-rng` or manage it with a monotonically increasing counter.
+
+## AES-256-CBC-HMAC-SHA256
+
+The use of CBC mode alone in NIST SP 800-38A only guarantees confidentiality and does not provide integrity. This implementation forces the **Encrypt-then-MAC** configuration. After encryption, an HMAC-SHA256 tag is generated for `IV || ciphertext` and attached to the output.
+
+### Output Format
+
+```
+┌─────────────────┬────────────────────────────────────────┬───────────────────────────────┐
+│ IV (16 B) │ Ciphertext + PKCS7 Padding (N×16 B) │ HMAC-SHA256(IV||CT) (32 B) │
+└─────────────────┴────────────────────────────────────────┴───────────────────────────────┘
+```
+
+PKCS7 padding is always added. Even if the plaintext fits exactly on a block boundary, a full padding block of 16 bytes (`0x10` × 16) is added, so the length of the output ciphertext is always $\lceil P / 16 \rceil + 1$ blocks.
+
+> [!NOTE]
+> **PKCS7 Constant-Time Verification**: When decrypting, the padding byte verification is performed with XOR and a bit mask.
+>
+> ```math
+> \begin{align}
+> \text{diff}_i &= \text{data}[i] \oplus \text{pad\_byte}, \quad \text{not\_zero}_i = \frac{\text{diff}_i \mathbin{|} (-\text{diff}_i)}{2^7} \\
+> \text{valid} &= \bigwedge_{i} \overline{(\text{not\_zero}_i - 1)} \quad (\text{0xFF if valid})
+> \end{align}
+>```
+>
+> Since padding verification is performed only after MAC verification passes, it is impossible for an attacker to use a padding oracle without a valid MAC.
+
+### Decryption Order
+
+1. Verify input format (minimum 64 bytes, block size aligned)
+2. Recalculate HMAC-SHA256 → constant-time comparison with `ct_eq_32` (`AESError::AuthenticationFailed` or pass)
+3. Perform AES-256-CBC decryption only after MAC verification passes
+4. Verify and remove PKCS7 padding
+
+### API
+
+```rust
+AES256CBCHmac::encrypt(
+ enc_key: &SecureBuffer, // 256-bit AES encryption key
+ mac_key: &SecureBuffer, // HMAC-SHA256 key (minimum 14 bytes, recommended 32 bytes)
+ iv: &[u8; 16], // 128-bit IV (must be unique for each message)
+ plaintext: &[u8],
+ output: &mut [u8], // minimum cbc_output_len(plaintext.len()) bytes
+) -> Result // Number of bytes written to output
+
+AES256CBCHmac::decrypt(
+ enc_key: &SecureBuffer,
+ mac_key: &SecureBuffer,
+ input: &[u8], // IV(16) || CT || HMAC(32) format
+ output: &mut [u8],
+) -> Result // Number of decrypted plaintext bytes
+
+// Buffer size calculation helpers
+cbc_output_len(plaintext_len: usize) -> usize
+cbc_plaintext_max_len(input_len: usize) -> Option
+```
+
+> [!IMPORTANT]
+> `enc_key` and `mac_key` must be independent and separate keys. Reusing the same key for both purposes invalidates the security proof of the cryptographic scheme. If key derivation is required, use `entlib-native-hkdf` to derive two independent subkeys from a master key.
+
+## Key Management Requirements
+
+| Parameter | Requirement | Rationale |
+|-----------|------------------------------|-----------------------------|
+| AES Key | Exactly 256 bits (32 bytes) | FIPS 140-3, NIST SP 800-38D |
+| GCM nonce | 96 bits (12 bytes), unique | NIST SP 800-38D §8.2 |
+| CBC IV | 128 bits (16 bytes), unique for each message | NIST SP 800-38A §6.2 |
+| CBC MAC Key | Independent of AES key, minimum 112 bits | NIST SP 800-107r1 |
+
+All keys must be managed with `SecureBuffer` from `entlib-native-secure-buffer` to ensure mlock-based memory locking and automatic erasure on Drop.
+
+## Verification
+
+### NIST CAVP Test Vectors
+
+| Test | Source | Result |
+|--------------------|---------------------------------------|----|
+| AES-256 ECB Block Encryption | NIST FIPS 197 Appendix B | O |
+| AES-256-GCM Encryption | NIST CAVP (OpenSSL cross-validation) | O |
+| AES-256-GCM Decryption | Reverse roundtrip | O |
+| AES-256-CBC Ciphertext | NIST SP 800-38A F.2.5 (OpenSSL cross-validation) | O |
+| GCM Tag 1-bit Tampering | Manipulated tag → `AuthenticationFailed` | O |
+| CBC MAC 1-bit Tampering | Manipulated MAC → `AuthenticationFailed` | O |
+
+```bash
+cargo test -p entlib-native-aes
+```
+
+> [!WARNING]
+> We are in the process of preparing to strictly pass the KAT (Known Answer Test) test vectors.
+>
+> The basis for the table above is the `aes_test.rs` test module, which verifies the matching of individual test blocks of the test vectors.
+
+## Summary of Design Principles
+
+1. **Force 256-bit single key** — Blocks security weaknesses due to key size selection errors at the API level.
+2. **Complete exclusion of lookup tables** — All operations, including the S-box, are performed as pure arithmetic bit operations, so there is no cache timing channel.
+3. **Fixed number of iterations** — All internal loops, such as `gmul` (8 times) and `gf128_mul` (128 times), are fixed to constants that are independent of the secret data.
+4. **Force Encrypt-then-MAC** — Structurally blocks padding oracle attacks by not exposing a CBC-only API.
+5. **Decrypt after verification principle** — Does not output plaintext before passing the constant-time verification of both the GCM tag and the CBC HMAC.
+6. **Immediate erasure of key material** — `KeySchedule`, `GHashState`, and block operation intermediate values are all erased immediately after use with `write_volatile`.
diff --git a/crypto/aes/src/aes.rs b/crypto/aes/src/aes.rs
new file mode 100644
index 0000000..63a9453
--- /dev/null
+++ b/crypto/aes/src/aes.rs
@@ -0,0 +1,292 @@
+//! AES-256 블록 암호 코어 모듈입니다.
+//! 룩업 테이블 없이 GF(2^8) 산술 연산만으로 구현하여 캐시-타이밍 부채널 공격을 차단합니다.
+//!
+//! # Examples
+//! ```rust
+//! use entlib_native_aes::{AES256GCM, GCM_NONCE_LEN, GCM_TAG_LEN};
+//! use entlib_native_secure_buffer::SecureBuffer;
+//!
+//! let mut key = SecureBuffer::new_owned(32).unwrap();
+//! key.as_mut_slice().copy_from_slice(&[0u8; 32]);
+//! let nonce = [0u8; GCM_NONCE_LEN];
+//! let plaintext = b"hello world";
+//! let mut ct = vec![0u8; plaintext.len()];
+//! let mut tag = [0u8; GCM_TAG_LEN];
+//! AES256GCM::encrypt(&key, &nonce, &[], plaintext, &mut ct, &mut tag).unwrap();
+//! ```
+
+use core::ptr::write_volatile;
+
+pub type Block = [u8; 16];
+
+// AES-256 field 연산: x^8 + x^4 + x^3 + x + 1 (0x11b)
+#[inline(always)]
+fn xtime(a: u8) -> u8 {
+ let mask = (a >> 7).wrapping_neg();
+ (a << 1) ^ (0x1b & mask)
+}
+
+// GF(2^8) 곱셈 — 고정 8회 반복, 분기 없음
+#[inline(always)]
+fn gmul(mut a: u8, mut b: u8) -> u8 {
+ let mut p = 0u8;
+ for _ in 0..8 {
+ let mask = (b & 1).wrapping_neg();
+ p ^= a & mask;
+ let hi = (a >> 7).wrapping_neg();
+ a = (a << 1) ^ (0x1b & hi);
+ b >>= 1;
+ }
+ p
+}
+
+// GF(2^8) 역원: a^254 = a^(11111110b), a=0이면 0 반환 (분기 없음)
+#[inline(always)]
+fn gf_inv(a: u8) -> u8 {
+ let a2 = gmul(a, a);
+ let a4 = gmul(a2, a2);
+ let a8 = gmul(a4, a4);
+ let a16 = gmul(a8, a8);
+ let a32 = gmul(a16, a16);
+ let a64 = gmul(a32, a32);
+ let a128 = gmul(a64, a64);
+ gmul(
+ gmul(gmul(gmul(gmul(gmul(a128, a64), a32), a16), a8), a4),
+ a2,
+ )
+}
+
+/// AES SubBytes 바이트 치환 함수입니다.
+/// GF(2^8) 역원(a^254) 계산 후 아핀 변환을 적용하여 S-Box 출력을 반환합니다.
+#[inline(always)]
+pub fn sub_byte(a: u8) -> u8 {
+ let inv = gf_inv(a);
+ inv ^ inv.rotate_left(1) ^ inv.rotate_left(2) ^ inv.rotate_left(3) ^ inv.rotate_left(4) ^ 0x63
+}
+
+// InvSubBytes: 역 아핀 후 역원
+#[inline(always)]
+fn inv_sub_byte(a: u8) -> u8 {
+ let t = a.rotate_left(1) ^ a.rotate_left(3) ^ a.rotate_left(6) ^ 0x05;
+ gf_inv(t)
+}
+
+#[inline(always)]
+fn sub_word(w: u32) -> u32 {
+ let b = w.to_be_bytes();
+ u32::from_be_bytes([
+ sub_byte(b[0]),
+ sub_byte(b[1]),
+ sub_byte(b[2]),
+ sub_byte(b[3]),
+ ])
+}
+
+fn sub_bytes(state: &mut Block) {
+ for b in state.iter_mut() {
+ *b = sub_byte(*b);
+ }
+}
+
+fn inv_sub_bytes(state: &mut Block) {
+ for b in state.iter_mut() {
+ *b = inv_sub_byte(*b);
+ }
+}
+
+// 상태: 열 우선(column-major). state[col*4 + row]
+// ShiftRows: row r을 왼쪽으로 r칸 회전
+fn shift_rows(state: &mut Block) {
+ let t = state[1];
+ state[1] = state[5];
+ state[5] = state[9];
+ state[9] = state[13];
+ state[13] = t;
+
+ state.swap(2, 10);
+ state.swap(6, 14);
+
+ let t = state[15];
+ state[15] = state[11];
+ state[11] = state[7];
+ state[7] = state[3];
+ state[3] = t;
+}
+
+fn inv_shift_rows(state: &mut Block) {
+ let t = state[13];
+ state[13] = state[9];
+ state[9] = state[5];
+ state[5] = state[1];
+ state[1] = t;
+
+ state.swap(2, 10);
+ state.swap(6, 14);
+
+ let t = state[3];
+ state[3] = state[7];
+ state[7] = state[11];
+ state[11] = state[15];
+ state[15] = t;
+}
+
+// MixColumns: [2 3 1 1 / 1 2 3 1 / 1 1 2 3 / 3 1 1 2] × column
+fn mix_columns(state: &mut Block) {
+ for col in 0..4 {
+ let b = col * 4;
+ let (s0, s1, s2, s3) = (state[b], state[b + 1], state[b + 2], state[b + 3]);
+ state[b] = xtime(s0) ^ (xtime(s1) ^ s1) ^ s2 ^ s3;
+ state[b + 1] = s0 ^ xtime(s1) ^ (xtime(s2) ^ s2) ^ s3;
+ state[b + 2] = s0 ^ s1 ^ xtime(s2) ^ (xtime(s3) ^ s3);
+ state[b + 3] = (xtime(s0) ^ s0) ^ s1 ^ s2 ^ xtime(s3);
+ }
+}
+
+// InvMixColumns: [14 11 13 9 / 9 14 11 13 / 13 9 14 11 / 11 13 9 14] × column
+fn inv_mix_columns(state: &mut Block) {
+ #[inline(always)]
+ fn m9(a: u8) -> u8 {
+ xtime(xtime(xtime(a))) ^ a
+ }
+ #[inline(always)]
+ fn m11(a: u8) -> u8 {
+ xtime(xtime(xtime(a))) ^ xtime(a) ^ a
+ }
+ #[inline(always)]
+ fn m13(a: u8) -> u8 {
+ xtime(xtime(xtime(a))) ^ xtime(xtime(a)) ^ a
+ }
+ #[inline(always)]
+ fn m14(a: u8) -> u8 {
+ xtime(xtime(xtime(a))) ^ xtime(xtime(a)) ^ xtime(a)
+ }
+
+ for col in 0..4 {
+ let b = col * 4;
+ let (s0, s1, s2, s3) = (state[b], state[b + 1], state[b + 2], state[b + 3]);
+ state[b] = m14(s0) ^ m11(s1) ^ m13(s2) ^ m9(s3);
+ state[b + 1] = m9(s0) ^ m14(s1) ^ m11(s2) ^ m13(s3);
+ state[b + 2] = m13(s0) ^ m9(s1) ^ m14(s2) ^ m11(s3);
+ state[b + 3] = m11(s0) ^ m13(s1) ^ m9(s2) ^ m14(s3);
+ }
+}
+
+#[inline(always)]
+fn add_round_key(state: &mut Block, rk: &Block) {
+ for i in 0..16 {
+ state[i] ^= rk[i];
+ }
+}
+
+// AES-256 Rcon: i/8 = 1..7 → index 0..6
+const RCON: [u32; 7] = [
+ 0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, 0x40000000,
+];
+
+/// AES-256 키 스케줄 구조체입니다.
+/// 256비트 키로부터 15개의 라운드 키를 파생하며, `Drop` 시 모든 라운드 키를 소거합니다.
+pub struct KeySchedule {
+ pub round_keys: [Block; 15],
+}
+
+impl KeySchedule {
+ /// AES-256 키 스케줄을 생성하는 함수입니다.
+ ///
+ /// # Arguments
+ /// `key` — 256비트(32 bytes) AES 키
+ pub fn new(key: &[u8; 32]) -> Self {
+ let mut w = [0u32; 60];
+
+ for i in 0..8 {
+ w[i] = u32::from_be_bytes([key[i * 4], key[i * 4 + 1], key[i * 4 + 2], key[i * 4 + 3]]);
+ }
+
+ for i in 8..60 {
+ let mut temp = w[i - 1];
+ if i % 8 == 0 {
+ temp = sub_word(temp.rotate_left(8)) ^ RCON[i / 8 - 1];
+ } else if i % 8 == 4 {
+ temp = sub_word(temp);
+ }
+ w[i] = w[i - 8] ^ temp;
+ }
+
+ let mut round_keys = [[0u8; 16]; 15];
+ for rk in 0..15 {
+ for j in 0..4 {
+ let bytes = w[rk * 4 + j].to_be_bytes();
+ round_keys[rk][j * 4] = bytes[0];
+ round_keys[rk][j * 4 + 1] = bytes[1];
+ round_keys[rk][j * 4 + 2] = bytes[2];
+ round_keys[rk][j * 4 + 3] = bytes[3];
+ }
+ }
+
+ // w에 잔존하는 키 파생 중간값 소거
+ for word in &mut w {
+ unsafe { write_volatile(word, 0) };
+ }
+
+ Self { round_keys }
+ }
+}
+
+impl Drop for KeySchedule {
+ fn drop(&mut self) {
+ for rk in &mut self.round_keys {
+ for b in rk {
+ unsafe { write_volatile(b, 0) };
+ }
+ }
+ }
+}
+
+/// AES-256 블록 암호화 함수입니다.
+/// 14라운드 순방향 암호(SubBytes → ShiftRows → MixColumns → AddRoundKey)를 수행합니다.
+///
+/// # Arguments
+/// - `state` — 입출력 16바이트 블록 (in-place)
+/// - `ks` — 사전 생성된 키 스케줄
+pub fn aes256_encrypt_block(state: &mut Block, ks: &KeySchedule) {
+ add_round_key(state, &ks.round_keys[0]);
+ for round in 1..14 {
+ sub_bytes(state);
+ shift_rows(state);
+ mix_columns(state);
+ add_round_key(state, &ks.round_keys[round]);
+ }
+ sub_bytes(state);
+ shift_rows(state);
+ add_round_key(state, &ks.round_keys[14]);
+}
+
+/// 단일 블록 ECB 암호화 함수입니다. KAT(Known Answer Test) 전용입니다.
+///
+/// # Security Note
+/// ECB 모드는 패턴을 보존하므로 실제 암호화에 사용할 수 없습니다.
+#[cfg_attr(not(test), allow(dead_code))]
+pub fn aes256_encrypt_ecb(key: &[u8; 32], plaintext: &[u8; 16]) -> Block {
+ let ks = KeySchedule::new(key);
+ let mut state = *plaintext;
+ aes256_encrypt_block(&mut state, &ks);
+ state
+}
+
+/// AES-256 블록 복호화 함수입니다.
+/// 14라운드 역방향 암호(InvShiftRows → InvSubBytes → AddRoundKey → InvMixColumns)를 수행합니다.
+///
+/// # Arguments
+/// - `state` — 입출력 16바이트 블록 (in-place)
+/// - `ks` — 사전 생성된 키 스케줄
+pub fn aes256_decrypt_block(state: &mut Block, ks: &KeySchedule) {
+ add_round_key(state, &ks.round_keys[14]);
+ for round in (1..14).rev() {
+ inv_shift_rows(state);
+ inv_sub_bytes(state);
+ add_round_key(state, &ks.round_keys[round]);
+ inv_mix_columns(state);
+ }
+ inv_shift_rows(state);
+ inv_sub_bytes(state);
+ add_round_key(state, &ks.round_keys[0]);
+}
diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs
new file mode 100644
index 0000000..52e648c
--- /dev/null
+++ b/crypto/aes/src/cbc.rs
@@ -0,0 +1,251 @@
+//! AES-256-CBC-HMAC-SHA256 모듈입니다.
+//! PKCS7 패딩, Encrypt-then-MAC 방식으로 기밀성과 무결성을 동시에 제공합니다.
+
+use core::ptr::write_volatile;
+use entlib_native_constant_time::traits::ConstantTimeEq;
+use entlib_native_hmac::HMACSHA256;
+use entlib_native_secure_buffer::SecureBuffer;
+
+use crate::aes::{KeySchedule, aes256_decrypt_block, aes256_encrypt_block};
+use crate::error::AESError;
+
+pub const CBC_IV_LEN: usize = 16;
+pub const CBC_HMAC_LEN: usize = 32;
+
+/// CBC 암호화 출력 크기를 반환하는 함수입니다.
+/// 출력 형식: `IV(16) || PKCS7-패딩된 암호문 || HMAC-SHA256(32)`
+pub fn cbc_output_len(plaintext_len: usize) -> usize {
+ let padded = (plaintext_len / 16 + 1) * 16;
+ CBC_IV_LEN + padded + CBC_HMAC_LEN
+}
+
+/// CBC 복호화 후 최대 평문 크기를 반환하는 함수입니다.
+/// 입력에서 IV(16), HMAC(32), PKCS7 최소 패딩(1)을 제외한 크기를 반환합니다.
+pub fn cbc_plaintext_max_len(input_len: usize) -> Option {
+ input_len.checked_sub(CBC_IV_LEN + CBC_HMAC_LEN + 1)
+}
+
+// 32바이트 슬라이스 상수-시간 비교
+fn ct_eq_32(a: &[u8], b: &[u8]) -> bool {
+ if a.len() != 32 || b.len() != 32 {
+ return false;
+ }
+ let mut r = 0xFFu8;
+ for i in 0..32 {
+ r &= a[i].ct_eq(&b[i]).unwrap_u8();
+ }
+ r == 0xFF
+}
+
+// PKCS7 패딩 검증 (복호화 후) — HMAC 검증 통과 후에만 호출
+fn pkcs7_unpad_len(data: &[u8]) -> Result {
+ if data.is_empty() || !data.len().is_multiple_of(16) {
+ return Err(AESError::InternalError);
+ }
+ let pad_byte = data[data.len() - 1];
+ let pad_len = pad_byte as usize;
+ if pad_len == 0 || pad_len > 16 {
+ return Err(AESError::InternalError);
+ }
+ // 패딩 바이트 상수-시간 검증
+ let mut valid = 0xFFu8;
+ for &b in data.iter().skip(data.len() - pad_len) {
+ let diff = b ^ pad_byte;
+ let not_zero = (diff | diff.wrapping_neg()) >> 7;
+ valid &= (not_zero ^ 1).wrapping_neg();
+ }
+ if valid == 0xFF {
+ Ok(data.len() - pad_len)
+ } else {
+ Err(AESError::InternalError)
+ }
+}
+
+// Q. T. Felix NOTE: 설계 중에 알아차린건데, HMAC-SHA-256 단일로 계산되도록 구현해버림
+// fxxk@@@ ^^7 일단 커밋 하고 pr로 수정
+//
+
+/// AES-256-CBC-HMAC-SHA256 AEAD 구조체입니다.
+/// CBC 모드는 단독으로 사용할 수 없으며, 반드시 Encrypt-then-MAC 방식으로 무결성 태그를 붙여야 합니다.
+pub struct AES256CBCHmac;
+
+impl AES256CBCHmac {
+ /// AES-256-CBC-HMAC-SHA256 암호화 함수입니다.
+ ///
+ /// # Arguments
+ /// - `enc_key` — 256비트(32 bytes) AES 암호화 키
+ /// - `mac_key` — HMAC-SHA256 무결성 키 (최소 14 bytes, 권장 32 bytes)
+ /// - `iv` — 128비트(16 bytes) 초기화 벡터 (각 메시지마다 고유해야 함)
+ /// - `plaintext` — 평문
+ /// - `output` — 출력 버퍼, 최소 `cbc_output_len(plaintext.len())` bytes
+ ///
+ /// # Returns
+ /// 출력에 쓰인 바이트 수
+ ///
+ /// # Security Note
+ /// 출력 형식: `IV(16) || CT_padded || HMAC-SHA256(IV||CT_padded)(32)`
+ /// IV는 각 암호화마다 고유한 값을 사용해야 합니다(nonce-reuse 금지).
+ pub fn encrypt(
+ enc_key: &SecureBuffer,
+ mac_key: &SecureBuffer,
+ iv: &[u8; CBC_IV_LEN],
+ plaintext: &[u8],
+ output: &mut [u8],
+ ) -> Result {
+ if enc_key.len() != 32 {
+ return Err(AESError::InvalidKeyLength);
+ }
+ let required = cbc_output_len(plaintext.len());
+ if output.len() < required {
+ return Err(AESError::OutputBufferTooSmall);
+ }
+
+ let enc_key_arr: [u8; 32] = {
+ let s = enc_key.as_slice();
+ [
+ s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], s[12],
+ s[13], s[14], s[15], s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], s[24],
+ s[25], s[26], s[27], s[28], s[29], s[30], s[31],
+ ]
+ };
+ let ks = KeySchedule::new(&enc_key_arr);
+
+ // IV를 출력 선두에 기록
+ output[..16].copy_from_slice(iv);
+
+ let ct_start = 16usize;
+ let padded_len = (plaintext.len() / 16 + 1) * 16;
+ let ct_end = ct_start + padded_len;
+
+ let mut prev_block = *iv;
+ let full_blocks = plaintext.len() / 16;
+
+ // 완전한 블록 암호화
+ for i in 0..full_blocks {
+ let mut block = [0u8; 16];
+ block.copy_from_slice(&plaintext[i * 16..(i + 1) * 16]);
+ for j in 0..16 {
+ block[j] ^= prev_block[j];
+ }
+ aes256_encrypt_block(&mut block, &ks);
+ output[ct_start + i * 16..ct_start + (i + 1) * 16].copy_from_slice(&block);
+ prev_block = block;
+ for b in &mut block {
+ unsafe { write_volatile(b, 0) };
+ }
+ }
+
+ // 마지막 블록: 나머지 바이트 + PKCS7 패딩
+ let rem = plaintext.len() - full_blocks * 16;
+ let pad_byte = (padded_len - plaintext.len()) as u8;
+ let mut last_block = [pad_byte; 16];
+ last_block[..rem].copy_from_slice(&plaintext[full_blocks * 16..]);
+ for j in 0..16 {
+ last_block[j] ^= prev_block[j];
+ }
+ aes256_encrypt_block(&mut last_block, &ks);
+ output[ct_start + full_blocks * 16..ct_end].copy_from_slice(&last_block);
+ for b in &mut last_block {
+ unsafe { write_volatile(b, 0) };
+ }
+ for b in &mut prev_block {
+ unsafe { write_volatile(b, 0) };
+ }
+
+ // Encrypt-then-MAC: HMAC-SHA256(IV || 암호문)
+ let mut hmac = HMACSHA256::new(mac_key.as_slice()).map_err(|_| AESError::InternalError)?;
+ hmac.update(&output[..ct_end]);
+ let mac = hmac.finalize().map_err(|_| AESError::InternalError)?;
+ output[ct_end..ct_end + 32].copy_from_slice(mac.as_slice());
+
+ Ok(ct_end + 32)
+ }
+
+ /// AES-256-CBC-HMAC-SHA256 복호화 함수입니다.
+ ///
+ /// # Arguments
+ /// - `enc_key` — 256비트(32 bytes) AES 복호화 키
+ /// - `mac_key` — HMAC-SHA256 검증 키
+ /// - `input` — `IV(16) || CT || HMAC(32)` 형식의 입력
+ /// - `output` — 평문 출력 버퍼
+ ///
+ /// # Returns
+ /// 복호화된 평문 바이트 수
+ ///
+ /// # Security Note
+ /// MAC 검증에 실패하면 복호화를 수행하지 않습니다. 패딩 오라클 공격 방지.
+ pub fn decrypt(
+ enc_key: &SecureBuffer,
+ mac_key: &SecureBuffer,
+ input: &[u8],
+ output: &mut [u8],
+ ) -> Result {
+ if enc_key.len() != 32 {
+ return Err(AESError::InvalidKeyLength);
+ }
+ // 최소 크기: IV(16) + 블록 1개(16) + HMAC(32) = 64
+ if input.len() < 64 || !(input.len() - 48).is_multiple_of(16) {
+ return Err(AESError::InvalidInputLength);
+ }
+
+ let mac_start = input.len() - 32;
+ let received_mac = &input[mac_start..];
+ let authenticated = &input[..mac_start];
+
+ // MAC 검증 (먼저, Encrypt-then-MAC)
+ let mut hmac = HMACSHA256::new(mac_key.as_slice()).map_err(|_| AESError::InternalError)?;
+ hmac.update(authenticated);
+ let expected_mac = hmac.finalize().map_err(|_| AESError::InternalError)?;
+
+ // 상수-시간 MAC 비교
+ if !ct_eq_32(expected_mac.as_slice(), received_mac) {
+ return Err(AESError::AuthenticationFailed);
+ }
+
+ // MAC 검증 통과 후 복호화
+ let iv: [u8; 16] = [
+ input[0], input[1], input[2], input[3], input[4], input[5], input[6], input[7],
+ input[8], input[9], input[10], input[11], input[12], input[13], input[14], input[15],
+ ];
+ let ciphertext = authenticated;
+ let ct_blocks = &ciphertext[16..]; // IV 제외한 암호문 부분
+
+ if output.len() < ct_blocks.len() {
+ return Err(AESError::OutputBufferTooSmall);
+ }
+
+ let enc_key_arr: [u8; 32] = {
+ let s = enc_key.as_slice();
+ [
+ s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], s[12],
+ s[13], s[14], s[15], s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], s[24],
+ s[25], s[26], s[27], s[28], s[29], s[30], s[31],
+ ]
+ };
+ let ks = KeySchedule::new(&enc_key_arr);
+
+ let block_count = ct_blocks.len() / 16;
+ let mut prev_block = iv;
+
+ for i in 0..block_count {
+ let mut block = [0u8; 16];
+ block.copy_from_slice(&ct_blocks[i * 16..(i + 1) * 16]);
+ let cipher_block = block;
+ aes256_decrypt_block(&mut block, &ks);
+ for j in 0..16 {
+ block[j] ^= prev_block[j];
+ }
+ output[i * 16..(i + 1) * 16].copy_from_slice(&block);
+ prev_block = cipher_block;
+ for b in &mut block {
+ unsafe { write_volatile(b, 0) };
+ }
+ }
+ for b in &mut prev_block {
+ unsafe { write_volatile(b, 0) };
+ }
+
+ let plaintext_len = pkcs7_unpad_len(&output[..ct_blocks.len()])?;
+ Ok(plaintext_len)
+ }
+}
diff --git a/crypto/aes/src/error.rs b/crypto/aes/src/error.rs
new file mode 100644
index 0000000..570e344
--- /dev/null
+++ b/crypto/aes/src/error.rs
@@ -0,0 +1,16 @@
+//! AES-256 오류 타입 모듈입니다.
+
+/// AES-256 연산 중 발생할 수 있는 오류 열거형입니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AESError {
+ /// 키 길이가 256비트(32 bytes)가 아님
+ InvalidKeyLength,
+ /// GCM 출력 버퍼 부족
+ OutputBufferTooSmall,
+ /// GCM 태그 검증 실패 또는 CBC HMAC 검증 실패
+ AuthenticationFailed,
+ /// CBC 입력 형식 오류 (최소 길이 미달 또는 블록 크기 불일치)
+ InvalidInputLength,
+ /// 내부 오류 (PKCS7 패딩 손상, HMAC 연산 실패 등)
+ InternalError,
+}
diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs
new file mode 100644
index 0000000..7024265
--- /dev/null
+++ b/crypto/aes/src/gcm.rs
@@ -0,0 +1,208 @@
+//! AES-256-GCM AEAD 모듈입니다.
+//! NIST SP 800-38D 준거. 96비트 nonce, 128비트 인증 태그를 지원합니다.
+
+use core::ptr::write_volatile;
+use entlib_native_constant_time::traits::ConstantTimeEq;
+use entlib_native_secure_buffer::SecureBuffer;
+
+use crate::aes::{KeySchedule, aes256_encrypt_block};
+use crate::error::AESError;
+use crate::ghash::GHashState;
+
+// GCM은 96비트(12 bytes) nonce만 지원 (NIST SP 800-38D 권고)
+pub const GCM_NONCE_LEN: usize = 12;
+pub const GCM_TAG_LEN: usize = 16;
+
+// J0 = nonce(12) || 0x00000001
+fn build_j0(nonce: &[u8; 12]) -> [u8; 16] {
+ let mut j0 = [0u8; 16];
+ j0[..12].copy_from_slice(nonce);
+ j0[15] = 0x01;
+ j0
+}
+
+// inc32: J0의 하위 32비트를 빅엔디안으로 1 증가
+fn inc32(block: &[u8; 16]) -> [u8; 16] {
+ let mut out = *block;
+ let ctr = u32::from_be_bytes([block[12], block[13], block[14], block[15]]);
+ let next = ctr.wrapping_add(1).to_be_bytes();
+ out[12] = next[0];
+ out[13] = next[1];
+ out[14] = next[2];
+ out[15] = next[3];
+ out
+}
+
+// GCTR: CTR 모드 암·복호화 (J0+1부터 시작)
+// output.len() == data.len() 보장
+fn gctr(ks: &KeySchedule, j0: &[u8; 16], data: &[u8], output: &mut [u8]) {
+ let mut ctr = inc32(j0);
+ let mut keystream = [0u8; 16];
+ let mut i = 0;
+ while i < data.len() {
+ keystream = ctr;
+ aes256_encrypt_block(&mut keystream, ks);
+ let chunk = core::cmp::min(16, data.len() - i);
+ for j in 0..chunk {
+ output[i + j] = data[i + j] ^ keystream[j];
+ }
+ i += chunk;
+ ctr = inc32(&ctr);
+ }
+ for b in &mut keystream {
+ unsafe { write_volatile(b, 0) };
+ }
+}
+
+// 16바이트 슬라이스 상수-시간 비교
+fn ct_eq_16(a: &[u8; 16], b: &[u8; 16]) -> bool {
+ let mut r = 0xFFu8;
+ for i in 0..16 {
+ r &= a[i].ct_eq(&b[i]).unwrap_u8();
+ }
+ r == 0xFF
+}
+
+/// AES-256-GCM AEAD 암호화 구조체입니다.
+/// NIST SP 800-38D 준거이며 96비트 nonce만 지원합니다.
+pub struct AES256GCM;
+
+impl AES256GCM {
+ /// AES-256-GCM 암호화 함수입니다.
+ ///
+ /// # Arguments
+ /// - `key` — 256비트(32 bytes) AES 키
+ /// - `nonce` — 96비트(12 bytes) nonce (반드시 유일해야 함)
+ /// - `aad` — 추가 인증 데이터 (암호화되지 않음)
+ /// - `plaintext` — 평문
+ /// - `ciphertext_out` — 암호문 출력 버퍼 (`plaintext.len()` bytes)
+ /// - `tag_out` — 16바이트 인증 태그 출력
+ ///
+ /// # Security Note
+ /// 동일한 (key, nonce) 쌍을 재사용하면 기밀성·무결성이 완전히 붕괴됩니다.
+ pub fn encrypt(
+ key: &SecureBuffer,
+ nonce: &[u8; GCM_NONCE_LEN],
+ aad: &[u8],
+ plaintext: &[u8],
+ ciphertext_out: &mut [u8],
+ tag_out: &mut [u8; GCM_TAG_LEN],
+ ) -> Result<(), AESError> {
+ if key.len() != 32 {
+ return Err(AESError::InvalidKeyLength);
+ }
+ if ciphertext_out.len() < plaintext.len() {
+ return Err(AESError::OutputBufferTooSmall);
+ }
+
+ let key_arr: [u8; 32] = {
+ let s = key.as_slice();
+ [
+ s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], s[12],
+ s[13], s[14], s[15], s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], s[24],
+ s[25], s[26], s[27], s[28], s[29], s[30], s[31],
+ ]
+ };
+ let ks = KeySchedule::new(&key_arr);
+
+ // H = AES_K(0^128)
+ let mut h_block = [0u8; 16];
+ aes256_encrypt_block(&mut h_block, &ks);
+
+ let j0 = build_j0(nonce);
+
+ // 평문 암호화 (CTR)
+ gctr(&ks, &j0, plaintext, ciphertext_out);
+
+ // GHASH(AAD, CT)
+ let mut ghash = GHashState::new(&h_block);
+ ghash.update(aad);
+ ghash.update(&ciphertext_out[..plaintext.len()]);
+ let s = ghash.finalize(aad.len() as u64, plaintext.len() as u64);
+
+ // 태그 = E_K(J0) XOR GHASH
+ let mut ej0 = j0;
+ aes256_encrypt_block(&mut ej0, &ks);
+ for i in 0..16 {
+ tag_out[i] = ej0[i] ^ s[i];
+ }
+
+ for b in &mut ej0 {
+ unsafe { write_volatile(b, 0) };
+ }
+ for b in &mut h_block {
+ unsafe { write_volatile(b, 0) };
+ }
+ Ok(())
+ }
+
+ /// AES-256-GCM 복호화 및 태그 검증 함수입니다.
+ ///
+ /// # Security Note
+ /// 태그 검증에 실패하면 평문을 출력하지 않습니다. 상수-시간 비교를 사용합니다.
+ pub fn decrypt(
+ key: &SecureBuffer,
+ nonce: &[u8; GCM_NONCE_LEN],
+ aad: &[u8],
+ ciphertext: &[u8],
+ tag: &[u8; GCM_TAG_LEN],
+ plaintext_out: &mut [u8],
+ ) -> Result<(), AESError> {
+ if key.len() != 32 {
+ return Err(AESError::InvalidKeyLength);
+ }
+ if plaintext_out.len() < ciphertext.len() {
+ return Err(AESError::OutputBufferTooSmall);
+ }
+
+ let key_arr: [u8; 32] = {
+ let s = key.as_slice();
+ [
+ s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], s[12],
+ s[13], s[14], s[15], s[16], s[17], s[18], s[19], s[20], s[21], s[22], s[23], s[24],
+ s[25], s[26], s[27], s[28], s[29], s[30], s[31],
+ ]
+ };
+ let ks = KeySchedule::new(&key_arr);
+
+ let mut h_block = [0u8; 16];
+ aes256_encrypt_block(&mut h_block, &ks);
+
+ let j0 = build_j0(nonce);
+
+ // 태그 재계산 (복호화 전)
+ let mut ghash = GHashState::new(&h_block);
+ ghash.update(aad);
+ ghash.update(ciphertext);
+ let s = ghash.finalize(aad.len() as u64, ciphertext.len() as u64);
+
+ let mut ej0 = j0;
+ aes256_encrypt_block(&mut ej0, &ks);
+ let mut expected_tag = [0u8; 16];
+ for i in 0..16 {
+ expected_tag[i] = ej0[i] ^ s[i];
+ }
+
+ // 상수-시간 태그 검증 — 검증 통과 전에 평문 출력 금지
+ if !ct_eq_16(&expected_tag, tag) {
+ for b in &mut ej0 {
+ unsafe { write_volatile(b, 0) };
+ }
+ for b in &mut h_block {
+ unsafe { write_volatile(b, 0) };
+ }
+ return Err(AESError::AuthenticationFailed);
+ }
+
+ // 태그 검증 통과 후에만 복호화
+ gctr(&ks, &j0, ciphertext, plaintext_out);
+
+ for b in &mut ej0 {
+ unsafe { write_volatile(b, 0) };
+ }
+ for b in &mut h_block {
+ unsafe { write_volatile(b, 0) };
+ }
+ Ok(())
+ }
+}
diff --git a/crypto/aes/src/ghash.rs b/crypto/aes/src/ghash.rs
new file mode 100644
index 0000000..38b4665
--- /dev/null
+++ b/crypto/aes/src/ghash.rs
@@ -0,0 +1,166 @@
+//! GHASH 인증 함수 모듈입니다.
+//! NIST SP 800-38D 준거 GF(2^128) 상수-시간 연산으로 GCM 인증 태그를 계산합니다.
+
+use core::ptr::write_volatile;
+
+// GCM GF(2^128) 곱셈: 환원 다항식 x^128 + x^7 + x^2 + x + 1
+// R = 0xE100...00 (128비트, MSB 우선)
+// 128회 고정 반복 — 분기 없음, 상수-시간 보장
+#[inline(never)]
+fn gf128_mul(x: &mut [u64; 2], h: &[u64; 2]) {
+ let mut z = [0u64; 2];
+ let mut v = *h;
+
+ for i in 0u32..128 {
+ let word = (i >> 6) as usize;
+ let bit = 63 - (i & 63);
+
+ // x의 i번째 비트 (MSB 우선) 추출 — 마스크 트릭, 분기 없음
+ let xi = ((x[word] >> bit) & 1).wrapping_neg();
+ z[0] ^= v[0] & xi;
+ z[1] ^= v[1] & xi;
+
+ // v를 오른쪽으로 1비트 시프트
+ let lsb = v[1] & 1;
+ v[1] = (v[1] >> 1) | (v[0] << 63);
+ v[0] >>= 1;
+
+ // LSB가 1이면 R로 XOR 환원 — 분기 없음
+ let r_mask = lsb.wrapping_neg();
+ v[0] ^= 0xE100000000000000u64 & r_mask;
+ }
+
+ *x = z;
+}
+
+/// GCM 인증 태그 계산을 위한 GHASH 상태 구조체입니다.
+/// `Drop` 시 내부 H 값과 누산 상태를 소거합니다.
+pub struct GHashState {
+ h: [u64; 2],
+ state: [u64; 2],
+}
+
+impl GHashState {
+ /// GHASH 상태를 초기화하는 함수입니다.
+ ///
+ /// # Arguments
+ /// `h_block` — H = AES_K(0^128) 블록
+ pub fn new(h_block: &[u8; 16]) -> Self {
+ let h = [
+ u64::from_be_bytes([
+ h_block[0], h_block[1], h_block[2], h_block[3], h_block[4], h_block[5], h_block[6],
+ h_block[7],
+ ]),
+ u64::from_be_bytes([
+ h_block[8],
+ h_block[9],
+ h_block[10],
+ h_block[11],
+ h_block[12],
+ h_block[13],
+ h_block[14],
+ h_block[15],
+ ]),
+ ];
+ Self {
+ h,
+ state: [0u64; 2],
+ }
+ }
+
+ fn update_block(&mut self, block: &[u8; 16]) {
+ let b0 = u64::from_be_bytes([
+ block[0], block[1], block[2], block[3], block[4], block[5], block[6], block[7],
+ ]);
+ let b1 = u64::from_be_bytes([
+ block[8], block[9], block[10], block[11], block[12], block[13], block[14], block[15],
+ ]);
+ self.state[0] ^= b0;
+ self.state[1] ^= b1;
+ gf128_mul(&mut self.state, &self.h);
+ }
+
+ /// 데이터를 GHASH 상태에 누적하는 함수입니다. 16바이트 단위로 처리하며 나머지는 0 패딩합니다.
+ pub fn update(&mut self, data: &[u8]) {
+ let mut i = 0;
+ while i + 16 <= data.len() {
+ let block: [u8; 16] = [
+ data[i],
+ data[i + 1],
+ data[i + 2],
+ data[i + 3],
+ data[i + 4],
+ data[i + 5],
+ data[i + 6],
+ data[i + 7],
+ data[i + 8],
+ data[i + 9],
+ data[i + 10],
+ data[i + 11],
+ data[i + 12],
+ data[i + 13],
+ data[i + 14],
+ data[i + 15],
+ ];
+ self.update_block(&block);
+ i += 16;
+ }
+ let rem = data.len() - i;
+ if rem > 0 {
+ let mut buf = [0u8; 16];
+ buf[..rem].copy_from_slice(&data[i..]);
+ self.update_block(&buf);
+ for b in &mut buf {
+ unsafe { write_volatile(b, 0) };
+ }
+ }
+ }
+
+ /// GHASH 최종값을 반환하는 함수입니다.
+ /// AAD/암호문 길이 블록을 처리한 뒤 16바이트 GHASH 출력을 반환합니다.
+ ///
+ /// # Arguments
+ /// - `aad_len` — AAD 바이트 수
+ /// - `ct_len` — 암호문 바이트 수
+ pub fn finalize(mut self, aad_len: u64, ct_len: u64) -> [u8; 16] {
+ let aad_bits = aad_len * 8;
+ let ct_bits = ct_len * 8;
+ let mut len_block = [0u8; 16];
+ len_block[0] = (aad_bits >> 56) as u8;
+ len_block[1] = (aad_bits >> 48) as u8;
+ len_block[2] = (aad_bits >> 40) as u8;
+ len_block[3] = (aad_bits >> 32) as u8;
+ len_block[4] = (aad_bits >> 24) as u8;
+ len_block[5] = (aad_bits >> 16) as u8;
+ len_block[6] = (aad_bits >> 8) as u8;
+ len_block[7] = aad_bits as u8;
+ len_block[8] = (ct_bits >> 56) as u8;
+ len_block[9] = (ct_bits >> 48) as u8;
+ len_block[10] = (ct_bits >> 40) as u8;
+ len_block[11] = (ct_bits >> 32) as u8;
+ len_block[12] = (ct_bits >> 24) as u8;
+ len_block[13] = (ct_bits >> 16) as u8;
+ len_block[14] = (ct_bits >> 8) as u8;
+ len_block[15] = ct_bits as u8;
+ self.update_block(&len_block);
+
+ let s = self.state;
+ let mut out = [0u8; 16];
+ let hi = s[0].to_be_bytes();
+ let lo = s[1].to_be_bytes();
+ out[..8].copy_from_slice(&hi);
+ out[8..].copy_from_slice(&lo);
+ out
+ }
+}
+
+impl Drop for GHashState {
+ fn drop(&mut self) {
+ for w in &mut self.state {
+ unsafe { write_volatile(w, 0) };
+ }
+ for w in &mut self.h {
+ unsafe { write_volatile(w, 0) };
+ }
+ }
+}
diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs
new file mode 100644
index 0000000..56be982
--- /dev/null
+++ b/crypto/aes/src/lib.rs
@@ -0,0 +1,43 @@
+//! AES-256 암호 모듈입니다.
+//! FIPS 140-3 요구사항을 충족하는 GCM(AEAD) 및 CBC+PKCS7+HMAC-SHA256 모드를 제공합니다.
+//!
+//! # Examples
+//! ```rust
+//! use entlib_native_aes::{AES256GCM, AES256CBCHmac, GCM_NONCE_LEN, GCM_TAG_LEN, CBC_IV_LEN, cbc_output_len};
+//! use entlib_native_secure_buffer::SecureBuffer;
+//!
+//! let mut key = SecureBuffer::new_owned(32).unwrap();
+//! key.as_mut_slice().copy_from_slice(&[0u8; 32]);
+//! let nonce = [0u8; GCM_NONCE_LEN];
+//! let plaintext = b"hello world";
+//! let mut ct = vec![0u8; plaintext.len()];
+//! let mut tag = [0u8; GCM_TAG_LEN];
+//! AES256GCM::encrypt(&key, &nonce, &[], plaintext, &mut ct, &mut tag).unwrap();
+//!
+//! let mut enc_key = SecureBuffer::new_owned(32).unwrap();
+//! enc_key.as_mut_slice().copy_from_slice(&[0u8; 32]);
+//! let mut mac_key = SecureBuffer::new_owned(32).unwrap();
+//! mac_key.as_mut_slice().copy_from_slice(&[1u8; 32]);
+//! let iv = [0u8; CBC_IV_LEN];
+//! let mut out = vec![0u8; cbc_output_len(plaintext.len())];
+//! AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, plaintext, &mut out).unwrap();
+//! ```
+//!
+//! # Authors
+//! Q. T. Felix
+
+#![no_std]
+#![doc = include_str!("../README.md")]
+
+extern crate alloc;
+
+mod aes;
+mod cbc;
+mod error;
+mod gcm;
+mod ghash;
+
+pub use aes::aes256_encrypt_ecb;
+pub use cbc::{AES256CBCHmac, CBC_HMAC_LEN, CBC_IV_LEN, cbc_output_len, cbc_plaintext_max_len};
+pub use error::AESError;
+pub use gcm::{AES256GCM, GCM_NONCE_LEN, GCM_TAG_LEN};
diff --git a/crypto/aes/tests/aes_test.rs b/crypto/aes/tests/aes_test.rs
new file mode 100644
index 0000000..05e2494
--- /dev/null
+++ b/crypto/aes/tests/aes_test.rs
@@ -0,0 +1,178 @@
+#[cfg(test)]
+mod tests {
+ extern crate std;
+ use std::vec;
+
+ use entlib_native_secure_buffer::SecureBuffer;
+
+ use entlib_native_aes::*;
+
+ fn make_key(bytes: &[u8]) -> SecureBuffer {
+ let mut buf = SecureBuffer::new_owned(bytes.len()).unwrap();
+ buf.as_mut_slice().copy_from_slice(bytes);
+ buf
+ }
+
+ // NIST FIPS 197 Appendix B: AES-256 ECB 단일 블록 암호화
+ #[test]
+ fn fips197_aes256_ecb_encrypt() {
+ let key: [u8; 32] = [
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+ 0x1c, 0x1d, 0x1e, 0x1f,
+ ];
+ let pt: [u8; 16] = [
+ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
+ 0xee, 0xff,
+ ];
+ let expected: [u8; 16] = [
+ 0x8e, 0xa2, 0xb7, 0xca, 0x51, 0x67, 0x45, 0xbf, 0xea, 0xfc, 0x49, 0x90, 0x4b, 0x49,
+ 0x60, 0x89,
+ ];
+ assert_eq!(aes256_encrypt_ecb(&key, &pt), expected);
+ }
+
+ // NIST CAVP AES-256-GCM 암호화 검증
+ #[test]
+ fn nist_cavp_aes256_gcm_encrypt() {
+ let key = make_key(&[
+ 0xfe, 0xff, 0xe9, 0x92, 0x86, 0x65, 0x73, 0x1c, 0x6d, 0x6a, 0x8f, 0x94, 0x67, 0x30,
+ 0x83, 0x08, 0xfe, 0xff, 0xe9, 0x92, 0x86, 0x65, 0x73, 0x1c, 0x6d, 0x6a, 0x8f, 0x94,
+ 0x67, 0x30, 0x83, 0x08,
+ ]);
+ let nonce: [u8; 12] = [
+ 0xca, 0xfe, 0xba, 0xbe, 0xfa, 0xce, 0xdb, 0xad, 0xde, 0xca, 0xf8, 0x88,
+ ];
+ let aad = [
+ 0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad, 0xbe, 0xef, 0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad,
+ 0xbe, 0xef, 0xab, 0xad, 0xda, 0xd2u8,
+ ];
+ let pt = [
+ 0xd9, 0x31, 0x32, 0x25, 0xf8, 0x84, 0x06, 0xe5, 0xa5, 0x59, 0x09, 0xc5, 0xaf, 0xf5,
+ 0x26, 0x9a, 0x86, 0xa7, 0xa9, 0x53, 0x15, 0x34, 0xf7, 0xda, 0x2e, 0x4c, 0x30, 0x3d,
+ 0x8a, 0x31, 0x8a, 0x72, 0x1c, 0x3c, 0x0c, 0x95, 0x95, 0x68, 0x09, 0x53, 0x2f, 0xcf,
+ 0x0e, 0x24, 0x49, 0xa6, 0xb5, 0x25, 0xb1, 0x6a, 0xed, 0xf5, 0xaa, 0x0d, 0xe6, 0x57,
+ 0xba, 0x63, 0x7b, 0x39u8,
+ ];
+ let expected_ct = [
+ 0x52, 0x2d, 0xc1, 0xf0, 0x99, 0x56, 0x7d, 0x07, 0xf4, 0x7f, 0x37, 0xa3, 0x2a, 0x84,
+ 0x42, 0x7d, 0x64, 0x3a, 0x8c, 0xdc, 0xbf, 0xe5, 0xc0, 0xc9, 0x75, 0x98, 0xa2, 0xbd,
+ 0x25, 0x55, 0xd1, 0xaa, 0x8c, 0xb0, 0x8e, 0x48, 0x59, 0x0d, 0xbb, 0x3d, 0xa7, 0xb0,
+ 0x8b, 0x10, 0x56, 0x82, 0x88, 0x38, 0xc5, 0xf6, 0x1e, 0x63, 0x93, 0xba, 0x7a, 0x0a,
+ 0xbc, 0xc9, 0xf6, 0x62u8,
+ ];
+ let expected_tag: [u8; 16] = [
+ 0x76, 0xfc, 0x6e, 0xce, 0x0f, 0x4e, 0x17, 0x68, 0xcd, 0xdf, 0x88, 0x53, 0xbb, 0x2d,
+ 0x55, 0x1b,
+ ];
+
+ let mut ct_out = vec![0u8; pt.len()];
+ let mut tag_out = [0u8; 16];
+ AES256GCM::encrypt(&key, &nonce, &aad, &pt, &mut ct_out, &mut tag_out).unwrap();
+ assert_eq!(&ct_out[..], &expected_ct[..]);
+ assert_eq!(tag_out, expected_tag);
+ }
+
+ // GCM: 암호화 후 복호화 일치 확인
+ #[test]
+ fn aes256_gcm_roundtrip() {
+ let key = make_key(&[0x42u8; 32]);
+ let nonce = [0x01u8; 12];
+ let aad = b"entlib-native";
+ let pt = b"FIPS 140-3 AES-256-GCM test message";
+
+ let mut ct = vec![0u8; pt.len()];
+ let mut tag = [0u8; 16];
+ AES256GCM::encrypt(&key, &nonce, aad, pt, &mut ct, &mut tag).unwrap();
+
+ let mut recovered = vec![0u8; ct.len()];
+ AES256GCM::decrypt(&key, &nonce, aad, &ct, &tag, &mut recovered).unwrap();
+ assert_eq!(&recovered[..], &pt[..]);
+ }
+
+ // GCM: 태그 1비트 변조 시 복호화 실패
+ #[test]
+ fn aes256_gcm_tag_tamper_rejected() {
+ let key = make_key(&[0x11u8; 32]);
+ let nonce = [0xabu8; 12];
+ let pt = b"tamper test";
+ let mut ct = vec![0u8; pt.len()];
+ let mut tag = [0u8; 16];
+ AES256GCM::encrypt(&key, &nonce, b"", pt, &mut ct, &mut tag).unwrap();
+ tag[0] ^= 0x01;
+ let mut out = vec![0u8; ct.len()];
+ let result = AES256GCM::decrypt(&key, &nonce, b"", &ct, &tag, &mut out);
+ assert_eq!(result, Err(AESError::AuthenticationFailed));
+ }
+
+ // CBC-HMAC NIST SP 800-38A F.2.5 + HMAC 검증
+ #[test]
+ fn nist_aes256_cbc_hmac_encrypt() {
+ let enc_key = make_key(&[
+ 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d,
+ 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3,
+ 0x09, 0x14, 0xdf, 0xf4,
+ ]);
+ let mac_key = make_key(&[0x00u8; 32]);
+ let iv: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
+ let pt = [
+ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93,
+ 0x17, 0x2a, 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac,
+ 0x45, 0xaf, 0x8e, 0x51, 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb,
+ 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xefu8,
+ ];
+ let expected_ct = [
+ 0xf5, 0x8c, 0x4c, 0x04, 0xd6, 0xe5, 0xf1, 0xba, 0x77, 0x9e, 0xab, 0xfb, 0x5f, 0x7b,
+ 0xfb, 0xd6, 0x9c, 0xfc, 0x4e, 0x96, 0x7e, 0xdb, 0x80, 0x8d, 0x67, 0x9f, 0x77, 0x7b,
+ 0xc6, 0x70, 0x2c, 0x7d, 0x39, 0xf2, 0x33, 0x69, 0xa9, 0xd9, 0xba, 0xcf, 0xa5, 0x30,
+ 0xe2, 0x63, 0x04, 0x23, 0x14, 0x61, 0x2f, 0x8d, 0xa7, 0x07, 0x64, 0x3c, 0x90, 0xa6,
+ 0xf7, 0x32, 0xb3, 0xde, 0x1d, 0x3f, 0x5c, 0xeeu8,
+ ];
+ let expected_mac: [u8; 32] = [
+ 0xa2, 0xfa, 0xcb, 0x5d, 0xa7, 0xd3, 0x35, 0x49, 0xd8, 0x26, 0x40, 0x3b, 0xe7, 0x39,
+ 0xd5, 0xae, 0x21, 0x25, 0x14, 0x2c, 0xc8, 0x26, 0xa6, 0xb6, 0xc9, 0xfc, 0x83, 0x97,
+ 0x06, 0x0d, 0x52, 0x56,
+ ];
+
+ let mut output = vec![0u8; cbc_output_len(pt.len())];
+ let written = AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, &pt, &mut output).unwrap();
+ assert_eq!(written, output.len());
+ assert_eq!(&output[16..80], &expected_ct[..]); // IV 다음이 CT
+ assert_eq!(&output[80..112], &expected_mac[..]); // 마지막 32바이트가 MAC
+ }
+
+ // CBC-HMAC: 암호화 후 복호화 일치
+ #[test]
+ fn aes256_cbc_hmac_roundtrip() {
+ let enc_key = make_key(&[0x23u8; 32]);
+ let mac_key = make_key(&[0x45u8; 32]);
+ let iv = [0x67u8; 16];
+ let pt = b"CBC-HMAC roundtrip plaintext test message!";
+
+ let mut output = vec![0u8; cbc_output_len(pt.len())];
+ AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, pt, &mut output).unwrap();
+
+ let mut recovered = vec![0u8; pt.len() + 16];
+ let plain_len =
+ AES256CBCHmac::decrypt(&enc_key, &mac_key, &output, &mut recovered).unwrap();
+ assert_eq!(&recovered[..plain_len], &pt[..]);
+ }
+
+ // CBC-HMAC: MAC 1비트 변조 시 복호화 실패
+ #[test]
+ fn aes256_cbc_hmac_mac_tamper_rejected() {
+ let enc_key = make_key(&[0x55u8; 32]);
+ let mac_key = make_key(&[0x77u8; 32]);
+ let iv = [0x99u8; 16];
+ let pt = b"tamper detection test";
+
+ let mut output = vec![0u8; cbc_output_len(pt.len())];
+ AES256CBCHmac::encrypt(&enc_key, &mac_key, &iv, pt, &mut output).unwrap();
+ let last = output.len() - 1;
+ output[last] ^= 0x01;
+
+ let mut recovered = vec![0u8; pt.len() + 16];
+ let result = AES256CBCHmac::decrypt(&enc_key, &mac_key, &output, &mut recovered);
+ assert_eq!(result, Err(AESError::AuthenticationFailed));
+ }
+}
diff --git a/crypto/argon2id/Cargo.toml b/crypto/argon2id/Cargo.toml
new file mode 100644
index 0000000..d1a892f
--- /dev/null
+++ b/crypto/argon2id/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "entlib-native-argon2id"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+entlib-native-blake.workspace = true
+entlib-native-secure-buffer.workspace = true
+entlib-native-base.workspace = true
diff --git a/crypto/argon2id/README.md b/crypto/argon2id/README.md
new file mode 100644
index 0000000..cd95745
--- /dev/null
+++ b/crypto/argon2id/README.md
@@ -0,0 +1,163 @@
+# Argon2id 패스워드 해시 함수 (entlib-native-argon2id)
+
+> Q. T. Felix (수정: 26.03.23 UTC+9)
+>
+> [English README](README_EN.md)
+
+`entlib-native-argon2id`는 RFC 9106 및 NIST SP 800-63B를 준수하는 `no_std` 호환 Argon2id 패스워드 해시 크레이트입니다. BLAKE2b를 내부 해시 함수로, BLAMKA 혼합 함수를 메모리 경화 연산으로 사용하며, 민감 데이터는 전적으로 `SecureBuffer`(mlock)에 보관합니다.
+
+## 보안 위협 모델
+
+GPU 및 ASIC 기반 대규모 병렬 공격은 메모리 대역폭이 아닌 연산 능력에 의존하는 해시 함수를 빠르게 무력화합니다. Argon2id는 다음 세 가지 공격 경로를 동시에 차단하도록 설계되었습니다.
+
+- **연산 경화(Computation-Hardness)**: 시간 비용 파라미터 `t`가 반복 횟수를 강제하여 최소 연산량을 보장합니다.
+- **메모리 경화(Memory-Hardness)**: 메모리 비용 파라미터 `m`이 KiB 단위의 작업 메모리를 강제하여 ASIC의 다이 면적 이점을 무력화합니다.
+- **부채널 저항(Side-Channel Resistance)**: 패스 0의 슬라이스 0–1을 Argon2i(데이터 독립 주소 지정) 모드로 처리하여 시간 기반 부채널 공격의 입지를 제거합니다.
+
+## 핵심 추상화: Argon2id 구조체
+
+`Argon2id` 구조체는 RFC 9106 파라미터를 캡슐화하며, 생성 시점에 유효성 검사를 수행합니다.
+
+```rust
+pub struct Argon2id {
+ time_cost: u32, // t ≥ 1
+ memory_cost: u32, // m ≥ 8p (KiB)
+ parallelism: u32, // p ∈ [1, 2²⁴−1]
+ tag_length: u32, // τ ≥ 4
+}
+```
+
+내부 상태는 없으며, `hash` 메서드 호출마다 독립적인 연산 흐름이 생성됩니다.
+
+## 알고리즘 구조
+
+### H0: 초기 해시
+
+패스워드, 솔트, 비밀 값, 연관 데이터 및 모든 파라미터를 단일 BLAKE2b-64 호출로 압축합니다.
+
+$$H_0 = \text{BLAKE2b-64}(p \mathbin{\|} \tau \mathbin{\|} m \mathbin{\|} t \mathbin{\|} v \mathbin{\|} y \mathbin{\|} \ell(P) \mathbin{\|} P \mathbin{\|} \ell(S) \mathbin{\|} S \mathbin{\|} \ell(K) \mathbin{\|} K \mathbin{\|} \ell(X) \mathbin{\|} X)$$
+
+### 초기 블록
+
+각 레인 $i$의 첫 두 블록을 H'(가변 출력 BLAKE2b, RFC 9106 Section 3.2)로 초기화합니다.
+
+$$B[i][0] = H'(H_0 \mathbin{\|} \mathtt{LE32}(0) \mathbin{\|} \mathtt{LE32}(i))$$
+$$B[i][1] = H'(H_0 \mathbin{\|} \mathtt{LE32}(1) \mathbin{\|} \mathtt{LE32}(i))$$
+
+H'는 출력 길이 ≤ 64바이트이면 단일 BLAKE2b 호출로 처리하고, 초과 시 중간 체인($r = \lceil T/32 \rceil - 2$, 각 64바이트 해시의 앞 32바이트를 이어 붙인 뒤 최종 `last_len`바이트 해시를 덧붙임)으로 구성됩니다.
+
+### 세그먼트 채우기 및 Argon2id 하이브리드 모드
+
+블록 배열은 $p$개의 레인으로 나뉘며, 각 레인은 $q = m'/p$개의 블록을 포함합니다. 레인은 4개의 동기화 지점(SYNC\_POINTS)으로 분할된 세그먼트 단위로 처리됩니다.
+
+| 패스 | 슬라이스 | 주소 지정 모드 |
+|-----|------|------------------|
+| 0 | 0, 1 | 데이터 독립 (Argon2i) |
+| 0 | 2, 3 | 데이터 의존 (Argon2d) |
+| ≥ 1 | 모두 | 데이터 의존 (Argon2d) |
+
+**데이터 독립 모드(Argon2i)**: 의사난수를 입력 블록으로부터 직접 읽지 않고, 별도의 주소 블록(`addr_block`)에서 취득합니다. 주소 블록은 카운터 기반 입력(`addr_input`)에 `block_g`를 두 번 적용하여 생성됩니다. 이로써 참조 인덱스가 비밀 메모리 내용에 의존하지 않습니다.
+
+**데이터 의존 모드(Argon2d)**: 이전 블록(`B[lane][prev]`)의 첫 번째 64비트 워드를 의사난수로 사용합니다.
+
+### phi 함수: 참조 블록 선택
+
+의사난수 $J_1$(하위 32비트), $J_2$(상위 32비트)로부터 참조 레인 $l$과 참조 열 $z$를 결정합니다.
+
+$$x = \left\lfloor J_1^2 / 2^{32} \right\rfloor, \quad y = \left\lfloor |R| \cdot x / 2^{32} \right\rfloor$$
+$$z = (\text{start} + |R| - 1 - y) \bmod q$$
+
+여기서 $|R|$은 참조 가능 영역 크기이며, `start`는 패스·슬라이스 조합에 따라 결정됩니다. 이 공식은 균등 분포에 근사하는 이차 함수 샘플링으로, 최근 블록에 더 높은 선택 확률을 부여합니다.
+
+### 최종화
+
+모든 패스 완료 후 각 레인의 마지막 블록을 XOR하여 최종 블록 $C$를 구성하고, H'로 태그를 추출합니다.
+
+$$C = \bigoplus_{i=0}^{p-1} B[i][q-1], \quad \text{tag} = H'(C, \tau)$$
+
+## BLAMKA 혼합 함수
+
+BLAMKA(BLAke2 Mixed cAr Key Algorithm)는 BLAKE2b의 G 함수에 64비트 곱셈 항을 추가하여 메모리 경화성을 강화한 혼합 함수입니다.
+
+### G_B 함수
+
+$$a \mathrel{+}= b + 2 \cdot (a_{32} \cdot b_{32}), \quad d = (d \oplus a) \ggg 32$$
+$$c \mathrel{+}= d + 2 \cdot (c_{32} \cdot d_{32}), \quad b = (b \oplus c) \ggg 24$$
+$$a \mathrel{+}= b + 2 \cdot (a_{32} \cdot b_{32}), \quad d = (d \oplus a) \ggg 16$$
+$$c \mathrel{+}= d + 2 \cdot (c_{32} \cdot d_{32}), \quad b = (b \oplus c) \ggg 63$$
+
+여기서 $a_{32} = a \mathbin{\&} \texttt{0xFFFF\_FFFF}$(하위 32비트)입니다. 곱셈 항 $2 \cdot a_{32} \cdot b_{32}$은 ASIC에서 병렬 처리 비용을 증가시켜 하드웨어 저항성을 제공합니다.
+
+### block_g 함수
+
+1024바이트(128 × u64) 블록에 대한 Argon2 혼합 함수입니다.
+
+$$R = X \oplus Y$$
+$$Z = \text{BLAMKA}(R)$$
+$$\text{dst} = R \oplus Z \quad (\text{XOR 모드이면 } \text{dst} \mathrel{\oplus}= R \oplus Z)$$
+
+BLAMKA 순열은 블록을 8행 × 8열의 행렬(각 셀 = 2 u64)로 해석하여 행 우선, 열 우선으로 `blamka_round`를 적용합니다.
+
+- **행 처리**: 연속된 16개 워드(`row * 16 .. row * 16 + 16`)에 1회 적용, 8회 반복
+- **열 처리**: 스트라이드 패턴 `z[row * 16 + col * 2 + offset]`으로 16개 워드를 추출하여 적용, 8회 반복
+
+## 메모리 보안
+
+| 메커니즘 | 적용 대상 |
+|--------------------------|----------------------------|
+| `SecureBuffer` (mlock) | H0, 태그, BLAKE2b 내부 상태 |
+| `write_volatile` | 연산 완료 후 작업 메모리 전체 소거 |
+| `compiler_fence(SeqCst)` | 컴파일러가 소거 연산을 재배치·제거하는 것 방지 |
+
+작업 블록 배열(`Vec<[u64; 128]>`)은 소거 후 drop됩니다. 소거는 각 워드에 `write_volatile(ptr, 0u64)`을 직접 호출하여 컴파일러 최적화 경로를 우회합니다.
+
+## 파라미터 유효성 검사 (RFC 9106)
+
+| 파라미터 | 조건 |
+|---------------|---------------------|
+| `time_cost` | ≥ 1 |
+| `memory_cost` | ≥ 8 × `parallelism` |
+| `parallelism` | 1 ≤ p ≤ 2²⁴ − 1 |
+| `tag_length` | ≥ 4 |
+| `salt` | ≥ 8바이트 (호출 시 검사) |
+
+세그먼트 길이 `sl = q / 4`가 2 미만이면 메모리가 지나치게 작은 것으로 판단하여 오류를 반환합니다.
+
+## 사용 예시
+
+```rust
+use entlib_native_argon2id::Argon2id;
+
+// NIST SP 800-63B 권고: m ≥ 19456 KiB, t ≥ 2, p = 1
+let params = Argon2id::new(2, 19456, 1, 32).unwrap();
+let tag = params.hash(
+ b"correct horse battery staple",
+ b"random_salt_16b!",
+ &[],
+ &[],
+).unwrap();
+assert_eq!(tag.as_slice().len(), 32);
+```
+
+## 테스트 벡터
+
+RFC 9106 Appendix B.4 공식 테스트 벡터를 내장합니다.
+
+| 파라미터 | 값 |
+|---------------|---------------------------------------------------------------------------|
+| `time_cost` | 3 |
+| `memory_cost` | 32 KiB |
+| `parallelism` | 4 |
+| `tag_length` | 32 |
+| `password` | `0x01` × 32 |
+| `salt` | `0x02` × 16 |
+| `secret` | `0x03` × 8 |
+| `ad` | `0x04` × 12 |
+| 예상 태그 | `0d640df5 8d78766c 08c037a3 4a8b53c9 d01ef045 2d75b65e b52520e9 6b01e659` |
+
+## 의존성
+
+| 크레이트 | 용도 |
+|-------------------------------|------------------------------|
+| `entlib-native-blake` | BLAKE2b 및 H'(`blake2b_long`) |
+| `entlib-native-secure-buffer` | 민감 데이터 mlock 보관 |
diff --git a/crypto/argon2id/README_EN.md b/crypto/argon2id/README_EN.md
new file mode 100644
index 0000000..1da8af4
--- /dev/null
+++ b/crypto/argon2id/README_EN.md
@@ -0,0 +1,163 @@
+# Argon2id Password Hash Function (entlib-native-argon2id)
+
+> Q. T. Felix (Modified: 26.03.23 UTC+9)
+>
+> [Korean README](README.md)
+
+`entlib-native-argon2id` is a `no_std` compatible Argon2id password hash crate that complies with RFC 9106 and NIST SP 800-63B. It uses BLAKE2b as the internal hash function, the BLAMKA mixing function for memory hardening operations, and stores sensitive data entirely in `SecureBuffer` (mlock).
+
+## Security Threat Model
+
+Massively parallel attacks based on GPUs and ASICs quickly neutralize hash functions that rely on computational power rather than memory bandwidth. Argon2id is designed to simultaneously block the following three attack paths:
+
+- **Computation-Hardness**: The time cost parameter `t` forces a number of iterations to ensure a minimum amount of computation.
+- **Memory-Hardness**: The memory cost parameter `m` forces working memory in KiB units to neutralize the die area advantage of ASICs.
+- **Side-Channel Resistance**: Slices 0–1 of pass 0 are processed in Argon2i (data-independent addressing) mode to eliminate the foothold for time-based side-channel attacks.
+
+## Core Abstraction: Argon2id Struct
+
+The `Argon2id` struct encapsulates the RFC 9106 parameters and performs validation at the time of creation.
+
+```rust
+pub struct Argon2id {
+ time_cost: u32, // t ≥ 1
+ memory_cost: u32, // m ≥ 8p (KiB)
+ parallelism: u32, // p ∈ [1, 2²⁴−1]
+ tag_length: u32, // τ ≥ 4
+}
+```
+
+There is no internal state, and an independent operation flow is created for each `hash` method call.
+
+## Algorithm Structure
+
+### H0: Initial Hash
+
+Compresses the password, salt, secret value, associated data, and all parameters with a single BLAKE2b-64 call.
+
+$$H_0 = \text{BLAKE2b-64}(p \mathbin{\|} \tau \mathbin{\|} m \mathbin{\|} t \mathbin{\|} v \mathbin{\|} y \mathbin{\|} \ell(P) \mathbin{\|} P \mathbin{\|} \ell(S) \mathbin{\|} S \mathbin{\|} \ell(K) \mathbin{\|} K \mathbin{\|} \ell(X) \mathbin{\|} X)$$
+
+### Initial Blocks
+
+Initializes the first two blocks of each lane $i$ with H' (variable output BLAKE2b, RFC 9106 Section 3.2).
+
+$$B[i][0] = H'(H_0 \mathbin{\|} \mathtt{LE32}(0) \mathbin{\|} \mathtt{LE32}(i))$$
+$$B[i][1] = H'(H_0 \mathbin{\|} \mathtt{LE32}(1) \mathbin{\|} \mathtt{LE32}(i))$$
+
+H' is processed with a single BLAKE2b call if the output length is ≤ 64 bytes, and if it exceeds, it is composed of an intermediate chain ($r = \lceil T/32 \rceil - 2$, concatenating the first 32 bytes of each 64-byte hash and then appending the final `last_len`-byte hash).
+
+### Segment Filling and Argon2id Hybrid Mode
+
+The block array is divided into $p$ lanes, and each lane contains $q = m'/p$ blocks. The lanes are processed in segment units divided by 4 synchronization points (SYNC\_POINTS).
+
+| Pass | Slice | Addressing Mode |
+|------|-------|----------------------------|
+| 0 | 0, 1 | Data-independent (Argon2i) |
+| 0 | 2, 3 | Data-dependent (Argon2d) |
+| ≥ 1 | All | Data-dependent (Argon2d) |
+
+**Data-independent mode (Argon2i)**: The pseudorandom number is not read directly from the input block, but is obtained from a separate address block (`addr_block`). The address block is generated by applying `block_g` twice to a counter-based input (`addr_input`). This prevents the reference index from depending on the secret memory content.
+
+**Data-dependent mode (Argon2d)**: The first 64-bit word of the previous block (`B[lane][prev]`) is used as a pseudorandom number.
+
+### phi function: Reference Block Selection
+
+Determines the reference lane $l$ and reference column $z$ from the pseudorandom numbers $J_1$ (lower 32 bits) and $J_2$ (upper 32 bits).
+
+$$x = \left\lfloor J_1^2 / 2^{32} \right\rfloor, \quad y = \left\lfloor |R| \cdot x / 2^{32} \right\rfloor$$
+$$z = (\text{start} + |R| - 1 - y) \bmod q$$
+
+Here, $|R|$ is the size of the referenceable area, and `start` is determined by the pass-slice combination. This formula is a quadratic function sampling that approximates a uniform distribution, giving a higher selection probability to recent blocks.
+
+### Finalization
+
+After all passes are complete, the last block of each lane is XORed to form the final block $C$, and the tag is extracted with H'.
+
+$$C = \bigoplus_{i=0}^{p-1} B[i][q-1], \quad \text{tag} = H'(C, \tau)$$
+
+## BLAMKA Mixing Function
+
+BLAMKA (BLAke2 Mixed cAr Key Algorithm) is a mixing function that enhances memory hardness by adding a 64-bit multiplication term to the G function of BLAKE2b.
+
+### G_B Function
+
+$$a \mathrel{+}= b + 2 \cdot (a_{32} \cdot b_{32}), \quad d = (d \oplus a) \ggg 32$$
+$$c \mathrel{+}= d + 2 \cdot (c_{32} \cdot d_{32}), \quad b = (b \oplus c) \ggg 24$$
+$$a \mathrel{+}= b + 2 \cdot (a_{32} \cdot b_{32}), \quad d = (d \oplus a) \ggg 16$$
+$$c \mathrel{+}= d + 2 \cdot (c_{32} \cdot d_{32}), \quad b = (b \oplus c) \ggg 63$$
+
+Here, $a_{32} = a \mathbin{\&} \texttt{0xFFFF\_FFFF}$ (lower 32 bits). The multiplication term $2 \cdot a_{32} \cdot b_{32}$ increases the cost of parallel processing in ASICs, providing hardware resistance.
+
+### block_g function
+
+The Argon2 mixing function for a 1024-byte (128 × u64) block.
+
+$$R = X \oplus Y$$
+$$Z = \text{BLAMKA}(R)$$
+$$\text{dst} = R \oplus Z \quad (\text{if in XOR mode, } \text{dst} \mathrel{\oplus}= R \oplus Z)$$
+
+The BLAMKA permutation interprets the block as an 8-row × 8-column matrix (each cell = 2 u64) and applies `blamka_round` row-wise and column-wise.
+
+- **Row processing**: Applied once to 16 consecutive words (`row * 16 .. row * 16 + 16`), repeated 8 times
+- **Column processing**: 16 words are extracted with the stride pattern `z[row * 16 + col * 2 + offset]` and applied, repeated 8 times
+
+## Memory Security
+
+| Mechanism | Applied to |
+|--------------------------|----------------------------------------------------------------------------|
+| `SecureBuffer` (mlock) | H0, tag, BLAKE2b internal state |
+| `write_volatile` | Erase the entire working memory after the operation is complete |
+| `compiler_fence(SeqCst)` | Prevents the compiler from reordering or eliminating the erasure operation |
+
+The working block array (`Vec<[u64; 128]>`) is dropped after being erased. The erasure bypasses the compiler optimization path by directly calling `write_volatile(ptr, 0u64)` on each word.
+
+## Parameter Validation (RFC 9106)
+
+| Parameter | Condition |
+|---------------|----------------------------------|
+| `time_cost` | ≥ 1 |
+| `memory_cost` | ≥ 8 × `parallelism` |
+| `parallelism` | 1 ≤ p ≤ 2²⁴ − 1 |
+| `tag_length` | ≥ 4 |
+| `salt` | ≥ 8 bytes (checked at call time) |
+
+If the segment length `sl = q / 4` is less than 2, the memory is judged to be too small and an error is returned.
+
+## Usage Example
+
+```rust
+use entlib_native_argon2id::Argon2id;
+
+// NIST SP 800-63B recommendation: m ≥ 19456 KiB, t ≥ 2, p = 1
+let params = Argon2id::new(2, 19456, 1, 32).unwrap();
+let tag = params.hash(
+ b"correct horse battery staple",
+ b"random_salt_16b!",
+ &[],
+ &[],
+).unwrap();
+assert_eq!(tag.as_slice().len(), 32);
+```
+
+## Test Vectors
+
+Includes the official test vectors from RFC 9106 Appendix B.4.
+
+| Parameter | Value |
+|---------------|---------------------------------------------------------------------------|
+| `time_cost` | 3 |
+| `memory_cost` | 32 KiB |
+| `parallelism` | 4 |
+| `tag_length` | 32 |
+| `password` | `0x01` × 32 |
+| `salt` | `0x02` × 16 |
+| `secret` | `0x03` × 8 |
+| `ad` | `0x04` × 12 |
+| Expected tag | `0d640df5 8d78766c 08c037a3 4a8b53c9 d01ef045 2d75b65e b52520e9 6b01e659` |
+
+## Dependencies
+
+| Crate | Purpose |
+|-------------------------------|----------------------------------|
+| `entlib-native-blake` | BLAKE2b and H'(`blake2b_long`) |
+| `entlib-native-secure-buffer` | mlock storage for sensitive data |
diff --git a/crypto/argon2id/src/blamka.rs b/crypto/argon2id/src/blamka.rs
new file mode 100644
index 0000000..ae417f2
--- /dev/null
+++ b/crypto/argon2id/src/blamka.rs
@@ -0,0 +1,125 @@
+//! BLAMKA(BLAke2 Mixed cAr Key Algorithm) 함수 모듈입니다.
+//! RFC 9106 Section 3.5에서 정의된 Argon2 블록 혼합 함수를 구현합니다.
+
+/// BLAMKA G_B 함수 — 64비트 곱셈 추가 혼합입니다.
+///
+/// # Security Note
+/// 64비트 곱셈 항(`2 * lo32(a) * lo32(b)`)이 메모리 경화성을 제공합니다.
+/// 하드웨어 병렬 처리 저항이 핵심 보안 속성입니다.
+#[inline(always)]
+pub(crate) fn gb(a: u64, b: u64, c: u64, d: u64) -> (u64, u64, u64, u64) {
+ let a = a.wrapping_add(b).wrapping_add(
+ 2u64.wrapping_mul(a & 0xFFFF_FFFF)
+ .wrapping_mul(b & 0xFFFF_FFFF),
+ );
+ let d = (d ^ a).rotate_right(32);
+ let c = c.wrapping_add(d).wrapping_add(
+ 2u64.wrapping_mul(c & 0xFFFF_FFFF)
+ .wrapping_mul(d & 0xFFFF_FFFF),
+ );
+ let b = (b ^ c).rotate_right(24);
+ let a = a.wrapping_add(b).wrapping_add(
+ 2u64.wrapping_mul(a & 0xFFFF_FFFF)
+ .wrapping_mul(b & 0xFFFF_FFFF),
+ );
+ let d = (d ^ a).rotate_right(16);
+ let c = c.wrapping_add(d).wrapping_add(
+ 2u64.wrapping_mul(c & 0xFFFF_FFFF)
+ .wrapping_mul(d & 0xFFFF_FFFF),
+ );
+ let b = (b ^ c).rotate_right(63);
+ (a, b, c, d)
+}
+
+/// 16-워드(128바이트) 슬라이스에 BLAMKA 라운드를 적용하는 함수입니다.
+///
+/// 4회 열(column) 혼합 + 4회 대각선(diagonal) 혼합 = 1 라운드.
+#[inline(always)]
+pub(crate) fn blamka_round(v: &mut [u64]) {
+ // column mixing
+ let (a, b, c, d) = gb(v[0], v[4], v[8], v[12]);
+ v[0] = a;
+ v[4] = b;
+ v[8] = c;
+ v[12] = d;
+ let (a, b, c, d) = gb(v[1], v[5], v[9], v[13]);
+ v[1] = a;
+ v[5] = b;
+ v[9] = c;
+ v[13] = d;
+ let (a, b, c, d) = gb(v[2], v[6], v[10], v[14]);
+ v[2] = a;
+ v[6] = b;
+ v[10] = c;
+ v[14] = d;
+ let (a, b, c, d) = gb(v[3], v[7], v[11], v[15]);
+ v[3] = a;
+ v[7] = b;
+ v[11] = c;
+ v[15] = d;
+ // diagonal mixing
+ let (a, b, c, d) = gb(v[0], v[5], v[10], v[15]);
+ v[0] = a;
+ v[5] = b;
+ v[10] = c;
+ v[15] = d;
+ let (a, b, c, d) = gb(v[1], v[6], v[11], v[12]);
+ v[1] = a;
+ v[6] = b;
+ v[11] = c;
+ v[12] = d;
+ let (a, b, c, d) = gb(v[2], v[7], v[8], v[13]);
+ v[2] = a;
+ v[7] = b;
+ v[8] = c;
+ v[13] = d;
+ let (a, b, c, d) = gb(v[3], v[4], v[9], v[14]);
+ v[3] = a;
+ v[4] = b;
+ v[9] = c;
+ v[14] = d;
+}
+
+/// Argon2 블록 G 함수입니다.
+///
+/// R = X XOR Y 를 계산한 뒤 BLAMKA를 적용하고 R과 XOR합니다.
+/// `dst`에 결과를 기록합니다. pass > 0이면 기존 `dst`와 XOR합니다.
+pub(crate) fn block_g(dst: &mut [u64; 128], x: &[u64; 128], y: &[u64; 128], xor: bool) {
+ let mut r = [0u64; 128];
+ for i in 0..128 {
+ r[i] = x[i] ^ y[i];
+ }
+
+ // Z = BLAMKA permutation of R
+ // 블록을 8×8 행렬(각 셀 = 16 u64)로 해석:
+ // 8개 행(row) 처리: 각 행 = 연속적인 16개 워드
+ let mut z = r;
+ for row in 0..8 {
+ blamka_round(&mut z[row * 16..(row + 1) * 16]);
+ }
+ // 8개 열(column) 처리: 각 열 = stride-8로 떨어진 16개 워드
+ for col in 0..8 {
+ let mut tmp = [0u64; 16];
+ for (i, item) in tmp.iter_mut().enumerate() {
+ let row = i / 2;
+ let word = (i % 2) + col * 2;
+ *item = z[row * 16 + word];
+ }
+ blamka_round(&mut tmp);
+ for (i, &item) in tmp.iter().enumerate() {
+ let row = i / 2;
+ let word = (i % 2) + col * 2;
+ z[row * 16 + word] = item;
+ }
+ }
+
+ if xor {
+ for i in 0..128 {
+ dst[i] ^= r[i] ^ z[i];
+ }
+ } else {
+ for i in 0..128 {
+ dst[i] = r[i] ^ z[i];
+ }
+ }
+}
diff --git a/crypto/argon2id/src/lib.rs b/crypto/argon2id/src/lib.rs
new file mode 100644
index 0000000..5002c40
--- /dev/null
+++ b/crypto/argon2id/src/lib.rs
@@ -0,0 +1,409 @@
+//! RFC 9106 준수 Argon2id 패스워드 해시 함수 모듈입니다.
+//!
+//! BLAKE2b를 내부 해시 함수로 사용하며, 메모리 경화(memory-hardness)를
+//! 통해 GPU/ASIC 공격을 방어합니다. NIST SP 800-63B 권고를 준수합니다.
+//!
+//! # Security Note
+//! - 패스워드와 최종 태그는 `SecureBuffer`(mlock)에 보관됩니다.
+//! - 모든 메모리 블록은 연산 완료 후 `write_volatile`로 강제 소거됩니다.
+//! - BLAMKA 64비트 곱셈이 ASIC 저항을 제공합니다.
+//! - Argon2id 하이브리드 모드: 패스 0 슬라이스 0-1(Argon2i) + 나머지(Argon2d).
+//!
+//! # Examples
+//! ```
+//! use entlib_native_argon2id::Argon2id;
+//!
+//! let params = Argon2id::new(1, 8192, 1, 32).unwrap();
+//! let tag = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+//! assert_eq!(tag.as_slice().len(), 32);
+//! ```
+
+mod blamka;
+
+use blamka::block_g;
+use core::ptr::write_volatile;
+use core::sync::atomic::{Ordering, compiler_fence};
+use entlib_native_base::error::argon2id::Argon2idError;
+use entlib_native_blake::{Blake2b, blake2b_long};
+use entlib_native_secure_buffer::SecureBuffer;
+
+const ARGON2ID_TYPE: u32 = 2;
+const ARGON2_VERSION: u32 = 0x13;
+const SYNC_POINTS: usize = 4;
+
+/// Argon2id 파라미터 및 해시 연산 구조체입니다.
+pub struct Argon2id {
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+ tag_length: u32,
+}
+
+impl Argon2id {
+ /// Argon2id 인스턴스를 생성하는 함수입니다.
+ ///
+ /// # Errors
+ /// 파라미터가 RFC 9106 범위를 벗어나면 `Err`.
+ pub fn new(
+ time_cost: u32,
+ memory_cost: u32,
+ parallelism: u32,
+ tag_length: u32,
+ ) -> Result {
+ if parallelism == 0 || parallelism > 0x00FF_FFFF {
+ return Err(Argon2idError::InvalidParameter);
+ }
+ if time_cost == 0 {
+ return Err(Argon2idError::InvalidParameter);
+ }
+ if memory_cost < 8 * parallelism {
+ return Err(Argon2idError::InvalidParameter);
+ }
+ if tag_length < 4 {
+ return Err(Argon2idError::InvalidParameter);
+ }
+ Ok(Self {
+ time_cost,
+ memory_cost,
+ parallelism,
+ tag_length,
+ })
+ }
+
+ /// 패스워드를 해시하여 태그를 SecureBuffer로 반환하는 함수입니다.
+ ///
+ /// # Arguments
+ /// - `password` — 해시할 패스워드
+ /// - `salt` — 솔트 (최소 8바이트)
+ /// - `secret` — 추가 비밀 값 (0..=32 bytes, 선택)
+ /// - `ad` — 연관 데이터 (선택)
+ ///
+ /// # Errors
+ /// 솔트 < 8바이트 또는 SecureBuffer 할당 실패 시 `Err`.
+ pub fn hash(
+ &self,
+ password: &[u8],
+ salt: &[u8],
+ secret: &[u8],
+ ad: &[u8],
+ ) -> Result {
+ if salt.len() < 8 {
+ return Err(Argon2idError::InvalidParameter);
+ }
+
+ let p = self.parallelism as usize;
+ let t = self.time_cost as usize;
+ let m = self.memory_cost as usize;
+
+ // m' = floor(m/(4p)) * 4p — 4p의 배수
+ let m_prime = (m / (4 * p)) * (4 * p);
+ let q = m_prime / p; // 레인당 블록 수
+ let sl = q / SYNC_POINTS; // 세그먼트 길이
+
+ if sl < 2 {
+ return Err(Argon2idError::InvalidParameter);
+ }
+
+ // H0: 초기 512비트 해시
+ let h0 = compute_h0(
+ p as u32,
+ self.tag_length,
+ m as u32,
+ t as u32,
+ ARGON2_VERSION,
+ ARGON2ID_TYPE,
+ password,
+ salt,
+ secret,
+ ad,
+ )?;
+
+ // 메모리 할당
+ let total = p * q;
+ let mut blocks: Vec<[u64; 128]> = vec![[0u64; 128]; total];
+
+ // 초기 두 블록 초기화
+ for lane in 0..p {
+ let h0_0 = concat_h0(h0.as_slice(), 0u32, lane as u32);
+ let b0 = blake2b_long(&h0_0, 1024)?;
+ copy_to_block(&mut blocks[lane * q], b0.as_slice());
+
+ let h0_1 = concat_h0(h0.as_slice(), 1u32, lane as u32);
+ let b1 = blake2b_long(&h0_1, 1024)?;
+ copy_to_block(&mut blocks[lane * q + 1], b1.as_slice());
+ }
+
+ // 패스 채우기
+ for pass in 0..t {
+ for slice in 0..SYNC_POINTS {
+ for lane in 0..p {
+ fill_segment(&mut blocks, pass, slice, lane, p, q, sl, t);
+ }
+ }
+ }
+
+ // 최종화
+ // C = XOR of B[i][q-1]
+ let mut c = blocks[q - 1];
+ for lane in 1..p {
+ let b = blocks[lane * q + (q - 1)];
+ for i in 0..128 {
+ c[i] ^= b[i];
+ }
+ }
+
+ let c_bytes = block_to_bytes(&c);
+ let tag = blake2b_long(&c_bytes, self.tag_length as usize)?;
+
+ // 메모리 소거
+ for block in &mut blocks {
+ for word in block.iter_mut() {
+ unsafe { write_volatile(word, 0u64) };
+ }
+ }
+ compiler_fence(Ordering::SeqCst);
+
+ Ok(tag)
+ }
+}
+
+//
+// 세그먼트 채우기
+//
+
+#[allow(clippy::too_many_arguments)]
+fn fill_segment(
+ blocks: &mut [[u64; 128]],
+ pass: usize,
+ slice: usize,
+ lane: usize,
+ p: usize,
+ q: usize,
+ sl: usize,
+ t: usize,
+) {
+ // Argon2id: 패스 0, 슬라이스 0-1 = 데이터 독립(Argon2i), 나머지 = Argon2d
+ let data_independent = pass == 0 && slice < 2;
+
+ let mut addr_input = [0u64; 128];
+ let mut addr_block = [0u64; 128];
+
+ if data_independent {
+ addr_input[0] = pass as u64;
+ addr_input[1] = lane as u64;
+ addr_input[2] = slice as u64;
+ addr_input[3] = (p * q) as u64;
+ addr_input[4] = t as u64;
+ addr_input[5] = ARGON2ID_TYPE as u64;
+ addr_input[6] = 0;
+ }
+
+ let start_col = if pass == 0 && slice == 0 { 2 } else { 0 };
+
+ for col_in_seg in start_col..sl {
+ let col = slice * sl + col_in_seg;
+ let cur_idx = lane * q + col;
+ let prev_col = if col == 0 { q - 1 } else { col - 1 };
+ let prev_idx = lane * q + prev_col;
+
+ // 의사난수 취득
+ let pseudo_rand: u64 = if data_independent {
+ if col_in_seg % 128 == 0 {
+ addr_input[6] += 1;
+ let zero = [0u64; 128];
+ let mut tmp = [0u64; 128];
+ block_g(&mut tmp, &zero, &addr_input, false);
+ block_g(&mut addr_block, &zero, &tmp, false);
+ }
+ addr_block[col_in_seg % 128]
+ } else {
+ blocks[prev_idx][0]
+ };
+
+ let j1 = pseudo_rand & 0xFFFF_FFFF;
+ let j2 = pseudo_rand >> 32;
+
+ // 참조 레인
+ let ref_lane = if pass == 0 && slice == 0 {
+ lane
+ } else {
+ (j2 as usize) % p
+ };
+
+ let same_lane = ref_lane == lane;
+
+ // 참조 영역 크기
+ let ref_area: usize = if pass == 0 {
+ if slice == 0 {
+ col_in_seg.saturating_sub(1)
+ } else if same_lane {
+ slice * sl + col_in_seg - 1
+ } else {
+ slice * sl - usize::from(col_in_seg == 0)
+ }
+ } else if same_lane {
+ q - sl + col_in_seg - 1
+ } else {
+ q - sl - usize::from(col_in_seg == 0)
+ };
+
+ // phi 함수 → 참조 열 인덱스
+ let ref_col = if ref_area == 0 {
+ 0
+ } else {
+ let x = j1.wrapping_mul(j1) >> 32;
+ let y = (ref_area as u64).wrapping_mul(x) >> 32;
+ let relative = ref_area - 1 - y as usize;
+ let start = if pass == 0 || slice == SYNC_POINTS - 1 {
+ 0
+ } else {
+ (slice + 1) * sl
+ };
+ (start + relative) % q
+ };
+
+ let ref_idx = ref_lane * q + ref_col;
+ let xor = pass > 0;
+
+ // G(B_prev, B_ref) → B_cur
+ // 인덱스 충돌 방지: 임시 복사 사용
+ let prev_copy = blocks[prev_idx];
+ let ref_copy = blocks[ref_idx];
+ block_g(&mut blocks[cur_idx], &prev_copy, &ref_copy, xor);
+ }
+}
+
+//
+// 헬퍼
+//
+
+#[allow(clippy::too_many_arguments)]
+fn compute_h0(
+ parallelism: u32,
+ tag_length: u32,
+ memory_cost: u32,
+ time_cost: u32,
+ version: u32,
+ argon2_type: u32,
+ password: &[u8],
+ salt: &[u8],
+ secret: &[u8],
+ ad: &[u8],
+) -> Result {
+ let mut h = Blake2b::new(64);
+ h.update(¶llelism.to_le_bytes());
+ h.update(&tag_length.to_le_bytes());
+ h.update(&memory_cost.to_le_bytes());
+ h.update(&time_cost.to_le_bytes());
+ h.update(&version.to_le_bytes());
+ h.update(&argon2_type.to_le_bytes());
+ h.update(&(password.len() as u32).to_le_bytes());
+ h.update(password);
+ h.update(&(salt.len() as u32).to_le_bytes());
+ h.update(salt);
+ h.update(&(secret.len() as u32).to_le_bytes());
+ h.update(secret);
+ h.update(&(ad.len() as u32).to_le_bytes());
+ h.update(ad);
+ Ok(h.finalize()?)
+}
+
+fn concat_h0(h0: &[u8], idx: u32, lane: u32) -> Vec {
+ let mut v = Vec::with_capacity(h0.len() + 8);
+ v.extend_from_slice(h0);
+ v.extend_from_slice(&idx.to_le_bytes());
+ v.extend_from_slice(&lane.to_le_bytes());
+ v
+}
+
+fn copy_to_block(block: &mut [u64; 128], bytes: &[u8]) {
+ for (i, word) in block.iter_mut().enumerate() {
+ let s = i * 8;
+ *word = u64::from_le_bytes([
+ bytes[s],
+ bytes[s + 1],
+ bytes[s + 2],
+ bytes[s + 3],
+ bytes[s + 4],
+ bytes[s + 5],
+ bytes[s + 6],
+ bytes[s + 7],
+ ]);
+ }
+}
+
+fn block_to_bytes(block: &[u64; 128]) -> Vec {
+ let mut v = Vec::with_capacity(1024);
+ for word in block {
+ v.extend_from_slice(&word.to_le_bytes());
+ }
+ v
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // RFC 9106 Appendix B.4 테스트 벡터
+ // t=3, m=32 (32 KiB), p=4, tag_length=32
+ #[test]
+ fn rfc9106_test_vector() {
+ let password = [0x01u8; 32];
+ let salt = [0x02u8; 16];
+ let secret = [0x03u8; 8];
+ let ad = [0x04u8; 12];
+
+ let params = Argon2id::new(3, 32, 4, 32).unwrap();
+ let tag = params.hash(&password, &salt, &secret, &ad).unwrap();
+
+ let expected = [
+ 0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b,
+ 0x53, 0xc9, 0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e, 0xb5, 0x25, 0x20, 0xe9,
+ 0x6b, 0x01, 0xe6, 0x59,
+ ];
+ assert_eq!(
+ tag.as_slice(),
+ &expected,
+ "RFC 9106 test vector mismatch\ngot: {:02x?}\nwant: {:02x?}",
+ tag.as_slice(),
+ &expected
+ );
+ }
+
+ #[test]
+ fn basic_hash_length() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let tag = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_eq!(tag.as_slice().len(), 32);
+ }
+
+ #[test]
+ fn different_passwords_give_different_tags() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"password1", b"somesalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"password2", b"somesalt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+ }
+
+ #[test]
+ fn different_salts_give_different_tags() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"password", b"otherslt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+ }
+
+ #[test]
+ fn salt_too_short_rejected() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ assert!(params.hash(b"password", b"short", &[], &[]).is_err());
+ }
+
+ #[test]
+ fn invalid_params_rejected() {
+ assert!(Argon2id::new(0, 64, 1, 32).is_err()); // time_cost = 0
+ assert!(Argon2id::new(1, 4, 1, 32).is_err()); // memory too small
+ assert!(Argon2id::new(1, 64, 0, 32).is_err()); // parallelism = 0
+ assert!(Argon2id::new(1, 64, 1, 3).is_err()); // tag_length < 4
+ }
+}
diff --git a/crypto/argon2id/tests/argon2id_test.rs b/crypto/argon2id/tests/argon2id_test.rs
new file mode 100644
index 0000000..6b2f747
--- /dev/null
+++ b/crypto/argon2id/tests/argon2id_test.rs
@@ -0,0 +1,251 @@
+use entlib_native_argon2id::Argon2id;
+
+//
+// RFC 9106 Appendix B.4 공식 테스트 벡터
+//
+
+#[test]
+fn rfc9106_b4_test_vector() {
+ let password = [0x01u8; 32];
+ let salt = [0x02u8; 16];
+ let secret = [0x03u8; 8];
+ let ad = [0x04u8; 12];
+
+ let params = Argon2id::new(3, 32, 4, 32).unwrap();
+ let tag = params.hash(&password, &salt, &secret, &ad).unwrap();
+
+ let expected = [
+ 0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c, 0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53,
+ 0xc9, 0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e, 0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01,
+ 0xe6, 0x59,
+ ];
+ assert_eq!(
+ tag.as_slice(),
+ &expected,
+ "RFC 9106 B.4 벡터 불일치\ngot: {:02x?}\nwant: {:02x?}",
+ tag.as_slice(),
+ &expected
+ );
+}
+
+//
+// 파라미터 유효성 검사
+//
+
+#[test]
+fn param_time_cost_zero_rejected() {
+ assert!(Argon2id::new(0, 64, 1, 32).is_err());
+}
+
+#[test]
+fn param_memory_too_small_rejected() {
+ // memory_cost < 8 * parallelism
+ assert!(Argon2id::new(1, 7, 1, 32).is_err());
+ assert!(Argon2id::new(1, 31, 4, 32).is_err());
+}
+
+#[test]
+fn param_parallelism_zero_rejected() {
+ assert!(Argon2id::new(1, 64, 0, 32).is_err());
+}
+
+#[test]
+fn param_tag_length_too_short_rejected() {
+ assert!(Argon2id::new(1, 64, 1, 0).is_err());
+ assert!(Argon2id::new(1, 64, 1, 3).is_err());
+}
+
+#[test]
+fn param_minimum_valid_accepted() {
+ assert!(Argon2id::new(1, 8, 1, 4).is_ok());
+}
+
+//
+// 솔트 유효성 검사
+//
+
+#[test]
+fn salt_too_short_rejected() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ assert!(params.hash(b"password", b"short", &[], &[]).is_err());
+ assert!(params.hash(b"password", b"1234567", &[], &[]).is_err()); // 7바이트
+}
+
+#[test]
+fn salt_minimum_length_accepted() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ assert!(params.hash(b"password", b"12345678", &[], &[]).is_ok()); // 8바이트
+}
+
+//
+// 출력 길이
+//
+
+#[test]
+fn tag_length_4() {
+ let params = Argon2id::new(1, 64, 1, 4).unwrap();
+ let tag = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_eq!(tag.as_slice().len(), 4);
+}
+
+#[test]
+fn tag_length_32() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let tag = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_eq!(tag.as_slice().len(), 32);
+}
+
+#[test]
+fn tag_length_64() {
+ let params = Argon2id::new(1, 64, 1, 64).unwrap();
+ let tag = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_eq!(tag.as_slice().len(), 64);
+}
+
+#[test]
+fn tag_length_77() {
+ // H' 가변 출력 경로 검증 (64 < τ)
+ let params = Argon2id::new(1, 64, 1, 77).unwrap();
+ let tag = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_eq!(tag.as_slice().len(), 77);
+}
+
+//
+// 결정론성
+//
+
+#[test]
+fn same_inputs_same_tag() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_eq!(t1.as_slice(), t2.as_slice());
+}
+
+//
+// 도메인 분리: 각 입력 필드가 독립적으로 출력에 영향을 주어야 함
+//
+
+#[test]
+fn different_passwords_give_different_tags() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"password1", b"somesalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"password2", b"somesalt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+#[test]
+fn different_salts_give_different_tags() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"password", b"otherslt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+#[test]
+fn different_secrets_give_different_tags() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params
+ .hash(b"password", b"somesalt", b"secret1!", &[])
+ .unwrap();
+ let t2 = params
+ .hash(b"password", b"somesalt", b"secret2!", &[])
+ .unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+#[test]
+fn different_ad_give_different_tags() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params
+ .hash(b"password", b"somesalt", &[], b"context1")
+ .unwrap();
+ let t2 = params
+ .hash(b"password", b"somesalt", &[], b"context2")
+ .unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+#[test]
+fn empty_secret_and_nonempty_secret_differ() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ let t2 = params
+ .hash(b"password", b"somesalt", b"secret!!", &[])
+ .unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+//
+// 파라미터 변경 시 출력이 달라져야 함
+//
+
+#[test]
+fn different_time_cost_gives_different_tags() {
+ let p1 = Argon2id::new(1, 64, 1, 32).unwrap();
+ let p2 = Argon2id::new(2, 64, 1, 32).unwrap();
+ let t1 = p1.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ let t2 = p2.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+#[test]
+fn different_memory_cost_gives_different_tags() {
+ let p1 = Argon2id::new(1, 64, 1, 32).unwrap();
+ let p2 = Argon2id::new(1, 128, 1, 32).unwrap();
+ let t1 = p1.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ let t2 = p2.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+#[test]
+fn different_parallelism_gives_different_tags() {
+ let p1 = Argon2id::new(1, 64, 1, 32).unwrap();
+ let p2 = Argon2id::new(1, 64, 2, 32).unwrap();
+ let t1 = p1.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ let t2 = p2.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+//
+// 빈 패스워드·솔트 경계
+//
+
+#[test]
+fn empty_password_accepted() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let tag = params.hash(b"", b"somesalt", &[], &[]).unwrap();
+ assert_eq!(tag.as_slice().len(), 32);
+}
+
+#[test]
+fn empty_password_differs_from_nonempty() {
+ let params = Argon2id::new(1, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"", b"somesalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"password", b"somesalt", &[], &[]).unwrap();
+ assert_ne!(t1.as_slice(), t2.as_slice());
+}
+
+//
+// 병렬성 = 1 경계: 단일 레인 경로
+//
+
+#[test]
+fn parallelism_1_produces_consistent_output() {
+ let params = Argon2id::new(2, 64, 1, 32).unwrap();
+ let t1 = params.hash(b"pw", b"saltsalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"pw", b"saltsalt", &[], &[]).unwrap();
+ assert_eq!(t1.as_slice(), t2.as_slice());
+}
+
+//
+// 멀티-레인 경로
+//
+
+#[test]
+fn parallelism_4_produces_consistent_output() {
+ let params = Argon2id::new(2, 64, 4, 32).unwrap();
+ let t1 = params.hash(b"pw", b"saltsalt", &[], &[]).unwrap();
+ let t2 = params.hash(b"pw", b"saltsalt", &[], &[]).unwrap();
+ assert_eq!(t1.as_slice(), t2.as_slice());
+}
diff --git a/crypto/armor/Cargo.toml b/crypto/armor/Cargo.toml
new file mode 100644
index 0000000..ad24547
--- /dev/null
+++ b/crypto/armor/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "entlib-native-armor"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[features]
+default = ["std"]
+std = ["entlib-native-secure-buffer/std"]
+
+[dependencies]
+entlib-native-secure-buffer.workspace = true
+entlib-native-constant-time.workspace = true
+entlib-native-base64.workspace = true
+
+[dev-dependencies]
+entlib-native-hex.workspace = true
diff --git a/crypto/armor/src/asn1/error.rs b/crypto/armor/src/asn1/error.rs
new file mode 100644
index 0000000..ad03eee
--- /dev/null
+++ b/crypto/armor/src/asn1/error.rs
@@ -0,0 +1,12 @@
+//! ASN.1 오류 타입 모듈입니다.
+
+/// ASN.1 관련 연산 중 발생하는 오류 열거형입니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ASN1Error {
+ /// 입력 버퍼가 예상보다 짧음
+ UnexpectedEof,
+ /// 유효하지 않은 OID 인코딩 또는 구조
+ InvalidOid,
+ /// 길이 계산 시 산술 오버플로우
+ LengthOverflow,
+}
diff --git a/crypto/armor/src/asn1/mod.rs b/crypto/armor/src/asn1/mod.rs
new file mode 100644
index 0000000..eb80229
--- /dev/null
+++ b/crypto/armor/src/asn1/mod.rs
@@ -0,0 +1,8 @@
+mod error;
+mod oid;
+mod tag;
+
+pub use error::ASN1Error;
+pub use oid::{OID_MAX_ARCS, Oid};
+pub(crate) use oid::{decode_oid, encode_base128};
+pub use tag::{Tag, TagClass};
diff --git a/crypto/armor/src/asn1/oid.rs b/crypto/armor/src/asn1/oid.rs
new file mode 100644
index 0000000..38024b5
--- /dev/null
+++ b/crypto/armor/src/asn1/oid.rs
@@ -0,0 +1,203 @@
+//! ASN.1 OID(Object Identifier) 타입 모듈입니다.
+//! OID 비교는 알고리즘 식별자 누출을 막기 위해 상수-시간으로 수행합니다.
+
+use entlib_native_constant_time::traits::ConstantTimeEq;
+
+use crate::asn1::error::ASN1Error;
+use crate::error::ArmorError;
+use crate::error::ArmorError::ASN1;
+
+/// OID 최대 아크(arc) 수
+pub const OID_MAX_ARCS: usize = 16;
+
+/// ASN.1 OID를 나타내는 구조체입니다.
+/// 내부 아크 배열은 항상 `OID_MAX_ARCS` 크기로 고정되어 있으며,
+/// `len` 이후의 슬롯은 0으로 패딩되어 상수-시간 비교를 보장합니다.
+#[derive(Clone, Copy, Debug)]
+pub struct Oid {
+ arcs: [u32; OID_MAX_ARCS],
+ len: usize,
+}
+
+impl Oid {
+ /// 아크 슬라이스로 OID를 생성하는 함수입니다.
+ ///
+ /// # Arguments
+ /// `arcs` — OID 아크 배열. 첫 번째 아크는 0, 1, 2 중 하나여야 합니다.
+ ///
+ /// # Errors
+ /// 아크 수가 2 미만이거나 `OID_MAX_ARCS` 초과, 또는 첫 아크 > 2이면 `InvalidOid`.
+ pub fn from_arcs(arcs: &[u32]) -> Result {
+ if arcs.len() < 2 || arcs.len() > OID_MAX_ARCS {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+ if arcs[0] > 2 {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+ // 첫 아크가 0 또는 1이면 두 번째 아크는 반드시 0–39
+ if arcs[0] < 2 && arcs[1] > 39 {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+ // 첫 두 아크의 결합 값(40*a0+a1)이 u32에 맞는지 확인
+ let combined = (arcs[0] as u64)
+ .checked_mul(40)
+ .and_then(|v| v.checked_add(arcs[1] as u64))
+ .ok_or(ASN1(ASN1Error::InvalidOid))?;
+ if combined > u32::MAX as u64 {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+
+ let mut result = Oid {
+ arcs: [0u32; OID_MAX_ARCS],
+ len: arcs.len(),
+ };
+ result.arcs[..arcs.len()].copy_from_slice(arcs);
+ Ok(result)
+ }
+
+ /// OID 아크 슬라이스를 반환합니다.
+ #[inline(always)]
+ pub fn arcs(&self) -> &[u32] {
+ &self.arcs[..self.len]
+ }
+
+ /// 아크 수를 반환합니다.
+ #[inline(always)]
+ pub fn arc_count(&self) -> usize {
+ self.len
+ }
+
+ /// 두 OID를 상수-시간으로 비교하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 알고리즘 식별자 비교 시 타이밍 부채널 공격을 방지하기 위해
+ /// 모든 `OID_MAX_ARCS` 슬롯을 항상 비교합니다.
+ pub fn ct_eq(&self, other: &Oid) -> bool {
+ // 길이 비교 (상수-시간)
+ let len_eq = self.len.ct_eq(&other.len).unwrap_u8();
+
+ // 아크 배열 전체 비교 — len 이후는 0 패딩이므로 항상 동일
+ let mut acc = 0xFFu8;
+ for i in 0..OID_MAX_ARCS {
+ acc &= self.arcs[i].ct_eq(&other.arcs[i]).unwrap_u8();
+ }
+
+ (acc & len_eq) == 0xFF
+ }
+
+ /// DER 인코딩 시 값 바이트 길이를 반환하는 함수입니다.
+ #[allow(dead_code)]
+ pub(crate) fn der_value_len(&self) -> Result {
+ let first = (self.arcs[0] as u64)
+ .checked_mul(40)
+ .and_then(|v| v.checked_add(self.arcs[1] as u64))
+ .ok_or(ASN1(ASN1Error::InvalidOid))? as u32;
+
+ let mut total = base128_encoded_len(first);
+ for i in 2..self.len {
+ total = total
+ .checked_add(base128_encoded_len(self.arcs[i]))
+ .ok_or(ASN1(ASN1Error::LengthOverflow))?;
+ }
+ Ok(total)
+ }
+}
+
+/// base-128 VarInt 인코딩 바이트 수를 반환하는 함수입니다.
+#[allow(dead_code)]
+pub(crate) fn base128_encoded_len(val: u32) -> usize {
+ match val {
+ 0x0000_0000..=0x0000_007F => 1,
+ 0x0000_0080..=0x0000_3FFF => 2,
+ 0x0000_4000..=0x001F_FFFF => 3,
+ 0x0020_0000..=0x0FFF_FFFF => 4,
+ _ => 5,
+ }
+}
+
+/// base-128 VarInt를 `buf`에 인코딩하는 함수입니다.
+pub(crate) fn encode_base128(buf: &mut alloc::vec::Vec, val: u32) {
+ if val == 0 {
+ buf.push(0x00);
+ return;
+ }
+ let mut tmp = [0u8; 5];
+ let mut count = 0usize;
+ let mut v = val;
+ while v > 0 {
+ tmp[count] = (v & 0x7F) as u8;
+ v >>= 7;
+ count += 1;
+ }
+ // 최상위 그룹부터 내림차순으로 기록 (모든 바이트 except last에 0x80 세트)
+ for i in (0..count).rev() {
+ let continuation = if i > 0 { 0x80u8 } else { 0x00u8 };
+ buf.push(tmp[i] | continuation);
+ }
+}
+
+/// `buf[pos..end]`에서 base-128 VarInt를 디코딩하는 함수입니다.
+///
+/// # Security Note
+/// 5바이트 초과 시 오류, u32 범위 초과 시 오류 (버퍼 오버리드 방지).
+pub(crate) fn decode_base128(buf: &[u8], pos: &mut usize, end: usize) -> Result {
+ let mut val: u64 = 0;
+ let mut count = 0usize;
+ loop {
+ if *pos >= end {
+ return Err(ASN1(ASN1Error::UnexpectedEof));
+ }
+ if count >= 5 {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+ let byte = buf[*pos];
+ *pos += 1;
+ count += 1;
+ val = (val << 7) | (byte & 0x7F) as u64;
+ if val > u32::MAX as u64 {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+ if byte & 0x80 == 0 {
+ return Ok(val as u32);
+ }
+ }
+}
+
+/// DER OID 값 바이트로부터 `Oid`를 파싱하는 함수입니다.
+pub(crate) fn decode_oid(bytes: &[u8]) -> Result {
+ if bytes.is_empty() {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+
+ let mut arcs = [0u32; OID_MAX_ARCS];
+ let mut arc_count = 0usize;
+ let mut pos = 0usize;
+
+ // 첫 번째 하위식별자: 40*a0 + a1
+ let first_sub = decode_base128(bytes, &mut pos, bytes.len())?;
+ let (a0, a1) = if first_sub < 40 {
+ (0u32, first_sub)
+ } else if first_sub < 80 {
+ (1u32, first_sub - 40)
+ } else {
+ (2u32, first_sub.wrapping_sub(80))
+ };
+ arcs[arc_count] = a0;
+ arc_count += 1;
+ arcs[arc_count] = a1;
+ arc_count += 1;
+
+ // 나머지 하위식별자
+ while pos < bytes.len() {
+ if arc_count >= OID_MAX_ARCS {
+ return Err(ASN1(ASN1Error::InvalidOid));
+ }
+ arcs[arc_count] = decode_base128(bytes, &mut pos, bytes.len())?;
+ arc_count += 1;
+ }
+
+ Ok(Oid {
+ arcs,
+ len: arc_count,
+ })
+}
diff --git a/crypto/armor/src/asn1/tag.rs b/crypto/armor/src/asn1/tag.rs
new file mode 100644
index 0000000..6c9ad14
--- /dev/null
+++ b/crypto/armor/src/asn1/tag.rs
@@ -0,0 +1,65 @@
+/// ASN.1 태그 클래스입니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TagClass {
+ Universal = 0x00,
+ Application = 0x40,
+ Context = 0x80,
+ Private = 0xC0,
+}
+
+/// DER 태그 바이트를 감싸는 구조체입니다.
+/// 단순 u8 래퍼로, 비트 필드(클래스·구성·번호)를 직접 해석합니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct Tag(pub u8);
+
+impl Tag {
+ pub const BOOLEAN: Tag = Tag(0x01);
+ pub const INTEGER: Tag = Tag(0x02);
+ pub const BIT_STRING: Tag = Tag(0x03);
+ pub const OCTET_STRING: Tag = Tag(0x04);
+ pub const NULL: Tag = Tag(0x05);
+ pub const OID: Tag = Tag(0x06);
+ pub const UTF8_STRING: Tag = Tag(0x0C);
+ pub const PRINTABLE_STRING: Tag = Tag(0x13);
+ pub const IA5_STRING: Tag = Tag(0x16);
+ pub const UTC_TIME: Tag = Tag(0x17);
+ pub const GENERALIZED_TIME: Tag = Tag(0x18);
+ /// SEQUENCE OF / SEQUENCE (구성형 0x30)
+ pub const SEQUENCE: Tag = Tag(0x30);
+ /// SET OF / SET (구성형 0x31)
+ pub const SET: Tag = Tag(0x31);
+
+ /// 태그 클래스를 반환합니다.
+ #[inline(always)]
+ pub fn class(self) -> TagClass {
+ match self.0 & 0xC0 {
+ 0x00 => TagClass::Universal,
+ 0x40 => TagClass::Application,
+ 0x80 => TagClass::Context,
+ _ => TagClass::Private,
+ }
+ }
+
+ /// 태그가 구성형(Constructed)이면 true를 반환합니다.
+ #[inline(always)]
+ pub fn is_constructed(self) -> bool {
+ self.0 & 0x20 != 0
+ }
+
+ /// 태그 번호(하위 5비트)를 반환합니다.
+ #[inline(always)]
+ pub fn number(self) -> u8 {
+ self.0 & 0x1F
+ }
+
+ /// 컨텍스트 태그를 생성합니다.
+ ///
+ /// # Arguments
+ /// - `num` — 태그 번호 (0-30)
+ /// - `constructed` — 구성형 여부
+ #[inline(always)]
+ pub fn context(num: u8, constructed: bool) -> Tag {
+ Tag(0x80 | (if constructed { 0x20 } else { 0x00 }) | (num & 0x1F))
+ }
+}
diff --git a/crypto/armor/src/der/error.rs b/crypto/armor/src/der/error.rs
new file mode 100644
index 0000000..e6cc685
--- /dev/null
+++ b/crypto/armor/src/der/error.rs
@@ -0,0 +1,36 @@
+//! DER 파싱·인코딩 오류 타입 모듈입니다.
+
+/// DER 파싱 및 인코딩 중 발생하는 오류 열거형입니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DerError {
+ /// 입력 버퍼가 예상보다 짧음
+ UnexpectedEof,
+ /// 유효하지 않은 태그 바이트 (EOC, 장형식 태그 등)
+ InvalidTag,
+ /// 유효하지 않은 길이 인코딩
+ InvalidLength,
+ /// 부정길이(Indefinite-Length) 형식 거부 — BER 전용
+ IndefiniteLength,
+ /// 비최소 길이 인코딩 (DER 위반)
+ NonMinimalLength,
+ /// 비최소 INTEGER 인코딩 (불필요한 선행 0x00/0xFF 바이트)
+ NonMinimalInteger,
+ /// BOOLEAN 값이 0x00 또는 0xFF가 아님 (DER 위반)
+ InvalidBooleanEncoding,
+ /// BIT STRING의 미사용 비트 수가 0-7 범위를 벗어남
+ InvalidBitString,
+ /// 유효하지 않은 OID 인코딩 또는 구조
+ InvalidOid,
+ /// 예상한 태그와 실제 태그가 불일치
+ UnexpectedTag { expected: u8, got: u8 },
+ /// 길이 계산 시 산술 오버플로우
+ LengthOverflow,
+ /// 최대 중첩 깊이 초과 — 재귀 폭탄 방지
+ MaxDepthExceeded,
+ /// 파싱 완료 후 잔여 바이트 존재
+ TrailingData,
+ /// 빈 입력
+ EmptyInput,
+ /// SecureBuffer 할당 실패
+ AllocationError,
+}
diff --git a/crypto/armor/src/der/length.rs b/crypto/armor/src/der/length.rs
new file mode 100644
index 0000000..d98e31e
--- /dev/null
+++ b/crypto/armor/src/der/length.rs
@@ -0,0 +1,112 @@
+//! DER 길이 인코딩/디코딩 모듈입니다.
+//! 부정길이(BER) 거부, 비최소 인코딩 거부, 오버플로우 방어를 포함합니다.
+
+use alloc::vec::Vec;
+
+use crate::der::error::DerError;
+use crate::error::ArmorError;
+use crate::error::ArmorError::DER;
+
+/// 단일 TLV 값의 최대 허용 바이트 수 (16 MiB − 1)
+pub(crate) const MAX_VALUE_LEN: usize = 0x00FF_FFFF;
+
+/// `buf[pos]`에서 DER 길이를 디코딩하여 `(length, consumed_bytes)`를 반환하는 함수입니다.
+///
+/// # Security Note
+/// - 부정길이(0x80) 즉시 거부
+/// - 예약 바이트(0xFF) 거부
+/// - 비최소 인코딩 (long-form으로 표현 가능한 길이를 short-form으로 표현하거나,
+/// long-form 바이트에 선행 0x00) 거부
+/// - 길이 계산 오버플로우: `checked_mul` + `checked_add`로 방어
+pub(crate) fn decode_length(buf: &[u8], pos: usize) -> Result<(usize, usize), ArmorError> {
+ if pos >= buf.len() {
+ return Err(DER(DerError::UnexpectedEof));
+ }
+ let first = buf[pos];
+
+ // 부정길이 (BER 전용)
+ if first == 0x80 {
+ return Err(DER(DerError::IndefiniteLength));
+ }
+ // 예약 바이트
+ if first == 0xFF {
+ return Err(DER(DerError::InvalidLength));
+ }
+
+ // 단형식 (short form): 0x00–0x7F
+ if first & 0x80 == 0 {
+ return Ok((first as usize, 1));
+ }
+
+ // 장형식 (long form): 0x81–0x84
+ let num_len_bytes = (first & 0x7F) as usize;
+ // 지원 범위 초과 또는 num_len_bytes == 0 (부정길이로 이미 처리됨)
+ if num_len_bytes == 0 || num_len_bytes > 4 {
+ return Err(DER(DerError::InvalidLength));
+ }
+
+ let end = pos
+ .checked_add(1)
+ .and_then(|p| p.checked_add(num_len_bytes))
+ .ok_or(DER(DerError::LengthOverflow))?;
+ if end > buf.len() {
+ return Err(DER(DerError::UnexpectedEof));
+ }
+
+ // 선행 0x00 금지 (비최소 인코딩)
+ if buf[pos + 1] == 0x00 {
+ return Err(DER(DerError::NonMinimalLength));
+ }
+
+ let mut length: usize = 0;
+ for i in 0..num_len_bytes {
+ length = length
+ .checked_mul(256)
+ .ok_or(DER(DerError::LengthOverflow))?
+ .checked_add(buf[pos + 1 + i] as usize)
+ .ok_or(DER(DerError::LengthOverflow))?;
+ }
+
+ // DER: 길이 < 128이면 반드시 단형식으로 인코딩해야 함
+ if length < 128 {
+ return Err(DER(DerError::NonMinimalLength));
+ }
+
+ if length > MAX_VALUE_LEN {
+ return Err(DER(DerError::LengthOverflow));
+ }
+
+ Ok((length, 1 + num_len_bytes))
+}
+
+/// DER 길이를 `buf`에 인코딩하는 함수입니다.
+///
+/// # Security Note
+/// 항상 최소 바이트로 인코딩합니다 (DER 요구사항).
+pub(crate) fn encode_length(buf: &mut Vec, length: usize) -> Result<(), ArmorError> {
+ if length > MAX_VALUE_LEN {
+ return Err(DER(DerError::LengthOverflow));
+ }
+ if length < 128 {
+ buf.push(length as u8);
+ } else if length <= 0xFF {
+ buf.push(0x81);
+ buf.push(length as u8);
+ } else if length <= 0xFFFF {
+ buf.push(0x82);
+ buf.push((length >> 8) as u8);
+ buf.push(length as u8);
+ } else if length <= 0xFF_FFFF {
+ buf.push(0x83);
+ buf.push((length >> 16) as u8);
+ buf.push((length >> 8) as u8);
+ buf.push(length as u8);
+ } else {
+ buf.push(0x84);
+ buf.push((length >> 24) as u8);
+ buf.push((length >> 16) as u8);
+ buf.push((length >> 8) as u8);
+ buf.push(length as u8);
+ }
+ Ok(())
+}
diff --git a/crypto/armor/src/der/mod.rs b/crypto/armor/src/der/mod.rs
new file mode 100644
index 0000000..038a25b
--- /dev/null
+++ b/crypto/armor/src/der/mod.rs
@@ -0,0 +1,12 @@
+mod error;
+mod length;
+mod reader;
+mod writer;
+
+pub use error::DerError;
+pub use reader::{DerReader, DerTlv};
+pub use writer::DerWriter;
+
+/// 최대 허용 중첩 깊이
+/// 재귀 폭탄 방어
+pub const MAX_DEPTH: u8 = 16;
diff --git a/crypto/armor/src/der/reader.rs b/crypto/armor/src/der/reader.rs
new file mode 100644
index 0000000..f9cb5ea
--- /dev/null
+++ b/crypto/armor/src/der/reader.rs
@@ -0,0 +1,371 @@
+//! DER 파서(리더) 모듈입니다.
+//! 재귀 없이 커서 기반으로 TLV를 순회하며 깊이 제한으로 중첩 폭탄을 방어합니다.
+
+use crate::asn1::Oid;
+use crate::asn1::Tag;
+use crate::der::error::DerError;
+use crate::der::length;
+use crate::error::ArmorError;
+use crate::error::ArmorError::DER;
+use entlib_native_secure_buffer::SecureBuffer;
+
+/// 단일 TLV(Tag-Length-Value) 파싱 결과입니다.
+#[derive(Debug)]
+pub struct DerTlv<'a> {
+ /// 파싱된 태그
+ pub tag: Tag,
+ /// 값 바이트 슬라이스 (복사 없이 원본 버퍼 참조)
+ pub value: &'a [u8],
+}
+
+/// 커서 기반 DER 파서 구조체입니다.
+#[derive(Debug)]
+///
+/// # Security Note
+/// - 스택 재귀 없이 반복(iterative) 방식으로 동작합니다.
+/// - `read_sequence` 등 중첩 진입 시 `depth`를 감소시켜 최대 `MAX_DEPTH` 레벨로 제한합니다.
+/// - 모든 길이 계산은 `checked_add`를 사용하여 오버플로우를 방어합니다.
+pub struct DerReader<'a> {
+ buf: &'a [u8],
+ pos: usize,
+}
+
+impl<'a> DerReader<'a> {
+ /// 입력 슬라이스로 DerReader를 생성하는 함수입니다.
+ ///
+ /// # Errors
+ /// 빈 입력이면 `EmptyInput`.
+ pub fn new(data: &'a [u8]) -> Result {
+ if data.is_empty() {
+ return Err(DER(DerError::EmptyInput));
+ }
+ Ok(Self { buf: data, pos: 0 })
+ }
+
+ pub(crate) fn from_slice(data: &'a [u8]) -> Self {
+ Self { buf: data, pos: 0 }
+ }
+
+ /// 남은 바이트가 없으면 true를 반환합니다.
+ #[inline(always)]
+ pub fn is_empty(&self) -> bool {
+ self.pos >= self.buf.len()
+ }
+
+ /// 남은 바이트 수를 반환합니다.
+ #[inline(always)]
+ pub fn remaining(&self) -> usize {
+ self.buf.len().saturating_sub(self.pos)
+ }
+
+ /// 다음 태그를 소비하지 않고 미리 읽는 함수입니다.
+ ///
+ /// # Errors
+ /// EOF, 장형식 태그, EOC 태그 시 오류.
+ pub fn peek_tag(&self) -> Result {
+ if self.pos >= self.buf.len() {
+ return Err(DER(DerError::UnexpectedEof));
+ }
+ let byte = self.buf[self.pos];
+ validate_tag_byte(byte)?;
+ Ok(Tag(byte))
+ }
+
+ /// 다음 TLV 하나를 파싱하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 값 범위 검증(`pos + length <= buf.len()`)을 통해 버퍼 오버리드를 방어합니다.
+ pub fn read_tlv(&mut self) -> Result, ArmorError> {
+ let (tag, length) = self.read_tag_and_length()?;
+ let value = self.take_value_slice(length)?;
+ Ok(DerTlv { tag, value })
+ }
+
+ /// SEQUENCE를 읽어 내부 컨텐츠에 대한 새 DerReader를 반환하는 함수입니다.
+ ///
+ /// # Arguments
+ /// `depth` — 남은 허용 중첩 깊이. 진입 시 1 감소.
+ ///
+ /// # Errors
+ /// `depth == 0`이면 `MaxDepthExceeded`.
+ pub fn read_sequence(&mut self, depth: &mut u8) -> Result, ArmorError> {
+ self.read_constructed(Tag::SEQUENCE, depth)
+ }
+
+ /// SET을 읽어 내부 컨텐츠에 대한 새 DerReader를 반환하는 함수입니다.
+ pub fn read_set(&mut self, depth: &mut u8) -> Result, ArmorError> {
+ self.read_constructed(Tag::SET, depth)
+ }
+
+ /// EXPLICIT 컨텍스트 태그 `[tag_num]`를 읽어 내부 DerReader를 반환하는 함수입니다.
+ ///
+ /// # Arguments
+ /// - `tag_num` — 컨텍스트 태그 번호 (0-30)
+ /// - `depth` — 남은 허용 중첩 깊이
+ pub fn read_explicit_tag(
+ &mut self,
+ tag_num: u8,
+ depth: &mut u8,
+ ) -> Result, ArmorError> {
+ let expected = Tag::context(tag_num, true);
+ self.read_constructed(expected, depth)
+ }
+
+ /// IMPLICIT 컨텍스트 태그 `[tag_num]`의 값 바이트를 반환하는 함수입니다.
+ ///
+ /// IMPLICIT 태그는 원래 타입의 태그가 `[tag_num]`로 교체된 것이므로
+ /// 값 바이트는 원래 타입의 내용과 동일합니다.
+ pub fn read_implicit_value(&mut self, tag_num: u8) -> Result<&'a [u8], ArmorError> {
+ let expected = Tag::context(tag_num, false);
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != expected {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: expected.0,
+ got: tag.0,
+ }));
+ }
+ self.take_value_slice(length)
+ }
+
+ /// INTEGER 값 바이트를 반환하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 비최소 인코딩(불필요한 선행 0x00 또는 0xFF)을 거부합니다.
+ /// 반환 슬라이스에는 부호 바이트(선행 0x00)가 포함될 수 있습니다.
+ pub fn read_integer_bytes(&mut self) -> Result<&'a [u8], ArmorError> {
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != Tag::INTEGER {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: Tag::INTEGER.0,
+ got: tag.0,
+ }));
+ }
+ let value = self.take_value_slice(length)?;
+ validate_integer_encoding(value)?;
+ Ok(value)
+ }
+
+ /// INTEGER 값을 SecureBuffer로 복사하여 반환하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 비밀 키 파싱 등 민감 데이터에 사용합니다.
+ /// 메모리 잠금된 버퍼로 복사하여 스왑 유출을 방지합니다.
+ pub fn read_integer_secure(&mut self) -> Result {
+ let bytes = self.read_integer_bytes()?;
+ copy_to_secure_buffer(bytes)
+ }
+
+ /// OCTET STRING 값 바이트를 반환하는 함수입니다.
+ pub fn read_octet_string(&mut self) -> Result<&'a [u8], ArmorError> {
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != Tag::OCTET_STRING {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: Tag::OCTET_STRING.0,
+ got: tag.0,
+ }));
+ }
+ self.take_value_slice(length)
+ }
+
+ /// OCTET STRING 값을 SecureBuffer로 복사하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 암호화된 키 블롭 등 민감 데이터에 사용합니다.
+ pub fn read_octet_string_secure(&mut self) -> Result {
+ let bytes = self.read_octet_string()?;
+ copy_to_secure_buffer(bytes)
+ }
+
+ /// BIT STRING을 파싱하여 `(데이터 슬라이스, 미사용 비트 수)`를 반환하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 미사용 비트 수 바이트가 0–7 범위를 벗어나면 즉시 거부합니다.
+ /// 암호 키 파싱 시 미사용 비트는 항상 0이어야 합니다.
+ pub fn read_bit_string(&mut self) -> Result<(&'a [u8], u8), ArmorError> {
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != Tag::BIT_STRING {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: Tag::BIT_STRING.0,
+ got: tag.0,
+ }));
+ }
+ if length == 0 {
+ return Err(DER(DerError::InvalidBitString));
+ }
+ let raw = self.take_value_slice(length)?;
+ let unused_bits = raw[0];
+ if unused_bits > 7 {
+ return Err(DER(DerError::InvalidBitString));
+ }
+ // 미사용 비트가 있는 경우 데이터가 최소 1바이트 이상이어야 함
+ if unused_bits > 0 && length < 2 {
+ return Err(DER(DerError::InvalidBitString));
+ }
+ Ok((&raw[1..], unused_bits))
+ }
+
+ /// OID를 파싱하는 함수입니다.
+ pub fn read_oid(&mut self) -> Result {
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != Tag::OID {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: Tag::OID.0,
+ got: tag.0,
+ }));
+ }
+ if length == 0 {
+ return Err(DER(DerError::InvalidOid));
+ }
+ let value = self.take_value_slice(length)?;
+ crate::asn1::decode_oid(value)
+ }
+
+ /// NULL을 파싱하는 함수입니다.
+ ///
+ /// # Errors
+ /// NULL의 길이가 0이 아니면 `InvalidLength`.
+ pub fn read_null(&mut self) -> Result<(), ArmorError> {
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != Tag::NULL {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: Tag::NULL.0,
+ got: tag.0,
+ }));
+ }
+ if length != 0 {
+ return Err(DER(DerError::InvalidLength));
+ }
+ Ok(())
+ }
+
+ /// BOOLEAN을 파싱하는 함수입니다.
+ ///
+ /// # Security Note
+ /// DER에서 BOOLEAN은 0x00(false) 또는 0xFF(true)만 허용합니다.
+ /// BER의 0x01..0xFE는 거부됩니다.
+ pub fn read_boolean(&mut self) -> Result {
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != Tag::BOOLEAN {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: Tag::BOOLEAN.0,
+ got: tag.0,
+ }));
+ }
+ if length != 1 {
+ return Err(DER(DerError::InvalidLength));
+ }
+ let value = self.take_value_slice(1)?;
+ match value[0] {
+ 0x00 => Ok(false),
+ 0xFF => Ok(true),
+ _ => Err(DER(DerError::InvalidBooleanEncoding)),
+ }
+ }
+
+ /// 파싱 완료 후 잔여 바이트가 없는지 확인하는 함수입니다.
+ ///
+ /// # Errors
+ /// 잔여 바이트가 있으면 `TrailingData`.
+ pub fn expect_empty(&self) -> Result<(), ArmorError> {
+ if self.is_empty() {
+ Ok(())
+ } else {
+ Err(DER(DerError::TrailingData))
+ }
+ }
+
+ //
+ // 내부 헬퍼
+ //
+
+ fn read_tag_and_length(&mut self) -> Result<(Tag, usize), ArmorError> {
+ if self.pos >= self.buf.len() {
+ return Err(DER(DerError::UnexpectedEof));
+ }
+ let tag_byte = self.buf[self.pos];
+ validate_tag_byte(tag_byte)?;
+ self.pos += 1;
+
+ let (length, consumed) = length::decode_length(self.buf, self.pos)?;
+ self.pos += consumed;
+ Ok((Tag(tag_byte), length))
+ }
+
+ fn take_value_slice(&mut self, length: usize) -> Result<&'a [u8], ArmorError> {
+ let end = self
+ .pos
+ .checked_add(length)
+ .ok_or(DER(DerError::LengthOverflow))?;
+ if end > self.buf.len() {
+ return Err(DER(DerError::UnexpectedEof));
+ }
+ let slice = &self.buf[self.pos..end];
+ self.pos = end;
+ Ok(slice)
+ }
+
+ fn read_constructed(
+ &mut self,
+ expected_tag: Tag,
+ depth: &mut u8,
+ ) -> Result, ArmorError> {
+ if *depth == 0 {
+ return Err(DER(DerError::MaxDepthExceeded));
+ }
+ let (tag, length) = self.read_tag_and_length()?;
+ if tag != expected_tag {
+ return Err(DER(DerError::UnexpectedTag {
+ expected: expected_tag.0,
+ got: tag.0,
+ }));
+ }
+ let inner = self.take_value_slice(length)?;
+ *depth -= 1;
+ Ok(DerReader::from_slice(inner))
+ }
+}
+
+//
+// 파일-내부 헬퍼 함수
+//
+
+/// 태그 바이트 유효성 검사 함수입니다.
+///
+/// # Security Note
+/// - 장형식 태그(0x1F 마스크) 거부: 다중 바이트 태그 파싱 로직 제거로 공격 면 축소
+/// - EOC 태그(0x00) 거부: 부정길이 BER 구조에서만 사용
+fn validate_tag_byte(byte: u8) -> Result<(), ArmorError> {
+ if byte & 0x1F == 0x1F {
+ return Err(DER(DerError::InvalidTag));
+ }
+ if byte == 0x00 {
+ return Err(DER(DerError::InvalidTag));
+ }
+ Ok(())
+}
+
+/// INTEGER 인코딩의 최소성을 검증하는 함수입니다.
+fn validate_integer_encoding(bytes: &[u8]) -> Result<(), ArmorError> {
+ if bytes.is_empty() {
+ return Err(DER(DerError::NonMinimalInteger));
+ }
+ if bytes.len() > 1 {
+ // 불필요한 선행 0x00 (양수의 비최소 인코딩)
+ if bytes[0] == 0x00 && bytes[1] & 0x80 == 0 {
+ return Err(DER(DerError::NonMinimalInteger));
+ }
+ // 불필요한 선행 0xFF (음수의 비최소 인코딩)
+ if bytes[0] == 0xFF && bytes[1] & 0x80 != 0 {
+ return Err(DER(DerError::NonMinimalInteger));
+ }
+ }
+ Ok(())
+}
+
+/// 바이트 슬라이스를 SecureBuffer에 복사하는 함수입니다.
+fn copy_to_secure_buffer(bytes: &[u8]) -> Result {
+ let mut buf =
+ SecureBuffer::new_owned(bytes.len()).map_err(|_| DER(DerError::AllocationError))?;
+ buf.as_mut_slice().copy_from_slice(bytes);
+ Ok(buf)
+}
diff --git a/crypto/armor/src/der/writer.rs b/crypto/armor/src/der/writer.rs
new file mode 100644
index 0000000..6ceae07
--- /dev/null
+++ b/crypto/armor/src/der/writer.rs
@@ -0,0 +1,194 @@
+//! DER 인코더(라이터) 모듈입니다.
+//! 항상 최소 바이트 DER 형식으로 인코딩합니다.
+
+use crate::asn1;
+use crate::asn1::Oid;
+use crate::asn1::Tag;
+use crate::der::error::DerError;
+use crate::der::length;
+use crate::error::ArmorError;
+use crate::error::ArmorError::DER;
+use alloc::vec::Vec;
+
+/// DER 인코더 구조체입니다.
+///
+/// 개별 write_* 메서드로 TLV를 누적하고 `finish()`로 최종 바이트열을 회수합니다.
+/// 중첩 SEQUENCE는 내부 컨텐츠를 별도 DerWriter로 인코딩한 뒤
+/// `write_sequence(inner.finish())` 형태로 조립합니다.
+pub struct DerWriter {
+ buf: Vec,
+}
+
+impl DerWriter {
+ /// 빈 DerWriter를 생성합니다.
+ pub fn new() -> Self {
+ Self { buf: Vec::new() }
+ }
+
+ /// 누적된 인코딩 결과를 반환하는 함수입니다.
+ pub fn finish(self) -> Vec {
+ self.buf
+ }
+
+ /// SEQUENCE TLV를 인코딩하는 함수입니다.
+ pub fn write_sequence(&mut self, inner: &[u8]) -> Result<(), ArmorError> {
+ self.push_tlv(Tag::SEQUENCE.0, inner)
+ }
+
+ /// SET TLV를 인코딩하는 함수입니다.
+ pub fn write_set(&mut self, inner: &[u8]) -> Result<(), ArmorError> {
+ self.push_tlv(Tag::SET.0, inner)
+ }
+
+ /// EXPLICIT 컨텍스트 태그 `[tag_num]`를 인코딩하는 함수입니다.
+ pub fn write_explicit_tag(&mut self, tag_num: u8, inner: &[u8]) -> Result<(), ArmorError> {
+ let tag = Tag::context(tag_num, true);
+ self.push_tlv(tag.0, inner)
+ }
+
+ /// IMPLICIT 컨텍스트 태그 `[tag_num]`를 원시 값 바이트로 인코딩하는 함수입니다.
+ pub fn write_implicit_tag(&mut self, tag_num: u8, value: &[u8]) -> Result<(), ArmorError> {
+ let tag = Tag::context(tag_num, false);
+ self.push_tlv(tag.0, value)
+ }
+
+ /// 부호 없는 정수를 DER INTEGER로 인코딩하는 함수입니다.
+ ///
+ /// # Security Note
+ /// - 선행 0x00 바이트를 제거하여 최소 표현을 보장합니다.
+ /// - 최상위 비트가 1이면(음수로 오독될 수 있으므로) 0x00 부호 바이트를 자동 삽입합니다.
+ /// - 입력이 비어 있으면 정수 0으로 인코딩합니다.
+ pub fn write_integer_unsigned(&mut self, bytes: &[u8]) -> Result<(), ArmorError> {
+ let stripped = strip_leading_zeros(bytes);
+
+ if stripped.is_empty() {
+ // 값 0: INTEGER 0x00
+ return self.push_tlv(Tag::INTEGER.0, &[0x00]);
+ }
+
+ if stripped[0] & 0x80 != 0 {
+ // 최상위 비트가 1: 부호 바이트 0x00 삽입
+ let value_len = stripped
+ .len()
+ .checked_add(1)
+ .ok_or(DER(DerError::LengthOverflow))?;
+ self.buf.push(Tag::INTEGER.0);
+ length::encode_length(&mut self.buf, value_len)?;
+ self.buf.push(0x00);
+ self.buf.extend_from_slice(stripped);
+ } else {
+ self.push_tlv(Tag::INTEGER.0, stripped)?;
+ }
+ Ok(())
+ }
+
+ /// 이미 DER 인코딩된 INTEGER 바이트열을 그대로 기록하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 호출자가 유효한 DER INTEGER 값 바이트를 제공해야 합니다.
+ pub fn write_integer_raw(&mut self, der_integer_value: &[u8]) -> Result<(), ArmorError> {
+ self.push_tlv(Tag::INTEGER.0, der_integer_value)
+ }
+
+ /// OCTET STRING을 인코딩하는 함수입니다.
+ pub fn write_octet_string(&mut self, data: &[u8]) -> Result<(), ArmorError> {
+ self.push_tlv(Tag::OCTET_STRING.0, data)
+ }
+
+ /// BIT STRING을 인코딩하는 함수입니다.
+ ///
+ /// # Arguments
+ /// - `data` — 비트 데이터 바이트열
+ /// - `unused_bits` — 마지막 바이트의 미사용 비트 수 (0-7)
+ ///
+ /// # Errors
+ /// `unused_bits > 7` 또는 `unused_bits > 0`이면서 `data`가 비어 있으면 `InvalidBitString`.
+ pub fn write_bit_string(&mut self, data: &[u8], unused_bits: u8) -> Result<(), ArmorError> {
+ if unused_bits > 7 {
+ return Err(DER(DerError::InvalidBitString));
+ }
+ if unused_bits > 0 && data.is_empty() {
+ return Err(DER(DerError::InvalidBitString));
+ }
+ let value_len = data
+ .len()
+ .checked_add(1)
+ .ok_or(DER(DerError::LengthOverflow))?;
+ self.buf.push(Tag::BIT_STRING.0);
+ length::encode_length(&mut self.buf, value_len)?;
+ self.buf.push(unused_bits);
+ self.buf.extend_from_slice(data);
+ Ok(())
+ }
+
+ /// OID를 인코딩하는 함수입니다.
+ pub fn write_oid(&mut self, oid: &Oid) -> Result<(), ArmorError> {
+ let arcs = oid.arcs();
+ if arcs.len() < 2 {
+ return Err(DER(DerError::InvalidOid));
+ }
+
+ // 값 바이트를 임시 버퍼에 인코딩
+ let mut value: Vec = Vec::new();
+
+ // 첫 번째 하위식별자: 40*a0 + a1
+ let first_sub = (arcs[0] as u64)
+ .checked_mul(40)
+ .and_then(|v| v.checked_add(arcs[1] as u64))
+ .ok_or(DER(DerError::InvalidOid))? as u32;
+ asn1::encode_base128(&mut value, first_sub);
+
+ for &arc in &arcs[2..] {
+ asn1::encode_base128(&mut value, arc);
+ }
+
+ self.push_tlv(Tag::OID.0, &value)
+ }
+
+ /// NULL을 인코딩하는 함수입니다.
+ pub fn write_null(&mut self) -> Result<(), ArmorError> {
+ self.buf.push(Tag::NULL.0);
+ self.buf.push(0x00);
+ Ok(())
+ }
+
+ /// BOOLEAN을 인코딩하는 함수입니다.
+ ///
+ /// # Security Note
+ /// DER 규칙: true는 0xFF, false는 0x00으로 인코딩합니다.
+ pub fn write_boolean(&mut self, value: bool) -> Result<(), ArmorError> {
+ self.buf.push(Tag::BOOLEAN.0);
+ self.buf.push(0x01);
+ self.buf.push(if value { 0xFF } else { 0x00 });
+ Ok(())
+ }
+
+ //
+ // 내부 헬퍼
+ //
+
+ fn push_tlv(&mut self, tag: u8, value: &[u8]) -> Result<(), ArmorError> {
+ self.buf.push(tag);
+ length::encode_length(&mut self.buf, value.len())?;
+ self.buf.extend_from_slice(value);
+ Ok(())
+ }
+}
+
+impl Default for DerWriter {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// 선행 0x00 바이트를 제거하되, 마지막 바이트는 보존합니다.
+fn strip_leading_zeros(bytes: &[u8]) -> &[u8] {
+ if bytes.is_empty() {
+ return bytes;
+ }
+ let first_nonzero = bytes
+ .iter()
+ .position(|&b| b != 0x00)
+ .unwrap_or(bytes.len() - 1);
+ &bytes[first_nonzero..]
+}
diff --git a/crypto/armor/src/error.rs b/crypto/armor/src/error.rs
new file mode 100644
index 0000000..8ec9b15
--- /dev/null
+++ b/crypto/armor/src/error.rs
@@ -0,0 +1,11 @@
+//! ArmorError 통합 오류 타입 모듈입니다.
+
+/// armor 크레이트 전체 오류 열거형입니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ArmorError {
+ ASN1(crate::asn1::ASN1Error),
+ DER(crate::der::DerError),
+ PEM(crate::pem::PemError),
+ #[cfg(feature = "std")]
+ IO(crate::io::IoError),
+}
diff --git a/crypto/armor/src/io/error.rs b/crypto/armor/src/io/error.rs
new file mode 100644
index 0000000..ebeea10
--- /dev/null
+++ b/crypto/armor/src/io/error.rs
@@ -0,0 +1,22 @@
+//! IO 오류 타입 모듈입니다.
+
+/// 파일 I/O 중 발생하는 오류 열거형입니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum IoError {
+ /// 파일 읽기 실패
+ ReadFailed,
+ /// 파일 쓰기 실패
+ WriteFailed,
+ /// 파일 크기가 허용 한도를 초과
+ FileTooLarge,
+ /// 파일을 찾을 수 없음
+ FileNotFound,
+ /// 권한 부족
+ PermissionDenied,
+ /// 유효하지 않은 경로 (빈 경로, null 바이트 등)
+ InvalidPath,
+ /// 원자적 파일 교체(rename) 실패
+ AtomicRenameFailed,
+ /// SecureBuffer 할당 실패
+ AllocationError,
+}
diff --git a/crypto/armor/src/io/mod.rs b/crypto/armor/src/io/mod.rs
new file mode 100644
index 0000000..ddcf1da
--- /dev/null
+++ b/crypto/armor/src/io/mod.rs
@@ -0,0 +1,30 @@
+//! 고보안 DER/PEM 파일 I/O 모듈입니다.
+//!
+//! RFC 7468 표준을 준수하는 PEM 파일 출력 및 DER/PEM 파일 읽기를 제공합니다.
+//!
+//! # Security Note
+//! - 파일 읽기: 최대 1 MiB 크기 제한으로 메모리 고갈 공격 방어
+//! - 파일 쓰기: Unix에서 `0o600` 권한 강제, 원자적 임시 파일 교체
+//! - 경로 검증: 빈 경로, null 바이트 삽입 즉시 거부
+//! - 모든 데이터 버퍼는 SecureBuffer(mlock)에 보관
+//!
+//! # Examples
+//! ```rust,ignore
+//! use entlib_native_armor::io::{read_pem, write_pem};
+//! use entlib_native_armor::pem::PemLabel;
+//! use std::path::Path;
+//!
+//! // PEM 파일 쓰기 (0o600 권한, 원자적)
+//! write_pem(Path::new("key.pem"), &der_bytes, PemLabel::EncryptedPrivateKey).unwrap();
+//!
+//! // PEM 파일 읽기 (DER 구조 검증 포함)
+//! let (label, der) = read_pem(Path::new("key.pem")).unwrap();
+//! ```
+
+mod error;
+mod reader;
+mod writer;
+
+pub use error::IoError;
+pub use reader::{read_der, read_pem};
+pub use writer::{write_der, write_pem};
diff --git a/crypto/armor/src/io/reader.rs b/crypto/armor/src/io/reader.rs
new file mode 100644
index 0000000..95dc31e
--- /dev/null
+++ b/crypto/armor/src/io/reader.rs
@@ -0,0 +1,77 @@
+//! DER/PEM 파일 읽기 모듈입니다.
+
+use super::error::IoError;
+use crate::error::ArmorError;
+use crate::error::ArmorError::IO;
+use crate::pem::{PemLabel, decode as pem_decode};
+use entlib_native_secure_buffer::SecureBuffer;
+use std::fs;
+use std::path::Path;
+
+/// 파일 최대 읽기 크기 (1 MiB) — DoS 방어
+const MAX_FILE_BYTES: u64 = 1024 * 1024;
+
+/// DER 파일을 읽어 SecureBuffer로 반환하는 함수입니다.
+///
+/// # Security Note
+/// 파일 크기를 1 MiB로 제한하여 메모리 고갈 공격을 방어합니다.
+/// 결과는 SecureBuffer(mlock)에 보관됩니다.
+///
+/// # Errors
+/// `InvalidPath`, `FileNotFound`, `PermissionDenied`, `FileTooLarge`,
+/// `ReadFailed`, `AllocationError`
+pub fn read_der(path: &Path) -> Result {
+ validate_path(path)?;
+ let bytes = read_file_bounded(path)?;
+ Ok(bytes)
+}
+
+/// PEM 파일을 읽어 레이블과 DER SecureBuffer를 반환하는 함수입니다.
+///
+/// # Security Note
+/// 파일 크기를 1 MiB로 제한합니다.
+/// PEM 디코딩 후 DER 외곽 TLV 구조와 레이블 허용 목록을 검증합니다.
+///
+/// # Errors
+/// `InvalidPath`, `FileNotFound`, `PermissionDenied`, `FileTooLarge`,
+/// `ReadFailed`, `AllocationError`, 그리고 PEM 파싱 오류
+pub fn read_pem(path: &Path) -> Result<(PemLabel, SecureBuffer), ArmorError> {
+ validate_path(path)?;
+ let raw = read_file_bounded(path)?;
+ pem_decode(raw.as_slice())
+}
+
+fn validate_path(path: &Path) -> Result<(), ArmorError> {
+ let s = path.as_os_str().as_encoded_bytes();
+ if s.is_empty() {
+ return Err(IO(IoError::InvalidPath));
+ }
+ if s.contains(&0u8) {
+ return Err(IO(IoError::InvalidPath));
+ }
+ Ok(())
+}
+
+fn read_file_bounded(path: &Path) -> Result {
+ let meta = fs::metadata(path).map_err(map_read_error)?;
+
+ if meta.len() > MAX_FILE_BYTES {
+ return Err(IO(IoError::FileTooLarge));
+ }
+
+ let len = meta.len() as usize;
+ let content = fs::read(path).map_err(map_read_error)?;
+
+ let mut buf = SecureBuffer::new_owned(len).map_err(|_| IO(IoError::AllocationError))?;
+ buf.as_mut_slice().copy_from_slice(&content);
+ Ok(buf)
+}
+
+fn map_read_error(e: std::io::Error) -> ArmorError {
+ use std::io::ErrorKind;
+ match e.kind() {
+ ErrorKind::NotFound => IO(IoError::FileNotFound),
+ ErrorKind::PermissionDenied => IO(IoError::PermissionDenied),
+ _ => IO(IoError::ReadFailed),
+ }
+}
diff --git a/crypto/armor/src/io/writer.rs b/crypto/armor/src/io/writer.rs
new file mode 100644
index 0000000..fa87f22
--- /dev/null
+++ b/crypto/armor/src/io/writer.rs
@@ -0,0 +1,94 @@
+//! DER/PEM 파일 쓰기 모듈입니다.
+//!
+//! RFC 7468 표준을 준수하는 PEM 출력과 원자적 파일 교체를 구현합니다.
+
+use super::error::IoError;
+use crate::error::ArmorError;
+use crate::error::ArmorError::IO;
+use crate::pem::{PemLabel, encode as pem_encode};
+use std::fs::{self, OpenOptions};
+use std::io::Write;
+use std::path::Path;
+
+/// DER 바이트열을 파일에 쓰는 함수입니다.
+///
+/// # Security Note
+/// Unix에서 파일 권한을 `0o600`(소유자 읽기/쓰기 전용)으로 설정합니다.
+/// 임시 파일에 먼저 쓰고 원자적으로 교체하여 부분 기록을 방지합니다.
+///
+/// # Errors
+/// `InvalidPath`, `WriteFailed`, `AtomicRenameFailed`
+pub fn write_der(path: &Path, der: &[u8]) -> Result<(), ArmorError> {
+ validate_path(path)?;
+ write_atomic(path, der)
+}
+
+/// DER 바이트열을 RFC 7468 PEM 형식으로 파일에 쓰는 함수입니다.
+///
+/// # Security Note
+/// PEM 인코딩 전 DER 외곽 TLV 구조를 검증합니다.
+/// Unix에서 파일 권한을 `0o600`으로 설정합니다.
+/// 원자적 파일 교체로 부분 기록을 방지합니다.
+///
+/// # Errors
+/// `InvalidPath`, `WriteFailed`, `AtomicRenameFailed`, PEM 인코딩 오류
+pub fn write_pem(path: &Path, der: &[u8], label: PemLabel) -> Result<(), ArmorError> {
+ validate_path(path)?;
+ let pem_buf = pem_encode(der, label)?;
+ write_atomic(path, pem_buf.as_slice())
+}
+
+fn validate_path(path: &Path) -> Result<(), ArmorError> {
+ let s = path.as_os_str().as_encoded_bytes();
+ if s.is_empty() {
+ return Err(IO(IoError::InvalidPath));
+ }
+ if s.contains(&0u8) {
+ return Err(IO(IoError::InvalidPath));
+ }
+ Ok(())
+}
+
+fn write_atomic(path: &Path, data: &[u8]) -> Result<(), ArmorError> {
+ let parent = path.parent().ok_or(IO(IoError::InvalidPath))?;
+ let file_name = path.file_name().ok_or(IO(IoError::InvalidPath))?;
+
+ let mut tmp_name = file_name.to_os_string();
+ tmp_name.push(".tmp");
+ let tmp_path = parent.join(tmp_name);
+
+ {
+ let mut file = create_secure_file(&tmp_path).map_err(|_| IO(IoError::WriteFailed))?;
+ file.write_all(data).map_err(|_| IO(IoError::WriteFailed))?;
+ file.flush().map_err(|_| IO(IoError::WriteFailed))?;
+ // file closed here — OS flushes buffers
+ }
+
+ fs::rename(&tmp_path, path).map_err(|_| {
+ // 최선 시도: 임시 파일 정리
+ let _ = fs::remove_file(&tmp_path);
+ IO(IoError::AtomicRenameFailed)
+ })?;
+
+ Ok(())
+}
+
+#[cfg(unix)]
+fn create_secure_file(path: &Path) -> std::io::Result {
+ use std::os::unix::fs::OpenOptionsExt;
+ OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .mode(0o600)
+ .open(path)
+}
+
+#[cfg(not(unix))]
+fn create_secure_file(path: &Path) -> std::io::Result {
+ OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(path)
+}
diff --git a/crypto/armor/src/lib.rs b/crypto/armor/src/lib.rs
new file mode 100644
index 0000000..8ecf7a7
--- /dev/null
+++ b/crypto/armor/src/lib.rs
@@ -0,0 +1,59 @@
+//! NIST FIPS 140-3 준수 암호 아머(Armor) 크레이트 모듈입니다.
+//! 이 모듈은 데이터 직렬화 및 인코딩 파이프라인 구성 기능을 제공하기
+//! 위해 만들어졌습니다.
+//!
+//! ASN.1/DER 파싱·인코딩, RFC 7468 PEM 포맷 변환, 고보안 파일 I/O를
+//! 단일 크레이트에서 제공합니다. 모든 민감 데이터는 `SecureBuffer`로
+//! 격리하며, 부채널 공격 방어를 위해 상수-시간 연산을 적용합니다.
+//!
+//! # Security Note
+//! - **DER 파서**: 버퍼 오버리드·재귀 폭탄·비최소 인코딩·부정길이(BER)를
+//! 거부하는 반복(iterative) 커서 기반 구현입니다.
+//! - **PEM 디코더**: 허용 레이블 목록(`EncryptedPrivateKey`, `Certificate`,
+//! `PublicKey`, `CertificateRequest`)으로 비승인 구조체 타입을 차단합니다.
+//! 비암호화 개인 키(`PRIVATE KEY`)는 의도적으로 허용 목록에서 제외됩니다.
+//! - **파일 I/O**: Unix `0o600` 권한 강제, 원자적 임시 파일 교체,
+//! 1 MiB 읽기 크기 제한, 경로 null 바이트 주입 거부.
+//! - **OID 비교**: 타이밍 정보 누출을 막기 위해 전체 슬롯을 항상 순회하는
+//! 상수-시간 `ct_eq()` 비교를 사용합니다.
+//!
+//! # Examples
+//! ```rust,ignore
+//! use entlib_native_armor::der::{DerReader, DerWriter, MAX_DEPTH};
+//! use entlib_native_armor::pem::{encode, decode, PemLabel};
+//! use entlib_native_armor::asn1::Oid;
+//!
+//! // DER 인코딩
+//! let mut w = DerWriter::new();
+//! w.write_octet_string(&[0xDE, 0xAD, 0xBE, 0xEF]).unwrap();
+//! let der = w.finish();
+//!
+//! // PEM 래핑 (이미 암호화된 DER 페이로드 가정)
+//! let pem = encode(&der, PemLabel::EncryptedPrivateKey).unwrap();
+//!
+//! // PEM 언래핑 및 DER 복원
+//! let (label, restored) = decode(pem.as_slice()).unwrap();
+//! assert_eq!(label, PemLabel::EncryptedPrivateKey);
+//! assert_eq!(restored.as_slice(), &der);
+//! ```
+//!
+//! # Authors
+//! Q. T. Felix
+
+#![no_std]
+
+extern crate alloc;
+#[cfg(feature = "std")]
+extern crate std;
+
+pub mod asn1;
+pub mod der;
+mod error;
+#[cfg(feature = "std")]
+pub mod io;
+pub mod pem;
+
+pub use error::ArmorError;
+#[cfg(feature = "std")]
+pub use io::IoError;
+pub use pem::{PemError, PemLabel};
diff --git a/crypto/armor/src/pem/decoder.rs b/crypto/armor/src/pem/decoder.rs
new file mode 100644
index 0000000..5685236
--- /dev/null
+++ b/crypto/armor/src/pem/decoder.rs
@@ -0,0 +1,124 @@
+//! PEM 디코더 모듈입니다.
+
+use super::error::PemError;
+use super::filter::validate_der_envelope;
+use super::label::PemLabel;
+use crate::error::ArmorError;
+use crate::error::ArmorError::PEM;
+use alloc::vec::Vec;
+use entlib_native_base64 as b64;
+use entlib_native_secure_buffer::SecureBuffer;
+
+const BEGIN_PREFIX: &[u8] = b"-----BEGIN ";
+const END_PREFIX: &[u8] = b"-----END ";
+const BOUNDARY_SUFFIX: &[u8] = b"-----";
+
+/// PEM 형식 데이터를 DER 바이트열로 디코딩하는 함수입니다.
+///
+/// # Security Note
+/// 레이블을 허용 목록과 대조하여 비승인 구조체 타입을 거부합니다.
+/// 디코딩된 DER의 외곽 TLV 구조를 검증하여 손상된 페이로드를 거부합니다.
+/// 결과는 SecureBuffer(mlock)에 보관됩니다.
+///
+/// # Errors
+/// `MissingHeader`, `MissingFooter`, `InvalidHeader`, `InvalidFooter`,
+/// `UnknownLabel`, `LabelMismatch`, `EmptyBody`, `Base64Error`,
+/// `InvalidDer`, `AllocationError`
+pub fn decode(pem: &[u8]) -> Result<(PemLabel, SecureBuffer), ArmorError> {
+ let (label, rest) = parse_header(pem)?;
+ let (footer_label, b64_body) = collect_body(rest)?;
+
+ if footer_label != label {
+ return Err(PEM(PemError::LabelMismatch));
+ }
+ if b64_body.is_empty() {
+ return Err(PEM(PemError::EmptyBody));
+ }
+
+ let mut b64_buf =
+ SecureBuffer::new_owned(b64_body.len()).map_err(|_| PEM(PemError::AllocationError))?;
+ b64_buf.as_mut_slice().copy_from_slice(&b64_body);
+
+ let der = b64::decode(&b64_buf).map_err(|_| PEM(PemError::Base64Error))?;
+ validate_der_envelope(der.as_slice())?;
+
+ Ok((label, der))
+}
+
+fn parse_header(input: &[u8]) -> Result<(PemLabel, &[u8]), ArmorError> {
+ let input = trim_leading_whitespace(input);
+ if !input.starts_with(BEGIN_PREFIX) {
+ return Err(PEM(PemError::MissingHeader));
+ }
+ let after_prefix = &input[BEGIN_PREFIX.len()..];
+ let dash_pos =
+ find_pattern(after_prefix, BOUNDARY_SUFFIX).ok_or(PEM(PemError::InvalidHeader))?;
+ let label = PemLabel::from_bytes(&after_prefix[..dash_pos])?;
+ let after_suffix = &after_prefix[dash_pos + BOUNDARY_SUFFIX.len()..];
+ let after_nl = consume_newline(after_suffix).ok_or(PEM(PemError::InvalidHeader))?;
+ Ok((label, after_nl))
+}
+
+fn collect_body(mut input: &[u8]) -> Result<(PemLabel, Vec), ArmorError> {
+ let mut body: Vec = Vec::new();
+ loop {
+ if input.is_empty() {
+ return Err(PEM(PemError::MissingFooter));
+ }
+ if input.starts_with(END_PREFIX) {
+ let after_prefix = &input[END_PREFIX.len()..];
+ let dash_pos =
+ find_pattern(after_prefix, BOUNDARY_SUFFIX).ok_or(PEM(PemError::InvalidFooter))?;
+ let footer_label = PemLabel::from_bytes(&after_prefix[..dash_pos])?;
+ return Ok((footer_label, body));
+ }
+ let (line, rest) = split_line(input);
+ for &byte in line {
+ if !byte.is_ascii_whitespace() {
+ body.push(byte);
+ }
+ }
+ input = rest;
+ }
+}
+
+fn trim_leading_whitespace(input: &[u8]) -> &[u8] {
+ let pos = input
+ .iter()
+ .position(|&b| !b.is_ascii_whitespace())
+ .unwrap_or(input.len());
+ &input[pos..]
+}
+
+fn consume_newline(input: &[u8]) -> Option<&[u8]> {
+ if input.starts_with(b"\r\n") {
+ Some(&input[2..])
+ } else if input.starts_with(b"\n") {
+ Some(&input[1..])
+ } else if input.is_empty() {
+ Some(input)
+ } else {
+ None
+ }
+}
+
+fn split_line(input: &[u8]) -> (&[u8], &[u8]) {
+ match input.iter().position(|&b| b == b'\n') {
+ Some(pos) => {
+ let line_end = if pos > 0 && input[pos - 1] == b'\r' {
+ pos - 1
+ } else {
+ pos
+ };
+ (&input[..line_end], &input[pos + 1..])
+ }
+ None => (input, &[]),
+ }
+}
+
+fn find_pattern(haystack: &[u8], needle: &[u8]) -> Option {
+ if needle.is_empty() || haystack.len() < needle.len() {
+ return None;
+ }
+ haystack.windows(needle.len()).position(|w| w == needle)
+}
diff --git a/crypto/armor/src/pem/encoder.rs b/crypto/armor/src/pem/encoder.rs
new file mode 100644
index 0000000..a6bbb05
--- /dev/null
+++ b/crypto/armor/src/pem/encoder.rs
@@ -0,0 +1,73 @@
+//! PEM 인코더 모듈입니다.
+
+use super::error::PemError;
+use super::filter::validate_der_envelope;
+use super::label::PemLabel;
+use crate::error::ArmorError;
+use crate::error::ArmorError::PEM;
+use entlib_native_base64 as b64;
+use entlib_native_secure_buffer::SecureBuffer;
+
+const LINE_LEN: usize = 64;
+
+/// DER 바이트열을 RFC 7468 PEM 형식으로 인코딩하는 함수입니다.
+///
+/// # Security Note
+/// 인코딩 전 DER 외곽 TLV 구조를 검증하여 손상된 데이터를 거부합니다.
+/// DER 원본과 Base64 중간값은 SecureBuffer(mlock)에 보관됩니다.
+///
+/// # Errors
+/// `InvalidDer`, `AllocationError`, `Base64Error`
+pub fn encode(der: &[u8], label: PemLabel) -> Result {
+ validate_der_envelope(der)?;
+
+ let mut src = SecureBuffer::new_owned(der.len()).map_err(|_| PEM(PemError::AllocationError))?;
+ src.as_mut_slice().copy_from_slice(der);
+
+ let encoded = b64::encode(&src).map_err(|_| PEM(PemError::Base64Error))?;
+ let b64_bytes = encoded.as_slice();
+
+ let label_b = label.as_bytes();
+ let b64_len = b64_bytes.len();
+ let num_lines = b64_len.div_ceil(LINE_LEN);
+ let body_len = b64_len + num_lines;
+ // "-----BEGIN " (11) + label + "-----\n" (6)
+ let header_len = 11 + label_b.len() + 6;
+ // "-----END " (9) + label + "-----\n" (6)
+ let footer_len = 9 + label_b.len() + 6;
+
+ let total = header_len
+ .checked_add(body_len)
+ .and_then(|v| v.checked_add(footer_len))
+ .ok_or(PEM(PemError::AllocationError))?;
+
+ let mut out = SecureBuffer::new_owned(total).map_err(|_| PEM(PemError::AllocationError))?;
+ let buf = out.as_mut_slice();
+ let mut pos = 0;
+
+ write_bytes(buf, &mut pos, b"-----BEGIN ");
+ write_bytes(buf, &mut pos, label_b);
+ write_bytes(buf, &mut pos, b"-----\n");
+
+ let mut b64_pos = 0;
+ while b64_pos < b64_len {
+ let end = (b64_pos + LINE_LEN).min(b64_len);
+ write_bytes(buf, &mut pos, &b64_bytes[b64_pos..end]);
+ buf[pos] = b'\n';
+ pos += 1;
+ b64_pos = end;
+ }
+
+ write_bytes(buf, &mut pos, b"-----END ");
+ write_bytes(buf, &mut pos, label_b);
+ write_bytes(buf, &mut pos, b"-----\n");
+
+ debug_assert_eq!(pos, total);
+ Ok(out)
+}
+
+#[inline(always)]
+fn write_bytes(buf: &mut [u8], pos: &mut usize, src: &[u8]) {
+ buf[*pos..*pos + src.len()].copy_from_slice(src);
+ *pos += src.len();
+}
diff --git a/crypto/armor/src/pem/error.rs b/crypto/armor/src/pem/error.rs
new file mode 100644
index 0000000..7d022c0
--- /dev/null
+++ b/crypto/armor/src/pem/error.rs
@@ -0,0 +1,26 @@
+//! PEM 오류 타입 모듈입니다.
+
+/// PEM 인코딩/디코딩 중 발생하는 오류 열거형입니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PemError {
+ /// `-----BEGIN ...-----` 줄을 찾을 수 없음
+ MissingHeader,
+ /// `-----END ...-----` 줄을 찾을 수 없음
+ MissingFooter,
+ /// 헤더 형식 위반
+ InvalidHeader,
+ /// 푸터 형식 위반
+ InvalidFooter,
+ /// BEGIN과 END 레이블 불일치
+ LabelMismatch,
+ /// 허용 목록에 없는 레이블
+ UnknownLabel,
+ /// 유효하지 않은 Base64 인코딩
+ Base64Error,
+ /// Base64 본문이 없음
+ EmptyBody,
+ /// 유효하지 않은 DER 외곽 구조
+ InvalidDer,
+ /// SecureBuffer 할당 실패
+ AllocationError,
+}
diff --git a/crypto/armor/src/pem/filter.rs b/crypto/armor/src/pem/filter.rs
new file mode 100644
index 0000000..547dcd0
--- /dev/null
+++ b/crypto/armor/src/pem/filter.rs
@@ -0,0 +1,20 @@
+//! DER 봉투 검증 필터 모듈입니다.
+
+use super::error::PemError;
+use crate::der::DerReader;
+use crate::error::ArmorError;
+use crate::error::ArmorError::PEM;
+
+/// DER 최상위 TLV 구조를 검증하는 함수입니다.
+///
+/// # Security Note
+/// 정확히 하나의 완전한 TLV를 강제하여 절단 페이로드와
+/// 트레일링 데이터를 거부합니다. DER 파서의 검증 로직을 재사용합니다.
+pub(crate) fn validate_der_envelope(der: &[u8]) -> Result<(), ArmorError> {
+ let mut reader = DerReader::new(der).map_err(|_| PEM(PemError::InvalidDer))?;
+ reader.read_tlv().map_err(|_| PEM(PemError::InvalidDer))?;
+ reader
+ .expect_empty()
+ .map_err(|_| PEM(PemError::InvalidDer))?;
+ Ok(())
+}
diff --git a/crypto/armor/src/pem/label.rs b/crypto/armor/src/pem/label.rs
new file mode 100644
index 0000000..d4b2b61
--- /dev/null
+++ b/crypto/armor/src/pem/label.rs
@@ -0,0 +1,43 @@
+//! PEM 레이블 허용 목록 모듈입니다.
+
+use super::error::PemError;
+use crate::error::ArmorError;
+use crate::error::ArmorError::PEM;
+
+/// 파이프라인 보안 정책에 따라 허용된 PEM 레이블 열거형입니다.
+///
+/// # Security Note
+/// 허용 목록 외 레이블은 즉시 `UnknownLabel`로 거부합니다.
+/// 암호화되지 않은 개인 키(`PRIVATE KEY`)는 의도적으로 제외됩니다.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PemLabel {
+ /// PKCS#8 EncryptedPrivateKeyInfo (RFC 5958) — 암호화된 개인 키
+ EncryptedPrivateKey,
+ /// X.509 인증서 (RFC 5280)
+ Certificate, // todo: 크레이트 격리
+ /// SubjectPublicKeyInfo (RFC 5480)
+ PublicKey,
+ /// PKCS#10 인증서 서명 요청 (RFC 2986)
+ CertificateRequest,
+}
+
+impl PemLabel {
+ pub(crate) fn as_bytes(&self) -> &'static [u8] {
+ match self {
+ Self::EncryptedPrivateKey => b"ENCRYPTED PRIVATE KEY",
+ Self::Certificate => b"CERTIFICATE", // todo: 크레이트 격리
+ Self::PublicKey => b"PUBLIC KEY",
+ Self::CertificateRequest => b"CERTIFICATE REQUEST",
+ }
+ }
+
+ pub(crate) fn from_bytes(label: &[u8]) -> Result {
+ match label {
+ b"ENCRYPTED PRIVATE KEY" => Ok(Self::EncryptedPrivateKey),
+ b"CERTIFICATE" => Ok(Self::Certificate), // todo: 크레이트 격리
+ b"PUBLIC KEY" => Ok(Self::PublicKey),
+ b"CERTIFICATE REQUEST" => Ok(Self::CertificateRequest),
+ _ => Err(PEM(PemError::UnknownLabel)),
+ }
+ }
+}
diff --git a/crypto/armor/src/pem/mod.rs b/crypto/armor/src/pem/mod.rs
new file mode 100644
index 0000000..efccd9e
--- /dev/null
+++ b/crypto/armor/src/pem/mod.rs
@@ -0,0 +1,27 @@
+//! PEM (Privacy Enhanced Mail) 인코더/디코더 모듈입니다.
+//!
+//! RFC 7468 기반의 PEM 포맷으로 DER 바이트열을 인코딩/디코딩합니다.
+//! DER 입력만 허용하며, 허용된 레이블 목록으로 구조체 타입을 제한합니다.
+//!
+//! # Examples
+//! ```rust,ignore
+//! use entlib_native_armor::pem::{encode, decode, PemLabel};
+//!
+//! // DER 인코딩 (DER은 이미 암호화된 상태여야 함)
+//! let pem_buf = encode(&der_bytes, PemLabel::EncryptedPrivateKey).unwrap();
+//!
+//! // PEM 디코딩
+//! let (label, der_buf) = decode(pem_buf.as_slice()).unwrap();
+//! assert_eq!(label, PemLabel::EncryptedPrivateKey);
+//! ```
+
+mod decoder;
+mod encoder;
+mod error;
+mod filter;
+mod label;
+
+pub use decoder::decode;
+pub use encoder::encode;
+pub use error::PemError;
+pub use label::PemLabel;
diff --git a/crypto/armor/tests/der_test.rs b/crypto/armor/tests/der_test.rs
new file mode 100644
index 0000000..2bc1909
--- /dev/null
+++ b/crypto/armor/tests/der_test.rs
@@ -0,0 +1,455 @@
+#[cfg(test)]
+mod tests {
+ extern crate std;
+ use entlib_native_armor::ArmorError::{ASN1, DER};
+ use entlib_native_armor::asn1::{ASN1Error, Oid};
+ use entlib_native_armor::der::{DerError, DerReader, DerWriter, MAX_DEPTH};
+ use std::vec;
+
+ //
+ // 길이 인코딩
+ //
+
+ #[test]
+ fn length_short_form_roundtrip() {
+ let mut w = DerWriter::new();
+ w.write_null().unwrap();
+ // NULL = 05 00 (길이 0 → 단형식 0x00)
+ assert_eq!(w.finish(), &[0x05, 0x00]);
+ }
+
+ #[test]
+ fn length_long_form_roundtrip() {
+ // 길이 128짜리 OCTET STRING: 04 81 80 [128 bytes of 0xAB]
+ let data = vec![0xABu8; 128];
+ let mut w = DerWriter::new();
+ w.write_octet_string(&data).unwrap();
+ let encoded = w.finish();
+ assert_eq!(&encoded[..3], &[0x04, 0x81, 0x80]);
+
+ let mut r = DerReader::new(&encoded).unwrap();
+ let decoded = r.read_octet_string().unwrap();
+ assert_eq!(decoded, data.as_slice());
+ r.expect_empty().unwrap();
+ }
+
+ #[test]
+ fn reject_indefinite_length() {
+ // 0x30 0x80 ... (SEQUENCE with indefinite length)
+ let input = [0x30u8, 0x80, 0x00, 0x00];
+ let mut depth = MAX_DEPTH;
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_sequence(&mut depth).unwrap_err();
+ assert_eq!(err, DER(DerError::IndefiniteLength));
+ }
+
+ #[test]
+ fn reject_non_minimal_length() {
+ // 길이 1을 장형식(0x81 0x01)으로 표현 — DER 위반
+ let input = [0x04u8, 0x81, 0x01, 0xAA];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_octet_string().unwrap_err();
+ assert_eq!(err, DER(DerError::NonMinimalLength));
+ }
+
+ #[test]
+ fn reject_length_leading_zero() {
+ // 장형식 길이 앞에 0x00 (0x82 0x00 0x80)
+ let input = [0x04u8, 0x82, 0x00, 0x80];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_octet_string().unwrap_err();
+ assert_eq!(err, DER(DerError::NonMinimalLength));
+ }
+
+ //
+ // 태그 거부
+ //
+
+ #[test]
+ fn reject_long_form_tag() {
+ // 0x1F = 장형식 태그 시작 마커 → 거부
+ let input = [0x1Fu8, 0x01, 0x01, 0x00];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_tlv().unwrap_err();
+ assert_eq!(err, DER(DerError::InvalidTag));
+ }
+
+ #[test]
+ fn reject_eoc_tag() {
+ let input = [0x00u8, 0x00];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_tlv().unwrap_err();
+ assert_eq!(err, DER(DerError::InvalidTag));
+ }
+
+ //
+ // NULL
+ //
+
+ #[test]
+ fn null_roundtrip() {
+ let mut w = DerWriter::new();
+ w.write_null().unwrap();
+ let enc = w.finish();
+ assert_eq!(enc, &[0x05, 0x00]);
+
+ let mut r = DerReader::new(&enc).unwrap();
+ r.read_null().unwrap();
+ r.expect_empty().unwrap();
+ }
+
+ #[test]
+ fn reject_null_with_content() {
+ // NULL이 0 이외의 길이를 가지면 거부
+ let input = [0x05u8, 0x01, 0x00];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_null().unwrap_err();
+ assert_eq!(err, DER(DerError::InvalidLength));
+ }
+
+ //
+ // BOOLEAN
+ //
+
+ #[test]
+ fn boolean_roundtrip() {
+ for &val in &[true, false] {
+ let mut w = DerWriter::new();
+ w.write_boolean(val).unwrap();
+ let enc = w.finish();
+ let mut r = DerReader::new(&enc).unwrap();
+ assert_eq!(r.read_boolean().unwrap(), val);
+ }
+ }
+
+ #[test]
+ fn reject_ber_boolean() {
+ // BER true는 0x01이지만 DER에서는 0xFF만 허용
+ let input = [0x01u8, 0x01, 0x01];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_boolean().unwrap_err();
+ assert_eq!(err, DER(DerError::InvalidBooleanEncoding));
+ }
+
+ //
+ // INTEGER
+ //
+
+ #[test]
+ fn integer_zero_roundtrip() {
+ let mut w = DerWriter::new();
+ w.write_integer_unsigned(&[]).unwrap();
+ let enc = w.finish();
+ // INTEGER 0 = 02 01 00
+ assert_eq!(enc, &[0x02, 0x01, 0x00]);
+
+ let mut r = DerReader::new(&enc).unwrap();
+ let bytes = r.read_integer_bytes().unwrap();
+ assert_eq!(bytes, &[0x00]);
+ }
+
+ #[test]
+ fn integer_positive_with_high_bit() {
+ // 값 0x80 → 부호 바이트 필요: 02 02 00 80
+ let mut w = DerWriter::new();
+ w.write_integer_unsigned(&[0x80]).unwrap();
+ let enc = w.finish();
+ assert_eq!(enc, &[0x02, 0x02, 0x00, 0x80]);
+ }
+
+ #[test]
+ fn integer_strips_leading_zeros() {
+ // [0x00, 0x00, 0x01] → 값 1 → 02 01 01
+ let mut w = DerWriter::new();
+ w.write_integer_unsigned(&[0x00, 0x00, 0x01]).unwrap();
+ let enc = w.finish();
+ assert_eq!(enc, &[0x02, 0x01, 0x01]);
+ }
+
+ #[test]
+ fn reject_non_minimal_integer() {
+ // 02 03 00 00 01 → 선행 0x00 뒤의 바이트 MSB = 0 → 비최소
+ let input = [0x02u8, 0x03, 0x00, 0x00, 0x01];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_integer_bytes().unwrap_err();
+ assert_eq!(err, DER(DerError::NonMinimalInteger));
+ }
+
+ #[test]
+ fn reject_non_minimal_integer_neg() {
+ // 02 03 FF FF 80 → 선행 0xFF 뒤의 바이트 MSB = 1 → 비최소
+ let input = [0x02u8, 0x03, 0xFF, 0xFF, 0x80];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_integer_bytes().unwrap_err();
+ assert_eq!(err, DER(DerError::NonMinimalInteger));
+ }
+
+ //
+ // OCTET STRING
+ //
+
+ #[test]
+ fn octet_string_roundtrip() {
+ let data = b"hello DER";
+ let mut w = DerWriter::new();
+ w.write_octet_string(data).unwrap();
+ let enc = w.finish();
+
+ let mut r = DerReader::new(&enc).unwrap();
+ assert_eq!(r.read_octet_string().unwrap(), data);
+ r.expect_empty().unwrap();
+ }
+
+ //
+ // BIT STRING
+ //
+
+ #[test]
+ fn bit_string_roundtrip() {
+ let data = [0xDE, 0xAD, 0xBE, 0xEF];
+ let mut w = DerWriter::new();
+ w.write_bit_string(&data, 0).unwrap();
+ let enc = w.finish();
+
+ let mut r = DerReader::new(&enc).unwrap();
+ let (decoded, unused) = r.read_bit_string().unwrap();
+ assert_eq!(decoded, &data);
+ assert_eq!(unused, 0);
+ }
+
+ #[test]
+ fn reject_bit_string_invalid_unused_bits() {
+ // unused_bits = 8 → 범위 초과
+ let input = [0x03u8, 0x02, 0x08, 0x00];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_bit_string().unwrap_err();
+ assert_eq!(err, DER(DerError::InvalidBitString));
+ }
+
+ // OID
+
+ // ML-DSA-44: 2.16.840.1.101.3.4.3.17 (18: 65)
+ // ML-DSA-87: .....................19
+ // 수동 인코딩 (44) >>>
+ // first_sub = 40*2+16 = 96 = 0x60 → [0x60]
+ // 840 = 6*128+72 → [0x86, 0x48]
+ // 1 → [0x01]
+ // 101 → [0x65]
+ // 3 → [0x03]
+ // 4 → [0x04]
+ // 3 → [0x03]
+ // 17 → [0x11]
+ // 값 바이트: 60 86 48 01 65 03 04 03 11 (9 bytes)
+ // TLV: 06 09 60 86 48 01 65 03 04 03 11
+ const MLDSA44_DER: [u8; 11] = [
+ 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03,
+ 0x11,
+ // 바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?
+ //바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?바이트 맞는데?
+ ];
+
+ #[test]
+ fn oid_decode_mldsa44() {
+ let expected = Oid::from_arcs(&[2, 16, 840, 1, 101, 3, 4, 3, 17]).unwrap();
+ let mut r = DerReader::new(&MLDSA44_DER).unwrap();
+ let oid = r.read_oid().unwrap();
+ assert!(oid.ct_eq(&expected));
+ }
+
+ #[test]
+ fn oid_encode_mldsa44() {
+ let oid = Oid::from_arcs(&[2, 16, 840, 1, 101, 3, 4, 3, 17]).unwrap();
+ let mut w = DerWriter::new();
+ w.write_oid(&oid).unwrap();
+ assert_eq!(w.finish().as_slice(), &MLDSA44_DER);
+ }
+
+ #[test]
+ fn oid_ct_eq_distinguishes_different_oids() {
+ let a = Oid::from_arcs(&[2, 16, 840, 1, 101, 3, 4, 3, 17]).unwrap();
+ let b = Oid::from_arcs(&[2, 16, 840, 1, 101, 3, 4, 3, 18]).unwrap(); // 65
+ assert!(!a.ct_eq(&b));
+ }
+
+ #[test]
+ fn oid_reject_invalid_first_arc() {
+ let err = Oid::from_arcs(&[3, 0]).unwrap_err();
+ assert_eq!(err, ASN1(ASN1Error::InvalidOid));
+ }
+
+ #[test]
+ fn oid_reject_second_arc_out_of_range() {
+ // 첫 아크 0이면 두 번째는 0–39
+ let err = Oid::from_arcs(&[0, 40]).unwrap_err();
+ assert_eq!(err, ASN1(ASN1Error::InvalidOid));
+ }
+
+ #[test]
+ fn oid_roundtrip_rsaencryption() {
+ // rsaEncryption: 1.2.840.113549.1.1.1
+ let oid = Oid::from_arcs(&[1, 2, 840, 113549, 1, 1, 1]).unwrap();
+ let mut w = DerWriter::new();
+ w.write_oid(&oid).unwrap();
+ let enc = w.finish();
+
+ let mut r = DerReader::new(&enc).unwrap();
+ let decoded = r.read_oid().unwrap();
+ assert!(decoded.ct_eq(&oid));
+ }
+
+ //
+ // SEQUENCE
+ //
+
+ #[test]
+ fn sequence_roundtrip() {
+ let oid = Oid::from_arcs(&[2, 16, 840, 1, 101, 3, 4, 3, 17]).unwrap();
+ let data = b"test payload";
+
+ let mut inner = DerWriter::new();
+ inner.write_oid(&oid).unwrap();
+ inner.write_octet_string(data).unwrap();
+
+ let mut outer = DerWriter::new();
+ outer.write_sequence(&inner.finish()).unwrap();
+ let enc = outer.finish();
+
+ let mut depth = MAX_DEPTH;
+ let mut r = DerReader::new(&enc).unwrap();
+ let mut seq = r.read_sequence(&mut depth).unwrap();
+ let decoded_oid = seq.read_oid().unwrap();
+ let decoded_data = seq.read_octet_string().unwrap();
+ seq.expect_empty().unwrap();
+ r.expect_empty().unwrap();
+
+ assert!(decoded_oid.ct_eq(&oid));
+ assert_eq!(decoded_data, data);
+ }
+
+ #[test]
+ fn nested_sequence_depth_tracking() {
+ // SEQUENCE { SEQUENCE { NULL } } — 깊이 2 소비
+ let mut level2 = DerWriter::new();
+ level2.write_null().unwrap();
+
+ let mut level1 = DerWriter::new();
+ level1.write_sequence(&level2.finish()).unwrap();
+
+ let mut outer = DerWriter::new();
+ outer.write_sequence(&level1.finish()).unwrap();
+ let enc = outer.finish();
+
+ let mut depth = MAX_DEPTH;
+ let mut r = DerReader::new(&enc).unwrap();
+ let mut s1 = r.read_sequence(&mut depth).unwrap();
+ assert_eq!(depth, MAX_DEPTH - 1);
+ let mut s2 = s1.read_sequence(&mut depth).unwrap();
+ assert_eq!(depth, MAX_DEPTH - 2);
+ s2.read_null().unwrap();
+ s2.expect_empty().unwrap();
+ s1.expect_empty().unwrap();
+ r.expect_empty().unwrap();
+ }
+
+ #[test]
+ fn reject_excessive_depth() {
+ // 최대 깊이 초과 시도
+ let mut inner = DerWriter::new();
+ inner.write_null().unwrap();
+ let mut outer = DerWriter::new();
+ outer.write_sequence(&inner.finish()).unwrap();
+ let enc = outer.finish();
+
+ // depth = 0으로 강제 설정
+ let mut depth = 0u8;
+ let mut r = DerReader::new(&enc).unwrap();
+ let err = r.read_sequence(&mut depth).unwrap_err();
+ assert_eq!(err, DER(DerError::MaxDepthExceeded));
+ }
+
+ //
+ // 버퍼 오버리드 방어
+ //
+
+ #[test]
+ fn reject_truncated_tlv() {
+ // 길이 16이라고 주장하지만 실제 데이터 1바이트만 있음
+ let input = [0x04u8, 0x10, 0xAA];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_octet_string().unwrap_err();
+ assert_eq!(err, DER(DerError::UnexpectedEof));
+ }
+
+ #[test]
+ fn reject_truncated_length() {
+ // 장형식 길이를 주장하지만 길이 바이트가 없음 (0x81 후 EOF)
+ let input = [0x04u8, 0x81];
+ let mut r = DerReader::new(&input).unwrap();
+ let err = r.read_octet_string().unwrap_err();
+ assert_eq!(err, DER(DerError::UnexpectedEof));
+ }
+
+ #[test]
+ fn reject_trailing_data() {
+ let mut w = DerWriter::new();
+ w.write_null().unwrap();
+ let mut enc = w.finish();
+ enc.push(0x00); // 잔여 바이트 추가
+
+ let mut r = DerReader::new(&enc).unwrap();
+ r.read_null().unwrap();
+ let err = r.expect_empty().unwrap_err();
+ assert_eq!(err, DER(DerError::TrailingData));
+ }
+
+ //
+ // EXPLICIT / IMPLICIT 컨텍스트 태그
+ //
+
+ #[test]
+ fn explicit_tag_roundtrip() {
+ let data = b"wrapped";
+ let mut inner = DerWriter::new();
+ inner.write_octet_string(data).unwrap();
+
+ let mut w = DerWriter::new();
+ w.write_explicit_tag(0, &inner.finish()).unwrap();
+ let enc = w.finish();
+
+ let mut depth = MAX_DEPTH;
+ let mut r = DerReader::new(&enc).unwrap();
+ let mut tagged = r.read_explicit_tag(0, &mut depth).unwrap();
+ assert_eq!(tagged.read_octet_string().unwrap(), data);
+ tagged.expect_empty().unwrap();
+ r.expect_empty().unwrap();
+ }
+
+ #[test]
+ fn implicit_tag_roundtrip() {
+ let data = b"implicit";
+ let mut w = DerWriter::new();
+ w.write_implicit_tag(1, data).unwrap();
+ let enc = w.finish();
+
+ let mut r = DerReader::new(&enc).unwrap();
+ let value = r.read_implicit_value(1).unwrap();
+ assert_eq!(value, data);
+ r.expect_empty().unwrap();
+ }
+
+ //
+ // SecureBuffer
+ //
+
+ #[test]
+ fn integer_secure_roundtrip() {
+ let key_bytes = [0x01u8, 0x23, 0x45, 0x67];
+ let mut w = DerWriter::new();
+ w.write_integer_unsigned(&key_bytes).unwrap();
+ let enc = w.finish();
+
+ let mut r = DerReader::new(&enc).unwrap();
+ let secure = r.read_integer_secure().unwrap();
+ assert_eq!(secure.as_slice(), &key_bytes);
+ }
+}
diff --git a/crypto/blake/Cargo.toml b/crypto/blake/Cargo.toml
new file mode 100644
index 0000000..8b4688e
--- /dev/null
+++ b/crypto/blake/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "entlib-native-blake"
+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-base.workspace = true
diff --git a/crypto/blake/README.md b/crypto/blake/README.md
new file mode 100644
index 0000000..1ba625f
--- /dev/null
+++ b/crypto/blake/README.md
@@ -0,0 +1,192 @@
+# BLAKE2b / BLAKE3 해시 함수 (entlib-native-blake)
+
+> Q. T. Felix (수정: 26.03.23 UTC+9)
+>
+> [English README](README_EN.md)
+
+`entlib-native-blake`는 RFC 7693(BLAKE2b) 및 BLAKE3 공식 명세를 준수하는 `no_std` 호환 해시 크레이트입니다. 민감 데이터는 `SecureBuffer`(mlock)에 보관하며, Drop 시 내부 상태를 `write_volatile`로 강제 소거합니다.
+
+## 구성
+
+| 모듈 | 알고리즘 | 표준 |
+|-----------|--------------------|----------------------|
+| `blake2b` | BLAKE2b | RFC 7693 |
+| `blake3` | BLAKE3 | BLAKE3 공식 명세 |
+| `lib` | H'(`blake2b_long`) | RFC 9106 Section 3.2 |
+
+---
+
+## BLAKE2b
+
+64비트 플랫폼에 최적화된 암호 해시 함수입니다. 최대 512비트(64바이트) 다이제스트를 생성하며, 키드(keyed) MAC 모드를 지원합니다.
+
+### 구조체
+
+```rust
+pub struct Blake2b {
+ h: [u64; 8], // 체이닝 값 (8 × 64비트)
+ t: [u64; 2], // 바이트 카운터
+ buf: SecureBuffer, // 128바이트 입력 버퍼 (mlock)
+ buf_len: usize,
+ hash_len: usize, // 1..=64
+}
+```
+
+### 초기화 벡터 및 파라미터 블록
+
+IV는 SHA-512 초기 해시 값(소수의 제곱근 소수부)에서 유래합니다.
+
+$$h_0 = \text{IV}[0] \oplus (\text{hash\_len} \mathbin{|} (\text{key\_len} \mathbin{\ll} 8) \mathbin{|} (1 \mathbin{\ll} 16) \mathbin{|} (1 \mathbin{\ll} 24))$$
+
+키드 모드에서는 키를 128바이트 블록으로 제로 패딩한 뒤 `buf_len = 128`로 설정하여 첫 번째 블록으로 처리합니다.
+
+### 압축 함수
+
+12라운드 Feistel 구조를 사용하며, 각 라운드는 SIGMA 치환에 따라 정렬된 메시지 워드를 G 함수에 적용합니다.
+
+**G 함수 (회전: 32 / 24 / 16 / 63)**
+
+$$a \mathrel{+}= b + x, \quad d = (d \oplus a) \ggg 32$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 24$$
+$$a \mathrel{+}= b + y, \quad d = (d \oplus a) \ggg 16$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 63$$
+
+16워드 작업 벡터 $v$는 체이닝 값 $h[0..8]$, IV, 카운터 $t$, 최종화 플래그 $f$로 초기화됩니다.
+
+$$v[12] = \text{IV}[4] \oplus t[0], \quad v[13] = \text{IV}[5] \oplus t[1]$$
+$$v[14] = \text{IV}[6] \oplus f[0], \quad v[15] = \text{IV}[7] \oplus f[1]$$
+
+12라운드 후 체이닝 값을 갱신합니다.
+
+$$h[i] \mathrel{\oplus}= v[i] \oplus v[i+8], \quad i \in [0, 7]$$
+
+### 최종화
+
+마지막 블록 처리 시 $f[0] = \texttt{0xFFFF\_FFFF\_FFFF\_FFFF}$를 설정합니다. 카운터는 `buf_len`만큼 증가하며, 버퍼 나머지는 제로 패딩됩니다. 결과는 $h$에서 LE 바이트 순으로 추출합니다.
+
+### 메모리 보안
+
+Drop 시 `write_volatile`로 `h[0..8]`, `t[0..2]`, `buf_len`을 소거하고 `compiler_fence(SeqCst)`로 재배치를 방지합니다.
+
+---
+
+## blake2b_long (H')
+
+RFC 9106 Section 3.2에서 정의된 가변 출력 해시 함수입니다. Argon2id 블록 초기화 및 최종 태그 생성에 사용됩니다.
+
+**입력**: `LE32(T) || input`, **출력**: T바이트
+
+$$A_1 = \text{BLAKE2b-64}(\mathtt{LE32}(T) \mathbin{\|} \text{input})$$
+
+- $T \le 64$: 단일 `BLAKE2b-T` 호출
+
+- $T > 64$: $r = \lceil T/32 \rceil - 2$, $\text{last\_len} = T - 32r$
+
+$$A_i = \text{BLAKE2b-64}(A_{i-1}), \quad i = 2, \ldots, r$$
+$$A_{r+1} = \text{BLAKE2b-last\_len}(A_r)$$
+
+$$\text{output} = A_1[0..32] \mathbin{\|} A_2[0..32] \mathbin{\|} \cdots \mathbin{\|} A_r[0..32] \mathbin{\|} A_{r+1}$$
+
+각 단계의 중간값은 `SecureBuffer`에 보관됩니다.
+
+---
+
+## BLAKE3
+
+머클 트리 구조 기반의 최신 해시 함수입니다. SIMD 및 다중 스레딩을 통한 병렬 처리가 설계 목표이며, 32바이트 고정 출력 외에 임의 길이 XOF를 지원합니다.
+
+### 구조체
+
+```rust
+pub struct Blake3 {
+ chunk_state: ChunkState, // 현재 청크 상태
+ key_words: [u32; 8], // IV 또는 키 워드
+ cv_stack: [[u32; 8]; 54], // 체이닝 값 스택 (최대 54 레벨)
+ cv_stack_len: usize,
+ flags: u32,
+}
+```
+
+청크 크기는 1024바이트이며, CV 스택의 최대 깊이 54는 입력 크기 $2^{54}$ KiB(약 18 EiB)를 커버합니다.
+
+### 도메인 분리 플래그
+
+| 플래그 | 값 | 용도 |
+|---------------|----------|-------------|
+| `CHUNK_START` | `1 << 0` | 청크의 첫 번째 블록 |
+| `CHUNK_END` | `1 << 1` | 청크의 마지막 블록 |
+| `PARENT` | `1 << 2` | 부모 노드 압축 |
+| `ROOT` | `1 << 3` | 루트 출력 생성 |
+| `KEYED_HASH` | `1 << 4` | 키드 모드 |
+
+### 압축 함수
+
+32비트 워드 기반, 7라운드 압축을 수행합니다. 16워드 상태 벡터를 초기화하고 각 라운드에서 G 함수와 메시지 치환을 적용합니다.
+
+$$\text{state} = [cv[0..8], \text{IV}[0..4], \text{ctr\_lo}, \text{ctr\_hi}, \text{block\_len}, \text{flags}]$$
+
+**G 함수 (회전: 16 / 12 / 8 / 7)**
+
+$$a \mathrel{+}= b + x, \quad d = (d \oplus a) \ggg 16$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 12$$
+$$a \mathrel{+}= b + y, \quad d = (d \oplus a) \ggg 8$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 7$$
+
+각 라운드 후 메시지 워드를 `MSG_PERMUTATION`에 따라 재배열합니다. 7라운드 완료 후:
+
+$$\text{state}[i] \mathrel{\oplus}= \text{state}[i+8], \quad \text{state}[i+8] \mathrel{\oplus}= cv[i]$$
+
+### 트리 해싱 및 CV 스택
+
+입력을 1024바이트 청크 단위로 처리하며, 각 청크의 체이닝 값(CV)을 스택에 누적합니다. `merge_cv_stack`은 누적된 청크 수(`total_chunks`)의 포피카운트(popcount) 불변 조건을 유지하며 부모 노드를 생성합니다.
+
+```
+total_chunks = 4 (이진: 100)이 될 때:
+ 스택: [CV_0, CV_1, CV_2, CV_3]
+ → merge: parent(CV_2, CV_3) → P_23
+ → merge: parent(CV_0, CV_1) → P_01
+ → merge: parent(P_01, P_23) → root
+```
+
+이 설계는 메시지 길이를 사전에 알지 못해도 단일 패스로 머클 트리를 구성할 수 있게 합니다.
+
+### XOF (확장 가능 출력)
+
+루트 노드에 `ROOT` 플래그를 설정하고 카운터를 증가시켜 임의 길이 출력을 생성합니다.
+
+$$\text{output}[64k .. 64k+64] = \text{compress}(cv_\text{root}, bw, k, bl, \text{flags} \mathbin{|} \text{ROOT}), \quad k = 0, 1, 2, \ldots$$
+
+### 메모리 보안
+
+Drop 시 `write_volatile`로 `key_words`, `cv_stack` 전체를 소거합니다. `ChunkState` Drop 시에도 `buf`와 `chaining_value`를 소거합니다.
+
+---
+
+## 사용 예시
+
+```rust
+use entlib_native_blake::{Blake2b, Blake3, blake2b_long};
+
+// BLAKE2b-32
+let mut h = Blake2b::new(32);
+h.update(b"hello world");
+let digest = h.finalize().unwrap();
+assert_eq!(digest.as_slice().len(), 32);
+
+// BLAKE3 (32바이트)
+let mut h = Blake3::new();
+h.update(b"hello world");
+let digest = h.finalize().unwrap();
+assert_eq!(digest.as_slice().len(), 32);
+
+// H' — Argon2id 블록 초기화용 (1024바이트)
+let out = blake2b_long(b"input", 1024).unwrap();
+assert_eq!(out.as_slice().len(), 1024);
+```
+
+## 의존성
+
+| 크레이트 | 용도 |
+|-------------------------------|-----------------|
+| `entlib-native-secure-buffer` | 민감 데이터 mlock 보관 |
+| `entlib-native-constant-time` | 상수-시간 연산 |
diff --git a/crypto/blake/README_EN.md b/crypto/blake/README_EN.md
new file mode 100644
index 0000000..64b39be
--- /dev/null
+++ b/crypto/blake/README_EN.md
@@ -0,0 +1,192 @@
+# BLAKE2b / BLAKE3 Hash Functions (entlib-native-blake)
+
+> Q. T. Felix (Modified: 26.03.23 UTC+9)
+>
+> [Korean README](README.md)
+
+`entlib-native-blake` is a `no_std` compatible hash crate that complies with RFC 7693 (BLAKE2b) and the official BLAKE3 specification. Sensitive data is stored in `SecureBuffer` (mlock), and the internal state is forcibly erased with `write_volatile` on Drop.
+
+## Configuration
+
+| Module | Algorithm | Standard |
+|-----------|--------------------|-------------------------------|
+| `blake2b` | BLAKE2b | RFC 7693 |
+| `blake3` | BLAKE3 | BLAKE3 Official Specification |
+| `lib` | H'(`blake2b_long`) | RFC 9106 Section 3.2 |
+
+---
+
+## BLAKE2b
+
+A cryptographic hash function optimized for 64-bit platforms. It generates a digest of up to 512 bits (64 bytes) and supports a keyed MAC mode.
+
+### Struct
+
+```rust
+pub struct Blake2b {
+ h: [u64; 8], // Chaining values (8 × 64-bit)
+ t: [u64; 2], // Byte counter
+ buf: SecureBuffer, // 128-byte input buffer (mlock)
+ buf_len: usize,
+ hash_len: usize, // 1..=64
+}
+```
+
+### Initialization Vector and Parameter Block
+
+The IV is derived from the SHA-512 initial hash values (fractional parts of the square roots of the first eight primes).
+
+$$h_0 = \text{IV}[0] \oplus (\text{hash\_len} \mathbin{|} (\text{key\_len} \mathbin{\ll} 8) \mathbin{|} (1 \mathbin{\ll} 16) \mathbin{|} (1 \mathbin{\ll} 24))$$
+
+In keyed mode, the key is zero-padded to a 128-byte block and processed as the first block by setting `buf_len = 128`.
+
+### Compression Function
+
+It uses a 12-round Feistel structure, and each round applies the G function to the message words sorted according to the SIGMA permutation.
+
+**G Function (Rotations: 32 / 24 / 16 / 63)**
+
+$$a \mathrel{+}= b + x, \quad d = (d \oplus a) \ggg 32$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 24$$
+$$a \mathrel{+}= b + y, \quad d = (d \oplus a) \ggg 16$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 63$$
+
+The 16-word working vector $v$ is initialized with the chaining values $h[0..8]$, the IV, the counter $t$, and the finalization flag $f$.
+
+$$v[12] = \text{IV}[4] \oplus t[0], \quad v[13] = \text{IV}[5] \oplus t[1]$$
+$$v[14] = \text{IV}[6] \oplus f[0], \quad v[15] = \text{IV}[7] \oplus f[1]$$
+
+After 12 rounds, the chaining values are updated.
+
+$$h[i] \mathrel{\oplus}= v[i] \oplus v[i+8], \quad i \in [0, 7]$$
+
+### Finalization
+
+When processing the last block, $f[0] = \texttt{0xFFFF\_FFFF\_FFFF\_FFFF}$ is set. The counter is incremented by `buf_len`, and the rest of the buffer is zero-padded. The result is extracted from $h$ in LE byte order.
+
+### Memory Security
+
+On Drop, `h[0..8]`, `t[0..2]`, and `buf_len` are erased with `write_volatile`, and `compiler_fence(SeqCst)` prevents reordering.
+
+---
+
+## blake2b_long (H')
+
+A variable-output hash function defined in RFC 9106 Section 3.2. It is used for Argon2id block initialization and final tag generation.
+
+**Input**: `LE32(T) || input`, **Output**: T bytes
+
+$$A_1 = \text{BLAKE2b-64}(\mathtt{LE32}(T) \mathbin{\|} \text{input})$$
+
+- $T \le 64$: Single `BLAKE2b-T` call
+
+- $T > 64$: $r = \lceil T/32 \rceil - 2$, $\text{last\_len} = T - 32r$
+
+$$A_i = \text{BLAKE2b-64}(A_{i-1}), \quad i = 2, \ldots, r$$
+$$A_{r+1} = \text{BLAKE2b-last\_len}(A_r)$$
+
+$$\text{output} = A_1[0..32] \mathbin{\|} A_2[0..32] \mathbin{\|} \cdots \mathbin{\|} A_r[0..32] \mathbin{\|} A_{r+1}$$
+
+The intermediate values of each step are stored in `SecureBuffer`.
+
+---
+
+## BLAKE3
+
+A modern hash function based on a Merkle tree structure. Its design goals are parallel processing through SIMD and multi-threading, and it supports an arbitrary-length XOF in addition to a 32-byte fixed output.
+
+### Struct
+
+```rust
+pub struct Blake3 {
+ chunk_state: ChunkState, // Current chunk state
+ key_words: [u32; 8], // IV or key words
+ cv_stack: [[u32; 8]; 54], // Chaining value stack (max 54 levels)
+ cv_stack_len: usize,
+ flags: u32,
+}
+```
+
+The chunk size is 1024 bytes, and the maximum depth of the CV stack, 54, covers an input size of $2^{54}$ KiB (about 18 EiB).
+
+### Domain Separation Flags
+
+| Flag | Value | Purpose |
+|---------------|----------|-------------------------|
+| `CHUNK_START` | `1 << 0` | First block of a chunk |
+| `CHUNK_END` | `1 << 1` | Last block of a chunk |
+| `PARENT` | `1 << 2` | Parent node compression |
+| `ROOT` | `1 << 3` | Root output generation |
+| `KEYED_HASH` | `1 << 4` | Keyed mode |
+
+### Compression Function
+
+Performs a 32-bit word-based, 7-round compression. It initializes a 16-word state vector and applies the G function and message permutation in each round.
+
+$$\text{state} = [cv[0..8], \text{IV}[0..4], \text{ctr\_lo}, \text{ctr\_hi}, \text{block\_len}, \text{flags}]$$
+
+**G Function (Rotations: 16 / 12 / 8 / 7)**
+
+$$a \mathrel{+}= b + x, \quad d = (d \oplus a) \ggg 16$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 12$$
+$$a \mathrel{+}= b + y, \quad d = (d \oplus a) \ggg 8$$
+$$c \mathrel{+}= d, \quad b = (b \oplus c) \ggg 7$$
+
+After each round, the message words are rearranged according to `MSG_PERMUTATION`. After 7 rounds are complete:
+
+$$\text{state}[i] \mathrel{\oplus}= \text{state}[i+8], \quad \text{state}[i+8] \mathrel{\oplus}= cv[i]$$
+
+### Tree Hashing and CV Stack
+
+The input is processed in 1024-byte chunks, and the chaining value (CV) of each chunk is accumulated on the stack. `merge_cv_stack` maintains the popcount invariant of the number of accumulated chunks (`total_chunks`) and generates parent nodes.
+
+```
+When total_chunks = 4 (binary: 100):
+ Stack: [CV_0, CV_1, CV_2, CV_3]
+ → merge: parent(CV_2, CV_3) → P_23
+ → merge: parent(CV_0, CV_1) → P_01
+ → merge: parent(P_01, P_23) → root
+```
+
+This design allows the construction of a Merkle tree in a single pass without knowing the message length in advance.
+
+### XOF (eXtendable-Output Function)
+
+Generates an arbitrary-length output by setting the `ROOT` flag on the root node and incrementing the counter.
+
+$$\text{output}[64k .. 64k+64] = \text{compress}(cv_\text{root}, bw, k, bl, \text{flags} \mathbin{|} \text{ROOT}), \quad k = 0, 1, 2, \ldots$$
+
+### Memory Security
+
+On Drop, the entire `key_words` and `cv_stack` are erased with `write_volatile`. On `ChunkState` Drop, `buf` and `chaining_value` are also erased.
+
+---
+
+## Usage Example
+
+```rust
+use entlib_native_blake::{Blake2b, Blake3, blake2b_long};
+
+// BLAKE2b-32
+let mut h = Blake2b::new(32);
+h.update(b"hello world");
+let digest = h.finalize().unwrap();
+assert_eq!(digest.as_slice().len(), 32);
+
+// BLAKE3 (32 bytes)
+let mut h = Blake3::new();
+h.update(b"hello world");
+let digest = h.finalize().unwrap();
+assert_eq!(digest.as_slice().len(), 32);
+
+// H' — for Argon2id block initialization (1024 bytes)
+let out = blake2b_long(b"input", 1024).unwrap();
+assert_eq!(out.as_slice().len(), 1024);
+```
+
+## Dependencies
+
+| Crate | Purpose |
+|-------------------------------|----------------------------------|
+| `entlib-native-secure-buffer` | mlock storage for sensitive data |
+| `entlib-native-constant-time` | Constant-time operations |
diff --git a/crypto/blake/src/blake2b.rs b/crypto/blake/src/blake2b.rs
new file mode 100644
index 0000000..e963b4c
--- /dev/null
+++ b/crypto/blake/src/blake2b.rs
@@ -0,0 +1,234 @@
+//! BLAKE2b 코어 구현 모듈입니다.
+//! RFC 7693 명세를 완전히 준수합니다.
+
+use core::ptr::write_volatile;
+use core::sync::atomic::{Ordering, compiler_fence};
+use entlib_native_base::error::hash::HashError;
+use entlib_native_secure_buffer::SecureBuffer;
+
+const IV: [u64; 8] = [
+ 0x6a09e667f3bcc908,
+ 0xbb67ae8584caa73b,
+ 0x3c6ef372fe94f82b,
+ 0xa54ff53a5f1d36f1,
+ 0x510e527fade682d1,
+ 0x9b05688c2b3e6c1f,
+ 0x1f83d9abfb41bd6b,
+ 0x5be0cd19137e2179,
+];
+
+const SIGMA: [[usize; 16]; 10] = [
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
+ [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
+ [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
+ [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
+ [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
+ [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
+ [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
+ [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
+ [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
+ [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
+];
+
+/// BLAKE2b 상태 구조체입니다.
+///
+/// # Security Note
+/// Drop 시 내부 체이닝 값과 카운터를 `write_volatile`로 강제 소거합니다.
+pub struct Blake2b {
+ h: [u64; 8],
+ t: [u64; 2],
+ buf: SecureBuffer,
+ buf_len: usize,
+ hash_len: usize,
+}
+
+impl Blake2b {
+ /// 비키드(plain) BLAKE2b 인스턴스를 생성하는 함수입니다.
+ ///
+ /// # Errors
+ /// `hash_len`이 1..=64 범위를 벗어나거나 SecureBuffer 할당 실패 시 패닉.
+ pub fn new(hash_len: usize) -> Self {
+ assert!((1..=64).contains(&hash_len), "hash_len must be 1..=64");
+ Self::init(hash_len, 0)
+ }
+
+ /// 키드(keyed MAC) BLAKE2b 인스턴스를 생성하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 키는 128바이트 패딩 후 첫 번째 블록으로 처리됩니다.
+ ///
+ /// # Errors
+ /// `key`가 1..=64 범위를 벗어나거나 `hash_len`이 범위를 벗어나면 패닉.
+ pub fn new_keyed(hash_len: usize, key: &[u8]) -> Self {
+ assert!((1..=64).contains(&hash_len), "hash_len must be 1..=64");
+ assert!((1..=64).contains(&key.len()), "key len must be 1..=64");
+ let mut state = Self::init(hash_len, key.len());
+ // 키를 128바이트 블록으로 패딩하여 버퍼에 저장
+ state.buf.as_mut_slice()[..key.len()].copy_from_slice(key);
+ state.buf_len = 128;
+ state
+ }
+
+ fn init(hash_len: usize, key_len: usize) -> Self {
+ let p0 = (hash_len as u64)
+ | ((key_len as u64) << 8)
+ | (1u64 << 16) // fanout = 1
+ | (1u64 << 24); // max_depth = 1
+ let mut h = IV;
+ h[0] ^= p0;
+ Self {
+ h,
+ t: [0u64; 2],
+ buf: SecureBuffer::new_owned(128).expect("Blake2b: SecureBuffer alloc failed"),
+ buf_len: 0,
+ hash_len,
+ }
+ }
+
+ /// 데이터를 해시 상태에 공급하는 함수입니다.
+ pub fn update(&mut self, data: &[u8]) {
+ let mut input = data;
+ loop {
+ // 버퍼가 가득 차 있고 추가 데이터가 있을 때만 비-최종 압축
+ if self.buf_len == 128 && !input.is_empty() {
+ add_to_counter(&mut self.t, 128);
+ let block = load_block(self.buf.as_slice());
+ compress(&mut self.h, &block, self.t, [0u64, 0u64]);
+ // 버퍼 소거 후 재사용
+ for b in self.buf.as_mut_slice() {
+ *b = 0;
+ }
+ self.buf_len = 0;
+ }
+ if input.is_empty() {
+ break;
+ }
+ let take = (128 - self.buf_len).min(input.len());
+ self.buf.as_mut_slice()[self.buf_len..self.buf_len + take]
+ .copy_from_slice(&input[..take]);
+ self.buf_len += take;
+ input = &input[take..];
+ }
+ }
+
+ /// 해시를 완료하고 다이제스트를 SecureBuffer로 반환하는 함수입니다.
+ ///
+ /// # Security Note
+ /// 내부 상태는 함수 종료 시 Drop을 통해 소거됩니다.
+ pub fn finalize(mut self) -> Result {
+ // 남은 바이트 수만큼 카운터 증가
+ add_to_counter(&mut self.t, self.buf_len as u64);
+ // 버퍼 나머지를 0으로 패딩
+ for b in &mut self.buf.as_mut_slice()[self.buf_len..] {
+ *b = 0;
+ }
+ let block = load_block(self.buf.as_slice());
+ // 최종 블록: f[0] = 0xFFFF...
+ compress(&mut self.h, &block, self.t, [0xFFFF_FFFF_FFFF_FFFF, 0u64]);
+
+ let mut out = SecureBuffer::new_owned(self.hash_len)?;
+ let out_slice = out.as_mut_slice();
+ let mut pos = 0;
+ for word in &self.h {
+ let bytes = word.to_le_bytes();
+ let take = (self.hash_len - pos).min(8);
+ out_slice[pos..pos + take].copy_from_slice(&bytes[..take]);
+ pos += take;
+ if pos >= self.hash_len {
+ break;
+ }
+ }
+ Ok(out)
+ }
+}
+
+impl Drop for Blake2b {
+ fn drop(&mut self) {
+ for word in &mut self.h {
+ unsafe { write_volatile(word, 0u64) };
+ }
+ unsafe {
+ write_volatile(&mut self.t[0], 0u64);
+ write_volatile(&mut self.t[1], 0u64);
+ write_volatile(&mut self.buf_len, 0usize);
+ }
+ compiler_fence(Ordering::SeqCst);
+ }
+}
+
+//
+// 내부 헬퍼
+//
+
+#[inline(always)]
+fn g(v: &mut [u64; 16], a: usize, b: usize, c: usize, d: usize, x: u64, y: u64) {
+ v[a] = v[a].wrapping_add(v[b]).wrapping_add(x);
+ v[d] = (v[d] ^ v[a]).rotate_right(32);
+ v[c] = v[c].wrapping_add(v[d]);
+ v[b] = (v[b] ^ v[c]).rotate_right(24);
+ v[a] = v[a].wrapping_add(v[b]).wrapping_add(y);
+ v[d] = (v[d] ^ v[a]).rotate_right(16);
+ v[c] = v[c].wrapping_add(v[d]);
+ v[b] = (v[b] ^ v[c]).rotate_right(63);
+}
+
+fn compress(h: &mut [u64; 8], m: &[u64; 16], t: [u64; 2], f: [u64; 2]) {
+ let mut v = [
+ h[0],
+ h[1],
+ h[2],
+ h[3],
+ h[4],
+ h[5],
+ h[6],
+ h[7],
+ IV[0],
+ IV[1],
+ IV[2],
+ IV[3],
+ IV[4] ^ t[0],
+ IV[5] ^ t[1],
+ IV[6] ^ f[0],
+ IV[7] ^ f[1],
+ ];
+ for r in 0..12 {
+ let s = &SIGMA[r % 10];
+ g(&mut v, 0, 4, 8, 12, m[s[0]], m[s[1]]);
+ g(&mut v, 1, 5, 9, 13, m[s[2]], m[s[3]]);
+ g(&mut v, 2, 6, 10, 14, m[s[4]], m[s[5]]);
+ g(&mut v, 3, 7, 11, 15, m[s[6]], m[s[7]]);
+ g(&mut v, 0, 5, 10, 15, m[s[8]], m[s[9]]);
+ g(&mut v, 1, 6, 11, 12, m[s[10]], m[s[11]]);
+ g(&mut v, 2, 7, 8, 13, m[s[12]], m[s[13]]);
+ g(&mut v, 3, 4, 9, 14, m[s[14]], m[s[15]]);
+ }
+ for i in 0..8 {
+ h[i] ^= v[i] ^ v[i + 8];
+ }
+}
+
+fn load_block(bytes: &[u8]) -> [u64; 16] {
+ let mut m = [0u64; 16];
+ for (i, word) in m.iter_mut().enumerate() {
+ let s = i * 8;
+ *word = u64::from_le_bytes([
+ bytes[s],
+ bytes[s + 1],
+ bytes[s + 2],
+ bytes[s + 3],
+ bytes[s + 4],
+ bytes[s + 5],
+ bytes[s + 6],
+ bytes[s + 7],
+ ]);
+ }
+ m
+}
+
+fn add_to_counter(t: &mut [u64; 2], n: u64) {
+ let (new_t0, overflow) = t[0].overflowing_add(n);
+ t[0] = new_t0;
+ if overflow {
+ t[1] = t[1].wrapping_add(1);
+ }
+}
diff --git a/crypto/blake/src/blake3.rs b/crypto/blake/src/blake3.rs
new file mode 100644
index 0000000..9d6a0e3
--- /dev/null
+++ b/crypto/blake/src/blake3.rs
@@ -0,0 +1,387 @@
+//! BLAKE3 코어 구현 모듈입니다.
+//! BLAKE3 명세(https://github.com/BLAKE3-team/BLAKE3-specs)를 준수합니다.
+
+use core::ptr::write_volatile;
+use core::sync::atomic::{Ordering, compiler_fence};
+use entlib_native_base::error::hash::HashError;
+use entlib_native_secure_buffer::SecureBuffer;
+
+const IV: [u32; 8] = [
+ 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
+];
+
+const MSG_PERMUTATION: [usize; 16] = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
+
+const CHUNK_START: u32 = 1 << 0;
+const CHUNK_END: u32 = 1 << 1;
+const PARENT: u32 = 1 << 2;
+const ROOT: u32 = 1 << 3;
+const KEYED_HASH: u32 = 1 << 4;
+const BLOCK_LEN: usize = 64;
+const CHUNK_LEN: usize = 1024;
+pub const OUT_LEN: usize = 32;
+
+/// BLAKE3 해시 상태 구조체입니다.
+///
+/// # Security Note
+/// Drop 시 키워드, CV 스택을 `write_volatile`로 소거합니다.
+/// 키드 모드(`new_keyed`)는 키를 IV로 변환하므로 키 바이트가 스택에 노출되지 않습니다.
+pub struct Blake3 {
+ chunk_state: ChunkState,
+ key_words: [u32; 8],
+ cv_stack: [[u32; 8]; 54],
+ cv_stack_len: usize,
+ flags: u32,
+}
+
+impl Blake3 {
+ /// 표준 BLAKE3 인스턴스를 생성하는 함수입니다.
+ pub fn new() -> Self {
+ Self {
+ chunk_state: ChunkState::new(&IV, 0, 0),
+ key_words: IV,
+ cv_stack: [[0u32; 8]; 54],
+ cv_stack_len: 0,
+ flags: 0,
+ }
+ }
+
+ /// 키드 BLAKE3 인스턴스를 생성하는 함수입니다.
+ ///
+ /// # Arguments
+ /// `key` — 정확히 32바이트
+ pub fn new_keyed(key: &[u8; 32]) -> Self {
+ let key_words = words_from_le_bytes_32(key);
+ Self {
+ chunk_state: ChunkState::new(&key_words, 0, KEYED_HASH),
+ key_words,
+ cv_stack: [[0u32; 8]; 54],
+ cv_stack_len: 0,
+ flags: KEYED_HASH,
+ }
+ }
+
+ /// 데이터를 공급하는 함수입니다.
+ pub fn update(&mut self, mut input: &[u8]) {
+ while !input.is_empty() {
+ if self.chunk_state.len() == CHUNK_LEN {
+ let chunk_cv = self.chunk_state.output().chaining_value();
+ let total_chunks = self.chunk_state.chunk_counter + 1;
+ self.push_cv(chunk_cv);
+ self.merge_cv_stack(total_chunks);
+ self.chunk_state = ChunkState::new(&self.key_words, total_chunks, self.flags);
+ }
+ let take = (CHUNK_LEN - self.chunk_state.len()).min(input.len());
+ self.chunk_state.update(&input[..take]);
+ input = &input[take..];
+ }
+ }
+
+ /// 32바이트 해시를 SecureBuffer로 반환하는 함수입니다.
+ pub fn finalize(self) -> Result {
+ self.finalize_xof(OUT_LEN)
+ }
+
+ /// 임의 길이 출력을 SecureBuffer로 반환하는 함수입니다.
+ ///
+ /// # Security Note
+ /// XOF 출력은 ROOT 플래그와 카운터 모드로 무제한 확장됩니다.
+ pub fn finalize_xof(self, out_len: usize) -> Result {
+ let mut output = self.chunk_state.output();
+ let mut parent_nodes = self.cv_stack_len;
+ while parent_nodes > 0 {
+ parent_nodes -= 1;
+ let left_cv = self.cv_stack[parent_nodes];
+ output = parent_output(
+ &left_cv,
+ &output.chaining_value(),
+ &self.key_words,
+ self.flags,
+ );
+ }
+ let mut result = SecureBuffer::new_owned(out_len)?;
+ output.root_output_bytes(result.as_mut_slice());
+ Ok(result)
+ }
+
+ fn push_cv(&mut self, cv: [u32; 8]) {
+ self.cv_stack[self.cv_stack_len] = cv;
+ self.cv_stack_len += 1;
+ }
+
+ fn pop_cv(&mut self) -> [u32; 8] {
+ self.cv_stack_len -= 1;
+ self.cv_stack[self.cv_stack_len]
+ }
+
+ fn merge_cv_stack(&mut self, total_chunks: u64) {
+ let post_merge_len = total_chunks.count_ones() as usize;
+ while self.cv_stack_len > post_merge_len {
+ let right = self.pop_cv();
+ let left = self.pop_cv();
+ let parent = parent_cv(&left, &right, &self.key_words, self.flags);
+ self.push_cv(parent);
+ }
+ }
+}
+
+impl Default for Blake3 {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Drop for Blake3 {
+ fn drop(&mut self) {
+ for word in &mut self.key_words {
+ unsafe { write_volatile(word, 0u32) };
+ }
+ for slot in &mut self.cv_stack {
+ for word in slot.iter_mut() {
+ unsafe { write_volatile(word, 0u32) };
+ }
+ }
+ unsafe { write_volatile(&mut self.cv_stack_len, 0usize) };
+ compiler_fence(Ordering::SeqCst);
+ }
+}
+
+//
+// ChunkState
+//
+
+struct ChunkState {
+ chaining_value: [u32; 8],
+ chunk_counter: u64,
+ buf: [u8; BLOCK_LEN],
+ buf_len: usize,
+ blocks_compressed: u8,
+ flags: u32,
+}
+
+impl ChunkState {
+ fn new(key_words: &[u32; 8], chunk_counter: u64, flags: u32) -> Self {
+ Self {
+ chaining_value: *key_words,
+ chunk_counter,
+ buf: [0u8; BLOCK_LEN],
+ buf_len: 0,
+ blocks_compressed: 0,
+ flags,
+ }
+ }
+
+ fn len(&self) -> usize {
+ BLOCK_LEN * self.blocks_compressed as usize + self.buf_len
+ }
+
+ fn start_flag(&self) -> u32 {
+ if self.blocks_compressed == 0 {
+ CHUNK_START
+ } else {
+ 0
+ }
+ }
+
+ fn update(&mut self, mut input: &[u8]) {
+ while !input.is_empty() {
+ if self.buf_len == BLOCK_LEN {
+ let block_words = words_from_le_bytes_64(&self.buf);
+ self.chaining_value = first_8_words(compress(
+ &self.chaining_value,
+ &block_words,
+ self.chunk_counter,
+ BLOCK_LEN as u32,
+ self.flags | self.start_flag(),
+ ));
+ self.blocks_compressed += 1;
+ self.buf = [0u8; BLOCK_LEN];
+ self.buf_len = 0;
+ }
+ let take = (BLOCK_LEN - self.buf_len).min(input.len());
+ self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&input[..take]);
+ self.buf_len += take;
+ input = &input[take..];
+ }
+ }
+
+ fn output(&self) -> Output {
+ let mut block_words = words_from_le_bytes_64(&self.buf);
+ // 버퍼 끝 이후 부분은 이미 0이어야 하지만 명시적으로 보장
+ let used_words = self.buf_len.div_ceil(4);
+ for w in &mut block_words[used_words..] {
+ *w = 0;
+ }
+ Output {
+ input_chaining_value: self.chaining_value,
+ block_words,
+ counter: self.chunk_counter,
+ block_len: self.buf_len as u32,
+ flags: self.flags | self.start_flag() | CHUNK_END,
+ }
+ }
+}
+
+impl Drop for ChunkState {
+ fn drop(&mut self) {
+ for b in &mut self.buf {
+ unsafe { write_volatile(b, 0u8) };
+ }
+ for w in &mut self.chaining_value {
+ unsafe { write_volatile(w, 0u32) };
+ }
+ compiler_fence(Ordering::SeqCst);
+ }
+}
+
+//
+// Output
+//
+
+struct Output {
+ input_chaining_value: [u32; 8],
+ block_words: [u32; 16],
+ counter: u64,
+ block_len: u32,
+ flags: u32,
+}
+
+impl Output {
+ fn chaining_value(&self) -> [u32; 8] {
+ first_8_words(compress(
+ &self.input_chaining_value,
+ &self.block_words,
+ self.counter,
+ self.block_len,
+ self.flags,
+ ))
+ }
+
+ fn root_output_bytes(&self, out: &mut [u8]) {
+ let mut counter = 0u64;
+ let mut pos = 0;
+ while pos < out.len() {
+ let words = compress(
+ &self.input_chaining_value,
+ &self.block_words,
+ counter,
+ self.block_len,
+ self.flags | ROOT,
+ );
+ for word in &words {
+ let bytes = word.to_le_bytes();
+ let take = (out.len() - pos).min(4);
+ out[pos..pos + take].copy_from_slice(&bytes[..take]);
+ pos += take;
+ if pos >= out.len() {
+ return;
+ }
+ }
+ counter += 1;
+ }
+ }
+}
+
+//
+// 내부 헬퍼
+//
+
+#[inline(always)]
+fn g3(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize, x: u32, y: u32) {
+ state[a] = state[a].wrapping_add(state[b]).wrapping_add(x);
+ state[d] = (state[d] ^ state[a]).rotate_right(16);
+ state[c] = state[c].wrapping_add(state[d]);
+ state[b] = (state[b] ^ state[c]).rotate_right(12);
+ state[a] = state[a].wrapping_add(state[b]).wrapping_add(y);
+ state[d] = (state[d] ^ state[a]).rotate_right(8);
+ state[c] = state[c].wrapping_add(state[d]);
+ state[b] = (state[b] ^ state[c]).rotate_right(7);
+}
+
+fn round(state: &mut [u32; 16], m: &[u32; 16]) {
+ g3(state, 0, 4, 8, 12, m[0], m[1]);
+ g3(state, 1, 5, 9, 13, m[2], m[3]);
+ g3(state, 2, 6, 10, 14, m[4], m[5]);
+ g3(state, 3, 7, 11, 15, m[6], m[7]);
+ g3(state, 0, 5, 10, 15, m[8], m[9]);
+ g3(state, 1, 6, 11, 12, m[10], m[11]);
+ g3(state, 2, 7, 8, 13, m[12], m[13]);
+ g3(state, 3, 4, 9, 14, m[14], m[15]);
+}
+
+fn compress(cv: &[u32; 8], bw: &[u32; 16], counter: u64, bl: u32, flags: u32) -> [u32; 16] {
+ let mut state = [
+ cv[0],
+ cv[1],
+ cv[2],
+ cv[3],
+ cv[4],
+ cv[5],
+ cv[6],
+ cv[7],
+ IV[0],
+ IV[1],
+ IV[2],
+ IV[3],
+ counter as u32,
+ (counter >> 32) as u32,
+ bl,
+ flags,
+ ];
+ let mut m = *bw;
+ for _ in 0..7 {
+ round(&mut state, &m);
+ let permuted: [u32; 16] = core::array::from_fn(|i| m[MSG_PERMUTATION[i]]);
+ m = permuted;
+ }
+ for i in 0..8 {
+ state[i] ^= state[i + 8];
+ state[i + 8] ^= cv[i];
+ }
+ state
+}
+
+fn parent_output(
+ left_cv: &[u32; 8],
+ right_cv: &[u32; 8],
+ key_words: &[u32; 8],
+ flags: u32,
+) -> Output {
+ let mut block_words = [0u32; 16];
+ block_words[..8].copy_from_slice(left_cv);
+ block_words[8..].copy_from_slice(right_cv);
+ Output {
+ input_chaining_value: *key_words,
+ block_words,
+ counter: 0,
+ block_len: BLOCK_LEN as u32,
+ flags: flags | PARENT,
+ }
+}
+
+fn parent_cv(
+ left_cv: &[u32; 8],
+ right_cv: &[u32; 8],
+ key_words: &[u32; 8],
+ flags: u32,
+) -> [u32; 8] {
+ parent_output(left_cv, right_cv, key_words, flags).chaining_value()
+}
+
+fn first_8_words(x: [u32; 16]) -> [u32; 8] {
+ x[..8].try_into().unwrap()
+}
+
+fn words_from_le_bytes_64(bytes: &[u8; BLOCK_LEN]) -> [u32; 16] {
+ core::array::from_fn(|i| {
+ let s = i * 4;
+ u32::from_le_bytes([bytes[s], bytes[s + 1], bytes[s + 2], bytes[s + 3]])
+ })
+}
+
+fn words_from_le_bytes_32(bytes: &[u8; 32]) -> [u32; 8] {
+ core::array::from_fn(|i| {
+ let s = i * 4;
+ u32::from_le_bytes([bytes[s], bytes[s + 1], bytes[s + 2], bytes[s + 3]])
+ })
+}
diff --git a/crypto/blake/src/file.rs b/crypto/blake/src/file.rs
new file mode 100644
index 0000000..66eeadb
--- /dev/null
+++ b/crypto/blake/src/file.rs
@@ -0,0 +1,83 @@
+use std::fs::File;
+use std::io::{self, Read};
+use std::path::Path;
+
+use entlib_native_base::error::hash::HashError;
+use entlib_native_secure_buffer::SecureBuffer;
+
+use crate::{Blake2b, Blake3};
+
+const BUF_SIZE: usize = 8192;
+
+#[derive(Debug)]
+pub enum FileHashError {
+ Io(io::Error),
+ Hash(HashError),
+}
+
+impl From for FileHashError {
+ fn from(e: io::Error) -> Self {
+ FileHashError::Io(e)
+ }
+}
+
+impl From for FileHashError {
+ fn from(e: HashError) -> Self {
+ FileHashError::Hash(e)
+ }
+}
+
+impl core::fmt::Display for FileHashError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ FileHashError::Io(e) => write!(f, "{}", e),
+ FileHashError::Hash(e) => write!(f, "{}", e),
+ }
+ }
+}
+
+impl std::error::Error for FileHashError {}
+
+pub mod blake2b {
+ use super::*;
+
+ pub fn hash_reader(reader: &mut R, output_len: usize) -> Result {
+ let mut hasher = Blake2b::new(output_len);
+ let mut buf = [0u8; BUF_SIZE];
+ loop {
+ let n = reader.read(&mut buf)?;
+ if n == 0 {
+ break;
+ }
+ hasher.update(&buf[..n]);
+ }
+ Ok(hasher.finalize()?)
+ }
+
+ pub fn hash_file>(path: P, output_len: usize) -> Result {
+ let mut file = File::open(path)?;
+ hash_reader(&mut file, output_len)
+ }
+}
+
+pub mod blake3 {
+ use super::*;
+
+ pub fn hash_reader(reader: &mut R, output_len: usize) -> Result {
+ let mut hasher = Blake3::new();
+ let mut buf = [0u8; BUF_SIZE];
+ loop {
+ let n = reader.read(&mut buf)?;
+ if n == 0 {
+ break;
+ }
+ hasher.update(&buf[..n]);
+ }
+ Ok(hasher.finalize_xof(output_len)?)
+ }
+
+ pub fn hash_file>(path: P, output_len: usize) -> Result {
+ let mut file = File::open(path)?;
+ hash_reader(&mut file, output_len)
+ }
+}
diff --git a/crypto/blake/src/lib.rs b/crypto/blake/src/lib.rs
new file mode 100644
index 0000000..6210977
--- /dev/null
+++ b/crypto/blake/src/lib.rs
@@ -0,0 +1,112 @@
+//! BLAKE2b 및 BLAKE3 암호 해시 함수 모듈입니다.
+//!
+//! BLAKE2b는 RFC 7693을 준수하며, BLAKE3는 공식 명세를 따릅니다.
+//! 민감 데이터는 `SecureBuffer`(mlock)에 보관하며, Drop 시 내부 상태를
+//! `write_volatile`로 강제 소거합니다.
+//!
+//! ---
+//!
+//! `blake2b` 해시는 `blake2`의 변형 중 하나로, 64비트 플랫폼(최신 서버,
+//! PC)에 최적화되어 있으며, 최대 512비트의 다이제스트를 생성합니다. 추 후
+//! 다중 코어를 활용하기 위한 병렬 처리를 지원하는 `blake2bp`, `blake2sp`
+//! 를 지원할 예정입니다.
+//!
+//! `blake3` 해시는 2020년에 발표된 최신 버전으로, 내부적으로 머클
+//! 트리(Merkle Tree) 구조를 채택하여 SIMD 명령어와 다중 스레딩을 통한
+//! 극단적인 병렬 처리가 가능합니다. 이는 `blake2b`보다도 압도적으로 빠르며,
+//! 단일 알고리즘으로 기존의 다양한 변형(다이제스트 크기 변경, 키 파생 등)을
+//! 모두 커버하도록 설계되었습니다.
+//!
+//! # Examples
+//! ```rust,ignore
+//! use entlib_native_blake::{Blake2b, Blake3, blake2b_long};
+//!
+//! // blake2b
+//! let mut h = Blake2b::new(32);
+//! h.update(b"hello world");
+//! let digest = h.finalize().unwrap();
+//! assert_eq!(digest.as_slice().len(), 32);
+//!
+//! // blake3
+//! let mut h = Blake3::new();
+//! h.update(b"hello world");
+//! let digest = h.finalize().unwrap();
+//! assert_eq!(digest.as_slice().len(), 32);
+//!
+//! let out = blake2b_long(b"input", 80).unwrap();
+//! assert_eq!(out.as_slice().len(), 80);
+//! ```
+//!
+//! # Authors
+//! Q. T. Felix
+
+mod blake2b;
+mod blake3;
+pub mod file;
+
+pub use blake2b::Blake2b;
+pub use blake3::{Blake3, OUT_LEN as BLAKE3_OUT_LEN};
+
+use entlib_native_base::error::hash::HashError;
+use entlib_native_secure_buffer::SecureBuffer;
+
+/// RFC 9106 Section 3.2에서 정의된 가변 출력 BLAKE2b 함수입니다 (H').
+///
+/// Argon2id 블록 초기화 및 최종 태그 생성에 사용됩니다.
+///
+/// # Security Note
+/// `out_len > 64`일 때 중간 다이제스트를 체인으로 연결합니다.
+/// 각 단계의 중간값은 SecureBuffer에 보관됩니다.
+///
+/// # Errors
+/// `out_len == 0` 또는 SecureBuffer 할당 실패 시 `Err`.
+pub fn blake2b_long(input: &[u8], out_len: usize) -> Result {
+ if out_len == 0 {
+ return Err(HashError::InvalidOutputLength);
+ }
+
+ let len_prefix = (out_len as u32).to_le_bytes();
+
+ if out_len <= 64 {
+ let mut h = Blake2b::new(out_len);
+ h.update(&len_prefix);
+ h.update(input);
+ return h.finalize();
+ }
+
+ // out_len > 64
+ // r = ceil(out_len/32) - 2 (number of full-64-byte intermediate hashes)
+ // last_len = out_len - 32*r (final hash length, always 33..=64)
+ let r = out_len.div_ceil(32).saturating_sub(2);
+ let last_len = out_len - 32 * r;
+
+ let mut out = SecureBuffer::new_owned(out_len)?;
+ let out_slice = out.as_mut_slice();
+
+ // A_1 = BLAKE2b-64(LE32(out_len) || input)
+ let mut h = Blake2b::new(64);
+ h.update(&len_prefix);
+ h.update(input);
+ let mut prev = h.finalize()?;
+
+ out_slice[..32].copy_from_slice(&prev.as_slice()[..32]);
+ let mut written = 32usize;
+
+ // A_2 .. A_r (r-1 iterations, each 64 bytes, take first 32)
+ for _ in 1..r {
+ let mut h = Blake2b::new(64);
+ h.update(prev.as_slice());
+ let a = h.finalize()?;
+ out_slice[written..written + 32].copy_from_slice(&a.as_slice()[..32]);
+ written += 32;
+ prev = a;
+ }
+
+ // A_{r+1} = BLAKE2b-last_len(A_r), write all last_len bytes
+ let mut h = Blake2b::new(last_len);
+ h.update(prev.as_slice());
+ let a = h.finalize()?;
+ out_slice[written..out_len].copy_from_slice(a.as_slice());
+
+ Ok(out)
+}
diff --git a/crypto/blake/tests/blake2b_test.rs b/crypto/blake/tests/blake2b_test.rs
new file mode 100644
index 0000000..da72a31
--- /dev/null
+++ b/crypto/blake/tests/blake2b_test.rs
@@ -0,0 +1,185 @@
+use entlib_native_blake::{Blake2b, blake2b_long};
+
+//
+// RFC 7693 Appendix A 테스트 벡터
+//
+
+#[test]
+fn blake2b_512_empty() {
+ let h = Blake2b::new(64);
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0x78, 0x6a, 0x02, 0xf7, 0x42, 0x01, 0x59, 0x03, 0xc6, 0xc6, 0xfd, 0x85, 0x25, 0x52, 0xd2,
+ 0x72, 0x91, 0x2f, 0x47, 0x40, 0xe1, 0x58, 0x47, 0x61, 0x8a, 0x86, 0xe2, 0x17, 0xf7, 0x1f,
+ 0x54, 0x19, 0xd2, 0x5e, 0x10, 0x31, 0xaf, 0xee, 0x58, 0x53, 0x13, 0x89, 0x64, 0x44, 0x93,
+ 0x4e, 0xb0, 0x4b, 0x90, 0x3a, 0x68, 0x5b, 0x14, 0x48, 0xb7, 0x55, 0xd5, 0x6f, 0x70, 0x1a,
+ 0xfe, 0x9b, 0xe2, 0xce,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+#[test]
+fn blake2b_512_abc() {
+ let mut h = Blake2b::new(64);
+ h.update(b"abc");
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0xba, 0x80, 0xa5, 0x3f, 0x98, 0x1c, 0x4d, 0x0d, 0x6a, 0x27, 0x97, 0xb6, 0x9f, 0x12, 0xf6,
+ 0xe9, 0x4c, 0x21, 0x2f, 0x14, 0x68, 0x5a, 0xc4, 0xb7, 0x4b, 0x12, 0xbb, 0x6f, 0xdb, 0xff,
+ 0xa2, 0xd1, 0x7d, 0x87, 0xc5, 0x39, 0x2a, 0xab, 0x79, 0x2d, 0xc2, 0x52, 0xd5, 0xde, 0x45,
+ 0x33, 0xcc, 0x95, 0x18, 0xd3, 0x8a, 0xa8, 0xdb, 0xf1, 0x92, 0x5a, 0xb9, 0x23, 0x86, 0xed,
+ 0xd4, 0x00, 0x99, 0x23,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// BLAKE2b-256("abc") — hash_len=32 파라미터 블록 적용
+#[test]
+fn blake2b_256_abc() {
+ let mut h = Blake2b::new(32);
+ h.update(b"abc");
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0xbd, 0xdd, 0x81, 0x3c, 0x63, 0x42, 0x39, 0x72, 0x31, 0x71, 0xef, 0x3f, 0xee, 0x98, 0x57,
+ 0x9b, 0x94, 0x96, 0x4e, 0x3b, 0xb1, 0xcb, 0x3e, 0x42, 0x72, 0x62, 0xc8, 0xc0, 0x68, 0xd5,
+ 0x23, 0x19,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// 멀티-청크: 128바이트 경계를 넘는 입력
+#[test]
+fn blake2b_multi_block() {
+ let input = vec![0x61u8; 200]; // 'a' × 200
+ let mut h1 = Blake2b::new(32);
+ h1.update(&input);
+ let d1 = h1.finalize().unwrap();
+
+ // 동일 입력을 청크로 나눠 공급한 결과와 동일해야 함
+ let mut h2 = Blake2b::new(32);
+ h2.update(&input[..100]);
+ h2.update(&input[100..]);
+ let d2 = h2.finalize().unwrap();
+
+ assert_eq!(d1.as_slice(), d2.as_slice());
+}
+
+// 블록 경계(128바이트)에서의 정확성
+#[test]
+fn blake2b_exact_block_boundary() {
+ let input = vec![0x00u8; 128];
+ let mut h = Blake2b::new(32);
+ h.update(&input);
+ let d = h.finalize().unwrap();
+ assert_eq!(d.as_slice().len(), 32);
+}
+
+// 키드 모드: 동일 키+입력은 동일 출력
+#[test]
+fn blake2b_keyed_deterministic() {
+ let key = vec![0x42u8; 32];
+ let mut h1 = Blake2b::new_keyed(32, &key);
+ h1.update(b"message");
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake2b::new_keyed(32, &key);
+ h2.update(b"message");
+ let d2 = h2.finalize().unwrap();
+
+ assert_eq!(d1.as_slice(), d2.as_slice());
+}
+
+// 키드 모드: 키가 다르면 출력이 달라야 함
+#[test]
+fn blake2b_keyed_different_keys() {
+ let key1 = vec![0x01u8; 32];
+ let key2 = vec![0x02u8; 32];
+
+ let mut h1 = Blake2b::new_keyed(32, &key1);
+ h1.update(b"message");
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake2b::new_keyed(32, &key2);
+ h2.update(b"message");
+ let d2 = h2.finalize().unwrap();
+
+ assert_ne!(d1.as_slice(), d2.as_slice());
+}
+
+// 키드 모드와 일반 모드의 출력이 달라야 함
+#[test]
+fn blake2b_keyed_differs_from_unkeyed() {
+ let key = vec![0x01u8; 32];
+
+ let mut h1 = Blake2b::new(32);
+ h1.update(b"message");
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake2b::new_keyed(32, &key);
+ h2.update(b"message");
+ let d2 = h2.finalize().unwrap();
+
+ assert_ne!(d1.as_slice(), d2.as_slice());
+}
+
+// 출력 길이 변경은 출력 값을 변경해야 함
+#[test]
+fn blake2b_different_hash_lengths() {
+ let mut h32 = Blake2b::new(32);
+ h32.update(b"test");
+ let d32 = h32.finalize().unwrap();
+
+ let mut h64 = Blake2b::new(64);
+ h64.update(b"test");
+ let d64 = h64.finalize().unwrap();
+
+ assert_ne!(d32.as_slice(), &d64.as_slice()[..32]);
+}
+
+// blake2b_long: 출력 길이 검증
+#[test]
+fn blake2b_long_lengths() {
+ for len in [1usize, 32, 64, 65, 128, 256, 1024] {
+ let out = blake2b_long(b"input", len).unwrap();
+ assert_eq!(out.as_slice().len(), len);
+ }
+}
+
+// blake2b_long: ≤64 바이트 경로는 단일 Blake2b와 일치해야 함
+#[test]
+fn blake2b_long_short_matches_direct() {
+ let input = b"test input";
+ for len in [1usize, 16, 32, 64] {
+ let long_out = blake2b_long(input, len).unwrap();
+
+ let len_prefix = (len as u32).to_le_bytes();
+ let mut h = Blake2b::new(len);
+ h.update(&len_prefix);
+ h.update(input);
+ let direct = h.finalize().unwrap();
+
+ assert_eq!(long_out.as_slice(), direct.as_slice(), "len={len}");
+ }
+}
+
+// blake2b_long: 동일 입력·길이는 항상 동일 출력
+#[test]
+fn blake2b_long_deterministic() {
+ let d1 = blake2b_long(b"hello", 80).unwrap();
+ let d2 = blake2b_long(b"hello", 80).unwrap();
+ assert_eq!(d1.as_slice(), d2.as_slice());
+}
+
+// blake2b_long: 입력이 다르면 출력이 달라야 함
+#[test]
+fn blake2b_long_different_inputs() {
+ let d1 = blake2b_long(b"input1", 64).unwrap();
+ let d2 = blake2b_long(b"input2", 64).unwrap();
+ assert_ne!(d1.as_slice(), d2.as_slice());
+}
+
+// blake2b_long: out_len=0 은 오류를 반환해야 함
+#[test]
+fn blake2b_long_zero_len_rejected() {
+ assert!(blake2b_long(b"input", 0).is_err());
+}
diff --git a/crypto/blake/tests/blake3_test.rs b/crypto/blake/tests/blake3_test.rs
new file mode 100644
index 0000000..daaf504
--- /dev/null
+++ b/crypto/blake/tests/blake3_test.rs
@@ -0,0 +1,228 @@
+use entlib_native_blake::{BLAKE3_OUT_LEN, Blake3};
+
+//
+// BLAKE3 공식 테스트 벡터 (https://github.com/BLAKE3-team/BLAKE3/blob/master/test_vectors/test_vectors.json)
+// 입력: 0x00, 0x01, ..., 0xFA (연속 바이트)
+//
+
+fn make_input(len: usize) -> Vec {
+ (0..len).map(|i| (i % 251) as u8).collect()
+}
+
+// 공식 벡터: 입력 0바이트
+#[test]
+fn blake3_empty() {
+ let h = Blake3::new();
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0xaf, 0x13, 0x49, 0xb9, 0xf5, 0xf9, 0xa1, 0xa6, 0xa0, 0x40, 0x4d, 0xea, 0x36, 0xdc, 0xc9,
+ 0x49, 0x9b, 0xcb, 0x25, 0xc9, 0xad, 0xc1, 0x12, 0xb7, 0xcc, 0x9a, 0x93, 0xca, 0xe4, 0x1f,
+ 0x32, 0x62,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// 공식 벡터: 입력 1바이트 (0x00)
+#[test]
+fn blake3_one_byte() {
+ let mut h = Blake3::new();
+ h.update(&[0x00]);
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0x2d, 0x3a, 0xde, 0xdf, 0xf1, 0x1b, 0x61, 0xf1, 0x4c, 0x88, 0x6e, 0x35, 0xaf, 0xa0, 0x36,
+ 0x73, 0x6d, 0xcd, 0x87, 0xa7, 0x4d, 0x27, 0xb5, 0xc1, 0x51, 0x02, 0x25, 0xd0, 0xf5, 0x92,
+ 0xe2, 0x13,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// 공식 벡터: 입력 1023바이트 (단일 청크 경계 직전)
+#[test]
+fn blake3_1023_bytes() {
+ let input = make_input(1023);
+ let mut h = Blake3::new();
+ h.update(&input);
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0x10, 0x10, 0x89, 0x70, 0xee, 0xda, 0x3e, 0xb9, 0x32, 0xba, 0xac, 0x14, 0x28, 0xc7, 0xa2,
+ 0x16, 0x3b, 0x0e, 0x92, 0x4c, 0x9a, 0x9e, 0x25, 0xb3, 0x5b, 0xba, 0x72, 0xb2, 0x8f, 0x70,
+ 0xbd, 0x11,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// 공식 벡터: 입력 1024바이트 (정확히 1청크)
+#[test]
+fn blake3_1024_bytes() {
+ let input = make_input(1024);
+ let mut h = Blake3::new();
+ h.update(&input);
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0x42, 0x21, 0x47, 0x39, 0xf0, 0x95, 0xa4, 0x06, 0xf3, 0xfc, 0x83, 0xde, 0xb8, 0x89, 0x74,
+ 0x4a, 0xc0, 0x0d, 0xf8, 0x31, 0xc1, 0x0d, 0xaa, 0x55, 0x18, 0x9b, 0x5d, 0x12, 0x1c, 0x85,
+ 0x5a, 0xf7,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// 공식 벡터: 입력 1025바이트 (청크 경계 직후, 트리 시작)
+#[test]
+fn blake3_1025_bytes() {
+ let input = make_input(1025);
+ let mut h = Blake3::new();
+ h.update(&input);
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0xd0, 0x02, 0x78, 0xae, 0x47, 0xeb, 0x27, 0xb3, 0x4f, 0xae, 0xcf, 0x67, 0xb4, 0xfe, 0x26,
+ 0x3f, 0x82, 0xd5, 0x41, 0x29, 0x16, 0xc1, 0xff, 0xd9, 0x7c, 0x8c, 0xb7, 0xfb, 0x81, 0x4b,
+ 0x84, 0x44,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// 공식 벡터: 입력 2048바이트 (정확히 2청크)
+#[test]
+fn blake3_2048_bytes() {
+ let input = make_input(2048);
+ let mut h = Blake3::new();
+ h.update(&input);
+ let d = h.finalize().unwrap();
+ let expected = [
+ 0xe7, 0x76, 0xb6, 0x02, 0x8c, 0x7c, 0xd2, 0x2a, 0x4d, 0x0b, 0xa1, 0x82, 0xa8, 0xbf, 0x62,
+ 0x20, 0x5d, 0x2e, 0xf5, 0x76, 0x46, 0x7e, 0x83, 0x8e, 0xd6, 0xf2, 0x52, 0x9b, 0x85, 0xfb,
+ 0xa2, 0x4a,
+ ];
+ assert_eq!(d.as_slice(), &expected);
+}
+
+// 결정론성: 동일 입력은 동일 출력
+#[test]
+fn blake3_deterministic() {
+ let input = make_input(500);
+ let mut h1 = Blake3::new();
+ h1.update(&input);
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake3::new();
+ h2.update(&input);
+ let d2 = h2.finalize().unwrap();
+
+ assert_eq!(d1.as_slice(), d2.as_slice());
+}
+
+// 스트리밍 업데이트: 분할 공급과 일괄 공급 결과 일치
+#[test]
+fn blake3_streaming_matches_single() {
+ let input = make_input(3000);
+
+ let mut h1 = Blake3::new();
+ h1.update(&input);
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake3::new();
+ h2.update(&input[..1000]);
+ h2.update(&input[1000..2000]);
+ h2.update(&input[2000..]);
+ let d2 = h2.finalize().unwrap();
+
+ assert_eq!(d1.as_slice(), d2.as_slice());
+}
+
+// 입력이 다르면 출력이 달라야 함
+#[test]
+fn blake3_different_inputs() {
+ let mut h1 = Blake3::new();
+ h1.update(b"input one");
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake3::new();
+ h2.update(b"input two");
+ let d2 = h2.finalize().unwrap();
+
+ assert_ne!(d1.as_slice(), d2.as_slice());
+}
+
+// 출력 길이
+#[test]
+fn blake3_output_length() {
+ let h = Blake3::new();
+ let d = h.finalize().unwrap();
+ assert_eq!(d.as_slice().len(), BLAKE3_OUT_LEN);
+ assert_eq!(BLAKE3_OUT_LEN, 32);
+}
+
+// XOF: 임의 길이 출력
+#[test]
+fn blake3_xof_lengths() {
+ for len in [1usize, 32, 64, 128, 256, 1000] {
+ let mut h = Blake3::new();
+ h.update(b"xof test");
+ let d = h.finalize_xof(len).unwrap();
+ assert_eq!(d.as_slice().len(), len);
+ }
+}
+
+// XOF: 출력 앞 32바이트는 finalize()와 일치해야 함
+#[test]
+fn blake3_xof_prefix_matches_finalize() {
+ let input = make_input(512);
+
+ let mut h1 = Blake3::new();
+ h1.update(&input);
+ let d32 = h1.finalize().unwrap();
+
+ let mut h2 = Blake3::new();
+ h2.update(&input);
+ let d64 = h2.finalize_xof(64).unwrap();
+
+ assert_eq!(d32.as_slice(), &d64.as_slice()[..32]);
+}
+
+// 키드 모드: 결정론성
+#[test]
+fn blake3_keyed_deterministic() {
+ let key = [0x42u8; 32];
+ let mut h1 = Blake3::new_keyed(&key);
+ h1.update(b"message");
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake3::new_keyed(&key);
+ h2.update(b"message");
+ let d2 = h2.finalize().unwrap();
+
+ assert_eq!(d1.as_slice(), d2.as_slice());
+}
+
+// 키드 모드와 일반 모드의 출력이 달라야 함
+#[test]
+fn blake3_keyed_differs_from_unkeyed() {
+ let key = [0x01u8; 32];
+
+ let mut h1 = Blake3::new();
+ h1.update(b"message");
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake3::new_keyed(&key);
+ h2.update(b"message");
+ let d2 = h2.finalize().unwrap();
+
+ assert_ne!(d1.as_slice(), d2.as_slice());
+}
+
+// 키가 다르면 출력이 달라야 함
+#[test]
+fn blake3_different_keys() {
+ let key1 = [0x01u8; 32];
+ let key2 = [0x02u8; 32];
+
+ let mut h1 = Blake3::new_keyed(&key1);
+ h1.update(b"message");
+ let d1 = h1.finalize().unwrap();
+
+ let mut h2 = Blake3::new_keyed(&key2);
+ h2.update(b"message");
+ let d2 = h2.finalize().unwrap();
+
+ assert_ne!(d1.as_slice(), d2.as_slice());
+}
diff --git a/crypto/hmac/Cargo.toml b/crypto/hmac/Cargo.toml
index dc28562..b854bfd 100644
--- a/crypto/hmac/Cargo.toml
+++ b/crypto/hmac/Cargo.toml
@@ -10,6 +10,7 @@ entlib-native-secure-buffer.workspace = true
entlib-native-constant-time.workspace = true
entlib-native-sha2.workspace = true
entlib-native-sha3.workspace = true
+entlib-native-base.workspace = true
[dev-dependencies]
entlib-native-hex.workspace = true
\ No newline at end of file
diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs
index 055ea6a..8056ca0 100644
--- a/crypto/hmac/src/lib.rs
+++ b/crypto/hmac/src/lib.rs
@@ -9,13 +9,16 @@ pub use hmac::{
HMACSHA512, MacResult,
};
+use entlib_native_base::error::hash::HashError;
+use entlib_native_base::error::secure_buffer::SecureBufferError;
+
/// HMAC 연산 중 발생할 수 있는 보안 오류
#[derive(Debug)]
pub enum HmacError {
/// NIST SP 800-107r1에 따른 최소 키 길이(112 bits / 14 bytes) 미달
WeakKeyLength,
/// 내부 해시 연산 중 발생한 오류
- HashComputationError(&'static str),
+ HashComputationError(HashError),
/// MAC 결과를 저장하기 위한 SecureBuffer 할당 실패
- AllocationError(&'static str),
+ AllocationError(SecureBufferError),
}
diff --git a/crypto/mldsa/Cargo.toml b/crypto/mldsa/Cargo.toml
new file mode 100644
index 0000000..9052a76
--- /dev/null
+++ b/crypto/mldsa/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "entlib-native-mldsa"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+entlib-native-constant-time.workspace = true
+entlib-native-rng.workspace = true
+entlib-native-secure-buffer.workspace = true
+entlib-native-sha3.workspace = true
+entlib-native-base.workspace = true
\ No newline at end of file
diff --git a/crypto/mldsa/README.md b/crypto/mldsa/README.md
new file mode 100644
index 0000000..ac22108
--- /dev/null
+++ b/crypto/mldsa/README.md
@@ -0,0 +1,151 @@
+# ML-DSA 크레이트 (entlib-native-mldsa)
+
+> Q. T. Felix (수정: 26.03.24 UTC+9)
+>
+> [English README](README_EN.md)
+
+`entlib-native-mldsa`는 NIST FIPS 204에 규정된 모듈 격자 기반 전자 서명 알고리즘(Module Lattice-based Digital Signature Algorithm, ML-DSA)의 순수 Rust 구현체입니다. 본 크레이트는 세 가지 파라미터 셋(ML-DSA-44/65/87)을 지원하며, 비밀 키 메모리 보호, 헤지드 서명, 상수-시간 필드 연산을 통해 부채널 공격을 방어합니다.
+
+## 보안 위협 모델
+
+RSA 및 ECDSA와 같은 기존 전자 서명 알고리즘은 Shor 알고리즘을 구현한 양자 컴퓨터에 의해 다항식 시간 내 파훼됩니다. ML-DSA는 모듈 격자 위의 Learning With Errors(LWE) 문제와 Short Integer Solution(SIS) 문제의 계산적 난해성에 안전성을 근거하며, 현재 알려진 양자 알고리즘으로도 지수 시간이 소요됩니다.
+
+구현 수준의 공격 표면은 세 가지입니다. 첫째, 비밀 키 메모리 노출: `s1`, `s2`, `t0`, `K_seed`, `tr` 등 비밀 성분이 스왑 파일이나 코어 덤프에 유출될 수 있습니다. 이를 `SecureBuffer`(OS `mlock` + Drop 시 자동 소거)로 방어합니다. 둘째, 서명 시 타이밍 부채널: 비밀 성분에 의존하는 분기가 서명 키를 노출할 수 있습니다. 유한체 연산(`Fq::add`, `Fq::sub`, `power2round` 등)은 `entlib-native-constant-time`의 상수-시간 선택 연산으로 구현됩니다. 셋째, nonce 재사용: 동일한 `rnd`로 두 개의 서명을 생성하면 비밀 키가 복원됩니다. 헤지드(Hedged) 서명 모드(`rnd ← RNG`)로 이를 완전히 방지합니다.
+
+## 파라미터 셋
+
+NIST FIPS 204 Section 4에 정의된 세 가지 파라미터 셋을 지원합니다.
+
+| 파라미터 셋 | NIST 보안 카테고리 | pk 크기 | sk 크기 | 서명 크기 | λ (충돌 강도) |
+|-----------|:--------------:|-------:|-------:|-------:|:---------:|
+| ML-DSA-44 | 2 (AES-128 동급) | 1312 B | 2560 B | 2420 B | 128-bit |
+| ML-DSA-65 | 3 (AES-192 동급) | 1952 B | 4032 B | 3309 B | 192-bit |
+| ML-DSA-87 | 5 (AES-256 동급) | 2592 B | 4896 B | 4627 B | 256-bit |
+
+각 파라미터 셋은 행렬 차원 $(k, l)$, 비밀 계수 범위 $\eta$, 챌린지 다항식 가중치 $\tau$, 마스킹 범위 $\gamma_1$, 분해 범위 $\gamma_2$, 힌트 최대 가중치 $\omega$를 달리합니다. 컴파일 타임 const 제네릭으로 단형화(monomorphization)되어 런타임 오버헤드가 없습니다.
+
+## 공개 API
+
+### `MLDSA` 구조체: Algorithm 1–3
+
+`MLDSA`는 정적 메소드만 제공하는 진입점입니다. 파라미터 셋 정보는 키 타입에 내장되므로 서명·검증 시 별도로 지정하지 않습니다.
+
+```rust
+// Algorithm 1: ML-DSA.KeyGen
+let mut rng = HashDRBGRng::new_from_os(None).unwrap();
+let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).unwrap();
+
+// Algorithm 2: ML-DSA.Sign (헤지드 — rnd ← RNG)
+let sig = MLDSA::sign(&sk, message, ctx, &mut rng).unwrap();
+
+// Algorithm 3: ML-DSA.Verify
+let ok = MLDSA::verify(&pk, message, &sig, ctx).unwrap();
+assert!(ok);
+```
+
+**메시지 전처리**: 외부 인터페이스는 FIPS 204 Section 5.2에 따라 $M' = \texttt{0x00} \| \text{IntegerToBytes}(|ctx|, 1) \| ctx \| M$ 을 구성하여 내부 알고리즘에 전달합니다. `ctx.len() > 255` 이면 `ContextTooLong`을 반환합니다.
+
+**헤지드 서명**: `sign`은 32바이트 `rnd`를 RNG에서 생성하여 내부 알고리즘에 전달합니다. `rnd`가 공개되더라도 결정론적 서명이 되지 않으므로 nonce 재사용 공격이 불가능합니다.
+
+### `MLDSAParameter` 열거형
+
+```rust
+pub enum MLDSAParameter { MLDSA44, MLDSA65, MLDSA87 }
+```
+
+`pk_len()`, `sk_len()`, `sig_len()`은 `const fn`으로 제공됩니다.
+
+## 키 타입
+
+### `MLDSAPublicKey`
+
+인코딩된 공개 키 바이트($`\rho \| \text{SimpleBitPack}(t_1, 10)`$)와 파라미터 셋을 보유합니다. `from_bytes`로 외부 바이트열에서 복원할 수 있으며, 길이 불일치 시 `InvalidLength`를 반환합니다.
+
+> [!NOTE]
+> **pkEncode 레이아웃**: $\rho$ (32 B) $\|$ SimpleBitPack$`(t_1[0], 10)`$ $\|$ $\cdots$ $\|$ SimpleBitPack$`(t_1[k-1], 10)`$
+>
+> $t_1$ 계수는 10비트씩 패킹되어 다항식당 320 B, 전체 $32 + 320k$ B입니다.
+
+### `MLDSAPrivateKey`
+
+비밀 키 바이트를 `SecureBuffer`(OS `mlock`)에 보관합니다. `Drop` 시 메모리가 즉시 소거(Zeroize)됩니다. `as_bytes()`로 슬라이스를 참조할 수 있으나, 파일 저장 시 반드시 PKCS#8 암호화를 적용해야 합니다.
+
+> [!NOTE]
+> **skEncode 레이아웃**: $\rho$ (32 B) $\|$ $K_{\text{seed}}$ (32 B) $\|$ $tr$ (64 B) $\|$ BitPack$`(s_1, \eta, \eta)`$ $\|$ BitPack$`(s_2, \eta, \eta)`$ $\|$ BitPack$`(t_0, 4095, 4096)`$
+>
+> $\eta = 2$이면 계수당 3비트, $\eta = 4$이면 4비트로 인코딩됩니다.
+
+## RNG 추상화
+
+### `MLDSARng` 트레이트
+
+```rust
+pub trait MLDSARng {
+ fn fill_random(&mut self, dest: &mut [u8]) -> Result<(), MLDSAError>;
+}
+```
+
+구현체는 NIST SP 800-90A Rev.1 이상의 보안 강도(≥ 256-bit)를 제공하는 DRBG여야 합니다.
+
+### `HashDRBGRng`
+
+NIST Hash_DRBG (SHA-512 기반, Security Strength 256-bit) 래퍼입니다. `new_from_os`가 유일한 초기화 경로이며, OS 엔트로피 소스(`getrandom`/`getentropy`)만 허용됩니다. 내부 상태 V, C는 `SecureBuffer`에 보관되어 Drop 시 자동 소거됩니다. `MLDSAError::RngError(ReseedRequired)` 수신 시 `reseed()`를 호출해야 합니다.
+
+### `CtrDRBGRng`
+
+AES-256-CTR 기반 CTR_DRBG 예약 구조체입니다. `entlib-native-aes` 완료 전까지 모든 메소드가 `NotImplemented`를 반환합니다.
+
+## 내부 알고리즘 구조
+
+### 키 생성 (Algorithm 4/6)
+
+$\xi \in \mathbb{B}^{32}$ 시드로부터 SHAKE256 확장으로 $(\rho, \rho', K_{\text{seed}})$를 유도합니다.
+
+$$A_{\hat{}} \leftarrow \text{ExpandA}(\rho), \quad (s_1, s_2) \leftarrow \text{ExpandS}(\rho')$$
+
+$$t = \text{INTT}(A_{\hat{}} \circ \text{NTT}(s_1)) + s_2, \quad (t_1, t_0) \leftarrow \text{Power2Round}(t, d)$$
+
+Power2Round는 $a_1 = \lceil a / 2^{13} \rceil$, $a_0 = a - a_1 \cdot 2^{13}$ 으로 분할하며, 음수 $a_0$의 $\mathbb{Z}_q$ 표현 변환에 `ct_is_negative` + `ct_select`를 사용합니다.
+
+트레이스 $tr = H(\text{pkEncode}(\rho, t_1), 64)$는 SHAKE256 증분 해싱으로 계산됩니다.
+
+### 서명 (Algorithm 5)
+
+거절 샘플링 기반 반복 루프입니다. 각 시도에서:
+
+$$y \leftarrow \text{ExpandMask}(\rho'', \kappa), \quad w = \text{INTT}(A_{\hat{}} \circ \text{NTT}(y))$$
+
+$$w_1 = \text{HighBits}(w, 2\gamma_2), \quad \tilde{c} \leftarrow H(\mu \| w_1, \lambda/4)$$
+
+$$z = y + c \cdot s_1$$
+
+$`\|z\|_\infty \ge \gamma_1 - \beta`$ 이거나 $`\|\text{LowBits}(w - c \cdot s_2, 2\gamma_2)\|_\infty \ge \gamma_2 - \beta`$ 이면 거절하고 재시도합니다. 힌트 $h = \text{MakeHint}(-c \cdot t_0, w - c \cdot s_2 + c \cdot t_0, 2\gamma_2)$를 생성하고, $\|h\|_1 \le \omega$를 확인합니다. 최대 시도 횟수 초과 시 `SigningFailed`를 반환합니다.
+
+### 검증 (Algorithm 7)
+
+$\|z\|_\infty \ge \gamma_1 - \beta$ 또는 $\|h\|_1 > \omega$ 이면 즉시 `false`를 반환합니다.
+
+$$w_1' = \text{UseHint}(h, \text{INTT}(A_{\hat{}} \circ \text{NTT}(z)) - c \cdot t_1 \cdot 2^d, 2\gamma_2)$$
+
+$\tilde{c}$와 재구성된 $H(\mu \| w_1', \lambda/4)$를 비교합니다.
+
+> [!NOTE]
+> **상수-시간 챌린지 비교**: 서명 유효성을 결정하는 챌린지 해시 비교(`c_tilde` ↔ 재계산값)는 비밀 데이터가 아니므로 표준 바이트 비교를 사용합니다. 노름 검사(`fq_to_signed`)도 서명 재시도 여부를 결정하는 공개 데이터이므로 동일하게 타이밍-가변 경로를 허용합니다.
+
+### NTT / 유한체 연산
+
+다항식 환 $R_q = \mathbb{Z}_q[X]/(X^{256}+1)$, $q = 8{,}380{,}417$ 위에서 동작합니다. NTT는 비트 반전 순서의 몽고메리 도메인 원시 단위근 배열(`ZETAS[256]`)을 사용합니다. 몽고메리 환원 상수는 $q^{-1} \bmod 2^{32} = 58{,}728{,}449$이며, INTT 정규화 상수는 $N^{-1} \cdot R^2 \bmod q = 41{,}978$입니다.
+
+`Fq::add`, `Fq::sub`는 분기 없는 상수-시간 구현(`ct_is_negative` + `ct_select`)을 사용합니다.
+
+## 오류 타입
+
+| 오류 | 의미 |
+|------|------|
+| `InvalidLength` | 키 또는 서명 바이트 길이 불일치 |
+| `InternalError` | 해시 함수 오류, 메모리 할당 실패 |
+| `RngError` | RNG 내부 오류 또는 reseed 필요 |
+| `ContextTooLong` | ctx가 255바이트 초과 |
+| `SigningFailed` | 거절 샘플링 최대 반복 초과 (극히 희박) |
+| `InvalidSignature` | 서명 검증 실패 |
+| `NotImplemented` | CTR_DRBG 등 미구현 기능 |
diff --git a/crypto/mldsa/README_EN.md b/crypto/mldsa/README_EN.md
new file mode 100644
index 0000000..8beff9e
--- /dev/null
+++ b/crypto/mldsa/README_EN.md
@@ -0,0 +1,151 @@
+# ML-DSA Crate (entlib-native-mldsa)
+
+> Q. T. Felix (revised: 26.03.24 UTC+9)
+>
+> [Korean README](README.md)
+
+`entlib-native-mldsa` is a pure Rust implementation of the Module Lattice-based Digital Signature Algorithm (ML-DSA) as specified in NIST FIPS 204. This crate supports three parameter sets (ML-DSA-44/65/87) and defends against side-channel attacks through private key memory protection, hedged signing, and constant-time field arithmetic.
+
+## Security Threat Model
+
+Classical signature algorithms such as RSA and ECDSA are broken in polynomial time by a quantum computer running Shor's algorithm. ML-DSA grounds its security in the computational hardness of the Learning With Errors (LWE) problem and the Short Integer Solution (SIS) problem over module lattices; no known quantum algorithm reduces these below exponential time.
+
+Three implementation-level attack surfaces are addressed. First, private key memory exposure: secret components such as `s1`, `s2`, `t0`, `K_seed`, and `tr` may leak via swap files or core dumps. This is mitigated by `SecureBuffer` (OS `mlock` + automatic zeroization on `Drop`). Second, timing side-channels during signing: branches dependent on secret components can expose the signing key. Finite field operations (`Fq::add`, `Fq::sub`, `power2round`, etc.) are implemented with constant-time select primitives from `entlib-native-constant-time`. Third, nonce reuse: generating two signatures with the same `rnd` allows full key recovery. Hedged signing mode (`rnd ← RNG`) eliminates this entirely.
+
+## Parameter Sets
+
+Three parameter sets defined in NIST FIPS 204 Section 4 are supported.
+
+| Parameter Set | NIST Security Category | pk size | sk size | sig size | λ (collision strength) |
+|---------------|:-----------------------:|--------:|--------:|---------:|:----------------------:|
+| ML-DSA-44 | 2 (≈ AES-128) | 1312 B | 2560 B | 2420 B | 128-bit |
+| ML-DSA-65 | 3 (≈ AES-192) | 1952 B | 4032 B | 3309 B | 192-bit |
+| ML-DSA87 | 5 (≈ AES-256) | 2592 B | 4896 B | 4627 B | 256-bit |
+
+Each parameter set differs in matrix dimensions $(k, l)$, secret coefficient range $\eta$, challenge polynomial weight $\tau$, masking range $\gamma_1$, decomposition range $\gamma_2$, and maximum hint weight $\omega$. Compile-time const generics monomorphize each variant with zero runtime overhead.
+
+## Public API
+
+### `MLDSA` Struct: Algorithms 1–3
+
+`MLDSA` is the top-level entry point exposing only static methods. The parameter set is embedded in the key types, so it need not be specified separately at sign or verify time.
+
+```rust
+// Algorithm 1: ML-DSA.KeyGen
+let mut rng = HashDRBGRng::new_from_os(None).unwrap();
+let (pk, sk) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).unwrap();
+
+// Algorithm 2: ML-DSA.Sign (hedged — rnd ← RNG)
+let sig = MLDSA::sign(&sk, message, ctx, &mut rng).unwrap();
+
+// Algorithm 3: ML-DSA.Verify
+let ok = MLDSA::verify(&pk, message, &sig, ctx).unwrap();
+assert!(ok);
+```
+
+**Message preprocessing**: The external interface constructs $M' = \texttt{0x00} \| \text{IntegerToBytes}(|ctx|, 1) \| ctx \| M$ per FIPS 204 Section 5.2 before passing it to internal algorithms. Returns `ContextTooLong` if `ctx.len() > 255`.
+
+**Hedged signing**: `sign` draws 32 bytes of `rnd` from the RNG and passes them to the internal algorithm. Even if `rnd` is disclosed, the signing is not deterministic, making nonce-reuse attacks impossible.
+
+### `MLDSAParameter` Enum
+
+```rust
+pub enum MLDSAParameter { MLDSA44, MLDSA65, MLDSA87 }
+```
+
+`pk_len()`, `sk_len()`, and `sig_len()` are provided as `const fn`.
+
+## Key Types
+
+### `MLDSAPublicKey`
+
+Holds the encoded public key bytes ($\rho \| \text{SimpleBitPack}(t_1, 10)$) together with the parameter set. Can be reconstructed from an external byte slice via `from_bytes`; returns `InvalidLength` on a size mismatch.
+
+> [!NOTE]
+> **pkEncode layout**: $\rho$ (32 B) $\|$ SimpleBitPack$(t_1[0], 10)$ $\|$ $\cdots$ $\|$ SimpleBitPack$(t_1[k-1], 10)$
+>
+> Coefficients of $t_1$ are packed at 10 bits each, yielding 320 B per polynomial and $32 + 320k$ B in total.
+
+### `MLDSAPrivateKey`
+
+Stores the encoded private key bytes in a `SecureBuffer` (OS `mlock`). Memory is immediately zeroized on `Drop`. The slice is accessible via `as_bytes()`, but PKCS#8 encryption must be applied when persisting to disk.
+
+> [!NOTE]
+> **skEncode layout**: $\rho$ (32 B) $\|$ $K_{\text{seed}}$ (32 B) $\|$ $tr$ (64 B) $\|$ BitPack$(s_1, \eta, \eta)$ $\|$ BitPack$(s_2, \eta, \eta)$ $\|$ BitPack$(t_0, 4095, 4096)$
+>
+> $\eta = 2$ encodes 3 bits per coefficient; $\eta = 4$ encodes 4 bits per coefficient.
+
+## RNG Abstraction
+
+### `MLDSARng` Trait
+
+```rust
+pub trait MLDSARng {
+ fn fill_random(&mut self, dest: &mut [u8]) -> Result<(), MLDSAError>;
+}
+```
+
+Implementors must provide a DRBG with security strength ≥ 256-bit per NIST SP 800-90A Rev.1 or later.
+
+### `HashDRBGRng`
+
+Wrapper around NIST Hash_DRBG (SHA-512, Security Strength 256-bit). `new_from_os` is the only initialization path and accepts only OS entropy (`getrandom`/`getentropy`). Internal state V and C are held in `SecureBuffer` and zeroized on `Drop`. Call `reseed()` upon receiving `MLDSAError::RngError(ReseedRequired)`.
+
+### `CtrDRBGRng`
+
+Reserved struct for NIST CTR_DRBG (AES-256-CTR). All methods return `NotImplemented` until `entlib-native-aes` is complete.
+
+## Internal Algorithm Structure
+
+### Key Generation (Algorithm 4/6)
+
+The seed $\xi \in \mathbb{B}^{32}$ is expanded via SHAKE256 to derive $(\rho, \rho', K_{\text{seed}})$.
+
+$$A_{\hat{}} \leftarrow \text{ExpandA}(\rho), \quad (s_1, s_2) \leftarrow \text{ExpandS}(\rho')$$
+
+$$t = \text{INTT}(A_{\hat{}} \circ \text{NTT}(s_1)) + s_2, \quad (t_1, t_0) \leftarrow \text{Power2Round}(t, d)$$
+
+Power2Round splits coefficients as $a_1 = \lceil a / 2^{13} \rceil$, $a_0 = a - a_1 \cdot 2^{13}$. Converting negative $a_0$ to its $\mathbb{Z}_q$ representation uses `ct_is_negative` + `ct_select`.
+
+The trace $tr = H(\text{pkEncode}(\rho, t_1), 64)$ is computed via incremental SHAKE256 hashing.
+
+### Signing (Algorithm 5)
+
+A rejection-sampling loop. On each attempt:
+
+$$y \leftarrow \text{ExpandMask}(\rho'', \kappa), \quad w = \text{INTT}(A_{\hat{}} \circ \text{NTT}(y))$$
+
+$$w_1 = \text{HighBits}(w, 2\gamma_2), \quad \tilde{c} \leftarrow H(\mu \| w_1, \lambda/4)$$
+
+$$z = y + c \cdot s_1$$
+
+The attempt is rejected and retried if $\|z\|_\infty \ge \gamma_1 - \beta$ or $\|\text{LowBits}(w - c \cdot s_2, 2\gamma_2)\|_\infty \ge \gamma_2 - \beta$. The hint $h = \text{MakeHint}(-c \cdot t_0,\, w - c \cdot s_2 + c \cdot t_0,\, 2\gamma_2)$ is produced and $\|h\|_1 \le \omega$ is checked. Exceeding the maximum attempt count returns `SigningFailed`.
+
+### Verification (Algorithm 7)
+
+Returns `false` immediately if $\|z\|_\infty \ge \gamma_1 - \beta$ or $\|h\|_1 > \omega$.
+
+$$w_1' = \text{UseHint}(h,\; \text{INTT}(A_{\hat{}} \circ \text{NTT}(z)) - c \cdot t_1 \cdot 2^d,\; 2\gamma_2)$$
+
+Compares $\tilde{c}$ against the recomputed $H(\mu \| w_1', \lambda/4)$.
+
+> [!NOTE]
+> **Constant-time challenge comparison**: The challenge hash comparison (`c_tilde` ↔ recomputed value) that determines signature validity involves no secret data, so a standard byte comparison is used. The norm check (`fq_to_signed`) likewise operates on public data (controlling retry decisions), so a timing-variable path is acceptable there.
+
+### NTT / Finite Field Arithmetic
+
+Operates over the polynomial ring $R_q = \mathbb{Z}_q[X]/(X^{256}+1)$, $q = 8{,}380{,}417$. The NTT uses a bit-reversed array of Montgomery-domain primitive roots of unity (`ZETAS[256]`). The Montgomery reduction constant is $q^{-1} \bmod 2^{32} = 58{,}728{,}449$; the INTT normalization constant is $N^{-1} \cdot R^2 \bmod q = 41{,}978$.
+
+`Fq::add` and `Fq::sub` are branch-free constant-time implementations using `ct_is_negative` + `ct_select`.
+
+## Error Types
+
+| Error | Meaning |
+|--------------------|------------------------------------------------------|
+| `InvalidLength` | Key or signature byte length mismatch |
+| `InternalError` | Hash function error or memory allocation failure |
+| `RngError` | RNG internal error or reseed required |
+| `ContextTooLong` | `ctx` exceeds 255 bytes |
+| `SigningFailed` | Rejection sampling exceeded maximum attempts (rare) |
+| `InvalidSignature` | Signature verification failure |
+| `NotImplemented` | Unimplemented feature (e.g., CTR_DRBG) |
diff --git a/crypto/mldsa/src/_mldsa_test.rs b/crypto/mldsa/src/_mldsa_test.rs
new file mode 100644
index 0000000..b3391a6
--- /dev/null
+++ b/crypto/mldsa/src/_mldsa_test.rs
@@ -0,0 +1,276 @@
+#[cfg(test)]
+mod tests {
+ use crate::mldsa::{MLDSA, MLDSAParameter};
+ use crate::mldsa_keys::{
+ MLDSAPrivateKey, MLDSAPrivateKeyTrait, MLDSAPublicKey, MLDSAPublicKeyTrait, keygen_internal,
+ };
+ use crate::ntt::N;
+
+ // ML-DSA-44 파라미터
+ const K44: usize = 4;
+ const L44: usize = 4;
+ const ETA44: i32 = 2;
+ const PK44_LEN: usize = 1312;
+ const SK44_LEN: usize = 2560;
+
+ // ML-DSA-65 파라미터
+ const K65: usize = 6;
+ const L65: usize = 5;
+ const ETA65: i32 = 4;
+ const PK65_LEN: usize = 1952;
+ const SK65_LEN: usize = 4032;
+
+ //
+ // pkEncode / pkDecode 라운드트립 (ML-DSA-44)
+ //
+
+ #[test]
+ fn test_pk_encode_decode_roundtrip_44() {
+ let xi = [0u8; 32];
+ let (pk, _sk) = keygen_internal::(&xi).expect("keygen_internal failed");
+
+ // pkEncode
+ let pk_bytes: [u8; PK44_LEN] =
+ as MLDSAPublicKeyTrait>::pk_encode(&pk);
+
+ // pkDecode
+ let pk2 = as MLDSAPublicKeyTrait>::pk_decode(&pk_bytes);
+
+ // ρ 일치 검증
+ assert_eq!(pk.rho, pk2.rho, "pkDecode: ρ 불일치");
+
+ // t1 계수 일치 검증
+ for i in 0..K44 {
+ for j in 0..N {
+ assert_eq!(
+ pk.t1.vec[i].coeffs[j].0, pk2.t1.vec[i].coeffs[j].0,
+ "pkDecode: t1[{i}][{j}] 불일치"
+ );
+ }
+ }
+ }
+
+ //
+ // pkEncode / pkDecode 라운드트립 (ML-DSA-65)
+ //
+
+ #[test]
+ fn test_pk_encode_decode_roundtrip_65() {
+ let xi = [1u8; 32];
+ let (pk, _sk) = keygen_internal::(&xi).expect("keygen_internal failed");
+
+ let pk_bytes: [u8; PK65_LEN] =
+ as MLDSAPublicKeyTrait>::pk_encode(&pk);
+ let pk2 = as MLDSAPublicKeyTrait>::pk_decode(&pk_bytes);
+
+ assert_eq!(pk.rho, pk2.rho, "pkDecode: ρ 불일치");
+ for i in 0..K65 {
+ for j in 0..N {
+ assert_eq!(
+ pk.t1.vec[i].coeffs[j].0, pk2.t1.vec[i].coeffs[j].0,
+ "pkDecode: t1[{i}][{j}] 불일치"
+ );
+ }
+ }
+ }
+
+ //
+ // skEncode / skDecode 라운드트립 (ML-DSA-44)
+ //
+
+ #[test]
+ fn test_sk_encode_decode_roundtrip_44() {
+ type SK44 = MLDSAPrivateKey;
+
+ let xi = [2u8; 32];
+ let (_pk, sk) = keygen_internal::(&xi).expect("keygen_internal failed");
+
+ // skEncode → SecureBuffer
+ let sk_buf = >::sk_encode(&sk)
+ .expect("skEncode failed");
+
+ // SecureBuffer 길이 검증
+ assert_eq!(sk_buf.len(), SK44_LEN, "skEncode: 길이 불일치");
+
+ // skDecode
+ let sk2 = >::sk_decode(&sk_buf)
+ .expect("skDecode failed");
+
+ // 고정 필드 일치 검증
+ assert_eq!(sk.rho, sk2.rho, "skDecode: ρ 불일치");
+ assert_eq!(sk.k_seed, sk2.k_seed, "skDecode: K_seed 불일치");
+ assert_eq!(sk.tr, sk2.tr, "skDecode: tr 불일치");
+
+ // s1 계수 일치 검증
+ for i in 0..L44 {
+ for j in 0..N {
+ assert_eq!(
+ sk.s1.vec[i].coeffs[j].0, sk2.s1.vec[i].coeffs[j].0,
+ "skDecode: s1[{i}][{j}] 불일치"
+ );
+ }
+ }
+
+ // s2 계수 일치 검증
+ for i in 0..K44 {
+ for j in 0..N {
+ assert_eq!(
+ sk.s2.vec[i].coeffs[j].0, sk2.s2.vec[i].coeffs[j].0,
+ "skDecode: s2[{i}][{j}] 불일치"
+ );
+ }
+ }
+
+ // t0 계수 일치 검증
+ for i in 0..K44 {
+ for j in 0..N {
+ assert_eq!(
+ sk.t0.vec[i].coeffs[j].0, sk2.t0.vec[i].coeffs[j].0,
+ "skDecode: t0[{i}][{j}] 불일치"
+ );
+ }
+ }
+ }
+
+ //
+ // skEncode / skDecode 라운드트립 (ML-DSA-65)
+ //
+
+ #[test]
+ fn test_sk_encode_decode_roundtrip_65() {
+ type SK65 = MLDSAPrivateKey;
+
+ let xi = [3u8; 32];
+ let (_pk, sk) = keygen_internal::(&xi).expect("keygen_internal failed");
+
+ let sk_buf = >::sk_encode(&sk)
+ .expect("skEncode failed");
+
+ assert_eq!(sk_buf.len(), SK65_LEN, "skEncode: 길이 불일치");
+
+ let sk2 = >::sk_decode(&sk_buf)
+ .expect("skDecode failed");
+
+ assert_eq!(sk.rho, sk2.rho, "skDecode: ρ 불일치");
+ assert_eq!(sk.k_seed, sk2.k_seed, "skDecode: K_seed 불일치");
+ assert_eq!(sk.tr, sk2.tr, "skDecode: tr 불일치");
+ }
+
+ //
+ // 서명 + 검증 종단 간 테스트 (ML-DSA-44)
+ //
+
+ #[test]
+ fn test_sign_verify_roundtrip_44() {
+ let xi = [0xAAu8; 32];
+ let (pk_bytes, sk_buf) =
+ MLDSA::key_gen_internal(MLDSAParameter::MLDSA44, &xi).expect("key_gen_internal failed");
+
+ let message = b"Hello, ML-DSA-44!";
+ let m_prime = {
+ let mut v = Vec::new();
+ v.push(0x00u8); // domain_sep
+ v.push(0u8); // |ctx| = 0
+ v.extend_from_slice(message);
+ v
+ };
+ let rnd = [0u8; 32]; // 결정론적 서명
+
+ let sig = MLDSA::sign_internal(MLDSAParameter::MLDSA44, &sk_buf, &m_prime, &rnd)
+ .expect("sign_internal failed");
+
+ assert_eq!(sig.len(), 2420, "서명 길이 불일치");
+
+ let ok =
+ MLDSA::verify_internal(MLDSAParameter::MLDSA44, &pk_bytes, &m_prime, sig.as_slice())
+ .expect("verify_internal failed");
+
+ assert!(ok, "서명 검증 실패 (ML-DSA-44)");
+ }
+
+ //
+ // 서명 + 검증 종단 간 테스트 (ML-DSA-65)
+ //
+
+ #[test]
+ fn test_sign_verify_roundtrip_65() {
+ let xi = [0xBBu8; 32];
+ let (pk_bytes, sk_buf) =
+ MLDSA::key_gen_internal(MLDSAParameter::MLDSA65, &xi).expect("key_gen_internal failed");
+
+ let message = b"Hello, ML-DSA-65!";
+ let m_prime = {
+ let mut v = Vec::new();
+ v.push(0x00u8);
+ v.push(0u8);
+ v.extend_from_slice(message);
+ v
+ };
+ let rnd = [0u8; 32];
+
+ let sig = MLDSA::sign_internal(MLDSAParameter::MLDSA65, &sk_buf, &m_prime, &rnd)
+ .expect("sign_internal failed");
+
+ assert_eq!(sig.len(), 3309, "서명 길이 불일치");
+
+ let ok =
+ MLDSA::verify_internal(MLDSAParameter::MLDSA65, &pk_bytes, &m_prime, sig.as_slice())
+ .expect("verify_internal failed");
+
+ assert!(ok, "서명 검증 실패 (ML-DSA-65)");
+ }
+
+ //
+ // 변조된 메시지 검증 거부 테스트
+ //
+
+ #[test]
+ fn test_verify_rejects_tampered_message_44() {
+ let xi = [0xCCu8; 32];
+ let (pk_bytes, sk_buf) =
+ MLDSA::key_gen_internal(MLDSAParameter::MLDSA44, &xi).expect("key_gen_internal failed");
+
+ let m_prime_orig = b"\x00\x00Hello";
+ let rnd = [0u8; 32];
+
+ let sig = MLDSA::sign_internal(MLDSAParameter::MLDSA44, &sk_buf, m_prime_orig, &rnd)
+ .expect("sign_internal failed");
+
+ let m_prime_tampered = b"\x00\x00World";
+ let ok = MLDSA::verify_internal(
+ MLDSAParameter::MLDSA44,
+ &pk_bytes,
+ m_prime_tampered,
+ sig.as_slice(),
+ )
+ .expect("verify_internal error");
+
+ assert!(!ok, "변조된 메시지가 검증을 통과해서는 안 됩니다");
+ }
+
+ //
+ // 변조된 서명 검증 거부 테스트
+ //
+
+ #[test]
+ fn test_verify_rejects_tampered_signature_44() {
+ let xi = [0xDDu8; 32];
+ let (pk_bytes, sk_buf) =
+ MLDSA::key_gen_internal(MLDSAParameter::MLDSA44, &xi).expect("key_gen_internal failed");
+
+ let m_prime = b"\x00\x00TestMessage";
+ let rnd = [0u8; 32];
+
+ let mut sig = MLDSA::sign_internal(MLDSAParameter::MLDSA44, &sk_buf, m_prime, &rnd)
+ .expect("sign_internal failed");
+
+ // 서명 중간 바이트 비트 플립
+ sig.as_mut_slice()[100] ^= 0xFF;
+
+ let ok =
+ MLDSA::verify_internal(MLDSAParameter::MLDSA44, &pk_bytes, m_prime, sig.as_slice())
+ .expect("verify_internal error");
+
+ assert!(!ok, "변조된 서명이 검증을 통과해서는 안 됩니다");
+ }
+}
diff --git a/crypto/mldsa/src/error.rs b/crypto/mldsa/src/error.rs
new file mode 100644
index 0000000..0c2be58
--- /dev/null
+++ b/crypto/mldsa/src/error.rs
@@ -0,0 +1,36 @@
+use entlib_native_base::error::hash::HashError;
+use entlib_native_base::error::secure_buffer::SecureBufferError;
+
+#[derive(Debug)]
+pub enum MLDSAError {
+ /// 입력 바이트 슬라이스의 길이가 요구사항과 일치하지 않습니다.
+ InvalidLength,
+ /// 내부 연산 실패 (예: 해시 함수 오류, 메모리 할당 실패)
+ InternalError,
+ /// 난수 생성기(RNG) 오류
+ RngError,
+ /// ctx(컨텍스트) 길이가 FIPS 204 제한(255바이트)을 초과합니다.
+ ContextTooLong,
+ /// 서명 시도가 최대 반복 횟수를 초과하였습니다 (극히 희박한 경우).
+ SigningFailed,
+ /// 서명 검증 실패
+ InvalidSignature,
+ /// 아직 구현되지 않은 기능입니다.
+ NotImplemented,
+ /// 내부 해시 연산 실패
+ Hash(HashError),
+ /// SecureBuffer 할당 실패
+ Buffer(SecureBufferError),
+}
+
+impl From for MLDSAError {
+ fn from(e: HashError) -> Self {
+ MLDSAError::Hash(e)
+ }
+}
+
+impl From for MLDSAError {
+ fn from(e: SecureBufferError) -> Self {
+ MLDSAError::Buffer(e)
+ }
+}
diff --git a/crypto/mldsa/src/field.rs b/crypto/mldsa/src/field.rs
new file mode 100644
index 0000000..1a93891
--- /dev/null
+++ b/crypto/mldsa/src/field.rs
@@ -0,0 +1,57 @@
+use crate::Q;
+use crate::Q_INV;
+use entlib_native_constant_time::traits::{ConstantTimeIsNegative, ConstantTimeSelect};
+
+/// 유한체 Z_q의 원소를 나타내는 구조체
+#[derive(Clone, Copy, Debug, Default)]
+pub struct Fq(pub i32);
+
+impl Fq {
+ /// 새로운 필드 요소를 생성합니다. (입력값은 [0, Q-1] 범위 내에 있어야 함)
+ #[inline(always)]
+ pub const fn new(val: i32) -> Self {
+ Self(val)
+ }
+
+ /// 상수-시간 모듈러 덧셈
+ pub fn add(self, other: Self) -> Self {
+ let sum = self.0 + other.0;
+ // sum은 최대 2Q - 2 값을 가질 수 있습니다. Q를 빼서 범위를 맞춥니다.
+ let sub = sum - Q;
+
+ // sub가 음수(즉, sum < Q)이면 sum을 선택하고, 그렇지 않으면 sub를 선택합니다.
+ let is_neg = sub.ct_is_negative();
+ Self(i32::ct_select(&sum, &sub, is_neg))
+ }
+
+ /// 상수-시간 모듈러 뺄셈
+ pub fn sub(self, other: Self) -> Self {
+ let diff = self.0 - other.0;
+ // diff는 음수가 될 수 있으므로 Q를 더한 값을 준비합니다.
+ let add = diff + Q;
+
+ // diff가 음수이면 Q를 더한 add를 선택하고, 그렇지 않으면 diff를 유지합니다.
+ let is_neg = diff.ct_is_negative();
+ Self(i32::ct_select(&add, &diff, is_neg))
+ }
+
+ /// 몽고메리 환원을 이용한 상수-시간 모듈러 곱셈
+ ///
+ /// a * b * R^(-1) mod Q 연산을 수행합니다. (여기서 R = 2^32)
+ pub fn mul(self, other: Self) -> Self {
+ let prod = (self.0 as i64) * (other.0 as i64);
+
+ // t = (prod * Q_INV) mod 2^32
+ let t = (prod as i32).wrapping_mul(Q_INV);
+ // t_q = t * Q
+ let t_q = (t as i64) * (Q as i64);
+
+ // u = (prod - t_q) / 2^32
+ let u = ((prod - t_q) >> 32) as i32;
+
+ // u는 [-Q, Q-1] 범위에 있습니다. 음수인 경우 Q를 더해 보정합니다.
+ let is_neg = u.ct_is_negative();
+ let u_plus_q = u + Q;
+ Self(i32::ct_select(&u_plus_q, &u, is_neg))
+ }
+}
diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs
new file mode 100644
index 0000000..bd6b340
--- /dev/null
+++ b/crypto/mldsa/src/lib.rs
@@ -0,0 +1,132 @@
+// 어중간한 에러는 모두 InternalError 로 모호하게
+mod error;
+mod field;
+mod mldsa;
+mod mldsa_keys;
+mod mldsa_sign;
+mod ntt;
+mod pack;
+mod poly;
+mod sample;
+
+#[cfg(test)]
+mod _mldsa_test;
+
+//
+// Commons
+//
+
+/// 모듈러스 q
+pub(crate) const Q: i32 = 8380417;
+/// t에서 버려지는 비트 수
+pub(crate) const D: usize = 13;
+// Z_q 내의 512 제곱근
+// pub(crate) const ZETA: i32 = 1753;
+/// 몽고메리 환원을 위한 상수 q^(-1) mod 2^32
+pub(crate) const Q_INV: i32 = 58728449;
+pub(crate) const SEED_LEN: usize = 32;
+
+//
+// ML-DSA-44 Params
+//
+
+mod mldsa44 {
+ /// ML-DSA-44 공개 키 길이
+ pub(crate) const MLDSA44_PK_LEN: usize = 1312;
+ /// ML-DSA-44 비밀 키 길이
+ pub(crate) const MLDSA44_SK_LEN: usize = 2560;
+ /// ML-DSA-44 서명 길이
+ pub(crate) const MLDSA44_SIG_LEN: usize = 2420;
+ /// 행렬 A의 k 차원
+ pub(crate) const K_44: usize = 4;
+ /// 행렬 A의 l 차원
+ pub(crate) const L_44: usize = 4;
+ /// 개인키(Private key) 계수 범위 η (eta)
+ pub(crate) const ETA_44: i32 = 2;
+ /// 다항식 c에서 ±1의 개수 τ (tau)
+ pub(crate) const TAU_44: usize = 39;
+ /// β = τ * η
+ pub(crate) const BETA_44: i32 = 78;
+ /// c 틸다(tilde)의 충돌 강도 λ (lambda)
+ pub(crate) const LAMBDA_44: usize = 128;
+ /// y의 계수 범위 γ1 (gamma1) = 2^17
+ pub(crate) const GAMMA1_44: i32 = 131072;
+ /// 하위 차수 반올림 범위 γ2 (gamma2) = (q - 1) / 88
+ pub(crate) const GAMMA2_44: i32 = 95232;
+ /// 힌트 h에서 1의 최대 개수 ω (omega)
+ pub(crate) const OMEGA_44: usize = 80;
+}
+
+//
+// ML-DSA-65 Params
+//
+
+mod mldsa65 {
+ /// ML-DSA-65 공개 키 길이
+ pub(crate) const MLDSA65_PK_LEN: usize = 1952;
+ /// ML-DSA-65 비밀 키 길이
+ pub(crate) const MLDSA65_SK_LEN: usize = 4032;
+ /// ML-DSA-65 서명 길이
+ pub(crate) const MLDSA65_SIG_LEN: usize = 3309;
+ /// 행렬 A의 k 차원
+ pub(crate) const K_65: usize = 6;
+ /// 행렬 A의 l 차원
+ pub(crate) const L_65: usize = 5;
+ /// 개인키(Private key) 계수 범위 η (eta)
+ pub(crate) const ETA_65: i32 = 4;
+ /// 다항식 c에서 ±1의 개수 τ (tau)
+ pub(crate) const TAU_65: usize = 49;
+ /// β = τ * η
+ pub(crate) const BETA_65: i32 = 196;
+ /// c 틸다(tilde)의 충돌 강도 λ (lambda)
+ pub(crate) const LAMBDA_65: usize = 192;
+ /// y의 계수 범위 γ1 (gamma1) = 2^19
+ pub(crate) const GAMMA1_65: i32 = 524288;
+ /// 하위 차수 반올림 범위 γ2 (gamma2) = (q - 1) / 32
+ pub(crate) const GAMMA2_65: i32 = 261888;
+ /// 힌트 h에서 1의 최대 개수 ω (omega)
+ pub(crate) const OMEGA_65: usize = 55;
+}
+
+//
+// ML-DSA-87 Params
+//
+
+mod mldsa87 {
+ /// ML-DSA-87 공개 키 길이
+ pub(crate) const MLDSA87_PK_LEN: usize = 2592;
+ /// ML-DSA-87 비밀 키 길이
+ pub(crate) const MLDSA87_SK_LEN: usize = 4896;
+ /// ML-DSA-87 서명 길이
+ pub(crate) const MLDSA87_SIG_LEN: usize = 4627;
+ /// 행렬 A의 k 차원
+ pub(crate) const K_87: usize = 8;
+ /// 행렬 A의 l 차원
+ pub(crate) const L_87: usize = 7;
+ /// 개인키(Private key) 계수 범위 η (eta)
+ pub(crate) const ETA_87: i32 = 2;
+ /// 다항식 c에서 ±1의 개수 τ (tau)
+ pub(crate) const TAU_87: usize = 60;
+ /// β = τ * η
+ pub(crate) const BETA_87: i32 = 120;
+ /// c 틸다(tilde)의 충돌 강도 λ (lambda)
+ pub(crate) const LAMBDA_87: usize = 256;
+ /// y의 계수 범위 γ1 (gamma1) = 2^19
+ pub(crate) const GAMMA1_87: i32 = 524288;
+ /// 하위 차수 반올림 범위 γ2 (gamma2) = (q - 1) / 32
+ pub(crate) const GAMMA2_87: i32 = 261888;
+ /// 힌트 h에서 1의 최대 개수 ω (omega)
+ pub(crate) const OMEGA_87: usize = 75;
+}
+
+//
+// API Signature
+//
+
+pub use error::MLDSAError;
+pub use mldsa::{
+ CtrDRBGRng, HashDRBGRng, MLDSA, MLDSAParameter, MLDSAPrivateKey, MLDSAPublicKey, MLDSARng,
+};
+
+// todo: 아마도 유닛 테스트는 src/ 에 추가
+// 외부 시그니처에 대한 테스트는 tests/
diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs
new file mode 100644
index 0000000..4e1c821
--- /dev/null
+++ b/crypto/mldsa/src/mldsa.rs
@@ -0,0 +1,617 @@
+//! FIPS 204 명세에 따른 모듈 격자 기반 전자 서명(Module Lattice-based Digital Signature Algorithm, ML-DSA)
+//! 알고리즘 구현 모듈입니다. 해당 명세 서명 스키마의 최상위 공개 인터페이스를 제공합니다.
+//!
+//! # Example
+//! ```rust,ignore
+//! use entlib_native_mldsa::{MLDSA, MLDSAParameter, HashDRBGRng};
+//!
+//! // 1. RNG 초기화 (OS 엔트로피 소스 사용 — 임의 엔트로피 주입 불가)
+//! let mut rng = HashDRBGRng::new_from_os(None).unwrap();
+//!
+//! // 2. 키 쌍 생성 (ML-DSA-44)
+//! let (pk_bytes, sk_buf) = MLDSA::key_gen(MLDSAParameter::MLDSA44, &mut rng).unwrap();
+//!
+//! // 3. 서명
+//! let message = b"Hello, ML-DSA!";
+//! let ctx = b"";
+//! let sig = MLDSA::sign(MLDSAParameter::MLDSA44, &sk_buf, message, ctx, &mut rng).unwrap();
+//!
+//! // 4. 검증
+//! let ok = MLDSA::verify(MLDSAParameter::MLDSA44, &pk_bytes, message, &sig, ctx).unwrap();
+//! assert!(ok);
+//! ```
+
+use super::mldsa44::{
+ BETA_44, ETA_44, GAMMA1_44, GAMMA2_44, K_44, L_44, LAMBDA_44, MLDSA44_PK_LEN, MLDSA44_SIG_LEN,
+ MLDSA44_SK_LEN, OMEGA_44, TAU_44,
+};
+use super::mldsa65::{
+ BETA_65, ETA_65, GAMMA1_65, GAMMA2_65, K_65, L_65, LAMBDA_65, MLDSA65_PK_LEN, MLDSA65_SIG_LEN,
+ MLDSA65_SK_LEN, OMEGA_65, TAU_65,
+};
+use super::mldsa87::{
+ BETA_87, ETA_87, GAMMA1_87, GAMMA2_87, K_87, L_87, LAMBDA_87, MLDSA87_PK_LEN, MLDSA87_SIG_LEN,
+ MLDSA87_SK_LEN, OMEGA_87, TAU_87,
+};
+use crate::error::MLDSAError;
+use crate::mldsa_keys::keygen_internal;
+use crate::mldsa_keys::{
+ MLDSAPrivateKey as SkComponents, MLDSAPrivateKeyTrait, MLDSAPublicKey as PkComponents,
+ MLDSAPublicKeyTrait,
+};
+use crate::mldsa_sign::{sign_internal_impl, verify_internal_impl};
+use entlib_native_rng::{DrbgError, HashDRBGSHA512};
+use entlib_native_secure_buffer::SecureBuffer;
+
+//
+// RNG 추상화 트레이트
+//
+
+/// ML-DSA 연산에 사용되는 암호학적으로 안전한 난수 생성기 트레이트.
+///
+/// 이 트레이트를 구현하는 타입은 NIST SP 800-90A Rev.1 이상의 보안 강도를
+/// 제공하는 결정론적 난수 비트 생성기(DRBG)여야 합니다.
+///
+/// # Features
+/// - [`HashDRBGRng`]: NIST Hash_DRBG (SHA-512, Security Strength 256-bit)
+/// - [`CtrDRBGRng`]: NIST CTR_DRBG (AES-256-CTR) **향후 개발 예정**
+pub trait MLDSARng {
+ /// `dest` 슬라이스를 암호학적으로 안전한 난수 바이트로 채웁니다.
+ ///
+ /// # Errors
+ /// - `MLDSAError::RngError`: RNG 내부 오류 또는 reseed가 필요한 경우
+ fn fill_random(&mut self, dest: &mut [u8]) -> Result<(), MLDSAError>;
+}
+
+//
+// Hash_DRBG 래퍼
+//
+
+/// NIST SP 800-90A Rev.1 Hash_DRBG (SHA-512 기반) RNG 래퍼.
+///
+/// ML-DSA 키 생성 및 서명에 사용하도록 설계되었습니다.
+/// - Security Strength: **256-bit**
+/// - 최소 엔트로피: 32바이트
+/// - 최소 Nonce: 16바이트
+/// - 내부 상태(V, C)는 [`SecureBuffer`]에 보관되어 Drop 시 자동 소거됩니다.
+///
+/// # Security Note
+/// `entropy_input`은 반드시 `/dev/urandom`, HWRNG, 또는 동등한 암호학적
+/// 엔트로피 소스에서 획득해야 합니다. 예측 가능한 값을 절대 사용하지 마세요.
+pub struct HashDRBGRng {
+ inner: HashDRBGSHA512,
+}
+
+impl HashDRBGRng {
+ /// OS 엔트로피 소스로부터 Hash_DRBG(SHA-512)를 초기화합니다.
+ ///
+ /// 이것이 유일한 초기화 경로입니다. 외부에서 임의 엔트로피를 주입할 수 없으며,
+ /// OS(Linux: `getrandom(2)`, macOS: `getentropy(2)`)가 수집한 엔트로피만 사용됩니다.
+ ///
+ /// # Arguments
+ /// - `personalization_string`: 선택적 응용 프로그램 식별 문자열 (최대 125 bytes)
+ ///
+ /// # Errors
+ /// - `MLDSAError::RngError`: OS 엔트로피 소스 접근 실패 또는 내부 오류
+ pub fn new_from_os(personalization_string: Option<&[u8]>) -> Result {
+ let inner = HashDRBGSHA512::new_from_os(personalization_string).map_err(drbg_err)?;
+ Ok(Self { inner })
+ }
+
+ /// 현재 RNG 상태를 새 엔트로피로 갱신합니다.
+ ///
+ /// `MLDSAError::RngError(ReseedRequired)`를 수신한 경우 반드시 호출해야 합니다.
+ pub fn reseed(
+ &mut self,
+ entropy_input: &[u8],
+ additional_input: Option<&[u8]>,
+ ) -> Result<(), MLDSAError> {
+ self.inner
+ .reseed(entropy_input, additional_input)
+ .map_err(drbg_err)
+ }
+}
+
+impl MLDSARng for HashDRBGRng {
+ fn fill_random(&mut self, dest: &mut [u8]) -> Result<(), MLDSAError> {
+ self.inner.generate(dest, None).map_err(drbg_err)
+ }
+}
+
+//
+// CTR_DRBG 래퍼 (미구현 — 확장 예약)
+//
+
+/// NIST SP 800-90A Rev.1 CTR_DRBG (AES-256-CTR 기반) RNG 래퍼.
+///
+/// AES-256-CTR 구현이 준비되면 이 구조체에 내부 상태를 추가하고
+/// [`MLDSARng`] impl 내에서 CTR_DRBG 알고리즘을 구현합니다.
+///
+/// # Security Note
+/// **현재 미구현 상태입니다.**
+/// AES-256 블록 암호 구현 크레이트(`entlib-native-aes`) 완료 후 제공될 예정입니다.
+pub struct CtrDRBGRng {
+ // todo: AES-256-CTR DRBG 상태 여기에 추가하면 됌
+ // key: SecureBuffer (256-bit)
+ // value: SecureBuffer (128-bit, AES block size)
+ // reseed_counter: u64
+ _private: (),
+}
+
+impl CtrDRBGRng {
+ /// CTR_DRBG를 초기화합니다.
+ ///
+ /// **현재 항상 `MLDSAError::NotImplemented`를 반환합니다.**
+ pub fn new(_entropy_input: &[u8], _nonce: &[u8]) -> Result {
+ Err(MLDSAError::NotImplemented)
+ }
+}
+
+impl MLDSARng for CtrDRBGRng {
+ fn fill_random(&mut self, _dest: &mut [u8]) -> Result<(), MLDSAError> {
+ Err(MLDSAError::NotImplemented)
+ }
+}
+
+//
+// ML-DSA 파라미터 셋
+//
+
+/// NIST FIPS 204에 정의된 ML-DSA 파라미터 셋
+///
+/// | 파라미터 셋 | NIST 카테고리 | pk 크기 | sk 크기 | 서명 크기 |
+/// |-------------|:------------:|--------:|--------:|----------:|
+/// | MLDSA44 | 2 (AES-128 동급) | 1312 B | 2560 B | 2420 B |
+/// | MLDSA65 | 3 (AES-192 동급) | 1952 B | 4032 B | 3309 B |
+/// | MLDSA87 | 5 (AES-256 동급) | 2592 B | 4896 B | 4627 B |
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MLDSAParameter {
+ /// ML-DSA-44: NIST 보안 카테고리 2 (Security Strength ≥ 128-bit)
+ MLDSA44,
+ /// ML-DSA-65: NIST 보안 카테고리 3 (Security Strength ≥ 192-bit)
+ MLDSA65,
+ /// ML-DSA-87: NIST 보안 카테고리 5 (Security Strength ≥ 256-bit)
+ MLDSA87,
+}
+
+impl MLDSAParameter {
+ /// 공개 키 바이트 길이를 반환합니다.
+ #[inline]
+ pub const fn pk_len(self) -> usize {
+ match self {
+ MLDSAParameter::MLDSA44 => 1312,
+ MLDSAParameter::MLDSA65 => 1952,
+ MLDSAParameter::MLDSA87 => 2592,
+ }
+ }
+
+ /// 비밀 키 바이트 길이를 반환합니다.
+ #[inline]
+ pub const fn sk_len(self) -> usize {
+ match self {
+ MLDSAParameter::MLDSA44 => 2560,
+ MLDSAParameter::MLDSA65 => 4032,
+ MLDSAParameter::MLDSA87 => 4896,
+ }
+ }
+
+ /// 서명 바이트 길이를 반환합니다.
+ #[inline]
+ pub const fn sig_len(self) -> usize {
+ match self {
+ MLDSAParameter::MLDSA44 => 2420,
+ MLDSAParameter::MLDSA65 => 3309,
+ MLDSAParameter::MLDSA87 => 4627,
+ }
+ }
+}
+
+//
+// 공개 키 / 비밀 키 타입
+//
+
+/// ML-DSA 공개 키.
+///
+/// 인코딩된 공개 키 바이트(`ρ || SimpleBitPack(t1)`)와 파라미터 셋을 함께 보유합니다.
+/// [`MLDSA::key_gen`]이 반환하며, [`MLDSA::verify`]에 직접 전달할 수 있습니다.
+pub struct MLDSAPublicKey {
+ param: MLDSAParameter,
+ bytes: Vec,
+}
+
+impl MLDSAPublicKey {
+ /// 이 공개 키가 속한 파라미터 셋을 반환합니다.
+ #[inline]
+ pub fn param(&self) -> MLDSAParameter {
+ self.param
+ }
+
+ /// 인코딩된 공개 키 바이트 슬라이스를 반환합니다.
+ ///
+ /// 반환값은 FIPS 204 `pkEncode` 출력(`ρ || SimpleBitPack(t1, 10)`)과 동일합니다.
+ #[inline]
+ pub fn as_bytes(&self) -> &[u8] {
+ &self.bytes
+ }
+
+ /// 인코딩된 공개 키의 바이트 길이를 반환합니다.
+ #[inline]
+ pub fn len(&self) -> usize {
+ self.bytes.len()
+ }
+
+ /// 공개 키가 비어 있으면 `true`를 반환합니다 (정상적으로 생성된 키에서는 발생하지 않습니다).
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.bytes.is_empty()
+ }
+
+ /// 인코딩된 바이트열로부터 공개 키를 복원합니다.
+ ///
+ /// # Errors
+ /// 바이트 길이가 파라미터 셋과 일치하지 않으면 `InvalidLength`.
+ pub fn from_bytes(param: MLDSAParameter, bytes: Vec) -> Result {
+ if bytes.len() != param.pk_len() {
+ return Err(MLDSAError::InvalidLength);
+ }
+ Ok(Self { param, bytes })
+ }
+}
+
+/// ML-DSA 비밀 키.
+///
+/// 직렬화된 비밀 키 바이트를 OS 레벨 잠금 메모리([`SecureBuffer`])에 보관합니다.
+/// `Drop` 시점에 메모리가 자동으로 소거(Zeroize)됩니다.
+///
+/// [`MLDSA::key_gen`]이 반환하며, [`MLDSA::sign`]에 직접 전달할 수 있습니다.
+pub struct MLDSAPrivateKey {
+ param: MLDSAParameter,
+ sk_buf: SecureBuffer,
+}
+
+impl MLDSAPrivateKey {
+ /// 이 비밀 키가 속한 파라미터 셋을 반환합니다.
+ #[inline]
+ pub fn param(&self) -> MLDSAParameter {
+ self.param
+ }
+
+ /// 인코딩된 비밀 키의 바이트 길이를 반환합니다.
+ #[inline]
+ pub fn len(&self) -> usize {
+ self.sk_buf.len()
+ }
+
+ /// 비밀 키가 비어 있으면 `true`를 반환합니다 (정상적으로 생성된 키에서는 발생하지 않습니다).
+ #[inline]
+ pub fn is_empty(&self) -> bool {
+ self.sk_buf.is_empty()
+ }
+
+ /// 인코딩된 비밀 키 바이트 슬라이스를 반환합니다.
+ ///
+ /// # Security Note
+ /// 반환된 슬라이스는 잠금 메모리(mlock)에 보관된 민감 데이터입니다.
+ /// 파일 저장 시 반드시 PKCS#8 암호화를 적용하십시오.
+ #[inline]
+ pub fn as_bytes(&self) -> &[u8] {
+ self.sk_buf.as_slice()
+ }
+
+ /// 인코딩된 바이트열로부터 비밀 키를 복원합니다.
+ ///
+ /// # Security Note
+ /// `bytes`는 호출 즉시 SecureBuffer(mlock)로 이전됩니다.
+ ///
+ /// # Errors
+ /// 바이트 길이가 파라미터 셋과 일치하지 않으면 `InvalidLength`.
+ pub fn from_bytes(param: MLDSAParameter, bytes: &[u8]) -> Result {
+ if bytes.len() != param.sk_len() {
+ return Err(MLDSAError::InvalidLength);
+ }
+ let mut sk_buf =
+ SecureBuffer::new_owned(bytes.len()).map_err(|_| MLDSAError::InternalError)?;
+ sk_buf.as_mut_slice().copy_from_slice(bytes);
+ Ok(Self { param, sk_buf })
+ }
+}
+
+//
+// MLDSA 공개 API
+//
+
+/// NIST FIPS 204 ML-DSA 서명 스키마의 최상위 진입점.
+///
+/// 모든 메소드는 정적(static)이며, 파라미터 셋 정보는 키 타입에 내장됩니다.
+pub struct MLDSA;
+
+impl MLDSA {
+ //
+ // 외부 인터페이스 (FIPS 204 Algorithms 1–3)
+ //
+
+ /// Algorithm 1: ML-DSA.KeyGen(λ)
+ ///
+ /// RNG로 32바이트 시드 ξ를 생성하고, 이를 바탕으로 공개 키와 비밀 키 쌍을
+ /// 결정론적으로 유도합니다.
+ ///
+ /// # Returns
+ /// - [`MLDSAPublicKey`]: 파라미터 셋과 인코딩된 공개 키 바이트를 보유
+ /// - [`MLDSAPrivateKey`]: 비밀 키를 OS 잠금 메모리([`SecureBuffer`])에 보관 (Drop 시 자동 소거)
+ ///
+ /// # Errors
+ /// - `MLDSAError::RngError`: RNG에서 시드를 얻지 못한 경우
+ /// - `MLDSAError::InternalError`: 내부 연산 실패
+ pub fn key_gen(
+ param: MLDSAParameter,
+ rng: &mut R,
+ ) -> Result<(MLDSAPublicKey, MLDSAPrivateKey), MLDSAError> {
+ // 32바이트 시드 ξ를 RNG에서 생성
+ let mut xi = [0u8; 32];
+ rng.fill_random(&mut xi)?;
+
+ let (pk_bytes, sk_buf) = Self::key_gen_internal(param, &xi)?;
+ Ok((
+ MLDSAPublicKey {
+ param,
+ bytes: pk_bytes,
+ },
+ MLDSAPrivateKey { param, sk_buf },
+ ))
+ }
+
+ /// Algorithm 2: ML-DSA.Sign(dk, M, ctx)
+ ///
+ /// 비밀 키 `sk`와 메시지 `message`를 이용하여 디지털 서명을 생성합니다.
+ /// 서명은 RNG에서 얻은 32바이트 rnd 값으로 헤지드(hedged) 처리됩니다.
+ /// 파라미터 셋은 `sk`에 내장되어 있으므로 별도로 지정하지 않습니다.
+ ///
+ /// # Arguments
+ /// - `sk`: [`key_gen`](Self::key_gen)이 반환한 [`MLDSAPrivateKey`]
+ /// - `message`: 서명할 메시지 바이트 슬라이스 (크기 제한 없음)
+ /// - `ctx`: 응용 컨텍스트 문자열 (`ctx.len() ≤ 255`, FIPS 204 Section 5.2)
+ /// - `rng`: 헤지드 서명에 사용할 RNG
+ ///
+ /// # Returns
+ /// 직렬화된 서명을 OS 잠금 메모리([`SecureBuffer`])에 담아 반환합니다.
+ /// 길이 = `sk.param().sig_len()`
+ pub fn sign(
+ sk: &MLDSAPrivateKey,
+ message: &[u8],
+ ctx: &[u8],
+ rng: &mut R,
+ ) -> Result {
+ // ctx 길이 검증: FIPS 204 Section 5.2, 255바이트 이하
+ if ctx.len() > 255 {
+ return Err(MLDSAError::ContextTooLong);
+ }
+
+ // 32바이트 rnd를 RNG에서 생성 (헤지드 서명)
+ let mut rnd = [0u8; 32];
+ rng.fill_random(&mut rnd)?;
+
+ // M' = 0x00 || IntegerToBytes(|ctx|, 1) || ctx || M
+ let m_prime = build_m_prime(0x00, ctx, message);
+
+ Self::sign_internal(sk.param, &sk.sk_buf, &m_prime, &rnd)
+ }
+
+ /// Algorithm 3: ML-DSA.Verify(ek, M, σ, ctx)
+ ///
+ /// 공개 키 `pk`를 이용하여 서명 `sig`가 `message`에 대한 유효한
+ /// ML-DSA 서명인지 검증합니다.
+ /// 파라미터 셋은 `pk`에 내장되어 있으므로 별도로 지정하지 않습니다.
+ ///
+ /// # Arguments
+ /// - `pk`: [`key_gen`](Self::key_gen)이 반환한 [`MLDSAPublicKey`]
+ /// - `message`: 원본 메시지 바이트 슬라이스
+ /// - `sig`: 검증할 서명 바이트 슬라이스
+ /// - `ctx`: 서명 시 사용한 컨텍스트 문자열 (동일해야 함)
+ ///
+ /// # Returns
+ /// - `Ok(true)`: 서명 유효
+ /// - `Ok(false)`: 서명 무효 (상수-시간 비교)
+ /// - `Err(MLDSAError::ContextTooLong)`: ctx가 255바이트 초과
+ pub fn verify(
+ pk: &MLDSAPublicKey,
+ message: &[u8],
+ sig: &[u8],
+ ctx: &[u8],
+ ) -> Result {
+ if ctx.len() > 255 {
+ return Err(MLDSAError::ContextTooLong);
+ }
+
+ // 서명 길이 사전 검증 (빠른 거부)
+ if sig.len() != pk.param.sig_len() {
+ return Ok(false);
+ }
+
+ // M' = 0x00 || IntegerToBytes(|ctx|, 1) || ctx || M
+ let m_prime = build_m_prime(0x00, ctx, message);
+
+ Self::verify_internal(pk.param, &pk.bytes, &m_prime, sig)
+ }
+
+ //
+ // 내부 인터페이스 (FIPS 204 Algorithms 4–7)
+ //
+
+ /// Algorithm 4: ML-DSA.KeyGen_internal(ξ)
+ ///
+ /// 32바이트 시드 ξ로부터 공개 키와 비밀 키 쌍을 결정론적으로 생성합니다.
+ /// 주어진 ξ에 대해 항상 동일한 키 쌍을 반환합니다 (KAT 테스트에 사용).
+ ///
+ /// # Security Note
+ /// ξ는 암호학적으로 안전한 RNG로 생성해야 합니다.
+ /// 예측 가능하거나 재사용된 ξ는 심각한 보안 취약점을 야기합니다.
+ pub(crate) fn key_gen_internal(
+ param: MLDSAParameter,
+ xi: &[u8; 32],
+ ) -> Result<(Vec, SecureBuffer), MLDSAError> {
+ match param {
+ MLDSAParameter::MLDSA44 => {
+ keygen_encode::(xi)
+ }
+ MLDSAParameter::MLDSA65 => {
+ keygen_encode::(xi)
+ }
+ MLDSAParameter::MLDSA87 => {
+ keygen_encode::(xi)
+ }
+ }
+ }
+
+ /// Algorithm 5: ML-DSA.Sign_internal(dk, M', rnd)
+ ///
+ /// 결정론적 서명 내부 알고리즘. `rnd`가 [0u8; 32]이면 순수 결정론적 서명,
+ /// 그 외에는 헤지드(hedged) 서명입니다.
+ ///
+ /// 거절 샘플링 기반 서명 루프(ExpandMask, Decompose, MakeHint, SigEncode)를
+ /// 파라미터 셋별로 단형화하여 호출합니다.
+ pub(crate) fn sign_internal(
+ param: MLDSAParameter,
+ sk_buf: &SecureBuffer,
+ m_prime: &[u8],
+ rnd: &[u8; 32],
+ ) -> Result {
+ match param {
+ MLDSAParameter::MLDSA44 => sign_internal_impl::<
+ K_44,
+ L_44,
+ ETA_44,
+ GAMMA1_44,
+ GAMMA2_44,
+ BETA_44,
+ OMEGA_44,
+ LAMBDA_44,
+ TAU_44,
+ MLDSA44_SK_LEN,
+ MLDSA44_SIG_LEN,
+ >(sk_buf, m_prime, rnd),
+ MLDSAParameter::MLDSA65 => sign_internal_impl::<
+ K_65,
+ L_65,
+ ETA_65,
+ GAMMA1_65,
+ GAMMA2_65,
+ BETA_65,
+ OMEGA_65,
+ LAMBDA_65,
+ TAU_65,
+ MLDSA65_SK_LEN,
+ MLDSA65_SIG_LEN,
+ >(sk_buf, m_prime, rnd),
+ MLDSAParameter::MLDSA87 => sign_internal_impl::<
+ K_87,
+ L_87,
+ ETA_87,
+ GAMMA1_87,
+ GAMMA2_87,
+ BETA_87,
+ OMEGA_87,
+ LAMBDA_87,
+ TAU_87,
+ MLDSA87_SK_LEN,
+ MLDSA87_SIG_LEN,
+ >(sk_buf, m_prime, rnd),
+ }
+ }
+
+ /// Algorithm 7: ML-DSA.Verify_internal(ek, M', σ)
+ ///
+ /// 결정론적 검증 내부 알고리즘.
+ /// w1' 재구성 및 챌린지 해시 비교를 파라미터 셋별로 단형화하여 호출합니다.
+ pub(crate) fn verify_internal(
+ param: MLDSAParameter,
+ pk_bytes: &[u8],
+ m_prime: &[u8],
+ sig: &[u8],
+ ) -> Result {
+ match param {
+ MLDSAParameter::MLDSA44 => verify_internal_impl::<
+ K_44,
+ L_44,
+ GAMMA1_44,
+ GAMMA2_44,
+ BETA_44,
+ OMEGA_44,
+ LAMBDA_44,
+ TAU_44,
+ MLDSA44_PK_LEN,
+ MLDSA44_SIG_LEN,
+ >(pk_bytes, m_prime, sig),
+ MLDSAParameter::MLDSA65 => verify_internal_impl::<
+ K_65,
+ L_65,
+ GAMMA1_65,
+ GAMMA2_65,
+ BETA_65,
+ OMEGA_65,
+ LAMBDA_65,
+ TAU_65,
+ MLDSA65_PK_LEN,
+ MLDSA65_SIG_LEN,
+ >(pk_bytes, m_prime, sig),
+ MLDSAParameter::MLDSA87 => verify_internal_impl::<
+ K_87,
+ L_87,
+ GAMMA1_87,
+ GAMMA2_87,
+ BETA_87,
+ OMEGA_87,
+ LAMBDA_87,
+ TAU_87,
+ MLDSA87_PK_LEN,
+ MLDSA87_SIG_LEN,
+ >(pk_bytes, m_prime, sig),
+ }
+ }
+}
+
+//
+// 내부 유틸리티
+//
+
+/// M' 구성: `domain_sep || IntegerToBytes(|ctx|, 1) || ctx || M`
+///
+/// FIPS 204 Section 5.2에 따른 외부 인터페이스 메시지 전처리.
+/// - ML-DSA.Sign/Verify: domain_sep = 0x00
+/// - HashML-DSA.Sign/Verify: domain_sep = 0x01
+fn build_m_prime(domain_sep: u8, ctx: &[u8], message: &[u8]) -> Vec {
+ let mut m_prime = Vec::with_capacity(2 + ctx.len() + message.len());
+ m_prime.push(domain_sep);
+ m_prime.push(ctx.len() as u8); // |ctx| ≤ 255이므로 u8 안전
+ m_prime.extend_from_slice(ctx);
+ m_prime.extend_from_slice(message);
+ m_prime
+}
+
+/// 키 생성 + 인코딩 헬퍼 (파라미터 셋별 단형화)
+///
+/// `keygen_internal`을 호출하고 pk를 바이트로, sk를 SecureBuffer로 직렬화합니다.
+fn keygen_encode<
+ const K: usize,
+ const L: usize,
+ const ETA: i32,
+ const PK_LEN: usize,
+ const SK_LEN: usize,
+>(
+ xi: &[u8; 32],
+) -> Result<(Vec, SecureBuffer), MLDSAError> {
+ let (pk, sk) = keygen_internal::(xi)?;
+
+ // pkEncode: ρ || SimpleBitPack(t1, 10) — PK_LEN 바이트
+ let pk_bytes = as MLDSAPublicKeyTrait>::pk_encode(&pk);
+
+ // skEncode: SecureBuffer (OS 잠금 메모리)
+ let sk_buf = as MLDSAPrivateKeyTrait>::sk_encode(&sk)?;
+
+ Ok((pk_bytes.to_vec(), sk_buf))
+}
+
+/// `DrbgError`를 `MLDSAError::RngError`로 변환
+#[inline(always)]
+fn drbg_err(_e: DrbgError) -> MLDSAError {
+ MLDSAError::RngError
+}
diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs
new file mode 100644
index 0000000..00e7372
--- /dev/null
+++ b/crypto/mldsa/src/mldsa_keys.rs
@@ -0,0 +1,362 @@
+use crate::error::MLDSAError;
+use crate::error::MLDSAError::InvalidLength;
+use crate::field::Fq;
+use crate::ntt::N;
+use crate::pack::{
+ poly_simple_bit_pack_t1, poly_simple_bit_unpack_t1, polyvec_bit_pack_eta, polyvec_bit_pack_t0,
+ polyvec_bit_unpack_eta, polyvec_bit_unpack_t0,
+};
+use crate::poly::PolyVec;
+use crate::sample::{expand_a, expand_s};
+use crate::{Q, SEED_LEN};
+use entlib_native_constant_time::traits::{ConstantTimeIsNegative, ConstantTimeSelect};
+use entlib_native_secure_buffer::SecureBuffer;
+use entlib_native_sha3::api::SHAKE256;
+
+//
+// 트레이트 정의
+//
+
+pub trait MLDSAPublicKeyTrait {
+ /// Algorithm 22: pkEncode(ρ, t1)
+ ///
+ /// 공개 키를 바이트 문자열로 인코딩합니다.
+ /// 입력: ρ ∈ 𝔹^32, t1 ∈ R_q^k (계수 ∈ [0, 2^(bitlen(q-1)-d) - 1])
+ /// 출력: pk ∈ 𝔹^(32 + 32k·(bitlen(q-1)-d))
+ fn pk_encode(&self) -> [u8; PK_LEN];
+
+ /// Algorithm 23: pkDecode(pk)
+ ///
+ /// pkEncode의 역연산.
+ fn pk_decode(pk: &[u8; PK_LEN]) -> Self;
+}
+
+pub trait MLDSAPrivateKeyTrait {
+ /// Algorithm 24: skEncode(ρ, K, tr, s1, s2, t0)
+ ///
+ /// 비밀 키를 SecureBuffer에 직렬화합니다. 민감 데이터는 OS 레벨로 잠긴
+ /// 물리 메모리에 저장되며, Drop 시 자동 소거됩니다.
+ fn sk_encode(&self) -> Result;
+
+ /// Algorithm 25: skDecode(sk)
+ ///
+ /// skEncode의 역연산. 길이 검증 후 필드를 복원합니다.
+ fn sk_decode(sk: &SecureBuffer) -> Result
+ where
+ Self: Sized;
+}
+
+//
+// 키 구조체
+//
+
+/// ML-DSA 공개 키 구조체
+pub struct MLDSAPublicKey {
+ pub(crate) rho: [u8; SEED_LEN],
+ pub(crate) t1: PolyVec,
+}
+
+/// ML-DSA 비밀 키 구조체
+///
+/// `ETA`는 s1, s2의 계수 범위 [-η, η]를 결정하는 파라미터로,
+/// sk_encode/sk_decode 시 올바른 비트 너비를 계산하는 데 사용됩니다.
+/// 비밀 키는 SecureBuffer를 통해 외부에 직렬화하며, 구조체 자체는
+/// 스택에 임시로만 존재합니다.
+pub struct MLDSAPrivateKey {
+ pub(crate) rho: [u8; 32],
+ pub(crate) k_seed: [u8; 32],
+ pub(crate) tr: [u8; 64],
+ pub(crate) s1: PolyVec,
+ pub(crate) s2: PolyVec,
+ pub(crate) t0: PolyVec,
+}
+
+//
+// 내부 유틸리티
+//
+
+/// bitlen(2η): s1, s2 인코딩에 사용하는 계수당 비트 수를 반환합니다.
+///
+/// - η=2 → bitlen(4) = 3
+/// - η=4 → bitlen(8) = 4
+#[inline(always)]
+fn eta_bit_width(eta: i32) -> usize {
+ (u32::BITS - (2 * eta as u32).leading_zeros()) as usize
+}
+
+/// Power2Round (Algorithm 35)
+///
+/// 다항식 벡터 t의 각 계수를 상위 10비트(t1)와 하위 13비트(t0)로 분할합니다.
+/// - t1 = ⌈t / 2^d⌉, t0 = t - t1 * 2^d
+/// - t0 ∈ [-2^(d-1)+1, 2^(d-1)], d = 13
+fn power2round_vec(t: &PolyVec) -> (PolyVec, PolyVec) {
+ let mut t1 = PolyVec::::new_zero();
+ let mut t0 = PolyVec::::new_zero();
+
+ for i in 0..K {
+ for j in 0..N {
+ let a = t.vec[i].coeffs[j].0;
+
+ // a1 = ⌈a / 2^13⌉ = (a + 2^12 - 1) >> 13 (상수-시간 올림 나눗셈)
+ let a1 = (a + 4095) >> 13;
+ let a0 = a - (a1 << 13); // a0 ∈ [-4095, 4096]
+
+ // 음수 a0를 Fq 표현으로 상수-시간 변환 (부채널 방지)
+ let is_neg = a0.ct_is_negative();
+ let a0_fq = i32::ct_select(&(a0 + Q), &a0, is_neg);
+
+ t1.vec[i].coeffs[j] = Fq::new(a1);
+ t0.vec[i].coeffs[j] = Fq::new(a0_fq);
+ }
+ }
+
+ (t1, t0)
+}
+
+//
+// Algorithm 6: ML-DSA.KeyGen_internal(ξ)
+//
+
+/// Algorithm 6: ML-DSA.KeyGen_internal(ξ)
+///
+/// 32바이트 시드 ξ로부터 공개키와 비밀키 쌍을 결정론적으로 생성합니다.
+pub(crate) fn keygen_internal(
+ xi: &[u8; 32],
+) -> Result<(MLDSAPublicKey, MLDSAPrivateKey), MLDSAError> {
+ // 1: (ρ, ρ', K) ← H(ξ || IntegerToBytes(k, 1) || IntegerToBytes(l, 1), 128)
+ let mut seed_input = [0u8; 34];
+ seed_input[..32].copy_from_slice(xi);
+ seed_input[32] = K as u8;
+ seed_input[33] = L as u8;
+
+ let mut shake = SHAKE256::new();
+ shake.update(&seed_input);
+ // let rho sfkjwenfoinf
+ let expanded = shake.finalize(128)?;
+ let ex_slice = expanded.as_slice();
+
+ let mut rho = [0u8; 32];
+ let mut rho_prime = [0u8; 64];
+ let mut k_seed = [0u8; 32];
+ rho.copy_from_slice(&ex_slice[0..32]);
+ rho_prime.copy_from_slice(&ex_slice[32..96]);
+ k_seed.copy_from_slice(&ex_slice[96..128]);
+
+ // 3: A_hat ← ExpandA(ρ)
+ let a_hat = expand_a::(&rho)?;
+
+ // 4: (s1, s2) ← ExpandS(ρ')
+ let (mut s1, s2) = expand_s::(&rho_prime)?;
+
+ // 5: t ← INTT(A_hat ∘ NTT(s1)) + s2
+ let s1_original = s1;
+ s1.ntt();
+ let mut t = a_hat.multiply_vector(&s1);
+ t.intt();
+ t = t.add(&s2);
+
+ // 6: (t1, t0) ← Power2Round(t)
+ let (t1, t0) = power2round_vec(&t);
+
+ // 8: pk_bytes ← pkEncode(ρ, t1)
+ // 9: tr ← H(pk_bytes, 64)
+ //
+ // FIPS 204에 따라 pkEncode 출력(ρ || SimpleBitPack(t1, 10))을 SHAKE256으로 해싱합니다.
+ // pkEncode는 rho(32B) || 각 t1 다항식(320B씩 K개) 순서로 구성됩니다.
+ // PK_LEN이 keygen_internal의 제네릭이 아니므로 인크리멘탈 해싱으로 처리합니다.
+ let mut shake_tr = SHAKE256::new();
+ shake_tr.update(&rho);
+ for i in 0..K {
+ let mut t1_poly_bytes = [0u8; 320]; // 32 * 10 = 320
+ poly_simple_bit_pack_t1(&t1.vec[i], &mut t1_poly_bytes);
+ shake_tr.update(&t1_poly_bytes);
+ }
+ let tr_buf = shake_tr.finalize(64)?;
+ let mut tr = [0u8; 64];
+ tr.copy_from_slice(tr_buf.as_slice());
+
+ let pk = MLDSAPublicKey { rho, t1 };
+ let sk = MLDSAPrivateKey {
+ rho,
+ k_seed,
+ tr,
+ s1: s1_original,
+ s2,
+ t0,
+ };
+
+ Ok((pk, sk))
+}
+
+//
+// Algorithm 22/23: pkEncode / pkDecode
+//
+
+impl MLDSAPublicKeyTrait for MLDSAPublicKey {
+ /// Algorithm 22: pkEncode(ρ, t1)
+ ///
+ /// pk = ρ (32B) || SimpleBitPack(t1[0], 10) || ... || SimpleBitPack(t1[K-1], 10)
+ /// t1 계수당 10비트, 다항식당 320바이트, 총 PK_LEN = 32 + 320K 바이트.
+ fn pk_encode(&self) -> [u8; PK_LEN] {
+ assert_eq!(
+ PK_LEN,
+ 32 + 320 * K,
+ "pkEncode: PK_LEN이 파라미터 셋과 일치하지 않습니다"
+ ); // todo: 어썰션은 중요한데 이렇게 잡아주는게 좋을지... 다른 인/디코딩 함수도 동일
+
+ let mut pk = [0u8; PK_LEN];
+
+ // 1. ρ (32바이트)
+ pk[..32].copy_from_slice(&self.rho);
+
+ // 2. SimpleBitPack(t1[i], 10) (다항식당 320바이트)
+ for i in 0..K {
+ poly_simple_bit_pack_t1(&self.t1.vec[i], &mut pk[32 + i * 320..32 + (i + 1) * 320]);
+ }
+
+ pk
+ }
+
+ /// Algorithm 23: pkDecode(pk)
+ ///
+ /// pkEncode의 역연산. ρ와 t1을 복원합니다.
+ fn pk_decode(pk: &[u8; PK_LEN]) -> Self {
+ assert_eq!(
+ PK_LEN,
+ 32 + 320 * K,
+ "pkDecode: PK_LEN이 파라미터 셋과 일치하지 않습니다"
+ );
+
+ let mut rho = [0u8; SEED_LEN];
+ rho.copy_from_slice(&pk[..32]);
+
+ let mut t1 = PolyVec::::new_zero();
+ for i in 0..K {
+ t1.vec[i] = poly_simple_bit_unpack_t1(&pk[32 + i * 320..32 + (i + 1) * 320]);
+ }
+
+ Self { rho, t1 }
+ }
+}
+
+//
+// Algorithm 24/25: skEncode / skDecode
+//
+
+impl
+ MLDSAPrivateKeyTrait