From 86f9a937a4bc295ce3f8c6a003e9691a4ec3e29a Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:00:45 +0900 Subject: [PATCH 01/65] =?UTF-8?q?rng=20=EB=AA=A8=EB=93=88=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=83=9D=EC=84=B1=20(=EC=98=81=EB=AC=B8=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rng/README.md | 246 +++++++++++++++++++++++++++++++++++++++++++++++ rng/README_EN.md | 246 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 rng/README.md create mode 100644 rng/README_EN.md diff --git a/rng/README.md b/rng/README.md new file mode 100644 index 0000000..5230ad1 --- /dev/null +++ b/rng/README.md @@ -0,0 +1,246 @@ +# 난수 생성 (Random Number Generation, RNG) 모듈 + +[![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) + +암호 시스템의 모든 비밀은 결국 하나의 난수원으로 수렴합니다. 키, IV, nonce, Capability 토큰이 예측 가능해지는 순간 그 위의 모든 표준 준수는 무의미해집니다. 이 문서는 `rng` 크레이트가 [NIST SP 800-90A Rev. 1](https://csrc.nist.gov/pubs/sp/800/90/a/r1/final) Hash_DRBG와 OS 엔트로피 어댑터를 어떻게 구현했는지, 그리고 그 보안 근거가 기계어 수준에서 어디에 뿌리내리고 있는지를 기술적으로 설명합니다. + +--- + +## 제공하는 기능 + +크레이트는 두 계층으로 구성됩니다. 결정론적 비트 생성기(Deterministic Random Bit Generator, DRBG)와 그 시드를 공급하는 OS 엔트로피 어댑터입니다. + +### Hash_DRBG (`hash_drbg.rs`) + +NIST SP 800-90A Rev. 1, Section 10.1의 Hash_DRBG를 SHA-2의 4종류로 인스턴스화합니다. `impl_hash_drbg!` 매크로가 표준 Table 2 파라미터만 바꿔 동일 로직을 생성하므로 변형 간 동작 차이가 없습니다. + +| 타입 | 해시 | 보안 강도 | outlen | seedlen | 최소 엔트로피 | +|------------------|---------|----------|--------|---------|-----------| +| `HashDRBGSHA224` | SHA-224 | 112 bits | 28 B | 55 B | 14 B | +| `HashDRBGSHA256` | SHA-256 | 128 bits | 32 B | 55 B | 16 B | +| `HashDRBGSHA384` | SHA-384 | 192 bits | 48 B | 111 B | 24 B | +| `HashDRBGSHA512` | SHA-512 | 256 bits | 64 B | 111 B | 32 B (권장) | + +표준 알고리즘 세 개를 그대로 노출합니다. + +- `Instantiate` (10.1.1.2): `V = Hash_df(entropy || nonce || personalization, seedlen)`, `C = Hash_df(0x00 || V, seedlen)`, `reseed_counter = 1`. +- `Reseed` (10.1.1.3): `V = Hash_df(0x01 || V || entropy || additional_input, seedlen)` 후 `C` 재유도, 카운터 1로 리셋. +- `Generate` (10.1.1.4): `additional_input` 반영(`w` 경로) -> `Hashgen` -> `V = (V + H + C + reseed_counter) mod 2^seedlen`. + +공개 API는 엔트로피 출처에 따라 두 쌍으로 나뉩니다. + +| 경로 | 초기화 | 재시드(Reseed) | 엔트로피 출처 | +|------|-------------------------------|------------------|--------------------------| +| 권장 | `new_from_os` | `reseed_from_os` | OS CSPRNG (내부 수집) | +| 베어메탈 | `new_from_entropy` (`unsafe`) | `reseed` | 호출자 주입 (RDSEED 및 TRNG 등) | + +`new_from_os` 계열은 호출자가 엔트로피를 만질 수 없어 예측 가능한 시드 주입 경로를 원천 차단합니다. `new_from_entropy`는 OS가 없는 환경을 위한 탈출구라 `unsafe`로 표시되고, 엔트로피 품질/독립성/재사용 금지 책임을 호출자에게 명시적으로 넘깁니다. + +```rust,ignore +use rng::{HashDRBGSHA256, DrbgError}; + +// 호스트 측: OS CSPRNG로 시드 (권장) +let mut drbg = HashDRBGSHA256::new_from_os(Some(b"host-boot-id"))?; +let mut out = [0u8; 128]; +drbg.generate(&mut out, None)?; + +// ReseedRequired 수신 시 OS 엔트로피로 안전하게 재시드 +drbg.reseed_from_os(None)?; +``` + +### OS 엔트로피 어댑터 (`os_entropy.rs`) + +플랫폼별 CSPRNG를 직접 호출하는 얇은 어댑터입니다. 외부 crate(`getrandom`, `rand`) 없이 syscall 또는 libc 심볼에 직접 연결합니다(Zero-Trust). `fill_bytes`, `get_bytes::`, `extract_os_entropy`(crate 내부) 세 진입점을 제공합니다. + +### SecureBuffer (`lib.rs`) + +DRBG 내부 상태(V, C)와 수집된 엔트로피를 담는 고정 128바이트 스택 버퍼입니다. `alloc` 없이(`no_std`) 동작하며 Drop/`zeroize` 시 활성 영역이 아니라 backing storage 전체를 휘발성 쓰기로 소거합니다. + +--- + +## 엔트로피 수집의 어셈블리 측면 근거 + +이 크레이트에서 "기계어 수준" 보안이 가장 직접적으로 드러나는 지점은 엔트로피 syscall입니다. 표준 라이브러리나 외부 crate를 거치지 않고 인-라인 어셈블리로 커널에 직접 진입하므로, 무엇이 클로버(clobber)되고 무엇이 보존되는지가 소스에 그대로 박혀 있습니다. + +### 1. 직접 syscall, 중간 계층 없음 + +Linux x86_64의 `getrandom(2)` 호출은 다음과 같습니다. + +```rust +core::arch::asm!( + "syscall", + inlateout("rax") SYS_GETRANDOM => ret, // 318 + in("rdi") ptr, + in("rsi") len, + in("rdx") 0u32, // flags = 0 (블로킹) + lateout("rcx") _, // syscall이 복귀 RIP 저장 -> 클로버 + lateout("r11") _, // syscall이 RFLAGS 저장 -> 클로버 + options(nostack), +); +``` + +Linux x86_64 syscall ABI 그대로입니다. 번호는 `rax`, 인자는 `rdi`/`rsi`/`rdx`, 반환은 `rax`. `syscall` 명령은 복귀 주소를 `rcx`에, 플래그를 `r11`에 덮어쓰므로 두 레지스터를 `lateout(_)`로 클로버 선언해 컴파일러가 살아있는 값을 그곳에 두지 못하게 합니다. aarch64는 동일 구조를 `svc #0` + `x8`(번호) + `x0..x2`(인자)로 표현하며 반환값은 `x0`에 옵니다. + +| 플랫폼 | 메커니즘 | 명령/심볼 | 번호 | +|------------------------|-------------|--------------------------------------|----------------| +| Linux x86_64 | raw syscall | `syscall` | rax = 318 | +| Linux aarch64 | raw syscall | `svc #0` | x8 = 278 | +| FreeBSD x86_64/aarch64 | raw syscall | `syscall`/`svc #0` | 563 | +| OpenBSD x86_64/aarch64 | raw syscall | `syscall`/`svc #0` | getentropy = 7 | +| macOS x86_64/aarch64 | libc | `getentropy` | (심볼 링크) | +| NetBSD x86_64 | libc | `getentropy` | (심볼 링크) | +| Windows x86_64 | advapi32 | `RtlGenRandom` (`SystemFunction036`) | (심볼 링크) | + +macOS/NetBSD만 raw syscall 대신 libc `getentropy` 심볼에 링크합니다. 이 두 플랫폼은 syscall 번호가 안정 ABI가 아니어서 직접 호출이 깨질 수 있기 때문이며, 불가피한 예외로 명시합니다. 나머지 Linux/BSD는 번호가 안정적이라 raw syscall을 씁니다. + +### 2. `options(nomem)`를 쓰지 않는 이유 (정확성이 곧 보안) + +상수-시간 프리미티브(`constant-time` 크레이트)의 asm 블록은 `options(nomem, nostack)`으로 메모리 비접근을 선언합니다. 그러나 엔트로피 syscall은 `nostack`만 선언하고 `nomem`은 **의도적으로 생략**합니다. + +커널이 `ptr`가 가리키는 사용자 버퍼에 난수를 써넣기 때문입니다. 즉 이 asm은 포인터를 통해 메모리를 변경합니다. 여기에 `nomem`을 붙이면 "이 asm은 메모리를 읽거나 쓰지 않는다"고 컴파일러에 거짓을 알리는 것이고, 최적화기가 버퍼 채움을 죽은 코드로 제거하거나 인접 메모리 연산과 재정렬할 수 있어 정의되지 않은 동작(UB)이 됩니다. `nomem`의 생략은 누락이 아니라, 버퍼가 실제로 채워졌음을 컴파일러가 신뢰하도록 만드는 정확성 보장입니다. + +### 3. 블로킹 모드와 부분 읽기 처리 + +`flags = 0`은 `GRND_NONBLOCK`도 `GRND_RANDOM`도 켜지 않습니다. + +- `GRND_RANDOM` 미설정: 고갈되기 쉬운 블로킹 `/dev/random` 풀이 아니라, 한 번 초기화되면 고갈되지 않는 CSPRNG(`/dev/urandom` 의미론)에서 뽑습니다. +- `GRND_NONBLOCK` 미설정: 부팅 직후 풀이 아직 시드되지 않았다면 시드될 때까지 블로킹합니다. 약한 엔트로피로 조용히 진행하는 것보다 기다리는 쪽을 택합니다. + +반환값 처리도 기계어 ABI에 맞춰 방어적입니다. + +- `ret < 0`이고 `-4`(EINTR): 시그널로 중단됐을 뿐이므로 루프 재시도. +- `ret < 0` 그 외: `OsEntropyFailed` 반환. +- `ret == 0`: getrandom 계약상 정상 경로에선 나오지 않지만, 무한 루프/과소 시드를 막기 위해 실패로 처리. +- `0 < ret < len`: 부분 읽기. `offset += ret`로 남은 만큼 다시 요청. + +`getentropy` 경로(macOS/BSD)는 호출당 256바이트 상한이 있어 `min(remaining, 256)`으로 잘라 반복합니다. + +### 4. 베어메탈 기본 타겟에서의 부재 + +워크스페이스 기본 빌드 타겟은 `x86_64-unknown-none`입니다. 이 타겟에서는 위 어떤 `cfg`도 만족되지 않아 `mod sys`가 항상 `Err(OsEntropyFailed)`만 반환하는 fallback으로 컴파일됩니다. 즉 실제 마이크로커널 Ring-3 데몬 배포본에서 OS 엔트로피는 비가용이며, 시드는 `new_from_entropy`를 통해 커널 또는 하드웨어 TRNG에서 주입되어야 합니다. 위 플랫폼별 syscall 경로는 주로 호스트 측 빌드/테스트를 위한 것입니다. + +--- + +## 메모리 소거의 어셈블리 측면 근거 + +DRBG의 핵심 가치는 "한 요청/한 데이터/즉시 소거"입니다. 내부 상태 V/C와 중간 계산값이 사용 후 메모리에 남으면 메모리 덤프나 잔존(remanence) 분석으로 출력 전체가 역산될 수 있습니다. 이 크레이트는 두 가지 도구로 잔존을 차단합니다. + +### 1. SecureBuffer 전체 영역 소거 + +`SecureBuffer::zeroize`는 활성 길이 `len`만이 아니라 backing `[u8; 128]` 전체를 소거합니다. + +```rust +fn zeroize(&mut self) { + self.data.zeroize(); // 활성 영역 밖 잔존 바이트까지 + self.len.zeroize(); +} +``` + +과거 더 큰 `len`으로 쓰였다가 줄어든 경우 비활성 영역에 남는 잔존 바이트를 함께 제거하기 위함입니다. 실제 소거는 `zeroize` 크레이트가 담당하며, 그 핵심은 컴파일러가 죽은 쓰기로 제거하지 못하도록 휘발성 쓰기(`volatile_write`) 후 아키텍처별 메모리/컴파일러 배리어(x86_64/aarch64 인-라인 어셈블리, 그 외 `compiler_fence`)를 거는 것입니다. 평범한 `self.data = [0; 128]` 대입은 LLVM의 죽은 저장 제거(DSE)로 사라질 수 있으나, 휘발성 쓰기는 기계어 수준에서 store가 반드시 방출되도록 강제합니다. + +`Drop`이 이 `zeroize`를 호출하므로 정상 종료든 패닉(`panic = "abort"` 환경)이든 스코프를 벗어나는 모든 경로에서 backing storage가 0이 됩니다. + +### 2. 스택 중간값은 Secret으로 + +`hash_df`/`hashgen`/`generate`/`reseed`는 V를 스택 배열에 복사해 가공합니다. 이 복사본은 `zeroize::Secret`으로 감쌉니다. + +```rust +let mut data = Secret::new([0u8; $seedlen]); +data.expose_mut().copy_from_slice(self.v.as_slice()); +// ... 사용 ... +// Drop 시 휘발성 쓰기 + 배리어로 자동 소거 +``` + +`Secret`이 중요한 이유는 `?` 조기 반환과 패닉 경로입니다. `hash_df`가 중간에 `InvalidArgument`로 빠져나가도 `Secret`의 `Drop`이 보장되어, 부분 계산된 `new_v`/`new_c`가 평문으로 스택에 남지 않습니다. `generate` 안의 `w_padded`/`h_padded`/`c_copy`/`data`가 모두 같은 방식으로 보호됩니다. + +### 3. reseed_counter와 필드 Drop 순서 + +DRBG의 `Drop` 본문은 `reseed_counter`만 명시적으로 소거합니다. + +```rust +impl Drop for $struct_name { + fn drop(&mut self) { + self.reseed_counter.zeroize(); + } +} +``` + +V/C는 `SecureBuffer` 자신의 `Drop`이 소거하며, Rust가 `Drop::drop` 본문 실행 후 필드를 선언 순서(`v`, `c`, `reseed_counter`)대로 떨어뜨리므로 별도 호출 없이 둘 다 소거됩니다. 카운터는 정수라 자체 Drop이 없어 본문에서 직접 처리합니다. + +참고로 이 환경에는 OS 페이지 잠금(`mlock`)이 없습니다. 베어메탈/`no_std`에는 잠글 OS가 없기 때문입니다. 따라서 메모리 보호는 페이지 잠금이 아니라 "사용 직후 휘발성 소거"로 잔존 노출 창을 최소화하는 데 의존합니다. + +--- + +## 상수-시간 불변식 + +DRBG의 위협 모델에서 보호 대상은 **내부 상태 V/C**이고, `generate`의 반환값은 공개입니다. 따라서 출력 자체의 타이밍 보호는 불필요하지만, 내부 상태를 다루는 산술이 그 값에 따라 분기하면 상태가 새어 나갈 수 있습니다. 상태를 만지는 두 산술은 값-독립적입니다. + +### add_mod/add_u64_mod: 값에 무관한 모듈러 덧셈 + +`V = (V + w) mod 2^seedlen` 같은 갱신은 big-endian 모듈러 덧셈으로 구현됩니다. + +```rust +fn add_mod(dst: &mut [u8], src: &[u8]) { + let mut carry: u16 = 0; + 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; // 분기 없는 자리올림 + } +} +``` + +- 반복 횟수는 항상 `dst.len()`(= seedlen, 공개 상수)입니다. 비밀 값에 따라 늘거나 줄지 않습니다. +- 자리올림은 `if carry > 0` 같은 분기가 아니라 `u16` 산술과 `>> 8` 마스킹으로만 처리됩니다. 기계어 수준에서 조건 분기(`jcc`/`b.cc`)가 생기지 않습니다. +- 메모리 접근은 인덱스 선형 증가뿐이라 비밀 의존 캐시/TLB 타이밍이 없습니다. + +`add_u64_mod`의 `if i < 8` 분기는 비밀 값이 아니라 **인덱스**(공개 상수)에 의존하며, 더하는 `val`은 비밀이 아닌 단조 증가 `reseed_counter`입니다. 따라서 이 분기의 타이밍 관찰은 위협이 되지 않습니다. + +### Hashgen: 공개 길이에만 의존 + +`hashgen`의 루프 횟수는 `ceil(requested_bytes / outlen)`로 요청 길이(공개)에만 의존하고 비밀 V의 값에 무관합니다. 해시 입력 크기가 고정이라 해시 연산 시간도 V 값과 무관합니다. + +> 다만 이 산술의 상수-시간성은 `constant-time` 크레이트의 검증된 인-라인 어셈블리 프리미티브가 아니라 일반 Rust 산술의 분기 부재에 기댑니다. 최적화기가 이 패턴을 분기로 재구성하지 않으리라는 보장은 `constant-time`의 asm 경로만큼 강하지 않습니다. **분기가 비밀이 아닌 카운터/공개 길이에만 걸려 있어 위협면이 좁다는 점이 현재의 근거**입니다. + +--- + +## 표준 준수와 경계값 + +NIST SP 800-90A Rev. 1 Table 2의 경계가 코드로 강제됩니다. + +| 항목 | 값 | 위반 시 | +|-----------|----------------------------------|------------------------------------------------| +| 최소 엔트로피 | security_strength bytes | `EntropyTooShort` | +| 최대 입력 길이 | 2^32 bytes (2^35 bits) | `EntropyTooLong`/`InputTooLong`/`NonceTooLong` | +| 최소 nonce | security_strength / 2 bytes | `NonceTooShort` | +| 요청당 최대 출력 | 65536 bytes (2^16 B = 2^19 bits) | `RequestTooLarge` | +| 재시드 간격 | 2^48 | `ReseedRequired` | + +추가적으로 다음 특징을 가집니다. + +- **엔트로피 2배 수집**: `new_from_os`/`reseed_from_os`는 `2 * security_strength` 바이트를 모읍니다(Section 8.6.7). +- **nonce 독립성**: entropy와 nonce를 OS에 대한 **별개 호출**로 수집해 상관을 없앱니다. +- **Hash_df 카운터 wrap 가드**: `Hash_df`의 1바이트 카운터가 넘칠 `m > 255` 입력을 `InvalidArgument`로 거부합니다. 현재 비즈니스 로직 상으로는 절대 발생할 수 없는 문제이지만, 입력 단계에서 엄격한 방어벽을 형성합니다. +- **`additional_input`의 Null 구분**: 표준상 `additional_input != Null`이면 길이 0이어도 `w` 경로가 V를 갱신해야 합니다. `Some(&[])`와 `None`을 구분해 전자는 갱신, 후자만 건너뜁니다. + +--- + +## 설계 한계와 주의 + +알고 있는 약점을 명시합니다. + +### OS CSPRNG는 SP 800-90B 검증 엔트로피원이 아니다 + +본 모듈이 노출하는 것은 OS CSPRNG(`getrandom`/`getentropy`/`RtlGenRandom`) 출력입니다. 이는 SP 800-90B로 검증된(헬스 테스트를 갖춘) 엔트로피 소스가 아니라 SP 800-90C 관점의 RBG 시드원입니다. FIPS 인증 시 엔트로피 평가는 플랫폼 RBG 보증에 의존합니다. + +### 베어메탈에서 OS 엔트로피는 비가용 + +기본 타겟에서 OS 경로는 컴파일되지 않습니다. 시드는 반드시 외부(`new_from_entropy`)에서 주입해야 하며, 약한 엔트로피 주입은 DRBG 출력을 예측 가능하게 만들어 상위 계층(키/토큰/IV) 전체를 붕괴시킵니다. 그래서 그 경로가 `unsafe`입니다. + +### 예측 저항(prediction resistance) 미구현 + +`generate`는 호출마다 새 엔트로피를 끌어오지 않습니다. 예측 저항이 필요하면 `reseed_from_os`를 호출 전에 명시적으로 실행해야 합니다. + +### macOS/NetBSD의 libc 의존 + +이 두 플랫폼은 raw syscall 대신 libc `getentropy`에 링크합니다. syscall 번호가 안정 ABI가 아니어서 불가피하며, Zero-Trust의 "외부 의존성 0" 원칙에서 OS 표준 C 런타임 심볼 하나에만 한정된 예외입니다. \ No newline at end of file diff --git a/rng/README_EN.md b/rng/README_EN.md new file mode 100644 index 0000000..190208f --- /dev/null +++ b/rng/README_EN.md @@ -0,0 +1,246 @@ +# Random Number Generation (RNG) Module + +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) + +Every secret in a cryptographic system ultimately converges to a single source of randomness. The moment a key, IV, nonce, or capability token becomes predictable, all the standards compliance built on top of it becomes meaningless. This document technically describes how the `rng` crate implements [NIST SP 800-90A Rev. 1](https://csrc.nist.gov/pubs/sp/800/90/a/r1/final) Hash_DRBG and the OS entropy adapter, and where the security rationale is rooted at the machine-instruction level. + +--- + +## Provided Functionality + +The crate is organized into two layers: a Deterministic Random Bit Generator (DRBG) and the OS entropy adapter that feeds it seeds. + +### Hash_DRBG (`hash_drbg.rs`) + +Instantiates Hash_DRBG from NIST SP 800-90A Rev. 1, Section 10.1 with four SHA-2 variants. The `impl_hash_drbg!` macro generates identical logic varying only the standard Table 2 parameters, so there is no behavioral difference between variants. + +| Type | Hash | Security Strength | outlen | seedlen | Min Entropy | +|------------------|---------|----------|--------|---------|-----------| +| `HashDRBGSHA224` | SHA-224 | 112 bits | 28 B | 55 B | 14 B | +| `HashDRBGSHA256` | SHA-256 | 128 bits | 32 B | 55 B | 16 B | +| `HashDRBGSHA384` | SHA-384 | 192 bits | 48 B | 111 B | 24 B | +| `HashDRBGSHA512` | SHA-512 | 256 bits | 64 B | 111 B | 32 B (recommended) | + +The three standard algorithms are exposed as-is. + +- `Instantiate` (10.1.1.2): `V = Hash_df(entropy || nonce || personalization, seedlen)`, `C = Hash_df(0x00 || V, seedlen)`, `reseed_counter = 1`. +- `Reseed` (10.1.1.3): `V = Hash_df(0x01 || V || entropy || additional_input, seedlen)`, re-derive `C`, reset counter to 1. +- `Generate` (10.1.1.4): apply `additional_input` (`w` path) -> `Hashgen` -> `V = (V + H + C + reseed_counter) mod 2^seedlen`. + +The public API is split into two pairs depending on the entropy source. + +| Path | Initialization | Reseed | Entropy Source | +|------|-------------------------------|------------------|--------------------------| +| Recommended | `new_from_os` | `reseed_from_os` | OS CSPRNG (collected internally) | +| Bare-metal | `new_from_entropy` (`unsafe`) | `reseed` | Caller-injected (RDSEED, TRNG, etc.) | + +The `new_from_os` family blocks the caller from touching entropy directly, eliminating any predictable seed injection path at the source. `new_from_entropy` is an escape hatch for environments without an OS, marked `unsafe` to explicitly transfer the responsibility for entropy quality, independence, and non-reuse to the caller. + +```rust,ignore +use rng::{HashDRBGSHA256, DrbgError}; + +// Host side: seed with OS CSPRNG (recommended) +let mut drbg = HashDRBGSHA256::new_from_os(Some(b"host-boot-id"))?; +let mut out = [0u8; 128]; +drbg.generate(&mut out, None)?; + +// On receiving ReseedRequired, safely reseed with OS entropy +drbg.reseed_from_os(None)?; +``` + +### OS Entropy Adapter (`os_entropy.rs`) + +A thin adapter that directly calls platform-specific CSPRNGs. Connects directly to syscalls or libc symbols without external crates (`getrandom`, `rand`) — Zero-Trust. Provides three entry points: `fill_bytes`, `get_bytes::`, and `extract_os_entropy` (crate-internal). + +### SecureBuffer (`lib.rs`) + +A fixed 128-byte stack buffer that holds DRBG internal state (V, C) and collected entropy. Operates without `alloc` (`no_std`), and on Drop/`zeroize` performs a volatile write wipe across the entire backing storage, not just the active region. + +--- + +## Assembly-Level Rationale for Entropy Collection + +The point where "machine-instruction level" security is most directly visible in this crate is the entropy syscall. It enters the kernel directly via inline assembly without going through the standard library or external crates, so what gets clobbered and what gets preserved is spelled out directly in the source. + +### 1. Direct Syscall — No Intermediate Layer + +The `getrandom(2)` call on Linux x86_64 is as follows. + +```rust +core::arch::asm!( + "syscall", + inlateout("rax") SYS_GETRANDOM => ret, // 318 + in("rdi") ptr, + in("rsi") len, + in("rdx") 0u32, // flags = 0 (blocking) + lateout("rcx") _, // syscall saves return RIP -> clobbered + lateout("r11") _, // syscall saves RFLAGS -> clobbered + options(nostack), +); +``` + +This follows the Linux x86_64 syscall ABI exactly: number in `rax`, arguments in `rdi`/`rsi`/`rdx`, return in `rax`. The `syscall` instruction overwrites `rcx` with the return address and `r11` with flags, so both registers are declared as `lateout(_)` clobbers to prevent the compiler from placing live values there. aarch64 expresses the same structure with `svc #0` + `x8` (number) + `x0..x2` (arguments), with the return value in `x0`. + +| Platform | Mechanism | Instruction/Symbol | Number | +|------------------------|-------------|--------------------------------------|----------------| +| Linux x86_64 | raw syscall | `syscall` | rax = 318 | +| Linux aarch64 | raw syscall | `svc #0` | x8 = 278 | +| FreeBSD x86_64/aarch64 | raw syscall | `syscall`/`svc #0` | 563 | +| OpenBSD x86_64/aarch64 | raw syscall | `syscall`/`svc #0` | getentropy = 7 | +| macOS x86_64/aarch64 | libc | `getentropy` | (symbol link) | +| NetBSD x86_64 | libc | `getentropy` | (symbol link) | +| Windows x86_64 | advapi32 | `RtlGenRandom` (`SystemFunction036`) | (symbol link) | + +Only macOS/NetBSD use the libc `getentropy` symbol instead of a raw syscall. These two platforms have unstable syscall number ABIs where direct calls can break, and this is noted as an unavoidable exception. The remaining Linux/BSD platforms use raw syscalls since their numbers are stable. + +### 2. Why `options(nomem)` Is Not Used (Correctness Is Security) + +The asm blocks in the constant-time primitives (`constant-time` crate) declare `options(nomem, nostack)` to indicate no memory access. However, the entropy syscall deliberately omits `nomem` and only declares `nostack`. + +This is because the kernel writes random bytes into the user buffer pointed to by `ptr` — this asm modifies memory through a pointer. Adding `nomem` here would be a lie to the compiler: "this asm does not read or write memory." The optimizer could then eliminate the buffer fill as dead code or reorder it with adjacent memory operations, resulting in undefined behavior. Omitting `nomem` is not an oversight — it is a correctness guarantee that forces the compiler to trust that the buffer has actually been filled. + +### 3. Blocking Mode and Partial Read Handling + +`flags = 0` sets neither `GRND_NONBLOCK` nor `GRND_RANDOM`. + +- No `GRND_RANDOM`: draws from the CSPRNG (`/dev/urandom` semantics) that never exhausts once initialized, rather than the easily-depleted blocking `/dev/random` pool. +- No `GRND_NONBLOCK`: if the pool has not been seeded yet immediately after boot, it blocks until seeding is complete. Waiting is preferred over silently proceeding with weak entropy. + +Return value handling is also defensive, matching the machine ABI. + +- `ret < 0` and `-4` (EINTR): interrupted by a signal — retry in loop. +- `ret < 0` otherwise: return `OsEntropyFailed`. +- `ret == 0`: should not appear on the normal path per the getrandom contract, but treated as failure to prevent infinite loops or under-seeding. +- `0 < ret < len`: partial read. Retry the remainder with `offset += ret`. + +The `getentropy` path (macOS/BSD) has a 256-byte cap per call, so it slices requests with `min(remaining, 256)` and loops. + +### 4. Absent on Bare-Metal Default Target + +The workspace default build target is `x86_64-unknown-none`. On this target, none of the above `cfg` conditions are satisfied, so `mod sys` always compiles to a fallback that returns `Err(OsEntropyFailed)`. This means OS entropy is unavailable in the actual microkernel Ring-3 daemon deployment, and seeds must be injected via `new_from_entropy` from the kernel or hardware TRNG. The platform-specific syscall paths above are primarily for host-side builds and testing. + +--- + +## Assembly-Level Rationale for Memory Zeroization + +The core value proposition of the DRBG is "one request, one datum, immediate wipe." If internal state V/C and intermediate computed values remain in memory after use, a memory dump or remanence analysis could reverse-engineer the entire output. This crate blocks remanence with two tools. + +### 1. SecureBuffer Full-Region Wipe + +`SecureBuffer::zeroize` wipes the entire backing `[u8; 128]`, not just the active length `len`. + +```rust +fn zeroize(&mut self) { + self.data.zeroize(); // covers bytes beyond the active region too + self.len.zeroize(); +} +``` + +This removes residual bytes that remain in the inactive region when the buffer was previously used with a larger `len` that has since shrunk. The actual wipe is handled by the `zeroize` crate, whose core mechanism is a volatile write (`volatile_write`) followed by an architecture-specific memory/compiler barrier (x86_64/aarch64 inline assembly, otherwise `compiler_fence`) to prevent the compiler from eliminating it. A plain `self.data = [0; 128]` assignment could be removed by LLVM's Dead Store Elimination (DSE), but a volatile write forces the store to be emitted at the machine level. + +`Drop` calls this `zeroize`, so the backing storage is zeroed on every path that exits the scope — whether normal exit or panic (`panic = "abort"` environment). + +### 2. Stack Intermediates as Secret + +`hash_df`/`hashgen`/`generate`/`reseed` copy V to a stack array for processing. This copy is wrapped in `zeroize::Secret`. + +```rust +let mut data = Secret::new([0u8; $seedlen]); +data.expose_mut().copy_from_slice(self.v.as_slice()); +// ... use ... +// automatic wipe via volatile write + barrier on Drop +``` + +The importance of `Secret` is the `?` early-return and panic paths. Even if `hash_df` exits early with `InvalidArgument`, `Secret`'s `Drop` is guaranteed, so partially-computed `new_v`/`new_c` do not remain on the stack in plaintext. All of `w_padded`/`h_padded`/`c_copy`/`data` inside `generate` are protected the same way. + +### 3. `reseed_counter` and Field Drop Order + +The DRBG's `Drop` body explicitly wipes only `reseed_counter`. + +```rust +impl Drop for $struct_name { + fn drop(&mut self) { + self.reseed_counter.zeroize(); + } +} +``` + +V and C are wiped by `SecureBuffer`'s own `Drop`, and Rust drops fields in declaration order (`v`, `c`, `reseed_counter`) after the `Drop::drop` body executes — so both are wiped without an explicit call. The counter is an integer with no own `Drop`, so it is handled directly in the body. + +Note that OS page locking (`mlock`) is absent here. There is no OS to lock against on bare-metal/`no_std`. Therefore, memory protection relies on "volatile wipe immediately after use" to minimize the remanence exposure window, rather than page locking. + +--- + +## Constant-Time Invariants + +In the DRBG threat model, the protected secret is **internal state V/C**, and the return value of `generate` is public. Therefore, timing protection of the output itself is unnecessary, but if the arithmetic that handles internal state branches on its value, state could leak. The two arithmetic operations that touch state are value-independent. + +### add_mod / add_u64_mod: Value-Independent Modular Addition + +Updates like `V = (V + w) mod 2^seedlen` are implemented as big-endian modular addition. + +```rust +fn add_mod(dst: &mut [u8], src: &[u8]) { + let mut carry: u16 = 0; + 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; // branchless carry propagation + } +} +``` + +- The iteration count is always `dst.len()` (= seedlen, a public constant). It does not grow or shrink based on secret values. +- Carry propagation uses `u16` arithmetic and `>> 8` masking rather than a branch like `if carry > 0`. No conditional branch (`jcc`/`b.cc`) is generated at the machine level. +- Memory accesses are linearly indexed only, so there are no secret-dependent cache/TLB timing effects. + +The `if i < 8` branch in `add_u64_mod` depends on an **index** (a public constant), not a secret value, and the `val` being added is the monotonically increasing `reseed_counter`, which is not a secret. Observing the timing of this branch is therefore not a threat. + +### Hashgen: Depends Only on Public Length + +The loop count in `hashgen` is `ceil(requested_bytes / outlen)`, which depends only on the request length (public) and is independent of the secret value of V. The hash input size is fixed, so hash operation timing is also independent of V's value. + +> Note: the constant-time property of this arithmetic relies on the absence of branches in ordinary Rust arithmetic, not on the verified inline-assembly primitives of the `constant-time` crate. There is no guarantee as strong as the `constant-time` asm path that the optimizer will not reconstruct this pattern into a branch. **The current rationale is that any branch is only conditioned on a counter or public length, not a secret, so the threat surface is narrow.** + +--- + +## Standards Compliance and Boundary Values + +The boundary values from NIST SP 800-90A Rev. 1 Table 2 are enforced in code. + +| Parameter | Value | On Violation | +|-----------|----------------------------------|------------------------------------------------| +| Minimum entropy | security_strength bytes | `EntropyTooShort` | +| Maximum input length | 2^32 bytes (2^35 bits) | `EntropyTooLong`/`InputTooLong`/`NonceTooLong` | +| Minimum nonce | security_strength / 2 bytes | `NonceTooShort` | +| Max output per request | 65536 bytes (2^16 B = 2^19 bits) | `RequestTooLarge` | +| Reseed interval | 2^48 | `ReseedRequired` | + +Additional properties: + +- **2x entropy collection**: `new_from_os`/`reseed_from_os` collect `2 * security_strength` bytes (Section 8.6.7). +- **Nonce independence**: entropy and nonce are collected via **separate calls** to the OS to eliminate correlation. +- **Hash_df counter wrap guard**: `Hash_df` rejects inputs where the 1-byte counter would overflow (`m > 255`) with `InvalidArgument`. This cannot occur in current business logic, but forms a strict defensive barrier at the input stage. +- **`additional_input` null distinction**: per the standard, if `additional_input != Null`, even a length-0 value should update V via the `w` path. `Some(&[])` and `None` are distinguished: the former triggers an update, only the latter is skipped. + +--- + +## Design Limitations and Caveats + +Known weaknesses are stated explicitly. + +### OS CSPRNG Is Not an SP 800-90B Validated Entropy Source + +What this module exposes is OS CSPRNG output (`getrandom`/`getentropy`/`RtlGenRandom`). This is not an SP 800-90B validated entropy source (with health tests) — it is an RBG seed source from the SP 800-90C perspective. For FIPS certification, entropy assessment depends on the platform RBG assurances. + +### OS Entropy Is Unavailable on Bare-Metal + +On the default target, the OS path is not compiled. Seeds must be injected externally via `new_from_entropy`, and weak entropy injection makes DRBG output predictable, collapsing the entire upper layer (keys/tokens/IVs). That is why this path is `unsafe`. + +### Prediction Resistance Not Implemented + +`generate` does not pull new entropy on every call. If prediction resistance is required, `reseed_from_os` must be called explicitly before generating. + +### libc Dependency on macOS/NetBSD + +These two platforms link to libc `getentropy` instead of a raw syscall. This is unavoidable because their syscall numbers are not stable ABIs. It is a limited exception to the Zero-Trust "zero external dependencies" principle, confined to a single OS standard C runtime symbol. From d32127801adde057c9fee4be221c8833f312ddd4 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:31:10 +0900 Subject: [PATCH 02/65] =?UTF-8?q?1.1.0=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EC=A4=80=EB=B9=84=20-=20=EA=B5=90=EC=B0=A8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=ED=98=84=ED=99=A9=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20README=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20-=20cross-confirm.md=20=EC=B6=94=EA=B0=80=20(?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EA=B2=80=EC=A6=9D=20=ED=98=84=ED=99=A9=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81)=20-=20README.md=20/=20README=5FEN.md?= =?UTF-8?q?=EC=97=90=20=EB=A6=B4=EB=A6=AC=EC=A6=88=201.1.0=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=20-=20AI=5FSCOPE.md=20/=20AI=5FS?= =?UTF-8?q?COPE=5FEN.md=20=EB=AA=A8=EB=8D=B8=EB=AA=85=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20(Fable=205=20=EB=B0=98=EC=98=81)=20-=20rng?= =?UTF-8?q?/README=5FEN.md=20=EC=B6=94=EA=B0=80=20(=EC=98=81=EB=AC=B8=20?= =?UTF-8?q?=EB=B2=88=EC=97=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AI_SCOPE.md | 2 +- AI_SCOPE_EN.md | 2 +- README.md | 6 +++++- README_EN.md | 4 ++++ cross-confirm.md | 26 ++++++++++++++++++++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 cross-confirm.md diff --git a/AI_SCOPE.md b/AI_SCOPE.md index 5d20978..1873cb8 100644 --- a/AI_SCOPE.md +++ b/AI_SCOPE.md @@ -4,7 +4,7 @@ 저는 이 암호 모듈의 다양한 암호 기능과 기술적인 작동 방식을 상세화한 명세나 모듈과 함수 등의 기능 작동 사항에 대한 다이렉트 주석을 제가 직접 작성하려고 최대한 노력합니다만, 작성된 결과물을 '이해하기 쉽게' 만드는 데(Mermaid 다이어그램을 생성하거나, 명세의 가독성 향상을 위한 맥락, 설명체 등의 간결화 작업과 영문 번역 등) [Claude Code](http://claude.ai/)의 Sonnet 4.6 모델을 적극적으로 사용합니다. -추가적으로, [ISO-LIGHT-K0](https://github.com/Quant-Off/iso-light-k0) 마이크로커널과의 통합을 위해 기능적으로 어떤 부분이 필요한지 점검하고, 누락됐거나 개선이 필요한 부분에 대해 피드백(리뷰) 받고, 암호 알고리즘이 국제적 표준을 준수하는지 확인하기 위해 [NIST FIPS 140-2 또는 3](https://csrc.nist.gov/projects/cryptographic-module-validation-program/fips-140-3-standards)의 명세를 파악하기 위해 Claude Code의 Opus 4.6, 4.8, [Gemini 3.1 Pro](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-1-pro?hl=ko), [Qwen 3.7 Max](https://qwen.ai/blog?id=qwen3.7) 모델을 사용합니다. 모든 작업을 이 분야의 전문가를 통해 교차 검증하는 것을 절대 잊지 않습니다. +추가적으로, [ISO-LIGHT-K0](https://github.com/Quant-Off/iso-light-k0) 마이크로커널과의 통합을 위해 기능적으로 어떤 부분이 필요한지 점검하고, 누락됐거나 개선이 필요한 부분에 대해 피드백(리뷰) 받고, 암호 알고리즘이 국제적 표준을 준수하는지 확인하기 위해 [NIST FIPS 140-2 또는 3](https://csrc.nist.gov/projects/cryptographic-module-validation-program/fips-140-3-standards)의 명세를 파악하기 위해 Claude Code의 Opus 4.8, Fable 5, [Gemini 3.1 Pro](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-1-pro?hl=ko), [Qwen 3.7 Max](https://qwen.ai/blog?id=qwen3.7) 모델을 사용합니다. 모든 작업을 이 분야의 전문가를 통해 교차 검증하는 것을 절대 잊지 않습니다. 이 프로젝트는 1인 개발임에 따라 '문서 작업', '주석 작업'에 에이전트를 적극 활용하고 있습니다. 에이전트가 이 프로젝트의 파일을 수정하는 부분은 `.md` 파일과 작성된 모듈, 함수의 주석 추가를 위한 `.rs` 파일입니다. **민감한 기능**(암호 알고리즘의 구현 등)에 대해서는 절대적으로 접근할 수 없도록 제한하고 있습니다. 저는 바이브 코더가 아니며, 다소 구시대적인 개발자일 뿐입니다. 이 명시는 AI를 활용하여 다양한 자동화 작업이나, 개발, 보안 감사를 진행하는 사람들에 대해 비아냥거리기 위해 작성된 것이 아닙니다. 발전하는 AI 시대에 맞게 AI 사용 범위를 인간을 위해 명시했을 뿐입니다. diff --git a/AI_SCOPE_EN.md b/AI_SCOPE_EN.md index 015e99e..cc1ac2c 100644 --- a/AI_SCOPE_EN.md +++ b/AI_SCOPE_EN.md @@ -4,7 +4,7 @@ I personally write detailed specifications covering the cryptographic features and technical workings of this module, along with inline comments on the behavior of individual modules and functions. That said, I actively use the [Claude Code](http://claude.ai/) Sonnet 4.6 model to make the produced artifacts **easier to understand** — generating Mermaid diagrams, condensing context and explanations for improved readability in specifications, and handling English translation. -In addition, I use Opus 4.6 / 4.8 (via Claude Code), [Gemini 3.1 Pro](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-1-pro?hl=en), and [Qwen 3.7 Max](https://qwen.ai/blog?id=qwen3.7) to review which functional pieces are needed for integration with the [ISO-LIGHT-K0](https://github.com/Quant-Off/iso-light-k0) microkernel, receive feedback on missing or improvable areas, and cross-check whether the cryptographic algorithms comply with international standards such as [NIST FIPS 140-2 / 140-3](https://csrc.nist.gov/projects/cryptographic-module-validation-program/fips-140-3-standards). All results are cross-validated by human domain experts. +In addition, I use Opus 4.8 / Fable 5 (via Claude Code), [Gemini 3.1 Pro](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-1-pro?hl=en), and [Qwen 3.7 Max](https://qwen.ai/blog?id=qwen3.7) to review which functional pieces are needed for integration with the [ISO-LIGHT-K0](https://github.com/Quant-Off/iso-light-k0) microkernel, receive feedback on missing or improvable areas, and cross-check whether the cryptographic algorithms comply with international standards such as [NIST FIPS 140-2 / 140-3](https://csrc.nist.gov/projects/cryptographic-module-validation-program/fips-140-3-standards). All results are cross-validated by human domain experts. Because this is a solo project, I rely heavily on AI agents for **documentation and comment writing**. Agents are permitted to modify only `.md` files and `.rs` files, and solely for the purpose of adding comments to existing modules and functions. Access to **sensitive logic** — such as cryptographic algorithm implementations — is strictly restricted. I am not a vibe coder; I am simply a developer who still believes in doing things the old-fashioned way. This statement is not meant to ridicule those who use AI for automation, development, or security auditing. It is simply a disclosure of where AI assistance ends and human judgment begins, for the benefit of anyone reading this project. diff --git a/README.md b/README.md index bc94c3e..d88518c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,11 @@ 여러분은 언제나 프로젝트를 소개한 문서인 [INTRODUCTION.md](INTRODUCTION.md) 파일을 참고할 수 있습니다. -또한, 이 프로젝트에서 AI 에이전트가 어떤 범위에 맞춰 사용되고, 실제로 수정하는 부분과 사용되는 스킬 및 프롬프트는 무엇인지에 관해 [AI_SCOPE.md](AI_SCOPE.md) 문서에 작성했습니다. +또한, 이 프로젝트에서 AI 에이전트가 어떤 범위에 맞춰 사용되고, 실제로 수정하는 부분과 사용되는 스킬 및 프롬프트는 무엇인지에 관해 [AI_SCOPE.md](AI_SCOPE.md) 문서에 작성했습니다. + +# 릴리즈 `1.1.0` + +과거 `1.0.0` 릴리즈에서 미처 발견하지 못 했던 몇 가지 문제와 컴플라이언스 측면에서의 구현 오류를 바로잡기 위해 릴리즈를 준비 중에 있습니다. 이 작업은 세밀하고 체계적인 교차 검증 작업을 기반으로 수행되며, 그 기록은 [이 문서](cross-confirm.md)에서 확인하실 수 있습니다. 해당 문서는 `1.1.0` 릴리즈가 공개되면 제거됩니다. # 라이선스 diff --git a/README_EN.md b/README_EN.md index 9378e97..77af0fb 100644 --- a/README_EN.md +++ b/README_EN.md @@ -16,6 +16,10 @@ You can always refer to the [INTRODUCTION_EN.md](INTRODUCTION_EN.md) file for a Additionally, information about what scope AI agents are used for in this project, what parts they actually modify, and the skills and prompts used is documented in [AI_SCOPE_EN.md](AI_SCOPE_EN.md). +# Release `1.1.0` + +We are preparing a release to correct several issues and compliance-related implementation errors that were not discovered in the previous `1.0.0` release. This work is performed based on meticulous and systematic cross-validation, and the records can be found in [this document (Korean)](cross-confirm.md). That document will be removed once the `1.1.0` release is published. + # License This project is under the [MIT LICENSE](LICENSE). diff --git a/cross-confirm.md b/cross-confirm.md new file mode 100644 index 0000000..1b65369 --- /dev/null +++ b/cross-confirm.md @@ -0,0 +1,26 @@ +# 구현 검증 현황 + +> 타겟 릴리즈: `1.1.0` +> 작업 브랜치: `feature/1.1.0-verify` + +이 검증 작업은 4명의 보안 및 컴플라이언스 종사자(전문가)와 Claude Fable 5 모델을 통해 수행됩니다. 다음 목록은 현황을 명시한 것입니다. + +- [ ] AES +- [ ] BLAKE +- [X] SHA2 +- [ ] SHA3 +- [ ] ChaCha20 +- [ ] Ed448 +- [ ] Ed25519 +- [ ] X448 +- [ ] X25519 +- [ ] ML-DSA +- [ ] ML-KEM +- [X] RNG +- [X] Constant-Time +- [X] Zeroize + +여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "담당 명세에 따라 정확하게 구현되어 있으며, 정상적으로 동작한다." 는 것을 명시하는 것입니다. + +> [!IMPORTANT] +> 말인 즉슨, 이 검증 작업이 완료되었다고 해서 이 암호 모듈이 완전해진 것은 아니라는 것입니다. 암호 모듈 검증을 위해선 전문 검증 단체의 승인(CMVP)이 필요하고, 이 작업은 수 개월에서 많게는 수 년까지 소요될 수 있습니다. From 20c1297321ce13a116e05ea721fd58f0e613e462 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:19:52 +0900 Subject: [PATCH 03/65] =?UTF-8?q?aes=20=ED=81=AC=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94=20=EB=B0=8F?= =?UTF-8?q?=201.1.0=20=EB=B2=84=EC=A0=84=20=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - block.rs: encrypt_block/decrypt_block state 소거, sub_bytes/inv_sub_bytes bytes 소거 - cbc.rs: debug_assert -> assert 로 교체, 루프 내 block/decrypted 소거 - ctr.rs: keystream 소거, counter 소거, CTR_MAX_INPUT_LEN 가드 추가 - gcm.rs: 상수시간 태그 비교(CtEqOps), keystream/cb/j0/icb/s/e_j0 소거, 입력 길이 가드 추가 - ghash.rs: reset()에서 state_n.zeroize() 사용 - key.rs: expand_key 시그니처 변경(반환값 제거 -> &mut 출력), temp 소거 - sbox.rs: bitslice plane q 소거 - lib.rs: Secret 내부에서 직접 키 확장 - Cargo.toml: workspace version 1.0.0 -> 1.1.0 --- Cargo.toml | 10 +++---- README.md | 3 -- aes/src/block.rs | 11 ++++++-- aes/src/cbc.rs | 26 ++++++++++++++---- aes/src/ctr.rs | 29 +++++++++++++++++--- aes/src/gcm.rs | 71 +++++++++++++++++++++++++++++++++++++++--------- aes/src/ghash.rs | 4 +-- aes/src/key.rs | 8 ++---- aes/src/lib.rs | 6 ++-- aes/src/sbox.rs | 5 ++++ 10 files changed, 131 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1def578..7a3c298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ members = [ resolver = "3" [workspace.package] -version = "1.0.0" +version = "1.1.0" edition = "2024" authors = ["Q. T. Felix "] # 루트 LICENSE (MIT) 와 동기화. SPDX 식별자로 명시해 다운스트림 @@ -37,17 +37,17 @@ lto = true # Link Time Optimization을 통한 바이너리 최소화 opt-level = "z" # 크기 최적화 [workspace.dependencies] -zeroize = { path = "zeroize", version = "1.0.0" } -constant-time = { path = "constant-time", version = "1.0.0" } +zeroize = { path = "zeroize", version = "1.1.0" } +constant-time = { path = "constant-time", version = "1.1.0" } sha2 = { path = "sha2", version = "1.0.0" } sha3 = { path = "sha3", version = "1.0.0" } blake = { path = "blake", version = "1.0.0" } -rng = { path = "rng", version = "1.0.0" } +rng = { path = "rng", version = "1.1.0" } ed25519 = { path = "ed25519", version = "1.0.0" } ed448 = { path = "ed448", version = "1.0.0" } x25519 = { path = "x25519", version = "1.0.0" } x448 = { path = "x448", version = "1.0.0" } -aes = { path = "aes", version = "1.0.0" } +aes = { path = "aes", version = "1.1.0" } chacha20 = { path = "chacha20", version = "1.0.0" } mldsa = { path = "mldsa", version = "1.0.0" } mlkem = { path = "mlkem", version = "1.0.0" } diff --git a/README.md b/README.md index d88518c..f03e3e5 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,6 @@ `entlib-native` 암호 모듈의 NT을 타게팅한 만큼 100% Rust 언어로 작성되며, 가벼워졌음에도 여전히 강한 보안성을 보입니다. -> [!IMPORTANT] -> 이 프로젝트는 `entlib-native`에서처럼 각 암호 기능에 대해 복잡한 정형 문서(또는 기술 명세)를 작성하지 않습니다. 대신, 가능의 API 시그니처와 사용법은 1차적으로 Rust 문서 주석으로 설명되어 있으며, 이를 요약한 내용을 기능이 제공되는 모듈(또는 크레이트)에 `README.md`로 게시하겠습니다. - 여러분은 언제나 프로젝트를 소개한 문서인 [INTRODUCTION.md](INTRODUCTION.md) 파일을 참고할 수 있습니다. 또한, 이 프로젝트에서 AI 에이전트가 어떤 범위에 맞춰 사용되고, 실제로 수정하는 부분과 사용되는 스킬 및 프롬프트는 무엇인지에 관해 [AI_SCOPE.md](AI_SCOPE.md) 문서에 작성했습니다. diff --git a/aes/src/block.rs b/aes/src/block.rs index 06eaf1d..5a87b24 100644 --- a/aes/src/block.rs +++ b/aes/src/block.rs @@ -1,6 +1,7 @@ #![allow(clippy::needless_range_loop)] use crate::sbox::{inv_sub_bytes_block, sub_bytes_block}; +use zeroize::Zeroize; const NB: usize = 4; const NR: usize = 14; @@ -46,6 +47,7 @@ fn sub_bytes(state: &mut State) { state[r][c] = bytes[r * 4 + c]; } } + bytes.zeroize(); } #[inline] @@ -62,6 +64,7 @@ fn inv_sub_bytes(state: &mut State) { state[r][c] = bytes[r * 4 + c]; } } + bytes.zeroize(); } #[inline] @@ -200,7 +203,9 @@ pub fn encrypt_block(block: &[u8; 16], round_keys: &[u32; NB * (NR + 1)]) -> [u8 shift_rows(&mut state); add_round_key(&mut state, &round_keys[NR * NB..(NR + 1) * NB]); - state_to_block(&state) + let out = state_to_block(&state); + state.zeroize(); + out } pub fn decrypt_block(block: &[u8; 16], round_keys: &[u32; NB * (NR + 1)]) -> [u8; 16] { @@ -219,5 +224,7 @@ pub fn decrypt_block(block: &[u8; 16], round_keys: &[u32; NB * (NR + 1)]) -> [u8 inv_sub_bytes(&mut state); add_round_key(&mut state, &round_keys[0..NB]); - state_to_block(&state) + let out = state_to_block(&state); + state.zeroize(); + out } diff --git a/aes/src/cbc.rs b/aes/src/cbc.rs index 8dd8b08..f76a25d 100644 --- a/aes/src/cbc.rs +++ b/aes/src/cbc.rs @@ -1,4 +1,5 @@ use crate::AES256; +use zeroize::Zeroize; pub const CBC_IV_SIZE: usize = 16; @@ -15,8 +16,14 @@ impl AES256CBC { } pub fn encrypt(&self, iv: &[u8; CBC_IV_SIZE], plaintext: &[u8], ciphertext: &mut [u8]) { - debug_assert!(plaintext.len().is_multiple_of(16)); - debug_assert!(ciphertext.len() >= plaintext.len()); + assert!( + plaintext.len().is_multiple_of(16), + "평문 길이가 블록 크기의 배수가 아님" + ); + assert!( + ciphertext.len() >= plaintext.len(), + "암호문 버퍼가 평문보다 작아 무음 절단 발생" + ); let mut prev = *iv; @@ -31,12 +38,19 @@ impl AES256CBC { let encrypted = self.cipher.encrypt(&block); ct_chunk.copy_from_slice(&encrypted); prev = encrypted; + block.zeroize(); } } pub fn decrypt(&self, iv: &[u8; CBC_IV_SIZE], ciphertext: &[u8], plaintext: &mut [u8]) { - debug_assert!(ciphertext.len().is_multiple_of(16)); - debug_assert!(plaintext.len() >= ciphertext.len()); + assert!( + ciphertext.len().is_multiple_of(16), + "암호문 길이가 블록 크기의 배수가 아님" + ); + assert!( + plaintext.len() >= ciphertext.len(), + "평문 버퍼가 암호문보다 작아 무음 절단 발생" + ); let mut prev = *iv; @@ -44,13 +58,15 @@ impl AES256CBC { let mut block = [0u8; 16]; block.copy_from_slice(ct_chunk); - let decrypted = self.cipher.decrypt(&block); + let mut decrypted = self.cipher.decrypt(&block); for i in 0..16 { pt_chunk[i] = decrypted[i] ^ prev[i]; } prev.copy_from_slice(ct_chunk); + decrypted.zeroize(); + block.zeroize(); } } } diff --git a/aes/src/ctr.rs b/aes/src/ctr.rs index 6fbb1a2..de6bbdd 100644 --- a/aes/src/ctr.rs +++ b/aes/src/ctr.rs @@ -1,8 +1,11 @@ use crate::AES256; +use zeroize::Zeroize; pub const CTR_NONCE_SIZE: usize = 12; pub const CTR_IV_SIZE: usize = 16; +const CTR_MAX_INPUT_LEN: u64 = 1 << 36; + fn inc32(block: &mut [u8; 16]) { let mut carry = 1u16; for i in (12..16).rev() { @@ -28,34 +31,52 @@ impl AES256CTR { let mut offset = 0; while offset + 16 <= input.len() { - let keystream = self.cipher.encrypt(counter); + let mut keystream = self.cipher.encrypt(counter); for i in 0..16 { output[offset + i] = input[offset + i] ^ keystream[i]; } inc32(counter); + keystream.zeroize(); offset += 16; } if offset < input.len() { - let keystream = self.cipher.encrypt(counter); + let mut keystream = self.cipher.encrypt(counter); for i in 0..(input.len() - offset) { output[offset + i] = input[offset + i] ^ keystream[i]; } + keystream.zeroize(); } } pub fn apply_iv(&self, iv: &[u8; CTR_IV_SIZE], input: &[u8], output: &mut [u8]) { - debug_assert!(output.len() >= input.len()); + assert!( + output.len() >= input.len(), + "출력 버퍼가 입력보다 작아 무음 절단 발생" + ); + assert!( + input.len() as u64 <= CTR_MAX_INPUT_LEN, + "입력 길이 한계(2^32 블록) 초과로 카운터 재사용 발생" + ); let mut counter = *iv; self.apply_internal(&mut counter, input, output); + counter.zeroize(); } pub fn apply(&self, nonce: &[u8; CTR_NONCE_SIZE], input: &[u8], output: &mut [u8]) { - debug_assert!(output.len() >= input.len()); + assert!( + output.len() >= input.len(), + "출력 버퍼가 입력보다 작아 무음 절단 발생" + ); + assert!( + input.len() as u64 <= CTR_MAX_INPUT_LEN, + "입력 길이 한계(2^32 블록) 초과로 카운터 재사용 발생" + ); let mut counter = [0u8; 16]; counter[..12].copy_from_slice(nonce); counter[12..16].copy_from_slice(&1u32.to_be_bytes()); self.apply_internal(&mut counter, input, output); + counter.zeroize(); } #[inline] diff --git a/aes/src/gcm.rs b/aes/src/gcm.rs index f7b3fc3..24fbe35 100644 --- a/aes/src/gcm.rs +++ b/aes/src/gcm.rs @@ -1,10 +1,14 @@ use crate::AES256; use crate::ghash::GHash; +use constant_time::{Choice, CtEqOps}; use zeroize::Zeroize; pub const GCM_TAG_SIZE: usize = 16; pub const GCM_NONCE_SIZE: usize = 12; +const GCM_MAX_INPUT_LEN: u64 = (1 << 36) - 32; +const GCM_MAX_AAD_LEN: u64 = (1 << 61) - 1; + fn inc32(block: &mut [u8; 16]) { let mut carry = 1u16; for i in (12..16).rev() { @@ -32,20 +36,23 @@ impl AES256GCM { let mut offset = 0; while offset + 16 <= input.len() { - let keystream = self.cipher.encrypt(&cb); + let mut keystream = self.cipher.encrypt(&cb); for i in 0..16 { output[offset + i] = input[offset + i] ^ keystream[i]; } inc32(&mut cb); + keystream.zeroize(); offset += 16; } if offset < input.len() { - let keystream = self.cipher.encrypt(&cb); + let mut keystream = self.cipher.encrypt(&cb); for i in 0..(input.len() - offset) { output[offset + i] = input[offset + i] ^ keystream[i]; } + keystream.zeroize(); } + cb.zeroize(); } fn compute_j0(&self, nonce: &[u8; GCM_NONCE_SIZE]) -> [u8; 16] { @@ -61,16 +68,19 @@ impl AES256GCM { ghash.update_padded(aad); ghash.update_padded(ciphertext); - let len_block = Self::len_block(aad.len(), ciphertext.len()); + let mut len_block = Self::len_block(aad.len(), ciphertext.len()); ghash.update(&len_block); - let s = ghash.finalize(); - let e_j0 = self.cipher.encrypt(j0); + let mut s = ghash.finalize(); + let mut e_j0 = self.cipher.encrypt(j0); let mut tag = [0u8; 16]; for i in 0..16 { tag[i] = s[i] ^ e_j0[i]; } + s.zeroize(); + e_j0.zeroize(); + len_block.zeroize(); tag } @@ -91,13 +101,29 @@ impl AES256GCM { ciphertext: &mut [u8], tag: &mut [u8; GCM_TAG_SIZE], ) { - let j0 = self.compute_j0(nonce); + assert!( + ciphertext.len() >= plaintext.len(), + "암호문 버퍼가 평문보다 작아 무음 절단 발생" + ); + assert!( + plaintext.len() as u64 <= GCM_MAX_INPUT_LEN, + "SP 800-38D 평문 길이 한계(2^39-256 비트) 초과로 카운터 재사용 발생" + ); + assert!( + aad.len() as u64 <= GCM_MAX_AAD_LEN, + "SP 800-38D AAD 길이 한계(2^64-1 비트) 초과" + ); + + let mut j0 = self.compute_j0(nonce); let mut icb = j0; inc32(&mut icb); self.gctr(&icb, plaintext, ciphertext); *tag = self.compute_tag(aad, &ciphertext[..plaintext.len()], &j0); + + icb.zeroize(); + j0.zeroize(); } pub fn decrypt( @@ -108,16 +134,32 @@ impl AES256GCM { tag: &[u8; GCM_TAG_SIZE], plaintext: &mut [u8], ) -> bool { - let j0 = self.compute_j0(nonce); - - let expected_tag = self.compute_tag(aad, ciphertext, &j0); - - let mut diff = 0u8; + assert!( + plaintext.len() >= ciphertext.len(), + "평문 버퍼가 암호문보다 작아 무음 절단 발생" + ); + assert!( + ciphertext.len() as u64 <= GCM_MAX_INPUT_LEN, + "SP 800-38D 암호문 길이 한계(2^39-256 비트) 초과로 카운터 재사용 발생" + ); + assert!( + aad.len() as u64 <= GCM_MAX_AAD_LEN, + "SP 800-38D AAD 길이 한계(2^64-1 비트) 초과" + ); + + let mut j0 = self.compute_j0(nonce); + + let mut expected_tag = self.compute_tag(aad, ciphertext, &j0); + + let mut eq = Choice::from_u8(1); for i in 0..16 { - diff |= tag[i] ^ expected_tag[i]; + eq &= CtEqOps::eq(&tag[i], &expected_tag[i]); } + let authentic = eq.unwrap_u8() == 1; + expected_tag.zeroize(); - if diff != 0 { + if !authentic { + j0.zeroize(); return false; } @@ -125,6 +167,9 @@ impl AES256GCM { inc32(&mut icb); self.gctr(&icb, ciphertext, plaintext); + icb.zeroize(); + j0.zeroize(); + true } } diff --git a/aes/src/ghash.rs b/aes/src/ghash.rs index ac864cf..480b7a0 100644 --- a/aes/src/ghash.rs +++ b/aes/src/ghash.rs @@ -6,7 +6,7 @@ use zeroize::Zeroize; // 부분곱이 합산되며 (8 < 16), 4비트 lane안에 carry없이 들어가므로 정확히 다항식 비트가 추출 // // AES-NI/PCLMULQDQ 미지원 환경 (TCG 등) 에서 정수 곱셈만으로 상수-시간으로 실행됨 -// (AMD64/AArch64 정수 곱셈은 데이터 비종속 시간 보장, todo: 근데 추가적인 구글링 필요) +// (AMD64/AArch64 정수 곱셈은 데이터 비종속 시간 보장) #[inline] const fn bmul32(x: u32, y: u32) -> u64 { const MX0: u32 = 0x1111_1111; @@ -143,7 +143,7 @@ impl GHash { } pub fn reset(&mut self) { - self.state_n = 0; + self.state_n.zeroize(); } } diff --git a/aes/src/key.rs b/aes/src/key.rs index 96a5aa7..f153787 100644 --- a/aes/src/key.rs +++ b/aes/src/key.rs @@ -1,4 +1,5 @@ use crate::sbox::sub_word; +use zeroize::Zeroize; const NK: usize = 8; const NB: usize = 4; @@ -13,9 +14,7 @@ fn rot_word(w: u32) -> u32 { w.rotate_left(8) } -pub fn expand_key(key: &[u8; 32]) -> [u32; NB * (NR + 1)] { - let mut w = [0u32; NB * (NR + 1)]; - +pub fn expand_key(key: &[u8; 32], w: &mut [u32; NB * (NR + 1)]) { for i in 0..NK { w[i] = u32::from_be_bytes([key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]]); } @@ -28,7 +27,6 @@ pub fn expand_key(key: &[u8; 32]) -> [u32; NB * (NR + 1)] { temp = sub_word(temp); } w[i] = w[i - NK] ^ temp; + temp.zeroize(); } - - w } diff --git a/aes/src/lib.rs b/aes/src/lib.rs index 66e6978..682b175 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -68,9 +68,9 @@ impl AES256 { /// - `key`: 32바이트 암호화 키 #[must_use] pub fn new(key: &[u8; KEY_SIZE]) -> Self { - Self { - round_keys: Secret::new(expand_key(key)), - } + let mut round_keys = Secret::new([0u32; NB * (NR + 1)]); + expand_key(key, round_keys.expose_mut()); + Self { round_keys } } /// 16바이트 블록을 암호화합니다. diff --git a/aes/src/sbox.rs b/aes/src/sbox.rs index e3078ce..779279a 100644 --- a/aes/src/sbox.rs +++ b/aes/src/sbox.rs @@ -15,6 +15,8 @@ #![allow(clippy::many_single_char_names, clippy::similar_names)] +use zeroize::Zeroize; + // 16바이트 블록을 [u32; 8]비트슬라이스 평면으로 변환 // q[k]의 i번째 비트 = bytes[i]의 k번째 비트 #[inline] @@ -227,6 +229,7 @@ pub fn sub_bytes_block(bytes: &mut [u8; 16]) { let mut q = bitslice_block(bytes); bitsliced_sbox(&mut q); *bytes = unbitslice_block(&q); + q.zeroize(); } /// 16 바이트 블록 InvSubBytes 를 비트슬라이스 BP 회로로 일괄 적용합니다. @@ -234,6 +237,7 @@ pub fn inv_sub_bytes_block(bytes: &mut [u8; 16]) { let mut q = bitslice_block(bytes); bitsliced_inv_sbox(&mut q); *bytes = unbitslice_block(&q); + q.zeroize(); } /// 단일 바이트 S-box. 키 스케줄 (sub_word) 에서 호출됩니다. @@ -250,6 +254,7 @@ pub fn sub_byte(x: u8) -> u8 { for (k, plane) in q.iter().enumerate() { out |= ((*plane & 1) as u8) << k; } + q.zeroize(); out } From e6457664b44d70cb81c083d700a7139be591f473 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:28:29 +0900 Subject: [PATCH 04/65] =?UTF-8?q?aes/README=5FEN.md=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(README.md=20=EC=98=81=EB=AC=B8=20=EB=B2=88=EC=97=AD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/README_EN.md | 149 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 aes/README_EN.md diff --git a/aes/README_EN.md b/aes/README_EN.md new file mode 100644 index 0000000..86544ea --- /dev/null +++ b/aes/README_EN.md @@ -0,0 +1,149 @@ +# AES-256 Module + +A `no_std` pure-Rust crate implementing FIPS 197 AES-256 block cipher and NIST SP 800-38A (CBC, CTR) and SP 800-38D (GCM) modes of operation with zero external dependencies. This document covers the feature specification, security design rationale, and the issues found — along with their resolutions — during the 1.1.0 cross-validation pass. + +--- + +## Implemented Primitives + +| Type | Standard | Description | +|---------------|--------------|------------------------------------------------------------------------| +| `AES256` | FIPS 197 | Single 16-byte block encrypt/decrypt. Round keys protected by `Secret` | +| `AES256CBC` | SP 800-38A | CBC mode without padding. Input must be a multiple of 16 bytes | +| `AES256CTR` | SP 800-38A | Counter mode. Initialised with a 96-bit nonce or a 128-bit IV | +| `AES256GCM` | SP 800-38D | Authenticated encryption (AEAD). 96-bit nonce, 128-bit tag | +| `GHash` | SP 800-38D | GF(2^128) authentication hash. Used internally by GCM | + +Size constants are fixed: `KEY_SIZE = 32`, `BLOCK_SIZE = 16`, `CBC_IV_SIZE = 16`, `CTR_NONCE_SIZE = 12`, `CTR_IV_SIZE = 16`, `GCM_NONCE_SIZE = 12`, `GCM_TAG_SIZE = 16`. Key, nonce, and tag lengths are enforced by type (`&[u8; N]`), so length errors are caught at compile time. + +Design decisions: + +- Only AES-256 (256-bit keys) is supported. AES-128/192 are intentionally omitted. +- GCM nonce is fixed at 96 bits. GHASH-based J0 derivation for non-96-bit IVs is not implemented (the SP 800-38D recommended configuration). +- GCM tag length is fixed at 128 bits. Truncated tags are not supported, preserving the upper bound on forgery probability. +- GCM decryption does not write a single byte of plaintext to the output buffer until tag verification succeeds (SP 800-38D Section 7.2). +- CTR `apply` initialises the counter block as `nonce || 0x00000001` and increments only the low 32 bits via `inc32`. +- Input length limits are enforced in code. GCM plaintext: 2^39-256 bits; GCM AAD: 2^64-1 bits; CTR input: 2^32 blocks. Violations trigger `assert!` and abort immediately (`panic = "abort"`). +- All buffers are fixed-size stack arrays. `alloc` is not used. + +## Constant-Time Guarantees + +### 1. Bitsliced Boyar-Peralta S-box + +The S-box is computed as a circuit of approximately 115 AND/XOR/NOT gates without any lookup table. A 16-byte block is converted into `[u32; 8]` bit-planes and processed in a single circuit pass for SubBytes. There are no secret-dependent branches and no secret-dependent memory accesses, entirely eliminating cache and TLB timing side channels. The circuit originates from Boyar & Peralta (2010) and is identical to the one used in BearSSL `aes_ct.c`. + +The inverse S-box reuses the forward circuit via the identity `InvSBox(y) = Affine^-1(SBox(Affine^-1(y)))`. The key schedule's `sub_word` uses the same circuit, so key expansion is also table-free. + +### 2. GHASH Carryless Multiplication + +GF(2^128) multiplication is computed using only integer arithmetic, with no lookup tables. Following the BearSSL `bmul32` approach, each operand is split into four parts using 4-bit-spaced masks; partial products within each lane accumulate without carry, extracting the GF(2)[X] polynomial product exactly from ordinary integer multiplication. 64-bit and 128-bit products are composed via Karatsuba 3-multiplication, and reduction modulo `p(X) = X^128 + X^7 + X^2 + X + 1` is performed with shift-and-XOR, branch-free. + +This path assumes that integer multiplication on AMD64 and AArch64 runs in data-independent time. The `compile_error!` gate in the `constant-time` crate rejects builds for any other architecture, so this assumption holds across all supported targets. DIT (AArch64) and DOITM (x86) hardening is a shared open item with the `constant-time` crate. + +### 3. Tag Comparison + +GCM tag verification accumulates byte-level equality using `CtEqOps` from the `constant-time` crate (inline-assembly `cmp + sete` / `cset`), with no branches or early exits during the comparison. Branching occurs only on the public accept/reject result after all 16 bytes have been compared. + +### 4. Remaining Operations + +- `xtime` / `gf_mul` (MixColumns): uses `(b & 1).wrapping_neg()` masks and multiply-mask forms instead of secret-dependent branches +- ShiftRows and state transposition: fixed-index accesses only +- `inc32`: branchless via u16 carry arithmetic + +## Secret Zeroization + +Following the "one request, one datum, immediate erase" principle, the following guarantees are provided: + +| Secret | Protection | +|--------------------------------------------------|-----------------------------------------------------------| +| Round keys (60 words) | `Secret<[u32; 60]>`, volatile-erased on Drop | +| GCM hash subkey `h` | Zeroized in `Drop` | +| `GHash` fields `h_n` and `state_n` | Zeroized in `Drop`; `reset` also goes through `zeroize` | +| Keystream blocks (CTR, GCM) | Zeroized immediately after each block is consumed | +| AES state matrix and bitslice planes | Zeroized before the function returns | +| Key schedule `temp` | Zeroized after each iteration | +| `E(J0)`, GHASH output, `expected_tag` | Zeroized immediately after tag synthesis and comparison | + +Drop-time erasure is regression-tested by `test_aes256_zeroize_on_drop`, `test_aes256gcm_zeroize_on_drop`, and `test_ghash_zeroize_on_drop`, which inspect memory directly after deallocation using `MaybeUninit`. + +Remaining limitation: sub-word temporaries residing in registers (MixColumns column variables, bytes in `sub_word`, u128 intermediates in GHASH) are not zeroized. Register and spill residue (CWE-316) is a known limitation of the zeroize model; correctness relies on the short lifetime of these values being overwritten by immediately following operations. + +## Standards Compliance Verification + +Key expansion matches the FIPS 197 KeyExpansion specification (Nk=8, Nr=14, 7 RCON values, the additional SubWord branch at `i mod 8 = 4`). GCM has been verified to match the specification for `J0 = IV || 0^31 || 1`, GCTR start counter `inc32(J0)`, and GHASH input ordering `A || pad || C || pad || [len(A)]64 || [len(C)]64`. + +| Test | Source | +|----------------------------------|----------------------------------------------------| +| `fips197_c3_test_vector` | FIPS 197 Appendix C.3 (AES-256) | +| `cbc_nist_f_2_5` | SP 800-38A F.2.5 (CBC-AES256) | +| `ctr_nist_f_5_5` | SP 800-38A F.5.5 (CTR-AES256) | +| `gcm_test_case_14/15/16` | GCM spec (McGrew & Viega) AES-256 test cases | +| `ghash_basic` / `gf128_mul_test` | GCM spec GHASH intermediate values | +| `sub_byte_matches_reference` | Exhaustive comparison of all 256 FIPS 197 S-box entries | +| `bmul32_against_bitserial` | Carryless multiply vs. bit-serial reference | +| `gcm_auth_failure` | Tampered tag rejected, no plaintext released | + +`cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test -p aes --target `, and `cargo build -p aes --target x86_64-unknown-none` (bare-metal) all pass with no warnings. + +## Caller Contract + +This crate is stateless; the following invariants must be upheld by the caller: + +1. **Nonce/IV uniqueness:** Reusing a GCM or CTR nonce under the same key immediately collapses confidentiality and, for GCM, integrity. The library holds no state across calls and cannot detect reuse. +2. **CBC IV unpredictability:** Per SP 800-38A Appendix C, CBC IVs must be unpredictable. +3. **Key separation across modes:** The CTR `apply` first counter block (`nonce || 1`) has the same format as GCM's J0. Using the same key and nonce in both CTR and GCM makes the first CTR keystream block identical to the GCM tag mask `E_K(J0)`, enabling tag forgery and plaintext recovery. One key must be used with exactly one mode. +4. **No raw ECB composition:** `AES256::encrypt` is a single-block primitive. Concatenating multiple blocks without a mode is forbidden. +5. **CBC provides no integrity:** Nor does it provide padding. Use GCM when integrity is required. + +--- + +## Issues Found and Resolved + +Issues discovered during the cross-validation pass and their resolutions. + +### Keystream and Intermediate State Zeroization Missing (Resolved) + +Round keys (`Secret`) were protected, but CTR/GCM keystream blocks, CBC plaintext XOR blocks, the AES state matrix, bitslice planes, the key schedule `temp`, `E(J0)`, and GHASH output were left on the stack. This violated the "one request, one datum, immediate erase" charter. + +#### Resolution + +`zeroize()` was added immediately after each secret intermediate value is used. The table in the zeroization section above reflects the state after this fix. + +### Buffer Validation Silently Dropped in Release Builds (Resolved) + +Length checks in CBC and CTR were `debug_assert!`, causing them to be eliminated in release builds. When the output buffer is smaller than the input, CBC's `zip` terminates early, silently truncating output (CWE-1284); non-multiple-of-16 input has its tail silently ignored. CTR and GCM would abort with an index panic, but without a meaningful diagnostic. + +#### Resolution + +Assertions were promoted to `assert!` to enforce them in release as well. Buffer size checks that were absent from `GCM encrypt`/`decrypt` were added. Violations abort immediately with a diagnostic message. + +### GCM Tag Comparison Open to Compiler Reordering (Resolved) + +The previous tag comparison used XOR accumulation (`diff |= tag[i] ^ expected[i]`). While branch-free at the source level, pure Rust arithmetic offers no language-level guarantee that the optimizer cannot reconstruct a branching pattern — the same issue discussed in the `constant-time` crate README regarding `black_box`. The project charter requires using the `constant-time` crate for all cryptographic comparisons. + +#### Resolution + +Replaced with byte-wise `CtEqOps` accumulation backed by inline assembly. The assembly is emitted as-is, eliminating the risk of compiler reordering. + +### SP 800-38D and 38A Input Length Limits Not Enforced (Resolved) + +Both GCM and CTR increment only the low 32 bits of the counter via `inc32`. Exceeding 2^32 blocks in a single call wraps the counter and reuses the keystream. In GCM, a wrap that reaches J0 leaks the tag mask and enables forgery. Additionally, the AAD bit-length computation in `len_block` (`aad_len * 8`) silently overflowed for inputs larger than 2^61 bytes. SP 800-38D Section 5.2.1.1 requires `len(P) <= 2^39-256` bits and `len(A) <= 2^64-1` bits, but neither limit was checked. + +#### Resolution + +`GCM_MAX_INPUT_LEN = 2^36 - 32` bytes (= 2^39-256 bits), `GCM_MAX_AAD_LEN = 2^61 - 1` bytes (= 2^64-8 bits), and `CTR_MAX_INPUT_LEN = 2^36` bytes (= 2^32 blocks) are now enforced at the entry of `encrypt`, `decrypt`, and `apply` via `assert!`. The GCM limit is the exact value at which the counter stops before wrapping back to J0. Inputs of 64 GiB scale are unreachable in a fixed stack-array environment, but encoding the standard's requirement in code turns it from an assumption into a guarantee. + +### GHash::reset Using Plain Zero Assignment (Resolved) + +`reset` used the plain assignment `self.state_n = 0`. This is the anti-pattern the project prohibits (DCE-eligible erasure). Although functionally harmless given that the field is overwritten before the next use, it broke consistency in the erasure path. + +#### Resolution + +Replaced with a `zeroize()` call, unifying the erasure path through the volatile-write guarantee. + +### Open Items + +- Register-resident temporaries (see the remaining limitation in the zeroization section above) +- AArch64 DIT and x86 DOITM hardening — shared open item with the `constant-time` crate +- `inc32` is defined identically in both `ctr.rs` and `gcm.rs`; consolidation is deferred to a follow-up cleanup +- TODO annotations in `sbox.rs` and `ghash.rs` module comments to be cleaned up From 1e5de2a331f6bf009dfc33a489d1fd35d70e6dc7 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:35:27 +0900 Subject: [PATCH 05/65] =?UTF-8?q?aes/README.md=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=ED=95=9C/=EC=98=81=20=ED=81=AC=EB=A1=9C=EC=8A=A4=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=B6=94=EA=B0=80=20-=20=EA=B7=B9=20=EC=82=AC?= =?UTF-8?q?=EC=86=8C=ED=95=9C=20Docstring=20=EB=A6=B0=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20AES=20=EA=B2=80=EC=A6=9D=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=20->=20cross-confirm.md=20=EC=99=84=EB=A3=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/README.md | 151 +++++++++++++++++++++++++++++++++++++++++++++++ aes/README_EN.md | 2 + blake/src/lib.rs | 2 +- cross-confirm.md | 2 +- 4 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 aes/README.md diff --git a/aes/README.md b/aes/README.md new file mode 100644 index 0000000..482ad06 --- /dev/null +++ b/aes/README.md @@ -0,0 +1,151 @@ +# AES-256 모듈 + +[![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) + +FIPS 197 AES-256 블록 암호와 NIST SP 800-38A(CBC·CTR), SP 800-38D(GCM) 운용 모드를 외부 의존성 없이 `no_std` 순수 Rust로 구현한 크레이트입니다. 이 문서는 기능 명세와 보안 설계 근거, 그리고 1.1.0 교차 검증에서 발견한 문제와 조치를 기술합니다. + +--- + +## 구현된 프리미티브 + +| 타입 | 표준 | 설명 | +|-------------|------------|-----------------------------------------| +| `AES256` | FIPS 197 | 단일 16바이트 블록 암·복호화. 라운드 키는 `Secret`으로 보호 | +| `AES256CBC` | SP 800-38A | 패딩 없는 CBC 모드. 입력은 16바이트 배수여야 함 | +| `AES256CTR` | SP 800-38A | 카운터 모드. 96비트 nonce 또는 128비트 IV 초기화 | +| `AES256GCM` | SP 800-38D | 인증 암호화(AEAD). 96비트 nonce, 128비트 태그 | +| `GHash` | SP 800-38D | GF(2^128) 인증 해시. GCM 내부에서 사용 | + +크기 상수는 `KEY_SIZE = 32`, `BLOCK_SIZE = 16`, `CBC_IV_SIZE = 16`, `CTR_NONCE_SIZE = 12`, `CTR_IV_SIZE = 16`, `GCM_NONCE_SIZE = 12`, `GCM_TAG_SIZE = 16`으로 고정되며, 키·nonce·태그 길이는 타입(`&[u8; N]`)으로 강제되어 길이 오류가 컴파일 단계에서 차단됩니다. + +설계 결정 명세는 다음과 같습니다. + +- 키는 AES-256(256비트)만 지원합니다. AES-128/192는 의도적으로 제공하지 않습니다. +- GCM nonce는 96비트 고정입니다. 비-96비트 IV의 GHASH 기반 J0 유도는 구현하지 않습니다(SP 800-38D 권장 구성). +- GCM 태그는 128비트 전체 길이만 지원합니다. 절단 태그를 막아 위조 확률 상한을 보존합니다. +- GCM 복호화는 태그 검증이 성공하기 전까지 평문을 단 한 바이트도 출력 버퍼에 기록하지 않습니다(SP 800-38D 7.2절). +- CTR `apply`는 카운터 블록을 `nonce || 0x00000001`로 초기화하고 하위 32비트만 inc32로 증가시킵니다. +- 입력 길이 한계를 코드로 강제합니다. GCM 평문 2^39-256 비트, GCM AAD 2^64-1 비트, CTR 입력 2^32 블록. 위반 시 `assert!`가 즉시 중단시킵니다(`panic = "abort"`). +- 모든 버퍼는 고정 크기 스택 배열입니다. `alloc`을 사용하지 않습니다. + +## 상수-시간 보장 근거 + +### 1. 비트슬라이스 Boyar-Peralta S-box + +S-box를 룩업 테이블 없이 약 115개의 AND/XOR/NOT 게이트 회로로 계산합니다. 16바이트 블록을 `[u32; 8]` 비트 평면으로 변환해 한 번의 회로 통과로 SubBytes를 일괄 처리하며, 비밀 의존 분기와 비밀 의존 메모리 접근이 전혀 없어 캐시·TLB 타이밍 부채널이 원천 차단됩니다. 회로 출처는 Boyar & Peralta(2010)이며 BearSSL `aes_ct.c`와 동일한 회로입니다. + +역 S-box는 항등식 `InvSBox(y) = Affine^-1(SBox(Affine^-1(y)))`로 정방향 회로를 재사용합니다. 키 확장의 `sub_word`도 같은 회로를 사용하므로 키 스케줄 역시 테이블 없이 수행됩니다. + +### 2. GHASH carryless 곱셈 + +GF(2^128) 곱을 룩업 테이블 없이 정수 곱셈만으로 합성합니다. BearSSL `bmul32` 방식으로 피연산자를 4비트 간격 마스크로 4등분하면 lane당 부분곱이 carry 없이 합산되어 일반 정수 곱셈에서 GF(2)[X] 다항식 곱이 정확히 추출됩니다. 64비트와 128비트는 Karatsuba 3-곱으로 합성하고, 환원은 `p(X) = X^128 + X^7 + X^2 + X + 1`에 대한 시프트-XOR로 분기 없이 수행합니다. + +이 경로의 전제는 "AMD64·AArch64의 정수 곱셈은 데이터 비종속 시간"입니다. 이 전제는 constant-time 크레이트의 `compile_error!` 게이트가 두 아키텍처 외 빌드를 거부하므로 지원 타겟 전체에서 성립합니다. DIT(aarch64)·DOITM(x86) 하드닝 과제는 constant-time README와 공유합니다. + +### 3. 태그 비교 + +GCM 태그 검증은 constant-time 크레이트의 `CtEqOps`(인-라인 어셈블리 `cmp + sete / cset`)를 바이트별로 누적해 분기·조기 종료 없이 수행합니다. 비교가 끝난 뒤 수락/거부라는 공개 결과에만 분기합니다. + +### 4. 나머지 연산 + +- `xtime`·`gf_mul`(MixColumns): 비밀 의존 분기 대신 `(b & 1).wrapping_neg()` 마스크와 곱셈-마스크 형태만 사용 +- ShiftRows·상태 전치: 고정 인덱스 접근만 존재 +- `inc32`: u16 carry 산술로 브랜치리스 + +## 비밀 소거 (zeroize) + +트랜잭션 단위 전부 소거 원칙에 따라 다음이 보장됩니다. + +| 비밀 | 보호 방식 | +|---------------------------------|-----------------------------------------| +| 라운드 키(60워드) | `Secret<[u32; 60]>`, Drop 시 volatile 소거 | +| GCM 해시 서브키 `h` | `Drop`에서 zeroize | +| `GHash`의 `h_n`·`state_n` | `Drop`에서 zeroize, `reset`도 zeroize 경유 | +| 키스트림 블록(CTR·GCM) | 매 블록 사용 직후 zeroize | +| AES 상태 행렬·비트슬라이스 평면 | 함수 종료 전 zeroize | +| 키 확장 `temp` | 매 반복 직후 zeroize | +| `E(J0)`·GHASH 출력·`expected_tag` | 태그 합성·비교 직후 zeroize | + +Drop 소거는 `test_aes256_zeroize_on_drop`, `test_aes256gcm_zeroize_on_drop`, `test_ghash_zeroize_on_drop`이 `MaybeUninit` 기반으로 해제 후 메모리를 직접 검사해 회귀를 방지합니다. + +잔여 한계: 레지스터에 상주하는 워드 미만 임시값(MixColumns 열 변수, `sub_word`의 바이트, GHASH의 u128 중간값)은 zeroize 대상이 아닙니다. 레지스터·스필 잔존(CWE-316)은 zeroize 모델의 알려진 한계이며, 후속 연산이 즉시 덮어쓰는 짧은 수명에 의존합니다. + +## 표준 부합 검증 + +키 확장은 FIPS 197 KeyExpansion(Nk=8, Nr=14, RCON 7개, `i mod 8 = 4`에서의 추가 SubWord 분기)과 일치하며, GCM은 `J0 = IV || 0^31 || 1`, GCTR 시작 카운터 `inc32(J0)`, GHASH 입력 순서 `A || pad || C || pad || [len(A)]64 || [len(C)]64`가 명세와 일치함을 확인했습니다. + +| 테스트 | 출처 | +|--------------------------------|------------------------------------| +| `fips197_c3_test_vector` | FIPS 197 부록 C.3 (AES-256) | +| `cbc_nist_f_2_5` | SP 800-38A F.2.5 (CBC-AES256) | +| `ctr_nist_f_5_5` | SP 800-38A F.5.5 (CTR-AES256) | +| `gcm_test_case_14/15/16` | GCM 명세(McGrew & Viega) AES-256 케이스 | +| `ghash_basic`·`gf128_mul_test` | GCM 명세 GHASH 중간값 | +| `sub_byte_matches_reference` | FIPS 197 S-box 256개 전수 대조 | +| `bmul32_against_bitserial` | carryless 곱 비트시리얼 대조 | +| `gcm_auth_failure` | 변조 태그 거부 + 평문 미방출 | + +`cargo fmt --check`, `cargo clippy --all-targets --all-features -- -D warnings`, `cargo test -p aes --target <호스트 트리플>`, `cargo build -p aes --target x86_64-unknown-none`(베어메탈) 모두 무경고 통과합니다. + +## 호출자 계약 + +이 크레이트는 stateless이므로 다음은 호출자가 보장해야 합니다. + +1. **nonce/IV 유일성:** 같은 키로 GCM 및 CTR nonce를 재사용하면 키스트림 재사용으로 기밀성과 무결성이 즉시 붕괴합니다. 라이브러리는 호출 간 상태를 갖지 않아 이를 감지할 수 없습니다. +2. **CBC IV 예측 불가성:** SP 800-38A 부록 C에 따라 CBC IV는 예측 불가능해야 합니다. +3. **모드 간 키 분리:** CTR `apply`의 첫 카운터 블록(`nonce || 1`)이 GCM의 J0와 동일한 형식입니다. 같은 키·nonce를 CTR과 GCM 양쪽에 쓰면 CTR 키스트림 첫 블록이 GCM 태그 마스크 `E_K(J0)`와 일치해 태그 위조와 평문 복원으로 이어집니다. 한 키는 한 모드에만 사용해야 합니다. +4. **ECB 구성 금지:** `AES256::encrypt`는 단일 블록 프리미티브입니다. 여러 블록을 모드 없이 이어붙이는 사용은 금지됩니다. +5. **CBC는 무결성을 제공하지 않습니다:** 패딩도 제공하지 않습니다. 무결성이 필요하면 GCM을 사용해야 합니다. + +--- + +## 발견한 문제와 조치 + +교차 검증에서 발견한 문제와 해결을 기술합니다. + +### 키스트림·중간 상태 zeroize 누락 (해결) + +라운드 키(`Secret`)는 보호되고 있었지만, CTR·GCM 키스트림 블록, CBC의 평문 XOR 블록, AES 상태 행렬, 비트슬라이스 평면, 키 확장 `temp`, `E(J0)`, GHASH 출력이 스택에 잔존했습니다. "한 요청·한 데이터·즉시 소거" 헌장 위반입니다. + +#### 해결 + +모든 비밀 중간값에 사용 직후 `zeroize()`를 추가했습니다. 위 zeroize 절의 표가 조치 후 상태입니다. + +### release 빌드에서 버퍼 검증 소실 (해결) + +CBC·CTR의 길이 검증이 `debug_assert!`여서 release 빌드에서 제거되었습니다. 출력 버퍼가 입력보다 작으면 CBC는 `zip`이 조기 종료해 무음 절단(CWE-1284)이 발생하고, 블록 비배수 입력은 꼬리가 침묵 속에 무시되었습니다. CTR·GCM은 인덱스 패닉으로 중단되긴 하나 사유 없는 abort였습니다. + +#### 해결 + +검증을 `assert!`로 승격해 release에서도 강제하고, GCM `encrypt`/`decrypt`에는 없던 출력 버퍼 검증을 신설했습니다. 위반은 진단 메시지와 함께 즉시 중단됩니다. + +### GCM 태그 비교의 최적화기 재구성 여지 (해결) + +기존 태그 비교는 XOR 누적(`diff |= tag[i] ^ expected[i]`) 방식으로 소스 수준에서는 분기가 없지만, 순수 Rust 산술이라 최적화기가 패턴을 재구성할 여지를 언어 차원에서 막을 수 없습니다(constant-time README의 `black_box` 논의와 동일한 문제). 프로젝트 헌장은 암호 로직의 비교 연산에 constant-time 크레이트 사용을 요구합니다. + +#### 해결 + +인-라인 어셈블리 기반 `CtEqOps` 바이트 누적으로 교체했습니다. 어셈블리는 그대로 방출되므로 재구성 위험이 없습니다. + +### SP 800-38D·38A 입력 길이 한계 미검증 (해결) + +GCM과 CTR의 카운터는 하위 32비트만 inc32로 증가하므로 한 호출에서 2^32 블록을 넘으면 카운터가 wrap되어 키스트림이 재사용됩니다. GCM에서는 wrap이 J0까지 도달하면 태그 마스크 유출로 위조까지 가능해집니다. 또한 `len_block`의 AAD 비트 길이 계산(`aad_len * 8`)이 2^61 바이트 초과 입력에서 silent overflow였습니다. SP 800-38D 5.2.1.1절은 `len(P) <= 2^39-256` 비트, `len(A) <= 2^64-1` 비트를 요구하지만 어떤 검증도 없었습니다. + +#### 해결 + +`GCM_MAX_INPUT_LEN = 2^36 - 32`바이트(= 2^39-256 비트), `GCM_MAX_AAD_LEN = 2^61 - 1`바이트(= 2^64-8 비트), `CTR_MAX_INPUT_LEN = 2^36`바이트(= 2^32 블록)를 `encrypt`/`decrypt`/`apply` 입구에서 `assert!`로 강제했습니다. GCM 한계는 카운터가 J0로 되돌아오기 전에 정확히 멈추는 값입니다. 64 GiB급 입력은 고정 스택 배열 환경에서 실질적으로 도달 불가능하지만, 표준 요구 사항을 코드로 명시해 가정이 아닌 보장으로 만들었습니다. + +### GHash::reset의 Plain 0 대입 (해결) + +`reset`이 `self.state_n = 0` 플레인 대입이었습니다. 프로젝트가 금지하는 anti-pattern(DCE-eligible 소거)으로, 재사용 직전이라 기능상 문제는 없으나 소거 경로의 일관성을 깨뜨립니다. + +#### 해결 + +`zeroize()` 경유로 교체해 volatile 보장 경로로 일원화했습니다. + +### 잔여 과제 + +- 레지스터 상주 임시값의 잔존(위 zeroize 절의 잔여 한계) +- aarch64 DIT·x86 DOITM 하드닝은 constant-time 크레이트와 공동 과제 +- `inc32`가 `ctr.rs`와 `gcm.rs`에 중복 정의되어 있음(동작 동일, 통합은 후속 정리) +- `sbox.rs`·`ghash.rs` 모듈 주석의 TODO 표기 정리 diff --git a/aes/README_EN.md b/aes/README_EN.md index 86544ea..e7d5859 100644 --- a/aes/README_EN.md +++ b/aes/README_EN.md @@ -1,5 +1,7 @@ # AES-256 Module +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) + A `no_std` pure-Rust crate implementing FIPS 197 AES-256 block cipher and NIST SP 800-38A (CBC, CTR) and SP 800-38D (GCM) modes of operation with zero external dependencies. This document covers the feature specification, security design rationale, and the issues found — along with their resolutions — during the 1.1.0 cross-validation pass. --- diff --git a/blake/src/lib.rs b/blake/src/lib.rs index 3bb9f47..bd02e4f 100644 --- a/blake/src/lib.rs +++ b/blake/src/lib.rs @@ -4,7 +4,7 @@ //! 민감 데이터는 [SecureBuffer]에 보관하며, Drop 시 내부 상태를 //! [ptr::write_volatile]로 강제 소거합니다. //! -//! 상수-시간 비교는 플랫폼(환경)에 상관없이 `constant-time` +//! 상수-시간 비교는 플랫폼(환경)에 상관없이 `constant-time` //! 크레이트를 통해 정상적으로 수행됩니다. //! //! --- diff --git a/cross-confirm.md b/cross-confirm.md index 1b65369..831d8f0 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -5,7 +5,7 @@ 이 검증 작업은 4명의 보안 및 컴플라이언스 종사자(전문가)와 Claude Fable 5 모델을 통해 수행됩니다. 다음 목록은 현황을 명시한 것입니다. -- [ ] AES +- [X] AES - [ ] BLAKE - [X] SHA2 - [ ] SHA3 From e073d177f81fe9fba5606ff5e0795bc3d0633527 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:10:23 +0900 Subject: [PATCH 06/65] =?UTF-8?q?SHA3=20=EA=B2=80=EC=A6=9D=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EB=B0=8F=20keccak=20=EB=B3=B4=EC=95=88=20=EA=B0=95?= =?UTF-8?q?=ED=99=94=20-=20cross-confirm.md=20SHA3=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EC=B2=98=EB=A6=AC=20-=20FIPS=20202=20?= =?UTF-8?q?=EA=B3=B5=EC=8B=9D=20=EB=B2=A1=ED=84=B0=20=EB=B0=8F=20zeroize?= =?UTF-8?q?=20on=20drop=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20-=20keccak=20c/d=20=EB=B0=B0=EC=97=B4=20=EB=A3=A8=ED=94=84?= =?UTF-8?q?=20=EC=99=B8=EB=B6=80=20=EC=9D=B4=EB=8F=99=20=ED=9B=84=20?= =?UTF-8?q?=EC=86=8C=EA=B1=B0=20=EC=B6=94=EA=B0=80=20-=20absorb=5Fblock=20?= =?UTF-8?q?=EC=9E=90=EA=B8=B0=20=EC=B0=A8=EC=9A=A9=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=A4=ED=83=9D=20=EC=9E=94=EC=A1=B4=20=EC=86=8C?= =?UTF-8?q?=EA=B1=B0=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- sha3/src/_sha3_test.rs | 281 ++++++++++++++++++++++++++++++++++++++++- sha3/src/keccak.rs | 41 +++--- 3 files changed, 306 insertions(+), 18 deletions(-) diff --git a/cross-confirm.md b/cross-confirm.md index 831d8f0..bd9eeac 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -8,7 +8,7 @@ - [X] AES - [ ] BLAKE - [X] SHA2 -- [ ] SHA3 +- [X] SHA3 - [ ] ChaCha20 - [ ] Ed448 - [ ] Ed25519 diff --git a/sha3/src/_sha3_test.rs b/sha3/src/_sha3_test.rs index c3b7a3f..5027fcf 100644 --- a/sha3/src/_sha3_test.rs +++ b/sha3/src/_sha3_test.rs @@ -1,6 +1,22 @@ #[cfg(test)] mod tests { - use crate::{SHA3, SHA3_256, SHA3_512}; + use crate::{ + Digest, KeccakState, MAX_RATE_BYTES, SHA3, SHA3_224, SHA3_256, SHA3_384, SHA3_512, + SHAKE128, SHAKE256, XOF, + }; + use core::mem::MaybeUninit; + + fn sha3(data: &[u8]) -> Digest { + let mut h = H::new(); + h.update(data); + h.finalize() + } + + fn shake(data: &[u8], out: &mut [u8]) { + let mut x = H::new(); + x.update(data); + x.finalize_into(out); + } #[test] fn sha3_256() { @@ -21,4 +37,267 @@ mod tests { let expected: &[u8; 64] = b"\x38\xe0\x5c\x33\xd7\xb0\x67\x12\x7f\x21\x7d\x8c\x85\x6e\x55\x4f\xcf\xf0\x9c\x93\x20\xb8\xa5\x97\x9c\xe2\xff\x5d\x95\xdd\x27\xba\x35\xd1\xfb\xa5\x0c\x56\x2d\xfd\x1d\x6c\xc4\x8b\xc9\xc5\xba\xa4\x39\x08\x94\x41\x8c\xc9\x42\xd9\x68\xf9\x7b\xcb\x65\x94\x19\xed"; assert_eq!(s512_digest.as_bytes(), expected.as_ref()); } + + // NIST FIPS 202 공식 빈 메시지 알려진 값 (전체 6개 변형의 도메인 접미사와 패딩 검증) + #[test] + fn fips202_empty_message_vectors() { + assert_eq!( + sha3::(b"").as_bytes(), + b"\x6b\x4e\x03\x42\x36\x67\xdb\xb7\x3b\x6e\x15\x45\x4f\x0e\xb1\xab\xd4\x59\x7f\x9a\x1b\x07\x8e\x3f\x5b\x5a\x6b\xc7".as_ref() + ); + assert_eq!( + sha3::(b"").as_bytes(), + b"\xa7\xff\xc6\xf8\xbf\x1e\xd7\x66\x51\xc1\x47\x56\xa0\x61\xd6\x62\xf5\x80\xff\x4d\xe4\x3b\x49\xfa\x82\xd8\x0a\x4b\x80\xf8\x43\x4a".as_ref() + ); + assert_eq!( + sha3::(b"").as_bytes(), + b"\x0c\x63\xa7\x5b\x84\x5e\x4f\x7d\x01\x10\x7d\x85\x2e\x4c\x24\x85\xc5\x1a\x50\xaa\xaa\x94\xfc\x61\x99\x5e\x71\xbb\xee\x98\x3a\x2a\xc3\x71\x38\x31\x26\x4a\xdb\x47\xfb\x6b\xd1\xe0\x58\xd5\xf0\x04".as_ref() + ); + assert_eq!( + sha3::(b"").as_bytes(), + b"\xa6\x9f\x73\xcc\xa2\x3a\x9a\xc5\xc8\xb5\x67\xdc\x18\x5a\x75\x6e\x97\xc9\x82\x16\x4f\xe2\x58\x59\xe0\xd1\xdc\xc1\x47\x5c\x80\xa6\x15\xb2\x12\x3a\xf1\xf5\xf9\x4c\x11\xe3\xe9\x40\x2c\x3a\xc5\x58\xf5\x00\x19\x9d\x95\xb6\xd3\xe3\x01\x75\x85\x86\x28\x1d\xcd\x26".as_ref() + ); + + let mut out128 = [0u8; 32]; + shake::(b"", &mut out128); + assert_eq!( + out128.as_ref(), + b"\x7f\x9c\x2b\xa4\xe8\x8f\x82\x7d\x61\x60\x45\x50\x76\x05\x85\x3e\xd7\x3b\x80\x93\xf6\xef\xbc\x88\xeb\x1a\x6e\xac\xfa\x66\xef\x26".as_ref() + ); + + let mut out256 = [0u8; 64]; + shake::(b"", &mut out256); + assert_eq!( + out256.as_ref(), + b"\x46\xb9\xdd\x2b\x0b\xa8\x8d\x13\x23\x3b\x3f\xeb\x74\x3e\xeb\x24\x3f\xcd\x52\xea\x62\xb8\x1b\x82\xb5\x0c\x27\x64\x6e\xd5\x76\x2f\xd7\x5d\xc4\xdd\xd8\xc0\xf2\x00\xcb\x05\x01\x9d\x67\xb5\x92\xf6\xfc\x82\x1c\x49\x47\x9a\xb4\x86\x40\x29\x2e\xac\xb3\xb7\xc4\xbe".as_ref() + ); + } + + // NIST FIPS 202 공식 1600비트 메시지 (0xA3 x 200) 값, 다중 블록 흡수 경로 검증 + #[test] + fn fips202_1600bit_message_vectors() { + let m = [0xA3u8; 200]; + assert_eq!( + sha3::(&m).as_bytes(), + b"\x93\x76\x81\x6a\xba\x50\x3f\x72\xf9\x6c\xe7\xeb\x65\xac\x09\x5d\xee\xe3\xbe\x4b\xf9\xbb\xc2\xa1\xcb\x7e\x11\xe0".as_ref() + ); + assert_eq!( + sha3::(&m).as_bytes(), + b"\x79\xf3\x8a\xde\xc5\xc2\x03\x07\xa9\x8e\xf7\x6e\x83\x24\xaf\xbf\xd4\x6c\xfd\x81\xb2\x2e\x39\x73\xc6\x5f\xa1\xbd\x9d\xe3\x17\x87".as_ref() + ); + assert_eq!( + sha3::(&m).as_bytes(), + b"\x18\x81\xde\x2c\xa7\xe4\x1e\xf9\x5d\xc4\x73\x2b\x8f\x5f\x00\x2b\x18\x9c\xc1\xe4\x2b\x74\x16\x8e\xd1\x73\x26\x49\xce\x1d\xbc\xdd\x76\x19\x7a\x31\xfd\x55\xee\x98\x9f\x2d\x70\x50\xdd\x47\x3e\x8f".as_ref() + ); + assert_eq!( + sha3::(&m).as_bytes(), + b"\xe7\x6d\xfa\xd2\x20\x84\xa8\xb1\x46\x7f\xcf\x2f\xfa\x58\x36\x1b\xec\x76\x28\xed\xf5\xf3\xfd\xc0\xe4\x80\x5d\xc4\x8c\xae\xec\xa8\x1b\x7c\x13\xc3\x0a\xdf\x52\xa3\x65\x95\x84\x73\x9a\x2d\xf4\x6b\xe5\x89\xc5\x1c\xa1\xa4\xa8\x41\x6d\xf6\x54\x5a\x1c\xe8\xba\x00".as_ref() + ); + } + + // rate 경계 패딩 검증, L = rate-1 (도메인 접미사와 0x80 병합), rate (전체 패딩 블록), rate+1 + #[test] + fn sha3_224_rate_boundary() { + let m = [0xA3u8; 145]; + assert_eq!( + sha3::(&m[..143]).as_bytes(), + b"\x1e\x66\xe6\xc6\x7c\xa1\xaf\xfe\xcd\x0b\xb4\xc3\x8b\x1a\x93\x09\x33\xcb\x7e\x34\xe4\x98\xe1\x32\xf1\xc6\x66\x1b".as_ref() + ); + assert_eq!( + sha3::(&m[..144]).as_bytes(), + b"\x5c\xf2\xd3\x62\x73\x84\x4c\xe1\x6e\xde\xdc\xc9\xaf\xb6\xa7\xa3\x93\xa6\xc7\x2c\x41\x73\x1a\xea\x14\x4b\x7a\x00".as_ref() + ); + assert_eq!( + sha3::(&m).as_bytes(), + b"\xa6\x20\x08\xd3\x3b\x7d\x2f\x3a\x62\x1b\x82\x90\x84\x8b\x6f\x21\xe7\xe2\x52\xf1\x01\xb0\x26\x3b\x98\x68\xb2\x05".as_ref() + ); + } + + #[test] + fn sha3_256_rate_boundary() { + let m = [0xA3u8; 137]; + assert_eq!( + sha3::(&m[..135]).as_bytes(), + b"\xd5\x19\x27\x26\x5c\xa4\xbf\x0c\xc8\xb4\x45\x33\x87\x70\x09\x18\xc0\x3f\x88\x94\xe3\x95\xad\x43\x7d\x45\x73\xf3\xbe\x4d\x2c\x34".as_ref() + ); + assert_eq!( + sha3::(&m[..136]).as_bytes(), + b"\x0a\xdf\x6b\xfb\x35\x9a\xe4\x00\x19\xb6\x7d\x8c\x49\xc3\x61\x57\x4b\x70\x24\x2a\x6b\x75\x2d\xe6\xf9\xe0\xd4\x26\xca\x17\x7f\x7a".as_ref() + ); + assert_eq!( + sha3::(&m).as_bytes(), + b"\xe2\xfa\x06\xea\xa2\x2f\xe6\x01\x06\xaf\x67\xd5\xf6\xea\x09\x3f\xe5\x8f\x07\xd2\xdc\xfb\x06\xd5\x10\x57\x95\x3f\x11\x48\x49\xa7".as_ref() + ); + } + + #[test] + fn sha3_384_rate_boundary() { + let m = [0xA3u8; 105]; + assert_eq!( + sha3::(&m[..103]).as_bytes(), + b"\x7c\x40\x34\x7d\xc9\xff\xa4\xd2\x33\x4e\x2f\xdd\xbe\xc2\x0a\x10\x01\x97\x55\x9e\xab\x92\x7e\x71\x20\x6a\x4f\xda\x3e\xe8\xbd\xc5\xb1\x7e\xb4\xfb\xbb\x21\x8f\x5b\x9c\xaa\xc0\x43\x3a\x8a\x53\x83".as_ref() + ); + assert_eq!( + sha3::(&m[..104]).as_bytes(), + b"\x27\xac\x5e\xbc\x6f\x99\x95\xeb\x10\x38\x25\x3a\x95\x1d\xf5\x47\x1c\x86\x6f\x4c\x76\x4a\x85\x09\x11\x24\xbe\x6a\xcd\x81\xe3\x69\xc1\x4b\x53\x23\xbb\xcd\x2b\x39\x31\x0d\x5e\x27\x68\x31\x7c\xbd".as_ref() + ); + assert_eq!( + sha3::(&m).as_bytes(), + b"\x25\x97\xbb\x72\x6c\x06\x8d\xc8\x59\x88\x41\x06\x71\x76\x9d\xba\x9a\x85\x28\xba\x4f\x63\xd2\xe9\xb1\x19\x57\xca\x24\x2f\x59\xcb\xc4\xf7\x46\xfc\x93\xc1\xc8\x7d\x7c\x66\xb5\xbe\xdb\x36\xf9\xe5".as_ref() + ); + } + + #[test] + fn sha3_512_rate_boundary() { + let m = [0xA3u8; 73]; + assert_eq!( + sha3::(&m[..71]).as_bytes(), + b"\x31\x79\xc8\x5b\x18\xc7\x90\x51\x8b\x1d\xdb\x02\xe6\x95\x3b\x01\xb2\xd0\x1f\xf7\x24\x09\xb1\xce\x0b\x38\x82\x8c\x71\x0a\xb7\xc0\xbd\x98\xf0\xa5\xc5\x86\x16\x92\xc3\x95\x4d\x8c\xe4\xfb\x02\xda\x42\x56\x0b\xe1\x29\xc4\xdd\x5b\x3e\xad\xcb\x02\x90\x86\x76\xe0".as_ref() + ); + assert_eq!( + sha3::(&m[..72]).as_bytes(), + b"\xd2\x4c\xe7\x5b\x87\xc7\xbe\x36\xe3\xfe\xdb\xaa\x28\x5f\x56\x3d\x3e\xfc\xc1\x36\x63\xf5\xeb\x2f\xdd\x0c\x60\x03\x3d\xab\x04\xe8\x94\xd3\x43\xb3\x97\x1b\xc0\xc9\xba\x30\xe0\xdd\xe1\x81\x06\xcb\xaa\xa9\x55\xc8\xc3\xc0\xbf\x1e\xc3\x49\x0a\xaf\xca\xe1\x57\x88".as_ref() + ); + assert_eq!( + sha3::(&m).as_bytes(), + b"\xb5\xd2\xe4\x26\x3c\x9e\xe9\xc6\x69\x93\xa2\x9d\xb8\x8c\x04\xa4\x79\xdf\x53\xad\x69\xfb\x67\x42\xdf\xfb\x07\x89\xa1\x4e\x35\xfe\x46\xbc\x0f\x3a\x8b\xac\x7a\x2b\x83\x33\x5b\x9b\x4e\xbb\x05\xb0\x7f\xce\x29\x60\xa7\x90\xe6\x28\xa1\xdd\xe0\x8e\xb6\xbb\x22\xe0".as_ref() + ); + } + + // XOF 다중 블록 스퀴즈 (rate 168 초과 출력의 재순열 경로), 부분 워드 출력, 접두사 일관성 검증 + #[test] + fn shake128_multiblock_squeeze() { + let m = [0xA3u8; 200]; + + let mut out200 = [0u8; 200]; + shake::(&m, &mut out200); + let expected: &[u8; 200] = b"\x13\x1a\xb8\xd2\xb5\x94\x94\x6b\x9c\x81\x33\x3f\x9b\xb6\xe0\xce\x75\xc3\xb9\x31\x04\xfa\x34\x69\xd3\x91\x74\x57\x38\x5d\xa0\x37\xcf\x23\x2e\xf7\x16\x4a\x6d\x1e\xb4\x48\xc8\x90\x81\x86\xad\x85\x2d\x3f\x85\xa5\xcf\x28\xda\x1a\xb6\xfe\x34\x38\x17\x19\x78\x46\x7f\x1c\x05\xd5\x8c\x7e\xf3\x8c\x28\x4c\x41\xf6\xc2\x22\x1a\x76\xf1\x2a\xb1\xc0\x40\x82\x66\x02\x50\x80\x22\x94\xfb\x87\x18\x02\x13\xfd\xef\x5b\x0e\xcb\x7d\xf5\x0c\xa1\xf8\x55\x5b\xe1\x4d\x32\xe1\x0f\x6e\xdc\xde\x89\x2c\x09\x42\x4b\x29\xf5\x97\xaf\xc2\x70\xc9\x04\x55\x6b\xfc\xb4\x7a\x7d\x40\x77\x8d\x39\x09\x23\x64\x2b\x3c\xbd\x05\x79\xe6\x09\x08\xd5\xa0\x00\xc1\xd0\x8b\x98\xef\x93\x3f\x80\x64\x45\xbf\x87\xf8\xb0\x09\xba\x9e\x94\xf7\x26\x61\x22\xed\x7a\xc2\x4e\x5e\x26\x6c\x42\xa8\x2f\xa1\xbb\xef\xb7\xb8\xdb\x00\x66\xe1\x6a\x85\xe0\x49\x3f"; + assert_eq!(out200.as_ref(), expected.as_ref()); + + let mut out33 = [0u8; 33]; + shake::(&m, &mut out33); + assert_eq!( + out33.as_ref(), + b"\x13\x1a\xb8\xd2\xb5\x94\x94\x6b\x9c\x81\x33\x3f\x9b\xb6\xe0\xce\x75\xc3\xb9\x31\x04\xfa\x34\x69\xd3\x91\x74\x57\x38\x5d\xa0\x37\xcf".as_ref() + ); + + assert_eq!(&out200[..33], out33.as_ref(), "XOF 접두사 일관성 위반"); + } + + #[test] + fn shake256_multiblock_squeeze() { + let m = [0xA3u8; 200]; + + let mut out200 = [0u8; 200]; + shake::(&m, &mut out200); + let expected: &[u8; 200] = b"\xcd\x8a\x92\x0e\xd1\x41\xaa\x04\x07\xa2\x2d\x59\x28\x86\x52\xe9\xd9\xf1\xa7\xee\x0c\x1e\x7c\x1c\xa6\x99\x42\x4d\xa8\x4a\x90\x4d\x2d\x70\x0c\xaa\xe7\x39\x6e\xce\x96\x60\x44\x40\x57\x7d\xa4\xf3\xaa\x22\xae\xb8\x85\x7f\x96\x1c\x4c\xd8\xe0\x6f\x0a\xe6\x61\x0b\x10\x48\xa7\xf6\x4e\x10\x74\xcd\x62\x9e\x85\xad\x75\x66\x04\x8e\xfc\x4f\xb5\x00\xb4\x86\xa3\x30\x9a\x8f\x26\x72\x4c\x0e\xd6\x28\x00\x1a\x10\x99\x42\x24\x68\xde\x72\x6f\x10\x61\xd9\x9e\xb9\xe9\x36\x04\xd5\xaa\x74\x67\xd4\xb1\xbd\x64\x84\x58\x2a\x38\x43\x17\xd7\xf4\x7d\x75\x0b\x8f\x54\x99\x51\x2b\xb8\x5a\x22\x6c\x42\x43\x55\x6e\x69\x6f\x6b\xd0\x72\xc5\xaa\x2d\x9b\x69\x73\x02\x44\xb5\x68\x53\xd1\x69\x70\xad\x81\x7e\x21\x3e\x47\x06\x18\x17\x80\x01\xc9\xfb\x56\xc5\x4f\xef\xa5\xfe\xe6\x7d\x2d\xa5\x24\xbb\x3b\x0b\x61\xef\x0e\x91\x14\xa9\x2c\xdb"; + assert_eq!(out200.as_ref(), expected.as_ref()); + + let mut out33 = [0u8; 33]; + shake::(&m, &mut out33); + assert_eq!( + out33.as_ref(), + b"\xcd\x8a\x92\x0e\xd1\x41\xaa\x04\x07\xa2\x2d\x59\x28\x86\x52\xe9\xd9\xf1\xa7\xee\x0c\x1e\x7c\x1c\xa6\x99\x42\x4d\xa8\x4a\x90\x4d\x2d".as_ref() + ); + + assert_eq!(&out200[..33], out33.as_ref(), "XOF 접두사 일관성 위반"); + } + + // 증분 update가 일괄 입력과 동일한 결과를 내는지 검증 (버퍼 경계 횡단 포함) + #[test] + fn incremental_update_equivalence() { + let mut m = [0u8; 300]; + for (j, b) in m.iter_mut().enumerate() { + *b = (j * 7 + 5) as u8; + } + + let oneshot = sha3::(&m); + for &chunk in &[1usize, 7, 64, 135, 136, 137, 299] { + let mut h = SHA3_256::new(); + for piece in m.chunks(chunk) { + h.update(piece); + } + assert_eq!( + h.finalize().as_bytes(), + oneshot.as_bytes(), + "SHA3-256 청크 {chunk} 분할 결과 불일치" + ); + } + + let mut xof_oneshot = [0u8; 99]; + shake::(&m, &mut xof_oneshot); + for &chunk in &[1usize, 7, 135, 136, 137] { + let mut x = SHAKE256::new(); + for piece in m.chunks(chunk) { + x.update(piece); + } + let mut out = [0u8; 99]; + x.finalize_into(&mut out); + assert_eq!(out, xof_oneshot, "SHAKE256 청크 {chunk} 분할 결과 불일치"); + } + } + + // Drop 후 스펀지 상태와 입력 버퍼가 소거되는지 검증 + #[test] + fn test_keccak_state_zeroize_on_drop() { + let mut storage: MaybeUninit = MaybeUninit::uninit(); + + unsafe { + storage.write(KeccakState::new(1088, 0x06)); + storage.assume_init_mut().update(&[0xA5u8; 200]); + + let state_ptr = storage.assume_init_ref().state.expose().as_ptr() as *const u8; + let state_len = core::mem::size_of::<[u64; 25]>(); + let buf_ptr = storage.assume_init_ref().buffer.expose().as_ptr(); + + let pre_state = core::slice::from_raw_parts(state_ptr, state_len); + assert!( + pre_state.iter().any(|&b| b != 0), + "state 가 비어 있음 — 테스트 전제 위반" + ); + let pre_buf = core::slice::from_raw_parts(buf_ptr, MAX_RATE_BYTES); + assert!( + pre_buf.iter().any(|&b| b != 0), + "buffer 가 비어 있음 — 테스트 전제 위반" + ); + + storage.assume_init_drop(); + + let post_state = core::slice::from_raw_parts(state_ptr, state_len); + assert!( + post_state.iter().all(|&b| b == 0), + "KeccakState state 가 Drop 후 소거되지 않음" + ); + let post_buf = core::slice::from_raw_parts(buf_ptr, MAX_RATE_BYTES); + assert!( + post_buf.iter().all(|&b| b == 0), + "KeccakState buffer 가 Drop 후 소거되지 않음" + ); + } + } + + // Drop 후 다이제스트 바이트가 소거되는지 검증 + #[test] + fn test_digest_zeroize_on_drop() { + let mut storage: MaybeUninit = MaybeUninit::uninit(); + + unsafe { + storage.write(sha3::(b"zeroize on drop")); + let ptr = storage.assume_init_ref().bytes.expose().as_ptr(); + + let pre = core::slice::from_raw_parts(ptr, 64); + assert!( + pre.iter().any(|&b| b != 0), + "다이제스트가 비어 있음 — 테스트 전제 위반" + ); + + storage.assume_init_drop(); + + let post = core::slice::from_raw_parts(ptr, 64); + assert!( + post.iter().all(|&b| b == 0), + "Digest 가 Drop 후 소거되지 않음" + ); + } + } } diff --git a/sha3/src/keccak.rs b/sha3/src/keccak.rs index 5bc36b7..9a8ff37 100644 --- a/sha3/src/keccak.rs +++ b/sha3/src/keccak.rs @@ -56,14 +56,14 @@ impl KeccakState { // Keccak-f[1600] — 24라운드 순열 pub(crate) fn keccak_f1600(state: &mut [u64; 25]) { let mut tmp = [0u64; 25]; + let mut c = [0u64; 5]; + let mut d = [0u64; 5]; for &rc in &KECCAK_ROUND_CONSTANTS { // θ (Theta) - let mut c = [0u64; 5]; for x in 0..5 { c[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20]; } - let mut d = [0u64; 5]; for x in 0..5 { d[x] = c[(x + 4) % 5] ^ c[(x + 1) % 5].rotate_left(1); } @@ -89,18 +89,21 @@ impl KeccakState { state[0] ^= rc; } - // 스택 중간 상태 소거 + // 스택 중간 상태 소거 (θ 열 패리티 포함) tmp.zeroize(); + c.zeroize(); + d.zeroize(); } // 흡수 // `block[..rate_bytes]`를 상태 레인에 XOR한 다음 순열 적용 // 모든 SHA-3 / SHAKE 속도는 8바이트의 정확한 배수 - fn absorb_block(&mut self, block: &[u8]) { + fn absorb_block(state: &mut [u64; 25], block: &[u8]) { let rate_words = block.len() / 8; - for i in 0..rate_words { + let mut word = 0u64; + for (i, lane) in state.iter_mut().enumerate().take(rate_words) { let o = i * 8; - let word = u64::from_le_bytes([ + word = u64::from_le_bytes([ block[o], block[o + 1], block[o + 2], @@ -110,9 +113,11 @@ impl KeccakState { block[o + 6], block[o + 7], ]); - self.state[i] ^= word; + *lane ^= word; } - Self::keccak_f1600(&mut self.state); + // 마지막 메시지 워드 스택 잔존 소거 + word.zeroize(); + Self::keccak_f1600(state); } pub(crate) fn update(&mut self, data: &[u8]) { @@ -136,10 +141,8 @@ impl KeccakState { i += chunk_len; if self.buffer_len == self.rate_bytes { - // 역참조 복사 - let block = Secret::new(*self.buffer.expose()); - self.absorb_block(&block[..self.rate_bytes]); - // block 스코프 종료 -> 소거 + // 서로 다른 필드 차용이므로 버퍼 사본 없이 직접 흡수 + Self::absorb_block(&mut self.state, &self.buffer[..self.rate_bytes]); self.buffer_len = 0; } } @@ -166,7 +169,7 @@ impl KeccakState { block[..self.buffer_len].copy_from_slice(&self.buffer[..self.buffer_len]); block[self.buffer_len] = self.domain; block[rate - 1] ^= 0x80; - self.absorb_block(&block[..rate]); + Self::absorb_block(&mut self.state, &block[..rate]); // block 스코프 종료 -> Secret::Drop 으로 소거 self.buffer_len = 0; } @@ -179,15 +182,18 @@ impl KeccakState { fn squeeze_fixed(&self, output_len: usize, out: &mut [u8; 64]) { let full_words = output_len / 8; let rem_bytes = output_len % 8; + let mut word_bytes = [0u8; 8]; for i in 0..full_words { - let word_bytes = self.state[i].to_le_bytes(); + word_bytes = self.state[i].to_le_bytes(); out[i * 8..i * 8 + 8].copy_from_slice(&word_bytes); } if rem_bytes > 0 { - let word_bytes = self.state[full_words].to_le_bytes(); + word_bytes = self.state[full_words].to_le_bytes(); out[full_words * 8..full_words * 8 + rem_bytes] .copy_from_slice(&word_bytes[..rem_bytes]); } + // 다이제스트 워드 스택 사본 소거 + word_bytes.zeroize(); } // 호출자가 제공한 버퍼로 스퀴즈, 다중 블록 XOF 출력을 위해 다시 순열 적용 @@ -195,12 +201,13 @@ impl KeccakState { let output_len = out.len(); let rate_words = self.rate_bytes / 8; let mut pos = 0usize; + let mut word_bytes = [0u8; 8]; while pos < output_len { for word_idx in 0..rate_words { if pos >= output_len { break; } - let word_bytes = self.state[word_idx].to_le_bytes(); + word_bytes = self.state[word_idx].to_le_bytes(); let remain: usize = output_len - pos; // CT take = min(remain, 8) let is_ge: Choice = CtGreeter::gt(&remain, &8usize) | CtEqOps::eq(&remain, &8usize); @@ -212,6 +219,8 @@ impl KeccakState { Self::keccak_f1600(&mut self.state); } } + // XOF 출력 워드 스택 사본 소거 + word_bytes.zeroize(); } // 공개 완료 진입점 From 61f850d331a8c89d18179729be2ba21fec6971f4 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:23:11 +0900 Subject: [PATCH 07/65] =?UTF-8?q?mldsa=20FIPS=20204=20=EC=A0=95=ED=99=95?= =?UTF-8?q?=EC=84=B1=20=EC=88=98=EC=A0=95=20-=20sample.rs=20CoeffFromHalfB?= =?UTF-8?q?yte=20=ED=91=9C=EC=A4=80=20=EA=B5=AC=ED=98=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EA=B3=A0=20rej=5Fbounded=5Fpoly=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20-=20pack.rs=20BitPack/BitUnpack=20?= =?UTF-8?q?=EB=B6=80=ED=98=B8=20=EA=B7=9C=EC=B9=99=EC=9D=84=20b=20-=20w=20?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mldsa/src/pack.rs | 4 ++-- mldsa/src/sample.rs | 28 ++++++++++++++++++---------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/mldsa/src/pack.rs b/mldsa/src/pack.rs index fadbc8b..6effeb5 100644 --- a/mldsa/src/pack.rs +++ b/mldsa/src/pack.rs @@ -64,7 +64,7 @@ pub fn bit_pack(w: &Poly, a: i32, b: i32, out: &mut [u8]) { for coeff in &w.coeffs { let signed = fq_to_signed_ct(coeff.0, b); - let encoded = (a as i64 + signed as i64) as u64; + let encoded = (b as i64 - signed as i64) as u64; buf |= (encoded & mask) << bits; bits += bw; while bits >= 8 { @@ -94,7 +94,7 @@ pub fn bit_unpack(v: &[u8], a: i32, b: i32) -> Poly { buf >>= bw; bits -= bw; - let signed = encoded - a; + let signed = b - encoded; poly.coeffs[i] = Fq::new(signed_to_fq(signed)); } poly diff --git a/mldsa/src/sample.rs b/mldsa/src/sample.rs index dc1d40c..1c7e3c3 100644 --- a/mldsa/src/sample.rs +++ b/mldsa/src/sample.rs @@ -70,6 +70,20 @@ pub fn expand_s( Ok((s1, s2)) } +fn coeff_from_half_byte(b: i32) -> Option { + let mut val = if ETA == 2 { + if b < 15 { 2 - (b % 5) } else { return None } + } else if b < 9 { + 4 - b + } else { + return None; + }; + if val < 0 { + val += Q; + } + Some(val) +} + fn rej_bounded_poly(rho_prime: &[u8; 64], nonce: u16) -> Result { let mut seed = [0u8; 66]; seed[..64].copy_from_slice(rho_prime); @@ -92,20 +106,14 @@ fn rej_bounded_poly(rho_prime: &[u8; 64], nonce: u16) -> Result< let z0 = (z & 0x0F) as i32; let z1 = (z >> 4) as i32; - if z0 <= 2 * ETA { - let mut val = ETA - z0; - if val < 0 { - val += Q; - } + if let Some(val) = coeff_from_half_byte::(z0) { poly.coeffs[count] = Fq::new(val); count += 1; } - if count < N && z1 <= 2 * ETA { - let mut val = ETA - z1; - if val < 0 { - val += Q; - } + if count < N + && let Some(val) = coeff_from_half_byte::(z1) + { poly.coeffs[count] = Fq::new(val); count += 1; } From d2ff363bebf87ed6e7d0fc1434058a126c636e1c Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:25:28 +0900 Subject: [PATCH 08/65] =?UTF-8?q?mldsa=20=EB=B9=84=EB=B0=80=20=EC=A4=91?= =?UTF-8?q?=EA=B0=84=EA=B0=92=20zeroize=20=EB=B3=B4=EA=B0=95=20-=20Private?= =?UTF-8?q?Key=20k=5Fseed=20=EB=A5=BC=20Secret=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=90=EC=8B=B8=EA=B3=A0=20=EC=82=AC=EC=9A=A9=EC=B2=98?= =?UTF-8?q?=EB=8A=94=20expose=20=EA=B2=BD=EC=9C=A0=20-=20keygen=5Finternal?= =?UTF-8?q?=20=EB=B9=84=EB=B0=80=20=EC=8A=A4=ED=83=9D=20=EC=9E=94=EC=A1=B4?= =?UTF-8?q?=EA=B0=92=20=EB=AA=85=EC=8B=9C=EC=A0=81=20=EC=86=8C=EA=B1=B0=20?= =?UTF-8?q?-=20sign=5Finternal=20=EA=B1=B0=EB=B6=80=20=EB=A3=A8=ED=94=84?= =?UTF-8?q?=EC=99=80=20=EC=84=B1=EA=B3=B5=20=EA=B2=BD=EB=A1=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=20=EC=A4=91=EA=B0=84=EA=B0=92=20=EC=86=8C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mldsa/src/keys.rs | 26 +++++++++++++++------- mldsa/src/sign.rs | 55 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/mldsa/src/keys.rs b/mldsa/src/keys.rs index 727da67..641a7f4 100644 --- a/mldsa/src/keys.rs +++ b/mldsa/src/keys.rs @@ -10,7 +10,7 @@ use crate::poly::PolyVec; use crate::sample::{expand_a, expand_s}; use constant_time::{Choice, CtSelOps}; use sha3::{SHAKE256, XOF}; -use zeroize::Secret; +use zeroize::{Secret, Zeroize}; pub struct PublicKey { pub rho: [u8; 32], @@ -19,7 +19,7 @@ pub struct PublicKey { pub struct PrivateKey { pub rho: [u8; 32], - pub k_seed: [u8; 32], + pub k_seed: Secret<[u8; 32]>, pub tr: [u8; 64], pub s1: Secret>, pub s2: Secret>, @@ -73,8 +73,9 @@ pub fn keygen_internal( k_seed.copy_from_slice(&expanded[96..128]); let a_hat = expand_a::(&rho)?; - let (mut s1, s2pv) = expand_s::(&rho_prime)?; + let (mut s1, mut s2pv) = expand_s::(&rho_prime)?; let s2 = Secret::new(s2pv); + s2pv.zeroize(); let s1_original = Secret::new(s1); s1.ntt(); @@ -82,8 +83,9 @@ pub fn keygen_internal( t.intt(); t = t.add(&s2); - let (t1, t0pv) = power2round_vec(&t); + let (t1, mut t0pv) = power2round_vec(&t); let t0 = Secret::new(t0pv); + t0pv.zeroize(); let mut shake_tr = SHAKE256::new(); shake_tr.update(&rho); @@ -98,13 +100,19 @@ pub fn keygen_internal( let pk = PublicKey { rho, t1 }; let sk = PrivateKey { rho, - k_seed, + k_seed: Secret::new(k_seed), tr, s1: s1_original, s2, t0, }; + k_seed.zeroize(); + expanded.zeroize(); + rho_prime.zeroize(); + s1.zeroize(); + t.zeroize(); + Ok((pk, sk)) } @@ -143,7 +151,7 @@ pub fn sk_encode(v: &PolyVec) -> i32 { let mut max = 0i32; @@ -258,7 +258,7 @@ pub fn sign_internal< shake_mu.finalize_into(mu.expose_mut()); let mut shake_rho_pp = SHAKE256::new(); - shake_rho_pp.update(&sk.k_seed); + shake_rho_pp.update(sk.k_seed.expose()); shake_rho_pp.update(rnd); shake_rho_pp.update(mu.expose()); let mut rho_pp = Secret::new([0u8; 64]); @@ -271,7 +271,7 @@ pub fn sign_internal< let w1_len = w1_bytes_len::(GAMMA2); for _ in 0..MAX_ITER { - let y = expand_mask::(rho_pp.expose(), kappa, GAMMA1)?; + let mut y = expand_mask::(rho_pp.expose(), kappa, GAMMA1)?; let mut y_hat = y; y_hat.ntt(); @@ -295,35 +295,74 @@ pub fn sign_internal< let mut cs1 = poly_mul_polyvec::(c_hat, &s1_hat); cs1.intt(); - let z = y.add(&cs1); + let mut z = y.add(&cs1); let mut cs2 = poly_mul_polyvec::(c_hat, &s2_hat); cs2.intt(); - let w_minus_cs2 = w.sub(&cs2); - let r0 = low_bits_vec::(&w_minus_cs2, GAMMA2); + let mut w_minus_cs2 = w.sub(&cs2); + let mut r0 = low_bits_vec::(&w_minus_cs2, GAMMA2); if inf_norm_vec::(&z) >= GAMMA1 - BETA || inf_norm_vec::(&r0) >= GAMMA2 - BETA { kappa = kappa.wrapping_add(L as u16); + y.zeroize(); + y_hat.zeroize(); + w.zeroize(); + cs1.zeroize(); + z.zeroize(); + cs2.zeroize(); + w_minus_cs2.zeroize(); + r0.zeroize(); continue; } let mut ct0 = poly_mul_polyvec::(c_hat, &t0_hat); ct0.intt(); - let neg_ct0 = polyvec_neg::(&ct0); - let w_minus_cs2_plus_ct0 = w_minus_cs2.add(&ct0); + let mut neg_ct0 = polyvec_neg::(&ct0); + let mut w_minus_cs2_plus_ct0 = w_minus_cs2.add(&ct0); let (h, h_count) = make_hint_vec::(&neg_ct0, &w_minus_cs2_plus_ct0, GAMMA2); if inf_norm_vec::(&ct0) >= GAMMA2 || h_count > OMEGA { kappa = kappa.wrapping_add(L as u16); + y.zeroize(); + y_hat.zeroize(); + w.zeroize(); + cs1.zeroize(); + z.zeroize(); + cs2.zeroize(); + w_minus_cs2.zeroize(); + r0.zeroize(); + ct0.zeroize(); + neg_ct0.zeroize(); + w_minus_cs2_plus_ct0.zeroize(); continue; } let sig = sig_encode::(&c_tilde_buf[..c_tilde_len], &z, &h, GAMMA1, OMEGA); + + y.zeroize(); + y_hat.zeroize(); + w.zeroize(); + cs1.zeroize(); + z.zeroize(); + cs2.zeroize(); + w_minus_cs2.zeroize(); + r0.zeroize(); + ct0.zeroize(); + neg_ct0.zeroize(); + w_minus_cs2_plus_ct0.zeroize(); + s1_hat.zeroize(); + s2_hat.zeroize(); + t0_hat.zeroize(); + return Ok(sig); } + s1_hat.zeroize(); + s2_hat.zeroize(); + t0_hat.zeroize(); + Err(Error::SigningFailed) } From de5e1f827b3942683f90559415e9d34e2b705778 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:27:20 +0900 Subject: [PATCH 09/65] =?UTF-8?q?mldsa=20FIPS=20204=20ACVP=20keyGen=20KAT?= =?UTF-8?q?=20=ED=9A=8C=EA=B7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=2044/65/87=20=EC=B2=AB=20ACVP=20=EB=B2=A1?= =?UTF-8?q?=ED=84=B0=EC=9D=98=20pk=20sk=20=EC=A0=95=ED=99=95=20=EC=9D=BC?= =?UTF-8?q?=EC=B9=98=20=EA=B2=80=EC=A6=9D=20-=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=20=EC=97=86=EC=9D=B4=20=EC=88=98=EC=9E=91?= =?UTF-8?q?=EC=97=85=20hex=20=EB=94=94=EC=BD=94=EB=8D=94=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mldsa/tests/mldsa_test.rs | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/mldsa/tests/mldsa_test.rs b/mldsa/tests/mldsa_test.rs index de9b470..bbfec53 100644 --- a/mldsa/tests/mldsa_test.rs +++ b/mldsa/tests/mldsa_test.rs @@ -103,3 +103,74 @@ fn test_context_too_long() { let result = MLDSA44::verify(&pk, message, &fake_sig, &long_ctx); assert!(result.is_err(), "검증에서도 오류여야 함"); } + +fn hex_to_bytes(s: &str) -> [u8; N] { + let bytes = s.as_bytes(); + assert_eq!(bytes.len(), N * 2, "hex 길이 불일치"); + let mut out = [0u8; N]; + let mut i = 0; + while i < N { + let hi = nibble(bytes[i * 2]); + let lo = nibble(bytes[i * 2 + 1]); + out[i] = (hi << 4) | lo; + i += 1; + } + out +} + +fn nibble(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => panic!("잘못된 hex 문자"), + } +} + +#[test] +fn test_mldsa44_acvp_keygen_kat() { + let xi = hex_to_bytes::<32>("D71361C000F9A7BC99DFB425BCB6BB27C32C36AB444FF3708B2D93B4E66D5B5B"); + let expected_pk = hex_to_bytes::<1312>( + "B845FA2881407A59183071629B08223128116014FB58FF6BB4C8C9FE19CF5B0BD77B16648A344FFE486BC3E3CB5FAB9ABC4CC2F1C34901692BEC5D290D815A6CDF7E9710A3388247A7E0371615507A572C9835E6737BF30B92A796FFF3A10A730C7B550924EB1FB6D56195F02DE6D3746F9F330BEBE990C90C4D676AD415F4268D2D6B548A8BCDF27FDD467E6749C0F87B71E85C2797694772BBA88D4F1AC06C7C0E91786472CD76353708D6BBC5C28E9DB891C3940E879052D30C8FD10965CBB8EE1BD79B060D37FB839098552AABDD3A57AB1C6A82B0911D1CF148654AA5613B07014B21E4A1182B4A5501671D112F5975FB0C8A2AC45D575DC42F48977FF37FFF421DB27C45E79F8A9472007023DF0B64205CD9F57C02CE9D1F61F2AE24F7139F5641984EE8DF783B9EA43E997C6E19D09E062AFCA56E4F76AAAB8F66600FC78F6AB4F6785690D185816EE35A939458B60324EEFC60E64B11FA0D20317ACB6CB29AA03C775F151672952689FA4F8F838329CB9E6DC9945B6C7ADE4E7B663578F87D3935F2A1522097AD5042A0D990A628510B6103CB242CD8A3AFC1A5ADA52331F4DF461BC1DA51D1D224094E7ABED3D87D98F0D817084780EE80370F397631ECB75D4264B6B5E2E66C0586B5FB743516399165837A0FDFF7C6134F033BFA69C1B2416965C6E578592F40E258CB6DFB29FB8E0F54355B6E24A65F67ABAE3193D007115CC0B9FF94CB911A93B1A76C0E7662F5E2B20139E0159ED929CB932D4895F89A02E55C59DF2DBB8F6E5DD7D5B1F3CEC37B4A9166B381C5440E23E67368CDE0A29D59AA05A3C9BE24A4DC8DD75BE30E82BC635D36AAC66DE880C6701A987D7E05F0F2FF287828BEC30595089D8AB9AA390ED719CAA6E576CDBBE9B184A322E5E2DABB69C23CC696D54FC32FF57001B6B64E2A837F3062D85AEB50B3510F7EDFC34DF38E083D4D9B94FFAB0DE15D73D9AF30B9F31CC4F41C9C24F2D618B2A7C3C4BDFB745D52D3EB54589C8BDA8AC05DAD14EC744505575A0988EEC651C1715439FDFB29923380A43C1A66A86C982A841F11820A6A0E1E2F2FFF5108ECAE51A6AABC9B949226D228FF84C4E5E5D63114D80359C4931E612DCED1838B7D066AC9182CECFA223A21A4C8E155AEFA780373BCC15098AEE40C033AF22F8E7C67A0D2526DA7475E830308C04AED9D32BCCC72E719EE70A8D13F09AC11E26EA237D5CC8F98B5AE0E54F933BD0507942ED900D056FD32F8E6E81777912FD482746029B71CCE3BA69B8FC2D03EB441027C387BC2F95031A0AE7052215EB24B9EA8FB0A961B0F80BFA80D0D6257C1C22B508C5D31B97FCDFE1D1766E8A9C8771932DD598ADB7E717743F45FC571F21E4A516249F81D747F15329790F0F70A0B8E461A4EDF50504AF03F30DDF8A8818E38761E1681D6DDEF0B1DD326B2EC228CE48570F285B49D29D7C2EF37866D5446DF82B8E43B34CB248962A21A9A3946159740F8AEE8E6A16A4EB2B42D143FE2612E05EF4B5E646D813248444556A2A8BF92CE10BADECB6B8A40B080DD42D53346FEFCC4B9B40B1E4998991EC753C95AA2F2A506F311E710B0F1D36C1DCA6644EE6D1D4AE9CEA5666EF4B3E888DBDBB95A77ECFE1E8B477DE7CB07639D682D53020EC14EA6C7DD7E715389D10938429FAB8A068A1466A4CD891359F8074E0F5A142ADD731B87878D985E4FA6ECB3B73D298553418273E9503AA84092C080E5F2902F90F5C59944D24CA0271D11D0D6734606D039550A37FCA2B735850E63F540F2F06B79144B5C4ED2C700BB51C33D265B3D037389C99EFD597642D829DB1EB58643CFCD07F4DEC60B8F727D97BD7C4B59BDA1", + ); + let expected_sk = hex_to_bytes::<2560>( + "B845FA2881407A59183071629B08223128116014FB58FF6BB4C8C9FE19CF5B0B28965F58D99EE0DE7BFB7840F59F65414289E259E05E8A18D47EC06D7900284AD7A0AE404B93B0498945ABD07638920FC7F8C21282031EECEB0CC48D71FB0AD1CE84B9B3DDD1B3A2A40A15D5BFE0BE23FC4757137C6C8FEB7F8055677BA4AF2814242A9AA0108A4046981800042130A2340A99140E01A931E1300A1A136959486601C111140451244206E4C848A1B80008B2490121901224500B03306112025C144912B30049424D14470E93342023898803238CA1C081CC0411184545C1C20862465210300D52B870A3B004DCA20D4A386511214A01B48C43104809461253068161348618A00920878CA0168682208E41188E52A29021014652184521A42902132ED0B8248816710AB245818480942670DC126C1BC16C4016246236008996108B124E1B3721639225E1C88CA346285A32111C856110B9295990910493651CC40063B810C1184A52900C1CB20D2380090A4021D4188043A64D0835060C98650CB4890C8661D804601C0789D09231D8081100881119294E48B661D8962DD2B04121A1051226860349301A246D99483001200C5CC2840128900A298850340E1804640AB409D1968413196DD91031133689808025E300294A06304448109B429008858902A20D13461110120EDC380C4C468842C004131490C1462248B82D48C80D64346C02348109136842A4211B26511C302A6328688B18609C922C22C9000031058AC08C894430D0142189424C4C106400B64DD2A8246306308BB4289B246EC34262A0C00C0C2050E112444A360AD122320C36014836804C00400C16090C9569A19829110602CC982D08C811498445D20290014942C1062E42C000A2902D9810711A054008058608B2651B36441109402107100B3306D298858148088034305C3844DC266ECA947004394A442841E1464898862D113621221509530826213088DA94804A80601425321295641B02120A92241A4422230624CC12818922600036608A32820CC368A1A04D5B286A110744589851C42831A4064EC0C831CAB46810B669132488840462A3C421E0920523303050B6105942285C3005102286CC260E2226450003928A242C9C024E40B20400148C4A086E4BA6444CC84924464D4946821AA4690A316E21408C22964949044618B72C12B864D1C851C3366E5C2008E3328513198564C2019A4822111300A4B64960C06D1944628C086119B761021570EA732640984F68143D81AB95763698DCD02625A980994C19B7D47626409370F87E45D39B76B6762F7827B2779BA44046F54228A7B1BAC9D8E751AC2DD248521564F81D028AD671DB6A3A5BA3A9463597A519AE0C1DA435FEF7C421CDE7F8483782C4334993192923796DDC3E5803F6C86CF2B37E39BBCB53775E5E9D2BDC65E84395B01AF95C4398503A7E63A8D72F7E9BC58D7655CB86BAFA0B679D1D791F09703F6E4B333A51DFB14B36EAC9A3582F795AFCECC2A67A42C7BC43B447B73EC8B73C9164B2D9EF4F767D607E975738E1D871FE61B7BB9570516959B9E7D8155455FC91590D84BD430921568B718241CCD09C989DDF75F529C4F15818969216EC0FBABEC64F184832BFE428BE44849C42F13CA84A5CE6CA628044CB9DA3FD9CB5EBC776BA0E12683B98253B213B286593779EACC489C985BEB734A7506791BE1A6E6E03B557CFEF2069091B846CDBBB72D36A89FD5C45C7C7FBFAED956EC9D8F3025225D6FEF811B1CC3C6705F056388BBFBBA1FEBF4D072281DC792ADA048216D60D0D6044150909B70A4753336F4D5D3FDB8BACA62946DB28332EC778A76F96577BAF868BF896773C200637495098B0F7749835915348D37641EC74D5A78E565D8100627468B0B5D05C2937618A98E5A12476533CE9D699E0F4F23B3127EE1F8D3CDE3851A94BEB8D606D836D47A9F090581B660FF8EC31E3EF4E45DB530DD88A44D48F84B6BD3CED53BE935EFBCE9930DD83BD004F086A064B3260B7211BA0547CB29E56603B922597B31A30A5FFCBD8A7AD2F3F05372DCEA89048C683164410E56A6E5B7EC2EA6E3B0176DB1EBF9258C97C297E752A202B1B4B02DA47235873CC3E4A376B655779AFFFC9AEF2E30B62BE022AEFF5E34B468507C921A00F1110B2CC1BC99B2EE295A31EAC141AB4D87EDC11D7FEE22A07D69B7008C107EBE47DD2F215D074868C8B929A3DF6614B5E95CC8A52F96C57E1697CEE332F45B1980E6CA372574BDB51825654E6FF31D8E755CFC15B602F46A0570D3BD0D65E43BDE8F792E7D526B3D362FE33638B3870E1C29E1030D3DBA43C3D0DB6E5F501EA3441B709E54FCA1FD3E8654B1A9301AD151A36C8FA1513D0229DE6E94D02A97B9D6F2EE8078A5D1228E783B8FDAC46102A19600C531276B447F3900EF8F723FC3B60D7BBBBDA9F949E4460B2F1103315ACE25B3497CA4B4674A6052304609A16E90D064B0F6A9EAE8D8E4BE38E3B781F9173654E876CE6CC00578171FD9CFDBA5704AF2E60CF84FD02EBEF19AED140287D195023000568E8D908EDEADC76BCA3E7FE8FE1F4B201C2A15603C025A61917AE664B4294C57030AC988D7967364A8BB5857E98218C2943E8CF1CB8C719DACCFAE8F42AA7DDE9B4130170068A9CC86B0552D1DD0A48F63DDB8F8A51B3C0E10F3174A87D18F96533CF0C30C0BE33D37C25CD3EBF8F5F10E66464AC2FB8260BA8B5BD443ABC6AE0D5F89DAA42BC71B2A9BC2D10140B117E32F41353FA1ECEC9DBCCB621350806C4E4EBFC966AA7122D85532728EF26AA40E55E2E1739790FFA157EFA4E2A68DA1EA4C9E3273CDE11DB7DA47220123C40D50B73B8460CD93CB449783EAD8A58F6FF272275A4F6C0437DAF3DF1D8D9617743D9302E73B715AA7CAA139CDD07EDAD2DE7B32B3F11E3C2B6A96D4630632C6D1DE21473A287D42B52D210CD4EF808A998148817BB5083BDAB3A3354B084C5773970B15DA56F755E1F25354EFEFF89FDF4E307998D623CED32C7B9E942B6816A87A233867E2BEB84582FFF61AF612BE6E521AB3005E17D471A5C67A0A927211802CA4A0C2FEDC01A12AF5E8A6E8FF7BD08883AF1D35C2F8570091D9E5FCD9D720644F0A45042443D2FAC309FA7A15B2DBBA1517EA19407B17CAF7E9EA81FBCE42F35A0568B2F70B279E12DA48858F0E0FF0B5CB3A3012308AB83DB627F5439E3A424D83F9E0B7750F2CBD9CA4AFB9BA77765A1EA590CE9E37BC7EABE1EF099765CE59EA74C2776ECCBCEB895997F577733C609B9E853CF03010C5B9CC4E819C041ABF11A9B3866502BF6AC0354E6CE1060D5155E6E5A1AC4FADF73080B20569317F9E118B97C78D7273300E329373E5B64A4181C4AC7B8933F82ADE505D52CDDCEBA373A54D072BC04A8D83273586615ADDC4D6F258BFD75AE94EFD120F20B721A1CAC33FF3446E554D6CFE266B05E4CD44DE77D263A9D347F53B282A7BCB1511DD405A67AC05E7233F5EBB40D38F953AC903F90E96F17A6357EB59ED7900FFD82F54C3043D37F7774A0F4DEFAF65887EB53FE156FB26794982B1249BAD27C89C136097AA2268347FC9C5E5BE79E23C923215353A850E63748E766", + ); + + let (pk, sk) = MLDSA44::keygen(&xi).unwrap(); + + assert_eq!(pk, expected_pk, "ACVP keyGen 44 공개키 불일치"); + assert_eq!(sk, expected_sk, "ACVP keyGen 44 비밀키 불일치"); +} + +#[test] +fn test_mldsa65_acvp_keygen_kat() { + let xi = hex_to_bytes::<32>("1BD67DC782B2958E189E315C040DD1F64C8AB232A6A170E1A7A52C33F10851B1"); + let expected_pk = hex_to_bytes::<1952>( + "43AD6560D3BB684667A559EE6EC7C816020E5B65671F270F2353A8C912B6C26B0DB0C2CF42DC747B10AA3EBDD573B300EEA46C4200B210094F9512119A6BB837242762B2CE94C2467278500EE7B139BED906676663355B813A9AD9D3DB70F7AF2D785040BFD51208BD3D2CFB09EAF7CEDF77D1B59DA75F7728F120C11898D9EC2CB22C73EB8F9436FF60524B56EE6B413030EB7DD10774261452CD8C5ADE75D1967628078CDA77E2B1AFB83B9F07F6939D37FF54D5E10ED17FF8A3C21546A89F514576AE780DE8761C4F2EA28828C69E38C730ACAA4CC8DC7DF63BA4C1525510FAE2C8E1B01812358BC5DFC01E955294A5DFDD1CFF0519E20B8F74FE18854D80C86051AA5CC2FC1DB078BC785BF4BAD6832B8C269156509B332038B4C3719DC49814FC6B6AD5360E945AFFF4D4AC235F56C7F7A9A872B518C1F0D48184DA0EB318F74EB84C4F324A2BD03704D2E2A59F64A8854C7AEFB2D3530E20C8AE8A487E6CBEDA645BD86A5A83E77A6A22888ED8E43A7F4804C2DE187F1ACBA3CF55CF99412A7A59CF77A4A977724A72686FDF7FC64492A5CB75921AD014EB727EDA1DFA7BD7ACE52FE292322F0BE0B004DCE44BEAA20FF06A7691DC36405361F9240DDF2FD1A5EC422ED639505AB8E137B971D5729B11E84C040247424A51DDDBDBC43AF261D038B0CD70D5BF44252A3786A26AF3FCD4EC100E5CDDE019F17BE6A64F820C3F622F78D4F56A984122D6FA2D438D548DD87B9095F1FF02437854E2419A0316C33EAFFA0161737E476A9E707CC40E78686D6A043DDE962B319BE2BF9F7A1EFF9EDEFD1B4CD07131494C084083BF76181E3EB1399929314473A75E199AC9D5444DB0CEC07E625EC70C6864093961950987FB1E96DCB7E001209865D66D829CD2E2B240818CACE003C9CC74DCE5151C65E59AC1EF6D495B0C717B4412C70B50CF44F44E648788F46BAF6F8AF3361F0E4B6119EDF6374DA596453169B935E1A3B875A6C1B9FE384AF961860514E8CF291D8650D7530DB42A46790649B5D8134AAEC33A41F0AB4296AE26203291F1C2BB5276AC305269778E7F2A4BAC15B5A31A6B6B76342596D39C7FD3D1C518689372EBD20B667BE5EE2ED11BC107A7600EDA1BE7A5DC05BB9F16D2B8BB1C7D8D10050207530BFFDAAE7B11E0615726F2E99CE99D6CA6048F9D61B14F7265473EC2D02989772B3D7E212AA68D89374C6CAF7AB160C6C5E09502049C3D03738D700457F706341DDEAFC6CA739ECFB4F193EA6B385B035EEA0F7BFD61FA776AF32AED6366E6C0642D1A01759FA6BDD295F7D18CA6DA1D48563EEE403F2F8BCB6A60326C481F12F8180B2B8117ADE61C7E29F5254207C5D4657B82BE4EBA436752EC7DA0627FCED830C15F10FB8D3CD90B4505FA325B54D954C5B6301DA72B262B226EAB2E4EE88226CC606B97736260ECB6D8F74A0440AFD5D751A90873FFF00C8D3E9CA0975F303F7AC263B8FF496C6C8FD22E8EF7B587BAB50A7DAD99BD55D3B7968584F1FB21255EE22DB56AF6034F3F13E659161A57CA8C9F2E87CA96BD7100FCEF8F74A8C6A1C92E2EFF74E2F5FAE512C09D26E0F3985D882401EFC54727BBC0F4E1110771A106898692D0C5A6997CA742846FA4D49E8ABDF123D92743E9949BEB6E46B9655EE698C23D74991C96067DEF06EACB981AD4A7A5ED91EACF05D374C74C443F3FBAD363B2450A1A47AAF2954D36E53B06345139138D38B941298982EEA84400C4DCC38F5127951906EE3E40F75A5DFE09FCE9BB0143A5D5ADC3C402F23A75A423AB98392CA3A4D5D23D3BCD56FF22C9612A5D2C223C7079958CD05175AA74DDD21B42051CDBDC14048CB43CB2F6535E2CA9F5B87052F633976F4795CBA69D39F2481CFB9D210C9B0E9EFD941AC875A9A6C3E839EC54F55585721DE41815DDFC05E8A58C97E2FA52984135AAB0931094FF8400CAB043C2A5E63C2942B7D36988C4ED9B73C11D913E758ADF94291A42743E4FB04C271ED5807EA03271EA6656CF967AB2595588B55F82AF2D07ADFFCCF859ABA70B1707B722DA1FF393CC5BBCC02014C0D4500655577946DB5F95EF1E7657DC98402E5CB048DCB372C9277FDA4D8F3A30C953822474CEEDA670D5E680029259260D91F8737CF7572651FB28A7DF46F671679BDD507696B021C2C7F4300F3098FF9460582DB58E122C585185BCD091E7ACCB608F7E0C3558627484529A662C0528D419248B6565D32ECC78F7891DB5BB1984CDE89C1AF25F0927205E734A7DFEB9AEE94F23F2FD11FB53EC768F6B8268E00E4054CD12EDE4832B07A254A4E2241854E8FF2AE1E1B248F9EB1C77581CA2A2EF2D4C9171177A1E040F9D4AD8D0D0C6CD14FCD13B233794E51704B6890C56BCE1B8CD1C9EAE6D59ACD91EB67B3A618D65F0F94E5458271E14DC6F6530AD0EE8B2B2F0CEC14612E563338E241602B997EC4E62C83942C7F18DAD6841B1348CAB99A78F598FE78A20205D88D826D2E163F6B628B266C187B427F253000E4EF99FEC0494A97D9B42E37EE613767D2651FB7CB2B9E99578CE2D78B9C9777C954DBD1D7BE8B568F88AB42DDFD293BE28747103B052AD81D8F6254E426802516500111ADF0A8F27AE7C55D3D5DB86278FAF58B68A26D12B2801AC28EDA87AA5D692EDA9BE08F7CC3E78517299A3FD9CE2A0A893E12D71062AE2514C465D399F165E4D2F71D1913D8B95396681486432B090F0CCE86AA84B661FF22D4A56035E821A1CE30F33AFEB6C7B8FA9CE", + ); + let expected_sk = hex_to_bytes::<4032>( + "43AD6560D3BB684667A559EE6EC7C816020E5B65671F270F2353A8C912B6C26B3DAC8325D5208FBBA385BE4D9C77611961D796200072F900727D715515AFE3A3839104C526A5806257D54D22CA1035F17DCA06D2CECBCFAB310C994F7D58E62CD8B9A5CAE7FEF2775FB612246459A94DEB8BE511BAD4C007C5607FE1C9CDB7C91871042734805760071827016058751083551404015714520370521746384075612325660115585060271425754524300210322561021486801010162177780477202683415726137828571450344171475716007373235645487001382355682365241402741446735211762873740822880881412810353573148554078386048742028384216832477015164257364347170070476826425234761712102353485186636564574447243771201641836834385432107528153272134368623722717823427554560072672463206348216547035015641236782111200887127213301537550124661416832623134008275127301110438274170284760336777540767006607015458542733644775276530314810335267270536635024462712033307648853181526103811626342733747741152737243631230364010563017427324681651021264141247752460772130064237162780040136355137636372534687431201613276233550463451322058364785885052601380733632615061100465258855854864707833387285270253717572283677208267866324426488886645676412153434442408831512432143710810473648751735011822833662424478745475884038716125567184208768860650416622351137275746336756553777453220673618100250174607842037110877466573677678706353178267287086771186367745572287304642684357468807241650421456533613420681644021216844631405340281021861520627758706484238277807350637843857572806363145528876076540884216485115253516510151067614034047278055348333071554060501056777512544758313056213375015458472237554531301468550435046337758252140465058317826818578051023048434771488433303371860434612207682051681007274235504651051105036485727048777861140347683323582177270861101745674724017721068503801314751007050330681375451457311657155115524167058165654376648748467456025718674620018777736488172045365831431275647364001826642826734268570001877424401811743854565183268240488347571103622607068764060037426823556464876211866704285442566432428611512475760138184868586371047026434127363468435820656468354150167168553621221763128784271170314625350386132370263571064140316816542013815265271072675687266120566678877825231880065251181231633822285122458443015318633364000784236458252384030804427720857853028048800238108156324700020705834462866200751015435325642622554202314710477450768266688562681300811782175711530744484640405743387208555323670078410875477644815740410477021318650866587571047373875644673507143062246681336181716752823720657328628827480281562757760882401671872655304836836112543587554817442804721806221028288400170254073756017317640577647108071763844565245115036540662834220751655287342221621752356237133878057213237585028728713738001774675108141608126487445165350021324223054841153544842584453322153837566711443274846273473770210668325262283138640602101707206151144184503676815802138832302751472227117561542036262326615431338165116758685720162285157116377643231431243852733776584261054544005228457187605483018422535700074218213083153112480627325618021284611307255002061572654863673186621261024440273457334313817453457750444B7169D30417F278FC99324BEDA10F1E123855584D14CB6C067B8E8D699C0F991FBD7BB1791F7B30DC418483BB316CF9613E5BD127EA122632DB2F204D78DF5278020117CDA39DD5A0EFAF087C794387A7EF66B8420B966B2326A802B34A3E92F71BED842371FDAE6B0D61D5C72DAA85AF99FB4C97B14E2E4BC4BB11D3B3434ADA822CD786F43DA8026FF857FA82D2C32715DE8DF951855EAA8644B2BDE0CD5E8F8B40624DE06C6BA60C4E50308A757C1B7B24B635DE0CFE2EFA64F14AEE1DB19A8A834C2C5B9C45CFE2D1842DB27B471E70D176735E05D4279270CB78D460834CDA4A25E344E5C5E1A03A224C3857E4912F944AE618B1F8387E8B2475C9A20D59694060BDCF3D83ECEA1A1445EE1A33F9C2ED52C39642BCD39C1D4B09062D121D028B220491A6B3C66B81442C5AC20B1BD3A047F1E57330C0FEAC6FE5CF725FC76DD3A84EC4FA9C9164F2061494AA89B0367E4A24B08FE656397E0924EC373898F2AA25FC62A9E271A0DA015D4E4FF2189654594D574B8F8884DA9991A6F54A2F13DF15F8B0F67D7491207539488505D591964B4A1F22650E881E74C84255E81D016F5A99AE559686098997CCD1AD102BE86AE4A1CCED1EEB057CF89DC103CC4026E4B244640055AE44B08A85C3C3A0617998A17A533ECA6904677D351E1C10575032FE493577AAFBDF32EE4C8784A1DAEA2F04EDE5E6ED69F6CBE75D1830EBF74A3F93C13A199895A4C5528BBA9A3DDC3BE87EAD4E2FD9AA951A9F5749502CF89FCB57F47E95B479372BFCD80D0203C9680D94B43671C12B35C511A6CCC24A284B6563C6F0833FA5E82261831C4E6D3740135200CF6D10342A808AE47EB0CFBBE2E9912B7EC2E53E69C3873E21D52B3993E080B7926E95E167EA3EB9D3E52B802B03BBE35CAB65C938ACA9DC7C282A0FE2E0EAFC9E27A5945B2EF7279145DD5A4F4DD63C30CC9728CB0849D5BD35D43D604B42EC4D76C0120A38FF97A495C38D60F573FFFDC704367618B919791A6806A2B9DA834C668097911568C554A821307E5CC522B6E607193C1CB7952E221957F2684B4700B5AFBFCAD7852735AE040D63ECF483FA2ED1BC358A274E80695AD706B47BDF7B8BEC73DD99187B8DB51E6CF0D04BCE594BB7EAB045C628B45C195005BFFF6F43FC61C2D617FFFA5C06B195B7284F15D009800856149930309E693060381F6958CDDE8EABE65B87FFFC6623D239EA72278E69AD8C505B16073C8A035F2BB9458FA38A6EC27B7D30E9F40B82F657B21B4CAE98FCC7E366EB20199BEE79BDDEDB3629A43FDB2ABF59CD2E37CCF15C59234DF85B15A1C6440F3F66DAF010E6BB7A3A0C1DE62CBBA224BCEF2C9233702B92B124A29FE54E45CA2C7BC371D52D9A9E11AD0C84771A0181378AC896F17398E6583B84A3E495E0AEDA09875EF4B676AB813C8C9DDE120203BFDDFD9B6412F5A10A49109921185D4DA5D89E455B036654F6108FDF36701A36538EA04AF2855C8D2A6E7810A45BCD4F8940320C8C1CD179070307906E016815E9F37D3A32943E164DEADB8C1A393EDEE5ACEAC9C7153B55805FEF3A5EE27234EF040B8F269FB96D3659044C8CAC05659F3E9F603C3C165EA9749AA1C0A7BE268E747B57B01521CFBDCC63B3AFDC45CF5EC82431C6CF18A98357B1E1FE184818E8896674E867314626AA5A50B3333CFC3B1FA1E9B8BCB45C181A6EEA07FC14566695171D3EAF5A60E92A2ED2FBC13E34C699AC1CA57AD278471601892E9674E2763ED697F80EEA52B06A747AA4B0FD2DEB55D565CC13C7AEBC2805BA2777E19D884A0A618B08D6BBB4AFFBE8A1C1CF8DDE4258B11E50904F488EF7A73FD0E0C141F493BCD0C648F23DD4B721ED5CF1041EFD1AF46446418EFBEF514AF9714F7127E98B6EBF17755ED6C0EAEACD61400E07F93E885748E248578784B42660AF8936CD565AFF9DC16647C014000039E3107B978FF28B3CA0E6C8D552925D9B3B7F9190363089C4BD709CE8DD0AE6AF6DECD6D5975539EBFC011975D268654E0A7BBE49AAB559B01D9F64CA7E858BC8F78AFBF9E7B360F0A039A33ADA0AD70677C6C90AACCBAEB5DD384CE28126B2ED11306662819BC51E71E1EF93CC989244FCC0DF266D90FF57F09FB4EA2ADE80AF9C9A1A67437F6FCCCBBC9181F4BEDC7B456AD592AE4ADA35AEB613B50D46EB3DEEC8F9A5F36F1C92E900DFE55B212EEE6FC963E0C149B9AF83E256EA3021AF7BBA9AFF46E2617BEF211987F1C0BB6F63737BDE142C96C6D284421A3E49B141D3125580391AC5EEAAF7D3FF1A61CC15AAD4F64F712D0A26F57A869E28BBD44A056000819D9F8A42FAF0046517B3B8DC43F04B2F438F147462ABAE1CBBBCDE086CA358C3596C34BD237509625AC7033CDC40EF5EDBC43E0ACB41CDFB813761200837E47F9780F5FD5ADBBF03065DE78FCE30DB1C0FDA6C64BC13C98CDBBD518A73CE446967F077A5D965739C8664438EC770C14DB0CAED34E751BB3AFEA13B53E92E3CDEEB1AC151C628A44A6F93D50C10FE5CE305EA9CDAF9920F7AABA65A65BA7038CD4EDFD633B15B8EE96595F5BA90C3E7367C6FD8AE7356B7C870B3DF7CAD77A57A189472EEBC1634059928F0E44F8A113D8328001834AFCF64D589FBBFF7FA6409E4BD0A08D96B751549B2DFD94D7784BE7943CDDCC41149EED29C003CF2F3678C8258B3061AE592B5C46BAF4DCF3DED7DFCF0DF4D9CE1A37954356A7FD794E4AFB8A30EE0779CC9A28E72F0671D5E928E99E302AD7FC8864472004F782EA17249B803A134D998C8D0A52EBE3AF26C20AD3B789C324456FDC1D8B432DD92A3A7CA8AC6F0EAD3F714C410D462FBAEFBE9138227172D58672A12203228F3141BAA173EA2BA78B6152BF472FAA9A8F83643F19659AFCB7709F82EA91779B746DC8B4522424222F71C9579E6D1050386A4762BAA1A38CC4A379F0744704CDB8DAF847A686DA42A26FA7CDE58800E0701EC7B7980A10F7464A4BD6710988919AF04E0174B058793E655ECE6F31F41EE01C6C9DD3D6BDB9C26F51A0013B7E2ABF343986ABD9617DDBE0D9B760E2238C4AE3CA620C515AA2CCD5C5BD6D0BA2061CCDE43DCA83834F110FD123569CEA234656904C2F02F12FDEB8996BCE10792D7E447BCBA1A91900EE7A1DDDC3FF4E05AE59A267BF78550C79847DFCF6960EF3D908A4D28FECA3800682581CE766A64097A5BE69A6DD376637625C8F9E10DCC951056BCC843E16BBEB7C137E2D83680F96AAF8808EE24FB6898742380ACA5A23C6EEC15C0AB7D6B4D32678005CC5E0F042146ABBB52D13EE3C7D33CE78BB55D8FCDB4F9023AACBF04D3658C6A81DA63E11C1F53FCB7F684A66994C55F7D024EACFAC708966A665658BA599D8D781DCF39F69A36278B6CBF500CCBA29211CAF2605FEE01AD47C3FD875C8DD889F0C699C6C88B22FDAC5E5DA14ED1F6FD475D74B647BB23F2C0F66F091CCD70EE089CED548CF11CB6A11D237C8CB4CB549B8BF9904F781865F17897F05B40B8B8C9BCFA4DD2BE7D23", + ); + + let (pk, sk) = MLDSA65::keygen(&xi).unwrap(); + + assert_eq!(pk, expected_pk, "ACVP keyGen 65 공개키 불일치"); + assert_eq!(sk, expected_sk, "ACVP keyGen 65 비밀키 불일치"); +} + +#[test] +fn test_mldsa87_acvp_keygen_kat() { + let xi = hex_to_bytes::<32>("F7052FBB921759CD8716773BA6355630121D6927899FDDA5768E2BC240FCCB7B"); + let expected_pk = hex_to_bytes::<2592>( + "18DFF392DEF5756EA23519A240E6B5CDCF912D89CD94DEC9DC71E53F8CDF37D96CAFF7EB6F182B85BEC2F534DCB918ABE8428E341DEDF6DB71B4B805767B6C9FDAE294BE7082D35FB68C926500F287602ADD34C3F95E2D63B48912AA476A4ECA076EB040B260303E412DB261F1D4741E742275FDE42156120B8BD6FE6CF4FD54EB2271F8466C73B20A7B230CAAF73C55B4F351D4E93A06940A4D4277DC0254AE825145C2E4A06BD33D822446A0DB08E0D1335CB0BFB57094CE81AD2B15E1F0D52B8E548170A8B44FF9F2B80AC88608DFD89489C114EE7C88B52ED2AAE5720F46C23265C67A24CBAA3CFE1358DAAD57D34CEA19B74AAF88FAF6B19250EDB3B66C6D272FA05F896A4763A1D60A5B859B214596A671A5832E0218C808D22FE2DF4AF850C638B81A682E68793F3F1CA8A5261D7104AB340556D66F40D5930AD669947E484CB965403FF5E57B935BA29DB5B97FFFF164E9508816E2CD811B8AAA768321FACAE3CE2B2A9CFFB2E4F1CB68CAE2FD672C314D327DCDF46A91F23929BA327AE8A8646FD20229FC5D85149484BCC473F52D5BE9A0EBC57E2E252EC895DCBA1CD39584CD7FC14E96201D90874211555C4BFFD9F906876F3E112CCDE00914E3E460D8E95FDF0C65F70C2325A1B077DB3CCD64F1BED863A8E82FC122746161696CE0A30C03E99296DD5ED031BA7920CC15BA3570BAC1BDFE00AA60A3AFEB73FDFACECF9BE1A9E118BD645A821CFC3E80C284CA1C1EE1F7B5EBAC2DACFC39D486F209F5808181127EC012A486C521A478F478E61B43E28EBF5C94E4E29827E33384D33BF6F95E32D824E28D3EEAD4A0736CA14C21818665A2B71FEC36E49FC5CAF058D29C3BEF0403AAA6B28A23D9663FFA8BD36659BF2FA4356BFA22CFD49EE0457B17BC9A67C4D653BAEC1E345751C6208E5EF0ADFF638044CF67ABCEC0B88FECE6FCB04E1016408A70FB734A13E14714E96AAF18215A4831DC009B7343839C728C9B9DABD96F472B3FBFF3DB4F27D82EDF014707DFD7C7E412F760637DD07CEDFFE1AA673B8AFB6CFBC1DDA4456863545336CF222D843361B1EC850E2003B41066A7FB799256653A8F46783E4E55B6AAA07B87DFC54932A0C64F1543A9912577BF61502D3BEFFF90DE16B456A49149F7F0BBF4F0A37718AA99FA4803A4A20E4DC8CB398BFC47CE13DAE175B78A6A5BA18200D35E775A55596E1BC0E83F878612DAF3BA51F0E844AA1B5DFC8585D693800D2059C9E9B6135E094A9AE2AE06A00C43FCBE43AB22A344C6DF8C55C9BF1EF99F97F6E29A1FD0AAE7D2D1031B578FCF10E8C831DF2D537560B274D4BB305AA299BA64EF162083C20341E81916F065A483DCB9AF74AC78BFC4D19C4C42B22688B171ED10ABC43009E39E121AFE25CBD16F929D9EB164E0B36F17C286E5987D80B43A5906E3315DD3BCBF4E526FBA9EF5106837588256E2A8F069411A8CBB7FB7109C8E152F81DDE2304A11AC54EBEA1A1EB3AAA20311FB72FF9C8CB230B45348B8B031A81F68C2AAB84E5F6F977038BDFEDFEF57E4FBFBDF30B804ED8F3ABEC91EEB252EBEC52311C48EA0F59C36EF01A138E86F740C972285D8A7FE9B817E3E58A01F5FE60CDA1247C351AD0ACAA765351FC580A873E8ABAA728688FF7C044827D600AAD3578F6E4869FD2E2F75ECF38D577E4E2011B1CA14BB448F53596C5AB7E40C42FD34F147002EBC5765FB5AE3DF9A094336F85CA475FFCF43B7A998EFC9499D8636375BFEAB91688B0632636B692E39AD0BDABB5851334C7AD94396A10817284FCEF3436C93731F92F7021B272D6B8A631F251860E161674BDE79B245D86E9C9CD9947AEACD1A2AC7B319FECE3411C8C27144274D33CEA8D1B9F22411ECE82512A3ABD32D31C678025554157F309EAD3300360F3EE2B873E10ACE3CDC36F175497F095E496EA81E34C2C7369B5722259735E0ECFE684E875B71C4828D2A463A279C2552019696829AA64BED7F4C8E2442100901230469A9ABAAC443069B30536405CC3C25A11900DAF73D598A131E329CA35E6E7D8071DDC0A539ED9D7A4BBCD512595A931086128EE9387C904639DFE380080BCF56438F704F728EC5FBC1D543FFE7B76EB9497D2FE9124308CCD82939B66D478E123DAB08143BAEC36985B374C5FC88F5B41CA1AB2179583DE226BCFE1BA07F82AA38811F389A87DF714881B51EAF2C62E2D77DE24A59EB7935884348A3A61B75938AA5E7FDB5C1CFFD030EF41A18AF91CF96D55B239499826B3924F31F7C7BBF7466C5F69DBF67EB344DDE8E7246610F1B738FF71D14DA35B52D8695CBE8FA334F92431A629F004AC4FBA9400F2465030F19CAE0D34ECD485B143521970D3FB80DCF36B1EB3694946DD57C2016E056487518A3034E7C387AFF01320BADC460F6EDCA1098D07A52A67A73B3EC398A54AB4FA8F0862DC23D6945781943A0651F3A0315EC7349C33A22937512474F1042D5CEA69F3A188CE10E395E19C8A1921E186BC91BA8BEA87753C15699491296AF4DACD2D211B3E98A2CC403F3545A2CED565400364FA1F86F2BF6067A515F2E3AB1F2DF934F7C5E12685C7D87644DCA48E6A0DD272E979CF35E4A0829A494EC779A9F7EEF8C39FC2EB101CABBDA7C1EABDEA92A818B628D265BBE4E08AE1157935968353ADC6B87CD0D7CD2F9F724CDF1FAB5A6CF0B145D559859F05F54477B543D2A6BCBC27C460961A7FEF9F8A3F661B8BB581717CEF42342E4AA7D11A7CF2BFC7C0F13DCC18F5D2222935CD9E26A28EDB13E2C34C496B2D1247A3DC7AABB919D23045545666757775FAD49684ECE444C8B0A8AF1E1FDD17EBEE6270482327CAB00C382526028A48D77039A3147789D326FF407937B6F393641BD6727F10AFA6F5222C502C420355CD1FD8860C6F24459E30FD8DB3BC0830340E2BB4A707DEC0EF6FF40090885474D361888573AB3CB9A17D3CBF895764F202E74369D207055A8F4D2262C317E9B45CFABB34A9FEEC03193D502A9589B6B010CA2F429FBF4377400BEA071428CEA42A3F2C0B08173F279EE1292ACEA8A5C67F8EA6692CB312EF666917E01E731B943DB002F9A89A37099EAE6C1437BF22D5ED82574B6A9265C6A51E4B012B0468B8339EA53C021B670DEF4EC4D955E9F09250EF179654CC9897C685FDE21A4B5A51FF2A0F41C6FAF966EA2D6BCF32B654CA1FA0CD0DF62C78B6411151509B019E69E124B0C079CDF61ED4E534CDDC0A60225117312E2669631E353ADC1890F1E54A0922E340E861FC6C51037133002EB21845EACA15AE968E26418B8D5A88881D7B01906862062A8708D1E8C10F187C76DE78CC9FAF6D210DC4479F3A1D216477D1A1B51796002E572D01E763580CD9425138C1AEE52D847466DD4FAFCB59C7E57415E843EA76BAC9B4F3B2A80597F986222D385B99BC9F599C5BFF0638275485238DE6945F534EF4CC13E97379AD8935056045430A69B785A7878CC425AACC87C8AB35C5AC7DD32EAC31080AE99B0574EA56A2B8260594EE39B90C9536C2C4F22DB40EA71953A30C72C75DB5261C375D3E450BA741FD4EDF0C0A0EFC9D69C7FDABAE473DA7857576DE9112D60E12AE3646F9461A7032C844C41A30074874CE3ABC7505DE15A16698DA0FBC0D2AD86D64A8FD768619EE073EDA689E2430D9ECF3C", + ); + let expected_sk = hex_to_bytes::<4896>( + "18DFF392DEF5756EA23519A240E6B5CDCF912D89CD94DEC9DC71E53F8CDF37D9D707D38455D5262BE13174016480537CA0E28A167AEAE91598B49FED93B94AD2B66C22023DD7F19B3CD372E14A276A44CEC10D377B88961FCF17C273781D8625AA5E0E304141057CE295AC59B1BB4C3F2B1E42F6873215EB70B6B524B84D46DDC84412E3A0019A020919058164307104017200398421A48548103182A440C228248B984000274C613625081106D9020CD1A848983681DA124E10360511830C1A8641424650D1C2649C3240A1960DD33206A2C8482433298AB6908C3268E3B84D1A32661B31855B348989B461103132640601111688DB2005C3C420A34629032401138909DA1608D0260810A92D5C0468C288800447501C2112C046280C024608104918094ECA100CE396600A29821C490AE1224ED1A20C50448199B00488204D5194919198055922318CA26001270C10872122334CE434001114889B1082033092C3A24422274ECC286101158112414820B570D2B06114172D03C191222842C24626E2B44C23C76022306690B81002B73054340062482E208029A1823012946D84102C9A2290213612DA86701016709A068580244A020869D8A680192182003206413488E14884533091243922D3C2010A236250088D88920919256E01999061A248C1064458280202C90CD2B4691A3622109801DB96699A20910281410C884418044A4C380A44482EDC048202178221440850B245D284010B8670414404DB1461182931E2388D5A264022452A53A20C9406225C908DA3361023952508A12C90108414052854A4280CC560C4B48D60C84424437220103109A350CAC66CCB462A21802D12B364A3B28D638004D8908181106D89066A1B466400B4511B20221493606140698084849140300C3289028285632848A0C220DC2452D21029082281D1203204354612892820474112176AA0104C59928189B8019C181009A644C1362181804921058644820121140802392ADCB82DA0A8210A022892026D24A501C2B861CC120EDC00628920608088701C9265602805DCB608E0920990B20419B520591490219184D0142E82184A0C0224533409C8108A99420A01028A63909120416208294612072E014111D2842881B0841B87604B420A201709DC224412B50D8446454B964942B884CA224051446A19A368E3802909930C8C1860584491020428A0B061A1C004C0000C1AA5845A487101248994169211926851048912A12D8AC6418226248A92401B436D042412C126252293680CB8606018311224640A4308CB3232D4C668A1466A0A886C92084040084802874D0AB25044280541A4712245406194450334418B8429DC1445C9122D88486654C28912383213C5008A966522484D190328C106014B10716304420817010B318119A251A2268E11B3000B930D1232891CA4495AB62100296A634446C4C08123879113B29199864999B22DE1A42522A84D98162483924DDB18890BC74960024454380CCC224C801466E2B4659C08224A9411D2168424B74048028C2141451BB98542C8485B02020109644940724904904C844552946C04392018173220258D81480254B488834468989264123229522230182460D0C271E2067120250022380E92B22884002ECC302513448622B3084C36866086814A40699A8049C21025CA08601499051A402648200C08006AE200421224320428221932901A05905C2831092185180311214112499824A2B88C01078A9B02881C005064368C10226D5A26889906480086655B0892A11045909204A324499B9891C9140D8212521AA4499800920A41201811720C8081A22249C284090B1766212708888421CBB86112486D634840102340C0C641CC402D08906501428C0B942DD4168D89928C893469E4A444D81851E410524CC62008150583124801C84512B829C8102C1C295141C40D58189293060280064E8C9488D03061CC324420C84D49344AC412028808059B940414314913364D54966120366023930DD13450842825A0A45112472E04808511965041324D02806CDA828882362E48B44C4346505A361111081054826021331022492211B44DC8207252489122952CC2207203A665491284830084E1068411C30C08266A1433899346644B469100010480242C144480D2020923488098260C1AB72111C4491182648B82681223692229046BDC8095AA4863C06F03811E2A8848D251CCF80BA1F64DEE2C7C1CDBD1AC37AC42C7B2A660F8C5A828BCFF10F78982AFE4C0ABB6D3AC5B0A475500B5C66250A23255FAFB79B0B5C500DB1290A09621B6772DF1776FF6B0943D6AF53C58168EC57D038FCE12EC6735194448AA2A87D00AB4FB33FAF61B6AB0F6D7A65963F9D46D645A50B7F495C2710215A4F0332ED9B0613B6B90C79C70AE975AA59EFD2CDFA9F777B65608E0A08C7B08790ED85E06F9D2E2E9775AEA198A6A97D5C27437DA0AFA6E11A0CD5E1FBEF1DD5D4430888BA75A5E554D2C2C7406834622979D121E2B59D3CEF3F1C70B3E7E1E0A3403F33FBC528DB80F7C3ECA23A4C357D5AC526D6E200133C01F72CB04EB4D07EA55185CEBAF42D44E7A34C214E531E0E2E8F08D4CD335109C6BABD1E1775E0001C067A42B0C7DE66DEE96CC637A9107C041FD90FFE7929ED210C71C29715C8172C9FBB7A694A41BE8AD3F5F10DAF567D3D6E15C3AAE4051F2693C1E58B08862C8ACF706416A088C3E3C65D9A540A6C65027CFA1A3CD16AA5B18BD3C5F785CECD714EBD858B40EC0E4E7F6790B8EBB495B2A38E04F5E67741CEB9A948E325165348108489843F0FBBF4860B39596A360C3ECFADA3CFD37BB7E025EE8C63B38AC7F0FC2234C427BBD6E21ADECF87C20ABDD4DDC1DAB22F8F207995426733F546BA135A4E4CE50E71D24C86B3E7D0990843CC3FC2EA8E811231AAEB075D1917218BA5C9852ABE28AB34990BC93A6FD7EE338E31A7B90D518D273FCAECE27D58F5A5045547BE5547715FB05EC53C93DD76D2C5FF4F04C4EAD0C8E019D2586F30F7E14AFBFD27D1C4DAFD616900DE1018F74AD5A9522392040D387E5566FD8C3EF7B5A89EFDB39D456CADC6081F7E124098878A3CD580344DB86ABBEDEA7FA349FC0256E6EAFDE4F57D4075486D940560A31CCA1254FCACDDC07A4CB0A57364B4B039C7803834A1E85EA194D796389AF28364242A57CC49F7D4B4F124923D50705BD9D6DA04720B817007CA611DCEDF17DF59CF601D01B30FB660A3A87B8085BEC72D0B30285739BBED124C4F50A3AB572530EACF1F7BECEAF474156398EC3F12BDD0939666691D89529612B7E8FE7B672FD4DBE630121B5F864D9E12C7BC1A953BCF557023A953E031704FF30A814934D368263D9536BF002D00244375DAE22905143A47FFF8777F8C8522241C0E28AEB4C5CBDC156EB45C6762058A2D53C81A29446C128171D13D24A6248341CB7D09B2F535E8BB528DE76D3B1726A3D5E3D6813990916B294945485B7A531076787FF4CA7DBF32D8B26C9613BBE2473680A8FC782843E475949711E3516DD563B8A6CEC6C5E37EFA9563FC9EE4454298077B7148EADB21F898D01452C5BEBDFAE8C9C58620E8553D13D72DBF05A8B82D0EC3EE2176B4FC1FF364FB610B43E766B61C9962BC93F63FF3ABB60A9CFFA19B18DF9C7E588228597B0408299010563CD8098974397FC84D033432860E84EB9B7A9CF1BB3B38C503D59CFAD70A488E8FA63A1D4DA15B87542C6C737C82A5BBF2A59894C4F44330948136492279CE52DDF007F75DCE818D1D58C41483DB70AB975F2D95611CCA577576AD0FBC23D1DD20C63B160DC227D7EB8D41F582170C3C59575F770C5C9CA702F3601D781E25A74C0117035AD742B5E41BE3E1A5711A2D4E17A0228CED21BDC3320C32F3F048476E2440AA1019C7BD076C8E16129325073C93F5E2B2FCF859428B51D827F372E2427848B7B24B42D18FA452B6614DD55B8382F9D1C98ABB0AF1721ABAEB0E7BDF1D6752B130A994B9C940660259B690A0FCEC5AB3668FA41E24C3D1D87962815D739CA341D5C4B687F3DBC0E598432A5C9CE6C78952A79DAEF94925787C34883A16DEEE435711FC41ACA62286C46301A6F22221C57DAD0686D0AFF2DE15D8D258EC01A5756DE75A67246E2F9AD6705A68EBFFE08BBFCF000412A22648A6A84A0B4310D63F460E012710D95C0728176019E361B100C5C531AD55AFF9A6C613BC6AE99456CA03FB2A2A36907FDAA54F91633CD35CE7C9926D46D223386F517874D6905D39A6801FB007C39B36D70A39A4F0E39D8D2EC8594168C3D4ED10C200AB3652C40E1779F582425594CEE70363CADD3E6A3C06F7548A76724A087633DAEE1F87FB4D5C0FC490D57A5B7E4348D9436779639E215E0DCEC31987772AFBEBD6D28B7222C999320E5F8262614207A7C02E07335BC7F69AC76CB5FA2F30607CCB73FD9A926CDE3CF6D31CDFB98D6BB0F55BD2FD7AF2D0C92E5D8883D8B3A471509682B850DDE31305A1D29D501A6E0E094845BB05B0F07C40EBA4FE13BE407CC41F5AEECA943960B0DD05497CAE56E6E22A70993C6B85B49682D5477A9730C4F99EB54D598D9B3F8232031E1812C2473F5C88D6B928ECB126C0FF020540073C0423EF3B44AE35964D64C0210F67095E1E567D7F742DBAE21EFC433EB900B6640C4DE1FB8B1B6655324B54FF38B02BCE7E00BEFF81FBDE5B70DADA4EE409483275AF77B75C844FC8043F6DA74E4BA197E81EF7169D46D70226E29B9F48282C4F10F1BA297369B09FC6DCDEF3ADCB38F0B2C36E0BB5A3CC84DED2925E315380598CF85B915FFE70A6C74E9E1FFAB173B582D187507DD80C7C8D33DCF4200B47AA3B5211D3AE01968349B14D3E985C95F2E25824BFC22CAAB2698E2D568B9C6DCD995E5A17C90D680DEA9EB674487D2A9A8F71AFD28089A3F301CB1EC1F5DA5E70633A8708185485CB95088698ABDBF172E440C7050952081831AFEB3FE91014909D7B8A87489E77C37F17F8D2F670BE73ACCE286EA7D1E33DF403F0EE2EFD5ED412AAD3E0546E122F6435880207B60FD15C5996A996A4E7C129C679C92BDBA9529A17306444C4F523E2CCB7434CA4960EAEE53552EC03ACC0BE4DF7C83476BFC3D446B673672A0B4872C63C6409711EEB9371B95BCFF1A5D89A4B285ED40B55FB6311C6BE8C2B5BE04A387AB67B175348B7401E565FED5B2A3077AE35D9DF95D3A152268CD4B141BD92D886436F9C006A98C271D92764D0EA580BEA79E360F9AA71F1AC6FA568D64C0A091940F872F98BC68776BF1B5364D6207AC697EB4B694BECB289B20081E2C33590726E9011993562DEEAFA3FBB8D51294A358A3C59EFEAAF070A2600E04FE06E6536F8FF3F268B4FA5656D438D2294EF1439E4A07D3EF10DC857F358FDC3F9C99C751901A015C238209AE3B74B9E169E25E212EA44DD7158AB2FE6F9082C613A488CE74FA545A8F5A6B706B8587F12A9E39229DE2069968D700F2A0219BB49B3361D696D5A10CCDFFFBE41F32B38FEDA79C0A86B9B1BED5A087EF7F7AD2D5455083733919D19EF151C5073C9315CBE479F0CA6737861182E80A3B4E0710AD4FF7F2F904C46DA58E7D2FCE62353C45C641D30B0E494927E4D6971F7BC07B853AC40C312798727E80FA4E842DDDA9DFCAD19D1F7DFE059A7EF19EB2CBAE61B8F64E4C9F1B41EB142F2ACB79B44401039F53631BF479944754005EEDD982356B1EACF020E8781FEA794214AF6C174333EE8A2F82F9CF72CF944B21B493BDD2B7B562A4C87D85AA19C35CED4DF069E75057DE578CB92EE6C0F5892BDFC2369C35305180F4E5291B278D56F5E4A851D1949B9747F4C788B3EC0A0B4B68D0020DA8A96C0A79E66BD4094C54CAA6B9A59EBC95BF9056AC16FB8F1C46702A091D1DAD582B27FC7656D84DC1EAB6258E35B78F5016236444B03C1CAA66326B73F786B0C85C7BEE4AA157C6DF9F1C53238010A95515248328CADC3CD69EBE7BBEB87B5AF5D6C16ECB99ADDA7753BB7DE09F3040A1BBAAB040E60CE708FC25E66987B7298201E8C5A0121CD14160A0088752AC499FF01CF361249CD2E436465E99AD4E52D137431A271C56AFDC5D527DA81F9B29B59507B7D0A4D8CB3DE03135A8751098B9A05D7025B32ED98CE72BF621BBE5BDC800765D4B4E295CA02444A9631636A56C2D123589E67DC3C8E06D0C9572DB29E160FF2DA6387B2127382BA891DF5B1D6DB7925D941EAABB55958A089CAC6AFE37C224427D917B61AAB363242C6385E4F1BC811B20AF66089C5B941601F1F551B385DF3D49E920DCD9B5B0F0CF51B54A0D77378B21C1FA0DF2F825173410D292B8017441068BE8B6E7719747705B8AEF7899A60A823F38D72194CDA3C242E67EC95BADAB1F4124C8D2660FA0DE2D29D2BC76341287D8B68597E714D192BA0F15C8F9EE6B06E5243C5E57745EF40A46BD5342FC8F0108CAB434224CCF6E26D86977E7879DC21646122D8D8008BC2E387629C083738CB248A0CCC8642379EDF73B1A2C7A84C90E7394E4B3DEA13C210CCA51544C00D8E568A3688E77A61423FF2304F6091D9FFCB7C7BE873FC40A76C7389B387A407134F952A4B170F68C29A3B55D7E20185CFA2BBF69B2685C6A4246F14C326E736F6883FFFCF5E7365607F199C952A570A4E753B4CE80C03FCCB765BEEA3D964DCAEB643EFA27E7A1BA71A0C13B4CF7B1021E2F9A2E9777DB30258A97314E2AA2BFD82D30D7EC90F4410E65BF89D272BD5EB9C158F3A440F2928474AA0FF4D5C23409F1146021C2510A25815CD5B4EF32F753AB8D95F59CCAC60048AF0A7688E5A3A7CD099DD1C4F0BE826F1CEB7F01A3D5614753A85BAFDC2A4F797B504F07B1C8281E12E4E27C883735D742812EEE497AC0A6C4C9D59D3D602FB0DBDABCC2CE", + ); + + let (pk, sk) = MLDSA87::keygen(&xi).unwrap(); + + assert_eq!(pk, expected_pk, "ACVP keyGen 87 공개키 불일치"); + assert_eq!(sk, expected_sk, "ACVP keyGen 87 비밀키 불일치"); +} From a50c5ea38f8ec77f3d668aabc01184e20833e482 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:23:23 +0900 Subject: [PATCH 10/65] =?UTF-8?q?ML-DSA=20=EA=B2=80=EC=A6=9D=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cross-confirm.md b/cross-confirm.md index bd9eeac..fd4daf2 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -8,19 +8,19 @@ - [X] AES - [ ] BLAKE - [X] SHA2 -- [X] SHA3 +- [X] SHA3 (명세 없음) - [ ] ChaCha20 - [ ] Ed448 - [ ] Ed25519 - [ ] X448 - [ ] X25519 -- [ ] ML-DSA +- [X] ML-DSA - [ ] ML-KEM - [X] RNG - [X] Constant-Time - [X] Zeroize -여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "담당 명세에 따라 정확하게 구현되어 있으며, 정상적으로 동작한다." 는 것을 명시하는 것입니다. +여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "담당 명세에 따라 정확하게 구현되어 있으며, 정상적으로 동작한다"는 것을 명시하는 것입니다. > [!IMPORTANT] > 말인 즉슨, 이 검증 작업이 완료되었다고 해서 이 암호 모듈이 완전해진 것은 아니라는 것입니다. 암호 모듈 검증을 위해선 전문 검증 단체의 승인(CMVP)이 필요하고, 이 작업은 수 개월에서 많게는 수 년까지 소요될 수 있습니다. From 3fe881b8de3ded4198c8ccd0024574ee4afff079 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:24:24 +0900 Subject: [PATCH 11/65] =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=EB=90=9C=20=ED=81=AC=EB=A0=88=EC=9D=B4=ED=8A=B8=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7a3c298..5224328 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,8 +39,8 @@ opt-level = "z" # 크기 최적화 [workspace.dependencies] zeroize = { path = "zeroize", version = "1.1.0" } constant-time = { path = "constant-time", version = "1.1.0" } -sha2 = { path = "sha2", version = "1.0.0" } -sha3 = { path = "sha3", version = "1.0.0" } +sha2 = { path = "sha2", version = "1.1.0" } +sha3 = { path = "sha3", version = "1.1.0" } blake = { path = "blake", version = "1.0.0" } rng = { path = "rng", version = "1.1.0" } ed25519 = { path = "ed25519", version = "1.0.0" } @@ -49,7 +49,7 @@ x25519 = { path = "x25519", version = "1.0.0" } x448 = { path = "x448", version = "1.0.0" } aes = { path = "aes", version = "1.1.0" } chacha20 = { path = "chacha20", version = "1.0.0" } -mldsa = { path = "mldsa", version = "1.0.0" } +mldsa = { path = "mldsa", version = "1.1.0" } mlkem = { path = "mlkem", version = "1.0.0" } elib-k0-ipc = { path = "elib-k0-ipc", version = "1.0.0" } elib-k0-mock-transport = { path = "elib-k0-mock-transport", version = "1.0.0" } From 6f2a03c1cfcd259da457bb6511503fafe6260ae0 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:31:10 +0900 Subject: [PATCH 12/65] =?UTF-8?q?mldsa/README.md=20=EC=9E=AC=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20-=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EA=B3=BC=20=ED=82=A4=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=84=9C=EB=AA=85=20=EA=B2=80=EC=A6=9D=20=ED=9D=90=EB=A6=84=20?= =?UTF-8?q?=EA=B8=B0=EC=88=A0=20-=20=EC=A0=80=EC=88=98=EC=A4=80=20?= =?UTF-8?q?=EC=96=B4=EC=85=88=EB=B8=94=EB=A6=AC=20=EA=B4=80=EC=A0=90?= =?UTF-8?q?=EA=B3=BC=20=EB=AF=BC=EA=B0=90=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=86=8C=EA=B1=B0=20=EC=A0=88=20=EC=B6=94=EA=B0=80=20-=20KAT?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EA=B2=B0=EA=B3=BC=EC=99=80=20=EB=B0=9C?= =?UTF-8?q?=EA=B2=AC=ED=95=9C=20=EB=AC=B8=EC=A0=9C=20=EC=A1=B0=EC=B9=98=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mldsa/README.md | 179 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 173 insertions(+), 6 deletions(-) diff --git a/mldsa/README.md b/mldsa/README.md index 0c839f1..50b7a5e 100644 --- a/mldsa/README.md +++ b/mldsa/README.md @@ -2,19 +2,19 @@ [![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) -`mldsa`는 [NIST FIPS 204](https://csrc.nist.gov/pubs/fips/204/final)에 규정된 모듈 격자 기반 전자 서명 알고리즘(Module Lattice-based Digital Signature Algorithm, ML-DSA)의 순수 Rust 구현체입니다. 본 크레이트는 세 가지 파라미터 셋(ML-DSA-44/65/87)을 지원하며, 비밀 키 메모리 보호, 헤지드(hedged) 서명, 상수-시간 필드(field) 연산을 통해 부채널 공격을 방어합니다. +`mldsa`는 [NIST FIPS 204](https://csrc.nist.gov/pubs/fips/204/final)에 규정된 모듈 격자 기반 전자 서명 알고리즘(Module Lattice-based Digital Signature Algorithm, ML-DSA)의 순수 Rust 구현체입니다. 본 크레이트는 세 가지 파라미터 셋(ML-DSA-44/65/87)을 지원하며, 비밀 키 메모리 보호, 헤지드(hedged) 서명, 상수-시간 필드(field) 연산을 통해 부채널 공격을 방어합니다. 구현 범위는 pure ML-DSA이며, 사전 해시 변형(HashML-DSA)과 external-mu 인터페이스는 제공하지 않습니다. ## 보안 위협 모델 RSA 및 ECDSA와 같은 기존 전자 서명 알고리즘은 Shor 알고리즘을 구현한 양자 컴퓨터에 의해 다항식 시간 내 파훼됩니다. ML-DSA는 모듈 격자 위의 LWE(Learning With Errors) 문제와 SIS(Short Integer Solution) 문제의 계산적 난해성에 안전성을 근거하며, 현재 알려진 양자 알고리즘으로도 지수 시간이 소요됩니다. -구현 수준의 공격 표면은 세 가지입니다. +구현 수준의 공격 표면은 세 가지입니다. 1. 비밀 키 메모리 노출 - - `s1`, `s2`, `t0`, `K_seed`, `tr` 등 비밀 성분이 스왑 파일이나 코어 덤프에 유출될 수 있습니다. 이를 Drop 시 자동 소거로 방어합니다. + - `s1`, `s2`, `t0`, `K`(서명 시드) 등 비밀 성분이 스왑 파일이나 코어 덤프에 유출될 수 있습니다. 이를 `Secret` 래퍼의 Drop 시 자동 소거와 명시적 `zeroize()` 호출로 방어합니다(아래 '민감 데이터의 소거' 절 참고). 2. 서명 시 타이밍 공격(timing attack) - - 비밀 성분에 의존하는 분기가 서명 키를 노출할 수 있습니다. 유한체 연산(`Fq::add`, `Fq::sub`, `power2round` 등)은 [`constant-time`](../constant-time)의 상수-시간 선택 연산으로 구현됩니다. + - 비밀 성분에 의존하는 분기가 서명 키를 노출할 수 있습니다. 유한체 연산(`Fq::add`, `Fq::sub`, `Fq::mul`, `power2round` 등)은 [`constant-time`](../constant-time)의 상수-시간 선택 연산으로 구현됩니다. 3. nonce 재사용 - - 동일한 `rnd`로 두 개의 서명을 생성하면 비밀 키가 복원됩니다. 헤지드 서명 모드(`rnd ← RNG`)로 이를 완전히 방지합니다. + - 마스킹 벡터 `y`가 재사용되거나 노출되면 $z = y + c s_1$ 관계에서 비밀 키가 복원됩니다. 헤지드 서명 모드(`rnd <- RNG`)는 결정론적 모드의 fault 공격 내성 약화까지 함께 방어합니다. ## 파라미터 셋 @@ -28,7 +28,174 @@ NIST FIPS 204 Section 4에 정의된 세 가지 파라미터 셋을 지원합니 각 파라미터 셋은 행렬 차원 $(k, l)$, 비밀 계수 범위 $\eta$, 챌린지 다항식 가중치 $\tau$, 마스킹 범위 $\gamma_1$, 분해 범위 $\gamma_2$, 힌트 최대 가중치 $\omega$를 달리합니다. 컴파일 타임 const 제네릭으로 단형화(monomorphization)되어 런타임 오버헤드가 없습니다. -## 오류 타임 +--- + +## 알고리즘 구현 + +모든 연산은 $R_q = \mathbb{Z}_q[X]/(X^{256}+1)$, $q = 8380417$ 위에서 이루어지며, 다항식 곱셈은 NTT(Number-Theoretic Transform) 도메인에서 수행됩니다. 모듈 구성은 FIPS 204의 알고리즘 역할을 따릅니다. + +| 모듈 | FIPS 204 대응 | 책임 | +|-------------|----------------------------------------------------|-------------------------------| +| `keys.rs` | Algorithm 1/6 (KeyGen), Algorithm 35 (Power2Round) | 키 생성, sk/pk 인코딩·디코딩 | +| `sign.rs` | Algorithm 2/7 (Sign), Algorithm 3/8 (Verify) | 서명·검증, Decompose/Hint 계열 | +| `sample.rs` | Algorithm 29~34 (Sampling) | ExpandA, ExpandS, ExpandMask, SampleInBall | +| `pack.rs` | Algorithm 16~23 (Encoding) | BitPack/BitUnpack, HintBitPack/Unpack | +| `ntt.rs` | Algorithm 41/42 (NTT/NTT⁻¹) | 256-포인트 NTT, ζ 테이블 | +| `field.rs` | - | $\mathbb{Z}_q$ 상수-시간 산술 (Montgomery 곱셈) | + +### 키 생성 (Algorithm 6, `ML-DSA.KeyGen_internal`) + +32바이트 시드 $\xi$에서 결정론적으로 키 쌍을 유도합니다. + +1. $(\rho, \rho', K) \leftarrow \text{SHAKE256}(\xi \| k \| l)$ 으로 128바이트를 확장합니다. 도메인 분리 바이트 $k, l$ 포함은 파라미터 셋 간 시드 재사용 공격을 차단합니다. +2. 공개 행렬 $\hat{A} \in R_q^{k \times l}$를 $\rho$로부터 SHAKE128 거부 샘플링(`RejNTTPoly`)으로 생성합니다. 행렬은 NTT 도메인에서 직접 샘플링되므로 변환 비용이 없습니다. +3. 비밀 벡터 $s_1 \in R_q^l$, $s_2 \in R_q^k$를 $\rho'$로부터 SHAKE256 거부 샘플링(`RejBoundedPoly`, 계수 범위 $[-\eta, \eta]$)으로 생성합니다. +4. $t = \text{NTT}^{-1}(\hat{A} \circ \text{NTT}(s_1)) + s_2$를 계산하고, `Power2Round`($d = 13$)로 상위 비트 $t_1$(공개)과 하위 비트 $t_0$(비밀)로 분해합니다. +5. 공개 키는 $(\rho, t_1)$, 비밀 키는 $(\rho, K, tr, s_1, s_2, t_0)$이며 $tr = \text{SHAKE256}(pk)$ 입니다. + +### 서명 (Algorithm 7, `ML-DSA.Sign_internal`) + +Fiat-Shamir with Aborts 구조의 거부 샘플링 루프입니다. + +1. 메시지 표현자 $\mu = \text{SHAKE256}(tr \| M')$, 마스킹 시드 $\rho'' = \text{SHAKE256}(K \| rnd \| \mu)$를 유도합니다. `rnd`가 RNG 출력이면 헤지드, 0이면 결정론적 서명입니다. +2. 루프 반복마다 마스킹 벡터 $y \leftarrow \text{ExpandMask}(\rho'', \kappa)$를 생성하고 $w = \text{NTT}^{-1}(\hat{A} \circ \text{NTT}(y))$, $w_1 = \text{HighBits}(w)$를 계산합니다. +3. 챌린지 $\tilde{c} = \text{SHAKE256}(\mu \| w_1)$에서 `SampleInBall`로 $\tau$개 계수가 $\pm 1$인 희소 다항식 $c$를 만들고 $z = y + c s_1$을 계산합니다. +4. $\lVert z \rVert_\infty \ge \gamma_1 - \beta$ 또는 $\lVert \text{LowBits}(w - c s_2) \rVert_\infty \ge \gamma_2 - \beta$ 이면 해당 반복의 비밀 중간값을 전부 소거하고 $\kappa$를 증가시켜 재시도합니다. 이 거부 검사가 서명 분포에서 비밀 키 의존성을 제거하는 핵심입니다. +5. 검증자 보정용 힌트 $h = \text{MakeHint}(-c t_0,\ w - c s_2 + c t_0)$를 만들고, $\lVert c t_0 \rVert_\infty \ge \gamma_2$ 또는 힌트 개수가 $\omega$를 초과하면 역시 재시도합니다. +6. 서명은 $(\tilde{c}, z, h)$ 입니다. 루프는 최대 1,000회로 제한되며(FIPS 204 Table 2 기준 평균 반복 횟수는 4.25/5.1/3.85회) 초과 시 `Error::SigningFailed`를 반환합니다. + +### 검증 (Algorithm 8, `ML-DSA.Verify_internal`) + +1. `sigDecode`가 서명 구조를 검증합니다. 특히 `HintBitUnpack`(Algorithm 21)은 힌트 인덱스의 순증가, 누적 한도, 잔여 바이트의 0 패딩을 모두 검사하여 서명 가단성(malleability)을 차단합니다. 형식 위반은 즉시 검증 실패입니다. +2. $w'_{approx} = \hat{A} \circ \text{NTT}(z) - \text{NTT}(c) \circ \text{NTT}(t_1 \cdot 2^d)$를 계산하고, $w'_1 = \text{UseHint}(h, \text{NTT}^{-1}(w'_{approx}))$로 서명자의 $w_1$을 복원합니다. +3. $\lVert z \rVert_\infty < \gamma_1 - \beta$ 검사 후 $\tilde{c}' = \text{SHAKE256}(\mu \| w'_1)$를 재계산하여 $\tilde{c}$와 비교합니다. 비교는 XOR 누적 방식의 상수-시간 바이트 비교(`ct_eq_bytes`)로 수행합니다. + +공개 API(`MLDSA44::sign/verify` 등)는 FIPS 204 Algorithm 2/3에 따라 $M' = 0x00 \| |ctx| \| ctx \| M$ 형식으로 컨텍스트(최대 255바이트)를 바인딩합니다. 길이가 고정된 입력(pk, sk, 서명, 시드)은 전부 고정 크기 배열 참조(`&[u8; N]`)로 받으므로 잘못된 길이는 타입 시스템이 컴파일 단계에서 차단합니다. + +--- + +## 저수준 (어셈블리) 관점 + +`mldsa` 자체는 인-라인 어셈블리를 포함하지 않습니다. 대신 비밀 의존 연산을 [`constant-time`](../constant-time)과 [`zeroize`](../zeroize)의 검증된 저수준 프리미티브에 위임하고, 그 보장을 그대로 상속합니다. 두 크레이트 모두 검증된 인-라인 어셈블리가 없는 아키텍처를 컴파일 게이트로 거부하므로, `mldsa`의 지원 타겟도 x86_64와 aarch64로 한정됩니다. + +### 상수-시간 필드 산술의 기계어 수준 동작 + +$\mathbb{Z}_q$ 산술의 조건부 보정(reduction)은 비밀 값에 의존하는 분기를 만들 수 있는 유일한 지점입니다. 이 구현은 모든 보정을 `i32::select`(내부적으로 `ct_sel32`)로 처리하며, 해당 프리미티브는 데이터 독립 지연 명령만 사용합니다. + +| 연산 | x86_64 | aarch64 | +|-----------------------------|-------------------|----------------| +| 조건 선택 (`Fq::add/sub` 보정) | `test` + `cmovnz` | `cmp` + `csel` | +| 부호 판정 (`is_negative_ct`) | `sar` 산술 시프트 | `asr` 산술 시프트 | + +- `Fq::add`는 $a + b - q$를 무조건 계산한 뒤 부호 비트(`(v >> 31) & 1`)로 두 후보 중 하나를 `cmov`/`csel`로 고릅니다. 조건 분기(`jcc`, `b.cc`)가 생성되지 않습니다. +- `Fq::mul`은 $R = 2^{32}$ Montgomery REDC로 구현됩니다. 64비트 곱셈 명령(x86_64 `imul`, aarch64 `mul`/`smulh`)은 두 타겟 모두 피연산자 값과 무관한 고정 지연입니다. 다만 ISA 차원의 보장(aarch64 DIT, x86 DOITM)은 `constant-time` 크레이트와 동일하게 향후 하드닝 과제로 남아 있습니다. +- NTT 버터플라이의 메모리 접근 인덱스는 공개 루프 변수와 사전 계산된 ζ 테이블 인덱스뿐이므로, 비밀 의존 캐시 타이밍이 발생하지 않습니다. +- 비밀 계수의 직렬화(`BitPack`)는 `fq_to_signed_ct`가 `wrapping_sub`와 부호 비트 마스크만으로 부호 변환을 수행하여 패킹 경로에도 분기가 없습니다. + +거부 샘플링 루프의 노름 검사와 채택 여부 분기는 비밀 값에서 파생되지만, 거부된 후보는 즉시 소거되고 외부로 노출되지 않으며 채택 여부 자체는 공개 정보(서명 출력 시점)입니다. 이는 FIPS 204의 Fiat-Shamir with Aborts 설계가 의도한 동작입니다. + +### 소거의 기계어 수준 동작 + +일반 대입(`buf = [0; N]`)은 컴파일러가 dead store로 제거할 수 있습니다. `zeroize`는 모든 소거를 `write_volatile` 기반으로 수행하고, 아키텍처별 배리어로 스토어의 생존과 완료를 강제합니다. + +| 단계 | x86_64 | aarch64 | +|-----------|-------------------------|-------------------------| +| 휘발성 store | `write_volatile` (제거 불가) | `write_volatile` (제거 불가) | +| 컴파일러 배리어 | 빈 `asm!` + `compiler_fence(SeqCst)` | 빈 `asm!` + `compiler_fence(SeqCst)` | +| CPU 메모리 배리어 | `mfence` | `dsb sy` | + +릴리즈 바이너리에서 zero-store와 배리어 명령(aarch64 기준 `strb wzr` + `dsb sy`)이 LTO 이후에도 살아남는 것은 zeroize 크레이트의 별도 probe 빌드로 검증되어 있습니다. + +--- + +## 민감 데이터의 소거 + +이 크레이트의 모든 비밀 성분은 두 겹으로 소거됩니다. 장기 보관 값은 `Secret` RAII 래퍼가 Drop 시점에 자동 소거하고, `Poly`/`PolyVec`의 `Copy` 시맨틱 때문에 래퍼 밖에 남는 스택 사본은 명시적 `zeroize()`로 즉시 소거합니다. + +| 비밀 값 | 위치 | 소거 방식 | +|---------------------------------------|-------------------|-----------------------------------------| +| $s_1, s_2, t_0$, $K$ | `PrivateKey` 필드 | `Secret` Drop 자동 소거 | +| $\mu, \rho''$ | 서명 중 스택 | `Secret` Drop 자동 소거 | +| $\xi$ 확장 버퍼, $\rho'$, $K$ 지역 사본 | 키 생성 중 스택 | 반환 직전 명시적 `zeroize()` | +| NTT 적용된 $s_1$ 사본, $t = A s_1 + s_2$ | 키 생성 중 스택 | 반환 직전 명시적 `zeroize()` | +| $y, \hat{y}, w, c s_1, z, c s_2, r_0$ | 서명 거부 루프 | 거부·채택 모든 경로에서 반복 종료 전 명시적 `zeroize()` | +| $c t_0$, 힌트 계산 중간값 | 서명 거부 루프 | 거부·채택 모든 경로에서 반복 종료 전 명시적 `zeroize()` | +| $\hat{s}_1, \hat{s}_2, \hat{t}_0$ | 서명 함수 전역 | 성공 반환·실패 경로 모두에서 명시적 `zeroize()` | + +설계 원칙은 다음과 같습니다. + +- **거부 루프의 모든 탈출 경로에서 소거.** 노름 검사 실패로 `continue`하는 두 경로, 서명 채택 후 반환하는 경로 모두에서 해당 반복의 비밀 중간값을 소거합니다. 거부된 $y$와 $z$는 한 쌍만 노출되어도 $z - y = c s_1$ 관계로 비밀 키 복원에 직결되므로 가장 민감한 값입니다. +- **`Copy` 잔존 사본 직접 소거.** `Secret::new(v)`는 `v`의 복사본을 보호할 뿐 원본 스택 슬롯을 소거하지 않습니다. 키 생성은 `Secret` 래핑 직후 원본(`s2pv`, `t0pv`, NTT된 `s1` 등)을 명시적으로 소거합니다. +- **공개 값은 소거하지 않음.** $\rho$, $t_1$, $tr$(공개 키의 해시), $w_1$, $\tilde{c}$는 공개 성분이므로 소거 대상이 아닙니다. + +검증 경로는 공개 입력(pk, 서명, 메시지)만 다루므로 소거가 필요한 비밀이 존재하지 않습니다. + +--- + +## KAT 검증 + +자가 일관성 테스트(자기 서명을 자기가 검증)는 인코딩 버그를 탐지하지 못합니다. pack/unpack이 서로 역함수이기만 하면 비표준 직렬화도 라운드트립을 통과하기 때문입니다. 따라서 본 크레이트는 외부 공식 벡터로 바이트 단위 일치를 검증합니다. + +| 검증 항목 | 출처 | 결과 | +|-----------------------------|-----------------------------------------------------------------|-----------------------------------| +| keyGen (44/65/87) | [NIST ACVP](https://github.com/usnistgov/ACVP-Server) FIPS204 | 75/75 (pk·sk 바이트 정확 일치) | +| verify 유효 서명 (44/65/87) | [Wycheproof](https://github.com/C2SP/wycheproof) testvectors_v1 | 226/226 수락 | +| verify 무효 서명 (44/65/87) | Wycheproof testvectors_v1 | 391/391 거부 (가단성·경계값·컨텍스트 변조 포함) | +| sigGen 결정론적 (44) | Wycheproof testvectors_v1 | 73/73 (서명 바이트 정확 일치) | + +ACVP keyGen 첫 벡터(파라미터 셋별 1건)는 `tests/mldsa_test.rs`에 회귀 테스트로 상주합니다. + +## 발견한 문제와 조치 + +위 KAT 검증 과정(2026-06-10)에서 발견되어 수정된 문제를 기술합니다. 두 문제 모두 자가 일관성 테스트는 통과하면서 표준 비적합 상태를 만들었다는 공통점이 있으며, 외부 벡터 검증이 없었다면 발견되지 않았을 결함입니다. + +### CoeffFromHalfByte의 η=2 경로 오구현 (해결) + +`RejBoundedPoly`(Algorithm 31)의 half-byte 변환이 FIPS 204 Algorithm 14와 다르게 구현되어 있었습니다. 기존 코드는 η 값과 무관하게 `z <= 2η`이면 `η - z`를 채택했는데, 표준은 η=2일 때 `b < 15`에 대해 `2 - (b mod 5)`를 요구합니다. 채택 조건과 계수 공식이 모두 달라 ML-DSA-44/87(η=2)의 비밀 벡터 $s_1, s_2$ 분포 자체가 표준과 어긋났습니다. η=4 경로는 두 정의가 우연히 일치하여 ML-DSA-65의 공개 키만 ACVP 벡터와 일치했고, 이것이 원인 격리의 결정적 단서였습니다. + +#### 해결 + +`coeff_from_half_byte::`를 Algorithm 14 그대로 구현하여 교체했습니다. 거부 조건(η=2에서 `b >= 15`)이 mod 5 축소보다 먼저 적용되므로 출력 분포가 $[-2, 2]$ 균등으로 복원되었습니다. + +### BitPack 부호 규칙 위반 (해결) + +`BitPack`/`BitUnpack`(Algorithm 16/18)은 범위 $[-a, b]$의 계수 $w$를 `b - w`로 인코딩해야 하지만, 기존 구현은 `a + w`를 사용했습니다. pack과 unpack이 서로 정확한 역함수였기 때문에 자가 라운드트립과 자기 서명 검증은 전부 통과했지만, $s_1, s_2, t_0$(비밀 키)와 $z$(서명)의 직렬화가 비표준이어서 표준 구현체와 키·서명을 교환하면 전면 실패하는 상태였습니다. 실측으로는 Wycheproof 유효 서명 226건이 전부 거부되고 ACVP keyGen 75건이 전부 불일치했습니다. + +#### 해결 + +인코딩을 `b - w`, 디코딩을 `b - encoded`로 정정했습니다. 비트폭 계산(`bitlen(a + b)`)은 영향이 없으므로 서명·키 길이는 변하지 않습니다. 수정 후 위 KAT 표의 전 항목이 통과했으며, 거부 루프 소거 보강과 `K` 시드의 `Secret` 래핑이 같은 패치로 함께 적용되었습니다. + +--- + +## 오류 타입 + +모든 오류 변형은 `Copy`이며 할당과 문자열 메시지가 없습니다. + +| 변형 | 발생 조건 | +|------------------|---------------------------------------------| +| `ContextTooLong` | 컨텍스트 문자열이 255바이트 초과 | +| `InvalidLength` | 메시지와 컨텍스트 합이 내부 버퍼(1024바이트) 초과 | +| `SigningFailed` | 거부 샘플링 1,000회 초과 (확률적으로 사실상 도달 불가) | +| `InternalError` | XOF 출력 고갈 등 내부 샘플링 실패 (정상 입력에서는 도달 불가) | + +검증 실패는 오류가 아니라 `Ok(false)`로 반환됩니다. 형식 위반 서명(힌트 가단성 포함)과 암호학적 불일치를 호출자가 구분할 필요가 없도록 동일하게 처리합니다. ## 사용법 +```rust +use mldsa::MLDSA65; + +// 키 생성 (xi는 32바이트 시드, 반드시 암호학적 RNG로 생성) +let xi: [u8; 32] = /* RNG */; +let (pk, sk) = MLDSA65::keygen(&xi)?; + +// 헤지드 서명 (rnd 역시 RNG 출력 권장, 결정론적 서명은 [0u8; 32]) +let rnd: [u8; 32] = /* RNG */; +let ctx = b""; +let sig = MLDSA65::sign(&sk, b"message", ctx, &rnd)?; + +// 검증 +assert!(MLDSA65::verify(&pk, b"message", &sig, ctx)?); +``` + +시드 `xi`와 `rnd`는 워크스페이스의 [`rng`](../rng) 크레이트(NIST SP 800-90A Hash_DRBG)로 생성할 수 있습니다. `sk` 바이트 배열은 사용 후 호출자가 직접 `zeroize()` 해야 합니다. 크레이트 내부는 트랜잭션 단위로 모든 비밀 중간값을 소거하지만, 호출자가 보유한 키 사본까지 책임지지 않습니다. From fb692767112a45ba9115c0dc70a5d364130c2cfb Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:46:55 +0900 Subject: [PATCH 13/65] =?UTF-8?q?mldsa/README=5FEN.md=20=EC=9E=AC=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20-=20mldsa/README.md=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=86=8C=ED=95=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mldsa/README.md | 33 +++---- mldsa/README_EN.md | 229 +++++++++++++++++++++++++++------------------ 2 files changed, 157 insertions(+), 105 deletions(-) diff --git a/mldsa/README.md b/mldsa/README.md index 50b7a5e..e72e123 100644 --- a/mldsa/README.md +++ b/mldsa/README.md @@ -1,14 +1,15 @@ -# ML-DSA 크레이트 (mldsa) +# ML-DSA 모듈 [![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) -`mldsa`는 [NIST FIPS 204](https://csrc.nist.gov/pubs/fips/204/final)에 규정된 모듈 격자 기반 전자 서명 알고리즘(Module Lattice-based Digital Signature Algorithm, ML-DSA)의 순수 Rust 구현체입니다. 본 크레이트는 세 가지 파라미터 셋(ML-DSA-44/65/87)을 지원하며, 비밀 키 메모리 보호, 헤지드(hedged) 서명, 상수-시간 필드(field) 연산을 통해 부채널 공격을 방어합니다. 구현 범위는 pure ML-DSA이며, 사전 해시 변형(HashML-DSA)과 external-mu 인터페이스는 제공하지 않습니다. +`mldsa`는 [NIST FIPS 204](https://csrc.nist.gov/pubs/fips/204/final)에 규정된 모듈 격자 기반 전자 서명 알고리즘(Module Lattice-based Digital Signature Algorithm, ML-DSA)의 순수(pure) Rust 구현체입니다. 본 크레이트는 세 가지 파라미터 셋(ML-DSA-44/65/87)을 지원하며, 비밀 키 메모리 보호, 헤지드(hedged) 서명, 상수-시간 필드(field) 연산을 통해 부채널 공격을 방어합니다. 구현 범위는 순수 ML-DSA이며, 사전 해시 변형(HashML-DSA)과 external-mu 인터페이스는 제공하지 않습니다. ## 보안 위협 모델 -RSA 및 ECDSA와 같은 기존 전자 서명 알고리즘은 Shor 알고리즘을 구현한 양자 컴퓨터에 의해 다항식 시간 내 파훼됩니다. ML-DSA는 모듈 격자 위의 LWE(Learning With Errors) 문제와 SIS(Short Integer Solution) 문제의 계산적 난해성에 안전성을 근거하며, 현재 알려진 양자 알고리즘으로도 지수 시간이 소요됩니다. +RSA 및 ECDSA와 같은 기존 전자 서명 알고리즘은 쇼어(Shor) 알고리즘을 구현한 양자 컴퓨터에 의해 다항식 시간 내 파훼됩니다. ML-DSA는 모듈 격자 위의 LWE(Learning With Errors) 문제와 SIS(Short Integer Solution) 문제의 계산적 난해성에 안전성을 근거하며, 현재 알려진 양자 알고리즘으로도 지수 시간이 소요됩니다. + +구현 수준의 공격 표면은 다음 세 가지입니다. -구현 수준의 공격 표면은 세 가지입니다. 1. 비밀 키 메모리 노출 - `s1`, `s2`, `t0`, `K`(서명 시드) 등 비밀 성분이 스왑 파일이나 코어 덤프에 유출될 수 있습니다. 이를 `Secret` 래퍼의 Drop 시 자동 소거와 명시적 `zeroize()` 호출로 방어합니다(아래 '민감 데이터의 소거' 절 참고). 2. 서명 시 타이밍 공격(timing attack) @@ -34,14 +35,14 @@ NIST FIPS 204 Section 4에 정의된 세 가지 파라미터 셋을 지원합니 모든 연산은 $R_q = \mathbb{Z}_q[X]/(X^{256}+1)$, $q = 8380417$ 위에서 이루어지며, 다항식 곱셈은 NTT(Number-Theoretic Transform) 도메인에서 수행됩니다. 모듈 구성은 FIPS 204의 알고리즘 역할을 따릅니다. -| 모듈 | FIPS 204 대응 | 책임 | -|-------------|----------------------------------------------------|-------------------------------| -| `keys.rs` | Algorithm 1/6 (KeyGen), Algorithm 35 (Power2Round) | 키 생성, sk/pk 인코딩·디코딩 | -| `sign.rs` | Algorithm 2/7 (Sign), Algorithm 3/8 (Verify) | 서명·검증, Decompose/Hint 계열 | +| 모듈 | FIPS 204 대응 | 책임 | +|-------------|----------------------------------------------------|--------------------------------------------| +| `keys.rs` | Algorithm 1/6 (KeyGen), Algorithm 35 (Power2Round) | 키 생성, sk/pk 인코딩 및 디코딩 | +| `sign.rs` | Algorithm 2/7 (Sign), Algorithm 3/8 (Verify) | 서명/검증, Decompose/Hint 계열 | | `sample.rs` | Algorithm 29~34 (Sampling) | ExpandA, ExpandS, ExpandMask, SampleInBall | -| `pack.rs` | Algorithm 16~23 (Encoding) | BitPack/BitUnpack, HintBitPack/Unpack | -| `ntt.rs` | Algorithm 41/42 (NTT/NTT⁻¹) | 256-포인트 NTT, ζ 테이블 | -| `field.rs` | - | $\mathbb{Z}_q$ 상수-시간 산술 (Montgomery 곱셈) | +| `pack.rs` | Algorithm 16~23 (Encoding) | BitPack/BitUnpack, HintBitPack/Unpack | +| `ntt.rs` | Algorithm 41/42 (NTT/NTT⁻¹) | 256-포인트 NTT, $\zeta$ 테이블 | +| `field.rs` | - | $\mathbb{Z}_q$ 상수-시간 산술 (Montgomery 곱셈) | ### 키 생성 (Algorithm 6, `ML-DSA.KeyGen_internal`) @@ -98,11 +99,11 @@ $\mathbb{Z}_q$ 산술의 조건부 보정(reduction)은 비밀 값에 의존하 일반 대입(`buf = [0; N]`)은 컴파일러가 dead store로 제거할 수 있습니다. `zeroize`는 모든 소거를 `write_volatile` 기반으로 수행하고, 아키텍처별 배리어로 스토어의 생존과 완료를 강제합니다. -| 단계 | x86_64 | aarch64 | -|-----------|-------------------------|-------------------------| -| 휘발성 store | `write_volatile` (제거 불가) | `write_volatile` (제거 불가) | -| 컴파일러 배리어 | 빈 `asm!` + `compiler_fence(SeqCst)` | 빈 `asm!` + `compiler_fence(SeqCst)` | -| CPU 메모리 배리어 | `mfence` | `dsb sy` | +| 단계 | x86_64 | aarch64 | +|-------------|-------------------------------------|-------------------------------------| +| 휘발성 store | `write_volatile` (제거 불가) | `write_volatile` (제거 불가) | +| 컴파일러 배리어 | 빈 `asm!` + `compiler_fence(SeqCst)` | 빈 `asm!` + `compiler_fence(SeqCst)` | +| CPU 메모리 배리어 | `mfence` | `dsb sy` | 릴리즈 바이너리에서 zero-store와 배리어 명령(aarch64 기준 `strb wzr` + `dsb sy`)이 LTO 이후에도 살아남는 것은 zeroize 크레이트의 별도 probe 빌드로 검증되어 있습니다. diff --git a/mldsa/README_EN.md b/mldsa/README_EN.md index 8beff9e..2f2936b 100644 --- a/mldsa/README_EN.md +++ b/mldsa/README_EN.md @@ -1,151 +1,202 @@ -# ML-DSA Crate (entlib-native-mldsa) +# ML-DSA Module -> Q. T. Felix (revised: 26.03.24 UTC+9) -> -> [Korean README](README.md) +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](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. +`mldsa` is a pure Rust implementation of the Module Lattice-based Digital Signature Algorithm (ML-DSA) as specified in [NIST FIPS 204](https://csrc.nist.gov/pubs/fips/204/final). This crate supports three parameter sets (ML-DSA-44/65/87) and defends against side-channel attacks through secret key memory protection, hedged signing, and constant-time field arithmetic. The implementation scope is pure ML-DSA; the pre-hash variant (HashML-DSA) and the external-mu interface are not provided. ## 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. +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 LWE (Learning With Errors) problem and the SIS (Short Integer Solution) problem over module lattices, both of which require exponential time even under known quantum algorithms. -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. +The implementation-level attack surface consists of three areas. + +1. Secret key memory exposure + - Secret components such as `s1`, `s2`, `t0`, and `K` (signing seed) may leak into swap files or core dumps. This is mitigated by automatic zeroization on Drop via the `Secret` wrapper and by explicit `zeroize()` calls (see the "Sensitive Data Zeroization" section below). +2. Timing attacks during signing + - Branches that depend on secret components can expose the signing key. Finite-field operations (`Fq::add`, `Fq::sub`, `Fq::mul`, `power2round`, etc.) are implemented using constant-time select operations from the [`constant-time`](../constant-time) crate. +3. Nonce reuse + - If the masking vector `y` is reused or exposed, the secret key can be recovered from the relation $z = y + c s_1$. Hedged signing mode (`rnd <- RNG`) also guards against the reduced fault-attack resistance of the deterministic mode. ## 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 | +| Parameter Set | NIST Security Category | pk size | sk size | sig size | λ (collision strength) | +|---------------|:-----------------------:|--------:|--------:|--------:|:---------------------:| +| ML-DSA-44 | 2 (AES-128 equivalent) | 1312 B | 2560 B | 2420 B | 128-bit | +| ML-DSA-65 | 3 (AES-192 equivalent) | 1952 B | 4032 B | 3309 B | 192-bit | +| ML-DSA-87 | 5 (AES-256 equivalent) | 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. +Each parameter set varies the 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$. Monomorphization via compile-time const generics eliminates all runtime overhead. -## Public API +--- -### `MLDSA` Struct: Algorithms 1–3 +## Algorithm Implementation -`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. +All operations are performed over $R_q = \mathbb{Z}_q[X]/(X^{256}+1)$, $q = 8380417$, and polynomial multiplication is carried out in the NTT (Number-Theoretic Transform) domain. The module structure follows the algorithm roles defined in FIPS 204. -```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(); +| Module | FIPS 204 Correspondence | Responsibility | +|---------------|--------------------------------------------------------------|--------------------------------------------------------| +| `keys.rs` | Algorithm 1/6 (KeyGen), Algorithm 35 (Power2Round) | Key generation, sk/pk encoding and decoding | +| `sign.rs` | Algorithm 2/7 (Sign), Algorithm 3/8 (Verify) | Signing/verification, Decompose/Hint operations | +| `sample.rs` | Algorithm 29~34 (Sampling) | ExpandA, ExpandS, ExpandMask, SampleInBall | +| `pack.rs` | Algorithm 16~23 (Encoding) | BitPack/BitUnpack, HintBitPack/Unpack | +| `ntt.rs` | Algorithm 41/42 (NTT/NTT^-1) | 256-point NTT, $\zeta$ table | +| `field.rs` | - | $\mathbb{Z}_q$ constant-time arithmetic (Montgomery multiplication) | -// Algorithm 2: ML-DSA.Sign (hedged — rnd ← RNG) -let sig = MLDSA::sign(&sk, message, ctx, &mut rng).unwrap(); +### Key Generation (Algorithm 6, `ML-DSA.KeyGen_internal`) -// Algorithm 3: ML-DSA.Verify -let ok = MLDSA::verify(&pk, message, &sig, ctx).unwrap(); -assert!(ok); -``` +A key pair is derived deterministically from a 32-byte seed $\xi$. -**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`. +1. Expand to 128 bytes via $(\rho, \rho', K) \leftarrow \text{SHAKE256}(\xi \| k \| l)$. Including the domain-separation bytes $k, l$ prevents seed-reuse attacks across parameter sets. +2. Generate the public matrix $\hat{A} \in R_q^{k \times l}$ from $\rho$ via SHAKE128 rejection sampling (`RejNTTPoly`). The matrix is sampled directly in the NTT domain, eliminating transform overhead. +3. Generate secret vectors $s_1 \in R_q^l$ and $s_2 \in R_q^k$ from $\rho'$ via SHAKE256 rejection sampling (`RejBoundedPoly`, coefficient range $[-\eta, \eta]$). +4. Compute $t = \text{NTT}^{-1}(\hat{A} \circ \text{NTT}(s_1)) + s_2$ and decompose with `Power2Round` ($d = 13$) into the high bits $t_1$ (public) and low bits $t_0$ (secret). +5. The public key is $(\rho, t_1)$ and the secret key is $(\rho, K, tr, s_1, s_2, t_0)$, where $tr = \text{SHAKE256}(pk)$. -**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. +### Signing (Algorithm 7, `ML-DSA.Sign_internal`) -### `MLDSAParameter` Enum +A rejection-sampling loop with Fiat-Shamir with Aborts structure. -```rust -pub enum MLDSAParameter { MLDSA44, MLDSA65, MLDSA87 } -``` +1. Derive the message representative $\mu = \text{SHAKE256}(tr \| M')$ and masking seed $\rho'' = \text{SHAKE256}(K \| rnd \| \mu)$. When `rnd` is RNG output the mode is hedged; when it is zero the mode is deterministic. +2. Each loop iteration generates a masking vector $y \leftarrow \text{ExpandMask}(\rho'', \kappa)$, then computes $w = \text{NTT}^{-1}(\hat{A} \circ \text{NTT}(y))$ and $w_1 = \text{HighBits}(w)$. +3. From the challenge $\tilde{c} = \text{SHAKE256}(\mu \| w_1)$, `SampleInBall` constructs a sparse polynomial $c$ with $\tau$ coefficients in $\pm 1$, then computes $z = y + c s_1$. +4. If $\lVert z \rVert_\infty \ge \gamma_1 - \beta$ or $\lVert \text{LowBits}(w - c s_2) \rVert_\infty \ge \gamma_2 - \beta$, all secret intermediates of that iteration are zeroized and $\kappa$ is incremented for a retry. This rejection test is the key mechanism that removes secret key dependency from the signature distribution. +5. Construct the verifier correction hint $h = \text{MakeHint}(-c t_0,\ w - c s_2 + c t_0)$; if $\lVert c t_0 \rVert_\infty \ge \gamma_2$ or the hint count exceeds $\omega$, retry as well. +6. The signature is $(\tilde{c}, z, h)$. The loop is capped at 1,000 iterations (expected iteration counts per FIPS 204 Table 2 are 4.25/5.1/3.85) and returns `Error::SigningFailed` on overflow. -`pk_len()`, `sk_len()`, and `sig_len()` are provided as `const fn`. +### Verification (Algorithm 8, `ML-DSA.Verify_internal`) -## Key Types +1. `sigDecode` validates the signature structure. In particular, `HintBitUnpack` (Algorithm 21) checks that hint indices are strictly increasing, the cumulative limit is respected, and trailing bytes are zero-padded, blocking signature malleability. Any format violation immediately fails verification. +2. Compute $w'_{approx} = \hat{A} \circ \text{NTT}(z) - \text{NTT}(c) \circ \text{NTT}(t_1 \cdot 2^d)$ and recover the signer's $w_1$ via $w'_1 = \text{UseHint}(h, \text{NTT}^{-1}(w'_{approx}))$. +3. After the $\lVert z \rVert_\infty < \gamma_1 - \beta$ check, recompute $\tilde{c}' = \text{SHAKE256}(\mu \| w'_1)$ and compare against $\tilde{c}$. The comparison uses a constant-time byte comparison (`ct_eq_bytes`) based on XOR accumulation. -### `MLDSAPublicKey` +The public API (`MLDSA44::sign/verify`, etc.) binds context (up to 255 bytes) in the format $M' = 0x00 \| |ctx| \| ctx \| M$ per FIPS 204 Algorithms 2/3. Fixed-length inputs (pk, sk, signature, seed) are accepted as fixed-size array references (`&[u8; N]`), so incorrect lengths are rejected by the type system at compile time. -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. +## Low-Level (Assembly) Perspective -### `MLDSAPrivateKey` +`mldsa` itself contains no inline assembly. Instead, secret-dependent operations are delegated to the verified low-level primitives in [`constant-time`](../constant-time) and [`zeroize`](../zeroize), inheriting their guarantees directly. Both crates reject architectures without verified inline assembly support via compile-time gates, so `mldsa`'s supported targets are likewise limited to x86_64 and aarch64. -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. +### Machine-Level Behavior of Constant-Time Field Arithmetic -> [!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. +Conditional reduction in $\mathbb{Z}_q$ arithmetic is the sole point that can introduce branches dependent on secret values. This implementation handles all reductions via `i32::select` (internally `ct_sel32`), a primitive that uses only data-independent latency instructions. -## RNG Abstraction +| Operation | x86_64 | aarch64 | +|------------------------------------|-------------------|----------------| +| Conditional select (`Fq::add/sub` reduction) | `test` + `cmovnz` | `cmp` + `csel` | +| Sign determination (`is_negative_ct`) | `sar` arithmetic shift | `asr` arithmetic shift | -### `MLDSARng` Trait +- `Fq::add` unconditionally computes $a + b - q$, then selects one of the two candidates using the sign bit (`(v >> 31) & 1`) via `cmov`/`csel`. No conditional branch (`jcc`, `b.cc`) is emitted. +- `Fq::mul` is implemented with $R = 2^{32}$ Montgomery REDC. The 64-bit multiply instruction (x86_64 `imul`, aarch64 `mul`/`smulh`) has fixed latency independent of operand values on both targets. ISA-level guarantees (aarch64 DIT, x86 DOITM) remain a future hardening task, consistent with the `constant-time` crate. +- NTT butterfly memory access indices are derived only from public loop variables and precomputed $\zeta$ table indices, so no secret-dependent cache timing can arise. +- Serialization of secret coefficients (`BitPack`) uses `fq_to_signed_ct`, which performs sign conversion using only `wrapping_sub` and a sign-bit mask, keeping the packing path branch-free. -```rust -pub trait MLDSARng { - fn fill_random(&mut self, dest: &mut [u8]) -> Result<(), MLDSAError>; -} -``` +The norm check and accept/reject branch in the rejection sampling loop are derived from secret values, but rejected candidates are immediately zeroized and never exposed externally; the accept decision itself is public information (the point of signature output). This is the intended behavior of FIPS 204's Fiat-Shamir with Aborts design. -Implementors must provide a DRBG with security strength ≥ 256-bit per NIST SP 800-90A Rev.1 or later. +### Machine-Level Behavior of Zeroization -### `HashDRBGRng` +A plain assignment (`buf = [0; N]`) may be eliminated by the compiler as a dead store. `zeroize` performs all zeroization via `write_volatile` and forces the stores to survive and complete using architecture-specific barriers. -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)`. +| Step | x86_64 | aarch64 | +|--------------------|--------------------------------------|--------------------------------------| +| Volatile store | `write_volatile` (non-eliminatable) | `write_volatile` (non-eliminatable) | +| Compiler barrier | empty `asm!` + `compiler_fence(SeqCst)` | empty `asm!` + `compiler_fence(SeqCst)` | +| CPU memory barrier | `mfence` | `dsb sy` | -### `CtrDRBGRng` +The survival of zero-stores and barrier instructions (e.g. `strb wzr` + `dsb sy` on aarch64) through LTO in release binaries is verified by a separate probe build in the zeroize crate. -Reserved struct for NIST CTR_DRBG (AES-256-CTR). All methods return `NotImplemented` until `entlib-native-aes` is complete. +--- -## Internal Algorithm Structure +## Sensitive Data Zeroization -### Key Generation (Algorithm 4/6) +All secret components in this crate are zeroized in two layers. Long-lived values are automatically zeroized on Drop by the `Secret` RAII wrapper, and stack copies that escape the wrapper due to `Copy` semantics of `Poly`/`PolyVec` are immediately zeroized by explicit `zeroize()` calls. -The seed $\xi \in \mathbb{B}^{32}$ is expanded via SHAKE256 to derive $(\rho, \rho', K_{\text{seed}})$. +| Secret Value | Location | Zeroization Method | +|-------------------------------------------|-------------------------|-------------------------------------------------------| +| $s_1, s_2, t_0$, $K$ | `PrivateKey` fields | Automatic `Secret` Drop zeroization | +| $\mu, \rho''$ | Stack during signing | Automatic `Secret` Drop zeroization | +| $\xi$ expansion buffer, $\rho'$, $K$ local copy | Stack during keygen | Explicit `zeroize()` before return | +| NTT-transformed $s_1$ copy, $t = A s_1 + s_2$ | Stack during keygen | Explicit `zeroize()` before return | +| $y, \hat{y}, w, c s_1, z, c s_2, r_0$ | Signing rejection loop | Explicit `zeroize()` before each iteration ends, on all paths | +| $c t_0$, hint computation intermediates | Signing rejection loop | Explicit `zeroize()` before each iteration ends, on all paths | +| $\hat{s}_1, \hat{s}_2, \hat{t}_0$ | Signing function scope | Explicit `zeroize()` on both success and failure paths | -$$A_{\hat{}} \leftarrow \text{ExpandA}(\rho), \quad (s_1, s_2) \leftarrow \text{ExpandS}(\rho')$$ +The design principles are as follows. -$$t = \text{INTT}(A_{\hat{}} \circ \text{NTT}(s_1)) + s_2, \quad (t_1, t_0) \leftarrow \text{Power2Round}(t, d)$$ +- **Zeroize on every exit path of the rejection loop.** Both paths that `continue` on norm check failure and the path that returns after signature acceptance zeroize the iteration's secret intermediates. Rejected $y$ and $z$ pairs are the most sensitive values: a single exposed pair directly enables recovery of the secret key via $z - y = c s_1$. +- **Directly zeroize `Copy` residual copies.** `Secret::new(v)` protects only the copy held inside the wrapper; it does not zeroize the original stack slot. Keygen explicitly zeroizes originals (e.g. `s2pv`, `t0pv`, NTT-transformed `s1`) immediately after wrapping them in `Secret`. +- **Do not zeroize public values.** $\rho$, $t_1$, $tr$ (hash of the public key), $w_1$, and $\tilde{c}$ are public components and are not zeroization targets. -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 verification path processes only public inputs (pk, signature, message) and therefore contains no secrets that require zeroization. -The trace $tr = H(\text{pkEncode}(\rho, t_1), 64)$ is computed via incremental SHAKE256 hashing. +--- -### Signing (Algorithm 5) +## KAT Verification -A rejection-sampling loop. On each attempt: +Self-consistency tests (sign-then-verify with the same implementation) cannot detect encoding bugs, because a non-standard serialization will still pass roundtrip as long as pack and unpack are mutual inverses. This crate therefore verifies byte-exact agreement against official external vectors. -$$y \leftarrow \text{ExpandMask}(\rho'', \kappa), \quad w = \text{INTT}(A_{\hat{}} \circ \text{NTT}(y))$$ +| Verification Item | Source | Result | +|--------------------------------|-----------------------------------------------------------------|-------------------------------------------------------------| +| keyGen (44/65/87) | [NIST ACVP](https://github.com/usnistgov/ACVP-Server) FIPS204 | 75/75 (pk and sk byte-exact match) | +| verify valid signatures (44/65/87) | [Wycheproof](https://github.com/C2SP/wycheproof) testvectors_v1 | 226/226 accepted | +| verify invalid signatures (44/65/87) | Wycheproof testvectors_v1 | 391/391 rejected (includes malleability, boundary, context tampering) | +| sigGen deterministic (44) | Wycheproof testvectors_v1 | 73/73 (signature bytes exact match) | -$$w_1 = \text{HighBits}(w, 2\gamma_2), \quad \tilde{c} \leftarrow H(\mu \| w_1, \lambda/4)$$ +The first ACVP keyGen vector for each parameter set resides as a regression test in `tests/mldsa_test.rs`. -$$z = y + c \cdot s_1$$ +## Bugs Found and Fixed -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`. +This section describes issues discovered and corrected during the KAT verification process (2026-06-10). Both bugs passed self-consistency tests while leaving the implementation in a non-conformant state -- they would not have been found without external vector verification. -### Verification (Algorithm 7) +### CoeffFromHalfByte eta=2 Path Misimplemented (Resolved) -Returns `false` immediately if $\|z\|_\infty \ge \gamma_1 - \beta$ or $\|h\|_1 > \omega$. +The half-byte conversion in `RejBoundedPoly` (Algorithm 31) differed from FIPS 204 Algorithm 14. The original code accepted `z <= 2η` and returned `η - z` regardless of the η value, but the standard requires `2 - (b mod 5)` for η=2 when `b < 15`. Both the acceptance condition and the coefficient formula differed, causing the secret vectors $s_1$ and $s_2$ of ML-DSA-44/87 (η=2) to follow a distribution inconsistent with the standard. The η=4 path coincidentally matched both definitions, so only ML-DSA-65's public key agreed with the ACVP vectors -- this was the decisive clue for isolating the root cause. -$$w_1' = \text{UseHint}(h,\; \text{INTT}(A_{\hat{}} \circ \text{NTT}(z)) - c \cdot t_1 \cdot 2^d,\; 2\gamma_2)$$ +#### Resolution -Compares $\tilde{c}$ against the recomputed $H(\mu \| w_1', \lambda/4)$. +`coeff_from_half_byte::` was replaced with a direct implementation of Algorithm 14. Because the rejection condition (for η=2: `b >= 15`) is applied before the mod-5 reduction, the output distribution is restored to uniform over $[-2, 2]$. -> [!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. +### BitPack Sign Convention Violation (Resolved) -### NTT / Finite Field Arithmetic +`BitPack`/`BitUnpack` (Algorithm 16/18) must encode a coefficient $w$ in the range $[-a, b]$ as `b - w`, but the original implementation used `a + w`. Because pack and unpack were exact inverses of each other, self-roundtrip and self-verification tests all passed, but the serialization of $s_1$, $s_2$, $t_0$ (secret key) and $z$ (signature) was non-standard, causing a complete failure when exchanging keys or signatures with a conformant implementation. In practice, all 226 valid Wycheproof signatures were rejected and all 75 ACVP keyGen vectors mismatched. -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$. +#### Resolution -`Fq::add` and `Fq::sub` are branch-free constant-time implementations using `ct_is_negative` + `ct_select`. +Encoding was corrected to `b - w` and decoding to `b - encoded`. The bit-width calculation (`bitlen(a + b)`) was unaffected, so signature and key lengths are unchanged. After the fix, all entries in the KAT table above passed. The rejection loop zeroization reinforcement and wrapping `K` seed in `Secret` were applied in the same patch. + +--- ## 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) | +All error variants are `Copy` with no allocation or string messages. + +| Variant | Condition | +|------------------|----------------------------------------------------------------------| +| `ContextTooLong` | Context string exceeds 255 bytes | +| `InvalidLength` | Combined message and context exceeds the internal buffer (1024 bytes) | +| `SigningFailed` | Rejection sampling exceeded 1,000 iterations (probabilistically unreachable in practice) | +| `InternalError` | Internal sampling failure such as XOF output exhaustion (unreachable on valid input) | + +Verification failure is returned as `Ok(false)`, not an error. Malformed signatures (including hint malleability) and cryptographic mismatches are treated identically so that callers need not distinguish between them. + +## Usage + +```rust +use mldsa::MLDSA65; + +// Key generation (xi is a 32-byte seed; must be generated by a cryptographic RNG) +let xi: [u8; 32] = /* RNG */; +let (pk, sk) = MLDSA65::keygen(&xi)?; + +// Hedged signing (rnd should also be RNG output; use [0u8; 32] for deterministic signing) +let rnd: [u8; 32] = /* RNG */; +let ctx = b""; +let sig = MLDSA65::sign(&sk, b"message", ctx, &rnd)?; + +// Verification +assert!(MLDSA65::verify(&pk, b"message", &sig, ctx)?); +``` + +The seeds `xi` and `rnd` can be generated using the workspace [`rng`](../rng) crate (NIST SP 800-90A Hash_DRBG). The caller is responsible for explicitly calling `zeroize()` on the `sk` byte array after use. The crate internally zeroizes all secret intermediates on a per-transaction basis but does not take responsibility for key copies held by the caller. From 085a836af44389d9bc584f33b64f2e8b75e25978 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:59:17 +0900 Subject: [PATCH 14/65] =?UTF-8?q?cross-confirm.md=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cross-confirm.md b/cross-confirm.md index fd4daf2..514d076 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -7,7 +7,7 @@ - [X] AES - [ ] BLAKE -- [X] SHA2 +- [X] SHA2 (명세 없음) - [X] SHA3 (명세 없음) - [ ] ChaCha20 - [ ] Ed448 @@ -18,9 +18,9 @@ - [ ] ML-KEM - [X] RNG - [X] Constant-Time -- [X] Zeroize +- [X] Zeroize (명세 보강 필요) 여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "담당 명세에 따라 정확하게 구현되어 있으며, 정상적으로 동작한다"는 것을 명시하는 것입니다. > [!IMPORTANT] -> 말인 즉슨, 이 검증 작업이 완료되었다고 해서 이 암호 모듈이 완전해진 것은 아니라는 것입니다. 암호 모듈 검증을 위해선 전문 검증 단체의 승인(CMVP)이 필요하고, 이 작업은 수 개월에서 많게는 수 년까지 소요될 수 있습니다. +> 즉, 이 검증 작업이 완료되었다고 해서 이 암호 모듈이 완전히 안전해진 것이 아닙니다. 암호 모듈 검증을 위해선 전문 검증 단체의 승인(CMVP)이 필요하고, 이 작업은 수 개월에서 많게는 수 년까지 소요될 수 있습니다. From fc319fdd5be955a0b33106a77a2254d1e81e74b5 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:01:45 +0900 Subject: [PATCH 15/65] =?UTF-8?q?sha2=20=EB=AC=B8=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20-=20README=20=EC=88=98=ED=95=99=20=EA=B8=B0?= =?UTF-8?q?=ED=98=B8=20LaTeX=20=ED=91=9C=EA=B8=B0=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20README=5FEN=20=EC=98=81=EB=AC=B8=ED=8C=90=20?= =?UTF-8?q?=EC=8B=A0=EA=B7=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sha2/README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++ sha2/README_EN.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 sha2/README.md create mode 100644 sha2/README_EN.md diff --git a/sha2/README.md b/sha2/README.md new file mode 100644 index 0000000..0964fc8 --- /dev/null +++ b/sha2/README.md @@ -0,0 +1,121 @@ +# SHA-2 모듈 + +[![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) + +FIPS 180-4 SHA-224/256/384/512 해시 함수를 외부 의존성 없이 `no_std` 순수 Rust로 구현한 크레이트입니다. 이 문서는 기능 명세와 보안 설계 근거, 표준 부합 근거, 그리고 발견한 문제와 조치를 기술합니다. + +--- + +## 구현된 기능 + +| 타입 | 표준 | 출력 | 블록 크기 | 내부 워드 | 라운드 | 상태 구조체 | +|----------|-------------------|-------|--------|-------|-----|---------------| +| `SHA224` | FIPS 180-4 §5.3.2 | 28바이트 | 512비트 | `u32` | 64 | `SHA256State` | +| `SHA256` | FIPS 180-4 §5.3.3 | 32바이트 | 512비트 | `u32` | 64 | `SHA256State` | +| `SHA384` | FIPS 180-4 §5.3.5 | 48바이트 | 1024비트 | `u64` | 80 | `SHA512State` | +| `SHA512` | FIPS 180-4 §5.3.4 | 64바이트 | 1024비트 | `u64` | 80 | `SHA512State` | + +공개 API는 `SHA2` 트레이트와 `Digest` 타입으로 구성됩니다. + +- `SHA2` 트레이트: `new()` -> `update(&[u8])` (임의 횟수) -> `finalize(self) -> Digest`의 스트리밍 인터페이스. `finalize`는 `self`를 소비하므로 인스턴스 재사용이 타입 차원에서 차단됩니다. +- `Digest`: 고정 64바이트 버퍼와 길이 필드를 갖는 반환 컨테이너. `as_bytes()`로 길이만큼의 슬라이스를 노출하며, Drop 시 자체 소거됩니다. + +설계 결정 명세는 다음과 같습니다. + +- SHA-224는 SHA-256과, SHA-384는 SHA-512와 내부 상태 구조체 및 압축 함수를 공유합니다. 차이는 초기 해시 값과 출력 절단 길이뿐입니다. +- 다이제스트는 원시 `[u8; N]`이 아니라 자체 소거되는 `Digest`로만 반환합니다. 원시 배열 반환은 프로젝트가 금지하는 anti-pattern입니다. +- 모든 버퍼는 고정 크기 스택 배열입니다. `alloc`을 사용하지 않으며, 크레이트는 무조건 `#![no_std]`입니다. +- 메시지 길이 카운터(`total_len`)는 비트 단위 `u64`입니다. SHA-256 계열은 64비트 길이 필드(FIPS 180-4 §5.1.1)에 그대로 대응하고, SHA-512 계열의 128비트 길이 필드(§5.1.2)는 상위 64비트를 항상 0으로 두고 하위 64비트만 채웁니다. 따라서 실질 입력 한계는 두 계열 모두 $2^{64}-1$ 비트(약 2 EiB)이며, 고정 스택 환경에서 도달 불가능합니다. + +## 보안 처리 근거 + +SHA-2는 비밀 메시지(HMAC 키, Ed25519 nonce 등)에 적용될 수 있으므로, 실행 시간이 입력 값에 의존하지 않아야 합니다. 이 크레이트는 압축 함수와 패딩 경로 모두에서 데이터 비종속을 보장합니다. + +### 1. 압축 함수는 데이터 비종속 연산만 사용 + +메시지 스케줄과 라운드 함수는 `rotate_right`, `wrapping_add`, `^`, `&`, `!` 만으로 구성됩니다. $\mathrm{Ch}$, $\mathrm{Maj}$, $\Sigma_0$, $\Sigma_1$, $\sigma_0$, $\sigma_1$ 모두 회전·시프트·논리 연산의 합성이며, 비밀 값에 따른 분기나 비밀 의존 메모리 접근(룩업 테이블)이 전혀 없습니다. 라운드 상수 `K`는 고정 인덱스로만 접근하므로 캐시 및 TLB 타이밍 부채널이 원천 차단됩니다. + +### 2. 버퍼링·패딩의 상수-시간 처리 + +메시지 길이는 SHA-2의 공개 정보이지만, 구현 일관성과 블록 수의 예측 가능성을 위해 길이 비교를 상수-시간 연산으로 수행합니다. + +- `update`의 청크 길이 계산은 `CtGreeter::gt`와 `CtEqOps::eq`로 `is_ge`를 만든 뒤 `usize::select`로 `min(remain, fill)`을 분기 없이 선택합니다. +- `finalize`의 패딩은 길이 필드가 현재 블록에 들어가는지를 `Choice`(SHA-256 계열 `buffer_len > 56`, SHA-512 계열 `buffer_len > 112`)로 판정합니다. 길이 필드 주입은 `u8::select`로 바이트별 수행하고, 두 번째 블록은 필요 여부와 무관하게 무조건 처리한 뒤 최종 해시 상태를 `u32::select`/`u64::select`로 선택합니다. +- 결과적으로 `finalize`는 `needs_extra` 여부와 무관하게 항상 정확히 2개의 블록을 압축합니다. 메시지 길이가 패딩에 영향을 주는지 여부가 압축 횟수로 새지 않습니다. + +이 select·eq·gt 연산은 `constant-time` 크레이트의 인-라인 어셈블리 프리미티브에 위임됩니다. 검증된 상수-시간 구현이 있는 x86_64·aarch64에서만 빌드가 허용되는 점은 `constant-time` README와 공유합니다. + +## 비밀 소거 (zeroize) + +트랜잭션 단위 전부 소거 원칙에 따라 다음이 보장됩니다. + +| 비밀 | 보호 방식 | +|------------------------------------------------------|-----------------------------------------------| +| 해시 상태(`[u32; 8]` / `[u64; 8]`) | `Secret`, Drop 시 volatile 소거 + `Zeroize` | +| 입력 버퍼(`[u8; 64]` / `[u8; 128]`) | `Secret`, 블록 처리 직후 즉시 `zeroize` | +| 메시지 스케줄 `w`(`[u32; 64]` / `[u64; 80]`) | `Secret`, `process_block` 종료 시 Drop 소거 | +| 워킹 변수 `a`..`h` | `process_block` 종료 시 명시적 `zeroize` | +| 라운드 임시변수 `s0`·`s1`·`ch`·`maj`·`temp1`·`temp2` | 루프 밖 선언 후 명시적 `zeroize` | +| `process_block` 입력 블록 사본 | `Secret`, 스코프 종료 시 Drop 소거 | +| `finalize`의 `block1`·`block2`·`state_b1` | `Secret`, 스코프 종료 시 Drop 소거 | +| `total_len`(비트 카운터) | Drop에서 `volatile_write(0)` + `compiler_fence` | +| `Digest`의 출력 바이트 | `Secret<[u8; 64]>`, Drop 시 소거 + `Zeroize` | +| 미사용 다이제스트 워드(SHA-224 `[28..32]`, SHA-384 `[48..64]`) | 다이제스트 빌드 시 `zeroize` | + +상태 구조체(`SHA256State`·`SHA512State`)의 `Drop`은 비-`Secret` 필드인 `total_len`을 `volatile_write`로 0으로 만들고 `compiler_fence(SeqCst)`로 봉인하며, `Secret` 필드는 각자의 Drop에서 volatile 소거됩니다. + +잔여 한계: 메시지 스케줄 루프(`for i in 16..64` / `16..80`) 내부의 $\sigma$ 임시값과 `from_be_bytes`·회전 중간값은 레지스터에 짧게 상주하며 명시적 소거 대상이 아닙니다. 레지스터·스필 잔존(CWE-316)은 zeroize 모델의 알려진 한계이며, 후속 연산이 즉시 덮어쓰는 짧은 수명에 의존합니다. + +## 표준 부합 근거 + +| 요소 | FIPS 180-4 절 | +|--------------------|-------------------------------------------------------| +| 초기 해시 값 | §5.3.2(224) / §5.3.3(256) / §5.3.4(512) / §5.3.5(384) | +| 라운드 상수 `K` | §4.2.2(SHA-256 64개) / §4.2.3(SHA-512 80개) | +| $\mathrm{Ch}$·$\mathrm{Maj}$·$\Sigma$·$\sigma$ | §4.1.2(SHA-256) / §4.1.3(SHA-512) | +| 패딩·길이 필드 | §5.1.1(512비트 블록·64비트 길이) / §5.1.2(1024비트 블록·128비트 길이) | +| 메시지 스케줄·압축 | §6.2.2(SHA-256) / §6.4.2(SHA-512) | + +이 크레이트 자체에는 KAT 테스트가 포함되어 있지 않으나, 다음 두 경로로 간접 검증됩니다. + +- `ed25519`는 `sha2::SHA512`를 서명·검증에 사용하며 RFC 8032 테스트 벡터로 검증됩니다. +- `rng`의 Hash_DRBG는 `SHA224`/`SHA256`/`SHA384`/`SHA512`를 모두 사용하며 NIST SP 800-90A 절차로 검증됩니다. + +`cargo fmt -p sha2 -- --check`, `cargo clippy -p sha2 --all-targets --all-features -- -D warnings`, `cargo build -p sha2 --target x86_64-unknown-none`, `cargo build -p sha2 --target aarch64-unknown-none` 모두 무경고 통과합니다. + +## 호출자 계약 + +이 크레이트는 stateless이므로 다음은 호출자가 보장하거나 인지해야 합니다. + +1. **1회용 finalize:** `finalize`는 `self`를 소비합니다. 같은 입력에 대한 재해시는 새 인스턴스로 수행해야 합니다. +2. **다이제스트 즉시 사용:** `Digest`는 Drop 시 소거됩니다. `as_bytes()` 결과를 호출자 측에 보존하려면 그 즉시 복사해야 하며, 비밀 다이제스트라면 복사본도 호출자가 소거해야 합니다. +3. **길이 한계:** 입력은 $2^{64}-1$ 비트 미만이어야 합니다(설계 한계, 사실상 도달 불가능). 초과 시 비트 카운터가 wrap되어 패딩 길이가 어긋납니다. + +--- + +## 발견한 문제와 조치 + +검증 과정에서 발견한 문제와 해결을 기술합니다. + +### 압축 함수 워킹·임시 변수 소거 누락 (해결) + +해시 상태·버퍼·메시지 스케줄은 `Secret`으로 보호되고 있었으나, 압축 함수의 워킹 변수 `a`..`h`와 라운드 임시변수 `s0`·`s1`·`ch`·`maj`·`temp1`·`temp2`는 비밀 상태에서 파생된 값임에도 함수 종료 후 스택에 잔존했습니다. "한 요청·한 데이터·즉시 소거" 헌장 위반입니다. + +#### 해결 + +라운드 임시변수를 루프 밖으로 끌어올려 압축 종료 후 워킹 변수와 함께 명시적으로 `zeroize()`하도록 변경했습니다. 위 zeroize 절의 표가 조치 후 상태입니다. + +### SHA-224·SHA-384 미사용 다이제스트 워드 잔존 (해결) + +다이제스트 빌드는 항상 8개 워드를 64바이트 버퍼에 직렬화하지만, SHA-224는 7번째 워드 이후(`[28..32]`), SHA-384는 6번째 워드 이후(`[48..64]`)가 출력에 포함되지 않습니다. 이 영역은 `as_bytes()`로 노출되지 않으나 비밀 상태 파생값이 메모리에 남았습니다. + +#### 해결 + +`is_224`·`is_384` 경로에서 미사용 워드 영역을 읽기 전에 `zeroize()`하여 메모리 상주 시간을 제한했습니다. + +### TODO + +- 메시지 스케줄 루프 내부 $\sigma$ 임시값 등 레지스터 상주 중간값의 잔존(위 zeroize 절의 잔여 한계) +- SHA-512 계열 128비트 길이 필드의 상위 64비트 미지원(실질 $2^{64}-1$ 비트 한계) +- 크레이트 내 직접 KAT 부재(현재는 `ed25519`·`rng`를 통한 간접 검증) +- aarch64 DIT·x86 DOITM 하드닝은 `constant-time` 크레이트와 공동 과제 diff --git a/sha2/README_EN.md b/sha2/README_EN.md new file mode 100644 index 0000000..acd1fbd --- /dev/null +++ b/sha2/README_EN.md @@ -0,0 +1,121 @@ +# SHA-2 Module + +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) + +A crate implementing the FIPS 180-4 SHA-224 / SHA-256 / SHA-384 / SHA-512 hash functions in pure `no_std` Rust with no external dependencies. This document describes the functional specification, the security design rationale, the standards-conformance basis, and the issues found with their mitigations. + +--- + +## Implemented Features + +| Type | Standard | Output | Block Size | Internal Word | Rounds | State Struct | +|----------|-------------------|----------|------------|---------------|--------|---------------| +| `SHA224` | FIPS 180-4 §5.3.2 | 28 bytes | 512 bits | `u32` | 64 | `SHA256State` | +| `SHA256` | FIPS 180-4 §5.3.3 | 32 bytes | 512 bits | `u32` | 64 | `SHA256State` | +| `SHA384` | FIPS 180-4 §5.3.5 | 48 bytes | 1024 bits | `u64` | 80 | `SHA512State` | +| `SHA512` | FIPS 180-4 §5.3.4 | 64 bytes | 1024 bits | `u64` | 80 | `SHA512State` | + +The public API consists of the `SHA2` trait and the `Digest` type. + +- `SHA2` trait: a streaming interface of `new()` -> `update(&[u8])` (any number of times) -> `finalize(self) -> Digest`. `finalize` consumes `self`, so instance reuse is blocked at the type level. +- `Digest`: a return container holding a fixed 64-byte buffer and a length field. It exposes a slice of the relevant length via `as_bytes()` and zeroizes itself on drop. + +Design decisions are as follows. + +- SHA-224 shares its internal state struct and compression function with SHA-256, and SHA-384 with SHA-512. The only differences are the initial hash values and the output truncation length. +- Digests are returned only as the self-zeroizing `Digest`, never as a raw `[u8; N]`. Returning a raw array is an anti-pattern the project forbids. +- All buffers are fixed-size stack arrays. No `alloc` is used, and the crate is unconditionally `#![no_std]`. +- The message-length counter (`total_len`) is a bit-count `u64`. The SHA-256 family maps it directly onto the 64-bit length field (FIPS 180-4 §5.1.1); the SHA-512 family's 128-bit length field (§5.1.2) always keeps the upper 64 bits zero and fills only the lower 64 bits. The effective input limit is therefore $2^{64}-1$ bits (about 2 EiB) for both families, unreachable in a fixed-stack environment. + +## Security Processing Rationale + +SHA-2 may be applied to secret messages (HMAC keys, Ed25519 nonces, etc.), so execution time must not depend on input values. This crate guarantees data independence in both the compression function and the padding path. + +### 1. The Compression Function Uses Only Data-Independent Operations + +The message schedule and round function are composed solely of `rotate_right`, `wrapping_add`, `^`, `&`, and `!`. $\mathrm{Ch}$, $\mathrm{Maj}$, $\Sigma_0$, $\Sigma_1$, $\sigma_0$, and $\sigma_1$ are all compositions of rotation, shift, and logical operations, with no secret-dependent branches and no secret-dependent memory accesses (lookup tables). The round constant `K` is accessed only at fixed indices, so cache and TLB timing side channels are eliminated at the source. + +### 2. Constant-Time Buffering and Padding + +The message length is public information in SHA-2, but length comparisons are performed with constant-time operations for implementation consistency and to keep the block count predictable. + +- The chunk-length computation in `update` builds `is_ge` with `CtGreeter::gt` and `CtEqOps::eq`, then selects $\min(\text{remain}, \text{fill})$ branch-free via `usize::select`. +- The padding in `finalize` decides whether the length field fits in the current block using a `Choice` (`buffer_len > 56` for the SHA-256 family, `buffer_len > 112` for the SHA-512 family). The length-field injection is done byte by byte with `u8::select`, the second block is processed unconditionally regardless of necessity, and the final hash state is chosen with `u32::select` / `u64::select`. +- As a result, `finalize` always compresses exactly two blocks regardless of `needs_extra`. Whether the message length affects padding never leaks through the compression count. + +These select / eq / gt operations are delegated to the inline-assembly primitives of the `constant-time` crate. The restriction that builds are permitted only on x86_64 and aarch64 — which have verified constant-time implementations — is shared with the `constant-time` README. + +## Secret Zeroization (zeroize) + +Following the transaction-scoped full-erasure principle, the following are guaranteed. + +| Secret | Protection | +|-------------------------------------------------|-------------------------------------------------------| +| Hash state (`[u32; 8]` / `[u64; 8]`) | `Secret`, volatile erase on drop + `Zeroize` | +| Input buffer (`[u8; 64]` / `[u8; 128]`) | `Secret`, `zeroize` immediately after block processing | +| Message schedule `w` (`[u32; 64]` / `[u64; 80]`) | `Secret`, drop-erased at end of `process_block` | +| Working variables `a`..`h` | explicit `zeroize` at end of `process_block` | +| Round temporaries `s0`·`s1`·`ch`·`maj`·`temp1`·`temp2` | declared outside the loop, then explicit `zeroize` | +| Block copy passed to `process_block` | `Secret`, drop-erased at end of scope | +| `block1`·`block2`·`state_b1` in `finalize` | `Secret`, drop-erased at end of scope | +| `total_len` (bit counter) | `volatile_write(0)` + `compiler_fence` in `Drop` | +| Output bytes of `Digest` | `Secret<[u8; 64]>`, erased on drop + `Zeroize` | +| Unused digest words (SHA-224 `[28..32]`, SHA-384 `[48..64]`) | `zeroize` during digest assembly | + +The `Drop` of the state structs (`SHA256State`·`SHA512State`) clears the non-`Secret` field `total_len` to 0 with `volatile_write` and seals it with `compiler_fence(SeqCst)`; the `Secret` fields are volatile-erased in their own drops. + +Residual limitation: the $\sigma$ temporaries inside the message-schedule loops (`for i in 16..64` / `16..80`) and the intermediate values of `from_be_bytes` and rotations reside briefly in registers and are not explicit erasure targets. Register and spill residue (CWE-316) is a known limitation of the zeroize model and relies on the short lifetime before subsequent operations overwrite them. + +## Standards-Conformance Basis + +| Element | FIPS 180-4 Section | +|------------------------------------|----------------------------------------------------------| +| Initial hash values | §5.3.2(224) / §5.3.3(256) / §5.3.4(512) / §5.3.5(384) | +| Round constants `K` | §4.2.2(SHA-256, 64) / §4.2.3(SHA-512, 80) | +| $\mathrm{Ch}$·$\mathrm{Maj}$·$\Sigma$·$\sigma$ | §4.1.2(SHA-256) / §4.1.3(SHA-512) | +| Padding and length field | §5.1.1(512-bit block, 64-bit length) / §5.1.2(1024-bit block, 128-bit length) | +| Message schedule and compression | §6.2.2(SHA-256) / §6.4.2(SHA-512) | + +This crate ships no KAT tests of its own, but it is verified indirectly through two paths. + +- `ed25519` uses `sha2::SHA512` for signing and verification and is validated against the RFC 8032 test vectors. +- The Hash_DRBG in `rng` uses all of `SHA224`/`SHA256`/`SHA384`/`SHA512` and is validated by the NIST SP 800-90A procedures. + +`cargo fmt -p sha2 -- --check`, `cargo clippy -p sha2 --all-targets --all-features -- -D warnings`, `cargo build -p sha2 --target x86_64-unknown-none`, and `cargo build -p sha2 --target aarch64-unknown-none` all pass with no warnings. + +## Caller Contract + +Because this crate is stateless, the caller must guarantee or be aware of the following. + +1. **Single-use finalize:** `finalize` consumes `self`. Re-hashing the same input requires a new instance. +2. **Use the digest immediately:** `Digest` is erased on drop. To retain the result of `as_bytes()` on the caller side, it must be copied right away; if the digest is secret, the caller must also erase the copy. +3. **Length limit:** the input must be under $2^{64}-1$ bits (a design limit, effectively unreachable). Beyond that the bit counter wraps and the padding length becomes inconsistent. + +--- + +## Issues Found and Mitigations + +This section describes issues identified during verification and their resolutions. + +### Missing Erasure of Compression Working and Temporary Variables (Resolved) + +The hash state, buffer, and message schedule were protected with `Secret`, but the compression function's working variables `a`..`h` and round temporaries `s0`·`s1`·`ch`·`maj`·`temp1`·`temp2` — despite being values derived from the secret state — lingered on the stack after the function returned. This violates the "one request, one datum, immediate erasure" charter. + +#### Resolution + +The round temporaries were hoisted out of the loop and explicitly `zeroize()`d together with the working variables after compression. The table in the zeroize section above reflects the post-fix state. + +### Residue of Unused Digest Words in SHA-224 and SHA-384 (Resolved) + +Digest assembly always serializes eight words into the 64-byte buffer, but SHA-224 does not include the bytes after the seventh word (`[28..32]`), and SHA-384 does not include those after the sixth word (`[48..64]`). These regions are not exposed via `as_bytes()`, yet values derived from the secret state remained in memory. + +#### Resolution + +On the `is_224` / `is_384` paths, the unused word regions are `zeroize()`d before they are read, limiting their residence time in memory. + +### Remaining Work + +- Residue of register-resident intermediates such as the $\sigma$ temporaries inside the message-schedule loop (the residual limitation in the zeroize section above) +- The upper 64 bits of the SHA-512 family's 128-bit length field are unsupported (an effective $2^{64}-1$-bit limit) +- No direct KAT inside the crate (currently only indirect verification via `ed25519` and `rng`) +- aarch64 DIT and x86 DOITM hardening is a joint task with the `constant-time` crate From 23ff763c167b64ddee08defe43c16740f1115b39 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:03:10 +0900 Subject: [PATCH 16/65] =?UTF-8?q?sha2=20=EB=AA=85=EC=84=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cross-confirm.md b/cross-confirm.md index 514d076..c12e289 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -7,7 +7,7 @@ - [X] AES - [ ] BLAKE -- [X] SHA2 (명세 없음) +- [X] SHA2 - [X] SHA3 (명세 없음) - [ ] ChaCha20 - [ ] Ed448 From 08fadd4443ca02fe4432e61b880fa5afe880caf9 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:11:08 +0900 Subject: [PATCH 17/65] =?UTF-8?q?sha3=20=EB=AA=85=EC=84=B8=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EA=B0=80=20-=20README.md=20=EC=8B=A0?= =?UTF-8?q?=EA=B7=9C=20=EC=9E=91=EC=84=B1=20(=EC=88=98=ED=95=99=20?= =?UTF-8?q?=EA=B8=B0=ED=98=B8=20LaTeX=20=ED=91=9C=EA=B8=B0)=20-=20README?= =?UTF-8?q?=5FEN.md=20=EC=98=81=EB=AC=B8=ED=8C=90=20=EC=8B=A0=EA=B7=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20-=20cross-confirm.md=20SHA3=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EB=AA=85=EC=84=B8=20=EC=99=84=EB=A3=8C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- sha3/README.md | 152 ++++++++++++++++++++++++++++++++++++++++++++++ sha3/README_EN.md | 152 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 sha3/README.md create mode 100644 sha3/README_EN.md diff --git a/cross-confirm.md b/cross-confirm.md index c12e289..79519da 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -8,7 +8,7 @@ - [X] AES - [ ] BLAKE - [X] SHA2 -- [X] SHA3 (명세 없음) +- [X] SHA3 - [ ] ChaCha20 - [ ] Ed448 - [ ] Ed25519 diff --git a/sha3/README.md b/sha3/README.md new file mode 100644 index 0000000..64fedf1 --- /dev/null +++ b/sha3/README.md @@ -0,0 +1,152 @@ +# SHA-3 모듈 + +[![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) + +FIPS 202 SHA3-224/256/384/512 해시 함수와 SHAKE128/256 확장 출력 함수(XOF)를 외부 의존성 없이 `no_std` 순수 Rust로 구현한 크레이트입니다. 모든 변형은 Keccak-f[1600] 스펀지를 단일 코어로 공유합니다. 이 문서는 기능 명세와 보안 설계 근거, 표준 부합 근거, 그리고 발견한 문제와 조치를 기술합니다. + +--- + +## 구현된 기능 + +여섯 변형 모두 폭 $b = 1600$ 의 Keccak 순열 위에 스펀지 구조로 구성되며, $r + c = 1600$ (rate $r$, capacity $c$) 관계를 따릅니다. + +| 타입 | 표준 | 출력 | rate $r$ | capacity $c$ | 도메인 접미사 | +|------------|-----------------|---------------|-----------------|--------------|---------| +| `SHA3_224` | FIPS 202 §6.1 | 28바이트(224비트) | 1152비트(144바이트) | 448비트 | `0x06` | +| `SHA3_256` | FIPS 202 §6.1 | 32바이트(256비트) | 1088비트(136바이트) | 512비트 | `0x06` | +| `SHA3_384` | FIPS 202 §6.1 | 48바이트(384비트) | 832비트(104바이트) | 768비트 | `0x06` | +| `SHA3_512` | FIPS 202 §6.1 | 64바이트(512비트) | 576비트(72바이트) | 1024비트 | `0x06` | +| `SHAKE128` | FIPS 202 §6.2 | 가변(XOF) | 1344비트(168바이트) | 256비트 | `0x1f` | +| `SHAKE256` | FIPS 202 §6.2 | 가변(XOF) | 1088비트(136바이트) | 512비트 | `0x1f` | + +고정 출력 변형의 capacity 는 출력 길이의 2배($c = 2d$)이므로 충돌 저항은 $d/2$ 비트, 역상 저항은 $d$ 비트입니다. XOF 의 보안 강도는 $\min(d/2,\ c/2)$ 비트로, SHAKE128 은 최대 128비트, SHAKE256 은 최대 256비트입니다. + +공개 API 는 두 트레이트와 `Digest` 타입으로 구성됩니다. + +- `SHA3` 트레이트: `new()` -> `update(&[u8])` (임의 횟수) -> `finalize(self) -> Digest` 의 스트리밍 인터페이스. `finalize` 가 `self` 를 소비하므로 인스턴스 재사용이 타입 차원에서 차단됩니다. +- `XOF` 트레이트: `new()` -> `update(&[u8])` -> `finalize_into(self, &mut [u8])`. 출력 길이를 호출자 버퍼 크기로 지정하며, rate 를 넘는 출력은 순열 재적용으로 임의 길이까지 스퀴즈합니다. +- `Digest`: 고정 64바이트 버퍼와 길이 필드를 갖는 반환 컨테이너. `as_bytes()` 로 길이만큼의 슬라이스를 노출하며 Drop 시 자체 소거됩니다. + +설계 결정 명세는 다음과 같습니다. + +- 여섯 변형은 내부 `KeccakState` (스펀지 상태 $[u64; 25]$, 입력 버퍼, rate, 도메인 접미사) 하나만 공유합니다. 변형 간 차이는 rate, 출력 길이, 도메인 접미사 세 값뿐입니다. +- 입력 버퍼는 가장 큰 rate 인 SHAKE128 의 168바이트에 맞춘 고정 크기 `[u8; 168]` 입니다. `alloc` 을 사용하지 않으며, 크레이트는 무조건 `#![no_std]` 입니다. +- 다이제스트는 원시 `[u8; N]` 이 아니라 자체 소거되는 `Digest` 로만 반환합니다. 원시 배열 반환은 프로젝트가 금지하는 anti-pattern 입니다. +- 고정 출력 변형은 출력이 항상 단일 rate 블록 안에 들어가므로($d \le r$ 가 네 변형 모두 성립) 한 번의 스퀴즈로 완료됩니다. 다중 블록 스퀴즈 경로는 XOF 에만 필요합니다. + +## 보안 처리 근거 + +Keccak 순열은 비밀 상태(ML-DSA·ML-KEM 의 시드, Ed448 의 SHAKE256 nonce 등)에 적용되므로 실행 시간이 입력 값에 의존하지 않아야 합니다. 이 크레이트는 순열과 스펀지 버퍼링 양쪽에서 데이터 비종속을 보장합니다. + +### 1. Keccak-f[1600] 순열은 데이터 비종속 연산만 사용 + +24 라운드 순열($n_r = 12 + 2\ell$, $\ell = 6$)의 다섯 단계 매핑은 모두 회전·시프트·논리 연산의 합성이며, 비밀 값에 따른 분기나 비밀 의존 메모리 접근(룩업 테이블)이 전혀 없습니다. + +- $\theta$: 열 패리티 $C[x] = \bigoplus_{y} a[x,y]$ 를 만들고 $D[x] = C[x{-}1] \oplus \mathrm{ROT}(C[x{+}1],\ 1)$ 을 각 레인에 XOR 합니다. +- $\rho$: 각 레인을 **고정 상수** 오프셋(`RHO_OFFSETS`)만큼 회전합니다. 회전량이 데이터가 아니라 컴파일타임 상수이므로 가변-시프트 부채널이 없습니다. +- $\pi$: 레인을 **고정 인덱스**(`PI_INDICES`)로 재배치합니다. 비밀에 의존하는 주소 계산이 없습니다. +- $\chi$: 유일한 비선형 단계이며 $a'[x] = a[x] \oplus (\lnot\, a[x{+}1] \wedge a[x{+}2])$ (인덱스 mod 5)로, AES 의 S-box 같은 테이블 룩업 없이 NOT/AND/XOR 만 사용합니다. +- $\iota$: 라운드 상수 $RC_i$ 를 레인 0 에 XOR 합니다. 상수는 고정 인덱스로만 접근합니다. + +즉, Keccak 은 곱셈·나눗셈·조건 분기·비밀 의존 룩업·가변 시프트를 일절 쓰지 않는, **태생적으로 상수-시간** 인 순열입니다. 라운드 상수와 회전 오프셋이 메시지와 무관한 고정 테이블이므로 캐시 및 TLB 타이밍 부채널이 원천 차단됩니다. + +### 2. 스펀지 버퍼링의 상수-시간 처리 + +메시지 길이는 SHA-3 의 공개 정보이지만, 구현 일관성(sha2 크레이트와 동일 패턴)과 블록 수의 예측 가능성을 위해 길이 비교를 상수-시간 연산으로 수행합니다. + +- `update` 의 청크 길이 계산은 `CtGreeter::gt` 와 `CtEqOps::eq` 로 `is_ge` 를 만든 뒤 `usize::select` 로 $\min(\text{remain},\ \text{fill})$ 을 분기 없이 선택합니다. +- XOF 의 `squeeze_into` 도 워드별 추출량을 `usize::select` 로 $\min(\text{remain},\ 8)$ 으로 분기 없이 결정합니다. +- 바이트 정렬 입력에서 pad10\*1 패딩은 항상 정확히 한 번의 추가 스펀지 호출에 들어가므로, 추가 블록 분기가 비밀에 의존하지 않습니다. + +이 select·eq·gt 연산은 `constant-time` 크레이트의 인-라인 어셈블리 프리미티브에 위임됩니다. 검증된 상수-시간 구현이 있는 x86_64·aarch64 에서만 빌드가 허용되는 점은 `constant-time` README 와 공유합니다. + +## 비밀 소거 (zeroize) + +트랜잭션 단위 전부 소거 원칙에 따라 다음이 보장됩니다. + +| 비밀 | 보호 방식 | +|---------------------------------------------|--------------------------------------------------| +| 스펀지 상태(`[u64; 25]`) | `Secret`, Drop 시 volatile 소거 + `Zeroize` | +| 입력 버퍼(`[u8; 168]`) | `Secret`, Drop 시 volatile 소거 + `Zeroize` | +| 순열 중간 상태 `tmp`·`c`·`d` | `keccak_f1600` 종료 시 명시적 `zeroize` | +| 흡수 메시지 워드 `word` | `absorb_block` 종료 시 명시적 `zeroize` | +| 패딩 블록 `block` | `Secret`, `pad` 스코프 종료 시 Drop 소거 | +| 스퀴즈 워드 사본 `word_bytes` | 고정·XOF 스퀴즈 종료 시 명시적 `zeroize` | +| `finalize_fixed` 의 다이제스트 스택 사본 `bytes` | `volatile_write(0)` + `atomic_compiler_fence` + `memory_barrier` | +| `Digest` 의 출력 바이트 | `Secret<[u8; 64]>`, Drop 시 소거 + `Zeroize` | + +`KeccakState` 의 `Zeroize` 는 `Secret` 필드인 스펀지 상태와 입력 버퍼를 volatile 소거하며, `self` 가 Drop 될 때 자동 호출됩니다. `pad` 는 별도 스택 블록을 `Secret` 으로 감싸 도메인 접미사·패딩 비트가 메모리에 잔존하지 않게 합니다. + +잔여 한계: 순열 한 라운드 안에서 각 레인 값이 일시적으로 상주하는 레지스터·스택 스필은 명시적 소거 대상이 아닙니다. 소거하는 것은 `tmp`·`c`·`d` 같은 스택 배열이며, 후속 라운드가 즉시 덮어쓰는 레지스터 잔존(CWE-316)은 zeroize 모델의 알려진 한계입니다. + +## 표준 부합 근거 + +| 요소 | FIPS 202 절 | +|------------------------------------------|--------------------------| +| Keccak-f[1600] 24 라운드 순열 | §3.4 | +| $\theta$·$\rho$·$\pi$·$\chi$·$\iota$ 단계 매핑 | §3.2.1 ~ §3.2.5 | +| $\rho$ 회전 오프셋 테이블 | §3.2.2 (Table 2) | +| 라운드 상수 $RC_i$ (24개) | §3.2.5 (Algorithm 5) | +| 스펀지 구조 | §4 | +| pad10\*1 다중 속도 패딩 | §5.1 | +| `KECCAK[c]` 정의 | §5.2 | +| SHA3-224/256/384/512 (접미사 `01`) | §6.1 | +| SHAKE128/256 (접미사 `1111`) | §6.2 | +| 도메인 접미사 바이트 인코딩(`0x06`·`0x1f`) | §B.1 (비트/바이트 변환) | + +도메인 접미사 바이트는 FIPS 202 의 도메인 분리 비트열과 pad10\*1 의 첫 `1` 비트를 little-endian 비트 순서로 결합한 값입니다. SHA-3 는 `01` 접미사에 패딩 시작 비트가 더해져 `0x06`, SHAKE 는 `1111` 접미사에 더해져 `0x1f` 이며, 두 패턴은 `0x80`(패딩 끝 비트)과 비트가 겹치지 않습니다($\texttt{0x06}\ |\ \texttt{0x1f}$ 모두 상위 비트 0). + +KAT 검증은 다음 벡터로 수행되며 전수 통과합니다. + +- **FIPS 202 공식 빈 메시지 벡터:** 여섯 변형 전부의 도메인 접미사·패딩을 검증. +- **FIPS 202 1600비트 메시지(`0xA3` × 200) 벡터:** 다중 블록 흡수 경로를 검증. +- **rate 경계 패딩:** 각 고정 출력 변형에 대해 $L = r{-}1$(도메인 접미사와 `0x80` 병합), $L = r$(전체 패딩 블록), $L = r{+}1$ 세 경계를 검증. +- **XOF 다중 블록 스퀴즈:** SHAKE128/256 의 rate 초과 출력(168바이트 초과) 재순열 경로와 부분 워드 출력, 접두사 일관성($\text{out}[..k]$ 가 더 긴 출력의 접두사와 일치)을 검증. +- **증분 입력 동치성:** 1·7·64·135·136·137·299 바이트 청크 분할이 일괄 입력과 동일한 결과를 내는지(버퍼 경계 횡단 포함) 검증. +- **Drop 후 소거:** `KeccakState` 의 상태·버퍼와 `Digest` 바이트가 Drop 후 0 으로 소거되는지 검증. + +`cargo fmt -p sha3 -- --check`, `cargo clippy -p sha3 --all-targets --all-features -- -D warnings`, `cargo build -p sha3 --target x86_64-unknown-none`, `cargo build -p sha3 --target aarch64-unknown-none` 모두 무경고 통과합니다. + +## 호출자 계약 + +이 크레이트는 stateless 이므로 다음은 호출자가 보장하거나 인지해야 합니다. + +1. **1회용 finalize:** `finalize` 와 `finalize_into` 는 `self` 를 소비합니다. 같은 입력에 대한 재해시는 새 인스턴스로 수행해야 합니다. +2. **다이제스트 즉시 사용:** `Digest` 는 Drop 시 소거됩니다. `as_bytes()` 결과를 호출자 측에 보존하려면 그 즉시 복사해야 하며, 비밀 다이제스트라면 복사본도 호출자가 소거해야 합니다. +3. **XOF 출력 버퍼:** `finalize_into` 는 호출자가 넘긴 슬라이스 길이만큼 출력합니다. 보안에 충분한 출력 길이(SHAKE128 $\ge 32$바이트, SHAKE256 $\ge 64$바이트 권장)는 호출자가 보장해야 합니다. + +--- + +## 발견한 문제와 조치 + +검증 과정에서 발견한 문제와 해결을 기술합니다(커밋 `e073d17`). + +### Keccak-f 순열 중간 상태 소거 누락 (해결) + +순열의 열 패리티 배열 `c`·`d` 와 $\rho$/$\pi$ 임시 배열 `tmp` 는 비밀 상태에서 파생된 값임에도, 라운드 루프 안에서 매번 새로 선언되어 함수 종료 후 스택에 잔존했습니다. "한 요청·한 데이터·즉시 소거" 헌장 위반입니다. + +#### 해결 + +`c`·`d`·`tmp` 를 라운드 루프 밖으로 끌어올려 24 라운드가 동일 버퍼를 재사용하게 하고, 순열 종료 직후 세 배열을 모두 명시적으로 `zeroize()` 하도록 변경했습니다. 위 zeroize 절의 표가 조치 후 상태입니다. + +### absorb_block 자기 차용과 스택 잔존 (해결) + +흡수 함수가 `&mut self` 를 받던 구조에서는 `update` 가 입력 버퍼(`self.buffer`)와 상태(`self.state`)를 동시에 차용할 수 없어 버퍼 사본을 떠야 했고, 그 사본이 소거되지 않은 채 남았습니다. 또한 흡수 루프가 메시지 워드를 담는 `word` 지역값을 소거하지 않았습니다. + +#### 해결 + +`absorb_block` 의 시그니처를 `state: &mut [u64; 25]` 와 `block: &[u8]` 두 개의 분리된 차용으로 바꿔, `update` 가 `&mut self.state` 와 `&self.buffer[..rate]` 를 서로 다른 필드 차용으로 직접 넘기게 했습니다. 버퍼 사본이 사라졌고, 마지막 메시지 워드 `word` 도 흡수 직후 `zeroize()` 합니다. + +### finalize 다이제스트 스택 사본 잔존 (해결) + +`finalize_fixed` 는 스퀴즈 결과를 스택 배열 `bytes: [u8; 64]` 에 쓴 뒤 `Secret::new(bytes)` 로 `Digest` 를 만드는데, `[u8; 64]: Copy` 이므로 `Secret::new` 가 값을 **복사** 하여 스택 원본이 비밀 다이제스트인 채로 남았습니다. + +#### 해결 + +`Secret` 생성 직후 스택 원본 `bytes` 의 각 바이트를 `volatile_write(0)` 으로 덮고 `atomic_compiler_fence` + `memory_barrier` 로 봉인하여, 최적화기가 소거를 제거하지 못하게 했습니다. 동일 패턴을 sha2 크레이트의 다이제스트 빌드와 공유합니다. + +### TODO + +- 순열 한 라운드 내부 레인 값의 레지스터·스필 잔존(위 zeroize 절의 잔여 한계) +- aarch64 DIT·x86 DOITM 하드닝은 `constant-time` 크레이트와 공동 과제 +- 임의 비트 길이(바이트 비정렬) 입력 미지원 - 현재 구현은 바이트 정렬 메시지만 처리 diff --git a/sha3/README_EN.md b/sha3/README_EN.md new file mode 100644 index 0000000..6822048 --- /dev/null +++ b/sha3/README_EN.md @@ -0,0 +1,152 @@ +# SHA-3 Module + +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) + +A crate implementing the FIPS 202 SHA3-224 / SHA3-256 / SHA3-384 / SHA3-512 hash functions and the SHAKE128 / SHAKE256 extendable-output functions (XOFs) in pure `no_std` Rust with no external dependencies. All variants share a single Keccak-f[1600] sponge as their core. This document describes the functional specification, the security design rationale, the standards-conformance basis, and the issues found with their mitigations. + +--- + +## Implemented Features + +All six variants are built as a sponge construction over the width-$b = 1600$ Keccak permutation, following the relation $r + c = 1600$ (rate $r$, capacity $c$). + +| Type | Standard | Output | rate $r$ | capacity $c$ | Domain Suffix | +|------------|-----------------|-------------------|---------------------|--------------|---------------| +| `SHA3_224` | FIPS 202 §6.1 | 28 bytes (224 bit) | 1152 bit (144 byte) | 448 bit | `0x06` | +| `SHA3_256` | FIPS 202 §6.1 | 32 bytes (256 bit) | 1088 bit (136 byte) | 512 bit | `0x06` | +| `SHA3_384` | FIPS 202 §6.1 | 48 bytes (384 bit) | 832 bit (104 byte) | 768 bit | `0x06` | +| `SHA3_512` | FIPS 202 §6.1 | 64 bytes (512 bit) | 576 bit (72 byte) | 1024 bit | `0x06` | +| `SHAKE128` | FIPS 202 §6.2 | variable (XOF) | 1344 bit (168 byte) | 256 bit | `0x1f` | +| `SHAKE256` | FIPS 202 §6.2 | variable (XOF) | 1088 bit (136 byte) | 512 bit | `0x1f` | + +The capacity of the fixed-output variants is twice the output length ($c = 2d$), so collision resistance is $d/2$ bits and preimage resistance is $d$ bits. The security strength of the XOFs is $\min(d/2,\ c/2)$ bits, up to 128 bits for SHAKE128 and up to 256 bits for SHAKE256. + +The public API consists of two traits and the `Digest` type. + +- `SHA3` trait: a streaming interface of `new()` -> `update(&[u8])` (any number of times) -> `finalize(self) -> Digest`. `finalize` consumes `self`, so instance reuse is blocked at the type level. +- `XOF` trait: `new()` -> `update(&[u8])` -> `finalize_into(self, &mut [u8])`. The output length is given by the caller's buffer size, and output beyond the rate is squeezed to arbitrary length by re-applying the permutation. +- `Digest`: a return container holding a fixed 64-byte buffer and a length field. It exposes a slice of the relevant length via `as_bytes()` and zeroizes itself on drop. + +Design decisions are as follows. + +- The six variants share a single internal `KeccakState` (the sponge state $[u64; 25]$, the input buffer, the rate, and the domain suffix). The only differences among the variants are three values: rate, output length, and domain suffix. +- The input buffer is a fixed-size `[u8; 168]` sized to the largest rate, SHAKE128's 168 bytes. No `alloc` is used, and the crate is unconditionally `#![no_std]`. +- Digests are returned only as the self-zeroizing `Digest`, never as a raw `[u8; N]`. Returning a raw array is an anti-pattern the project forbids. +- The fixed-output variants always fit their output within a single rate block ($d \le r$ holds for all four), so they complete in a single squeeze. The multi-block squeeze path is needed only for the XOFs. + +## Security Processing Rationale + +The Keccak permutation is applied to secret state (ML-DSA / ML-KEM seeds, the SHAKE256 nonce of Ed448, etc.), so execution time must not depend on input values. This crate guarantees data independence in both the permutation and the sponge buffering. + +### 1. The Keccak-f[1600] Permutation Uses Only Data-Independent Operations + +The five step mappings of the 24-round permutation ($n_r = 12 + 2\ell$, $\ell = 6$) are all compositions of rotation, shift, and logical operations, with no secret-dependent branches and no secret-dependent memory accesses (lookup tables). + +- $\theta$: builds the column parity $C[x] = \bigoplus_{y} a[x,y]$ and XORs $D[x] = C[x{-}1] \oplus \mathrm{ROT}(C[x{+}1],\ 1)$ into each lane. +- $\rho$: rotates each lane by a **fixed constant** offset (`RHO_OFFSETS`). Because the rotation amount is a compile-time constant rather than data, there is no variable-shift side channel. +- $\pi$: relocates lanes by **fixed indices** (`PI_INDICES`). There is no secret-dependent address computation. +- $\chi$: the only nonlinear step, $a'[x] = a[x] \oplus (\lnot\, a[x{+}1] \wedge a[x{+}2])$ (indices mod 5), which uses only NOT/AND/XOR without any table lookup like the AES S-box. +- $\iota$: XORs the round constant $RC_i$ into lane 0. The constants are accessed only at fixed indices. + +In other words, Keccak is an **inherently constant-time** permutation that uses no multiplication, division, conditional branch, secret-dependent lookup, or variable shift whatsoever. Because the round constants and rotation offsets are fixed tables independent of the message, cache and TLB timing side channels are eliminated at the source. + +### 2. Constant-Time Sponge Buffering + +The message length is public information in SHA-3, but length comparisons are performed with constant-time operations for implementation consistency (the same pattern as the sha2 crate) and to keep the block count predictable. + +- The chunk-length computation in `update` builds `is_ge` with `CtGreeter::gt` and `CtEqOps::eq`, then selects $\min(\text{remain},\ \text{fill})$ branch-free via `usize::select`. +- The XOF `squeeze_into` likewise decides the per-word extraction amount branch-free as $\min(\text{remain},\ 8)$ via `usize::select`. +- For byte-aligned input, the pad10\*1 padding always fits in exactly one additional sponge call, so the extra-block branch does not depend on the secret. + +These select / eq / gt operations are delegated to the inline-assembly primitives of the `constant-time` crate. The restriction that builds are permitted only on x86_64 and aarch64 — which have verified constant-time implementations — is shared with the `constant-time` README. + +## Secret Zeroization (zeroize) + +Following the transaction-scoped full-erasure principle, the following are guaranteed. + +| Secret | Protection | +|-------------------------------------------------|-------------------------------------------------------| +| Sponge state (`[u64; 25]`) | `Secret`, volatile erase on drop + `Zeroize` | +| Input buffer (`[u8; 168]`) | `Secret`, volatile erase on drop + `Zeroize` | +| Permutation intermediates `tmp`·`c`·`d` | explicit `zeroize` at end of `keccak_f1600` | +| Absorbed message word `word` | explicit `zeroize` at end of `absorb_block` | +| Padding block `block` | `Secret`, drop-erased at end of `pad` scope | +| Squeeze word copy `word_bytes` | explicit `zeroize` at end of the fixed / XOF squeeze | +| Digest stack copy `bytes` in `finalize_fixed` | `volatile_write(0)` + `atomic_compiler_fence` + `memory_barrier` | +| Output bytes of `Digest` | `Secret<[u8; 64]>`, erased on drop + `Zeroize` | + +The `Zeroize` of `KeccakState` volatile-erases its `Secret` fields — the sponge state and the input buffer — and is invoked automatically when `self` is dropped. `pad` wraps a separate stack block in `Secret` so that the domain suffix and padding bits do not linger in memory. + +Residual limitation: the registers and stack spills where each lane value transiently resides within a single permutation round are not explicit erasure targets. What is erased are the stack arrays such as `tmp`·`c`·`d`; the register residue (CWE-316) that the next round immediately overwrites is a known limitation of the zeroize model. + +## Standards-Conformance Basis + +| Element | FIPS 202 Section | +|------------------------------------------|---------------------------| +| Keccak-f[1600] 24-round permutation | §3.4 | +| $\theta$·$\rho$·$\pi$·$\chi$·$\iota$ step mappings | §3.2.1 ~ §3.2.5 | +| $\rho$ rotation-offset table | §3.2.2 (Table 2) | +| Round constants $RC_i$ (24 of them) | §3.2.5 (Algorithm 5) | +| Sponge construction | §4 | +| pad10\*1 multi-rate padding | §5.1 | +| `KECCAK[c]` definition | §5.2 | +| SHA3-224/256/384/512 (suffix `01`) | §6.1 | +| SHAKE128/256 (suffix `1111`) | §6.2 | +| Domain-suffix byte encoding (`0x06`·`0x1f`) | §B.1 (bit/byte conversion) | + +The domain-suffix byte is the value obtained by combining the FIPS 202 domain-separation bit string with the first `1` bit of pad10\*1 in little-endian bit order. SHA-3 adds the padding start bit to the `01` suffix to get `0x06`, and SHAKE adds it to the `1111` suffix to get `0x1f`; neither pattern overlaps in bits with `0x80` (the end-of-padding bit) — the high bit of both $\texttt{0x06}\ |\ \texttt{0x1f}$ is 0. + +KAT verification is performed with the following vectors, all of which pass. + +- **FIPS 202 official empty-message vectors:** verify the domain suffix and padding of all six variants. +- **FIPS 202 1600-bit message (`0xA3` × 200) vectors:** verify the multi-block absorption path. +- **Rate-boundary padding:** for each fixed-output variant, verify the three boundaries $L = r{-}1$ (domain suffix merged with `0x80`), $L = r$ (a full padding block), and $L = r{+}1$. +- **XOF multi-block squeeze:** verify the re-permutation path for SHAKE128/256 output beyond the rate (over 168 bytes), partial-word output, and prefix consistency ($\text{out}[..k]$ matching the prefix of a longer output). +- **Incremental-input equivalence:** verify that splitting into 1·7·64·135·136·137·299-byte chunks produces the same result as a single batched input (including crossing the buffer boundary). +- **Erasure on drop:** verify that the state and buffer of `KeccakState` and the `Digest` bytes are erased to 0 after drop. + +`cargo fmt -p sha3 -- --check`, `cargo clippy -p sha3 --all-targets --all-features -- -D warnings`, `cargo build -p sha3 --target x86_64-unknown-none`, and `cargo build -p sha3 --target aarch64-unknown-none` all pass with no warnings. + +## Caller Contract + +Because this crate is stateless, the caller must guarantee or be aware of the following. + +1. **Single-use finalize:** `finalize` and `finalize_into` consume `self`. Re-hashing the same input requires a new instance. +2. **Use the digest immediately:** `Digest` is erased on drop. To retain the result of `as_bytes()` on the caller side, it must be copied right away; if the digest is secret, the caller must also erase the copy. +3. **XOF output buffer:** `finalize_into` outputs exactly as many bytes as the length of the slice the caller passes. An output length sufficient for security (recommended $\ge 32$ bytes for SHAKE128 and $\ge 64$ bytes for SHAKE256) is the caller's responsibility. + +--- + +## Issues Found and Mitigations + +This section describes issues identified during verification and their resolutions (commit `e073d17`). + +### Missing Erasure of Keccak-f Permutation Intermediates (Resolved) + +The column-parity arrays `c`·`d` and the $\rho$/$\pi$ temporary array `tmp` of the permutation — despite being values derived from the secret state — were declared anew on each pass inside the round loop and lingered on the stack after the function returned. This violates the "one request, one datum, immediate erasure" charter. + +#### Resolution + +`c`·`d`·`tmp` were hoisted out of the round loop so that the 24 rounds reuse the same buffers, and all three arrays are explicitly `zeroize()`d immediately after the permutation ends. The table in the zeroize section above reflects the post-fix state. + +### absorb_block Self-Borrow and Stack Residue (Resolved) + +In the structure where the absorption function took `&mut self`, `update` could not borrow the input buffer (`self.buffer`) and the state (`self.state`) at the same time, so it had to make a copy of the buffer, and that copy was left behind unerased. The absorption loop also did not erase the `word` local that held the message word. + +#### Resolution + +The signature of `absorb_block` was changed to two disjoint borrows, `state: &mut [u64; 25]` and `block: &[u8]`, so that `update` passes `&mut self.state` and `&self.buffer[..rate]` directly as borrows of different fields. The buffer copy is gone, and the last message word `word` is also `zeroize()`d right after absorption. + +### Residue of the finalize Digest Stack Copy (Resolved) + +`finalize_fixed` writes the squeeze result into the stack array `bytes: [u8; 64]` and then builds the `Digest` with `Secret::new(bytes)`; because `[u8; 64]: Copy`, `Secret::new` **copies** the value, leaving the stack original holding the secret digest. + +#### Resolution + +Immediately after constructing the `Secret`, each byte of the stack original `bytes` is overwritten with `volatile_write(0)` and sealed with `atomic_compiler_fence` + `memory_barrier`, so the optimizer cannot remove the erasure. This pattern is shared with the digest assembly of the sha2 crate. + +### Remaining Work + +- Register and spill residue of lane values inside a single permutation round (the residual limitation in the zeroize section above) +- aarch64 DIT and x86 DOITM hardening is a joint task with the `constant-time` crate +- Arbitrary bit-length (non-byte-aligned) input is unsupported — the current implementation handles only byte-aligned messages From 1e1cd2ded5c7cc5efcbfa28aea0f1396535217cd Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:31:49 +0900 Subject: [PATCH 18/65] =?UTF-8?q?zeroize=20=EB=AA=85=EC=84=B8=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95=20-=20README=5FEN=20=EC=96=B4=EC=85=88=EB=B8=94?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A4=80=20=EA=B2=80=EC=A6=9D=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=20(llvm-objdump=20probe=20DSE=20?= =?UTF-8?q?=EB=B0=98=EB=A1=80=20=ED=98=95=EC=8B=9D=20=EB=AA=A8=EB=8D=B8)?= =?UTF-8?q?=20-=20README=20=ED=95=9C=EA=B5=AD=EC=96=B4=ED=8C=90=EC=97=90?= =?UTF-8?q?=20=EB=8F=99=EC=9D=BC=20=EB=82=B4=EC=9A=A9=20=EB=B2=88=EC=97=AD?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=20-=20cross-confirm=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95=20=ED=95=84=EC=9A=94=20=ED=91=9C=EA=B8=B0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- zeroize/README.md | 128 +++++++++++++++++++++++++++++++++++++++++++ zeroize/README_EN.md | 128 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 1 deletion(-) diff --git a/cross-confirm.md b/cross-confirm.md index 79519da..313bacc 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -18,7 +18,7 @@ - [ ] ML-KEM - [X] RNG - [X] Constant-Time -- [X] Zeroize (명세 보강 필요) +- [X] Zeroize 여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "담당 명세에 따라 정확하게 구현되어 있으며, 정상적으로 동작한다"는 것을 명시하는 것입니다. diff --git a/zeroize/README.md b/zeroize/README.md index f68b58a..b5e7d17 100644 --- a/zeroize/README.md +++ b/zeroize/README.md @@ -76,6 +76,134 @@ src/barrier/{x86_64, aarch64, fallback}.rs 6. black_box(ptr) -> 포인터를 레지스터에 로드하여 최적화 최종 차단 ``` +## 형식 모델 + +버퍼가 바이트 $p, p{+}1, \dots, p{+}n{-}1$을 차지한다고 합시다. 소거는 다음 store들의 집합입니다. + +$$ W = \bigl\{\, \mathrm{mem}[p{+}i] \leftarrow 0 \;\bigm|\; 0 \le i < n \,\bigr\}. $$ + +**Dead Store Elimination.** 주소 $a$에 쓰는 store $s \in W$는, $a$가 덮어써지거나 그 저장 공간이 해제되기 전에 $a$에 대한 어떤 load도 $s$로부터 도달 불가능할 때 *죽은(dead)* store입니다. + +$$ \mathrm{dead}(s) \iff \nexists\, \ell \in \mathrm{Loads}(a) \ \text{such that}\ s \prec \ell . $$ + +올바른 최적화기는 죽은 store를 삭제해도 프로그램의 관측 가능 동작 $\mathcal{O}$가 바뀌지 않으므로, 어떤 죽은 store든 삭제할 수 있습니다. 소거 이후 더 이상 사용되지 않는 비밀의 경우 $W$의 *모든* $s$가 죽은 store이므로, $W$는 DSE가 제거하도록 허용된 집합과 정확히 일치합니다. 이것이 암호 코드에서 최악의 경우입니다. 소거가 "쓸모없어" 보일수록 제거 대상이 되기 더 쉽습니다. + +**Volatile가 DSE를 무력화.** volatile 접근은 정의상 $\mathcal{O}$의 원소입니다. 바이트 $i$의 volatile store를 $w^{\mathrm{vol}}_i$로 쓰면, + +$$ w^{\mathrm{vol}}_i \in \mathcal{O} \quad\Longrightarrow\quad \forall\,\text{sound } \mathcal{T}:\; w^{\mathrm{vol}}_i \in \mathcal{T}(\mathcal{O}) . $$ + +죽은 store라는 전제가 무의미해집니다. 관측 가능한 부수 효과는 그 결과가 다시 읽히든 말든 보존됩니다. 이로써 "제거될 수 있음"이 "반드시 방출됨"으로 바뀝니다. + +**순서.** 배리어들은 소거 연산에 전순서(total order)를 부여합니다. + +$$ \mathrm{cb}_1 \;\prec\; w^{\mathrm{vol}}_0 \prec \dots \prec w^{\mathrm{vol}}_{n-1} \;\prec\; \mathrm{cb}_2 \;\prec\; \mathrm{acf} \;\prec\; \mathrm{mb} \;\prec\; \mathrm{bb}, $$ + +여기서 $\mathrm{cb}$는 컴파일러 배리어, $\mathrm{acf}$는 원자적 컴파일러 펜스, $\mathrm{mb}$는 CPU 메모리 배리어, $\mathrm{bb}$는 `black_box` 레지스터 고정입니다. $\mathrm{cb}_1$은 소거 이전 연산을 store 아래로 내리는 것을 막고, $\mathrm{cb}_2$와 $\mathrm{acf}$는 소거 이후 연산을 store 위로 올리는 것을 막으며, $\mathrm{mb}$는 확정된 store가 하드웨어에서 전역 가시화되도록 강제합니다. + +## 어셈블리 수준 검증 (llvm-objdump) + +volatile 의미론은 *언어 차원의* 계약입니다. 이 계약이 release 파이프라인 전체(`opt-level = "z"`, `lto = true`, `panic = "abort"`)를 거쳐 기계어까지 살아남는지 확인하기 위해, 산출된 바이너리를 디스어셈블하여 정적으로 검증합니다. 이는 constant-time 크레이트를 지키는 것과 동일한 하니스를 재사용합니다. `constant-time/scripts/check_ct_asm.sh`가 둘 다 구동하며, 그 `PROBES_ZEROIZE` 그룹이 이 크레이트를 담당합니다. + +`zeroize/examples/zeroize_asm_probes.rs`의 probe 두 개는 각각 `#[unsafe(no_mangle)] extern "C"` + `#[inline(never)]`로 독립 심볼로 방출됩니다. + +- `probe_zeroize_flat`은 64바이트 버퍼에 `zeroize_flat`을 실행합니다. +- `probe_secret_drop`은 `Secret<[u8; 32]>`를 생성하고 drop시킵니다. + +constant-time probe와 달리, 소거 경로는 조건 분기 부재를 검사하지 **않습니다**. 루프 한계가 컴파일타임 상수 `size_of`이므로 루프 분기는 비밀에 의존하지 않고, 분기 검사는 오탐이 됩니다. 대신 각 zeroize probe는 두 술어를 동시에 만족해야 합니다. + +$$ \#\{\text{volatile zero store}\} \ge 1 \quad\wedge\quad \#\{\text{memory fence}\} \ge 1 . $$ + +둘 중 하나라도 0이 되면 dead store 회귀(CWE-14 / CWE-316)가 끼어든 것이며 검사는 실패합니다. aarch64에서 영점화 store는 `strb wzr`, fence는 `dsb sy`이고, x86_64에서는 `mov $0` 계열 store와 `mfence`입니다. + +호스트(`aarch64-apple-darwin`)에서 `probe_zeroize_flat`을 디스어셈블하면 `0x40 = 64`바이트에 대한 바이트 루프가 살아남은 fence와 `black_box` 꼬리 호출로 끝나는 것을 볼 수 있습니다(라벨 축약, 주석 추가). + +```text +<_probe_zeroize_flat>: + mov x8, #0x0 + cmp x8, #0x40 ; i == 64 ? (카운터 한계, 비밀 아님) + b.eq <+0x18> ; 루프 종료 + strb wzr, [x0, x8] ; volatile store: mem[p+i] <- 0 (DCE 생존) + add x8, x8, #0x1 + b <+0x4> + dsb sy ; memory_barrier(): 하드웨어에서 store 확정 + b +``` + +`probe_secret_drop`은 32바이트 비밀을 스택에 구성하고(`dup.16b` + `stp q0, q0`), 같은 volatile 루프로 소거한 뒤 fence와 `black_box` 고정을 방출합니다. + +```text +<_probe_secret_drop>: + ... + dup.16b v0, w0 ; secret = [seed; 32] + stp q0, q0, [sp] + ... + cmp x8, #0x20 ; i == 32 ? + b.eq <+0x38> + strb wzr, [x9, x8] ; volatile 바이트 소거 + add x8, x8, #0x1 + b <+0x24> + dsb sy ; memory_barrier() + mov x0, sp + bl + ... + ret +``` + +전체 실행은 두 probe 모두 store와 fence를 보존함을 보고합니다. + +```text +>> zeroize probe (별도 크레이트 — zero store + fence 잔존 검증) + PASS probe_secret_drop — zero store 1 건 + fence 1 건 잔존 + PASS probe_zeroize_flat — zero store 1 건 + fence 1 건 잔존 +RESULT: PASS — 모든 probe 가 조건분기 부재 + 영점화 보존 +``` + +단일 심볼을 직접 확인하려면 다음과 같이 디스어셈블합니다. + +```bash +llvm-objdump -d --no-show-raw-insn target/<호스트-트리플>/release/examples/zeroize_asm_probes +``` + +macOS의 경우 이 바이너리는 `/Library/Developer/CommandLineTools/usr/bin/llvm-objdump`에 있습니다. + +### 반례: DSE의 실제 동작 + +DSE는 *허용된* 변환이며, 최적화기가 이를 실제로 수행할지는 opt-level, 컴파일러 버전, 주변 코드에 달려 있습니다. 그 예측 불가능성이 바로 위험입니다. 아래 대조가 그 자유를 구체적으로 보여줍니다. 두 함수는 체크섬을 읽은 직후 죽는 32바이트 스택 비밀을 소거하는데, 하나는 단순 store, 다른 하나는 `ptr::write_volatile`을 씁니다. `opt-level = 3`으로 빌드한 결과입니다. + +```text +<_naive_wipe_dead>: ; 단순 `*b = 0` + ubfiz w0, w0, #5, #3 ; 체크섬 반환 (32*seed mod 256) + ret ; 버퍼 + 32개 store 전부 제거 (store 0개) + +<_volatile_wipe_dead>: ; ptr::write_volatile(.., 0) + sub sp, sp, #0x20 + strb wzr, [sp, #0x1f] ; 32개 volatile store, 전부 보존 + strb wzr, [sp, #0x1e] + ... ; (총 32개) + strb wzr, [sp, #0x1] + ubfiz w0, w0, #5, #3 + strb wzr, [sp], #0x20 + ret +``` + +단순 버전은 두 명령으로 줄어듭니다. 최적화기가 버퍼를 죽었다고 증명하고 32개 store를 전부 삭제했으므로, 비밀의 바이트는 스택에 그대로 남게 됩니다. volatile 버전은 모든 store를 유지합니다. 이 동일한 단순 루프가 `opt-level = "z"`에서는 우연히 살아남지만 `opt-level = 3`에서는 사라진다는 점에 주목하십시오. 비-volatile 소거의 생존은 어떤 최적화 수준에서도 보장되지 않습니다. 따라서 이 크레이트는 최적화기의 재량에 결코 기대지 않습니다. volatile과 배리어가 프로젝트 자신의 `opt-level = "z"`를 포함한 모든 opt-level에서 소거가 살아남도록 강제합니다. + +### 런타임 readback 검증 + +정적 검사는 store가 *방출됨*을 증명하고, `zeroize/tests/zeroize_readback.rs`는 store가 *효력을 가짐*을 증명합니다. 각 테스트는 0이 아닌 패턴을 쓰고, raw 주소를 확보하고, 소거(`Secret::drop`, `zeroize_flat`, `[T; N]::zeroize`, `&mut [T]::zeroize`)를 유발한 뒤, 같은 주소를 되읽어 모든 바이트가 0인지 단언합니다. + +```text +running 5 tests +test secret_array_zeroized_on_drop ... ok +test secret_into_inner_extracts_value ... ok +test zeroize_flat_wipes_bytes ... ok +test array_zeroize_wipes ... ok +test slice_zeroize_wipes ... ok +test result: ok. 5 passed +``` + +두 계층이 함께 고리를 닫습니다. readback은 *실행 시* 소거가 올바름을 확인하고, llvm-objdump 검사는 최적화기가 출하 바이너리에서 소거를 조용히 제거할 수 없음을 확인합니다. + --- ## API diff --git a/zeroize/README_EN.md b/zeroize/README_EN.md index 45229d1..c8e2891 100644 --- a/zeroize/README_EN.md +++ b/zeroize/README_EN.md @@ -76,6 +76,134 @@ Modern CPUs can execute instructions Out-of-Order independently of the compiler 6. black_box(ptr) — Final prevention of optimization by loading the pointer into a register ``` +## Formal Model + +Let a buffer occupy the bytes $p, p{+}1, \dots, p{+}n{-}1$. A wipe is the family of stores + +$$ W = \bigl\{\, \mathrm{mem}[p{+}i] \leftarrow 0 \;\bigm|\; 0 \le i < n \,\bigr\}. $$ + +**Dead Store Elimination.** A store $s \in W$ to address $a$ is *dead* when no load of $a$ is reachable from $s$ before $a$ is overwritten or its storage is released: + +$$ \mathrm{dead}(s) \iff \nexists\, \ell \in \mathrm{Loads}(a) \ \text{such that}\ s \prec \ell . $$ + +A correct optimizer may delete any dead store, since deleting it leaves the program's observable behavior $\mathcal{O}$ unchanged. For a secret that is no longer used after the wipe, *every* $s \in W$ is dead, so $W$ is exactly the set DSE is permitted to remove. This is the worst case for cryptographic code: the more "useless" the wipe looks, the more eligible it is for elimination. + +**Volatile defeats DSE.** A volatile access is, by definition, an element of $\mathcal{O}$. Writing $w^{\mathrm{vol}}_i$ for the volatile store of byte $i$, + +$$ w^{\mathrm{vol}}_i \in \mathcal{O} \quad\Longrightarrow\quad \forall\,\text{sound } \mathcal{T}:\; w^{\mathrm{vol}}_i \in \mathcal{T}(\mathcal{O}) . $$ + +The deadness premise becomes irrelevant: an observable side effect is preserved whether or not its result is ever read. This converts "may be removed" into "must be emitted." + +**Ordering.** The barriers impose a total order on the wipe's operations, + +$$ \mathrm{cb}_1 \;\prec\; w^{\mathrm{vol}}_0 \prec \dots \prec w^{\mathrm{vol}}_{n-1} \;\prec\; \mathrm{cb}_2 \;\prec\; \mathrm{acf} \;\prec\; \mathrm{mb} \;\prec\; \mathrm{bb}, $$ + +where $\mathrm{cb}$ is a compiler barrier, $\mathrm{acf}$ an atomic compiler fence, $\mathrm{mb}$ the CPU memory barrier, and $\mathrm{bb}$ the `black_box` register pin. $\mathrm{cb}_1$ blocks sinking pre-wipe work below the stores; $\mathrm{cb}_2$ and $\mathrm{acf}$ block hoisting post-wipe work above them; $\mathrm{mb}$ forces the retired stores to become globally visible in hardware. + +## Assembly-Level Verification (llvm-objdump) + +Volatile semantics are a *language-level* contract. To confirm the contract survives the full release pipeline (`opt-level = "z"`, `lto = true`, `panic = "abort"`) all the way down to machine code, the emitted binary is disassembled and validated statically. This reuses the same harness that guards the constant-time crate: `constant-time/scripts/check_ct_asm.sh` drives both, and its `PROBES_ZEROIZE` group covers this crate. + +Two probes in `zeroize/examples/zeroize_asm_probes.rs` are each emitted as an independent symbol via `#[unsafe(no_mangle)] extern "C"` + `#[inline(never)]`: + +- `probe_zeroize_flat` runs `zeroize_flat` over a 64-byte buffer. +- `probe_secret_drop` constructs `Secret<[u8; 32]>` and lets it drop. + +Unlike the constant-time probes, the wipe path is **not** checked for conditional-branch absence. Its loop bound is the compile-time constant `size_of`, so the loop branch is not secret-dependent and a branch check would be a false positive. Instead each zeroize probe must satisfy two predicates at once: + +$$ \#\{\text{volatile zero store}\} \ge 1 \quad\wedge\quad \#\{\text{memory fence}\} \ge 1 . $$ + +If either count falls to zero, a dead-store regression (CWE-14 / CWE-316) has slipped in and the check fails. On aarch64 the zero store is `strb wzr` and the fence is `dsb sy`; on x86_64 they are `mov $0`-class stores and `mfence`. + +Disassembling `probe_zeroize_flat` on the host (`aarch64-apple-darwin`) shows the byte loop over `0x40 = 64` bytes, ending in the surviving fence and the `black_box` tail call (labels trimmed, comments added): + +```text +<_probe_zeroize_flat>: + mov x8, #0x0 + cmp x8, #0x40 ; i == 64 ? (counter bound, not secret) + b.eq <+0x18> ; loop exit + strb wzr, [x0, x8] ; volatile store: mem[p+i] <- 0 (survives DCE) + add x8, x8, #0x1 + b <+0x4> + dsb sy ; memory_barrier(): retire stores in hardware + b +``` + +`probe_secret_drop` materializes the 32-byte secret (`dup.16b` + `stp q0, q0`), wipes it through the same volatile loop, then emits the fence and the `black_box` pin: + +```text +<_probe_secret_drop>: + ... + dup.16b v0, w0 ; secret = [seed; 32] + stp q0, q0, [sp] + ... + cmp x8, #0x20 ; i == 32 ? + b.eq <+0x38> + strb wzr, [x9, x8] ; volatile byte wipe + add x8, x8, #0x1 + b <+0x24> + dsb sy ; memory_barrier() + mov x0, sp + bl + ... + ret +``` + +A full run reports both probes preserving their store and their fence: + +```text +>> zeroize probe (zero store + fence survival) + PASS probe_secret_drop (zero store: 1, fence: 1) + PASS probe_zeroize_flat (zero store: 1, fence: 1) +RESULT: PASS +``` + +To inspect a single symbol directly: + +```bash +llvm-objdump -d --no-show-raw-insn target//release/examples/zeroize_asm_probes +``` + +On macOS the binary is at `/Library/Developer/CommandLineTools/usr/bin/llvm-objdump`. + +### Counter-Example: DSE in Action + +DSE is a *permitted* transformation, and whether the optimizer exercises it depends on opt-level, compiler version, and surrounding code. That unpredictability is the hazard. The contrast below makes the freedom concrete: two functions wipe a 32-byte stack secret that becomes dead after a checksum is read from it, one with a plain store and one with `ptr::write_volatile`. Built at `opt-level = 3`: + +```text +<_naive_wipe_dead>: ; plain `*b = 0` + ubfiz w0, w0, #5, #3 ; return checksum (32*seed mod 256); + ret ; buffer + all 32 stores ELIMINATED (0 stores) + +<_volatile_wipe_dead>: ; ptr::write_volatile(.., 0) + sub sp, sp, #0x20 + strb wzr, [sp, #0x1f] ; 32 volatile stores, every one preserved + strb wzr, [sp, #0x1e] + ... ; (32 total) + strb wzr, [sp, #0x1] + ubfiz w0, w0, #5, #3 + strb wzr, [sp], #0x20 + ret +``` + +The plain version collapses to two instructions: the optimizer proved the buffer dead and deleted all 32 stores, so the secret's bytes would survive on the stack. The volatile version keeps every store. Note that this same plain loop happens to survive at `opt-level = "z"` yet vanishes at `opt-level = 3`: survival of a non-volatile wipe is guaranteed at no optimization level. The crate therefore never relies on the optimizer's discretion. Volatile plus barriers force the wipe to survive across every opt-level, including the project's own `opt-level = "z"`. + +### Runtime Readback Verification + +The static check proves the stores are *emitted*; `zeroize/tests/zeroize_readback.rs` proves they *take effect*. Each test writes a non-zero pattern, captures the raw address, triggers the wipe (`Secret::drop`, `zeroize_flat`, `[T; N]::zeroize`, `&mut [T]::zeroize`), then reads the same address back and asserts every byte is zero: + +```text +running 5 tests +test secret_array_zeroized_on_drop ... ok +test secret_into_inner_extracts_value ... ok +test zeroize_flat_wipes_bytes ... ok +test array_zeroize_wipes ... ok +test slice_zeroize_wipes ... ok +test result: ok. 5 passed +``` + +Together the two layers close the loop: readback confirms the wipe is correct *when it runs*, and the llvm-objdump check confirms the optimizer cannot silently drop it from the shipped binary. + --- ## API From ac301b79cc1a08c09efb2f99cc5f0963f80059d1 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:55:10 +0900 Subject: [PATCH 19/65] =?UTF-8?q?ed25519=20=EB=B3=B4=EC=95=88=20=EA=B2=B0?= =?UTF-8?q?=ED=95=A8=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1=20-=20=EC=8A=A4?= =?UTF-8?q?=EC=B9=BC=EB=9D=BC=20=EA=B3=B1=EC=85=88=20=EC=83=81=EC=88=98?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=ED=99=94=20(=EB=B9=84=EB=B0=80=20=EB=B9=84?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EA=B8=B0=20=EC=A0=9C=EA=B1=B0)=20-=20?= =?UTF-8?q?=EB=B9=84=EC=A0=95=EA=B7=9C=20=EC=A0=90=20=EC=9D=B8=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EA=B1=B0=EB=B6=80=20(RFC=208032=20=EC=A4=80?= =?UTF-8?q?=EC=88=98)=20-=20=EC=A3=BD=EC=9D=80=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EB=B9=84=EB=B0=80=20=EC=A4=91?= =?UTF-8?q?=EA=B0=84=EA=B0=92=20=EC=86=8C=EA=B1=B0=20=EB=B3=B4=EA=B0=95=20?= =?UTF-8?q?-=20Scalar=20Sub=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20RFC=208032?= =?UTF-8?q?=20KAT=20=EC=B6=94=EA=B0=80=20-=20README=20=ED=95=9C=EC=98=81?= =?UTF-8?q?=ED=8C=90=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20cross-confirm=20Ed?= =?UTF-8?q?25519=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- ed25519/README.md | 166 ++++++++++++++++++++++++++++++++++++++++++ ed25519/README_EN.md | 166 ++++++++++++++++++++++++++++++++++++++++++ ed25519/src/field.rs | 74 +------------------ ed25519/src/lib.rs | 73 ++++++++++++++++++- ed25519/src/point.rs | 77 ++++++++------------ ed25519/src/scalar.rs | 38 +++++++--- 7 files changed, 463 insertions(+), 133 deletions(-) create mode 100644 ed25519/README.md create mode 100644 ed25519/README_EN.md diff --git a/cross-confirm.md b/cross-confirm.md index 313bacc..f211dbb 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -11,7 +11,7 @@ - [X] SHA3 - [ ] ChaCha20 - [ ] Ed448 -- [ ] Ed25519 +- [X] Ed25519 - [ ] X448 - [ ] X25519 - [X] ML-DSA diff --git a/ed25519/README.md b/ed25519/README.md new file mode 100644 index 0000000..8b66d5d --- /dev/null +++ b/ed25519/README.md @@ -0,0 +1,166 @@ +# Ed25519 서명 모듈 + +[![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) + +Ed25519는 두 가지에 보안을 의존하는 디지털 서명 방식입니다. edwards25519 곡선의 수학, 그리고 비밀이 그 비밀을 다루는 코드의 타이밍이나 메모리 접근 패턴에 영향을 주지 않는다는 보장입니다. 이 문서는 그 둘을 기술적으로 설명합니다. 크레이트가 구현하는 프로토콜, 그리고 구현이 서명 키를 누출하지 않는 어셈블리 차원의 근거입니다. + +--- + +## 암호 인터페이스 + +공개 API는 의도적으로 작습니다. 모든 진입점은 stateless입니다. 한 요청, 한 데이터, 즉시 소거. + +- `sign(message: &[u8], &SecretKey) -> Signature` +- `verify(message: &[u8], &Signature, &PublicKey) -> Result<(), Ed25519Error>` +- `SecretKey`: 32바이트 시드를 `Secret<[u8; 32]>`로 보유하며 Drop 시 소거됩니다. +- `PublicKey`: 32바이트 압축 곡선 점. +- `Signature`: 64바이트, `R (32) || s (32)`. +- `Keypair`: `{ secret, public }` 편의 묶음. + +내부는 세 개의 산술 계층으로 나뉘며, 각각 자체 모듈에 있습니다. + +| 모듈 | 영역 | 표현 | +|-----------|------------------------------------------|-----------------------------------------------| +| `field` | `Fp`, `p = 2^255 - 19` | 5 × 51비트 limb (radix-2^51), 리틀 엔디언 | +| `scalar` | `Z/L`, `L = 2^252 + 27742317...648493` | 32바이트, 리덕션 시 12 × 21비트 부호 limb | +| `point` | 트위스티드 에드워즈 `-x^2 + y^2 = 1 + d x^2 y^2` | 확장 좌표 `(X, Y, Z, T)`, `x = X/Z`, `y = Y/Z`, `xy = T/Z` | + +외부 의존성은 없습니다(`sha2`, `zeroize`는 내부 워크스페이스 크레이트). 할당자도 없고, 테스트 외에는 `std`도 없습니다. 기본 베어메탈 타겟 `x86_64-unknown-none`으로 빌드됩니다. + +## 표준 준수 + +구현은 **RFC 8032** PureEdDSA(Ed25519, SHA-512)를 따릅니다. RFC 8032 7.1절 KAT(TEST 1 ~ 3)에 대해 바이트 단위로 검증됩니다. 각 벡터에서 유도된 공개키와 산출된 서명이 공표된 바이트와 정확히 일치하고, 공표된 서명의 검증도 성공합니다. + +두 가지 정규성 검사로 RFC 8032와 FIPS 186-5의 엄격한 해석(논문 *Taming the many EdDSAs*가 정리한 cofactored 대 cofactorless 및 비정규 인코딩 사례)에 부합합니다. + +- **스칼라 범위**: 검증은 `s >= L`인 모든 서명을 거부합니다. `s`에서 `L`을 바이트 단위로 빼며 borrow를 추적하되 조기 반환이 없으므로, 이 검사 자체가 데이터 독립적입니다. +- **점 인코딩**: 점 디코드는 `y >= p`인 모든 인코딩을 거부합니다. `y` 필드 원소를 정규 형태로 재직렬화해 입력과 비교하며, 불일치는 비정규 인코딩이므로 거부됩니다. 이는 `R`과 공개키 `A` 양쪽의 서명 가변성(malleability) 경로를 차단합니다. + +검증은 cofactorless 그룹 방정식 `[s]B = R + [k]A`를 사용하며, 확장 좌표에서 곡선 점을 직접 비교합니다. + +## 서명 프로토콜 + +서명은 비밀 데이터에 대한 트랜잭션입니다. 아래 흐름은 `sign`과 `verify`가 실제로 수행하는 것이며, 우측 주석은 어느 값이 비밀인지 표시합니다. + +### 키 확장 (RFC 8032 5.1.5) + +```text +h = SHA512(seed) seed, h : 비밀 +a_bytes = h[0..32] 클램핑: + a_bytes[0] &= 248 (하위 3비트 클리어) + a_bytes[31] &= 127 (최상위 비트 클리어) + a_bytes[31] |= 64 (비트 254 설정) +a = a_bytes as scalar a : 비밀 (서명 스칼라) +prefix = h[32..64] prefix : 비밀 (nonce 키) +A = [a]B A : 공개 (공개키) +``` + +클램핑은 스칼라를 소수 차수 부분군으로 밀어넣고 최상위 비트를 고정합니다. 이것이 다음 단계의 고정 길이 ladder를 상수 시간으로 안전하게 돌릴 수 있게 만드는 요소입니다. + +### 서명 (RFC 8032 5.1.6) + +```text +1. r = SHA512(prefix || M) mod L r : 비밀 nonce +2. R = [r]B R : 공개 +3. k = SHA512(R || A || M) mod L k : 공개 (공개 입력에서 파생) +4. s = (r + k·a) mod L s : 공개, 단 비밀 a 를 묶음 +5. signature = R || s +``` + +4단계는 `L`을 법으로 하는 단일 fused multiply-add입니다(`sc_muladd(k, a, r)`). `r`이 `prefix`와 메시지의 결정적 해시이므로 서명에는 난수 생성기가 필요 없습니다. nonce 재사용 실패 모드가 존재하지 않습니다. + +### 검증 (RFC 8032 5.1.7) + +```text +1. R 디코드 (비정규 y 거부) -> MalformedSignature +2. s 파싱, s < L 요구 -> NonCanonicalScalar +3. A 디코드 (비정규 또는 곡선 밖 거부) -> InvalidPublicKey +4. k = SHA512(R || A || M) mod L +5. [s]B == R + [k]A 이면 수락 그 외 InvalidSignature +``` + +검증은 공개 데이터만 다루므로, 타이밍 독립성보다 정확성과 엄격성을 위해 작성되었습니다. + +## 상수-시간 보장 근거 + +값에 의존하는 산술을 거치는 비밀은 둘뿐입니다. 서명 스칼라 `a`와 메시지별 nonce `r`입니다. 둘 다 정확히 두 곳에서 소비됩니다. 스칼라 곱 `[a]B`, `[r]B`와 곱셈-덧셈 `r + k·a`입니다. 따라서 상수-시간 논증은 *스칼라 곱과 스칼라 산술에 비밀 의존 분기, 비밀 의존 메모리 접근, 가변 지연 명령이 없음*을 보이는 것으로 귀결됩니다. 보장은 네 겹입니다. + +### 1. 비밀 의존 분기 없음 (분기 없는 스칼라 곱셈) + +스칼라 곱은 고정된 256회 double-and-add입니다. 매 반복은 더블링 한 번과 덧셈 한 번을 무조건 수행한 뒤, `if`가 아니라 분기 없는 마스크 선택으로 '더한' 누적값과 '더하지 않은' 누적값 중 하나를 고릅니다. + +```rust +for i in (0..256).rev() { + result = result.double_internal(); + let bit = (s[i / 8] >> (i % 8)) & 1; // i, i/8 은 공개 루프 인덱스 + let sum = result.add_internal(self); // 덧셈은 항상 계산 + result = EdwardsPoint::conditional_select(&result, &sum, bit); +} +``` + +`conditional_select`는 선택 비트로 전체 폭 마스크를 만들고 두 입력을 limb 단위로 병합합니다. `a ^ (mask & (a ^ b))`로 `neg`, `and`, `xor`만 사용합니다. 어떤 비밀을 비교하거나 비밀로 점프하는 일이 없습니다. 호스트(aarch64, release)로 컴파일하면 5-limb 필드 선택은 직선형 비트 논리로 낮아지고 조건 분기 없이 반환합니다. + +```text +<_probe_fe_cselect>: + mov x9, #0x0 + mov w10, w2 + sub x9, x9, w2, uxtw ; x9 = 마스크 = -(choice) + ldp x11, x12, [x1] ; b limb 로드 + and x11, x11, x9 ; b & mask + sub x10, x10, #0x1 ; x10 = ~마스크 = choice - 1 + ldp x13, x14, [x0] ; a limb 로드 + and x13, x13, x10 ; a & ~mask + orr x11, x11, x13 ; (b & mask) | (a & ~mask) + ... ; 나머지 limb 도 동일 패턴 + ret +``` + +명령 부류는 두 지원 아키텍처에서 동일합니다. + +| 단계 | x86_64 | aarch64 | +|----------------|---------------|----------------| +| 비트 -> 마스크 | `neg` | `sub`/`neg` | +| 병합 | `and` + `or` | `and` + `orr` | + +`cmov`/`csel`조차 필요 없으며, 무엇보다 `jcc`/`b.cc`가 그 비트를 읽는 일이 전혀 없습니다. 이전 구현은 `if bit == 1 { add }`를 사용했는데, 이는 정확히 그런 비밀 의존 조건 분기를 방출했습니다. 분기 예측과 명령 캐시 타이밍으로 관측 가능하고, 스칼라를 한 번에 한 비트씩 복원하기에 충분했습니다. 그 분기는 제거되었고, 위 디스어셈블이 그 회귀 검사입니다. + +### 2. 비밀 의존 메모리 접근 없음 + +곱셈은 가변 베이스이며 테이블이 없습니다. 비밀 비트로 인덱싱되는 기저점의 사전 계산 배수가 없으므로, 로드 주소가 비밀에 의존하지 않고 캐시나 TLB 타이밍이 키에 따라 달라질 수 없습니다. 핫 루프의 유일한 인덱스 `i`, `i / 8`은 공개 루프 카운터입니다. 컴파일러가 삽입하는 경계 검사 역시 비밀 데이터가 아니라 공개 인덱스에 대한 검사입니다. + +### 3. 데이터 독립 필드 및 스칼라 산술 + +- **역원**은 페르마 소정리 `a^(-1) = a^(p-2)`를 사용하며, 제곱과 곱셈의 고정 addition chain으로 평가합니다. 연산 순서는 모든 입력에 대해 동일합니다. 피연산자 크기에 반복 횟수가 의존하는 확장 유클리드 알고리즘은 결코 쓰지 않습니다. +- **제곱근**(점 압축 해제)도 고정 지수화 chain `a^((p+3)/8)` 뒤 후보 검사이며, 공개 인코딩에 대해서만 실행됩니다. +- **곱셈**은 `64 × 64 -> 128`비트 부분곱(`u128`)을 누적합니다. x86_64와 aarch64에서 정수 곱 지연은 피연산자 값과 무관하며, 워크스페이스가 지원하는 아키텍처가 정확히 이 둘이므로 그 가정은 희망이 아니라 설계상 성립합니다. 필드와 스칼라 코드 어디에도 `div`는 없습니다. +- **리덕션**은 `Fp` 캐리 체인과 `mod L` 리덕션(`sc_reduce`) 모두 컴파일타임에 고정된 횟수의 직선형 코드입니다. 어떤 값에도 루프 횟수가 의존하지 않습니다. + +### 4. 소거는 배리어 기반 프리미티브에 위임 + +최적화 배리어가 정말로 필요한 곳, 즉 컴파일러가 store를 제거하지 못하게 비밀을 지우는 경우에는 이 크레이트가 포터블 Rust에 기대지 않습니다. per-arch `barrier/{x86_64,aarch64}.rs`가 실제 메모리·컴파일러 펜스와 volatile write를 인-라인 어셈블리로 발행하는 `zeroize` 크레이트에 위임합니다. 상수-시간 *선택*은 산술 마스킹으로(1 ~ 3 레벨), 상수-시간 *소거*는 그 배리어로 달성합니다. 두 관심사는 각각에 맞는 도구를 씁니다. + +## 비밀 수명주기와 소거 + +데몬의 핵심 불변식은 비밀이 그것을 사용한 트랜잭션보다 오래 살아남지 않는다는 것입니다. 크레이트는 이를 구조적으로 강제합니다. + +- `SecretKey`는 시드를 `Secret<[u8; 32]>`로 보유하며, Drop이 volatile하고 배리어로 펜싱된 소거를 수행합니다. +- `expand`는 두 해시 절반을 모두 `Secret` 컨테이너에 둡니다. 클램핑된 스칼라는 `ExpandedSecretKey`에 살며, 그 Drop이 스칼라를 명시적으로 소거합니다(`Copy` 타입이라 Drop만으로는 지워지지 않음). nonce prefix는 자체 `Secret`이 소거합니다. +- `sign`은 반환 전에 nonce `r`, 챌린지 `k`, 출력 스칼라 `s`, 중간 점 `R`을 명시적으로 소거합니다. 64바이트 와이드 해시 버퍼는 `Secret`으로 감싸여 스코프 종료 시 자동 소거됩니다. +- 스칼라 리덕션 루틴(`sc_muladd`, `from_bytes_mod_order_wide`)은 비밀 스칼라의 분해 형태인 내부 limb 배열을 반환 전에 소거하므로, 호출 이후 스택에 남는 비밀이 없습니다. + +이로써 서명은 가장 강한 의미에서 stateless가 됩니다. `sign`이 반환된 뒤 메모리에 남는 비밀 파생 바이트는 호출자의 `SecretKey` 안 시드와, 설계상 공개인 서명 자체뿐입니다. + +--- + +## 검증 요약 + +| 속성 | 검사 방법 | +|----------------------------|-----------------------------------------------------------| +| RFC 8032 정확성 | 7.1절 TEST 1 ~ 3에 대한 바이트 일치 KAT | +| 비정규 점 거부 | `y = p` 디코드 거부(`MalformedSignature`) | +| 비정규 스칼라 거부 | `s = L` 거부(`NonCanonicalScalar`) | +| 분기 없는 비밀 스칼라 곱 | select의 `llvm-objdump`에 조건 분기 없음 | +| 린트 게이트 | `cargo clippy --all-targets --all-features -- -D warnings` 무경고 | +| 베어메탈 자세 | `x86_64-unknown-none` 빌드, `no_std`, `alloc` 없음 | + +남은 하드닝으로, 마스크 선택 명령의 데이터 독립 시간을 ISA 차원에서 고정하는 aarch64 DIT 비트와 x86 DOITM 모드를 명시적으로 켜는 작업은 향후 과제로 남으며, 이는 `constant-time` 크레이트에 기록된 동일한 미해결 항목을 따릅니다. diff --git a/ed25519/README_EN.md b/ed25519/README_EN.md new file mode 100644 index 0000000..4c0830d --- /dev/null +++ b/ed25519/README_EN.md @@ -0,0 +1,166 @@ +# Ed25519 Signature Module + +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) + +Ed25519 is a digital signature scheme whose security rests on two things: the math of the edwards25519 curve, and the guarantee that no secret ever influences the timing or memory-access pattern of the code that handles it. This document gives a technical account of both — the protocol the crate implements, and the assembly-level reasons the implementation does not leak the signing key. + +--- + +## Cryptographic Surface + +The public API is intentionally small. Every entry point is stateless: one request, one datum, immediate erase. + +- `sign(message: &[u8], &SecretKey) -> Signature` +- `verify(message: &[u8], &Signature, &PublicKey) -> Result<(), Ed25519Error>` +- `SecretKey` — wraps `Secret<[u8; 32]>` (the 32-byte seed); zeroized on drop. +- `PublicKey` — 32-byte compressed curve point. +- `Signature` — 64 bytes, `R (32) || s (32)`. +- `Keypair` — `{ secret, public }` convenience bundle. + +Internally the crate is split into three arithmetic layers, each in its own module: + +| Module | Domain | Representation | +|-------------|-----------------------------------------|---------------------------------------------| +| `field` | `Fp`, `p = 2^255 - 19` | 5 × 51-bit limbs (radix-2^51), little-endian | +| `scalar` | `Z/L`, `L = 2^252 + 27742317...648493` | 32 bytes; 12 × 21-bit signed limbs for reduction | +| `point` | twisted Edwards `-x^2 + y^2 = 1 + d x^2 y^2` | extended coordinates `(X, Y, Z, T)`, `x = X/Z`, `y = Y/Z`, `xy = T/Z` | + +There are no external dependencies (`sha2` and `zeroize` are internal workspace crates), no allocator, and no `std` outside tests. The crate builds for the default bare-metal target `x86_64-unknown-none`. + +## Standard Conformance + +The implementation follows **RFC 8032** PureEdDSA (Ed25519, SHA-512). It is validated byte-for-byte against the RFC 8032 Section 7.1 known-answer vectors (TEST 1 through 3): for each, the derived public key and the produced signature match the published bytes exactly, and verification of the published signature succeeds. + +Two canonicality checks bring the crate in line with the stricter reading of RFC 8032 and FIPS 186-5 (the "cofactored vs cofactorless" and "non-canonical encoding" cases catalogued in *Taming the many EdDSAs*): + +- **Scalar range.** Verification rejects any `s >= L`. The check subtracts `L` from `s` byte by byte while tracking a borrow, with no early return, so it is itself data-independent. +- **Point encoding.** Point decoding rejects any `y >= p`. A `y` field element is re-serialized to its canonical form and compared with the input; a mismatch means the encoding was non-canonical and is refused. This closes a signature-malleability avenue on both `R` and the public key `A`. + +Verification uses the cofactorless group equation `[s]B = R + [k]A`, comparing curve points directly in extended coordinates. + +## The Signing Protocol + +A signature is a transaction over secret material. The flow below is exactly what `sign` and `verify` execute; the right-hand annotations mark which quantities are secret. + +### Key expansion (RFC 8032 5.1.5) + +```text +h = SHA512(seed) seed, h : secret +a_bytes = h[0..32] clamp: + a_bytes[0] &= 248 (clear low 3 bits) + a_bytes[31] &= 127 (clear top bit) + a_bytes[31] |= 64 (set bit 254) +a = a_bytes as scalar a : secret (signing scalar) +prefix = h[32..64] prefix : secret (nonce key) +A = [a]B A : public (public key) +``` + +Clamping forces the scalar into the prime-order subgroup and fixes its top bit, which is what makes the fixed-length ladder in the next step safe to run in constant time. + +### Sign (RFC 8032 5.1.6) + +```text +1. r = SHA512(prefix || M) mod L r : secret nonce +2. R = [r]B R : public +3. k = SHA512(R || A || M) mod L k : public (derived from public inputs) +4. s = (r + k·a) mod L s : public, but binds the secret a +5. signature = R || s +``` + +Step 4 is a single fused multiply-add modulo `L` (`sc_muladd(k, a, r)`). Because `r` is a deterministic hash of `prefix` and the message, signing needs no random number generator — there is no nonce-reuse failure mode. + +### Verify (RFC 8032 5.1.7) + +```text +1. decode R (reject non-canonical y) -> MalformedSignature +2. parse s, require s < L -> NonCanonicalScalar +3. decode A (reject non-canonical / off-curve) -> InvalidPublicKey +4. k = SHA512(R || A || M) mod L +5. accept iff [s]B == R + [k]A else InvalidSignature +``` + +Verification touches only public data, so it is written for correctness and strictness rather than for timing-independence. + +## Constant-Time Guarantee Rationale + +Only two secrets ever flow through value-dependent arithmetic: the signing scalar `a` and the per-message nonce `r`. Both are consumed in exactly two places — the scalar multiplications `[a]B` and `[r]B`, and the multiply-add `r + k·a`. The constant-time argument therefore reduces to showing that *scalar multiplication and scalar arithmetic contain no secret-dependent branch, no secret-dependent memory access, and no variable-latency instruction.* The guarantee holds at four levels. + +### 1. No secret-dependent branch — branchless scalar multiplication + +The scalar multiply is a fixed 256-iteration double-and-add. Every iteration performs one doubling **and** one addition unconditionally, then chooses between the "added" and "not-added" accumulator with a branchless masked select, never with an `if`: + +```rust +for i in (0..256).rev() { + result = result.double_internal(); + let bit = (s[i / 8] >> (i % 8)) & 1; // i, i/8 are public loop indices + let sum = result.add_internal(self); // addition always computed + result = EdwardsPoint::conditional_select(&result, &sum, bit); +} +``` + +`conditional_select` builds a full-width mask from the choice bit and merges the two inputs limb by limb — `a ^ (mask & (a ^ b))`, using only `neg`, `and`, `xor`. There is no comparison of, or jump on, any secret. Compiled for the host (aarch64, release), the five-limb field select lowers to straight-line bitwise logic and returns with no conditional branch: + +```text +<_probe_fe_cselect>: + mov x9, #0x0 + mov w10, w2 + sub x9, x9, w2, uxtw ; x9 = mask = -(choice) + ldp x11, x12, [x1] ; b limbs + and x11, x11, x9 ; b & mask + sub x10, x10, #0x1 ; x10 = ~mask = choice - 1 + ldp x13, x14, [x0] ; a limbs + and x13, x13, x10 ; a & ~mask + orr x11, x11, x13 ; (b & mask) | (a & ~mask) + ... ; same pattern for the remaining limbs + ret +``` + +The instruction class is the same on both supported architectures: + +| Step | x86_64 | aarch64 | +|----------------|---------------|----------------| +| Mask from bit | `neg` | `sub`/`neg` | +| Merge | `and` + `or` | `and` + `orr` | + +No `cmov`/`csel` is even required, and crucially no `jcc`/`b.cc` ever reads the bit. The earlier implementation used `if bit == 1 { add }`, which emitted exactly such a secret-dependent conditional branch — observable through branch prediction and instruction-cache timing, and sufficient to recover the scalar one bit at a time. That branch has been removed; the disassembly above is the regression check. + +### 2. No secret-dependent memory access + +The multiply is variable-base and table-free: there is no precomputed multiple of the base point indexed by secret bits, so no load address depends on a secret and no cache or TLB timing can vary with the key. The only indices in the hot loop — `i`, `i / 8` — are the public loop counter. Any bounds check the compiler inserts is therefore a check on a public index, not on secret data. + +### 3. Data-independent field and scalar arithmetic + +- **Inversion** uses Fermat's little theorem, `a^(-1) = a^(p-2)`, evaluated by a fixed addition chain of squarings and multiplications. The sequence of operations is identical for every input. The crate never uses the extended Euclidean algorithm, whose iteration count depends on operand magnitude. +- **Square root** (point decompression) is likewise a fixed exponentiation chain `a^((p+3)/8)` followed by a candidate check — and it only ever runs on public encodings. +- **Multiplication** accumulates `64 × 64 -> 128`-bit partial products (`u128`). On x86_64 and aarch64 the integer multiply latency is independent of operand values; these are the only two architectures the workspace supports, so the assumption is sound by construction rather than by hope. There is no `div` anywhere in the field or scalar code. +- **Reduction** — both the `Fp` carry chain and the `mod L` reduction (`sc_reduce`) — is straight-line code with a compile-time-fixed number of carry passes. No loop trip count depends on a value. + +### 4. Erasure is delegated to a barrier-backed primitive + +Where an optimizer barrier is genuinely required — wiping a secret so the compiler cannot elide the store — this crate does not rely on portable Rust. It defers to the `zeroize` crate, whose per-architecture `barrier/{x86_64,aarch64}.rs` issue real memory/compiler fences and volatile writes in inline assembly. Constant-time *selection* is achieved with arithmetic masking (Levels 1-3); constant-time *erasure* is achieved with those barriers (below). The two concerns use the tool appropriate to each. + +## Secret Lifecycle and Zeroization + +The daemon's core invariant is that a secret never outlives the transaction that used it. The crate enforces this structurally: + +- `SecretKey` holds the seed in `Secret<[u8; 32]>`; its `Drop` performs a volatile, barrier-fenced wipe. +- `expand` keeps both hash halves in `Secret` containers; the clamped scalar lives in `ExpandedSecretKey`, whose `Drop` explicitly zeroizes the scalar (a `Copy` type that `Drop` alone would not clear), while the nonce prefix is wiped by its own `Secret`. +- `sign` explicitly zeroizes the nonce `r`, the challenge `k`, the output scalar `s`, and the intermediate point `R` before returning; the 64-byte wide-hash buffers are `Secret`-wrapped and wiped automatically on scope exit. +- The scalar reduction routines (`sc_muladd`, `from_bytes_mod_order_wide`) zeroize their internal limb arrays — the decomposed forms of the secret scalars — before they return, so no secret survives on the stack past the call. + +Together these make signing stateless in the strongest sense: after `sign` returns, the only secret-derived bytes still in memory are the seed inside the caller's `SecretKey`, and the signature itself, which is public by design. + +--- + +## Verification Summary + +| Property | How it is checked | +|-----------------------------------|---------------------------------------------------------------| +| RFC 8032 correctness | Byte-exact KAT against Section 7.1 TEST 1-3 | +| Non-canonical point rejected | Decode of `y = p` is refused (`MalformedSignature`) | +| Non-canonical scalar rejected | `s = L` is refused (`NonCanonicalScalar`) | +| Branchless secret scalar multiply | `llvm-objdump` of the select shows no conditional branch | +| Lint gate | `cargo clippy --all-targets --all-features -- -D warnings` clean | +| Bare-metal posture | Builds for `x86_64-unknown-none`, `no_std`, no `alloc` | + +As remaining hardening, explicitly enabling the aarch64 DIT bit and the x86 DOITM mode — which pin data-independent timing at the ISA level for the masked-select instructions — is left for future work, mirroring the same open item tracked in the `constant-time` crate. diff --git a/ed25519/src/field.rs b/ed25519/src/field.rs index 146ea29..a360800 100644 --- a/ed25519/src/field.rs +++ b/ed25519/src/field.rs @@ -6,10 +6,7 @@ #![allow( clippy::unusual_byte_groupings, clippy::wrong_self_convention, - clippy::needless_range_loop, - dead_code, - unused_variables, - unused_mut + clippy::needless_range_loop )] use core::ops::{Add, Mul, Neg, Sub}; @@ -162,68 +159,8 @@ impl FieldElement { /// /// 페르마 소정리 사용: a^(-1) = a^(p-2) mod p pub fn invert(&self) -> Self { - // p-2 = 2^255 - 21의 제곱-곱셈 체인 + // p - 2 = 2^255 - 21. ref10 스타일 addition chain (square-and-multiply) let x1 = *self; - let x2 = x1.square(); - let x4 = x2.square().square(); - let x5 = x4 * x1; - let x10 = x5.square().square().square().square().square(); - let x20 = x10 - .square() - .square() - .square() - .square() - .square() - .square() - .square() - .square() - .square() - .square(); - let x40 = { - let mut t = x20; - for _ in 0..20 { - t = t.square(); - } - t - }; - let x50 = x40 * x10; - let x100 = { - let mut t = x50; - for _ in 0..50 { - t = t.square(); - } - t - }; - let x200 = { - let mut t = x100; - for _ in 0..100 { - t = t.square(); - } - t - }; - let x250 = { - let mut t = x200 * x50; - for _ in 0..50 { - t = t.square(); - } - t - }; - - // x^(2^255 - 21) = x250 * x5 - // 그런데 2^255 - 21 = p - 2가 아님! - // p - 2 = 2^255 - 21 - - // 올바른 지수: p-2 = 2^255 - 19 - 2 = 2^255 - 21 - // 비트 분해로 계산 - - // 더 간단한 방법: 반복 제곱 - let mut result = FieldElement::one(); - let mut base = *self; - - // p - 2 = 0x7FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFEB - // 바이너리로 분해하여 square-and-multiply - - // 최적화된 체인 (Curve25519 스타일) let z2 = x1.square(); let z4 = z2.square(); let z8 = z4.square(); @@ -341,13 +278,6 @@ impl FieldElement { (bytes[0] & 1) == 1 } - /// 조건부 부정: choice가 1이면 부정합니다. - #[inline] - pub fn conditional_negate(&self, choice: u8) -> Self { - let neg = -*self; - Self::conditional_select(self, &neg, choice) - } - /// 조건부 선택: choice가 1이면 b, 0이면 a를 반환합니다. #[inline] pub fn conditional_select(a: &Self, b: &Self, choice: u8) -> Self { diff --git a/ed25519/src/lib.rs b/ed25519/src/lib.rs index 6cca3c7..2f713ae 100644 --- a/ed25519/src/lib.rs +++ b/ed25519/src/lib.rs @@ -199,7 +199,8 @@ impl Signature { /// `Secret` Drop 또는 명시적 `zeroize()` 호출로 소거됩니다. pub fn sign(message: &[u8], secret_key: &SecretKey) -> Signature { let expanded = secret_key.expand(); - let public_key = PublicKey::from(secret_key); + // 공개키 A 는 이미 계산한 확장 스칼라에서 직접 유도 (expand 중복 호출 제거) + let public_key = PublicKey(EdwardsPoint::basepoint_mul(&expanded.scalar).to_bytes()); // r = SHA512(nonce || message) mod L (r 은 비밀 nonce) let mut h1 = SHA512::new(); @@ -483,4 +484,74 @@ mod tests { let signature = keypair1.sign(message); assert!(verify(message, &signature, &keypair2.public).is_err()); } + + fn decode_hex(s: &str) -> [u8; N] { + let mut out = [0u8; N]; + let b = s.as_bytes(); + for i in 0..N { + let hi = (b[2 * i] as char).to_digit(16).unwrap() as u8; + let lo = (b[2 * i + 1] as char).to_digit(16).unwrap() as u8; + out[i] = (hi << 4) | lo; + } + out + } + + fn kat(seed_h: &str, pk_h: &str, msg: &[u8], sig_h: &str) { + let seed = decode_hex::<32>(seed_h); + let exp_pk = decode_hex::<32>(pk_h); + let exp_sig = decode_hex::<64>(sig_h); + + let sk = SecretKey::from_bytes(&seed); + let pk = PublicKey::from(&sk); + assert_eq!(pk.as_bytes(), &exp_pk, "공개키 KAT 불일치"); + + let sig = sign(msg, &sk); + assert_eq!(sig.as_bytes(), &exp_sig, "서명 KAT 불일치"); + + let pk2 = PublicKey::from_bytes(&exp_pk); + let sig2 = Signature::from_bytes(&exp_sig); + assert!(verify(msg, &sig2, &pk2).is_ok(), "검증 KAT 실패"); + } + + // RFC 8032 Section 7.1 표준 시험 벡터 (TEST 1 - 3) + #[test] + fn test_rfc8032_kat() { + kat( + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + &[], + "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b", + ); + kat( + "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", + &[0x72], + "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00", + ); + kat( + "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7", + "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", + &[0xaf, 0x82], + "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a", + ); + } + + // 비정규 점 인코딩 거부 (RFC 8032 5.1.3): R 의 y = p 표현은 거부되어야 함 + #[test] + fn test_reject_noncanonical_point() { + let pk = PublicKey::from_bytes(&decode_hex::<32>( + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + )); + let mut sig = [0u8; 64]; + sig[0] = 0xed; + sig[1..31].fill(0xff); + sig[31] = 0x7f; // R = y = p (비정규) + sig[32] = 1; // s = 1 (정규) + let sig = Signature::from_bytes(&sig); + assert_eq!( + verify(&[], &sig, &pk), + Err(Ed25519Error::MalformedSignature), + "비정규 R 인코딩이 거부되어야 함" + ); + } } diff --git a/ed25519/src/point.rs b/ed25519/src/point.rs index 42c4ffc..1662c9b 100644 --- a/ed25519/src/point.rs +++ b/ed25519/src/point.rs @@ -6,10 +6,7 @@ #![allow( clippy::unusual_byte_groupings, clippy::wrong_self_convention, - clippy::question_mark, - dead_code, - unused_variables, - unused_mut + clippy::question_mark )] use crate::field::FieldElement; @@ -29,10 +26,6 @@ fn d_constant() -> FieldElement { FieldElement::from_bytes(&d_bytes) } -fn d2_constant() -> FieldElement { - d_constant().double() -} - /// 기저점 B의 y 좌표 /// y = 4/5 mod p const BASE_Y: FieldElement = FieldElement([ @@ -98,6 +91,12 @@ impl EdwardsPoint { y_bytes[31] &= 0x7F; let y = FieldElement::from_bytes(&y_bytes); + // 비정규 인코딩 거부 (RFC 8032 5.1.3: y < p 이어야 함) + // 정규 직렬화 결과가 입력과 다르면 y >= p 인 비정규 표현 + if y.to_bytes() != y_bytes { + return None; + } + // x^2 = (y^2 - 1) / (d*y^2 + 1) 계산 let y2 = y.square(); let u = y2 - FieldElement::one(); // y^2 - 1 @@ -161,28 +160,16 @@ impl EdwardsPoint { /// /// RFC 8032 및 "Twisted Edwards Curves Revisited" 논문 참조 fn add_internal(&self, other: &Self) -> Self { - // 통합 덧셈 공식 (complete addition formula) - // C = d * T1 * T2 (NOT 2*d) + // add-2008-hwcd 통합 덧셈 (a = -1, d 비제곱이므로 Ed25519 에서 완전식) let a = self.x * other.x; let b = self.y * other.y; - let c = self.t * d_constant() * other.t; + let c = self.t * d_constant() * other.t; // C = d*T1*T2 let d = self.z * other.z; let e = (self.x + self.y) * (other.x + other.y) - a - b; let f = d - c; let g = d + c; - let h = b + a; // -x^2 + y^2 커브이므로 b - (-a) = b + a - - // 실제로는 -x^2 + y^2 = 1 + d*x^2*y^2 - // a 커브이므로 a = -1 - let h = b - a * FieldElement::from_bytes(&[ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 0x7f, - ]); // a = -1이므로 h = b - a = b - (-1)*a = b + a - - // 단순화: Ed25519는 a = -1 - let h = b + a; + let h = b + a; // H = B - a*A, a = -1 이므로 B + A EdwardsPoint { x: e * f, @@ -213,29 +200,35 @@ impl EdwardsPoint { } } + /// 조건부 점 선택 (상수시간): choice가 1이면 b, 0이면 a를 반환합니다. + #[inline] + fn conditional_select(a: &Self, b: &Self, choice: u8) -> Self { + EdwardsPoint { + x: FieldElement::conditional_select(&a.x, &b.x, choice), + y: FieldElement::conditional_select(&a.y, &b.y, choice), + z: FieldElement::conditional_select(&a.z, &b.z, choice), + t: FieldElement::conditional_select(&a.t, &b.t, choice), + } + } + /// 스칼라 곱셈: s * P + /// + /// # Security Note + /// 비밀 스칼라의 비트에 분기하지 않는 상수시간 double-and-add 입니다. + /// 매 비트마다 덧셈을 항상 수행한 뒤 조건부 선택으로 누적값을 갱신하여 + /// 타이밍 사이드 채널로 스칼라가 새지 않도록 합니다. pub fn scalar_mul(&self, scalar: &Scalar) -> Self { - // 고정-윈도우 방식 (4-bit window) - // 또는 간단한 double-and-add - - // 스칼라 바이트는 비밀일 수 있으므로 Secret 래핑 let mut s = scalar.to_bytes(); let mut result = EdwardsPoint::identity(); - // MSB부터 double-and-add for i in (0..256).rev() { result = result.double_internal(); - - let byte_idx = i / 8; - let bit_idx = i % 8; - let bit = (s[byte_idx] >> bit_idx) & 1; - - if bit == 1 { - result = result.add_internal(self); - } + let bit = (s[i / 8] >> (i % 8)) & 1; + let mut sum = result.add_internal(self); + result = EdwardsPoint::conditional_select(&result, &sum, bit); + sum.zeroize(); } - // 스칼라 사본 명시적 소거 s.zeroize(); result } @@ -244,16 +237,6 @@ impl EdwardsPoint { pub fn basepoint_mul(scalar: &Scalar) -> Self { Self::basepoint().scalar_mul(scalar) } - - /// 이중 스칼라 곱셈: a*A + b*B - /// - /// 검증에서 사용됩니다. Straus/Shamir 기법으로 최적화 가능. - pub fn double_scalar_mul_basepoint(a: &Scalar, point_a: &Self, b: &Scalar) -> Self { - // 간단한 구현: 별도 계산 후 덧셈 - let a_point = point_a.scalar_mul(a); - let b_point = Self::basepoint_mul(b); - a_point.add_internal(&b_point) - } } impl Add for EdwardsPoint { diff --git a/ed25519/src/scalar.rs b/ed25519/src/scalar.rs index 2a01230..e223096 100644 --- a/ed25519/src/scalar.rs +++ b/ed25519/src/scalar.rs @@ -8,8 +8,7 @@ #![allow( clippy::unusual_byte_groupings, clippy::wrong_self_convention, - clippy::needless_range_loop, - dead_code + clippy::needless_range_loop )] use core::ops::{Add, Mul, Sub}; @@ -22,6 +21,13 @@ pub const L_BYTES: [u8; 32] = [ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, ]; +/// (L - 1) mod L = -1 (스칼라 부정에 사용) +const MINUS_ONE: Scalar = { + let mut b = L_BYTES; + b[0] = L_BYTES[0] - 1; + Scalar(b) +}; + /// L의 21-bit signed limb 분해 계수 (low 부분, l3/l5는 음수) /// L = 2^252 + (L0 + L1·2^21 + L2·2^42 + L3·2^63 + L4·2^84 + L5·2^105) const L0: i64 = 666643; @@ -67,9 +73,11 @@ impl Scalar { pub fn from_bytes_mod_order_wide(bytes: &[u8; 64]) -> Self { let mut s = load_24_from_64(bytes); sc_reduce_24(&mut s); - Scalar(pack_12_to_32(&[ + let out = Scalar(pack_12_to_32(&[ 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.zeroize(); + out } /// 바이트 배열을 반환합니다. @@ -93,9 +101,9 @@ impl Scalar { /// 스칼라 곱셈 후 덧셈: (a · b + c) mod L pub fn sc_muladd(a: &Scalar, b: &Scalar, c: &Scalar) -> Scalar { - let a_limbs = load_12_from_32(&a.0); - let b_limbs = load_12_from_32(&b.0); - let c_limbs = load_12_from_32(&c.0); + let mut a_limbs = load_12_from_32(&a.0); + let mut b_limbs = load_12_from_32(&b.0); + let mut c_limbs = load_12_from_32(&c.0); // s = c + a · b // i+j 위치에 a[i]·b[j] 곱을 누적; c는 0..11 에 더함 @@ -114,9 +122,16 @@ pub fn sc_muladd(a: &Scalar, b: &Scalar, c: &Scalar) -> Scalar { sc_reduce_24(&mut s); - Scalar(pack_12_to_32(&[ + let out = Scalar(pack_12_to_32(&[ s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8], s[9], s[10], s[11], - ])) + ])); + + // 비밀 스칼라에서 유래한 limb 중간값 소거 + a_limbs.zeroize(); + b_limbs.zeroize(); + c_limbs.zeroize(); + s.zeroize(); + out } /// 24-limb 배열에 대한 첫 rounded 캐리 전파 (짝수 → 홀수, 인덱스 0..22). @@ -216,9 +231,8 @@ impl Sub for Scalar { #[allow(clippy::suspicious_arithmetic_impl)] fn sub(self, rhs: Self) -> Self { - // a - b = a + (L - b) mod L - // (-1) · rhs + 0 = -rhs (mod L) - let neg_rhs = sc_muladd(&Scalar([0xff; 32]), &rhs, &Scalar::zero()); + // a - b = a + (-1)*b mod L, (-1) = (L - 1) mod L + let neg_rhs = sc_muladd(&MINUS_ONE, &rhs, &Scalar::zero()); sc_muladd(&Scalar::one(), &self, &neg_rhs) } } From 2e7c4119e1af17952e058368873eb5152ce8c300 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:27:30 +0900 Subject: [PATCH 20/65] =?UTF-8?q?x25519=20=EB=B3=B4=EC=95=88=20=EA=B2=80?= =?UTF-8?q?=ED=86=A0=20=ED=9B=84=EC=86=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - diffie_hellman 저위수 점 거부 (all-zero 공유 비밀 -> X25519Error LowOrderPoint) - 몽고메리 사다리 루프 중간값 명시적 zeroize - FieldElement PartialEq 상수시간화 (XOR-OR 누적) - 미사용 dead code(neg, is_zero) 제거 및 PublicKey bit255 정규화 --- x25519/src/field.rs | 30 ++++------------- x25519/src/lib.rs | 80 +++++++++++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/x25519/src/field.rs b/x25519/src/field.rs index db4ae76..a45172a 100644 --- a/x25519/src/field.rs +++ b/x25519/src/field.rs @@ -1,12 +1,11 @@ #![allow( clippy::unusual_byte_groupings, clippy::wrong_self_convention, - clippy::needless_range_loop, - dead_code + clippy::needless_range_loop )] use constant_time::{Choice, CtSelOps}; -use core::ops::{Add, Mul, Neg, Sub}; +use core::ops::{Add, Mul, Sub}; use zeroize::Zeroize; const MASK51: u64 = (1u64 << 51) - 1; @@ -213,12 +212,6 @@ impl FieldElement { FieldElement([c0 as u64, c1 as u64, c2 as u64, c3 as u64, c4 as u64]) } - pub fn is_zero(&self) -> Choice { - let t = self.reduce(); - let or = t.0[0] | t.0[1] | t.0[2] | t.0[3] | t.0[4]; - Choice::from_u8((or == 0) as u8) - } - #[inline] pub fn conditional_swap(a: &mut Self, b: &mut Self, choice: Choice) { for i in 0..5 { @@ -269,15 +262,6 @@ impl Sub for FieldElement { } } -impl Neg for FieldElement { - type Output = Self; - - #[inline] - fn neg(self) -> Self { - FieldElement::zero() - self - } -} - impl Mul for FieldElement { type Output = Self; @@ -291,11 +275,11 @@ impl PartialEq for FieldElement { fn eq(&self, other: &Self) -> bool { let a = self.reduce(); let b = other.reduce(); - a.0[0] == b.0[0] - && a.0[1] == b.0[1] - && a.0[2] == b.0[2] - && a.0[3] == b.0[3] - && a.0[4] == b.0[4] + let mut acc = 0u64; + for i in 0..5 { + acc |= a.0[i] ^ b.0[i]; + } + acc == 0 } } diff --git a/x25519/src/lib.rs b/x25519/src/lib.rs index 74048e6..8344b32 100644 --- a/x25519/src/lib.rs +++ b/x25519/src/lib.rs @@ -42,6 +42,11 @@ const BASEPOINT_U: [u8; 32] = [ 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum X25519Error { + LowOrderPoint, +} + pub struct SecretKey(Secret<[u8; 32]>); impl SecretKey { @@ -58,18 +63,26 @@ impl SecretKey { PublicKey(public_bytes) } - pub fn diffie_hellman(&self, their_public: &PublicKey) -> SharedSecret { + pub fn diffie_hellman( + &self, + their_public: &PublicKey, + ) -> Result { let mut shared = x25519(self.0.expose(), &their_public.0); let result = SharedSecret(Secret::new(shared)); shared.zeroize(); - result + if result.is_zero() { + Err(X25519Error::LowOrderPoint) + } else { + Ok(result) + } } } pub struct PublicKey([u8; 32]); impl PublicKey { - pub fn from_bytes(bytes: [u8; 32]) -> Self { + pub fn from_bytes(mut bytes: [u8; 32]) -> Self { + bytes[31] &= 0x7f; PublicKey(bytes) } @@ -122,6 +135,19 @@ fn montgomery_ladder(k: &[u8; 32], u: &[u8; 32]) -> [u8; 32] { let mut swap: u8 = 0; + let mut a = FieldElement::zero(); + let mut aa = FieldElement::zero(); + let mut b = FieldElement::zero(); + let mut bb = FieldElement::zero(); + let mut e = FieldElement::zero(); + let mut c = FieldElement::zero(); + let mut d = FieldElement::zero(); + let mut da = FieldElement::zero(); + let mut cb = FieldElement::zero(); + let mut sum = FieldElement::zero(); + let mut diff = FieldElement::zero(); + let mut a24_e = FieldElement::zero(); + for pos in (0..255).rev() { let byte_idx = pos / 8; let bit_idx = pos % 8; @@ -133,21 +159,21 @@ fn montgomery_ladder(k: &[u8; 32], u: &[u8; 32]) -> [u8; 32] { FieldElement::conditional_swap(&mut z_2, &mut z_3, choice); swap = k_t; - let a = x_2 + z_2; - let aa = a.square(); - let b = x_2 - z_2; - let bb = b.square(); - let e = aa - bb; - let c = x_3 + z_3; - let d = x_3 - z_3; - let da = d * a; - let cb = c * b; - let sum = da + cb; - let diff = da - cb; + a = x_2 + z_2; + aa = a.square(); + b = x_2 - z_2; + bb = b.square(); + e = aa - bb; + c = x_3 + z_3; + d = x_3 - z_3; + da = d * a; + cb = c * b; + sum = da + cb; + diff = da - cb; x_3 = sum.square(); z_3 = x_1 * diff.square(); x_2 = aa * bb; - let a24_e = mul_by_a24(e); + a24_e = mul_by_a24(e); z_2 = e * (aa + a24_e); } @@ -169,6 +195,18 @@ fn montgomery_ladder(k: &[u8; 32], u: &[u8; 32]) -> [u8; 32] { z_2_inv.zeroize(); result.zeroize(); swap.zeroize(); + a.zeroize(); + aa.zeroize(); + b.zeroize(); + bb.zeroize(); + e.zeroize(); + c.zeroize(); + d.zeroize(); + da.zeroize(); + cb.zeroize(); + sum.zeroize(); + diff.zeroize(); + a24_e.zeroize(); bytes } @@ -266,7 +304,7 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(alice_secret.diffie_hellman(&bob_pk)); + storage.write(alice_secret.diffie_hellman(&bob_pk).expect("정상 공개키는 Ok 여야 함")); let ptr = storage.assume_init_ref().as_bytes().as_ptr(); let pre = core::slice::from_raw_parts(ptr, 32); @@ -402,8 +440,8 @@ mod tests { let alice_pk = alice_sk.public_key(); let bob_pk = bob_sk.public_key(); - let alice_shared = alice_sk.diffie_hellman(&bob_pk); - let bob_shared = bob_sk.diffie_hellman(&alice_pk); + let alice_shared = alice_sk.diffie_hellman(&bob_pk).unwrap(); + let bob_shared = bob_sk.diffie_hellman(&alice_pk).unwrap(); assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); @@ -481,10 +519,10 @@ mod tests { for low_order in &low_order_points { let public = PublicKey::from_bytes(*low_order); - let shared = secret.diffie_hellman(&public); + let result = secret.diffie_hellman(&public); assert!( - shared.is_zero(), - "low-order point should produce zero shared secret" + matches!(result, Err(X25519Error::LowOrderPoint)), + "low-order point should be rejected" ); } } From d229da108e7135ce308c73d46b74f74719d8bf4a Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:31:35 +0900 Subject: [PATCH 21/65] =?UTF-8?q?cross-confirm.md=20=EC=97=90=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cross-confirm.md b/cross-confirm.md index f211dbb..5ba2526 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -13,7 +13,7 @@ - [ ] Ed448 - [X] Ed25519 - [ ] X448 -- [ ] X25519 +- [X] X25519 - [X] ML-DSA - [ ] ML-KEM - [X] RNG From 1011687443d9f366843ad59cb82d63afa53c8a97 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:37:20 +0900 Subject: [PATCH 22/65] =?UTF-8?q?x25519=20README=5FEN=20=EC=8B=A0=EA=B7=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- x25519/README_EN.md | 145 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 x25519/README_EN.md diff --git a/x25519/README_EN.md b/x25519/README_EN.md new file mode 100644 index 0000000..d8f4aec --- /dev/null +++ b/x25519/README_EN.md @@ -0,0 +1,145 @@ +# X25519 Key-Agreement Module + +X25519 is an Elliptic-Curve Diffie-Hellman (ECDH) function whose security rests on two things: the math of the Curve25519 Montgomery curve, and the guarantee that no secret ever influences the timing or memory-access pattern of the code that handles it. This document gives a technical account of both — the protocol the crate implements, and the assembly-level reasons the implementation does not leak the private scalar. + +--- + +## Cryptographic Surface + +The public API is intentionally small. Every entry point is stateless: one request, one datum, immediate erase. + +- `SecretKey::from_bytes([u8; 32]) -> SecretKey` +- `SecretKey::public_key(&self) -> PublicKey` +- `SecretKey::diffie_hellman(&self, &PublicKey) -> Result` +- `PublicKey::from_bytes([u8; 32]) -> PublicKey` — normalizes the high bit on construction. +- `generate_keypair(rng) -> (SecretKey, PublicKey)` +- `is_contributory(&SharedSecret) -> Choice` +- `SecretKey` / `SharedSecret` — each wraps `Secret<[u8; 32]>`; zeroized on drop. +- `PublicKey` — 32-byte little-endian u-coordinate. +- `X25519Error::LowOrderPoint` — returned when the agreement collapses to the all-zero secret. + +X25519 operates entirely on the **u-coordinate** (the x-coordinate of the Montgomery form), so the crate needs only two arithmetic layers: + +| Layer | Domain | Representation | +|------------------|--------------------------------------------|-----------------------------------------------------| +| `field` | `Fp`, `p = 2^255 - 19` | 5 × 51-bit limbs (radix-2^51), little-endian | +| ladder (`lib.rs`)| Curve25519, `v^2 = u^3 + 486662 u^2 + u` | projective `(X : Z)` per rung, `u = X/Z`; 32-byte scalar | + +There are no external dependencies (`constant-time` and `zeroize` are internal workspace crates), no allocator, and no `std` outside tests. The crate builds for the default bare-metal target `x86_64-unknown-none`. + +## Standard Conformance + +The implementation follows **RFC 7748** (X25519 over Curve25519). It is validated byte-for-byte against the RFC 7748 known-answer vectors: the two Section 5.2 single-call scalar/u-coordinate pairs, the Section 5.2 iterated test (after 1 and after 1000 rounds of folding the output back into the scalar and base), the base-point scalar multiplication, and the Section 6.1 Alice/Bob Diffie-Hellman example. + +Three validity rules bring the crate in line with the stricter reading of RFC 7748 Sections 5–6.1 and NIST SP 800-56A: + +- **Scalar clamping (RFC 7748 §5).** Before the ladder the 32-byte scalar is clamped: `k[0] &= 248` (clear the low 3 bits — cofactor), `k[31] &= 127` (clear the top bit), `k[31] |= 64` (set bit 254). Clamping fixes the bit length, which is what makes the fixed-length ladder safe to run in constant time. +- **u-coordinate masking (RFC 7748 §5).** The most-significant bit of the input u-coordinate is ignored. Field decoding drops bit 255 structurally (`limbs[4] = (hi >> 12) & MASK51`), and `PublicKey::from_bytes` additionally masks `bytes[31] &= 0x7f`, so the stored key is canonical and a peer's encoding has no byte-level malleability. +- **Contributory behaviour (RFC 7748 §6.1, SP 800-56A).** A low-order public key drives the agreement to the all-zero shared secret — a value the peer could have predicted. `diffie_hellman` detects this and returns `Err(X25519Error::LowOrderPoint)` rather than handing back a non-contributory secret. The all-zero test (`SharedSecret::is_zero`) accumulates every output byte with `OR` and compares once, with no early return, so the test itself is data-independent. `is_contributory` exposes the same check as a `Choice` for callers that prefer to branch on it themselves. + +## The Key-Agreement Protocol + +X25519 is `x25519(k, u)` — multiply the curve point whose u-coordinate is `u` by the clamped scalar `k`, and return the resulting u-coordinate. Both public-key derivation and shared-secret agreement are this one operation: + +```text +public_key() -> X25519(k, 9) base point u = 9 -> public key (public) +diffie_hellman() -> X25519(k, u_peer) peer public key -> shared secret (secret) +``` + +### The Montgomery ladder (RFC 7748 §5) + +```text +k = clamp(scalar) k : secret +x_1 = u +(x_2, z_2) = (1, 0) identity rung +(x_3, z_3) = (u, 1) +swap = 0 +for pos in 254 .. 0: fixed 255 iterations + k_t = bit `pos` of k k_t : secret + swap ^= k_t + cswap((x_2, z_2), (x_3, z_3), swap) constant-time conditional swap + swap = k_t + ... fixed ladder step: 5 mul, 4 square, mul_by_a24 (a24 = 121665), 8 add/sub ... +cswap((x_2, z_2), (x_3, z_3), swap) +return x_2 * z_2^(p - 2) z_2^(p-2) = z_2^{-1} (Fermat) +``` + +Because there is no random nonce, X25519 has no randomness-failure mode. The only secret is the scalar `k`, and it is consumed in exactly one place — the ladder. + +## Constant-Time Guarantee Rationale + +The single secret that flows through value-dependent code is the scalar `k`. It is consumed only inside the ladder, and only through the per-iteration conditional swap. The constant-time argument therefore reduces to showing that *the ladder contains no secret-dependent branch, no secret-dependent memory access, and no variable-latency instruction.* The guarantee holds at four levels. + +### 1. No secret-dependent branch — branchless conditional swap + +The ladder is a fixed 255-iteration loop. Every iteration performs the same two `cswap`s and the same fixed sequence of field operations, unconditionally; the scalar bit only ever decides *whether two already-computed rungs are exchanged*, and that decision is a branchless masked move, never an `if`: + +```rust +for pos in (0..255).rev() { + let k_t = (k[pos / 8] >> (pos % 8)) & 1; // pos, pos/8 are public loop indices + swap ^= k_t; + let choice = Choice::from_u8(swap); + FieldElement::conditional_swap(&mut x_2, &mut x_3, choice); // exchange or not + FieldElement::conditional_swap(&mut z_2, &mut z_3, choice); + swap = k_t; + // ... fixed ladder-step field ops, identical every iteration ... +} +``` + +`conditional_swap` swaps the five limbs of two field elements through `u64::swap`, the `CtSelOps::swap` primitive of the `constant-time` crate. Unlike the Ed25519 point select — which uses portable arithmetic masking, `a ^ (mask & (a ^ b))` — X25519's swap bottoms out in a hand-written inline-assembly conditional move, `ct_sel64`. The choice byte selects with a single data-independent-latency instruction and no jump: + +x86_64: +```asm +test {c:e}, {c:e} ; set ZF from the choice byte +cmovnz {r}, {a} ; conditional move — no branch +``` + +aarch64: +```asm +cmp {c:w}, #0 +csel {r}, {a}, {b}, ne ; conditional select — no branch +``` + +Both `asm!` blocks carry `options(nomem, nostack)`: the operands are register-only, so the move touches no memory and emits no `jcc` / `b.cc` that could read the scalar bit. A naive `if bit == 1 { swap }` would emit exactly such a secret-dependent conditional branch — observable through branch prediction and instruction-cache timing, and sufficient to recover the scalar one bit at a time. The crate never does this, and the running `swap ^= k_t; ... ; swap = k_t` idiom means the conditional move is the *only* place a scalar bit is ever consumed. + +### 2. No secret-dependent memory access + +The multiply is variable-base and table-free: the only point fed to the ladder is the caller's u-coordinate, and there is no precomputed table of base-point multiples indexed by secret bits. No load address depends on a secret, so no cache or TLB timing can vary with the key. The only indices in the hot loop — `pos`, `pos / 8`, `pos % 8` — are the public loop counter, so any bounds check the compiler inserts is a check on a public index, not on secret data. + +### 3. Data-independent field arithmetic + +- **Inversion** — the final `z_2^{-1}` — uses Fermat's little theorem, `z^(-1) = z^(p-2)`, evaluated by a fixed addition chain of squarings and multiplications (`invert`). The operation sequence is identical for every input; the crate never uses the extended Euclidean algorithm, whose iteration count depends on operand magnitude. +- **Multiplication** accumulates `64 × 64 -> 128`-bit partial products (`u128`) with the `2^255 ≡ 19` reduction fold (`mul_inner`). On x86_64 and aarch64 — the only two architectures the workspace supports — integer-multiply latency is independent of operand values, so the assumption is sound by construction rather than by hope. There is no `div` anywhere in the field code. +- **Reduction** — both the radix-2^51 carry chain (`carry_propagate`) and the final canonical reduction (`reduce`) — is straight-line code with a compile-time-fixed number of carry passes. `reduce`'s conditional subtraction of `p` is selected with an arithmetic mask derived from the borrow sign bit (`(s[4] >> 63)`), not a branch. No loop trip count depends on a value. +- **Equality** on field elements (`PartialEq`) reduces both operands and `OR`-accumulates the limb differences before a single comparison — no short-circuit — so even an internal field comparison is data-independent. + +### 4. Erasure is delegated to a barrier-backed primitive + +Where an optimizer barrier is genuinely required — wiping a secret so the compiler cannot elide the store — this crate does not rely on portable Rust. Constant-time *selection* is achieved with the `cmov` / `csel` primitives above; constant-time *erasure* is delegated to the `zeroize` crate, whose per-architecture `barrier/{x86_64,aarch64}.rs` issue real memory/compiler fences and volatile writes in inline assembly. The same discipline reaches inside the swap itself: `CtSelOps::swap` holds a one-limb plaintext copy in a stack temporary, then volatile-zeroes the whole slot, raises a `compiler_fence(SeqCst)` against store reordering, and routes the slot through `black_box` so the scratch copy cannot be left alive in a register (CWE-316). + +## Secret Lifecycle and Zeroization + +The daemon's core invariant is that a secret never outlives the transaction that used it. The crate enforces this structurally: + +- `SecretKey` holds the scalar in `Secret<[u8; 32]>`; its `Drop` performs a volatile, barrier-fenced wipe. `SharedSecret` is wrapped identically. +- `x25519` copies the scalar into a `Secret` before clamping, so the clamped form is wiped on scope exit and the caller's key is never mutated in place. +- `montgomery_ladder` explicitly zeroizes every working field element before returning — the two rungs `(x_2, z_2)` and `(x_3, z_3)`, the base `x_1`, the inverse `z_2_inv`, the result, the `swap` byte, and all twelve per-iteration intermediates (`a, aa, b, bb, e, c, d, da, cb, sum, diff, a24_e`). The intermediates are declared once outside the loop and reassigned each round, so only the final round's values reach the wipe and none survive on the stack. +- `diffie_hellman` zeroizes the raw output buffer the instant it has been copied into the `Secret`-backed `SharedSecret`. + +Together these make agreement stateless in the strongest sense: after `diffie_hellman` returns, the only secret-derived bytes still in memory are the scalar inside the caller's `SecretKey` and the secret inside the returned `SharedSecret`, both barrier-wiped on drop. + +--- + +## Verification Summary + +| Property | How it is checked | +|-----------------------------------|----------------------------------------------------------------------------| +| RFC 7748 correctness | Byte-exact KAT: §5.2 vectors, §5.2 iterated test (1 and 1000), §6.1 example | +| Low-order point rejected | All-zero shared secret returns `Err(X25519Error::LowOrderPoint)` | +| u-coordinate canonicalized | Bit 255 masked on field decode and in `PublicKey::from_bytes` | +| Branchless secret scalar multiply | `cswap` lowers to `cmovnz` (x86_64) / `csel` (aarch64) via `ct_sel64`, no conditional branch | +| Data-independent inversion | Fixed Fermat addition chain — no extended-Euclid, no `div` | +| Lint gate | `cargo clippy --all-targets --all-features -- -D warnings` clean | +| Bare-metal posture | Builds for `x86_64-unknown-none`, `no_std`, no `alloc` | + +As remaining hardening, explicitly enabling the aarch64 DIT bit and the x86 DOITM mode — which pin data-independent timing at the ISA level for the `cmov` / `csel` instructions — is left for future work, mirroring the same open item tracked in the `constant-time` crate. From 9f02ae4f7675178f19d68a142dc4759d65e1d6a9 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:18:06 +0900 Subject: [PATCH 23/65] =?UTF-8?q?x25519=20README=20=ED=95=9C=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=ED=8C=90=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README_EN 영문판 번역 - 양쪽 언어 배지 상호 링크 추가 --- x25519/README.md | 147 ++++++++++++++++++++++++++++++++++++++++++++ x25519/README_EN.md | 2 + 2 files changed, 149 insertions(+) create mode 100644 x25519/README.md diff --git a/x25519/README.md b/x25519/README.md new file mode 100644 index 0000000..0dbbb82 --- /dev/null +++ b/x25519/README.md @@ -0,0 +1,147 @@ +# X25519 키 교환 모듈 + +[![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) + +X25519는 두 가지에 보안을 의존하는 타원곡선 디피-헬만(ECDH) 함수입니다. Curve25519 몽고메리 곡선의 수학, 그리고 비밀이 그 비밀을 다루는 코드의 타이밍이나 메모리 접근 패턴에 영향을 주지 않는다는 보장입니다. 이 문서는 그 둘을 기술적으로 설명합니다. 크레이트가 구현하는 프로토콜, 그리고 구현이 비밀 스칼라를 누출하지 않는 어셈블리 차원의 근거입니다. + +--- + +## 암호 인터페이스 + +공개 API는 의도적으로 작습니다. 모든 진입점은 stateless입니다. 한 요청, 한 데이터, 즉시 소거. + +- `SecretKey::from_bytes([u8; 32]) -> SecretKey` +- `SecretKey::public_key(&self) -> PublicKey` +- `SecretKey::diffie_hellman(&self, &PublicKey) -> Result` +- `PublicKey::from_bytes([u8; 32]) -> PublicKey`: 생성 시 최상위 비트를 정규화합니다. +- `generate_keypair(rng) -> (SecretKey, PublicKey)` +- `is_contributory(&SharedSecret) -> Choice` +- `SecretKey` / `SharedSecret`: 각각 `Secret<[u8; 32]>`를 감싸며 Drop 시 소거됩니다. +- `PublicKey`: 32바이트 리틀 엔디언 u좌표. +- `X25519Error::LowOrderPoint`: 합의가 all-zero 비밀로 붕괴할 때 반환됩니다. + +X25519는 전적으로 **u좌표**(몽고메리 형식의 x좌표)에서 동작하므로, 크레이트에는 두 산술 계층만 필요합니다. + +| 계층 | 영역 | 표현 | +|-------------------|--------------------------------------------|-----------------------------------------------------| +| `field` | `Fp`, `p = 2^255 - 19` | 5 × 51비트 limb (radix-2^51), 리틀 엔디언 | +| ladder (`lib.rs`) | Curve25519, `v^2 = u^3 + 486662 u^2 + u` | rung마다 사영 좌표 `(X : Z)`, `u = X/Z`, 32바이트 스칼라 | + +외부 의존성은 없습니다(`constant-time`, `zeroize`는 내부 워크스페이스 크레이트). 할당자도 없고, 테스트 외에는 `std`도 없습니다. 기본 베어메탈 타겟 `x86_64-unknown-none`으로 빌드됩니다. + +## 표준 준수 + +구현은 **RFC 7748**(Curve25519 위의 X25519)을 따릅니다. RFC 7748 KAT에 대해 바이트 단위로 검증됩니다. 5.2절의 단일 호출 스칼라/u좌표 쌍 두 개, 5.2절 반복 테스트(출력을 스칼라와 베이스에 되먹이는 1회 및 1000회 후), 기저점 스칼라 곱, 6.1절 Alice/Bob 디피-헬만 예제입니다. + +세 가지 유효성 규칙으로 RFC 7748 5 ~ 6.1절과 NIST SP 800-56A의 엄격한 해석에 부합합니다. + +- **스칼라 클램핑 (RFC 7748 §5)**: ladder 전에 32바이트 스칼라를 클램핑합니다. `k[0] &= 248`(하위 3비트 클리어, cofactor), `k[31] &= 127`(최상위 비트 클리어), `k[31] |= 64`(비트 254 설정). 클램핑은 비트 길이를 고정하며, 이것이 고정 길이 ladder를 상수 시간으로 안전하게 돌릴 수 있게 만듭니다. +- **u좌표 마스킹 (RFC 7748 §5)**: 입력 u좌표의 최상위 비트는 무시됩니다. 필드 디코드가 구조적으로 비트 255를 버리고(`limbs[4] = (hi >> 12) & MASK51`), `PublicKey::from_bytes`가 추가로 `bytes[31] &= 0x7f`를 마스킹하므로, 저장된 키는 정규형이고 상대의 인코딩에 바이트 단위 가변성이 없습니다. +- **기여성 동작 (RFC 7748 §6.1, SP 800-56A)**: 저위수 공개키는 합의를 all-zero 공유 비밀로 몰아갑니다. 이는 상대가 미리 알 수 있는 값입니다. `diffie_hellman`은 이를 탐지해 비기여 비밀을 돌려주는 대신 `Err(X25519Error::LowOrderPoint)`을 반환합니다. all-zero 검사(`SharedSecret::is_zero`)는 모든 출력 바이트를 `OR`로 누적해 한 번만 비교하며 조기 반환이 없으므로, 검사 자체가 데이터 독립적입니다. `is_contributory`는 직접 분기하길 원하는 호출자를 위해 같은 검사를 `Choice`로 노출합니다. + +## 키 교환 프로토콜 + +X25519는 `x25519(k, u)`입니다. u좌표가 `u`인 곡선 점에 클램핑된 스칼라 `k`를 곱하고, 결과 u좌표를 반환합니다. 공개키 유도와 공유 비밀 합의 모두 이 한 연산입니다. + +```text +public_key() -> X25519(k, 9) 기저점 u = 9 -> 공개키 (공개) +diffie_hellman() -> X25519(k, u_peer) 상대 공개키 -> 공유 비밀 (비밀) +``` + +### 몽고메리 ladder (RFC 7748 §5) + +```text +k = clamp(scalar) k : 비밀 +x_1 = u +(x_2, z_2) = (1, 0) 항등 rung +(x_3, z_3) = (u, 1) +swap = 0 +for pos in 254 .. 0: 고정 255회 반복 + k_t = k 의 pos 번째 비트 k_t : 비밀 + swap ^= k_t + cswap((x_2, z_2), (x_3, z_3), swap) 상수-시간 조건부 swap + swap = k_t + ... 고정 ladder 스텝: 5 mul, 4 square, mul_by_a24 (a24 = 121665), 8 add/sub ... +cswap((x_2, z_2), (x_3, z_3), swap) +return x_2 * z_2^(p - 2) z_2^(p-2) = z_2^{-1} (페르마) +``` + +난수 nonce가 없으므로 X25519에는 난수 실패 모드가 없습니다. 유일한 비밀은 스칼라 `k`이며, 정확히 한 곳, ladder에서 소비됩니다. + +## 상수-시간 보장 근거 + +값에 의존하는 코드를 거치는 비밀은 스칼라 `k` 하나뿐입니다. 오직 ladder 안에서, 그리고 반복마다의 조건부 swap을 통해서만 소비됩니다. 따라서 상수-시간 논증은 *ladder에 비밀 의존 분기, 비밀 의존 메모리 접근, 가변 지연 명령이 없음*을 보이는 것으로 귀결됩니다. 보장은 네 겹입니다. + +### 1. 비밀 의존 분기 없음 (분기 없는 조건부 swap) + +ladder는 고정된 255회 반복 루프입니다. 매 반복은 같은 두 `cswap`과 같은 고정 필드 연산열을 무조건 수행합니다. 스칼라 비트는 오직 *이미 계산된 두 rung을 교환할지 여부*만 결정하며, 그 결정은 `if`가 아니라 분기 없는 마스크 이동입니다. + +```rust +for pos in (0..255).rev() { + let k_t = (k[pos / 8] >> (pos % 8)) & 1; // pos, pos/8 은 공개 루프 인덱스 + swap ^= k_t; + let choice = Choice::from_u8(swap); + FieldElement::conditional_swap(&mut x_2, &mut x_3, choice); // 교환하거나 하지 않음 + FieldElement::conditional_swap(&mut z_2, &mut z_3, choice); + swap = k_t; + // ... 고정 ladder-스텝 필드 연산, 매 반복 동일 ... +} +``` + +`conditional_swap`은 두 필드 원소의 다섯 limb를 `constant-time` 크레이트의 `CtSelOps::swap` 프리미티브인 `u64::swap`으로 교환합니다. 포터블 산술 마스킹(`a ^ (mask & (a ^ b))`)을 쓰는 Ed25519 점 선택과 달리, X25519의 swap은 손으로 작성한 인-라인 어셈블리 조건 이동 `ct_sel64`로 귀착됩니다. choice 바이트는 단일 데이터 독립 지연 명령으로, 점프 없이 선택합니다. + +x86_64: +```asm +test {c:e}, {c:e} ; choice 바이트로 ZF 설정 +cmovnz {r}, {a} ; 조건 이동, 분기 없음 +``` + +aarch64: +```asm +cmp {c:w}, #0 +csel {r}, {a}, {b}, ne ; 조건 선택, 분기 없음 +``` + +두 `asm!` 블록 모두 `options(nomem, nostack)`을 답니다. 피연산자가 레지스터 전용이므로 이동은 메모리에 접근하지 않고, 스칼라 비트를 읽을 `jcc` / `b.cc`를 방출하지 않습니다. 순진한 `if bit == 1 { swap }`은 정확히 그런 비밀 의존 조건 분기를 방출했을 것입니다. 분기 예측과 명령 캐시 타이밍으로 관측 가능하고, 스칼라를 한 번에 한 비트씩 복원하기에 충분합니다. 크레이트는 결코 이렇게 하지 않으며, `swap ^= k_t; ... ; swap = k_t` 관용구는 조건 이동이 스칼라 비트를 소비하는 *유일한* 곳임을 의미합니다. + +### 2. 비밀 의존 메모리 접근 없음 + +곱셈은 가변 베이스이며 테이블이 없습니다. ladder에 들어가는 유일한 점은 호출자의 u좌표이고, 비밀 비트로 인덱싱되는 기저점 배수의 사전 계산 테이블이 없습니다. 로드 주소가 비밀에 의존하지 않으므로 캐시나 TLB 타이밍이 키에 따라 달라질 수 없습니다. 핫 루프의 유일한 인덱스 `pos`, `pos / 8`, `pos % 8`은 공개 루프 카운터이므로, 컴파일러가 삽입하는 경계 검사도 비밀 데이터가 아니라 공개 인덱스에 대한 검사입니다. + +### 3. 데이터 독립 필드 산술 + +- **역원**(최종 `z_2^{-1}`)은 페르마 소정리 `z^(-1) = z^(p-2)`를 사용하며, 제곱과 곱셈의 고정 addition chain(`invert`)으로 평가합니다. 연산 순서는 모든 입력에 대해 동일합니다. 피연산자 크기에 반복 횟수가 의존하는 확장 유클리드 알고리즘은 결코 쓰지 않습니다. +- **곱셈**은 `2^255 ≡ 19` 리덕션 폴드와 함께 `64 × 64 -> 128`비트 부분곱(`u128`)을 누적합니다(`mul_inner`). 워크스페이스가 지원하는 유일한 두 아키텍처인 x86_64와 aarch64에서 정수 곱 지연은 피연산자 값과 무관하므로, 그 가정은 희망이 아니라 설계상 성립합니다. 필드 코드 어디에도 `div`는 없습니다. +- **리덕션**은 radix-2^51 캐리 체인(`carry_propagate`)과 최종 정규 리덕션(`reduce`) 모두 컴파일타임에 고정된 횟수의 직선형 코드입니다. `reduce`의 조건부 `p` 뺄셈은 borrow 부호 비트에서 유도한 산술 마스크(`(s[4] >> 63)`)로 선택하며 분기가 아닙니다. 어떤 값에도 루프 횟수가 의존하지 않습니다. +- **상등 비교**(필드 원소 `PartialEq`)는 두 피연산자를 리덕션한 뒤 limb 차이를 `OR`로 누적해 한 번 비교합니다. 단축 평가가 없으므로 내부 필드 비교조차 데이터 독립적입니다. + +### 4. 소거는 배리어 기반 프리미티브에 위임 + +최적화 배리어가 정말로 필요한 곳, 즉 컴파일러가 store를 제거하지 못하게 비밀을 지우는 경우에는 이 크레이트가 포터블 Rust에 기대지 않습니다. 상수-시간 *선택*은 위의 `cmov` / `csel` 프리미티브로 달성하고, 상수-시간 *소거*는 per-arch `barrier/{x86_64,aarch64}.rs`가 실제 메모리·컴파일러 펜스와 volatile write를 인-라인 어셈블리로 발행하는 `zeroize` 크레이트에 위임합니다. 같은 규율이 swap 내부에도 미칩니다. `CtSelOps::swap`은 한 limb의 평문 사본을 스택 임시값에 두고, 슬롯 전체를 volatile 0으로 덮은 뒤, store 재정렬을 막는 `compiler_fence(SeqCst)`를 올리고, 그 슬롯을 `black_box`로 통과시켜 스크래치 사본이 레지스터에 살아남지 못하게 합니다(CWE-316). + +## 비밀 수명주기와 소거 + +데몬의 핵심 불변식은 비밀이 그것을 사용한 트랜잭션보다 오래 살아남지 않는다는 것입니다. 크레이트는 이를 구조적으로 강제합니다. + +- `SecretKey`는 스칼라를 `Secret<[u8; 32]>`로 보유하며, Drop이 volatile하고 배리어로 펜싱된 소거를 수행합니다. `SharedSecret`도 동일하게 감쌉니다. +- `x25519`는 클램핑 전에 스칼라를 `Secret`으로 복사하므로, 클램핑된 형태는 스코프 종료 시 소거되고 호출자의 키는 제자리에서 변경되지 않습니다. +- `montgomery_ladder`는 반환 전에 모든 작업 필드 원소를 명시적으로 소거합니다. 두 rung `(x_2, z_2)`와 `(x_3, z_3)`, 베이스 `x_1`, 역원 `z_2_inv`, 결과, `swap` 바이트, 그리고 반복마다의 중간값 열두 개(`a, aa, b, bb, e, c, d, da, cb, sum, diff, a24_e`)입니다. 중간값은 루프 밖에서 한 번 선언해 매 회 재대입하므로, 마지막 회차의 값만 소거에 도달하고 스택에 남는 것이 없습니다. +- `diffie_hellman`은 원시 출력 버퍼를 `Secret` 기반 `SharedSecret`에 복사한 즉시 소거합니다. + +이로써 합의는 가장 강한 의미에서 stateless가 됩니다. `diffie_hellman`이 반환된 뒤 메모리에 남는 비밀 파생 바이트는 호출자의 `SecretKey` 안 스칼라와 반환된 `SharedSecret` 안 비밀뿐이며, 둘 다 Drop 시 배리어로 소거됩니다. + +--- + +## 검증 요약 + +| 속성 | 검사 방법 | +|---------------------------|-------------------------------------------------------------------| +| RFC 7748 정확성 | 바이트 일치 KAT: §5.2 벡터, §5.2 반복 테스트(1회 및 1000회), §6.1 예제 | +| 저위수 점 거부 | all-zero 공유 비밀은 `Err(X25519Error::LowOrderPoint)` 반환 | +| u좌표 정규화 | 필드 디코드와 `PublicKey::from_bytes`에서 비트 255 마스킹 | +| 분기 없는 비밀 스칼라 곱 | `cswap`이 `ct_sel64`를 통해 `cmovnz`(x86_64) / `csel`(aarch64)로 낮아지며 조건 분기 없음 | +| 데이터 독립 역원 | 고정 페르마 addition chain, 확장 유클리드 없음, `div` 없음 | +| 린트 게이트 | `cargo clippy --all-targets --all-features -- -D warnings` 무경고 | +| 베어메탈 자세 | `x86_64-unknown-none` 빌드, `no_std`, `alloc` 없음 | + +남은 하드닝으로, `cmov` / `csel` 명령의 데이터 독립 시간을 ISA 차원에서 고정하는 aarch64 DIT 비트와 x86 DOITM 모드를 명시적으로 켜는 작업은 향후 과제로 남으며, 이는 `constant-time` 크레이트에 기록된 동일한 미해결 항목을 따릅니다. diff --git a/x25519/README_EN.md b/x25519/README_EN.md index d8f4aec..82b862e 100644 --- a/x25519/README_EN.md +++ b/x25519/README_EN.md @@ -1,5 +1,7 @@ # X25519 Key-Agreement Module +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) + X25519 is an Elliptic-Curve Diffie-Hellman (ECDH) function whose security rests on two things: the math of the Curve25519 Montgomery curve, and the guarantee that no secret ever influences the timing or memory-access pattern of the code that handles it. This document gives a technical account of both — the protocol the crate implements, and the assembly-level reasons the implementation does not leak the private scalar. --- From 17c525fcae1d77407a13e6e72028ddce5d4906ab Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:54:55 +0900 Subject: [PATCH 24/65] =?UTF-8?q?blake=20=EB=B3=B4=EC=95=88=20=EA=B2=80?= =?UTF-8?q?=ED=86=A0=20=ED=9B=84=EC=86=8D=20=EC=88=98=EC=A0=95=20-=20BLAKE?= =?UTF-8?q?3=20new=5Fkeyed=20=ED=82=A4=20=EC=9B=8C=EB=93=9C=20=ED=8F=89?= =?UTF-8?q?=EB=AC=B8=20=EC=9E=94=EB=A5=98=20=EC=A0=9C=EA=B1=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=A6=89=EC=8B=9C=20Secret=20=EB=9E=98=ED=95=91=20?= =?UTF-8?q?-=20finalize=20=EC=B6=9C=EB=A0=A5=20=EA=B8=B8=EC=9D=B4=20?= =?UTF-8?q?=EB=AF=B8=EB=8B=AC=20=EC=8B=9C=20=EC=9E=94=EC=97=AC=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=A0=9C=EC=8A=A4=ED=8A=B8=20=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=86=8C=EA=B1=B0=20-=20=ED=82=A4=EB=93=9C=20BLAKE?= =?UTF-8?q?2b=20=EB=B0=8F=20blake2b=5Flong=20KAT=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20=ED=8C=A8=EB=8B=89=20=EB=8F=84=EC=8A=A4=ED=8A=B8=EB=A7=81=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=B0=8F=20RFC=209106=20=EC=A0=88=20?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blake/src/blake2b.rs | 8 ++-- blake/src/blake3.rs | 23 ++++++++---- blake/src/lib.rs | 89 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/blake/src/blake2b.rs b/blake/src/blake2b.rs index 447be2e..219f505 100644 --- a/blake/src/blake2b.rs +++ b/blake/src/blake2b.rs @@ -45,7 +45,7 @@ pub struct Blake2b { impl Blake2b { /// 비키드(plain) BLAKE2b 인스턴스를 생성하는 함수입니다. /// - /// # Errors + /// # Panics /// `hash_len`이 1..=64 범위를 벗어나거나 SecureBuffer 할당 실패 시 패닉. pub fn new(hash_len: usize) -> Self { assert!((1..=64).contains(&hash_len), "hash_len must be 1..=64"); @@ -57,7 +57,7 @@ impl Blake2b { /// # Security Note /// 키는 128바이트 패딩 후 첫 번째 블록으로 처리됩니다. /// - /// # Errors + /// # Panics /// `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"); @@ -135,9 +135,11 @@ impl Blake2b { let out_slice = out.as_mut_slice(); let mut pos = 0; for word in self.h.expose().iter() { - let bytes = word.to_le_bytes(); + let mut bytes = word.to_le_bytes(); let take = (self.hash_len - pos).min(8); out_slice[pos..pos + take].copy_from_slice(&bytes[..take]); + // bytes[take..] 는 출력 길이 미달 시 출력되지 않은 h 잔여 바이트이므로 소거 + bytes.zeroize(); pos += take; if pos >= self.hash_len { break; diff --git a/blake/src/blake3.rs b/blake/src/blake3.rs index 42e8010..f6a60c6 100644 --- a/blake/src/blake3.rs +++ b/blake/src/blake3.rs @@ -24,7 +24,7 @@ pub const OUT_LEN: usize = 32; /// /// # Security Note /// 키워드와 CV 스택은 `Secret`으로 보호되어 Drop 시 자동 소거됩니다. -/// 키드 모드(`new_keyed`)는 키를 IV로 변환하므로 키 바이트가 스택에 노출되지 않습니다. +/// 키드 모드(`new_keyed`)는 키 워드를 즉시 `Secret`으로 감싸 평문 사본을 남기지 않습니다. pub struct Blake3 { chunk_state: ChunkState, key_words: Secret<[u32; 8]>, @@ -48,15 +48,20 @@ impl Blake3 { /// 키드 BLAKE3 인스턴스를 생성하는 함수입니다. /// /// # Arguments - /// `key` — 정확히 32바이트 + /// - `key`: 정확히 32바이트 키 + /// + /// # Security Note + /// 키를 워드로 변환한 중간값은 생성 즉시 `Secret`으로 감싸지며 평문 + /// 사본이 함수 프레임에 잔류하지 않습니다. 키는 KEYED_HASH 플래그와 함께 + /// IV 를 대체하는 초기 체이닝 값으로만 사용됩니다. 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: Secret::new(key_words), + chunk_state: ChunkState::new(key_words.expose(), 0, KEYED_HASH), cv_stack: Secret::new([[0u32; 8]; 54]), cv_stack_len: 0, flags: KEYED_HASH, + key_words, } } @@ -285,9 +290,11 @@ impl Output { self.flags | ROOT, ); for word in words.expose().iter() { - let bytes = word.to_le_bytes(); + let mut bytes = word.to_le_bytes(); let take = (out.len() - pos).min(4); out[pos..pos + take].copy_from_slice(&bytes[..take]); + // bytes[take..] 는 출력에 쓰이지 않은 루트 압축 상태 잔여분이므로 소거 + bytes.zeroize(); pos += take; if pos >= out.len() { return; @@ -403,9 +410,9 @@ fn words_from_le_bytes_64(bytes: &[u8; BLOCK_LEN]) -> Secret<[u32; 16]> { })) } -fn words_from_le_bytes_32(bytes: &[u8; 32]) -> [u32; 8] { - core::array::from_fn(|i| { +fn words_from_le_bytes_32(bytes: &[u8; 32]) -> Secret<[u32; 8]> { + Secret::new(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/blake/src/lib.rs b/blake/src/lib.rs index bd02e4f..4e52ba6 100644 --- a/blake/src/lib.rs +++ b/blake/src/lib.rs @@ -156,7 +156,7 @@ impl CtEqOps for SecureBuffer { } } -/// RFC 9106 Section 3.2에서 정의된 가변 출력 BLAKE2b 함수입니다 (H'). +/// RFC 9106 Section 3.3에서 정의된 가변 출력 BLAKE2b 함수입니다 (H'). /// /// Argon2id 블록 초기화 및 최종 태그 생성에 사용됩니다. /// @@ -252,6 +252,59 @@ mod tests { assert_eq!(digest.as_slice(), &expected); } + // BLAKE2b keyed(MAC) 공식 KAT (key 00..3f, RFC 7693 reference blake2-kat) + // 키드 경로(new_keyed) 의 byte-identity 회귀 가드 단일 키 블록 케이스 + #[test] + fn blake2b_keyed_empty() { + let key: [u8; 64] = core::array::from_fn(|i| i as u8); + let h = Blake2b::new_keyed(64, &key); + let digest = h.finalize().unwrap(); + let expected = [ + 0x10, 0xeb, 0xb6, 0x77, 0x00, 0xb1, 0x86, 0x8e, 0xfb, 0x44, 0x17, 0x98, 0x7a, 0xcf, + 0x46, 0x90, 0xae, 0x9d, 0x97, 0x2f, 0xb7, 0xa5, 0x90, 0xc2, 0xf0, 0x28, 0x71, 0x79, + 0x9a, 0xaa, 0x47, 0x86, 0xb5, 0xe9, 0x96, 0xe8, 0xf0, 0xf4, 0xeb, 0x98, 0x1f, 0xc2, + 0x14, 0xb0, 0x05, 0xf4, 0x2d, 0x2f, 0xf4, 0x23, 0x34, 0x99, 0x39, 0x16, 0x53, 0xdf, + 0x7a, 0xef, 0xcb, 0xc1, 0x3f, 0xc5, 0x15, 0x68, + ]; + assert_eq!(digest.as_slice(), &expected); + } + + // 키 블록 직후 1바이트 메시지 키 블록(t=128) 비-최종 압축 + 메시지 블록(t=129) 최종 압축 경로 + #[test] + fn blake2b_keyed_one_byte() { + let key: [u8; 64] = core::array::from_fn(|i| i as u8); + let mut h = Blake2b::new_keyed(64, &key); + h.update(&[0u8]); + let digest = h.finalize().unwrap(); + let expected = [ + 0x96, 0x1f, 0x6d, 0xd1, 0xe4, 0xdd, 0x30, 0xf6, 0x39, 0x01, 0x69, 0x0c, 0x51, 0x2e, + 0x78, 0xe4, 0xb4, 0x5e, 0x47, 0x42, 0xed, 0x19, 0x7c, 0x3c, 0x5e, 0x45, 0xc5, 0x49, + 0xfd, 0x25, 0xf2, 0xe4, 0x18, 0x7b, 0x0b, 0xc9, 0xfe, 0x30, 0x49, 0x2b, 0x16, 0xb0, + 0xd0, 0xbc, 0x4e, 0xf9, 0xb0, 0xf3, 0x4c, 0x70, 0x03, 0xfa, 0xc0, 0x9a, 0x5e, 0xf1, + 0x53, 0x2e, 0x69, 0x43, 0x02, 0x34, 0xce, 0xbd, + ]; + assert_eq!(digest.as_slice(), &expected); + } + + // 키(32B) + 200B 메시지 키 블록 경계를 넘는 다중 블록 키드 카운터 회귀 (dlen=32) + #[test] + fn blake2b_keyed_multiblock() { + let key: [u8; 32] = core::array::from_fn(|i| i as u8); + let mut msg = [0u8; 200]; + for (i, b) in msg.iter_mut().enumerate() { + *b = ((i * 7 + 1) % 256) as u8; + } + let mut h = Blake2b::new_keyed(32, &key); + h.update(&msg); + let digest = h.finalize().unwrap(); + let expected = [ + 0x98, 0x08, 0x23, 0x5b, 0x8b, 0xc5, 0x94, 0xf9, 0x77, 0x4c, 0x06, 0xf4, 0xbf, 0x16, + 0xe2, 0xbd, 0xd2, 0x0e, 0x51, 0x26, 0x77, 0x05, 0xee, 0x29, 0x29, 0x83, 0x5c, 0x91, + 0x4d, 0x9b, 0xce, 0xaa, + ]; + assert_eq!(digest.as_slice(), &expected); + } + // BLAKE3 테스트 벡터 #[test] fn blake3_empty() { @@ -392,11 +445,39 @@ mod tests { assert_eq!(digest.as_slice(), &expected); } - // blake2b_long 테스트 + // blake2b_long(RFC 9106 H') byte-identity KAT + // out_len=80 r=1 last_len=48 (V_1 32B + V_2[..48]) 체이닝 진입 경로 #[test] - fn blake2b_long_80() { + fn blake2b_long_kat_80() { let out = blake2b_long(b"test", 80).unwrap(); - assert_eq!(out.as_slice().len(), 80); + let expected = [ + 0xf2, 0xe5, 0x95, 0xe6, 0xc0, 0xdc, 0x15, 0x97, 0xb2, 0x31, 0xa5, 0x65, 0xc0, 0xdb, + 0xf1, 0x8f, 0x02, 0x1a, 0x36, 0xed, 0x3c, 0x7d, 0x72, 0x98, 0xc3, 0x6c, 0xc0, 0xe7, + 0xe7, 0x90, 0xe2, 0x45, 0x6d, 0x1e, 0xc4, 0x5f, 0x62, 0x2c, 0xe9, 0x73, 0x78, 0x2e, + 0x02, 0x1f, 0x35, 0x46, 0xd1, 0xe8, 0xec, 0xcd, 0x41, 0x52, 0xe1, 0x14, 0x16, 0x36, + 0xce, 0xae, 0x73, 0x23, 0x29, 0x9f, 0x54, 0xad, 0xf4, 0xcd, 0x9a, 0xaf, 0x1a, 0xcf, + 0x03, 0x9d, 0xe3, 0xe0, 0x20, 0x7c, 0x73, 0x49, 0x8f, 0x37, + ]; + assert_eq!(out.as_slice(), &expected); + } + + // out_len=128 r=2 (V_1, V_2 각 32B + V_3 64B) 루프 1회 경로 + #[test] + fn blake2b_long_kat_128() { + let out = blake2b_long(b"threat-input", 128).unwrap(); + let expected = [ + 0x44, 0x95, 0x9e, 0x1c, 0x28, 0x60, 0x77, 0x81, 0x89, 0xaf, 0xeb, 0x91, 0x87, 0xe9, + 0x51, 0x5c, 0xe8, 0x4f, 0x88, 0xa6, 0x94, 0xab, 0xe3, 0xfa, 0x9c, 0x26, 0x83, 0x99, + 0xf3, 0x7e, 0x9d, 0x3f, 0x05, 0x7c, 0x51, 0x61, 0x15, 0x39, 0xd5, 0x32, 0xe1, 0x3d, + 0xde, 0xf4, 0xa1, 0xc7, 0xac, 0xde, 0x9a, 0x92, 0x8e, 0xb7, 0x07, 0x26, 0xb4, 0xd0, + 0xb8, 0xaf, 0xeb, 0x2b, 0xf0, 0x2a, 0xcd, 0x48, 0xa8, 0x4a, 0x38, 0xa9, 0xa6, 0x1a, + 0x77, 0x0b, 0x19, 0x88, 0xfe, 0x8c, 0x4c, 0xe7, 0x7e, 0x51, 0x39, 0x43, 0xe1, 0x58, + 0x50, 0x4b, 0x59, 0x23, 0x24, 0x70, 0x73, 0xfe, 0xe8, 0xf7, 0xc2, 0xca, 0x94, 0x80, + 0x90, 0xf5, 0x4a, 0xf9, 0x37, 0x1a, 0xd8, 0x6c, 0xd4, 0xfe, 0xc1, 0x93, 0xbb, 0x94, + 0x75, 0x7b, 0x17, 0xf3, 0xa9, 0x08, 0xb3, 0x76, 0xe7, 0x4b, 0x98, 0xbe, 0x7c, 0x4a, + 0x39, 0x87, + ]; + assert_eq!(out.as_slice(), &expected); } // 상수-시간 비교 테스트 From 54a02e21d942c621af6381baea4bbfa4a01c4d17 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:08:27 +0900 Subject: [PATCH 25/65] =?UTF-8?q?blake=20README=20=ED=95=9C=EC=98=81?= =?UTF-8?q?=ED=8C=90=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blake/README.md | 139 +++++++++++++++++++++++++++++++++++++++++++++ blake/README_EN.md | 139 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 blake/README.md create mode 100644 blake/README_EN.md diff --git a/blake/README.md b/blake/README.md new file mode 100644 index 0000000..ee2e463 --- /dev/null +++ b/blake/README.md @@ -0,0 +1,139 @@ +# BLAKE 모듈 + +[![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) + +RFC 7693 BLAKE2b 해시·MAC, BLAKE3 해시·키드 해시·XOF, RFC 9106 가변 길이 해시 `H'`를 외부 의존성 없이 `no_std` 순수 Rust로 구현한 크레이트입니다. 이 문서는 기능 명세와 보안 설계 근거, 표준 부합 근거, 그리고 발견한 문제와 조치를 기술합니다. + +--- + +## 구현된 기능 + +| 알고리즘 | 표준 | 출력 | 블록 크기 | 내부 워드 | 라운드 | 키드 모드 | +|--------------|-------------|--------------|------------------|-------|-----|------------------------| +| `Blake2b` | RFC 7693 | 1..=64바이트 | 128바이트 | `u64` | 12 | `new_keyed` (1..=64 B 키) | +| `Blake3` | BLAKE3 명세 | 32바이트 / XOF | 64 B (1024 B 청크) | `u32` | 7 | `new_keyed` (32 B 키) | +| `blake2b_long` | RFC 9106 §3.3 | 1..=1024바이트 | (BLAKE2b 코어) | `u64` | 12 | 없음 | + +공개 API는 `Blake2b`·`Blake3` 타입, 자유 함수 `blake2b_long`, `SecureBuffer` 컨테이너, `ct_eq_slice` 비교 헬퍼로 구성됩니다. + +- `Blake2b`: `new(hash_len)` / `new_keyed(hash_len, key)` -> `update(&[u8])` (임의 횟수) -> `finalize(self) -> Result`의 스트리밍 인터페이스. `finalize`는 `self`를 소비하므로 인스턴스 재사용이 타입 차원에서 차단됩니다. 키드 생성자는 키를 128바이트 0-패딩 첫 블록으로 처리하여 BLAKE2b MAC을 제공합니다. +- `Blake3`: `new()` / `new_keyed(&[u8; 32])` -> `update(&[u8])` -> `finalize(self)`(32바이트 출력) 또는 `finalize_xof(self, out_len)`(`MAX_OUTPUT_LEN`까지 임의 길이)의 스트리밍 인터페이스. 내부 머클 트리는 고정 깊이 54의 체이닝 값 스택으로 구동됩니다. +- `blake2b_long`: Argon2id 블록 초기화와 최종 태그 생성에 쓰이는 RFC 9106 `H'` 구성. `out_len <= 64`이면 단일 `BLAKE2b(LE32(out_len) || input)`이고, 더 큰 출력은 64바이트 다이제스트를 체이닝하며 32바이트 프리픽스와 마지막 `(out_len - 32r)`바이트 블록을 방출합니다. +- `SecureBuffer`: 고정 용량(`MAX_OUTPUT_LEN` = 1024바이트) 스택 기반 버퍼로, 페이로드를 `Secret` 내부에 보관하고 길이 필드를 별도로 둡니다. 유일한 다이제스트 반환 타입이며 원시 `[u8; N]`은 반환하지 않습니다. +- `ct_eq_slice` / `CtEqOps for SecureBuffer`: 공격자 제어 값과 MAC 태그·다이제스트를 비교하기 위한 상수-시간 동등 비교. + +설계 결정 명세는 다음과 같습니다. + +- 모든 버퍼는 고정 크기 스택 배열입니다. `alloc`을 사용하지 않으며, 크레이트는 `#![cfg_attr(not(test), no_std)]`입니다. +- 다이제스트는 원시 배열이 아니라 자체 소거되는 `SecureBuffer`로만 반환합니다. 원시 배열 반환은 프로젝트가 금지하는 anti-pattern입니다. +- `Blake2b::finalize`와 `Blake3::finalize`·`finalize_xof`는 `self`를 소비하므로, 출력이 산출되는 순간 전체 해싱 상태가 volatile 소거됩니다. +- BLAKE2b와 BLAKE3 코어 모두 지연 압축 전략을 씁니다. 마지막 가득 찬 블록(BLAKE2b)과 마지막 가득 찬 청크(BLAKE3)를 보류하여 최종 블록 플래그(`f0`)와 `CHUNK_END`·`ROOT` 플래그를 정확히 적용합니다. 버퍼된 블록은 추가 입력이 있음이 확인된 뒤에야 압축됩니다. +- BLAKE2b는 순차 모드만 동작합니다(fanout 1, depth 1, salt·personalization 없음). 이는 RFC 7693이 표준화한 범위와 정확히 일치합니다. BLAKE3는 해시와 키드 해시 모드를 구현하며, 컨텍스트 기반 derive-key 모드는 제공하지 않습니다. + +## 보안 처리 근거 + +두 해시 모두 비밀 메시지(BLAKE2b MAC 키, BLAKE3 키드 해시 자료)에 적용될 수 있으므로, 실행 시간이 비밀 값에 의존하지 않아야 합니다. 데이터 비종속은 다음 수준에서 보장됩니다. + +### 1. 압축 함수는 데이터 비종속 연산만 사용 + +BLAKE2b 믹싱 함수 `g`와 BLAKE3 믹싱 함수 `g3`는 `wrapping_add`, `^`, `rotate_right`만으로 구성됩니다(회전량 BLAKE2b 32/24/16/63, BLAKE3 16/12/8/7). 비밀 값에 따른 분기나 비밀 의존 메모리 접근이 전혀 없습니다. 라운드 순열 `SIGMA`(BLAKE2b)와 `MSG_PERMUTATION`(BLAKE3)은 공개 정보인 라운드 번호로만 인덱싱되므로 비밀 인덱스 룩업 테이블을 형성하지 않습니다. 따라서 캐시·TLB 타이밍 부채널이 원천 차단됩니다. + +### 2. 메시지 길이는 공개 정보 + +두 표준 모두에서 메시지 길이는 공개 정보입니다. `update`·`finalize`의 길이 기반 제어 흐름(버퍼 채우기, `.min()`, 블록·청크 카운팅, 체이닝 값 스택의 `popcount` 병합)은 입력 길이에만 의존하며 입력 바이트에는 의존하지 않습니다. 128비트 BLAKE2b 바이트 카운터는 분기 없는 캐리(`overflowing_add` -> `wrapping_add(carry as u64)`)로 증가합니다. + +### 3. 상수-시간 태그·다이제스트 비교 + +`ct_eq_slice`와 `SecureBuffer`의 `CtEqOps` 구현은 두 바이트 영역을 상수-시간으로 비교하며, 각 바이트 비교를 `constant-time` 크레이트의 인-라인 어셈블리 프리미티브에 위임합니다. 이 보장은 두 길이가 공개라고 가정하며, 이는 길이가 표준 파라미터인 MAC 태그·다이제스트·키에 대해 성립합니다. 길이 불일치 시 짧은 프리픽스를 비교한 뒤 결과를 `Choice(0)`으로 마스킹하므로 어떤 분기 결과도 호출자에게 새지 않습니다. 크레이트가 `constant-time`에 의존하므로, 검증된 상수-시간 구현이 있는 x86_64·aarch64에서만 빌드가 허용되는 제약이 전이적으로 적용됩니다. + +## 비밀 소거 (zeroize) + +트랜잭션 단위 전부 소거 원칙에 따라 다음이 보장됩니다. + +| 비밀 | 보호 방식 | +|-------------------------------------------------|-------------------------------------------------| +| BLAKE2b 체이닝 값 `h`(`[u64; 8]`) | `Secret`, Drop 시 volatile 소거 + `Zeroize` | +| BLAKE2b 카운터 `t`(`[u64; 2]`) | `Secret`, Drop 시 volatile 소거 | +| BLAKE2b·BLAKE3 입력 버퍼 | `SecureBuffer`·`Secret`, 블록 압축 직후 즉시 `zeroize` | +| BLAKE2b 워크 벡터 `v`(`[u64; 16]`) | `Secret`, `compress` 종료 시 Drop 소거 | +| BLAKE2b 로드된 블록 `m`(`[u64; 16]`) | `Secret`, Drop 소거(명시적 `drop`) | +| BLAKE3 키 워드(`[u32; 8]`) | `Secret`, 생성 시점에 래핑되어 평문 사본 미잔류 | +| BLAKE3 체이닝 값 스택(`[[u32; 8]; 54]`) | `Secret`, Drop 시 volatile 소거 | +| BLAKE3 청크 체이닝 값·블록 워드·new CV | `Secret`, Drop 소거 | +| BLAKE3 압축 상태·`m`·순열된 워드 | `Secret`, 반복마다·스코프 종료 시 Drop 소거 | +| BLAKE3 부모·병합 CV(`Output`·`parent_cv`) | `Secret`, 스코프 종료 시 Drop 소거 | +| 출력 직렬화 임시값(`word.to_le_bytes()`) | `finalize`·`root_output_bytes`에서 워드마다 명시적 `zeroize` | +| finalize 시 전체 해싱 상태 | `finalize`가 `self`를 소비하므로 `Drop`이 전 필드 volatile 소거 | + +`Secret::drop`은 `T: Zeroize` 여부와 무관하게 전체 바이트 범위를 `ptr::write_volatile`로 기록하고 컴파일러·메모리 배리어로 봉인하므로, 위에 나열된 모든 `Secret` 래핑 중간값을 덮습니다. `Blake2b`와 `Blake3`는 추가로 `Zeroize`·`Drop`을 구현하여 비-비밀 길이·플래그 필드도 volatile 쓰기로 소거합니다. + +잔여 한계: `g`·`g3` 내부의 라운드별 워킹 값과 `to_le_bytes`·`from_le_bytes`가 만드는 레지스터 사본은 레지스터에 짧게 상주하며 명시적 소거 대상이 아닙니다. 레지스터·스필 잔존(CWE-316)은 zeroize 모델의 알려진 한계이며, 후속 연산이 즉시 덮어쓰는 짧은 수명에 의존합니다. + +## 표준 부합 근거 + +| 요소 | 근거 | +|---------------------------------------------|---------------------| +| BLAKE2b 상수(IV, `SIGMA`)·파라미터 블록·키드 초기화 | RFC 7693 §2 | +| BLAKE2b 믹싱 함수 `G`·압축 `F`(12라운드, 회전 32/24/16/63) | RFC 7693 §3.2 | +| BLAKE2b 계산 예시·`"abc"` 테스트 벡터 | RFC 7693 Appendix A | +| 가변 길이 해시 `H'` | RFC 9106 §3.3 | +| BLAKE3 IV·메시지 순열·도메인 플래그·머클 트리 | BLAKE3 명세 | + +간접 검증에만 의존하는 크레이트와 달리, 이 크레이트는 모든 모드에 대해 자체 KAT(known-answer test)를 포함합니다. + +- BLAKE2b: RFC 7693 빈 입력·`"abc"` 벡터, 64바이트 키 `00..3f`에 대한 공식 `blake2-kat` 키드 벡터(빈 입력·단일 바이트 입력), 키 블록 경계를 넘는 레퍼런스 기반 다중 블록 키드 벡터. +- `blake2b_long`: RFC 9106 `H'` 레퍼런스 구현에서 산출한 `out_len` 80(`r = 1`)·128(`r = 2`)의 바이트 정확 벡터. +- BLAKE3: 공식 비키드 빈 입력·`"hello"` 벡터, 그리고 길이 0·1·64·1024·1025·8192의 공식 키드 벡터로, 각각 단일 블록·블록 경계·단일 청크·다중 청크·3단 머클 병합 경로를 운동합니다. +- `tests/threat_inputs.rs`: 경계 길이·비대칭 길이 회귀 커버리지와, 분할 `update` 호출이 단일 호출과 바이트 동일 출력을 내는지 검증하는 스트리밍 등가성 점검. + +`cargo fmt -p blake -- --check`, `cargo clippy -p blake --all-targets --all-features -- -D warnings`, `cargo build -p blake --target x86_64-unknown-none`, `cargo build -p blake --target aarch64-unknown-none` 모두 무경고 통과합니다. + +## 호출자 계약 + +이 크레이트는 stateless이므로 다음은 호출자가 보장하거나 인지해야 합니다. + +1. **1회용 finalize:** `finalize`·`finalize_xof`는 `self`를 소비합니다. 같은 입력에 대한 재해시는 새 인스턴스로 수행해야 합니다. +2. **다이제스트 즉시 사용:** `SecureBuffer`는 Drop 시 소거됩니다. `as_slice()` 결과를 호출자 측에 보존하려면 그 즉시 복사해야 하며, 다이제스트가 비밀(예: MAC 태그)이라면 복사본도 호출자가 소거해야 합니다. +3. **상수-시간 비교는 공개 길이 데이터용:** 태그·다이제스트 비교에는 `ct_eq_slice`·`CtEqOps::eq`를 사용합니다. 상수-시간 보장은 비교 길이가 공개임을 가정합니다. +4. **길이 사전조건:** `Blake2b::new`·`new_keyed`는 1..=64 출력 길이(및 1..=64 키 길이)를 단언하며, 위반 시 패닉(`panic = "abort"` 하)합니다. 호출자가 유효한 길이를 전달해야 합니다. `blake2b_long`은 `out_len == 0` 또는 `out_len > MAX_OUTPUT_LEN`이면 `Err(HashError)`를 반환합니다. BLAKE3 키 길이는 `&[u8; 32]` 타입으로 32바이트에 고정됩니다. + +--- + +## 발견한 문제와 조치 + +검증 과정에서 발견한 문제와 해결을 기술합니다. + +### BLAKE3 `new_keyed` 평문 키 워드 잔류 (해결) + +`new_keyed`는 32바이트 키를 `Secret`으로 감싸기 전에 `[u32; 8]` 평문 지역변수로 변환했습니다. `[u32; 8]`은 `Copy`이므로 `Secret::new`이 배열을 이동이 아니라 복사하여, 함수 반환 후에도 키의 비-소거 평문 사본(키 바이트로 자명하게 역변환 가능)이 함수 스택 프레임에 남았습니다. 구조체 도스트링도 키가 스택에 노출되지 않는다고 과장했습니다. + +#### 해결 + +헬퍼 `words_from_le_bytes_32`가 기존 `words_from_le_bytes_64` 패턴을 따라 `Secret<[u32; 8]>`을 반환하도록 변경하여, 변환된 키가 생성 시점에 래핑되고 명명된 평문 사본이 잔류하지 않게 했습니다. 도스트링도 실제 동작에 맞게 정정했습니다. + +### 다이제스트 직렬화의 미사용 출력 바이트 잔존 (해결) + +요청한 출력 길이가 워드 크기의 배수가 아닐 때, 마지막 `word.to_le_bytes()`는 8바이트(BLAKE2b) 또는 4바이트(BLAKE3) 배열 전체를 만들지만 그중 앞부분만 출력에 기록되었습니다. 나머지 바이트는 최종 내부 체이닝 상태에서 파생되어 출력에 포함되지 않으면서 스택에 남았습니다. + +#### 해결 + +`Blake2b::finalize`와 `Output::root_output_bytes`에서 워드마다 직렬화 임시값을 `zeroize()`하여 메모리 상주 시간을 제한했습니다. + +### 키드 BLAKE2b 미검증·`blake2b_long` 출력 미검증 (해결) + +`Blake2b::new_keyed`(MAC 경로)는 테스트 커버리지가 전혀 없었고, 유일한 `blake2b_long` 테스트는 값이 아니라 출력 길이만 단언했습니다. Argon2id에 입력되는 함수에서 조용한 오류 위험입니다. + +#### 해결 + +공식·레퍼런스 기반 키드 BLAKE2b KAT(빈 입력·단일 바이트·다중 블록)와 바이트 정확 RFC 9106 `H'` 값 KAT를 추가했으며 모두 통과하여, 키드 카운터 로직과 `H'` 체이닝이 바이트 정확함을 확인했습니다. + +### 문서 정정 (해결) + +`Blake2b::new`·`new_keyed` 생성자는 `Result`가 아니라 `Self`를 반환함에도 패닉 조건을 `# Errors`에 기술했으므로 `# Panics`로 변경했습니다. `blake2b_long` 도스트링은 `H'`를 "RFC 9106 §3.2"로 인용했으나 §3.3에 정의됩니다. + +### TODO + +- `g`·`g3` 워킹 값과 `to_le_bytes`·`from_le_bytes` 중간값의 레지스터·스필 잔존(CWE-316)은 다른 알고리즘 크레이트와 공유하는 알려진 한계입니다. +- `Blake2b::new`·`new_keyed`는 길이 사전조건을 `Result`가 아니라 `assert!`(panic = abort)로 강제합니다. 이는 전역(total) 생성자의 문서화된 강제 경계이나, 실패 가능 생성자라면 호출자 유발 abort를 피할 수 있습니다. +- BLAKE2b salt·personalization과 BLAKE3 derive-key 모드는 의도적으로 범위 밖입니다. +- aarch64 DIT·x86 DOITM 하드닝은 `constant-time` 크레이트와 공동 과제입니다. diff --git a/blake/README_EN.md b/blake/README_EN.md new file mode 100644 index 0000000..e924db9 --- /dev/null +++ b/blake/README_EN.md @@ -0,0 +1,139 @@ +# BLAKE Module + +[![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) + +A crate implementing the RFC 7693 BLAKE2b hash and MAC, the BLAKE3 hash / keyed-hash / XOF, and the RFC 9106 variable-length hash `H'` in pure `no_std` Rust with no external dependencies. This document describes the functional specification, the security design rationale, the standards-conformance basis, and the issues found with their mitigations. + +--- + +## Implemented Features + +| Algorithm | Standard | Output | Block Size | Internal Word | Rounds | Keyed Mode | +|---------------|---------------|------------------|------------------|---------------|--------|--------------------------| +| `Blake2b` | RFC 7693 | 1..=64 bytes | 128 bytes | `u64` | 12 | `new_keyed` (1..=64 B key) | +| `Blake3` | BLAKE3 spec | 32 bytes / XOF | 64 B (1024 B chunk) | `u32` | 7 | `new_keyed` (32 B key) | +| `blake2b_long` | RFC 9106 §3.3 | 1..=1024 bytes | (BLAKE2b core) | `u64` | 12 | none | + +The public API consists of the `Blake2b` and `Blake3` types, the free function `blake2b_long`, the `SecureBuffer` container, and the `ct_eq_slice` comparison helper. + +- `Blake2b`: a streaming interface of `new(hash_len)` / `new_keyed(hash_len, key)` -> `update(&[u8])` (any number of times) -> `finalize(self) -> Result`. `finalize` consumes `self`, so instance reuse is blocked at the type level. The keyed constructor processes the key as a single 128-byte zero-padded first block, providing a BLAKE2b MAC. +- `Blake3`: a streaming interface of `new()` / `new_keyed(&[u8; 32])` -> `update(&[u8])` -> `finalize(self)` (32-byte output) or `finalize_xof(self, out_len)` (arbitrary length up to `MAX_OUTPUT_LEN`). The internal Merkle tree is driven by a chaining-value stack of fixed depth 54. +- `blake2b_long`: the RFC 9106 `H'` construction used for Argon2id block initialization and final-tag generation. For `out_len <= 64` it is a single `BLAKE2b(LE32(out_len) || input)`; for larger outputs it chains 64-byte digests and emits 32-byte prefixes plus a final `(out_len - 32r)`-byte block. +- `SecureBuffer`: a fixed-capacity (`MAX_OUTPUT_LEN` = 1024 bytes) stack-backed buffer whose payload is held inside a `Secret`, with a separate length field. It is the only digest return type; raw `[u8; N]` is never returned. +- `ct_eq_slice` / `CtEqOps for SecureBuffer`: constant-time equality for comparing MAC tags and digests against attacker-controlled values. + +Design decisions are as follows. + +- All buffers are fixed-size stack arrays. No `alloc` is used, and the crate is `#![cfg_attr(not(test), no_std)]`. +- Digests are returned only as the self-zeroizing `SecureBuffer`, never as a raw array. Returning a raw array is an anti-pattern the project forbids. +- `Blake2b::finalize` and `Blake3::finalize` / `finalize_xof` consume `self`, so the entire hashing state is volatile-erased the moment the output is produced. +- The BLAKE2b and BLAKE3 cores both use a lazy-compression strategy: the last full block (BLAKE2b) and the last full chunk (BLAKE3) are held back so that the final-block flag (`f0`) and the `CHUNK_END` / `ROOT` flags can be applied correctly. A buffered block is compressed only once it is known that more input follows. +- BLAKE2b runs in sequential mode only (fanout 1, depth 1, no salt and no personalization), which is exactly the scope standardized by RFC 7693. BLAKE3 implements the hash and keyed-hash modes; the context-based derive-key mode is not provided. + +## Security Processing Rationale + +Both hashes may be applied to secret messages (BLAKE2b MAC keys, BLAKE3 keyed-hash material), so execution time must not depend on secret values. Data independence is guaranteed at the following levels. + +### 1. The Compression Functions Use Only Data-Independent Operations + +The BLAKE2b mixing function `g` and the BLAKE3 mixing function `g3` are composed solely of `wrapping_add`, `^`, and `rotate_right` (rotations 32/24/16/63 for BLAKE2b, 16/12/8/7 for BLAKE3). There are no secret-dependent branches and no secret-dependent memory accesses. The round permutations `SIGMA` (BLAKE2b) and `MSG_PERMUTATION` (BLAKE3) are indexed only by the round number, which is public, so they never form a secret-indexed table lookup. Cache and TLB timing side channels are therefore eliminated at the source. + +### 2. Message Length Is Public + +In both standards the message length is public information. The length-driven control flow in `update` and `finalize` (buffer fill, `.min()`, block and chunk counting, the chaining-value-stack `popcount` merge) depends only on the input length, never on the input bytes. The 128-bit BLAKE2b byte counter is advanced with a branchless carry (`overflowing_add` -> `wrapping_add(carry as u64)`). + +### 3. Constant-Time Tag and Digest Comparison + +`ct_eq_slice` and the `CtEqOps` implementation for `SecureBuffer` compare two byte regions in constant time, delegating each byte comparison to the inline-assembly primitives of the `constant-time` crate. The guarantee assumes that the two lengths are public, which holds for MAC tags, digests, and keys whose lengths are standard parameters. On a length mismatch the result is masked to `Choice(0)` after comparing the shorter prefix, so no branch outcome is leaked to the caller. Because the crate depends on `constant-time`, the restriction that builds are permitted only on x86_64 and aarch64 — which have verified constant-time implementations — applies transitively. + +## Secret Zeroization (zeroize) + +Following the transaction-scoped full-erasure principle, the following are guaranteed. + +| Secret | Protection | +|-------------------------------------------------------|--------------------------------------------------------------| +| BLAKE2b chaining value `h` (`[u64; 8]`) | `Secret`, volatile erase on drop + `Zeroize` | +| BLAKE2b counter `t` (`[u64; 2]`) | `Secret`, volatile erase on drop | +| BLAKE2b / BLAKE3 input buffer | `SecureBuffer` / `Secret`, `zeroize` immediately after each block compression | +| BLAKE2b work vector `v` (`[u64; 16]`) | `Secret`, drop-erased at end of `compress` | +| BLAKE2b loaded block `m` (`[u64; 16]`) | `Secret`, drop-erased (explicit `drop`) | +| BLAKE3 key words (`[u32; 8]`) | `Secret`, wrapped at construction with no plaintext copy left | +| BLAKE3 chaining-value stack (`[[u32; 8]; 54]`) | `Secret`, volatile erase on drop | +| BLAKE3 chunk chaining value / block words / new CV | `Secret`, drop-erased | +| BLAKE3 compression state, `m`, permuted words | `Secret`, drop-erased per iteration / at scope end | +| BLAKE3 parent and merged CVs (`Output`, `parent_cv`) | `Secret`, drop-erased at scope end | +| Output serialization temporary (`word.to_le_bytes()`) | explicit `zeroize` after each word in `finalize` / `root_output_bytes` | +| Whole hashing state on finalize | `finalize` consumes `self`, so `Drop` volatile-erases every field | + +`Secret::drop` writes the full byte extent with `ptr::write_volatile` and seals it with compiler and memory barriers, independent of whether `T: Zeroize`; this covers every `Secret`-wrapped intermediate listed above. `Blake2b` and `Blake3` additionally implement `Zeroize` + `Drop`, clearing the non-secret length and flag fields with volatile writes as well. + +Residual limitation: the per-round working values inside `g` / `g3`, and the register copies produced by `to_le_bytes` / `from_le_bytes`, reside briefly in registers and are not explicit erasure targets. Register and spill residue (CWE-316) is a known limitation of the zeroize model and relies on the short lifetime before subsequent operations overwrite them. + +## Standards-Conformance Basis + +| Element | Reference | +|----------------------------------------------------------|----------------------------| +| BLAKE2b constants (IV, `SIGMA`), parameter block, keyed init | RFC 7693 §2 | +| BLAKE2b mixing function `G` and compression `F` (12 rounds, rot 32/24/16/63) | RFC 7693 §3.2 | +| BLAKE2b worked example and `"abc"` test vector | RFC 7693 Appendix A | +| Variable-length hash `H'` | RFC 9106 §3.3 | +| BLAKE3 IV, message permutation, domain flags, Merkle tree | BLAKE3 specification | + +Unlike crates that are only verified indirectly, this crate ships its own known-answer tests (KATs) for every mode. + +- BLAKE2b: the RFC 7693 empty-input and `"abc"` vectors; the official `blake2-kat` keyed vectors for the 64-byte key `00..3f` (empty input and single-byte input); and a reference-derived multi-block keyed vector that crosses the key-block boundary. +- `blake2b_long`: byte-exact vectors derived from an RFC 9106 `H'` reference implementation for `out_len` 80 (`r = 1`) and 128 (`r = 2`). +- BLAKE3: the official unkeyed empty / `"hello"` vectors, and the official keyed vectors at lengths 0, 1, 64, 1024, 1025, and 8192, which exercise the single-block, block-boundary, single-chunk, multi-chunk, and three-level Merkle-merge paths respectively. +- `tests/threat_inputs.rs`: boundary-length and asymmetric-length regression coverage, plus streaming-equivalence checks confirming that split `update` calls produce byte-identical output to a single call. + +`cargo fmt -p blake -- --check`, `cargo clippy -p blake --all-targets --all-features -- -D warnings`, `cargo build -p blake --target x86_64-unknown-none`, and `cargo build -p blake --target aarch64-unknown-none` all pass with no warnings. + +## Caller Contract + +Because this crate is stateless, the caller must guarantee or be aware of the following. + +1. **Single-use finalize:** `finalize` / `finalize_xof` consume `self`. Re-hashing the same input requires a new instance. +2. **Use the digest immediately:** `SecureBuffer` is erased on drop. To retain the result of `as_slice()` on the caller side, it must be copied right away; if the digest is a secret (a MAC tag, for example), the caller must also erase the copy. +3. **Constant-time comparison is for public-length data:** use `ct_eq_slice` / `CtEqOps::eq` to compare tags and digests; the constant-time guarantee assumes the compared lengths are public. +4. **Length preconditions:** `Blake2b::new` and `new_keyed` assert a 1..=64 output length (and a 1..=64 key length), and panic (under `panic = "abort"`) on violation; the caller must pass valid lengths. `blake2b_long` returns `Err(HashError)` for `out_len == 0` or `out_len > MAX_OUTPUT_LEN`. The BLAKE3 key length is fixed to 32 bytes by the `&[u8; 32]` type. + +--- + +## Issues Found and Mitigations + +This section describes issues identified during verification and their resolutions. + +### Plaintext Key-Word Residue in BLAKE3 `new_keyed` (Resolved) + +`new_keyed` converted the 32-byte key into a `[u32; 8]` plaintext local before wrapping it in `Secret`. Because `[u32; 8]` is `Copy`, `Secret::new` copied the array rather than moving it, leaving a non-erased plaintext copy of the key (trivially reversible to the key bytes) on the function's stack frame after it returned. The struct-level docstring also overclaimed that the key was never exposed on the stack. + +#### Resolution + +The helper `words_from_le_bytes_32` now returns `Secret<[u32; 8]>`, mirroring the existing `words_from_le_bytes_64` pattern, so the converted key is wrapped at the point of creation and no named plaintext copy persists. The docstrings were corrected to describe the actual behavior. + +### Residue of Unused Output Bytes in Digest Serialization (Resolved) + +When the requested output length is not a multiple of the word size, the last `word.to_le_bytes()` produced a full 8-byte (BLAKE2b) or 4-byte (BLAKE3) array of which only the leading bytes were written to the output. The trailing bytes — derived from the final internal chaining state and not part of the output — remained on the stack. + +#### Resolution + +The serialization temporary is now `zeroize()`d after each word in `Blake2b::finalize` and `Output::root_output_bytes`, limiting its residence time in memory. + +### Untested Keyed BLAKE2b and Unverified `blake2b_long` Output (Resolved) + +`Blake2b::new_keyed` (the MAC path) had no test coverage at all, and the sole `blake2b_long` test asserted only the output length, not its value — a silent-error risk for a function that feeds Argon2id. + +#### Resolution + +Official and reference-derived keyed BLAKE2b KATs (empty, single-byte, multi-block) and byte-exact RFC 9106 `H'` value KATs were added; all pass, confirming the keyed counter logic and the `H'` chaining are byte-correct. + +### Documentation Corrections (Resolved) + +The `Blake2b::new` / `new_keyed` constructors documented their panic conditions under `# Errors` despite returning `Self` rather than `Result`; these were changed to `# Panics`. The `blake2b_long` docstring cited "RFC 9106 §3.2" for `H'`, which is defined in §3.3. + +### Remaining Work + +- Register and spill residue (CWE-316) of the `g` / `g3` working values and the `to_le_bytes` / `from_le_bytes` intermediates is a known limitation shared with the other algorithm crates. +- `Blake2b::new` / `new_keyed` enforce their length preconditions with `assert!` (panic = abort) rather than a `Result`; this is the documented enforcement boundary for a total constructor, but a fallible constructor would avoid a caller-triggered abort. +- BLAKE2b salt and personalization, and the BLAKE3 derive-key mode, are intentionally out of scope. +- aarch64 DIT and x86 DOITM hardening is a joint task with the `constant-time` crate. From 8255ea4efaa3367876994b992e65ee5be86b5ec8 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:12:28 +0900 Subject: [PATCH 26/65] =?UTF-8?q?cross-confirm.md=20=EC=97=90=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cross-confirm.md b/cross-confirm.md index 5ba2526..14d43a7 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -6,7 +6,7 @@ 이 검증 작업은 4명의 보안 및 컴플라이언스 종사자(전문가)와 Claude Fable 5 모델을 통해 수행됩니다. 다음 목록은 현황을 명시한 것입니다. - [X] AES -- [ ] BLAKE +- [X] BLAKE - [X] SHA2 - [X] SHA3 - [ ] ChaCha20 From 6a82d463f0ed31575d6d92f69aea6a5bc529375a Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:57:57 +0900 Subject: [PATCH 27/65] =?UTF-8?q?ed448=20RFC=208032=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=ED=9B=84=EC=86=8D=20=EB=B3=B4=EC=95=88=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20-=20=EC=8A=A4=EC=B9=BC=EB=9D=BC=20=EA=B3=B1=EC=85=88=20?= =?UTF-8?q?=EC=83=81=EC=88=98=EC=8B=9C=EA=B0=84=ED=99=94=20double-and-add-?= =?UTF-8?q?always=20=EB=B0=8F=20field=20Sub=20Neg=20=EB=B6=84=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20=EC=99=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=8D=95=EC=85=98=20=EB=B9=84=EB=B0=80=20=EB=B2=84?= =?UTF-8?q?=ED=8D=BC=20=EC=86=8C=EA=B1=B0=20-=20=EB=B9=84=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=20y=20=EC=A2=8C=ED=91=9C=20=EA=B1=B0=EB=B6=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20255=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80=20-?= =?UTF-8?q?=20Scalar=20Sub=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20-?= =?UTF-8?q?=20cross-confirm=20Ed448=20=EA=B2=80=EC=A6=9D=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- ed448/src/field.rs | 42 +++++------------------------------------- ed448/src/lib.rs | 5 +++++ ed448/src/point.rs | 33 ++++++++++++++++++++++++--------- ed448/src/scalar.rs | 20 +++++++++++++++++--- 5 files changed, 52 insertions(+), 50 deletions(-) diff --git a/cross-confirm.md b/cross-confirm.md index 14d43a7..d1fa777 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -10,7 +10,7 @@ - [X] SHA2 - [X] SHA3 - [ ] ChaCha20 -- [ ] Ed448 +- [X] Ed448 - [X] Ed25519 - [ ] X448 - [X] X25519 diff --git a/ed448/src/field.rs b/ed448/src/field.rs index 1392ef3..bc94186 100644 --- a/ed448/src/field.rs +++ b/ed448/src/field.rs @@ -329,32 +329,13 @@ impl Sub for FieldElement { #[inline] fn sub(self, rhs: Self) -> Self { - let a = self.reduce(); - let b = rhs.reduce(); + // 2*p 바이어스를 더해 분기 없이 언더플로 방지 (상수시간) + // 약하게 축소된 limb 은 2*P[i] 미만이므로 limb 단위 음수 발생 안 함 let mut result = [0u64; LIMBS]; - let mut borrow = 0i64; - for i in 0..LIMBS { - let diff = (a.0[i] as i64) - (b.0[i] as i64) - borrow; - if diff < 0 { - result[i] = ((diff + (MASK as i64) + 1) as u64) & MASK; - borrow = 1; - } else { - result[i] = (diff as u64) & MASK; - borrow = 0; - } - } - - if borrow != 0 { - let mut carry = 0i64; - for i in 0..LIMBS { - let sum = (result[i] as i64) + (P[i] as i64) + carry; - result[i] = (sum as u64) & MASK; - carry = sum >> LIMB_BITS; - } + result[i] = (self.0[i] + 2 * P[i]) - rhs.0[i]; } - - FieldElement(result) + FieldElement(result).weak_reduce() } } @@ -363,20 +344,7 @@ impl Neg for FieldElement { #[inline] fn neg(self) -> Self { - let t = self.reduce(); - let mut result = [0u64; LIMBS]; - let mut borrow = 0i64; - for i in 0..LIMBS { - let diff = (P[i] as i64) - (t.0[i] as i64) - borrow; - if diff < 0 { - result[i] = ((diff + (MASK as i64) + 1) as u64) & MASK; - borrow = 1; - } else { - result[i] = (diff as u64) & MASK; - borrow = 0; - } - } - FieldElement(result) + FieldElement::zero() - self } } diff --git a/ed448/src/lib.rs b/ed448/src/lib.rs index b15ab8e..74c59a4 100644 --- a/ed448/src/lib.rs +++ b/ed448/src/lib.rs @@ -172,6 +172,11 @@ impl Signature { } fn dom4(context: &[u8]) -> ([u8; 10], usize) { + // RFC 8032: 컨텍스트는 최대 255 옥텟 OLEN 이 1 바이트라 초과 시 절단되어 적합성 위반 + assert!( + context.len() <= 255, + "Ed448 컨텍스트는 255 바이트 이하여야 함" + ); let mut dom = [0u8; 10]; dom[..8].copy_from_slice(DOM4_PREFIX); dom[8] = 0; diff --git a/ed448/src/point.rs b/ed448/src/point.rs index b48a075..ffc5a5f 100644 --- a/ed448/src/point.rs +++ b/ed448/src/point.rs @@ -74,6 +74,13 @@ impl EdwardsPoint { } let y = FieldElement::from_bytes(&y_bytes); + + // 비정규 인코딩 거부 (RFC 8032 5.2.3: y < p 이어야 함) + // 정규 직렬화 결과가 입력과 다르면 y >= p 인 비정규 표현 + if y.to_bytes() != y_bytes { + return None; + } + let y2 = y.square(); let d = d_constant(); @@ -162,22 +169,30 @@ impl EdwardsPoint { } } + // 상수시간 조건부 점 선택 choice 가 1 이면 b 0 이면 a + #[inline] + fn conditional_select(a: &Self, b: &Self, choice: u8) -> Self { + EdwardsPoint { + x: FieldElement::conditional_select(&a.x, &b.x, choice), + y: FieldElement::conditional_select(&a.y, &b.y, choice), + z: FieldElement::conditional_select(&a.z, &b.z, choice), + t: FieldElement::conditional_select(&a.t, &b.t, choice), + } + } + pub fn scalar_mul(&self, scalar: &Scalar) -> Self { // 스칼라 바이트는 비밀일 수 있으므로 사용 후 명시적 소거 let mut s = scalar.to_bytes(); let mut result = EdwardsPoint::identity(); + // 비밀 스칼라 비트에 분기하지 않는 상수시간 double-and-add + // 매 비트마다 덧셈을 항상 수행하고 조건부 선택으로 누적값 갱신 for i in (0..448).rev() { result = result.double_internal(); - - let byte_idx = i / 8; - let bit_idx = i % 8; - if byte_idx < 57 { - let bit = (s[byte_idx] >> bit_idx) & 1; - if bit == 1 { - result = result.add_internal(self); - } - } + let bit = (s[i / 8] >> (i % 8)) & 1; + let mut sum = result.add_internal(self); + result = EdwardsPoint::conditional_select(&result, &sum, bit); + sum.zeroize(); } // 스칼라 사본 명시적 소거 diff --git a/ed448/src/scalar.rs b/ed448/src/scalar.rs index 8cbd4c5..5de572a 100644 --- a/ed448/src/scalar.rs +++ b/ed448/src/scalar.rs @@ -71,6 +71,8 @@ impl Scalar { // byte 56 must be 0 for canonical scalars (L[56] = 0) result[56] = 0; + // 비밀 파생 중간 limb 소거 (result 는 호출자가 소거) + s.zeroize(); Scalar(result) } @@ -157,6 +159,9 @@ fn sc_reduce_wide(s: &mut [u64; 18]) { bytes[i] = (sum & 0xff) as u8; carry = sum >> 8; } + + // 비밀 파생 부분곱 소거 + product.zeroize(); } // bytes >= L인 동안 L 빼기 @@ -222,6 +227,9 @@ fn sc_reduce_wide(s: &mut [u64; 18]) { for i in 8..18 { s[i] = 0; } + + // 비밀 파생 바이트 버퍼 소거 + bytes.zeroize(); } pub fn sc_muladd(a: &Scalar, b: &Scalar, c: &Scalar) -> Scalar { @@ -273,6 +281,11 @@ pub fn sc_muladd(a: &Scalar, b: &Scalar, c: &Scalar) -> Scalar { // byte 56 must be 0 for canonical scalars result[56] = 0; + // 비밀 파생 중간 limb 소거 (result 는 호출자가 소거) + s.zeroize(); + a_limbs.zeroize(); + b_limbs.zeroize(); + c_limbs.zeroize(); Scalar(result) } @@ -289,9 +302,10 @@ impl Sub for Scalar { #[allow(clippy::suspicious_arithmetic_impl)] fn sub(self, rhs: Self) -> Self { - let mut neg_bytes = [0xffu8; 57]; - neg_bytes[56] = 0; - let neg_rhs = sc_muladd(&Scalar(neg_bytes), &rhs, &Scalar::zero()); + // -1 mod L = L - 1 로 rhs 부정 후 더함 (L_BYTES[0]=0xf3 이라 borrow 없음) + let mut neg_one = L_BYTES; + neg_one[0] = 0xf2; + let neg_rhs = sc_muladd(&Scalar(neg_one), &rhs, &Scalar::zero()); sc_muladd(&Scalar::one(), &self, &neg_rhs) } } From 84827b2a21c9e52ce5083298bdaf84423d6e5f55 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 2 Jul 2026 12:21:40 +0900 Subject: [PATCH 28/65] =?UTF-8?q?x448=20RFC=207748=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=9B=84=EC=86=8D=20=EB=B3=B4=EC=95=88=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20field=20Sub=20=EC=83=81=EC=88=98=EC=8B=9C=EA=B0=84=ED=99=94?= =?UTF-8?q?=202p=20=EB=B0=94=EC=9D=B4=EC=96=B4=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EB=B0=8F=20=ED=94=BC=EC=97=B0=EC=82=B0?= =?UTF-8?q?=EC=9E=90=20reduce=20=EC=A0=9C=EA=B1=B0=20-=20=EC=82=AC?= =?UTF-8?q?=EB=8B=A4=EB=A6=AC=20=EC=9E=84=EC=8B=9C=EA=B0=92=EA=B3=BC=20?= =?UTF-8?q?=EC=99=80=EC=9D=B4=EB=93=9C=20=EB=A6=AC=EB=8D=95=EC=85=98=20?= =?UTF-8?q?=EB=B2=84=ED=8D=BC=20=EB=B0=8F=20invert=20=EC=82=AC=EB=B3=B8=20?= =?UTF-8?q?=EC=86=8C=EA=B1=B0=20-=20diffie=5Fhellman=20=EC=A0=80=EC=B0=A8?= =?UTF-8?q?=EC=88=98=20=EC=A0=90=20=EA=B1=B0=EB=B6=80=20Result=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20-=20=EB=B9=84=EC=A0=95=EA=B7=9C=20p=20p+1=20?= =?UTF-8?q?=EC=A0=80=EC=B0=A8=EC=88=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B0=B1=EB=A7=8C=ED=9A=8C=20=EB=B0=98=EB=B3=B5=20?= =?UTF-8?q?KAT=20=EC=B6=94=EA=B0=80=20-=20cross-confirm=20X448=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=99=84=EB=A3=8C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- x448/src/field.rs | 97 ++++++++++------------------------- x448/src/lib.rs | 125 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 121 insertions(+), 103 deletions(-) diff --git a/cross-confirm.md b/cross-confirm.md index d1fa777..8c69eae 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -12,7 +12,7 @@ - [ ] ChaCha20 - [X] Ed448 - [X] Ed25519 -- [ ] X448 +- [X] X448 - [X] X25519 - [X] ML-DSA - [ ] ML-KEM diff --git a/x448/src/field.rs b/x448/src/field.rs index 97ec972..4bf2bbd 100644 --- a/x448/src/field.rs +++ b/x448/src/field.rs @@ -1,18 +1,28 @@ #![allow( clippy::unusual_byte_groupings, clippy::wrong_self_convention, - clippy::needless_range_loop, - dead_code + clippy::needless_range_loop )] use constant_time::{Choice, CtSelOps}; -use core::ops::{Add, Mul, Neg, Sub}; +use core::ops::{Add, Mul, Sub}; use zeroize::Zeroize; const LIMBS: usize = 8; const LIMB_BITS: usize = 56; const MASK: u64 = (1u64 << 56) - 1; +const P: [u64; LIMBS] = [ + 0xFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFE, + 0xFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFF, +]; + #[derive(Clone, Copy)] pub struct FieldElement(pub(crate) [u64; LIMBS]); @@ -102,17 +112,6 @@ impl FieldElement { } fn reduce(&self) -> Self { - let p: [u64; LIMBS] = [ - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFE, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - ]; - let mut t = self.weak_reduce(); t = t.weak_reduce(); t = t.weak_reduce(); @@ -120,14 +119,14 @@ impl FieldElement { for _ in 0..3 { let mut under = 0i64; for i in 0..LIMBS { - let diff = (t.0[i] as i64) - (p[i] as i64) + under; + let diff = (t.0[i] as i64) - (P[i] as i64) + under; under = diff >> 63; } if under >= 0 { let mut borrow = 0i64; for i in 0..LIMBS { - let diff = (t.0[i] as i64) - (p[i] as i64) - borrow; + let diff = (t.0[i] as i64) - (P[i] as i64) - borrow; borrow = if diff < 0 { 1 } else { 0 }; t.0[i] = (diff as u64) & MASK; } @@ -176,6 +175,9 @@ impl FieldElement { let mut fe = FieldElement(result); fe = fe.weak_reduce(); + c.zeroize(); + carry.zeroize(); + result.zeroize(); fe } @@ -195,18 +197,10 @@ impl FieldElement { base = base.square(); } + base.zeroize(); result } - pub fn is_zero(&self) -> Choice { - let t = self.reduce(); - let mut or = 0u64; - for i in 0..LIMBS { - or |= t.0[i]; - } - Choice::from_u8((or == 0) as u8) - } - #[inline] pub fn conditional_swap(a: &mut Self, b: &mut Self, choice: Choice) { for i in 0..LIMBS { @@ -233,52 +227,13 @@ impl Sub for FieldElement { #[inline] fn sub(self, rhs: Self) -> Self { - let p: [u64; LIMBS] = [ - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFE, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFF, - ]; - - let a = self.reduce(); - let b = rhs.reduce(); + // 2*p 바이어스를 더해 분기 없이 언더플로 방지 (상수시간) + // 약하게 축소된 limb 은 2*P[i] 미만이므로 limb 단위 음수 발생 안 함 let mut result = [0u64; LIMBS]; - let mut borrow = 0i64; - for i in 0..LIMBS { - let diff = (a.0[i] as i64) - (b.0[i] as i64) - borrow; - if diff < 0 { - result[i] = ((diff + (MASK as i64) + 1) as u64) & MASK; - borrow = 1; - } else { - result[i] = (diff as u64) & MASK; - borrow = 0; - } + result[i] = (self.0[i] + 2 * P[i]) - rhs.0[i]; } - - if borrow != 0 { - let mut carry = 0i64; - for i in 0..LIMBS { - let sum = (result[i] as i64) + (p[i] as i64) + carry; - result[i] = (sum as u64) & MASK; - carry = sum >> LIMB_BITS; - } - } - - FieldElement(result) - } -} - -impl Neg for FieldElement { - type Output = Self; - - #[inline] - fn neg(self) -> Self { - FieldElement::zero() - self + FieldElement(result).weak_reduce() } } @@ -295,11 +250,11 @@ impl PartialEq for FieldElement { fn eq(&self, other: &Self) -> bool { let a = self.reduce(); let b = other.reduce(); - let mut eq = true; + let mut acc = 0u64; for i in 0..LIMBS { - eq = eq && (a.0[i] == b.0[i]); + acc |= a.0[i] ^ b.0[i]; } - eq + acc == 0 } } diff --git a/x448/src/lib.rs b/x448/src/lib.rs index d00c752..3a28bce 100644 --- a/x448/src/lib.rs +++ b/x448/src/lib.rs @@ -22,8 +22,8 @@ //! let bob_public = bob_secret.public_key(); //! //! // 공유 비밀 계산 -//! let alice_shared = alice_secret.diffie_hellman(&bob_public); -//! let bob_shared = bob_secret.diffie_hellman(&alice_public); +//! let alice_shared = alice_secret.diffie_hellman(&bob_public).unwrap(); +//! let bob_shared = bob_secret.diffie_hellman(&alice_public).unwrap(); //! //! assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); //! ``` @@ -44,6 +44,11 @@ const BASEPOINT_U: [u8; 56] = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum X448Error { + LowOrderPoint, +} + pub struct SecretKey(Secret<[u8; 56]>); impl SecretKey { @@ -60,11 +65,15 @@ impl SecretKey { PublicKey(public_bytes) } - pub fn diffie_hellman(&self, their_public: &PublicKey) -> SharedSecret { + pub fn diffie_hellman(&self, their_public: &PublicKey) -> Result { let mut shared = x448(self.0.expose(), &their_public.0); let result = SharedSecret(Secret::new(shared)); shared.zeroize(); - result + if result.is_zero() { + Err(X448Error::LowOrderPoint) + } else { + Ok(result) + } } } @@ -123,6 +132,19 @@ fn montgomery_ladder(k: &[u8; 56], u: &[u8; 56]) -> [u8; 56] { let mut swap: u8 = 0; + let mut a = FieldElement::zero(); + let mut aa = FieldElement::zero(); + let mut b = FieldElement::zero(); + let mut bb = FieldElement::zero(); + let mut e = FieldElement::zero(); + let mut c = FieldElement::zero(); + let mut d = FieldElement::zero(); + let mut da = FieldElement::zero(); + let mut cb = FieldElement::zero(); + let mut sum = FieldElement::zero(); + let mut diff = FieldElement::zero(); + let mut a24_e = FieldElement::zero(); + for pos in (0..448).rev() { let byte_idx = pos / 8; let bit_idx = pos % 8; @@ -134,21 +156,21 @@ fn montgomery_ladder(k: &[u8; 56], u: &[u8; 56]) -> [u8; 56] { FieldElement::conditional_swap(&mut z_2, &mut z_3, choice); swap = k_t; - let a = x_2 + z_2; - let aa = a.square(); - let b = x_2 - z_2; - let bb = b.square(); - let e = aa - bb; - let c = x_3 + z_3; - let d = x_3 - z_3; - let da = d * a; - let cb = c * b; - let sum = da + cb; - let diff = da - cb; + a = x_2 + z_2; + aa = a.square(); + b = x_2 - z_2; + bb = b.square(); + e = aa - bb; + c = x_3 + z_3; + d = x_3 - z_3; + da = d * a; + cb = c * b; + sum = da + cb; + diff = da - cb; x_3 = sum.square(); z_3 = x_1 * diff.square(); x_2 = aa * bb; - let a24_e = mul_by_a24(e); + a24_e = mul_by_a24(e); z_2 = e * (aa + a24_e); } @@ -170,6 +192,18 @@ fn montgomery_ladder(k: &[u8; 56], u: &[u8; 56]) -> [u8; 56] { z_2_inv.zeroize(); result.zeroize(); swap.zeroize(); + a.zeroize(); + aa.zeroize(); + b.zeroize(); + bb.zeroize(); + e.zeroize(); + c.zeroize(); + d.zeroize(); + da.zeroize(); + cb.zeroize(); + sum.zeroize(); + diff.zeroize(); + a24_e.zeroize(); bytes } @@ -219,7 +253,7 @@ fn mul_by_a24(e: FieldElement) -> FieldElement { c[0] += carry; c[4] += carry; - FieldElement([ + let out = FieldElement([ c[0] as u64, c[1] as u64, c[2] as u64, @@ -228,7 +262,9 @@ fn mul_by_a24(e: FieldElement) -> FieldElement { c[5] as u64, c[6] as u64, c[7] as u64, - ]) + ]); + c.zeroize(); + out } pub fn generate_keypair(mut rng: R) -> (SecretKey, PublicKey) { @@ -287,7 +323,7 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(alice_secret.diffie_hellman(&bob_pk)); + storage.write(alice_secret.diffie_hellman(&bob_pk).unwrap()); let ptr = storage.assume_init_ref().as_bytes().as_ptr(); let pre = core::slice::from_raw_parts(ptr, 56); @@ -431,8 +467,8 @@ mod tests { let alice_pk = alice_sk.public_key(); let bob_pk = bob_sk.public_key(); - let alice_shared = alice_sk.diffie_hellman(&bob_pk); - let bob_shared = bob_sk.diffie_hellman(&alice_pk); + let alice_shared = alice_sk.diffie_hellman(&bob_pk).unwrap(); + let bob_shared = bob_sk.diffie_hellman(&alice_pk).unwrap(); assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); @@ -488,21 +524,48 @@ mod tests { #[test] fn test_low_order_point_rejection() { let secret = SecretKey::from_bytes([1u8; 56]); - let low_order_points: [[u8; 56]; 2] = [ - [0u8; 56], - [ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - ]; + + let mut u_one = [0u8; 56]; + u_one[0] = 1; + + // 비정규 인코딩 p 는 u = 0, p+1 은 u = 1 에 해당 + let mut p_bytes = [0xFFu8; 56]; + p_bytes[28] = 0xFE; + let mut p_plus_one = [0u8; 56]; + for byte in p_plus_one.iter_mut().skip(28) { + *byte = 0xFF; + } + + let low_order_points: [[u8; 56]; 4] = [[0u8; 56], u_one, p_bytes, p_plus_one]; for low_order in &low_order_points { let public = PublicKey::from_bytes(*low_order); - let shared = secret.diffie_hellman(&public); + let result = secret.diffie_hellman(&public); assert!( - shared.is_zero(), - "low-order point should produce zero shared secret" + matches!(result, Err(X448Error::LowOrderPoint)), + "low-order point should be rejected" ); } } + + #[test] + #[ignore] + fn test_iteration_1000000() { + let mut k = BASEPOINT_U; + let mut u = BASEPOINT_U; + + for _ in 0..1_000_000 { + let result = x448(&k, &u); + u = k; + k = result; + } + + let expected: [u8; 56] = [ + 0x07, 0x7f, 0x45, 0x36, 0x81, 0xca, 0xca, 0x36, 0x93, 0x19, 0x84, 0x20, 0xbb, 0xe5, + 0x15, 0xca, 0xe0, 0x00, 0x24, 0x72, 0x51, 0x9b, 0x3e, 0x67, 0x66, 0x1a, 0x7e, 0x89, + 0xca, 0xb9, 0x46, 0x95, 0xc8, 0xf4, 0xbc, 0xd6, 0x6e, 0x61, 0xb9, 0xb9, 0xc9, 0x46, + 0xda, 0x8d, 0x52, 0x4d, 0xe3, 0xd6, 0x9b, 0xd9, 0xd9, 0xd6, 0x6b, 0x99, 0x7e, 0x37, + ]; + assert_eq!(k, expected); + } } From 12e96a00aae877075195a89d15126c3863d05c6b Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:16:05 +0900 Subject: [PATCH 29/65] =?UTF-8?q?mlkem=20FIPS=20203=20=EC=BB=B4=ED=94=8C?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=EC=8A=A4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20KAT=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공유 비밀 KDF 제거 및 행렬 전치 교정으로 표준 벡터 일치 - encaps 모듈러스 검사 추가 Result 반환으로 비정규 ek 거부 - SampleNTT 무한 거부 샘플링 교정 sha3 증분 XOF 리더 추가 - 비밀 중간값 g_out k_bar r z sigma buf 강제 소거 보강 - NIST ACVP KAT 추가 keygen encaps decaps 및 모듈러스 거부 케이스 --- mlkem/src/kem.rs | 53 ++++++--- mlkem/src/kpke.rs | 3 +- mlkem/src/lib.rs | 46 ++++++-- mlkem/src/sample.rs | 32 ++--- mlkem/tests/kat.rs | 281 ++++++++++++++++++++++++++++++++++++++++++++ sha3/src/keccak.rs | 16 +++ sha3/src/lib.rs | 19 +++ 7 files changed, 400 insertions(+), 50 deletions(-) create mode 100644 mlkem/tests/kat.rs diff --git a/mlkem/src/kem.rs b/mlkem/src/kem.rs index cdce60a..7602570 100644 --- a/mlkem/src/kem.rs +++ b/mlkem/src/kem.rs @@ -1,13 +1,26 @@ #![allow(clippy::too_many_arguments)] +use crate::Error; use crate::kpke; -use crate::params::{N, SHAREDSECRETBYTES, SYMBYTES}; +use crate::params::{N, Q, SHAREDSECRETBYTES, SYMBYTES}; use constant_time::{Choice, CtEqOps, CtSelOps}; use sha3::{SHA3, SHA3_256, SHA3_512, SHAKE256, XOF}; use zeroize::Zeroize; const POLYBYTES: usize = N * 3 / 2; +fn ek_modulus_valid(ek: &[u8]) -> bool { + let polyvec_bytes = K * POLYBYTES; + for chunk in ek[..polyvec_bytes].chunks_exact(3) { + let c0 = (chunk[0] as u16) | (((chunk[1] & 0x0F) as u16) << 8); + let c1 = ((chunk[1] >> 4) as u16) | ((chunk[2] as u16) << 4); + if c0 >= Q || c1 >= Q { + return false; + } + } + true +} + fn hash_h(input: &[u8]) -> [u8; 32] { let mut hasher = SHA3_256::new(); hasher.update(input); @@ -64,10 +77,13 @@ pub fn encaps( eta2: usize, du: usize, dv: usize, -) { +) -> Result<(), Error> { let polyvec_bytes = K * POLYBYTES; let ek_len = polyvec_bytes + SYMBYTES; - let ct_len = K * N * du / 8 + N * dv / 8; + + if !ek_modulus_valid::(ek) { + return Err(Error::InvalidEncapsulationKey); + } let h_ek = hash_h(&ek[..ek_len]); @@ -75,15 +91,20 @@ pub fn encaps( g_input[..32].copy_from_slice(m); g_input[32..64].copy_from_slice(&h_ek); - let g_out = hash_g(&g_input); - let k_bar: [u8; 32] = g_out[..32].try_into().unwrap(); - let r: [u8; 32] = g_out[32..64].try_into().unwrap(); + let mut g_out = hash_g(&g_input); + let mut k_bar: [u8; 32] = g_out[..32].try_into().unwrap(); + let mut r: [u8; 32] = g_out[32..64].try_into().unwrap(); kpke::encrypt::(ct, &ek[..ek_len], m, &r, eta1, eta2, du, dv); - *ss = kdf(&ct[..ct_len], &k_bar); + ss.copy_from_slice(&k_bar); g_input.zeroize(); + g_out.zeroize(); + k_bar.zeroize(); + r.zeroize(); + + Ok(()) } pub fn decaps( @@ -104,7 +125,7 @@ pub fn decaps( let h_ek: [u8; 32] = dk[polyvec_bytes + ek_len..polyvec_bytes + ek_len + SYMBYTES] .try_into() .unwrap(); - let z: [u8; 32] = dk[polyvec_bytes + ek_len + SYMBYTES..polyvec_bytes + ek_len + 2 * SYMBYTES] + let mut z: [u8; 32] = dk[polyvec_bytes + ek_len + SYMBYTES..polyvec_bytes + ek_len + 2 * SYMBYTES] .try_into() .unwrap(); @@ -115,9 +136,9 @@ pub fn decaps( g_input[..32].copy_from_slice(&m_prime); g_input[32..64].copy_from_slice(&h_ek); - let g_out = hash_g(&g_input); - let k_bar: [u8; 32] = g_out[..32].try_into().unwrap(); - let r: [u8; 32] = g_out[32..64].try_into().unwrap(); + let mut g_out = hash_g(&g_input); + let mut k_bar: [u8; 32] = g_out[..32].try_into().unwrap(); + let mut r: [u8; 32] = g_out[32..64].try_into().unwrap(); let mut ct_prime = [0u8; 1568]; kpke::encrypt::( @@ -136,14 +157,18 @@ pub fn decaps( eq &= CtEqOps::eq(&ct[i], &ct_prime[i]); } - let k_bar_result = kdf(&ct[..ct_len], &k_bar); - let z_result = kdf(&ct[..ct_len], &z); + let mut z_result = kdf(&ct[..ct_len], &z); for i in 0..SHAREDSECRETBYTES { - ss[i] = u8::select(&z_result[i], &k_bar_result[i], eq); + ss[i] = u8::select(&z_result[i], &k_bar[i], eq); } m_prime.zeroize(); g_input.zeroize(); + g_out.zeroize(); + k_bar.zeroize(); + r.zeroize(); + z.zeroize(); + z_result.zeroize(); ct_prime.zeroize(); } diff --git a/mlkem/src/kpke.rs b/mlkem/src/kpke.rs index 6ff6081..127ffe3 100644 --- a/mlkem/src/kpke.rs +++ b/mlkem/src/kpke.rs @@ -22,7 +22,7 @@ pub fn keypair(ek: &mut [u8], dk: &mut [u8], d: &[u8; 32], eta1: } let rho: [u8; 32] = hash_out[..32].try_into().unwrap(); - let sigma: [u8; 32] = hash_out[32..64].try_into().unwrap(); + let mut sigma: [u8; 32] = hash_out[32..64].try_into().unwrap(); let a_hat = gen_matrix::(&rho, false); @@ -48,6 +48,7 @@ pub fn keypair(ek: &mut [u8], dk: &mut [u8], d: &[u8; 32], eta1: polyvec_tobytes(&mut dk[..polyvec_bytes], &s); + sigma.zeroize(); hash_out.zeroize(); } diff --git a/mlkem/src/lib.rs b/mlkem/src/lib.rs index 6c3007f..9accfa0 100644 --- a/mlkem/src/lib.rs +++ b/mlkem/src/lib.rs @@ -22,7 +22,7 @@ //! //! // 2. 캡슐화 (송신자: 공유 비밀 + 암호문 생성) //! let m = [0u8; 32]; // CSPRNG로 생성 필요 -//! let (ciphertext, shared_secret_enc) = mlkem768_encaps(&keypair.ek, &m); +//! let (ciphertext, shared_secret_enc) = mlkem768_encaps(&keypair.ek, &m).unwrap(); //! //! // 3. 역캡슐화 (수신자: 공유 비밀 복원) //! let shared_secret_dec = mlkem768_decaps(&ciphertext, keypair.dk.expose()); @@ -73,6 +73,17 @@ pub(crate) const MLKEM1024_DK_BYTES: usize = dk_bytes(4); /// ML-KEM-1024 암호문 바이트 크기 (1568 bytes) pub(crate) const MLKEM1024_CT_BYTES: usize = ct_bytes(4, 11, 5); +/// ML-KEM 캡슐화 오류 +/// +/// # Security Note +/// `InvalidEncapsulationKey`는 FIPS 203 7.2 모듈러스 검사 실패를 나타내며 +/// 비정규 캡슐화 키를 거부합니다. 공개 키 검증이므로 상수 시간 요구 사항은 없습니다. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + /// 캡슐화 키 계수가 비정규(q 이상) + InvalidEncapsulationKey, +} + /// ML-KEM-512 키 쌍 구조체 /// /// # Security Note @@ -132,10 +143,13 @@ pub fn mlkem512_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM512KeyPair { /// /// # Returns /// - 암호문과 공유 비밀 키의 튜플 +/// +/// # Errors +/// - `Error::InvalidEncapsulationKey`: `ek` 계수가 비정규(q 이상)일 때 (FIPS 203 7.2 모듈러스 검사) pub fn mlkem512_encaps( ek: &[u8; MLKEM512_EK_BYTES], m: &[u8; 32], -) -> ([u8; MLKEM512_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>) { +) -> Result<([u8; MLKEM512_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>), Error> { let mut ct = [0u8; MLKEM512_CT_BYTES]; let mut ss = [0u8; SHAREDSECRETBYTES]; kem::encaps::<2>( @@ -147,8 +161,8 @@ pub fn mlkem512_encaps( MLKEM512.eta2, MLKEM512.du, MLKEM512.dv, - ); - (ct, Secret::new(ss)) + )?; + Ok((ct, Secret::new(ss))) } /// ML-KEM-512 역캡슐화를 수행합니다. @@ -205,10 +219,13 @@ pub fn mlkem768_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM768KeyPair { /// /// # Returns /// - 암호문과 공유 비밀 키의 튜플 +/// +/// # Errors +/// - `Error::InvalidEncapsulationKey`: `ek` 계수가 비정규(q 이상)일 때 (FIPS 203 7.2 모듈러스 검사) pub fn mlkem768_encaps( ek: &[u8; MLKEM768_EK_BYTES], m: &[u8; 32], -) -> ([u8; MLKEM768_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>) { +) -> Result<([u8; MLKEM768_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>), Error> { let mut ct = [0u8; MLKEM768_CT_BYTES]; let mut ss = [0u8; SHAREDSECRETBYTES]; kem::encaps::<3>( @@ -220,8 +237,8 @@ pub fn mlkem768_encaps( MLKEM768.eta2, MLKEM768.du, MLKEM768.dv, - ); - (ct, Secret::new(ss)) + )?; + Ok((ct, Secret::new(ss))) } /// ML-KEM-768 역캡슐화를 수행합니다. @@ -278,10 +295,13 @@ pub fn mlkem1024_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM1024KeyPair { /// /// # Returns /// - 암호문과 공유 비밀 키의 튜플 +/// +/// # Errors +/// - `Error::InvalidEncapsulationKey`: `ek` 계수가 비정규(q 이상)일 때 (FIPS 203 7.2 모듈러스 검사) pub fn mlkem1024_encaps( ek: &[u8; MLKEM1024_EK_BYTES], m: &[u8; 32], -) -> ([u8; MLKEM1024_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>) { +) -> Result<([u8; MLKEM1024_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>), Error> { let mut ct = [0u8; MLKEM1024_CT_BYTES]; let mut ss = [0u8; SHAREDSECRETBYTES]; kem::encaps::<4>( @@ -293,8 +313,8 @@ pub fn mlkem1024_encaps( MLKEM1024.eta2, MLKEM1024.du, MLKEM1024.dv, - ); - (ct, Secret::new(ss)) + )?; + Ok((ct, Secret::new(ss))) } /// ML-KEM-1024 역캡슐화를 수행합니다. @@ -565,7 +585,7 @@ mod tests { let m = [3u8; 32]; let keypair = mlkem512_keygen(&d, &z); - let (ct, ss_enc) = mlkem512_encaps(&keypair.ek, &m); + let (ct, ss_enc) = mlkem512_encaps(&keypair.ek, &m).unwrap(); let ss_dec = mlkem512_decaps(&ct, keypair.dk.expose()); assert_eq!(ss_enc.expose(), ss_dec.expose()); @@ -578,7 +598,7 @@ mod tests { let m = [6u8; 32]; let keypair = mlkem768_keygen(&d, &z); - let (ct, ss_enc) = mlkem768_encaps(&keypair.ek, &m); + let (ct, ss_enc) = mlkem768_encaps(&keypair.ek, &m).unwrap(); let ss_dec = mlkem768_decaps(&ct, keypair.dk.expose()); assert_eq!(ss_enc.expose(), ss_dec.expose()); @@ -591,7 +611,7 @@ mod tests { let m = [9u8; 32]; let keypair = mlkem1024_keygen(&d, &z); - let (ct, ss_enc) = mlkem1024_encaps(&keypair.ek, &m); + let (ct, ss_enc) = mlkem1024_encaps(&keypair.ek, &m).unwrap(); let ss_dec = mlkem1024_decaps(&ct, keypair.dk.expose()); assert_eq!(ss_enc.expose(), ss_dec.expose()); diff --git a/mlkem/src/sample.rs b/mlkem/src/sample.rs index 18ab5f6..af220ea 100644 --- a/mlkem/src/sample.rs +++ b/mlkem/src/sample.rs @@ -1,6 +1,7 @@ use crate::params::{N, Q}; use crate::poly::{Poly, PolyVec}; use sha3::{SHAKE128, SHAKE256, XOF}; +use zeroize::Zeroize; pub fn rej_uniform(r: &mut [i16], ctr_start: usize, buf: &[u8]) -> usize { let mut ctr = ctr_start; @@ -69,27 +70,13 @@ pub fn sample_ntt(seed: &[u8; 32], i: u8, j: u8) -> Poly { let mut xof = SHAKE128::new(); xof.update(seed); xof.update(&[i, j]); - let mut buf = [0u8; 504]; - xof.finalize_into(&mut buf); - - let mut ctr = rej_uniform(&mut r.coeffs, 0, &buf); - - if ctr < N { - let mut xof2 = SHAKE128::new(); - xof2.update(seed); - xof2.update(&[i, j]); - let mut buf2 = [0u8; 672]; - xof2.finalize_into(&mut buf2); - ctr = rej_uniform(&mut r.coeffs, ctr, &buf2[504..]); - } + let mut reader = xof.finalize_xof_reader(); - if ctr < N { - let mut xof3 = SHAKE128::new(); - xof3.update(seed); - xof3.update(&[i, j]); - let mut buf3 = [0u8; 840]; - xof3.finalize_into(&mut buf3); - rej_uniform(&mut r.coeffs, ctr, &buf3[672..]); + let mut ctr = 0; + let mut block = [0u8; 168]; + while ctr < N { + reader.squeeze_block(&mut block); + ctr = rej_uniform(&mut r.coeffs, ctr, &block); } r @@ -106,6 +93,7 @@ pub fn sample_poly_cbd(seed: &[u8; 32], nonce: u8, eta: usize) -> Poly { prf.finalize_into(&mut buf[..buflen]); poly_cbd(&mut r, &buf[..buflen], eta); + buf.zeroize(); r } @@ -113,9 +101,9 @@ pub fn gen_matrix(seed: &[u8; 32], transposed: bool) -> [[Poly; core::array::from_fn(|i| { core::array::from_fn(|j| { if transposed { - sample_ntt(seed, j as u8, i as u8) - } else { sample_ntt(seed, i as u8, j as u8) + } else { + sample_ntt(seed, j as u8, i as u8) } }) }) diff --git a/mlkem/tests/kat.rs b/mlkem/tests/kat.rs new file mode 100644 index 0000000..5649913 --- /dev/null +++ b/mlkem/tests/kat.rs @@ -0,0 +1,281 @@ +//! FIPS 203 (ML-KEM) 공식 KAT 검증 테스트입니다. +//! +//! 정답 벡터 출처는 NIST ACVP-Server 저장소의 +//! `gen-val/json-files/ML-KEM-{keyGen,encapDecap}-FIPS203/internalProjection.json` +//! 이며, 벡터 값을 소스에 상수로 고정하여 실행 시 외부 의존성이 없습니다. +//! +//! # Features +//! - keyGen: `(d, z)` 로부터 `ek`, `dk` 전체 바이트 일치 검증 +//! - encaps: `(ek, m)` 로부터 `c`, 공유 비밀 `k` 일치 검증 +//! - decaps: `(dk, c)` 로부터 공유 비밀 `k` 검증 (정상 복호화 및 변조 암호문 암묵적 거부 포함) + +use mlkem::*; + +fn unhex(s: &str) -> Vec { + let b = s.as_bytes(); + assert!(b.len().is_multiple_of(2), "홀수 길이 hex"); + let val = |c: u8| -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => panic!("잘못된 hex 문자"), + } + }; + let mut v = Vec::with_capacity(b.len() / 2); + let mut i = 0; + while i < b.len() { + v.push((val(b[i]) << 4) | val(b[i + 1])); + i += 2; + } + v +} + +#[test] +fn kat_mlkem512_keygen() { + // (d, z, ek, dk) + let cases: &[(&str, &str, &str, &str)] = &[ + ("47B893474672BA92E4B12EE44FB32953AF8E8503B5FB471D1614FB8A021A660A", "1F8CB39E9E30BC458A0DC5408884B1187FB217018DF760FA57317703B844A0A9", "28266A088B3482439BCA01AFB7CA5C6136A979B5159985A9484B36B679A5F7B9819EB63577891F7BB9CB98413CCC434ADC79A16D6AB3076569CE6291C59B5D64612A7FB0C15013200BC8BEBB03A570174B5E4363AED86EB02A220D281FB5457F0A549FC5051D49A6B2015259A2C3084F405E1769952260675586A584904059275A265234EF3ABF88C171A80898FC783358BBC9803C8789027D917C9EBACBC568CC18DE84C85454B94249586C0C6E2B8A16FA789C51212DD1728EE9B8C6C40528BF93826FA82368419623032AF27B5694305816811D3CA85805100E9C1A9621E5089E54CB47F5A8FEA0B49EF81C6B5187F48924C7947D6B61697A4A8A18452EF803336AD4BE503275BCACC03C181405F7B1DC9B47FB169EB37BBE27E29C763A4E52B9A42520388CF09B8EDBCDF41CCF6537190E6156C37CC1AAC63C0F90CE78D0B9B190C548D71B6F26CC8F585EA14004B5B30AAA100B2ADC1263828833B24E46163B41446F98C882092A39941867B80632E2097674A793935227DB0B8577E03A69C50A514C7473C892E3FBA7C4316BDABC952A70644176687D4191323BAD93D85A3CA250868C0747E6C44F6126C874AFBEC0BDD4503CB2C59A69816E7D4109941467579A1FFE6A4F50FA379051729DAB6E2F61432F15BE67D667C7CC1054742B2B953078A5CF88D9133087309D88C61DA240D99C59137329907B47865321ECD5564E987333B4CB607B0AFCA86769DC95B2F921357213FCB80C3B152918E9BAB2228C0A1B77897AC68CE55088165F87F397DA9790873B62C5383C0CCC370F0267CBE195651CCF336182C22AC3924B76C9E779B7A271D166B6D24B84242B7E73CC723F764039F6C851744034C3304DB0C091A5764FDC9D593556FF734B82A87CCBC38CA99564D988BBD2D1BF071BB160722D365104FB27610651A8ED817F2742A6B5A1273A61ACAF4460B0AB1456A9922351400A1C7D95D856D6E3370622C9C4164BC6B401435624A98B95CAEB274F34CE92038D785068CDD8CF44C38D84ACB2C466A2756C870EE78C26E738CC451002304EB8C90AB24B6463EB124D779F937A2E3692611D2E34D57B36CC4B2CD3B31FF485C6684D408B972E0D5CA7D2224AAE4E", "89C31D05611AAAB258F78BC2DE0A80D5914BF80C376A990D33CB97F4F2077CE12D2DAC559DE3400B0622754A2E814730BB7C2B401A076CEC9524654DDAC661B2F2123EA64D3BD727CA42C8D2725475BC0AD4B698E61001D031105897CA746249D24538CD63849E874EB9449FBAE979CA2A3F357B7D87F112FB16AB8BA20BDD315688231B21A4083277663113BC70A806E772917FA95743A01F138BC7BD5CB3B21CBF8EE301EB0BCE71A4BAAC3907B469CBEDE767A55AA194930B4A2B227E633CF33A3FC715454A32873B717FE6A21C01989133E42113D985F46807A26B0DE105CD9A897334D9816075C3149A1919CC2DBED56A23F3192CDB68F6E305852B8B864BACB5349CFD98419FFC9486C6B07F788CBC79B7680441B0DB1DBD92AEDFF49DB6F2AE52B95FA9B966C6CC08714A131770903859B823D2CF9731A36D8795B60585F8F1BF1A2C4E544407D5C1359810BF99A7374511A663EC1B192B83E27C4F4B6C3387692E4D756A235C14D151056E829FB79936A0C8A52EE128F0F369F8EB7390783788037CE32B021E57C45EB968CD5B9A04B02F89149778653F208C14C2E786D4544749572BF0D9A31B13BBAD60B6756B9379FC2E1660C58ED373C957309EA213213A5988CC59B1C37153811F7271BC1E729AB1629714D1955F706796556213B08E3F766867D1CC02648BE0574BB8818D0978578C562FFB6795DC065037129C5E4738B5960FE7E2A22570C36571A4A7A0A5187609F8D27714CA299309509C7C8AC2776E6814A262359B971B1A153CAB81858D96350F9F2CCC9884C8F5509E237655B4B2B22E75C6919A82EB56B095E3C52B55583F6855E186CB75DA3A20640762922062721D5C1AC51B87B10633BF43860814C89CE16BBF66667C2101BD27BA80969C78DC487678989EF5AB22D9B42679590E1855C94A9A90B4E021333A4532C7AE238358646094B598AA431486D0B7BBD82C5C4EBB6195680C3E5AA806B155AF7004318867FE55B1996353495034DF459624F4B04B51167A5148499846A36C605AD85B997674DF241D8EC59E56E22CCDB342F535292AA444B69C3E7A252128266A088B3482439BCA01AFB7CA5C6136A979B5159985A9484B36B679A5F7B9819EB63577891F7BB9CB98413CCC434ADC79A16D6AB3076569CE6291C59B5D64612A7FB0C15013200BC8BEBB03A570174B5E4363AED86EB02A220D281FB5457F0A549FC5051D49A6B2015259A2C3084F405E1769952260675586A584904059275A265234EF3ABF88C171A80898FC783358BBC9803C8789027D917C9EBACBC568CC18DE84C85454B94249586C0C6E2B8A16FA789C51212DD1728EE9B8C6C40528BF93826FA82368419623032AF27B5694305816811D3CA85805100E9C1A9621E5089E54CB47F5A8FEA0B49EF81C6B5187F48924C7947D6B61697A4A8A18452EF803336AD4BE503275BCACC03C181405F7B1DC9B47FB169EB37BBE27E29C763A4E52B9A42520388CF09B8EDBCDF41CCF6537190E6156C37CC1AAC63C0F90CE78D0B9B190C548D71B6F26CC8F585EA14004B5B30AAA100B2ADC1263828833B24E46163B41446F98C882092A39941867B80632E2097674A793935227DB0B8577E03A69C50A514C7473C892E3FBA7C4316BDABC952A70644176687D4191323BAD93D85A3CA250868C0747E6C44F6126C874AFBEC0BDD4503CB2C59A69816E7D4109941467579A1FFE6A4F50FA379051729DAB6E2F61432F15BE67D667C7CC1054742B2B953078A5CF88D9133087309D88C61DA240D99C59137329907B47865321ECD5564E987333B4CB607B0AFCA86769DC95B2F921357213FCB80C3B152918E9BAB2228C0A1B77897AC68CE55088165F87F397DA9790873B62C5383C0CCC370F0267CBE195651CCF336182C22AC3924B76C9E779B7A271D166B6D24B84242B7E73CC723F764039F6C851744034C3304DB0C091A5764FDC9D593556FF734B82A87CCBC38CA99564D988BBD2D1BF071BB160722D365104FB27610651A8ED817F2742A6B5A1273A61ACAF4460B0AB1456A9922351400A1C7D95D856D6E3370622C9C4164BC6B401435624A98B95CAEB274F34CE92038D785068CDD8CF44C38D84ACB2C466A2756C870EE78C26E738CC451002304EB8C90AB24B6463EB124D779F937A2E3692611D2E34D57B36CC4B2CD3B31FF485C6684D408B972E0D5CA7D2224AAE4E3A389831056ED8FD81476869245782689C84B3CE90FE6A9E78D0A380FD6A15731F8CB39E9E30BC458A0DC5408884B1187FB217018DF760FA57317703B844A0A9"), + ("3DEF73C558E1F5F2BFB4F6EAA36EE200CDF54580958CC2176282628FF508A016", "B9958575128F97022525266064F9422E83F75FBDCD0F2BC2648655F0E31A17E5", "CAB840FD02818B280C161B2C815853580A92DABB3A7A070026128A75D86CDFA3BC012263E571365B4489907A7ED56AB2891381111131D040587E66B8F870A17A36B73AD8B35C02ACE0E7AC274644A6DC23A7401EF0E547CFB18E9C969164B78C36D06CAEA48670E63B187925EA208757B98BC2272C5DB5256A72BCF2130B9B93B2E793222BBB97AE464B042989811B37BDD38610895B65712C619C9AA1E78D872680B8730D1B7BA79121CC798B3E952C3181F071F19707D7BA1FE99C56A5BBACF4A170E24422C3478C84CB95D0167196A47CB78A17A0E414A1C37F0816594FB0888000C3242B2820BA4D65A15F1E7633117736FEE790F10A5B4567CCCB191466E40CB5E5661D433000E562A0029108D4CE91218625083602CAA7C13411DFA7137DC631D77A82DD4A05AEF81A47B77E3D6998147A7BCCC929F75548E866A6D1D9C7BEB70E63D706622B05A863A9C9D700A13B6426D34374D8BB0E007D8AD7CC1806B152C38DAEEB75DC0C5791C03485B8B45E7ABA834026F2C1BA5554277F636F59005F99D16F3D6430C37C2D24C7115EE04648C88DE90925D5B529AD0B7C3213AC9A6516D395239B91CBEB46547053C98B1C211E64CEC295178A1B2D194A3511B82B21C651E77C7BD1A5131599C5AB4C656CF4BD3763CBD01912C2713F438A463BE24B462B473CFA7D7CD58EDA3C05256A2DB4C57CF8FCC0D2FA6249CB9A36D44F9B8C9DE9E84CF3F49F7D8C08DE9230766C1776929E2F00453D28470A350AEE005A4602812AF31926085DC9FBBFFC3A1BAB2A2297C4C98346B7D48A5D3EDBBBB666AEA8A39E3E96885F0AB67CA81BCAC292A15C6C4789A5DE863676B123DF7B3FBE355448D9AB89C5014D0CA282ECB2504B3685A239DE6603FCAA755B5175A41CA876707CC56A31E3DB98D9BA612A3009121576449BB4E5ECCDC57330813C7CF692B11037122D026BE300942A8A217891672CC41FBA185A62B3B9D67419AC15B472D6C355F830D8EB56D5C892DC58BE0ACA8C6B304CC86A56F3CACC392B1503EA73E22863E6F9C3171A9E18F0331C4C5BC9553CDB4655926A6C4E1989A330B5323F422B9FB1329DB7BBE340AC26F11C6299252EAB254260453FF7B7F4ADD0F4", "3594A400134AE29552DB981BEBDA33EFF05BC5AC7FA0B539F92216CE790957EA34592011AF42AB6BE51311208C5A7A787AC84A330A28DCF6A69DF879CF876401FCB0AF84569C4B1FF556556BAB21F3728C8F172CEBDC3C684786DB0255746C582C3729BBE97B1405378F75979A9B07FDBB36A1B28E3C8C96431CBCE301510B7BA596F34E24F99499B0336F23C0DC5380C044B8395201491CBB85AC11DA6BB78E3A66545B40284C0A64EB8BDC63BA351B95F5EA29E6051C76FB0F7726BF3EF16D531C7CD7CC49ABD584CF237DAB0014DB49C8AA970EE6F39770E48CF44B0E64B86A9D4824042556B3331BCF245C42EAC9E2BB862615230D19B854B55BE721C2E23947FD7C74F3304155626731B4AD38064BC0FCB726E48F316AB24A4A8731DA0B28C0BBED56AF00909C1D0B5F86CB2105E37DF1385A4D8A038980C833B52FF6A1086589173BB96601713AE94C0EF48BCD567B8836985A21650AB72021E675225BEB09DDEC05FF324BC90840FEB8AF2046543F8933492558D6F42552553E477C29AD601EE2571DDE98399D77914B098E07F291167B041ED60AD8D5794FA65AC7E7663FC962AAE22F4375793E7A886278751ADA4E791743CB12CC5F24BBA5D57F3C3B61CF48315C6226DA27C8B7931C3D304F3156B47A07B0EF4CB68937473C982208D5AAFE3CA2948822EBC02BF5A01F62E09CF1B4AB95020306559677FAAE0A677574711CF0A92FA1A5240BD6323DA812AA437A449058589745790037EDA03F19D88AFC5A3D6B18ABA4A2605CE42568ABB7C1F303E508534FB7A10B77C273419C93F418801A81C4F3AE003BAFDD309381DC4DBE8065F978B3EB3B19E03597BC979C0CA0B36E830868E289E2CA54C2196C99922B9047CD4FE7B259A4447F41AE120185F9141EF1B186F68A869EA4373EA654FD6386EF2CA63E7CB68F8454506891C0BAAF3A66953FB26DA03C04F3A27B1F1956E4922E336314AD360D114C395C89810197BE73B8CEAAA1B00AB079A2D33C215450E4FC33F38383E2834F0E143173C1C1ECE2AE19BA48E0FCCAB0CC5712C9BCD8229C2AB00729F37B3BF974D2F718CAB840FD02818B280C161B2C815853580A92DABB3A7A070026128A75D86CDFA3BC012263E571365B4489907A7ED56AB2891381111131D040587E66B8F870A17A36B73AD8B35C02ACE0E7AC274644A6DC23A7401EF0E547CFB18E9C969164B78C36D06CAEA48670E63B187925EA208757B98BC2272C5DB5256A72BCF2130B9B93B2E793222BBB97AE464B042989811B37BDD38610895B65712C619C9AA1E78D872680B8730D1B7BA79121CC798B3E952C3181F071F19707D7BA1FE99C56A5BBACF4A170E24422C3478C84CB95D0167196A47CB78A17A0E414A1C37F0816594FB0888000C3242B2820BA4D65A15F1E7633117736FEE790F10A5B4567CCCB191466E40CB5E5661D433000E562A0029108D4CE91218625083602CAA7C13411DFA7137DC631D77A82DD4A05AEF81A47B77E3D6998147A7BCCC929F75548E866A6D1D9C7BEB70E63D706622B05A863A9C9D700A13B6426D34374D8BB0E007D8AD7CC1806B152C38DAEEB75DC0C5791C03485B8B45E7ABA834026F2C1BA5554277F636F59005F99D16F3D6430C37C2D24C7115EE04648C88DE90925D5B529AD0B7C3213AC9A6516D395239B91CBEB46547053C98B1C211E64CEC295178A1B2D194A3511B82B21C651E77C7BD1A5131599C5AB4C656CF4BD3763CBD01912C2713F438A463BE24B462B473CFA7D7CD58EDA3C05256A2DB4C57CF8FCC0D2FA6249CB9A36D44F9B8C9DE9E84CF3F49F7D8C08DE9230766C1776929E2F00453D28470A350AEE005A4602812AF31926085DC9FBBFFC3A1BAB2A2297C4C98346B7D48A5D3EDBBBB666AEA8A39E3E96885F0AB67CA81BCAC292A15C6C4789A5DE863676B123DF7B3FBE355448D9AB89C5014D0CA282ECB2504B3685A239DE6603FCAA755B5175A41CA876707CC56A31E3DB98D9BA612A3009121576449BB4E5ECCDC57330813C7CF692B11037122D026BE300942A8A217891672CC41FBA185A62B3B9D67419AC15B472D6C355F830D8EB56D5C892DC58BE0ACA8C6B304CC86A56F3CACC392B1503EA73E22863E6F9C3171A9E18F0331C4C5BC9553CDB4655926A6C4E1989A330B5323F422B9FB1329DB7BBE340AC26F11C6299252EAB254260453FF7B7F4ADD0F452BE25FBE4D87AA01253DA8891D2592A416E2F791479E50ABB19E8F215DD6216B9958575128F97022525266064F9422E83F75FBDCD0F2BC2648655F0E31A17E5"), + ("309398D92B95B68A725ED55D3E72AA96717FA08A9B919DCDABE7D893A73AD7CC", "C2FE9CB7F49BF7296785A1824CD798C377BFF4E1836473CD4A8C7423BF1D0F13", "5DFA5E5FE41C3F4064EF11004E943E7913458659CA5A0455D9BA880815B062879BD903136D3A73D021A812A9669DF0A68A04CFB11742307919103C8FE3710A95491204970583D49726A324A6BC240272A5CF26B56C1656F996B37FB430E27CC7A30742513439535B3C5E29C302ABC00265A2D7512027969F8A50CC2B22901D16AA719906D5094C8A91A3906CBFEB852CC3A581FA33B47685957A831AF48AC244F766DE32A4A0190ED48B336E4155A9795930DC4209DA637DF59CEE3033250083E8AAB97C30C9EE75196148CE6CC556E8B54AEAE743FF9A0CC49B00D7B0AAF35387A22403A4956056FCA423244877995FC272A9A623C176C911AA9B0227AC57FB204600546E6E10C843FCAAEF8A418E6739EE4669DFC577BCA851FCBACEA3A699A25BCA3DE397ECBCC9D31C3DBD0CB461A07DCB976F900BBF44CA3127121D48127D5FC3325A0643F0233C6C45583C217CC9A17A4882260544B43CC0972B730A2948639C29B5A0F383023112FB106CD9700D011B091A83CB099C21F2B2C08B3994FDD5369D4C5AF61896BB00922B113F80699423B1BC4A5885E95A33DCBA6E8165C4BA0A7B2BB16D00FD32B20837D717CD55C91189734D889C09416C89CDE21DF4BC7787780A1B4B50A3AA66ED38539B71596E473965165BCFA701A0153A46C9AE0B6B588A136BCB814FE0A8CAE7D63D2BF3AE61EA2FCB14036A76756B0387DF7123A73620DE7ACB82AB23991508D03078D83B221C047F864C15D7BA75C722988305798203849FA0BE732B23F41084032C75F097BDA90B2FED6C51ADC56153951F0F96BC9951CA34BC1B4C972F4515234ED664738CCE4330681C36BF773C4783216F1E964A0DE4B1A1923183E232C45B6992EBB8E0D5154261274DDB1AA6D2AA37D836D51BCB77DA1934B510A22A83072707412ABA89AC81F7D9A5B38B84CCD03B56B7133373991419CC09110FA1DB151D453BEEC20CB46163DE304723D947858299615B9E9CB15553207F09772116C843A315C389F160508A001BAA76F92759DB79B4613379AD66B7986303BD17C872DC9D276482372861F06B805B03C55F251A812137CDBA5AF5C65C2D795EBF84EE48F10338AF317CC5C961AE4AE8243C5F0E", "310AB0E7D75FD82CA23D0BAE25C4935465BA8596C6D08A44BBD6A9AAA51F741607A71238A9A24E3A878C388A8099D66A61A4474876197BD671D25A939D66B7D811541381C7C2694C53C8399F40780F5C3960366D70E76353676E0AA024ED8B9EFB81893FC1A7A06CBAD4BA2D6A16828ED5002B808ED52124BB8B21CAA53038EAA15509C1FAB3478CB39A4EA4A9644174A0F1A46E707240EA6C1B32CB90B0608370C238F96DFC404E00251F8E809DA6B72829578495E9236E1019B651B49DF920CA2697B8051C7D973538998D13A867D51AB491841BAF91A9C9F1454057937150361D7CAAFD9A9A2CF51158F659D3296BB4E30A32AC5E4AD23B3552AFD5C7CAE9F338A0191EFD4624EBA24DAF48C7453CA87ADA377C69C1DA2ACEE818CAB702434E5716695B2513F830CE797EA9EA0F5A04AFB84777ACDB7399D247797A5FD6C62368D8622B773B1C22382B5C1EABB3B09312BFE60C7A9972BDAE9AC737697B6CFC0582F938658A60E8D015F670B373D2A00021A890B656051B98E7182E08454E7F31355ADA6C56C7CF9D528AC1409A4A015B471472316610240C4570D6A2F7F297ED76A2EDA8B703427A22C056D4A549BF859612A56A5AEC469147118DEA0061E7620CE30355EAB40BA1B6AB4B39EB77061ADB9C0EA148B05BC0C7B6BF08FC91B20971AD8641051B230BA9C1A8D5C8C500231C88512361937C36CA9EF488913747C9E77346505007B55147F3540938983C4CCAC0733101C46B1A138AF1DB42F926C18CFC86A65B40ABBB25D3C4A51BDA8FC5D63CE510207B1932DE31CA0651323EC6C5F8826720C09413B6145E54CC18F9AE41B71F369B7F07207563707651797EE88A92544A46BE59063D95C1E88B7D4846C894338BDED33E1B088D81F48332C2079504147D493755D15084463D23175CFBE7A293424B66A61A71155C8C176FC7528C8A576418D577E9858E3FCBC6CC51C65040847ED6404B7A44B9E82F321C28D6176CF31C7F722B9AF7B9598348A27808A50A0374D581343EDAA74BC37187709879D17C8B853FD659645289B929603BF245AEB5EB92D655A7B3C42285C8235DFA5E5FE41C3F4064EF11004E943E7913458659CA5A0455D9BA880815B062879BD903136D3A73D021A812A9669DF0A68A04CFB11742307919103C8FE3710A95491204970583D49726A324A6BC240272A5CF26B56C1656F996B37FB430E27CC7A30742513439535B3C5E29C302ABC00265A2D7512027969F8A50CC2B22901D16AA719906D5094C8A91A3906CBFEB852CC3A581FA33B47685957A831AF48AC244F766DE32A4A0190ED48B336E4155A9795930DC4209DA637DF59CEE3033250083E8AAB97C30C9EE75196148CE6CC556E8B54AEAE743FF9A0CC49B00D7B0AAF35387A22403A4956056FCA423244877995FC272A9A623C176C911AA9B0227AC57FB204600546E6E10C843FCAAEF8A418E6739EE4669DFC577BCA851FCBACEA3A699A25BCA3DE397ECBCC9D31C3DBD0CB461A07DCB976F900BBF44CA3127121D48127D5FC3325A0643F0233C6C45583C217CC9A17A4882260544B43CC0972B730A2948639C29B5A0F383023112FB106CD9700D011B091A83CB099C21F2B2C08B3994FDD5369D4C5AF61896BB00922B113F80699423B1BC4A5885E95A33DCBA6E8165C4BA0A7B2BB16D00FD32B20837D717CD55C91189734D889C09416C89CDE21DF4BC7787780A1B4B50A3AA66ED38539B71596E473965165BCFA701A0153A46C9AE0B6B588A136BCB814FE0A8CAE7D63D2BF3AE61EA2FCB14036A76756B0387DF7123A73620DE7ACB82AB23991508D03078D83B221C047F864C15D7BA75C722988305798203849FA0BE732B23F41084032C75F097BDA90B2FED6C51ADC56153951F0F96BC9951CA34BC1B4C972F4515234ED664738CCE4330681C36BF773C4783216F1E964A0DE4B1A1923183E232C45B6992EBB8E0D5154261274DDB1AA6D2AA37D836D51BCB77DA1934B510A22A83072707412ABA89AC81F7D9A5B38B84CCD03B56B7133373991419CC09110FA1DB151D453BEEC20CB46163DE304723D947858299615B9E9CB15553207F09772116C843A315C389F160508A001BAA76F92759DB79B4613379AD66B7986303BD17C872DC9D276482372861F06B805B03C55F251A812137CDBA5AF5C65C2D795EBF84EE48F10338AF317CC5C961AE4AE8243C5F0E7FF658BC933FC07E44F8B4B3C3125FF493176D18380C6F7816B229D6D2F56EE3C2FE9CB7F49BF7296785A1824CD798C377BFF4E1836473CD4A8C7423BF1D0F13"), + ("FA58CAE89E664853035BA4EA8723C83225B89C8365024FE73A3E1B7AE01E6648", "ABF5D4E34BE28DB2984CAEFD09F06095921B3C991EF8C4CE3B16CA2C18E0A0B2", "40E90144374B8801711753236C08520F53BABDB82EA11B3E092B461D837C0CD95D90F7BCFF16956DF280FE69626825B26DB54E28129B07A0A32CA7726F7A93E2261EC5D604463C64669B7A7EE24A9BE822FAAC57837A4274B106C346143B0087FC5381C54040D4D897506C9C56385147F49D3FFAA51B9759B2E6A119AB7C421A8B48E0AA1C1C3C3F636349887E315A02A0B67135B732AA406090B09FE6B000A52C0E9FA737ACD1C8F4E2C31A17349AD60193950100506B957A6E381578224B37A2452B602AA9738884579B1D6DB6886364C07648B4EE6B7FA99418A891BEFDB67B7F76645F902DBEB117191073CAD23F9E227AC589708939B0FC6C60EFF7A321903224F342D4D29D78C660C1178204B80B0FA477F3F83FFDDA0AA213368EE3BD75B0C3D2C8533634AF9D94660B232775DA0E1788BCEF99B9642299775655AB63A1CC865D863834CA933A39E865198A8A93514DA351278AE67649203D773792AFC551E22367EAF48FCFB4507FF3ADBE217F4653642FA05BDC00AB3BCA8627F1ACF89040277C0DCD071136F97FBA75B59DCC3D0D22355E54335823BAC3904606D79EA845BC448C49BA2224DCAA7DDA9A583A16AAD382A8E15653AD19085FB9A2A70A1CA82ABBBDD6C05AD081E4C7686E917AF652AD5DBBC19FFC397BBB13EA7C82ACAC1A6695AABA32B39AC593774B1BBA46044459B11A7B134218C89CD24C22A49077D75BB3681F70059558916631911A05758068709671B02F04D0004FF31AB3D08FA68C63FE9BB95D4360810280DF384ECF773D0B3C97899596EFAAC05B8C816FB7877446366140C4B4150EF3F5A742076AD1A0A54100AFBCAA8531B1AD807B785F833C6F819C6DB679B0F0B087E555F8893C2ED7B6DF945DB623B21E78540F424FF66329464A8A59BA7202D8AD85131D81060928A45899F06332C0779D45A4D4D25EDD81138F9B70D3F90CDDDC2845557502413DF58C2E95692CCDD61A369A40A830A4B3E629B7BA81A1E873228109A5A24E4D6613224193201198FE6058E009BABC0757919912C0534A726304B8118831F73D66095F4E409941AB2595F994D84F674FA40EAE3C8B8F1E46F114141E6241795B3BB324521BD8E331A6B78663", "41E1C66EE49564867327963287EA72B0347DEC961A1F651A454771586C238398AD28730AD66B9AB5433E26AC0263A92B2A233785525662191507822FA546CF4ED82493353DE20B0D9A616A79BAAFB6252A812B9363A039B7E2AD61F417D08230E247A518E022B41A63FD47874DBC5274D0C332DBCDC335B5070419D999CE8AC5077C85691D167144488EFFEC5C16482E8D285DE572980E877223903A36912F4B0A409FC17501B8A299447D6D6518F04945E209C8985CB6C207042B446F74D5AC1B7A8429A5AC60F20BEF28926CD618441A5363C2B25231989F814732D948350C481738BC567190678310ACAB5665F9C09028038B2A1CAA27709ACBA80B5487918412458472B9E8758C2051A88240FF3340A8F8C5E44664A23A0201DB9D5DBA09C5345A11EC03FF48AA651A5BBEDB32D7DC4750A0ADBF81C64EB43032E6C828698AB97BC8861A101B05937C23C6DE419EC5F635C949A865731FB6834164F04BF664848C04B6C6EB8664D02492C64D9AABC504583531B4909E492FA1B726D5BB8CE04457E6038396F352770C11ECDBCA271CA9E6704A64B7749EE8A8DE925C831C31F70550AB781768CC8713F820E291BA39C0BAE2D3C195934429F5B322E8017A584482C608655A93111583D5AB5FA887556FDBAFF6076BEC4342DD9647EB1B3091BA559880186B9C0F63606DB3B390A2714546817C0D80290188751B4885F71362A097B7A97925969B553874081C57382FB92508E81C29E7A311741C6104C3D51A7A9A8B3FAA4703F8A94899317AB5A017A211A4F369C2ACD2235A24827E6012214BA87927179678CC5A8BA309911EB1D701767B94535532882C00C99123E4818BD968B8DC4CBA6D68972AB80FBA50C0DAFA2716A6B79BF021FCCCA4E5585CBCFB77F32A2DDCB06A09F37362D83FC2179636C8A720679C1EB604811C2403D4BA8005B11B75950346CE09A22306533F0F61659423AB449A03AA6A89AF2B7B90739F7FAB0D4DE9733227663DF397A0A5771BE893EEFBC7A1570C7D206CA6143C2DA3429D1C10A5F153F512B58651AEB3EB72CABB437B19A8AFA7BC21A8A527B07440E90144374B8801711753236C08520F53BABDB82EA11B3E092B461D837C0CD95D90F7BCFF16956DF280FE69626825B26DB54E28129B07A0A32CA7726F7A93E2261EC5D604463C64669B7A7EE24A9BE822FAAC57837A4274B106C346143B0087FC5381C54040D4D897506C9C56385147F49D3FFAA51B9759B2E6A119AB7C421A8B48E0AA1C1C3C3F636349887E315A02A0B67135B732AA406090B09FE6B000A52C0E9FA737ACD1C8F4E2C31A17349AD60193950100506B957A6E381578224B37A2452B602AA9738884579B1D6DB6886364C07648B4EE6B7FA99418A891BEFDB67B7F76645F902DBEB117191073CAD23F9E227AC589708939B0FC6C60EFF7A321903224F342D4D29D78C660C1178204B80B0FA477F3F83FFDDA0AA213368EE3BD75B0C3D2C8533634AF9D94660B232775DA0E1788BCEF99B9642299775655AB63A1CC865D863834CA933A39E865198A8A93514DA351278AE67649203D773792AFC551E22367EAF48FCFB4507FF3ADBE217F4653642FA05BDC00AB3BCA8627F1ACF89040277C0DCD071136F97FBA75B59DCC3D0D22355E54335823BAC3904606D79EA845BC448C49BA2224DCAA7DDA9A583A16AAD382A8E15653AD19085FB9A2A70A1CA82ABBBDD6C05AD081E4C7686E917AF652AD5DBBC19FFC397BBB13EA7C82ACAC1A6695AABA32B39AC593774B1BBA46044459B11A7B134218C89CD24C22A49077D75BB3681F70059558916631911A05758068709671B02F04D0004FF31AB3D08FA68C63FE9BB95D4360810280DF384ECF773D0B3C97899596EFAAC05B8C816FB7877446366140C4B4150EF3F5A742076AD1A0A54100AFBCAA8531B1AD807B785F833C6F819C6DB679B0F0B087E555F8893C2ED7B6DF945DB623B21E78540F424FF66329464A8A59BA7202D8AD85131D81060928A45899F06332C0779D45A4D4D25EDD81138F9B70D3F90CDDDC2845557502413DF58C2E95692CCDD61A369A40A830A4B3E629B7BA81A1E873228109A5A24E4D6613224193201198FE6058E009BABC0757919912C0534A726304B8118831F73D66095F4E409941AB2595F994D84F674FA40EAE3C8B8F1E46F114141E6241795B3BB324521BD8E331A6B78663C8C4EFE33D072A1F4DDF63EAB34ED9F1D5B34E15A0BC3981BA572545E773CD76ABF5D4E34BE28DB2984CAEFD09F06095921B3C991EF8C4CE3B16CA2C18E0A0B2"), + ("FF466BC223EE9EBF1DF06432873A6816BE864CFE408B2D676945311E31B7BF84", "482303CBF9E3963784E1BAC39086AADF7DE6DBB45E61A8021265A5BA2522A1D8", "64D24F31ACBCE41742EE149B50A331F9951D92449ECEB58C85520496770C543A8E4903574700083AF5CF85E7075AB494D406C44A106CDAE48F8F5C37709CB6F8A448957991FBBAA68A7710080A9680A3527A9B2036617EF91585369AB1A8C865D58B1F8AAA6F53BC32FDDAB1AD46B3E5274C6C09654C284FD8339F2A883118E69FBD262F057A4B1B2BBE5EB8B6D1988034A6184CF881BD642CB39059B6021257E37218E4ABDF6AC13200606423BCC96B3A7A03CC5F915D3C578158734FA748BFBC1712BCCB9213BA49ECF4181B2C2DD8007DCC347CBD3C4A5FBB833468523E6B58CE607E4AEC716F8A44EB211718C89899E5AF9396BF3AD50397B2ABDA921B19CC438E890D82C14D6053C735C4C3C3165D4D58CF74775D478C4BAAF2015795B061B0C25E52B6610448F8818C00E04F5825504CA995043288F4889CFDE1129646A304A1C583358534FB0A8110995AF7434A5447E6B31448C50D3C87481B626DD17B95242935A2C5373C8A9E1207AAEB60AFC4C96AE1B790899CB5C28055723547594B918A84BA82D885B0B41F4DF755134602750233BCA285295C3022E037CBD03CD2D7B141C06015E8A456C2A373CAC509805472E57A04186BF3A6CF36C7BFE76812B20CBC60F59CB5C0B378E1609F514FE7894852A329DEC82FD763684772B0666055C512A35457287D73B557D446CAF2A3BE425F152A62B91893CEB72FCD52156C22294B6421D53074788A4C0A23AFBD1A8C5375118AE2879912CB5B198EABE46E5502197D23315730458119BBFFA64EACC12C883A545A67288CB87D11004A4EBB1F692A127BDB0CBDD54F2E83951BD77498B90F8EA192805819F8582B91804F7022B5A0B75D88792EB7A94044D4BC0A583B45A1B2E2F6024E99C7959963F1C54478181822058E55637F755978B8F46A789BAA99EB88B70AC2056A5A752AA8753B8511AA77980C4BA1392D1D9BB77216089EC0BFFCA661998801BF499E7A4B82A2035FAF273D2B28322DD71E5B342E50016D29D91B81B405C365A9EE0CA6859230B3188D4BF9BE89EB651D7768D7C4807644929E964B17E98FF5EAA35A9816BD09B7490066720FCE37A99C7F1CA6EC12E3EDE24DE9BE115230F6E56ECE1FA2", "4B95C17A934210318070956FE0831459483F97F5016421790D6387AFE4A7FAE52461AC7A8B84425BC21A9AB9CF7E778023C69986693509F62A30617DCFE6805CE38CC034587A326692576E3857A581B79C2EB0C6CEF13217A44268036A9A911C9E52A521B03F22EB405DE4AD255211F210553587C20CE34A54738117689DA806B3D8C1AC33F7A0351598C86B1C57D97781A9B63C31C50D832FA4A25C71475CA5D03031C316E2ABBC64967B3E9A2B49F3686073CBB1D9CD02950F4E08597C423BDB386E06E782E10BCB4CBA9ED90ABBC8CAC59915CAB644329526652FFACCEB03AE02928CFD3B18DBE12C4D478FBDD56ED43C1F8305067E355D3209314BD6741DDB89811B541F2073600319F37002297601DEC38DC2E0BC5E87BAE0093C7DD27DF0A10BED4AB42CC884B19138895A6200DBCCFE2B0A7542CB27B7026FE544CECA213E78B0747373FA001E2DF3C354CA27A9818D6DE7B09AC970084B237D1791B6892EC9E8B632A167A23217A8C3981AD009CD127C67A40E3EA90D4967535BDA4A246CCDD0C103C4E23BAD2A4A2222A277A3BC2FEB769DB09D13EA26D20718D9E98E05C4B51523B834D9691B2969ABA3AC0632ABF3C26711531BCC6A54636A3422065B00F07A7EEB96124341E875B859738785126CFB2C4652E832841B38E3D50946C47B67746FF69C4855EB6D805879ACE8464880664D8BB52B35CBEC3C7F4528A826D92D7FF153AE7046E0EBBD89A16E73E554428903FF77000E70147BC7689EEC40207965AC105C1BA95CBFBA584DFC6431BA82F8F849B87CB7F55978908450958B578E3869C9348B445945E8635571CA1D237B9F2FA25BFB85477BA45D1DC974027BC903BC0B3DE111EC98722E3AC4E25497DEA304F8B4022753ACBA83BFC0187FE93C6D8271CAE796C4C701B60B4A434825AF44CB1C807C44F7655BAFEC2F4365B1357087E1096E6A6A49D2C686C52A7E0C5C07FAB6B1AB668088E9CB4B3C7316421A3281850EEB358B67A77AC40CDA765975D342B5E6BD1854583F038A56D82FDB2C8698180E0037722B2A8616B96DED13A2FD387E9B357615849C8F09B464D24F31ACBCE41742EE149B50A331F9951D92449ECEB58C85520496770C543A8E4903574700083AF5CF85E7075AB494D406C44A106CDAE48F8F5C37709CB6F8A448957991FBBAA68A7710080A9680A3527A9B2036617EF91585369AB1A8C865D58B1F8AAA6F53BC32FDDAB1AD46B3E5274C6C09654C284FD8339F2A883118E69FBD262F057A4B1B2BBE5EB8B6D1988034A6184CF881BD642CB39059B6021257E37218E4ABDF6AC13200606423BCC96B3A7A03CC5F915D3C578158734FA748BFBC1712BCCB9213BA49ECF4181B2C2DD8007DCC347CBD3C4A5FBB833468523E6B58CE607E4AEC716F8A44EB211718C89899E5AF9396BF3AD50397B2ABDA921B19CC438E890D82C14D6053C735C4C3C3165D4D58CF74775D478C4BAAF2015795B061B0C25E52B6610448F8818C00E04F5825504CA995043288F4889CFDE1129646A304A1C583358534FB0A8110995AF7434A5447E6B31448C50D3C87481B626DD17B95242935A2C5373C8A9E1207AAEB60AFC4C96AE1B790899CB5C28055723547594B918A84BA82D885B0B41F4DF755134602750233BCA285295C3022E037CBD03CD2D7B141C06015E8A456C2A373CAC509805472E57A04186BF3A6CF36C7BFE76812B20CBC60F59CB5C0B378E1609F514FE7894852A329DEC82FD763684772B0666055C512A35457287D73B557D446CAF2A3BE425F152A62B91893CEB72FCD52156C22294B6421D53074788A4C0A23AFBD1A8C5375118AE2879912CB5B198EABE46E5502197D23315730458119BBFFA64EACC12C883A545A67288CB87D11004A4EBB1F692A127BDB0CBDD54F2E83951BD77498B90F8EA192805819F8582B91804F7022B5A0B75D88792EB7A94044D4BC0A583B45A1B2E2F6024E99C7959963F1C54478181822058E55637F755978B8F46A789BAA99EB88B70AC2056A5A752AA8753B8511AA77980C4BA1392D1D9BB77216089EC0BFFCA661998801BF499E7A4B82A2035FAF273D2B28322DD71E5B342E50016D29D91B81B405C365A9EE0CA6859230B3188D4BF9BE89EB651D7768D7C4807644929E964B17E98FF5EAA35A9816BD09B7490066720FCE37A99C7F1CA6EC12E3EDE24DE9BE115230F6E56ECE1FA2B8DCD1A8B1B0A8CD37A0F09ED734E53BE83FDCA95E8680EE9816913C5BF47638482303CBF9E3963784E1BAC39086AADF7DE6DBB45E61A8021265A5BA2522A1D8"), + ]; + for (d, z, ek, dk) in cases { + let d: [u8; 32] = unhex(d).try_into().unwrap(); + let z: [u8; 32] = unhex(z).try_into().unwrap(); + let kp = mlkem512_keygen(&d, &z); + assert_eq!(&kp.ek[..], &unhex(ek)[..], "ek 불일치"); + assert_eq!(&kp.dk.expose()[..], &unhex(dk)[..], "dk 불일치"); + } +} + +#[test] +fn kat_mlkem512_encaps() { + // (ek, m, c, k) + let cases: &[(&str, &str, &str, &str)] = &[ + ("86C64C4CC2B50533235A338A567B089E57BB11160254B75B91A45D31A59DDFD133CDE0B5D2776BDEC5CEB392566243CF0796809D75C429AAC4DDF4A9E58CC13999C90CF3C03DA754E6AAB9A9F22625DABE8F5C0030C796A73AB114D905FFDCBC0D135D34F79BCFA2CC550036466415E96257D56A85DA1C294C7938F3C92992B55335F3C1BDEA0350345A87E600903A3F9327858EE0C4CD20C47588B8EC613AC8076FB1483160D356C1442E9400B210B07AF2BAADA476A01D6583C23A882B889CBF055F2A31CAC0574E0423C2D22A178FB233969857742739D01A71B4903655031740065C1370464683B3916BB1F9133264A447E3E92111B99ADCF5234B1403FD8A570EA20DF29C43245A4298B20C83B9B2FAB930DBE818C3940E1A5805770874A140B12DC3BE7DF9B0FE546222645A01B93DABA66D5648C064FA89E32792FAA12C5F76747317A3304164583691AD98A01854422B322488BA6F077825CDB8C0754A3937422B16656C3D80001D760EBDCC8A3E5980F6C353CDBC89BD7C10D4A24B3614AAC13497416977F9BBB73CD742BFC94496549FD4FB4E48AC91B87B432DB64E51377A1B54C5A54662BABCA2FD68B52DE239098C067D544B7DD69FA6AB16A22143965C24AF3B0736718C2F9230E2534DD9BB9E190462577856D19C0C309931993C942ACA682CB030DDF36EE0784333593B501466B9090923190EA707313FB6179F58C42BDA8EB16C141973CE45A577F3A96E72CAABACB7B5D056BE10DA8199B28B86F58C819CAFBF6C7CE2D5B45F4023C6391E1070774F1C033AF830A8D2B1B81A8B07727C117AA3812812BDF768F25CCF06BC1AF5C058221341C4F6B27A6B7BD1177BD55323239A57DD5021B6A92485553A8C30917215BCEEF97968A11A42FC0DA997C0984C7F9C277112DCAC0FF83CCB6517D54C5EB6E2AB9520AA39E260862B0B99D40E701064821576D78AC4908B1E8FE8C4825603F0545F2059674EF6A04CAC900AEBA76BFA8D21D7036F679DCD146D0AE7C1F4C340F41B2985F32344E5BEDFFC6C286AA2EDD252B78C3A5F3576D1252C2A96B91D23453E2C9476747C845554FE4D1DC2D29FB6D8F3897FD8ABE600CF428C76E77F3AD0A034EBD24845", "19C44D35AB9EF31B1360F0BF33CF63D80E405962D698415C5888F0AF385DCFF4", "A87953F9DC2996A8DD40BE55901417A933C3D36EC09ED8A6B81C684947086C23F75172E4380023FE3C120FEEDFA142F6F149619790B64C1D5794E24C83F99ACB8BAB2F8D21FB113189E66E666DB3D154B4D51B74B0180FD14E15039FD7869371DEF178DFA8A222C779A1959C3BD34A27F52ECD8B67E614B6C39FC4106B9835D8035716EC8F20E5B266506121CB1D61519001E4DE0774B521965506327A84AAA6515C64556283B13C342E553AD30DE725DFA33C8259BD09DF059BFEB770CF1DDA98E4CE1CF7DECB19125DECC2321BFCF87142C5CA001C64E69EB3AE2C607306BB0081E34804B70850005A378C4AD5675549414122C501499FD1E72FE3D340E5B9B7E31EED7025989162BCB44B4EB496836979712F3B342E10070F3C80F66BDBB9A0551375D78905395584ED9633396EA71B76354522583DF0D26B41406C80C7C16B170595F6B467159E8E3F8E85B3D0E9B090FF52841092D64A3E970DF47EC8DD4E0574D5D7AD2DC87526F0D932C9748E4763CE8133A3862A1D1DCBE2B7F7F5B7A3800272B9C0685EB01200AD5140211A93EC47EC7FC8F652B7CEAA52F9554B6E9E49675DE47EE2AE7864AA791ECB7B7AD9DDEED8533E2493DAB662EF278340A477CCDEF6FFC1AB492DFD5D70ED17158B3138E2233C37D264D59DDFD665EA049C109CBF4BA826E097B47748953905DF8510960AC2DBD020FDE56EBF2575918706CC98DA87661F2140F6674924DF7EBF887FD6BAD36857D5C6BAF541C92D38A46C8AD84CCB636C75AD7D9BA2A5056D9F1A5BFDBCD0DB89DD9827448AFBAC84642FC21C4352C82FCA17D547C350B2B48ED5A33BA2C11D97B15B9D707DB087875ADB217EBCCCB16E84D88269B6E28F1AF93BF92CE5CC390E0C7278AAB127CBB4A8122FB2087DD6AE73B8EF69A724B55DFAFA29A25EC59B96D768931E6E6F86D4F8C42DD1F2A768C032279BC65B3C224D9680E38B4DBF1B9F5565284FFB52DE69218A32AF7CFDD10BCBAB97AA09C79FABE8E6FBAAA041AFF553C8CD561FD5CE7E619D535CB0F53062546D0A6D0E618011144968A58EA2E25ED7FE4C125EBBEDC6E1D0", "4B7B1514D1BC9808F80E3BEE7B528E13B753C99D153F7EA116A5887063BFCACF"), + ("4F601C25349B73A7CE97F53AC9736FB150C2353922861B5D69293D5A6C8E8CE29EBE5718FDCC32595B83996A43CF54BBC9184D2AA9790D23CB3101207AB190009DCE7F7682EAB1A2CDF779A23A577A6C758776CC595916107728F71A45D4C745A50649D4CB387C406E1CB53C14BCCA5C56101B8C7F63181B8D9C2730B795688A64B24A23ABB93D505493F1796469E89E9D906C90683EF76B2EE8A63E24162EC1532367CA5301857DD20940BC7A733CC8A877708DD6116F1048A0F40952E1C32BC36364EF1898EDB0296019659CC59679C259A07577984BCE54569A4A7BA14ED56B48916DFAD3338601B81209BC028CBBD1CA975158592C6C8EEDB3152E44693A9704A904773AA16A05E4361D2035761322399CCD23F3AC85AC0EFBE7C5CEA480C74045CDD03CCE56856ED5C83E520DA9D5B23B04C270FA1731E631ED23802451C536FAA3DAFC3403087063C95C902C3E5EFC150692AE2A6940AA26C887F657739573DC2248551B110CC4020D121BDA897308819778345F5037877A3246B228339D7B947B7A394D9C0A74B3423543AB43B2B53143CB849837EADB2ACDB65682EA247266CBE45435FA245F05B0A913A9B984F3B575D283EA383EA422CD6FB57937B578BD89335FF39C1BF83EDBC63D8477657D646F33625CF47A3038916984AA543003CAA384279D14595D99CEB7893818C02F3F4C6628C69C4DF490BD48074C3C5445168692EB72A2A0B992108DF67473210C883C079A4674CEF1A0BA9EFA69D2686DC9562EAEAB23B76713C4C531D330042ADC64530717353C8C1268B90D5B6FFD696AD3EA663D3CBF6D08B3D271B753D15AC49371FC891AEDFBB61FF91183D59CF8CCACA1F22D55270817D280EE9808804A0E8999C022F4ABBEA48AF649C87B8717947AB19CC28C253A5F66927998B551BA75C5D56B261D9BBD65261128D840708646EBDA299571A48FA01F8048CC67E61F96C175AAD223C9EB51814407E3D71923A4471691BCE6921FC5DC2672D859E6104A25BBCB03B78F56356CB013458FD7A9B92351113C8A42063FAB504AA33AA38D5144A1300E5198328C729439EA53EC2F5939FDB2877D542D8357E777009516D12ADAAB19B12E957A69AC6B02474E", "F7F7E81932219A3713D1DB2C9A3FF2C663502B63DEC74E774C178233BFDF92F3", "D88071CBA8DC30519C0974486140744D7E5D777400BA4462BF60844073CBEC44BD349FEACADEAEC93DEC77B21727E606D401591E36E069551EDB42A35A28D86B1BAAAD1CD0C4F311194BBCA5103980C9B65F1C798077D4AE3ADF3FFCB301024C4987A5F3825C2D561D0CB00EEF3CB078B7ADE3638BF340A71E6DF6E2DF8900F3B986E1BB0B9F14167A7AECC855F429E90EBB7785660CCA32D60C96CDE69B28B54001D25B22CA4BF1D66DD245A7292F98A4155D3887BD2971B8575403EA4EB4C77D8BAC8DA2FBF5732F1C1FC22C32F3E9701D92595E6BE185008F1881D569B8421A9CC71FAB467AFA9F2F802D014307817F225655588BC45603661182F9CAA1445E69158AF0FEC173F8DADEBE06204994915181417062511A845E683A217BF682017C72C41923BE29CAA2A3048F51199702C012CC0593D8D2891039020DD2BFEEA5C46C8E70C489BC287A134636F9578D503BAEC4F4646377312AF78099F434D502CCFFE61E861C365C38CB6151C059DE22661F03AF2891D69287F04315604CD8F00C46C422F6E3981E141EBCD8D9CDFE9E95CC0EF0C6630A4EBBEEC3BDF26E1F95A38C514526C8D2539D9E0B01273544D8ED645EFF30C15534A6EDAC1BD585EA927F3A3BD4F24E202FC49510945CC98C492687037B202F5A5ABEE1F4631FBCDA98EAD48A8038EEFC624D32D56164C54D44B4EC7878561FC2AA6EE28343074C7630669A7D4D3B099ED3E2F72551F9DF8228659378DD7663FA7BEBA45F2A6DD420C7E30C640CA49A755AA4E11EDD4225781D62FF3A89937D4427C044BAC0DE56509A5B92EC078F66EA6507D80D304E05B5A3424E9DDF2C45C9CBD475602A129C143F9C6EBFB0F446C6BABCADB9F5D7FB2F12CB3549BF2BA12F89209B1D68DE077B6CB274E635725153ADAF24B4DC9F6BB96EB934335564C08D7ED8171DF05F44A5019767C880D71CDAD9EEFD318D009FC185DCA94C213493D8D3A8BC979687728EEDB7E34A1FBF50814AF6836382A78C534871B9784FADB40F86C2FE2486F55D3A90222FFE3C6FC991646F7EFDB8C9C1C330EE07230DBB4A2433358088E4DE1925", "2F361290F7F289EED298858F894380233FEA7BD2D049346B3D975DEDF756A9DE"), + ("889538C5751FE8EA834B31CFAA172720A1C9F1B379DD4A70E5A4AA51AA6FD75B955C30077CE39993B214255A0AA9D0319EB0A9544B549BBB079BD06D6113CEED441B937B2CCC5702D1F354C7170B3ED62C635548A5DC14F7E026F0EAB84C5913AAF8A639CCA3891B8D36C97717A4A0CCC31223E80ABF20C3E48C6F8F7A09BEF5205829A86A241DCE8308BB53781ED96D61F6B0FEF5CABA932421153F755C0E868B9DC72C9648B95D631C5DC7C304C493135BA1A9CF855AAD14B4CE7992D0E58A253A452BB12A790C739D7324FB0641CE2947C69C803BEAA15B3A40CBCC61E8D26F7E5512DDA8BFDE8768F12B68E567547C09B2191AA2AC191C81D91EF2C772A838406F0494EFBCA0ACC941DA7A2E332501F49203544727C2C664364693DC33A097830F72E62BE82219AD120CA469784A64414A1604F112946B48C5B8B622FE777487E1ADB08A8D219C74622980EE88132BE4487912CB5394B0F2B9B085B138F4A575DC314ACB33C5DDB81CCDEC1FDE1A2350F5014D96AA4FF96335309688E13DA1EB90258A049BF19AD35184594AAB88813DCC517FE7394236104728FB00C54AAA950AD061D242DCA00E39108AEA90A3653265D25389D1861AC735B53BC23173E3BDF7007300C87FDD3686265940AAE061A80236275451D1C7666DABCEE95059B61C320430CB33AC90498C8160188E2BAC3F46C57A420702A2868E7E4947553350CB6711D1BC4B8D55C4719113141C749C7BC2EFD5B665C200DB65924E62625F0C07F127C646AB6551069D95AA94C3071612B3917A79907CE0C30BE7295861AD45BC0F62C34128E6538A6302B1F774FFA80D418A376B63AD36175283D7A3B3049490C605D413CF15C4077EAAAA7E067AD68A704DF4B15EE36F61F88B52E99B04EA0434782DB652C991764CB65107E2F2804B18AE6D81524569B1A01AB44C63219496BE463C308AB66B240253F7230536A84A21EC66C9148D2F22500938010B1614F3804449E18BA32B35E893AA7DD6684C15462C927CE30161ABA86FBF47519022505B5A3B33F449CBD88FE280A863E04314060585BB4B4CE1590DBBCD9AA9B01FA545E7BC294B3C9D055672AB024FE63E5398FB32888A07B6173403A375DD58", "B6BFAC644646F211A2E067003E596540E8A5389BCAA96D110FC45BEC9EC16465", "CC8D1D67DE7966C3E2A4164437C4050B82F3B8F73DF2A67233712FBFFB3EE27166958E98F09D6905506C167E7F41BF4BB201136079DAB8F60A06A49373935E5D699C47B38290C21772E689D668BBF5126BEBB7B7F6F280B092B3A48C44F8910117FF95151FE2A8E030EF2043F37B63DAC92BC7E3EEEAC7EABB60547F7A140973D8167BEC991B30F8E1F6ABEA34931CC078D1CADB0D8928633C7D370B6B61FCF85B6668518CEDB38BF6E4BE28E6329A9572A0799BCDCDA11F9BFB7F5B6E1F24471AA71D0A6E0E020D2752ABCBDF9D4272651553BEC662D80FF3F5BB71C377C03C4887F29A06A944B7D2B0E45E93E192656C08D8D4880CADD34C2339F0A19EE2B5FC05DC08582B8D67A5A461F01A5071DAE764F0589B85280F3FFF2962DDB8A0E7C9CA16CCC96A46382512E8B44C56FBC1DE2847AD40F4420996DE208BAD322C4738E20679FF19D4923F8170A6BDE9510E358ABE7A6342FAF3D1BC61086313FDA4DDA2BEC416A227B84586171359B8E24180055C6D9FBA72918EAE143BECF9289081E524DAF70199DD36B5C47FEB29EC32198ADE2DCB1BB8F8F246BD915BA8F532719B82958BC20C1AFA9A84B3B2933964B0E52254A6B7D539499E7DF69477869B0EC779654B60F751013297FC96F852F4FF605EA0C7BE5405FB99F8A53D3BACBAD24390F241A6FE19896D057FD3E1202064C3C039326424D092FE729E0557783E08F3F73E165D4B58D3C7CD778716427E31DBF8FC5F5B1477F88E5F27FFAE9774CADA3EFC56892C92839CEC1C88FFCE80BBF28711BF952E9E6344E6FFCC57E6CB2A4FF0C19EA7B414DDFC37ED915AEC580719EC43DC9DB50703CC87BA1CA8B9E87F18314BBED9A4E665BB366A5590951026C47ECB777C9E5C6C7532847E0253A63EB081382242E0D364C70C45DDDC6C758A35059498902A70592C68BCDA19BE1176F9A0F32804FDBC503653B1C3A354D2A4CAE1A2CC9DA2F9B667A3C148F8FC71A48F56F5A83ECC9A760E7896A5CA3A3F3F5EDE2C8013FCE2A2DD09690BF436688AAF3D3226B20D8CF17544E43DD1134E105B7EFC6C997DB254AA29D60864C0C3", "ECB3CE59B3DCA488E67E76350F45C88ACEC0D2A46FEFC5546BF181679BA5AFC9"), + ("402874933B4B8F9509785BC8B2C88B76A705E1A686CA2B1DB89A8CB006C92CD108BA6B6F36C2B4DA4381B26A2B578B5B12844EDA93A60CD01C39FC8CF78A563FDB57DF9453731716A5B9442274B9DC662860A644AAB6A3F2E095B8EB0A4F41957E9138033C16FADA0CFE336A7B43178C48AED4A069B038978FACCC57618BDBFA2342B44EE80372BF629205EABAC9AA261ED318B04A3A350B8FEF208F118BAD2C584846B57FCE5B239BC33692BBBD39774E1C0B7A7356BA9D53C3912B23F4629A32588C7D385960992715A67322D8091F75B6FF244AD528A3501ABD6A5C1D9510B3A7696295EAC1F2C403D7B135C16B76C1E5C682C496610B5D900B14E40485EF2BB9488A1FB1D5392E62A3C878C5CEF439CFCA433C05CB39A00283E651B0964A4580BE3FB2BC95674F2F218EDEABCA6DFC074F492111538B99665792C125A0C8073F3C13F9A5BE5EDCAC726A4DF36C8A506008E9BBCBF8D83404E2CB0EF56C00B90814546EC75444CA764F5B83AA522CBE2C3CC255EAB5FDFC681840379DB96E431848B1658FA0A2631696A9A5115BE0488DB3B6670B13CB8FBC36C4FB9CF737BC911B2C05591D6EF2B2F6A109F11129142C15BFE1045D005E551C5AEBECB05D646E6CECB602E833D64116F0D68EA9AB02A112B411D2089F93A490CC6E04684693E1CA18E84DB57024F55594CE99923D714144C8A7B4401920E5159929395E1573F4815F581429F9861F0A04A576861B20F089FE92843B47B3B02B69FAECA98E866D16638BD0A006EB642564F1C314EBC488A53DAD44A41A97A6187A829A560BAF1AC77E582D8C40AFBE511FCF068B84167E49D03F84ACAB4E05AF737598FFF3132A4A8FAC55C954D64BAB315A73CB44D9A534BC7BA78737691390BD04E300C43156240B7995F54C02CC1025608B6EE1541A9234E4B9AE368A6522E2C3D1862ECA10165A79839EC35B8E67563716CAC90B3EE2D316D22795678A151928212BF6A59D593B1E6615951B9AA48026D8B73069135642F596FF8A30E4D4813386A268063FECE408B501358CF9C11AA2A3E5D89F929643493C2513F1243C6C7C7746BCC7078D04789B0CE423B431A5916C6A4F78891AEDCD19BD0DC9806C5D8C9557DB", "91386C8B1E53CCF9F2E721CA6638C53810E36593391EDE7EC1C4C51337E81135", "1799AB6D0014EC540F42CE0E2A924D4FE7327458497FC05E555891707AFB020A41592FC104AF7675346986AF2A06DDDB39A633F2E0D57E17B657DE463A02DE5BD6FB65B2420EAFE9F56EEC9AB9625EE800E8C79CC0BB81829633C7C2779616D96C33CEEF3FE21680B5F1E801BD1C304835C1FB4D127CD8A0D2361FCE6DE73D4DEA7B6A6C15B4FA8C1D982F6AFB4EFD2D4AEC3408B6BB0FEA6263E0BECD2BD078C28249615938366316387C1E49D4BEDB8A8B781621BF30BFC185104657B04C770568BE663EBED75CB2E8E59C8A6AC4A816A059F39B6C3FA0923D1A99339A9D23674CEBC12EC65EC0B1693C250B95DC688912D4DCC3EEAB1E85191CD1085BE71860126E32389F162AEC5CA5DB0625F09FA359BD16009746212E2FC6B74A9E1812D03B8A669EB19E2D22AA81551EA87D1542B899DFACC3B4B73FEBB120682DAB1DDBC2B6C278B803D923676B5F553708CC27442A9C61E9A043A10AF056FA5864A44383ABFA7C6B1DFEA1AE16065CDCD163B4BF2CDA2D75E02768923007FBA8B4EB1CBB58100DC6132144A0FE21C1EE1A0D4D7FE26518A7DECB20B827ABC44783C6AEE158CCF20DE0C18A32AD79628E6A52033030B13CBC018BFF943465D29E07D237D5394D09A3A748B944E0F625DCD828926473D21C9DA97995699EBA32387343B41E2A20E4E98E37982C97D8664EB134699CBF2441BEE6DA19AEE9F7417D9A2602C198AF7996253203194E67E978BA508D5220B7B465BC1AFC1DCC5AAEDE0AA1F85F41E68ACB57FF5A384B906A496D562184A185E65640CD10357882E11D78F27AA64CD6F83113BE197C4DE04292257208CAD7D7A5C4AC0FE1B240A432E8338E9AF22E3C8CA3CFDD557A95616382F77498037FB40FD51268BED8560E21DD00980914FE432D6C5F383FB2E5C3F4CEA0AE5F34167C52A98A033B6A76988DC97688CE9F4057EDD5A761866F33B8BDD4D1CA327B7B9E9BD880434334153AF74483B92042E46900963E42480E714F87D951EB5334C890DF3515880F51A55C59CC456DAF38FCDC9AAB37660082B899F88061343E0C7850746F6E000D6F8AD5D675FF59", "224716A4671C4E00188A2E11B40BC337C68B54FAF32DCD3312C606E8D62BD36C"), + ("B24B8656777875B5337608AACBFA9C7AF83B01CA84640C2E0BF0C91B8B1E8B79321C1B3F89116CC9E4AA9C72B9188A30165C5B90C12B55E9B4DB8BA02837AFC2A5893B574436F879A842ABC0D5CDB5645130696ED4A332DA780FEAE3831BD88110D3202384344C857437E74733E00ED061958630A837758CAE088215F357D7FB5CA7A71EB4E84A306A56FF66091213AC399098B386CD7D229E0DC6C2FBA23F6C4737495B70A1D2452EA6B249F31DAEF3CF8BD98B4C8242D0D03E0B0828ED383F71163F84387583527EC164B8F4000A44787D10F0A3145A478ADB598550094B3C172C3276D2BC0F9501BE83809A2181B37B612560638C81A458B73A67189C2E4A93CBC4DA3EF2DB44B9522B0151A11B0A33DEFBA65E66B5F7225938A74E88910A46810121955AE1C02E66C347221438C79075D90896537A0E0EC741DA4A40622C8AD30A0999354D965B301D1AB74950C6A610CDC6A7C19F29761F8427DB113C6858C4FFCC11CE993736209E82DC9845E1701B460208343FADF25F6D3AA4D0FB5A73B38B01CC2C1339567EB611568815700AAD1E5134DA92201F4B79E079570EC2C73A2C39B34B3C254B216C220DE9F9635566943044978BF67DFA44B19FA642E0A91A7A31ACA729799594B72FE1874B9434904A8342883707E9A8521736AAFB563BF27CE2335E688CCE8B0226722AB8327C06806515E5D7A67F98158C6B1FDAC051D5E0C8D3F09F8EA22667D21ACD733313E73CDCBA6AF5D53D1F49CB4A100C2AC64BED907A99ACBED9979AD431AFE7C6C48F597C7150A51572C0297019A12755F64A50CB04216989B05496CFE9CA8DEFF131AD64713EEC4C9C78A95CA6A0C44AA0D6183D681A663E519B4589942DEA3491F80304523E0B074B19325D9DC0C82AC83719DB300E787437524DED9429424749ADD037D08039B9B33969C89672737F6E804C27C064311B0BD7BA04A95BAAC9545F29847C5390B2D6467B67756DC4B47964324A2046833C99829A8B9F550C017B2BB44426267E49BFDB5ACCED7CA7BD8198A232184FAA8FDE193A129B1546907918D11E8EF0B69ED0A48FC1C1D79258D6C03F833AD3D3F2D69A9CCA751A7276AA55EEA973F18DDD1924B0CA73F8C439", "F6B342A07A2B183D2304E7E1FA5EF054A7B4CBC377B72913EE1E085B92E4453E", "3D01A48287761DF9E36B9FCA1B0D9FF3019210451C4217045202F6FDDBBE67BF32F76F6805EDD9C98CC046BB6BEC3DA56F27C9D0C70FD92F2631ABCF57D24D73B40900B525CF2FC60099D09579668F07DDECCC9CBC109B78AA7FAF4B1448A3CF3578D77AC63EEAF0B1DAEF8D878AA1E569FDDDC12088C663A848C5DB24074F83A434DAD5B2EDF6D90BDD0EB64E6FC8C8D44DA972C475C068C5990D9A3BEF2D8489E4E70FBF397F1EEEB9615D2BA7E3FC4250F61AA7012A6E323AD2267600A4BE1285AEA9EBA1A07711FC1A896430B629B9E01F23658B4DEABC97F47CAC15208863366534EE0038BB5C1120AB277CF37A1B6A8B372942AAD1C5E5C6810B80A7BB9BB18827B2DAE757AA7A589988B4D1BE001114CE4E2BD16371F0942329D06E8D69B2CBA3D7E83077EB531647E9E9057486DED2F743990511CE3787E75E74C3D205E2449974E30895C32497F0F9E2BB967C9903E96EEB30EFA211F1F1B38B09691AED0300245108B87D6D67F0CBE00ECE2AB1F033B4889F6B8BECB452138221C226793D91A11030C4B26077FF61C4722C52E1C74D19CC4641FE8692D28D073D4237E4FBF73EB21BB5D372D6447DF35A63EF6C5591D25DC0A0444AF0F1CFFD4D6AE90C68CE83D9BE30E6DD0CEBAD247390BBBD533460DE309388A8237ED4DE2044E816AED8BD056B1067029ACA87C80FE7A67B507F2528471060504F0C50359516C17502CCCA43F67A9A3EA08123A3E4474406BF91FD285777970F9B0EF2FF30C397ABB5EF9FE334327AB22BC340931B74CE8FBB54B420A6F259B46C0C5DCD7D936A4F030F94EC5FFA96C586549A0FCA6D6FBAE0FCBCDC200A231092B037D7EF7AC2CD5820E6D8D2B4FD26E72CB3BE29B5A56F8059FF16D41283069CACF58DDC7CFCB80C05C556F913D95B0D48268923524B7F22A3D664CCBE8C615F23E311BAC018BC678E00BFA4C43C6370A5003EDB687BA688661C1540BE59D123D87952FC01ADDDD0F1C5B9E930DBDF9402E306A1C59F386614A798AB6F57F3511484338389E7CBA85843FCDD363C000DCF3F787B019377472374872F8A1415E1540E14C776", "32BC11E65F42683B177FCFC62DB781C41E44C5B852069A27B5BDDF7B093AFEA1"), + ]; + for (ek, m, c, k) in cases { + let ek: [u8; 800] = unhex(ek).try_into().unwrap(); + let m: [u8; 32] = unhex(m).try_into().unwrap(); + let (ct, ss) = mlkem512_encaps(&ek, &m).unwrap(); + assert_eq!(&ct[..], &unhex(c)[..], "암호문 c 불일치"); + assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); + } +} + +#[test] +fn kat_mlkem512_decaps() { + // (dk, c, k) — 정상 복호화 및 변조 암호문(암묵적 거부) 혼합 + let cases: &[(&str, &str, &str)] = &[ + // valid decapsulation + ("BE26809AA451F5925CE5B1311789AFE966C34EA9B677EC50624109CE3A85FE560478E09B99D16C57D612CD8BAF90486B48170ADDB370A0C613CBE3BC04785F8B2B688215BFEEC192EB39CD0239751484161685A8197987BD600D72A0BE008C4BA9C56B9C495952435F29716C3C69660BDBBB4F66652A3A7EA08C6B8175C0D7C563C4060EADB1987322A2467C7E672BB54B2187E37952B6BC6C001B5DAA969E86F2120284213156A80F0BA258545549778D6D65AFBAA9CB073549AFF577CAB079C625236AE5C003158D0ACA16A91A9F45C32FDC182B800798DC2540A7011294D81EA3644467238C78C84D892A61133016E4768042A14A2C13598B51600B219C0C1B4E88C979DDBCBA52B836FA4095C2E47943A27010A58BB4065929EA6E33648CF9D2A121871838B39FFB3291E5781A06B1266542CE2A85A8BEBA7287703085C4B808E7C15448272FB37E9A04297CCC26EAC658CC30A4FD4C72F00070F04AC4BA30486D8396C149A4053C39A2C00A33CBC41FB563E7B38AE1913032311617D45BC83C59DC6845CE991D65BC442A39AF5C7A5638EA9BA93678E28A73EC24C032B4796F44385E2492E02734A5EA3972B5CA726A431C3C41ABFC1B637311F781629E69C0291A5AFF8AA8258129186CAF17AA4BBC927F76045076853437B95B0FD531C488C336D43504F95C82B970DD19202973C8CB12C468A9A17D644EAAD75ECEE7440B9901DE7998BDDC737014C1461137EFA28D32DCCF4AFC9B5089B6067A3D727510085950446AAA2A272CBC06B3D0E236DB180377810C435AAC5A60553BD0AE140969D8D2CDEB78B3D097540C6A9EE2613296A5AA4D6BCAFAFC4580896529E15308BC05BDD36A5DD9B6EF989080C2431ADCB35D8357097CAC6EB7BDAD905FD668A3ECF690DA814456933E6089641F252DAA90799E39AB6F037F79C5B13750005E202B98BB50C4D94B512533F4C2A67B3C31B2E03E691AA41867B8D0C93A73A27CA49A2DDCDC0188FC1965B579E516AAB43527D1D69DC723BCA288C5FB73C99A399A7D4107C0E76E00C7C9DC52095BB925E24A2575420BB3F4CDEB798CF377469405C22FB76F6C154B35E49DA6119D72DC139BB52A057B6E4F4A177FE88568B06D94C0A59562BC2BEA86EA7CB1F078223E7743ED8B6DCFA2CBE64699C773A5ED0C0935917B9E3015DB102C8B98A05559211B563EA1B0B7012C6332262FFFC09E779A1068380E9FE1B0E0948D00B8883F96A3D555A52D18687491916065A9E56A075464B625507560829CB94A2FC57C7CBC96864C0201F1D34BB21B0133A50C213077C7DC4CA0F871D1FB4AA8C99D7CD245607954A3E45DFBB24C320B97DD1231CA5A44EC041BE9D15504A72DB425CADAF57AD0E72CCBAB789FACAB053377F56B891FF7932F3A2A0B618544CBCB99158246F183E313443CC35BCEC71E9D30954C404D84540BE4152B3794A582953207813685A05F52624BD7926CD3812D53CAAAB2C392FFB26BB3A06F7A1AC1D0F145C9434B86695B58A673B4955ADA329D5B572152727CA3963F54898D39F38DF578965436025BD8C1C6342629BBA98B300FBE8AAC379BCE1212A247BB452701A82810B825066BE058CB78DB9E83A3ACC6637955411A7FA3C2AB7692A279A210CB9040CA1B36B65041B1A8AF874701DC3036035A96D22B38F4CBEDF72777C61C3248328B51A191690A9508B6A1185F0B890A91F0CF43E4127C51509E8B5AE6588863F72595A7524708A2A6A385A385BF750BBCD3B7326BA99F37F6402E1978AE963AE207CF1FAC8028024AF63CB6CA32298626CC8D935B72D8556024824260BA2E427C3930002023B7EFD41693E44D4482AF9A4463023869DE85820C848520D2B465905433848A3589ABFD4C39DB311B93B34086D12C4C8243518A75C14A69DA9235A6FA305258BF7BB5ADA8A428F6063FA1D437C4B36394727FC06957B8D6BB21F409969290E1346698A25E61937AE1E23BDFE09A53B437F172A57BB61FCC569746054601415E6E315648E03DA71239BF869E1F669B8838A486FB2E4F489DC8E2747DF4849B83B165E95D23E4CCCCB5C414934549B9046F359E06B6ABFEFA0E4745A346451EF8791D5582AB2DB22EBDD1126568930264C19F1BA747296B40E284A45459398587975C88071CBBF6C9275DC8C81C661A020443C39208359AF0A721A303479A6DA0357402827F09653C7A2CCDDD78688324A1678F678D49AE2658E0DDD0DA7FD25FF8375BD9708415D6BD80AA8C23425D1B45CFECFDADFEFBA627B7F88C03E56DF0227510BC47AE6EBA8B92C1A8", "FCDD461B68BB4A153F9203BD9822D4C4F6305ADA8786C564DA4F31518F8D4AB51D9485673921777D439482DA99D70482D0C3EAC998541CEC67759A19865D3EB42F3B7BA83180F6E9968EA84747A8B0E7C5CC3159A7BE5D471BAC5313C055BBB731CC73565E90FC5A64DA299C38C632C054A1E3EC1F13D0D332F8437F273F149CA419A922BCCB0AAF454162282A0080C6C31530C9A620A682069309B039264FB7A82057047ADEFE3C598C8FD17BFBB48CBDF7DF4326C69519615A6D81A797107532FE503ED0F8C7FEAE197E329C05D4041DCB6B3DA4428112FF2702994682BFF8FB7E6268162D3C85BB40574D8FC256335DA513E5C1BC06271DD2219674864BB1F51BD2CEED41ED425BF27A27B9CE312EA9AC0753D102D15DE9B3A668C2E154817A8AE7B6F2F5D0A38A0E6A46E06E1309CC9FF1879F6A5C8D7F3D56EBCA1BCCEF42ED57EDD349F321C07DA2F5D9242E6F31B9A0EA25BB904E19E7B771068EFAE868CCC4EBD72CBA16865698AA64DCC33D7BFAC4D8C472D31A7AA65733B54CB41E79CF16BBFC930EDD78CA21817DD5A61066CFF379363B5E43DE320066736879D5C4F16F4431D71AD9EF7E18D3166AF1D0A2C7126BBB542F182F20FD1A4AC88A218A46A849797A6C459029AF2E61C9707C7F142F333D3ED061872449D00F9744693BE1118F0597A61F1A808976504A3FE1F9A4582547AD780EA05A318FDDEC654F0826D1344BD986238A214840257B6E857C9A032579179FC374174E640AB5540865DE606948FB3A02EA70742BA7A08617B6FB01239F4D5138BB7A28836368E1E6CA63BFD9C3924261289BF6C1F9C3C040595CD52E4643F6C2726F3102E308494F4B33E312D59345279333A2AF49C157B4FE55D42083CBF436C66E0031609ED4BDED3DD7A54C98938BFB41A4F4DAF3A96B20E29F54BB7CC6742AB563467DA923D682E91ABA279A616E52DAAC56D07919697A5CB173FA24F09BC778AB1103F6F03719D088C549B18ED72ADF6738BC82BF4370AE9A7BBBCFB1C78717E11354DCF42654675AA0A45108F5B67D2CC77DC200A0F97BAF667A242627298E57BE6C761EC4", "9B0E2A90368B07C5A94CD4E880EEE7814E1703D09369DCB9D3C2AA079FD9F75A"), + // valid decapsulation + ("7950925EF032BF6CC75C330365606AB5C4955E124636FA896833932D16444A2C393975592028A3849694C2D59A6AB5B6167A983762B94717345BE71DBB07409C12609C99BE4D863BA5685A326B8D14F6B68DFBB994633669933FB97354C2E1C416F1A68295838882B4D6903FFFF9AFFEE3CBACF4A3C0D3BB15246631EC851E750FBC784ACA065610970996CB058FD0A97D9440A61C5370F54EF90777E93B73C3325730B5A86543B5994B304BC000CE08BCE1FB2F84BA1A868090D43224B82368888A3C22502E31D664044144782B3F82F927B9C8C59326288124C4EE0482FD011A098B0464358C694A28064178FF622A7152024BFC9D4B308A0F92347C6BB8A8671E723BB32F22B2585341BEE8286DC50AA8AB705A85B3798029AE168AAB23790A9C64B2693F4A001777F94EB7E4096866422F4903B474256DC244A6EB80C1C736C2206C0EB121A5742C2247212AC95F8D7B82F375300CF0015746902A3C5E5DC88C8C57BBBD372148328D59460852B72A896411F6290554F3594893CE2E37066000963D394C5AC85F7E261FBFE32F85236D338A82B5BA3A48A87B7DAB24FC164AD33A05ED1B6BE59A17884B79AD8C5F60767F11341F6C40AFBB92A05F39537C4886FAC92186E407AB63246E928244FC4DBEE1AA8E745DE195B53CA694B675CB75378D63956C26CB5AF4CA24613478E4B844D0925C3EF1C8DA98B265C31794D264A4B87E524C5F18E2C2C6F9B0DDC5957ECB9AD4F9AD961692EB044B665480DC6604E3B96128544F92108DB1CBCAE03AC2DC8A46CA16BBA3784563CB69C34B053EBC1FC2A590A976AB4E20939D604428044FC70552411C0AD5220B46AA09FAD75E16173976460A99C632FBD01633A7963D0C22AC91985BC20F16A36A2FE5A1CB6886AD736833F20941BA426CE172A2429E4B39A960C138D9E18254E7835ED91AFB7BCBD613C509073A1699026A381F585B1A6B457F13D9BFC9F4433B069878BA561145737F0A2E3D0B20ACC39EAA5597065202C316BF445697F3E9B71A2AAD49617161C39571920438B88C7B5A9B3B85C2ABA799F8CC5D99D6A91CF7506E15BAF9F042F3089E7CE381133992D984856F5968F0870FDAB7481C217A0EE50FDC0A694AB194D8223CB7541451CB666BC08B410AC9ADDAA778CB068B10056F28773E4231F0406D488B3E3B398AAF6128E3C781D271A9B0E68F64A5C5C276532E3C7F5DF20694429E69047D533BA2DAF135AECBBD5CF647266584BBDA9B2B17B732B4A0A6F732A753626442005D91B1485C6EC6E7ADFDC54A852942CB29355BD665432946B9683C85F63193A61ABE6B03C70385E8516AED4ACE9C4A7B6BC188A2C1824964131CD322620BC6FB393465453BEC3284A8E25431C626DAF3BBF6B085600772F4786833AB562B6337AA016B50304599E589AFA490B219CEC74657067A1AB5E5AD7AC2774155BBF7BA87FED18366F7769A7C160D86617F0A4D5DCA445A3C9D329B755914B8E9B3BEE22AA817014DD9473768F8133C837895076528486CC6A4A9ED5679614502781993AB477E50C613373218F3C488DCA6CCAA162C2D965C95851BF8EBC864B67450DA62F67901EA2A4114117E96C91F39122D39BC03D8B03CBC12B1ECCB07F53473F6D714A998388388B628B659C156658AC2A2F7156A60ECB44D930A5985AD58255E72666B9301879DA0047808A31DA2C14178C15464967EB2542629A772BA4F324BB6D605790D8AA6E86802337A34097BA778D409988408FBC977921A06E3425930461EF0CA3C0B23495DBC530E692EE100305F099F23E69B5ACA990721BA642C744BEA1759A5484FC5985ED506A3036C88E7A68D4A50C4EBAFD81C3D798473581694FE6A8B985C0F617011AFA4B1430BB984C3A96B800EB55BCAE91C0BB111317BC9CC92A4C78463B5C27267FC15408671158C47346A99818BF70ECF6912C15AA883795ACDE69FC772B879E12FE7A91872A5BB86767DAD1C84A476207A19B47236C2DDDACA75536E05A96C109B9F21C6B9E43170045CA22387452B4B21D5515624753698112BBFE563E7A8059601C86E3030B36B8DA1BCB1B4155C83FB9485D70E181B946F3921F8F7CB4649742BEA4D2954599C3A498430899590C83883A2AA8075DAA81A11A1A5E8B37903D61A288B8D84328724068538CFD7199A9B8C3008474F01297BD3B7F6A3CFE284A0A618AB4A8B790CCB690A3D93A6FF6D066B48CB9A9AF45F8D5F165C72CA6DE8C202A19729F46FF824B6400B689B27F86964629F3D9DA45D0026DA4F0C41878EEA5BEB8A36113341DC823C", "088EEC18E0639F82AF4A8C4E577E90C31716693C6AF31BE04A69B2751023DEA988991D919FDA0BC84E746ACA5CBF3D4A335C8718F1698F89E379F38F4AB02199F761CE4E1A80BA2B25289578F642614A70077735A3FCCF5948FFCFE2B8AECA166141432653D8E2E6E5EAE0BF455B48E5184285BE0685FE446F9ABE7FD0B721E0A3D34A30E984CC51FEBD52AF0CC58F9D8E65C23CAF01A0F90C0F36EC782E7F7143E387F97ED2332BA10F4AF29CAD22633883BBBAD58267B39C1A6F780DD6C55C6DC3627BFB4B81DA52A00DFE1E995081F5FA46C1CAEAA204B12B127E2B2B9D714A97EA2C941DC483DE3A8E6CC02435D22976FF9313A30C7B0A04433CE6E5BBBC9DF83208F2BEFA36DE3812BF8B3BFD9DAB4CA0EDF1D570E37E166B031A68F9135A01025B21D4833F57EC1906DA73BAE21FC2DCD18AE8DEC0E1E40CE66F0AA39014AF53C93509EE4A6ECAFECFFE668A8C8679F1D57E0865F12106718716E56F16069792F2D0419A70A3B0B7A2025E211CF4B7C5228685887802D3FD65E46B28B3350D318486D3881C6C84A1E0ED487C3D77A56F34C0A005454C5DAE77716C744DE1CC8EE96ED963EEAAC584B5B92D77D1F2DA7655AED5A1CDBA3E45FF9521A89F2AC05716CD5B11D91F7E2E1BF3587647566ADB251097C398A1E47ADFBE0207D5A44C6E35E4838D12424D97872476ACCF545B4E7B28046F5EC6C8DAA03140107AEFE9BFBBC1DF101A343E9D00B6ECC46A1DA91D1242DABB7070F06C5F72574FF2EF171EE9849A5174534CDF13F85E2D12ECE4BB358F9F5E5EFAD8EEA86CB01082922A4092878AB7B7E4C72E78081896E2634C64126512152CAA0D4B448A1BA357712DFAE926B6CE15DD9349F66DEA45DF545D4532072BD198CFD2F4533F2C85DF017B1133B5D41D012194CF5EA5302F6FE10D638F462FA4AA5429BA453AA3F5CC4BEF22B5CE7C005BA1DA45877F64EEF3D68AE271DB2865F2A0BB560AAF8127895144B30F4A3338029B589CF23F9D652E6E6F2DEB8858396C35AAD1FEDFFE843174E0AA86B1E4E98ACCC50D1C1B64A7B48E93C5A7A08B2B6D9E700F1C1B9479D7", "C238B69181CFB9E4C08D2B886B716301B4B216D5213BDDBE158C778D7C90FBE1"), + // valid decapsulation + ("7D3C9A9E5A8C69843DA2C4803F42CC058B1823797BBFB695CB90547A932833E3B5AC09935E117CA1C012F8A573EEDCB2666274B25425C1826ECBB83C0415BA3E8AABAFB18DD6DA0AF107604847547D404A8463C743241AE6D03B95E5490F140BE0F8095E74825EDBB6BF040F5D61372F416D2E565D116766F0156EFFA2C59EAAAFD4F695F7E0888749108D967755E84B6B7379D90788094B6053F21BC3536F2903C816D21756877673F529BCA61BE121194A06BF53825982C76D10BB5A772543E44A005606B631835499CA06C1E6C4A967CA2D008EDD5A86A37335DE0C303A19947D7320CFB4ACD0CB369A80B6FF66788F97B293E7004C17B9DFE55A9248381B898DE2FBA92AF7ABE9B749B5868852A564E24A54C822CD740AC7DB9C46C204C1AD435FC1B89C378845913B7A49F912BDB60F7A57CA261C25A188981007745FB7C71443850961C729DCC6885C989DD50B226B15275635991CC58FC88179966AE9C314AFD6413BC1579CF2CCE0216F636636734C2A87996171E7B35F50C00E19AB18A11BA5E492DC85AB82805408B551531544241CBE43A42102764BDEC83E88D210FB2B00ADDA8848C520A347903C6304F76AB2CB311AC650BA16A28DD605C8A92413A3A43B81819242B74089F5B4F14BA49E2642C8FA13BC06A0D9323E49B05472CA0BD3CA20CA548E2C8457A9445B77E318852B9B32EC6C956A242E07B58A183EEA68978389412E3B7D7EC22788051E34DB1ABC1B3E2427872626CAF556B05BCB634160830B70A3CF84CA4F8757668020B9A55DB92221C7B02CBB96703BD12EA9755254EB80C7A6C45B8B8CDECB4730F86ADEE24A965BAF6076535FAC13346407FCB1C199F2C8D3010EF2CC1E7F15ADEB622F37804CFA6612C2EACDEFEB284C588F5E2B5B07A158BDC6B45DA90D825B20F0DB1551245FDA7C7B73C7C6DEF65A89828EC8701CA36CC10B4279EA9BCA19CC700FA92E83D09BA7E92253C9CC07BB3989EB6DFA668279B71D40F61D13417523638F98D7700F448E14DC1163A479953B5724340A446A6A2CD48CE96B048CD01255B850333172FE860938598170DB73BDFCB1D211209CF13D866CBD971818AB1110888B25A74B27FF37774951996DA25F7170AFC98843D7F48BDD1041B9D2A70D0B8989E03D8FF3B931F8BE07E118D546445857C28378BC52985E66DB50A7B632909338579279D8862047EA3A1061357E9534F5E04BBB001A87E3BE084890C5CA171406C68C9058DE0415CB823ED2536B0BFC466A7455FC227CE8B2AC758A00E16380BC82737F3B196817204EEA531D8245AE51445754897DF78E9EEB73753BBAA658087977627F659332F5AA8F838A0E7960CAA1BE8B1BC2E661347EF5AA9B945CC3B49AD6352FACB486EC108027DC712289667E98B17666C1E3129C5E5C14C2B1221577C784223393EBAFAD1AC5E1281B6805606A76CA719B3CD9FB0B53856CCFA8A484B4313138564A5A835DCB5043F729B59606D71756E6790315E07E8C6A72816C8CCF2285C1DBB61D6A68EDD065DEBCA9CE5C54860711B386BC62E63C0AAACDD36BAAD690C6D4B7696681464C27984C902BA2CC03A07B7657A12AAEA670E4839F4544B2008481D32043FA5B7A9C4C9912E3CBF82715260A3E39D9AFE3F06972D44707A4B65E61587D3C650BD04F09BB9B0BD6745CAC69A5053734477A857B469910A11AFACDA38B2B77489BEB0B3573C163B1A199DBE85742468C1C5C29FA272D3FFC181802B76DD994C228255CB1512CCA4B26A60B1F40B5B2E3898EB4927A039A9BE7C1CD91900612C00028A55585835A5A846A162B3909501B07C76D48407A922CDB4A483957594F949B59A3CAD7E40AAA80BB9C88AABD15BF600B59B653B067B363AF65858F822BBA02411CD10FADEC5515900465D3B8EA1035AE4BA20E396BD0980C1D37C8BBF79EBB5741E3391DA4FBB55F6BB63A7C1EAE327685D953456A39B3E416557C41BA8237F8C7624402C992C37C86C5B73EB7B4CF34750A5A705A2B1EC137428938B8BFDC0FCDA27E7FE460EC316C05FA15BC19144A088ACE681DF636829F204016491978B4A315682763B32A88D08AC31C4F6EA1C2A2C93B3AE5894B4AA6F0752D70B59412533690C64FF97CB62F0B84B328C9AD287C64B6437EA12FE7D24E2E068C5C9B189F972B25F2BFF1B165BCB569768A1A1128D348947665F386F1BA674D44CD2BE8B54C233C4FE6EEC1160B9FBD4603702809F490273505FE2B746D702B189120806B9376DF9B7146A700B6F9CECCEFDA52BB64A8C34F1D139B21864C68B0C3F2D8", "E5416C65B1F75EDAF39CC240B4FF6239889F62784EA2A45D13BFB0A3221E58069028CE710050495FC50C402990A19F54845509610DD8FDAB7028D9C980206A7487865A6EDEC55CABEADEC8F1BCF75E9DF67F7DA0F62787475E95E719A562BB0A54631D333E5CCDDF10CBCB2F431719855EF68C149713276DA41CCABE67A8A57765E02B98D7117494D0F13AD04C161D4ACA485310070D34A280AF99DD687DC0DF116EEC29C03F3A5E0CDEB9F37F583BF818A68E410684229B03E6B70A21EC297460A411250D44CF51E34237A41AD5CA438EF0E6E2D17E0ABF80C7077456EE2B7B787B78B2FF1BCC8BC07FB220A99AC01447E8F3E63A2E99AB0AEC5E3E615786433E9CD336EFEC7152CD85DBA3A8F1227A90F81FFFC75E48C82C43BF54321868158137DC1464F5A6BDE2D3C2E9BAE1E3862BC44585830C4CC31703D96E38536CD3F3B16BEE749EC318DBE982FFC6EE06996E8167DA6D46A052FC0CADD06A6E18C5202379068E6ED0EEDC6B242010C3848ADE2955C25963CBA56B26372FBE2B262E6DC07FA046C08E2F3EBE66C95FB172C0F123AAFC6A0DEC44C2DD493899A9B98A6BB9B51A074D5153621297580B3103D542D4078B270E7874A839E6F3FCECE4D34527EE80D3E3AAA550BF73AE7C7388BF12212A20A3B4B8BCAD265609CF476D164FC65DCBA0D82B4469D2617E8D2A0F1F09EC3F17997A0C555BE924A778EA63FAA59B505881DC267EB415388F67B3BF54D180C8AFA7BF49888D59C7E0B750DCFBB6C7364432CCBB5B628208466726C75ECA8B58AFE0DA8B4ACC1CC7EC58CC78E6A95B812DAA638EB9CB93F0EA2870E39A3BB1FD1C818741B94E55783160EE5168FD6AF194C1D2A502D698FEE396575DC95DA36182D2DAB495EBF1F84B1A349A528BF2FE9BBEB8F8B2B94EA80EE9D5B6823F48A2B547469DAF656C12D0C0BA30D0D69CE84FE5CA168D2B61DC4607487E8B925E1139F71078728B052FA08AA39A4FA0C3AAD2A55BE4D7937B47CE1739E27B57DDDC91F7666E0BFDD9FB04BE2F61886103E23EE8826E983C9042733F8D59C4C877F467198AB09823BB091353C857B0", "64FA974EB768A0646EA038266B6A79814C9FA2E2C7A25A903FC37183E3C71FF8"), + // valid decapsulation + ("3F6361FCD422DAB61DAEC1B1A1007CFA943D79991BF76C463D04271CF41973860F2CE81ECBB20FA2DAC145F711D923C8F2606B6CD30B62979E24619BB66403B9B25C09B1568310873485957B3BC300E59AD41C1AB13C010FF12C4B4193B7DA75E71072529027F0F518393A76CDB3C8566A0190798EE02399D3FC19451849C75242378C22D898A8FB8742120B834DE6027D8CAF52506A28823C78914C25398C387BB7CA3A8D7D3B98E44CA817B76AD1095ADE01BDCF35992D10775387A1617969A96957D11CC8E7273CED7BCC7DB0C056C1815F14676B17B5AD86B595AB6BB8D55C8CF64AB7D89CB35569CA9017E84AC173397E78B978DBE5B5C197B39823CB5A331FD5A6C5C1F95A5FF142A946342FF084829616AD571F8F864C1A1540CD8A4F0FE92C919AC37A222966E57CAE63B920F55C4B464BF0E83901B32F01300E3CB6B692582F96E906176C6FA9E34711C4C2C38C3BAE730E42C980D5079A07340E50A097CFAB1F90502BFC92A9DA62A1D8393B15C335C4D3B14CE1379096C9464A8FD3E551D3C454D34C9986D3CA576730406535B8385949D91D66980BEBF66F08B6385096C0F706A8549203B3A873A4D4381E1B973996A116182C81E43B12D9BF27612B50BC1DA20600B4796E83938506C2CAC350C611482EEDF693B76AABE8A90DDBFC66BC5C8CBA5071CF6A9563EA2ACE5994552554B5ECC49ACC4D62464FA512CDF39940E2087138F87F8F978B37874D5FB709C947082C1B29C8040584639AC4BC291454164DB6BDDE556E73A184B699A26D485F3E4A61DAA202D1E253DBBB386260387D47603BD94D33FC2D2630022D87C8C7F01E33175294719B0402A66C3885B92B396B8272546C5FF0893C6221CF8A18232DA439223A3768D3A6B7D579BD27898AB669F71555BB9A8945912125D83B323165B1B7B7289CB577A1A2EAD081E3C30DDD76A252BA4DCDA0799B36CCE8516C9F576842A92A046B56E03C80B97449C475699A45A266E0BF33A0B32CE3B6C069B79D11521288BE98D9B1A8451B8D1697A4B90ABFA2C55B4040FBF3015BE51B4F7B7E416C763D542B95607B79A43B2583B26B080F4FE7150E6618DA8513A9648C02516EED3300E7F22BBC38BD9EA06E8C9A52091CADC6A2993E5245B98547501C68203A23C10A762CE9365C904E67E06B56046CADF44F00BCC817F57E2911304EF398E74882353C26F7B57C49A5379138845203A987A41DAAE6583D0C8F32B6BB98DCB3376533E6B2A478542144CA379EA5C82800C4C2E11A91BA5BC6D3A50D2454943BC919294E63E4C2DA4A1EC92591AD63896CE1569F368FF2F6322CEA5740C39CBD52BE7706047CF26A74297B2BCC2CB6C847748C8FD4A778731506E9256BA9691121792F309CC61E894C682A7AD5303914347D44393C105C13CA1468106837B421310362BC521068555ACDD98C84BED2A4598267EFE0645DA08ECBE63C639C8AF4AC8E08C25D3C9A1BD398BF1EB5ABB49ABC524C392797658A34C4BB1C295F0B72B088A83489732C3A5D904A0C2EE4A479F66E120ABDF0649A4EF9946636914C311B4D06196193914F0B1A4FF271BDCC314BA0AA1126A37C6B30A78B70CAA092E3A70451B605D28A6E99B63E3460192DF5736C362EDF09763FE68D2E0BA8EC952E176CA986116DBC9A4F5C4880D7ECBB3E407190712143A5B449E045EB9C0F732C3EB754915BE85829B21121E003B293BA4AD62804FC9EBB258BB0791D36232390DB7AC6148EDE099733637550A49BB385C469B4AE95158097FB15E46B7AB43306E01848A1D55D32F1213623534E7863586523A62093CB239AE226CF6C50A9AD4BCE792504746AB45FF0A06F8A569B04C25333A6B755AFBC166F8AC879AACA029D3CA9E938ACC9948F0505BCC32A8604657827C76C2240CEE762A708988255903C752BA0EEBA9D41E17F0EA97A8D0B9E6F690A0CA45CD8C11256B21C64B477B8573856F130944C96C9CB2C34696F47705930D848B8B81D49BB4D510CB3384087FD9225486B73DBE57169164918C6B877833177E659D76C86EDC27ACDEC6D44B83BBA10708737686EC815AAF76F741104B573A72D5808D0786B1785936F140A3C0585023AB30D85314F25BFEFDC3868FC182BB7438FDABF15270B3CF47881483A6EF9C89DF56C7AE10CCE8C5F12EB4CBDE53CBEDB732454AF31CF6DF8C527E5B0BA5B6FF73FA6DD6EA1B4BB5276140980C35D2FFE312598FADFD252A81DFB2B921F786DB4BC436631D8EF3D34E33872E4080786D0D565361C2B4C405126A567A05187E4941ACC7FE692EB648A", "A8EB0BC44EE33E8ADF58058E35F70DDECD13B0E90E6CFC36CB2FE4B84642D83C5C91F6D49CED07C2A8EF19A4A3F4A3460A4E099E04BC48507F36C8309F196BE71C2CF93FB699B7F04488902A61E8AFE6596CBB7981F6B9AA414A2F7BBAA9ADD3108D1A1EAD73219C595E2D00CC0403236F70883FB961C72D291DC41E1FD2EF96AA45B2F738632989B7A19AE08E8E2C098BB388D5C690C1E5E4E0A8BEB2CDB2A5476BA5FACD4054EC0C4EAF8ADC2182EDE5C39FC178CDAC1C177C60DAA0053F03FF602471A8094B6BA3D76BBE195D037F7A74D41B643E23680136636FD301D698BA1A9A2C526C23237312E5118E32558C4AA9FC1730BBDD5B6AEB71FF9448E06793159FA179D9B4FF8C300EBFE1629D58D1D1803491211D43D91D432A3D0A42DC2C3857C0045C0BDF0841E3257A1CC5E030E822D039EAB72AB6605EF8C57D78D9815D7B4DFD75E48D451E550B5C3940A2B6C56D8D1E8D603F566D5BA30415C8091EB62711E53262F0C3D2F13C7A71326A3F6C6B5506CB301DDB2702F8A73C547D0C5AC0C9DE03741F7C73A1A1F8D5BFBE4D5124C0A6931E9120AA35FDE7E1BCBD0B558BBCA6EB30B6B442221A5D625EEABF41338DF4D7E74C616FC660B8530B1C4CBD70A338EFAFA53EFBDF2CB57EF27454DD3201D4D14582322956E651BB848833EA780227A0DDF955F6040ABFED8D5F1DD0655A5326F959D9123859E4064C2AEB538C8B3D68F8A3F439669FEA8445A5BDFD3D9C87DEA90D1FFABA5489B8F40D906D7DCC51D7E237AABF4BD9B04A7B4B81D9A281B05E9FD0D9BA0E15BFEB15C85F2AA8E707413CAC05F54200C37016E775F38E47F14601B1D9B82E39BDC1A7FBE16FA2A722642395DAB19709DE110CB45BF901EC3476F112AA56E6F8A5D8CB6A32E406A3DEAC61CBBD762C5342849844C3B09975AE8E7B82EA47237DDFE9BD98D6E6EC1E63FF02818B1F1BE20E6DC7D9EE78E27BA2049E8372148817B9DBDE1480005AD734ECB4F0E9ECC2813E49E2832616574276A40CB2F8B7CD89C3BAE911C55861CE329AF42140564ACAD2644C3CB08BA9D6DD84498E073A105760BF00B4", "6232F7746C31F86370F1432C6A6A964BAE457F999142D8A121F8FFA5D809640A"), + // valid decapsulation + ("0AC64A3013A72421AB1591CE52635A37BBB805449EA437A76074B6E78051AF379F12F61FFCDB02C83522F7AB4345D8280443310BF43D493C5D41C5606AC1A8D4C5C0298634EAF5C2F082173ED41A7A7209DB6CB0C5B37F71F6272C15843A181508812A38A08DBF067C173BB7FB669BF53AB527B95DE777358528B890116FB9C3CEAC87A383692DF7ACAB4E734AF059154D090EC3A545DDC70696B1CE0EC9A42902B24792C96EA2218F01492D833C6B7875E969C14B109215D836D655488BF620814C6077CAAEF626C1F4A958DB81A806E06099D190CA0C42427AA2F8B5BA30592B860562B37C7C05CA7C538B88F1BB84E6504AC6B30AE9A1B887FB94CBC623FA51744B59ACCF39145ADC93311B134D497F282C3D6171A96C736AD6BCCCEC741C9738A839007599C03CDFF1B0D6121C574643DDC57466345530445F4095412457419A153117BC049C2AAC0DE9B0718098DB4A0A30590E5AC15676BC17C03CAE83E52F19506469CCCEC96789B938661C157050777D424014D16773A1D4080EC16917D399B2410C666C1BA85B76101197B152AFDCA940A12692ABEC5ACA3B635D7193D06A5B82A53E43AC8813C30C40AA7848BBBE1ABC6A761174D9619CE6F4BEB4549BBBFB0DA2CBB5BA66B9A9514B52F8AE0AF55D3AB704DA599B47E6B7E79731A2EC7E4053CEA0A79D38A380D8109208515528D41D31303A2F49B5C1F5B5668849D2A30796C2A8141A480B1383CFC70852B980B5138DAA96CE05E143ED3504E2DC6DC903ADE23936DA14B31ED619D9842AAD68234DF95260745B99388213B9580FE2319A2152E939A4EDD13B456B42FF48B53B4929E58A0301D1B01A8796435CB8BCD3181152474C2CABB6452CF63875883429FA3B8A4F8805AEE303683B29C5D59498C5B0F4F546AF358D36F294B422025A876868C923A9717955308AA1B188F7593467EBCD2FB533421241DE9AA59324CDF3F80A77485E187ABFE7E6BB49548AE957BB1EA89F8CD37A70C558E9CB8F62C32C77793C2A92A5C1E27072481DA502AB1C157F2A0A615C9222D81C1711F70E8EB19302637215E408A4857BDD4B9C405608E3E95AC8B13C69E72A43A8116F90AC571BB924EA1C3E994BDB30B9913A1C3B80B40392C4391B131A163401645F9A45035779B240491949E8595462CECFD161D012173464579137360B68A60C52AC3985CE82192D45C3CC358A5B8DDA2B76939446115C5B0677393C30F0B46F5DD1B94235C82C473BDF338D7AD529E10519B5668603AB1988602666090C8805BAB05A53953BA79F024FAE6A13D01A1FC70895C27664B089C69FE19009A80B45194E445B64BFB6A515A20D67A7CD5D329DB1B60929E0BAAC9B67297774E259C8B3F3A606494B21417B7469476CB76A84764EB1A63B57E20BE4FB4FDAF43CE9B633D9F0C93668A11E3A15171B0480E18E4C55390F43117C37265C5806A4C393AC2A7A5C3B581169C8CBC44DEC319A47D20780288C507CAFDC6B6D549650C8988E6A9723D8395923943BD3AA386CDA3320632A4A809FFA89837CF11AAAB98C8CF901328507CB944D84E91E1305449B416D03916196C03CCD0A86DA978AE9CAAC92342ABF0AAE8FC84413A3C3D68BAFD9A371F758BC1A03238B80657D7212A484A2BC85745D12624B038379D221ED512879BAA91FF59A26E471AAD875A1B686B0B16E58F3A7BFD84C29769201EC4F4DE3107F1547BD50B986017D2C9A29687923FEFC4709B73493801149457A8B37914413012EBC37FE700D12A231B6A321CF7BC3C59220ACA80CFD85B648C50BDC7BC9F478BCA7608C4EE6A9C451A87DCAACCFB185D5666B2830A695082B24B3B24357065C41AF438A5B13B169C0CC35D653C5D4F174C03BB9FF43124747ACC9723BD959CC94180E134C679C3204BABA8AD43C7FB455468CC8533E444CEC311857143F41F913CF460447D487115530FCA3AFF13C5C31A5635C923F5D278166818E3980C939112E18315ADFF845EB645FA75CC33435294606B0BCE1B0510AAC2162913E4677BADCAD7A7049FBB857F58C5FBCFBB20F90319911BE22B0B8425C2BE15A6755962349C21E55A823A8A77DF0326FAA3A6C9091CB57C5446883608BE2A2CE7AC0CE0C3E106547CEC54398E29342B4A19AE9A47C11CB50F232026051FFB0CD3C4A596DA9C0CDACDF47379A9DDAAD46D01B525849F62871783288068EA28A19997C7A03BE4A280F032731ADE86900F1A0D8CDA6758BB7E3ECD290F8969B058F20EDD71120EAC448D59C6EE64AD1C9628937FEF4275487BEBA177249D44E773251B441", "E39B4C28935E605361E62230450410B04B36FA339DCDFAA8471F4B4274F6568F39B4B7358B2ECDD54F4B000273B8F83D0F6EADE8D4497958E357FAC7DF67924F06C62B59A37DDBD0373A595A2C3EAB451C83023E0B5BB009F8073B934D0C46BAD1EFFA079CA378AF999A926E8DE404A48AE3B60BE0E63348FC80AFA29A5D48DD99F58E57902B3A484EC2DBC32B5B7AC5956B5F72FD8E8511EAE315D09FC548FB499669A2C2D7E548333A1CD3CE314E5FBFA98615ED79A26186447FBC52DA56421130971E7102ACD53EA7C29A30EABD6EACE75A79D3643FBF4FB44AF0C4C53886E4CFFE73287C5E070B44B4AF89348D921F080BA246C08DBB81E15F90B4344F1AD0CF3131C2994B203CD5CDC1E6B8B0ECC1CFC85A789148017098959AF216F5FA33B050B2A2FA6F0931E78CA3521DB52E1D330BF4B3A117220C962BCC62102CEA5814B3CFEF670BF8A00A058EF53D67FA28BD5403B68E59AD0289C479CAE5154F9102CB5501DF6F2593FB000E7AC71CFA95364184E7BDD141857681599A561D74CA063E3661D066A641F2FF608C393FA8A9DE3019BBD6FF85F6A2583474EA1A141A2723F43A022C1E92D037DC1BA844475045B8A879E577988F8CEB7AC5340EE5846E6CC19AC87EEA0F3D8553397B4A3E7B9B1BD4E920A829BD550D8369FC6A3B81901D46DE4B75B45932CE43801455ADADAA3EA47E313D77090ED3F8238DA8824ED0B1D90BD83708BA467FFE17D0416EDE29EFAC3850EF80D818C1D5AD1BF5EDB25A562DC0BE95EF886AFC79A50F12E6F35D4635A70377326EF82A1CCB6A9827E43D9999A26B8952EEA3001DB8E6D21AC20F69203DB66945D05C5CF3B67C66A29F7E99994B9125731582F1B5A1A5544E9A6CC83179F247492BC653807FB7AA29127427E69FACC906723C54D2854AD0F365921DC345426817F0481B52B5A2977620168168C921480335392DA75BDECB4FF3DD0F722B7E03534EAA31A066B89431706253FCACEA4BC1E5F7194BDB5034D6578D5E75C4B4516B907BADC907314DC451427A68819A00E705464D394CC850CE71E07EBF17C7768CD9198DBC940EAB1D", "19CC79A25E516B49F045E0E0CD715426A5E44CD8C8DD0A2CE7477451AB1EB1C1"), + // modified ciphertext + ("05C52D7B334DECCC71E083A807F14593A2487B57585EE4589FF607916939493539E121C43D958CB926B720AB46C1100922974A06A7772C485072C97427121A318C5AE2C8A47DF579F75008BA1B8FA6CA5BE19BCB21C55E161BCBB83B9D44F4BB9B3606F8E2730BCB4FAA1BA94E57AEE50052A3F653FB440023AB2F7C7CAFE096BE9844AFA39282AF24046C5735BBA80AF0680B32B25CC7D530B2ABBAA28329DF34CFE0317B0DE34D0A0248BB60314DEB2F80831575765D19199A234B3BB246813D67764BEC958258246BC936D8940F68291D6BD4707CF02AF26052271C0373F825F3B5631F9186926820FB831D0F413992169293B847B3A04742900FE2E4A7C6541F5CFB7287F851CAE79A320114B84391009D2CFC0961629CAA44D9A98148A72DD7CBA8B0A8105C7BD6466E25BB45B4857E1D097534E24180812FEFF4C56FBC6CC14317AE11A381447149A6463DF582F765363E0656FB0402E5308AC62798098CBFF83C6A37C51AFA92BFBD162D8209D02E1313AE3BA215FC267FE693FBEB809B453A0E3AAA28388DE3556EE46793FE59AC60A85D0280552B775EABE2337B0AAA79F238F2F727C5D5A2628B3101E511EB5696CA3A7BDDB2C1F6077C44814D8ED53FC38151A1AC47C37A068E3C222ABAB3D3A2CEF24799B78C73E42316F5866F0D71B5E5C35745851719F0575EC70C03A9C355CC68851B1735F37B845285826A0E55059E87024D3BF02DD14A135AD8B5494367CBBC5E4BA76ED97333D0BB900266203534A11F91124D947BF251C85A4048CB5BBF558125D4C088EA70C3B9771A5E1720C7CA58FD22A8A3428654622981BC663C547408441ACC1683C415163D6AB98CB0A659973081D10EAE875EFB787A08969FBBAB6E35292FFFC7B17266B7506AABD67AC1AC8803772B2A598BCF4B2B2AA4446BE3301DC5B59A1BB2467864A8C7FB8F88736CC265BBC81097220573C5E57A87951582B28A3AB839EE9323B3856E6B9115FCFC79FCFB0113E57B66514CCFA891FBDB137CF4416AF463C1FB48C5317430F92FA82B83FF60917F0CC1B8A60BEAC41A406796E7EB5266656616D85D3EA25A21E97EEBC469B2BBA977B97336223202B072D382B90E0986A1241011E7C85446CD340850F9E20C66BB70F686C8187C111BD3486E581302F28083D7840446359F9585448448E4C6C191A17A05301F19FB9AF37989664A096DE43FF5EC9390F9330EC87342D9614995521A1AB0A387110D183C1636BED16B3A0632AC5F81BD4F18033DBA2CC0D97092722B12F227678777F67CB601391D04223D3CC6526DF4361F7C555A1AC30649721DA548D5C4754668138D294D8316AB1A7597AEC041C8FCBE63D469DD54A45F876F3EF10C70EB0E400CCD9C8AA399E7B377B1AB8E45CBCE34312CBA97F9C3A8BC191EE00C384163023DB276BD6C8F3AB666F60252DD8B5E88A1B1A2A31ACF851C5AB448EAA74797FB5A4DB6BA7824789931501C042EF1D6BB63EBB6B39401113BCE1D6944A0D596699717E315B42DF86CC7008299C36C4B4B884324C6B6E8B780BB4E3EC74F362C90A9451B13849144D95F7D084180BB78E619792B5562C52CB29F6BC0C1803E5C5CB0C74A141BDB02CFB2B05CFB6C6D1B0BFA562745B984164C0D8749B931B76456F086B7052FAA60A11F04724ACB1770080EA3DB91ED5C6C77A903A3FC1ABD51206C4C5DA7760FCF988AE69C52BBE86EB7CA10705BAAB1D59383D875F55081FEF34A07025FC7F14912D0A72BA54414493ADB600A7D104E8602C3DCD686778AB22B4480A06C98C514300BE2C9DBAA33195BC8731536878A7A5CF783B6A5BE8B55B9D4DA00FC258F07223F43A40D9730AC064576683A9E1D786B0DD094BFA6795207705F0A6F9808BFE3B00AC294137093AE45A892F3918D4A8BB302CA067058C274E36677882B5F53A8B6952AE2C483B1F3BCD3A3266DD21167025E27600EC7748BD62C2AB3C09449129AB4B8775227565B2511F51457AAE36495D5862A452767A1422C117977A85F3AAA503B57017DC9B4AA09406B1A18E2ECAF8C761AFBD54427240E442860CFB50111867B856465C2D8A7B428710953BD1062877E2946E9A02334C461954272F3EABE0A6712F527908039A4D890138A641301969FEAE91C348878D53CB8C3D9170A07823CC05B638997928A0C593EDC86BC93C98C59B39B2E602B89129FF40A7771C3CEFE36589852794DED3FCE5A81A20A52C286EA35AB530DCB83DAB5C77B78AD795746D27F10B90D85881E1ED7BD51BD6AD69F7D2EEC361086A4B087F4A76067C53A142C73", "0CDCFEC9D62E3C8412849705B87B4EA72E6D5553B2EC6F2AB980A9AEFC330224F2E7C8B969DA9658DB470588023F3A2F7DEF4C3E955C34B594F991E55CD7C26C5AC624906D2C23955F104F4577E40F51746C7C738BCD15C34B56A949529A8F0D5DAAF902D4149EDA9CB134D5FD57D7795B52A38E7C6165C22C0AF4B13A127A2A2087F0B144C33464281350D4A4AFD39901CB1D3764A7340D712AA6CD8AB0E1464D84D8CDDCBB74A01C98979E1AAA21F5D86A7D2C882D5F02549A75E1B91A8FD6AD02A0A727B3CDAC1E2CFC2F5DC9BDB9B86DFB76919A42BD98DEEFCF8F9B322E0AB887F522A21AB2234F91F9055D32A5B4E6F78B52C281E3977B4102350279F87653B6FCBE5D1F150FCF6D37EB73F8C823AF15D39A26C6B78C5E6140317CCB0FD929BAE1A7C34D0FEE595776C755661DF749D1FA268E67B0B2F3716DCD7B0C4B6258C6BE84EBD2C037926183C6306420585E9362478662055132308A61012B324B5DCF4EE3A8A25060139E297D9D183D881AD9420676B7F8546A0CE8CCD92C3B89579492F39BDF0148923F15C2B7F3A77E6942B8B45CEC2307AA935F7A0A2448F4F3B433888036FF4E5FADF50E8913419CA17EE5A12453658FF8AB3B89ABBFEA35BA945800EE237725209496B2B5D598F1F1CB9779268340164C8690210139A4A07CBE0759A1413E0B28E9F88E51AEAA13F1ACCD9748E576866C37D8108067CBE12B1F6BDCA0B98FA8A0FCF457A7757390295BF11BDFB637918DF8868FC4191D94D49FBB1958880434E4C8B74EDC0AEAE415A29BB9F6BE5BC57B9D6F2750B8C435FDFF47F3F22F099B6EBA506B47819624414CF5C857DCB780510F5BAAFAE242FFC4FC66431AEBB85D5EB23745E4F74A49A5F098538D653660CF38BC567CA8D9E33D9B54BB224C79B5E78F4592BE4D232E4D08B64E11DD27E78BC526ED91D016D93F33946F4FC814853168831F3DC868C6372EC955CB40CF4902B4E02078D2C824325A30A9B9FB3A04788C51A7562B17F04A65924D8BDD1404867DC8D703C53933F4EFF7404D0CAE3EB5D6FC85DC90B47CB345E5FFD5A240D350F07566155E40", "3DE98CA3A5795225FBB69C5C80277F9B5AC7D370A567A1FFD130D5C1FED0F588"), + // modified ciphertext + ("00897D2F39CCAE11888BD6AD0EA8C6301C47144178525092A14B821D004561F1450DD9B2DB99A85E75B1AE401EE6295572318E196260AC425860748937943DF646B5673AC1BAF9B45F9AAE8F676BAF60CBE8255466AC91AF67B398B07BC16B11EA78066B14023A276F1C84348AD82DEBA62599EA83C7435BE9395B437154ED789DE5649BCD12AAC480C25C670E70B890B2777B8B6AB432F778110B1C685B6A00201931A9947E9C2C7C7C08F7708C1C7A7A13BBC71BE5240F2193600731A5443A40D7CE27A34F9B3B689D4C751FD42FE78B6CD45BA2D1755E137B2EBEFC884FE3B04A16674A113CD791595E16B7058C8191D30AB3F89AB1E53C730C05A0268CBA55475634BE3DECBF3FC93E33864D10569AE5C9A613CB8214431FF9CCC499DB0471035936E3B1B0FA7A1AEBC7CD778A79A863B2C1383D5323F43169AB999BA1442649F532FBEB0D407CC2372ABC7566027178BDDEF73BA68956F28827D11BAD3E655045186DA43396F2C29C02062932E9B7EEF46C7AE50484422CC7778AA9D0B8CA343A45A623DA15AC20008E186065DCB7B71EB41B0F7A1E5D3B40E1416F768868BC6837E7845C7819963154B5B41208B3D75F96586026053086A35BDFE870E9952865E33189852CE487038E687C2721AE1C6A1A0F722BB473782F040ABC3743C0D86DA708201A235C2EE9120C04AEB01CB8E36C7C0E27718903A45CC97406499A60857DBB749A1E1CC7E3D5919E017D0F4CAA3E1888BD8B3EBAB035B4161E39568288793FD78420F6D15D0925587C95573EF182DC83150C79239A818A05926C28B5B5C079CFB741C361177CAE058A3B76C177C82A447549A37A0DA28244EBC6BECE59BE5D27B6E79047D1E0498E870244A7A0DE8334C1F3BEB48219F0064DDAA6AD9812994C06A6837CA156D616A76B0B1189949C954136038AC9255E4BFC6979CC2D80B5AAABB93126B64D6E4C7EAB06256F86B4F5B1CF1728303091C9C8A90DCCC73670F839EABA1B137BBE1C9789C1B0509DFA22E9FCADB36A6B3D0863CC6874D525A19A950571FC267DF7B2AE657BE972124B661026133154C1A39B377D3C8854A120508E702F958492DDBA6C4F200D9FB3919796540A20C16E684DE3E9AFD4BB9325158931D23FC2C36BCEFC8F9A71208A8C1F2D4C590E58812D0B93FF227FA4E09AA680A2693461DB86454FA921128CBF54623DE4E5577AA818896760FA642D592A20C992139E250731B67B69770048C4135E220D88DA4293D28D304199B255AC07A32BCA637CA6B72A8CE1930ECA9F9556185894201E1C657568351C477BDB1B1EAD9BAA69524466A39B029954497291279B39261AC3C18C87E24B1D2A597E86112768C310B981C2DE7CA3FF0767D84C7F6BF63DE57171AC697772635BA9C9589773072C63528F528BBA898C00076A5EBA6A55CA621B04822EDCBDBB80B83BF8785D05790A8714C2B498585A52F55C8A96E41D9EBBC60148C300EC2CFD32590C42BA026754F79570F0E1A481A41CE300728C75AEDB9556142187B1654398B12E64535D8F103BFD360B63F4CB30D7BCC4C7AF32E48025F490B9B7C89E302D767C27BE144E228778427B977F411BE4547CC805057AF8A51AB1A699534DF734A230EA8BA035ABBE5B76C07AC516785DA2806BA9C822BF0A92B2E0672611091F386A4D9B261B572421E65F1BD1AEDDE8740490B8CE10567D4816C0C711D3D03A0E049B9B6036D882BABE541E0FE38C1EEAB73DEA9F66773414C503D8E67A79383683454333467B813B3774BBAA3A9072C60637D073157344C4BF12B426D8315484438A01CCE9591ACF2361BBD535DB63212A613A88A024B5B0423340324E2C94D8A8B8022B8CB17A749F7310D40C84654B031396B4C0DA3853D0C6C0491164338D126B8D7F3A470AC7B44426C3D1193A7CE243673C0D05374324CA9043C96C4DC23385D90960999541367863EAC311C96740A0687D1664759663158AAB5C143BEE1A44DFD44AEE6A91C2366B25A5C2B08143C0AB475CA205B7C1C1AC2313F4A1080ED007375585B4D42578355C56869B9FC97D4E7676477476C15AA562F702C6650CD8D88A0C276772C12231B0AD641633A62C8CEFD082A5B5ABEEF098394BA2C4E55D7070A1D799C4186053206C90645C4A900CA2069A23FB5C909DEA7F20C2D35417F046DD937CF7DA8D3E764C526473A3D594D53652C29C492B6034B6B30889AA6652877A84739FB51155858C6B16177C948CE8112422D28B82FFA9BA4EE3EA4D5C93F896623DC8706BEE4092D2DBE6FB277A57085C074136FB4B93CA", "BF78E75459EB54C961443A39375270B876E13B90F676DA2A85A545B12893E6E9678A28BF4ECEFB226679FFA469C028BEFED7DCA3EFD1B59801A4043AD05AA450BA24BB2E336EE9CABAFF39CFF8B9E55F57137271AD356EEB2CDBED441F30BD06B62E00809B311CB86F01F5152ACC1C2D847C908447D9B71201B30E654799CE50076AD481C9B1DB40EEF47992D455D8F5B2E88F9F62AC223D87395D580C0E2578F136CE6F91F880F9627ADE5A9178BA5C5C0B1FC93E6E081FD172131568AEDB72A99C88110172357FF8E89FBD6F0665A65DC429C5FC2DFC24328AA15234977D922DF0E9D78D8405402DFD2DD5FDD8AA16A7C3FE3420339731917189A4C39179E75804211B072C747113E4DD6C23D967D3564CCF6E47C4F1D3EE52F02A6D2E16C9AD74042AB468CA355942B115A5F5D57A95F0CA578E9A35EEF2303F547BE81161EA5C2F7A024488992F1F0CAED68AA24489F45885CEE0CCA063B7D3F85E7305EDBC6B0CFF73624677D92DC8DF66A14D1FD05D43BF41AC9A23CF40CFF3CB21C7A5B0D6CA20BAFC02F7F2B58A45801CAE3C7BC46141425B15B33EB5ECC8CE5EAB005616B91985A32573EE23064C4F46E0565729583C055E992DEBFB090CBC2E5C59643EBC597DF01FEDE1097DAEF224D7CEA21D2BFC76E76EEAE11396A2D37B4D7C02A2D154480103882D9928E380DA4AE69B00393CF5E0F431E9DF8C0A34B5F549471C2802E649AA01F68F08B0FE1E50504EFC0EF9D45D3921969C09D2EA4333D93B9CF4B4CEB06554E4FABCAD23891189CFA71A71062C9AEA0F80043E692D721F0C22509106E114B7CE611523C367A5088CF638271613C7DBE58C8A1B97AB72954A5AAAD391680BA467EB41A96B9E5084ED4ECCE9BEAD8580C6605683516A0BF51BF37AE875CB076FEF5311AF4BEF5F753387293AD2BBE0963FDD16AA8183F4CA7B1D2CA0ADDCDAE784F25ADAADD285894F09CA431408EE5EFB8524107577F76A47B0B0BA40E767B07A1C5673B5F2467250C05B1196641ADD5E25DE9C9A767B40D2F65147396E30C75BC006253E0F35A8972976951E2DD16005B417F1B94C1013", "36A5BCBCD7DD3B6DE9F8875262DFE47C4D1B75CDF71A56CFC64F54E64B92A161"), + // modified ciphertext + ("BE862BB7632AEDB569346A4DFBF44DD3B76416F8A546A3909516B0D8E25CA1116A61E254CAF40BB52182A2B87E5A7C7B157A07690533927B303B0698D93406892212846307EACC26BCE18F4F34B230A098C8444D5EC687F9D3BF8FDACB1EC9865080C2F9C77CEF2657B0812F6AD56EE8E786089A45A6162F1FFC92E5259347374EFA5B417BF1CE1528570D582EB61106EBAA72937A095EB9A374F69A16E85F584B5E1DECA20239A6312909E6535B9993094C512F77252B9B4292E06A4CE84A2993070028623511CA0351630A5B0193A4A477C8A4BB4BA9BB2DE876F9533B8CB77232384277B87EAE6A8F48EB28D8813A25750738348659F78E2FDB87C646BDF9FB254559A0FE38C6D1F6794A6666D6147E256706F4405E0E99AF7D745AA77A7B66E9301ED866BD9A1586D047AC9C2786B763057BC33CB70C628766F912055C40AA4A7423711C4E07C9A1DBC6531AE96137E0B5E4B811A53B9CF6E8ABEB82228F1718353C28EA7C80F4D871B565AD339254DA946EB67A89B8A505534BA4DCEB7F20968969848B8653C7AD4590403CC628E10A29F1B4A95BAD448A0D89B96AC12A7F5644C2739496EBA9B30CF616F27B42023A7753335178140E65886381E685616181608561BE82592840368D2B6DFB20245A623537A20B81D798CCF5761A87BF8C34800B7B80B4B6430B058C62969C2868CD60CA57C4B29E2C2B1AA1D5C7FE665F17AC30FFF5428B373E0F488CAC977D1F62BF3C113FEFC5931A70A3D71ABAB75C3DC306847E075706CB6E8A137012138D65F79F6CF014FDA9261BFB032759261C057BB6E509CD5549C42C3A398C43D61815165873546B21AB7B0B29CB5978B06F67B3C7795A367DA21DA11C3BB1A6A8BA949AF68B68DBD3AE0009AC2530B539274F7A7611F6979000C7593B844ABE44CFBFE6166FB69438969096080322629550966B22A40FD3D84ECBA45C512A3172CC4A1712283897805EE712B18B70EF1790DCFC94B191B0ECCA2A14911A0760C42A852FF7D31E8118BCAD459393265EAFF3C61C66618BF70CCC75BACA898061C46478274986F2346256343F20C058F994FB547A0BE68EA99938A7029B9B86A9F38687A17B1950728B204CB70549B115A17EF31321A7235933615FB8C7913B4BB3B85BA7873447810631AB480CC73A087992B36906AAD899BD68A26A137045E2520484A51618116AEAF2CC6A7CA918E45D2C405AC5512E9289A089D74B01FA9227CAC3F20783D6C099715681229AC4F97390FB63635B0755588803808B6FB0C2AEDB0665135122267927361B2D66BB1533436692F980DBE5AF926C2A96703E0CBBA855758422B4CE0E019A5DC4C7B3110E3D519CB68BB00A87905F068A8C689BD871CBAB2C6341C008346091CF1702EE733A8D83A56D878DB20B24BBDB9897E36FE3310950AC19A891499C089759F24DA5D190108C55ABA1CBABEA355CD15E7BA8200F67B43CEC79B0F790E3F4ACBA165A2DC7235CA9C10061034B425743A2988A207047132071398F22D6AC68002716892A2272B4D12481046709F1216E25C90CF3B72FF5AB9E3B71878D75AA6639975AA21ABECB83707A1E48AB55EFC563B2A00C22F34273C91231A94655199CD8E2420A109121A268EA1787DEB467940C5697AB5BDC0C3B16316A50B96A78099E10D54BB763BF15A76552325028E5AE1780458C3AC1831A7967C5260592B1BAEC14CFD952B8251A31480DF7147DA35592D38592B748A5F248C16B20C563C35B9F96AD92489F2DC109971691099231EAB2BAB11426DDA8A1041C601A579D68C07AB8F102E1C1BE889A563852BF59A9BD2442CCEE1957F65963062C5333996CBA731343839D250751E9294B4DC83CC0152E4C2790ABD8698DFB90C609A5755A3E5CD259CE37897AB04DECA0068E81767383996E622DE5B53E02BC0EDDB4B6A49C1364A12FFD4B0161219EC8945112A0CC76D1C436399D44B719D846318639AD0BD592D30C4491D19D74A6B3C897A7BFD954CC912A60933B3AF0B280F40252F70E4E858FB738911909532C048C2EC25491E6B9DB8ABE6DE0A722468A3A544F02A7CB16E731B57925B49A9F48067ED5D80F8E97C266E0020953319C1B773CFC74B2321754511CE8B597B1E708A9582839355C16EA8F4DC6605A086DDD22936A0ACEBCA76B5246C06BC846CCA8DEB9E406B923099784F17FFEF95CD35B8DAAED33172D83B31EFB2277EC16A29146FCBCD797412D05D2CECEC79E8521EF5E326FFDF527209190D6C93E21623B0839BEEED3313D0061E298AFEA9EEF6270F73E26E3E513694B", "4635A376E61E4A6FE9E462337229DCDE81D11F87E38D3A37B2B3487BB9AC85FFC9D45881F10420D2953CEEF13715B2AEA5F3E3CA4F6757BC27CE7AAF07998969DACCB093C2A584C21DE4D6E3D1BFDFEED91550F4D2AC5972B1A581D02C1EFD9E28FDF32B906F1DF0FBF73527DB5CCF326931407A56A64035554CBE3C9E3CBCC2794D9B0022A87A31C322C083B8A923C75DB70485AB2EF4E89929DF369065D3352EEC7F6D3F8607DB77F1E25AE4F4C46FA724793772175D48439AAF13CABC7CD70BE5FDCFAAE1079EFE14AA1F1A89BA953E5918142C245D14B03A5BE58B696F80F07E44065FCB38E63A80559063E0001606C048F1DCF29DD02CABAAEC247B36F132B4A9413D4FDBA79C2068459505B42B3EE719B9E4B785E1405BE2843A30B17F351B98AF012091B91C414F1B8DCBE92FF47C1F979D3FBD4EAC19092A6B7F54CD3ACE6925114659CE8C84B44C5B577B1E95EFA7F5EC57752AA03F10781156FEB3514034BD382470DB6C85AC9E6AC63C45D5AE51CB620665C53F5E15371F1F4888B6EDA8C7F064A44DDD065DB2E57ED7A7BD096E51C7670C9442DDAE096CE0A0BCEB1711E720038A3437AAF4F2E343DE2504AA1C3993F0C2AF6567C36376F0E863F6A42FA5151A3A9C2B4CB72CFB9EE286608AAD9B8F57F7C2162EAA074A1F09DC66119A08FF2C703A1C8DC43C309F4E6B10C5A96BA87AA1635D2293B12365E6BC2FFEB0D15ED8F4360B9B0B790DF575A1904C53B40F15E301F78B38235D8D959C7DF7990459E0C08249F7AB50D23F940A1F511F61D9E404F8EB23DABBF0D4C82432E0E043613B556B96DACA4094BFF69BC989078D7F3D396ABF0ED719E19EAF4C80B985F0AF88174FBE17F8C666C054DDA825896ED0CBF4001F846A7CD74A0AF1487EB22A6692B5D4824A5C3B1F5739CABBF9DEBF53521B375A83C49A81465F6DD7CA562244A137F68CC8C9874EE0EADF84A2087F055669616D4EDF52A05FF271002A1416633045A04B3BEB20119EA05E9EBC2924ED42B74D5086CFA3D52EECEEA0E7D7D49FDEE8BB06BFCF997CE992644D510982E25B1A74F69FB5A9AC28E138", "E127EB4228395463C415F558464E7A91A84AA92C2F476D8AB05BCA3F5435599E"), + // modified ciphertext + ("1E147CBDB5081FCC5B41461AE6FB10BAA09DA3BA057C9B2CE0793F05B525DEEB046C42B3AE2A14831014A367BC4BE913A1C3779110ADA4386918287A75F2AC96F6A010352DCA6B5189BCBFEBA5B524FB3A2E203FE07A1B62CA9D18511050DB37044429C1D0B5690B3224A0856C46B78A63A1335B7696B9C792A7B8DB696DB4AB21E83523B9E75FAD29B940834A389798E6042174526E4403B8977C1C7881132C8CB5348BB34B40B0966CCAEFEC19904851947A91D7E25C8D7611FA15C003A0C938100FA697536136CE6C848DE34A71F86BC68EF02D3B460FAB701E41D70050CB9F88A9973D1C29F2719CA955684CE225D033B89F8927E3171672D5654E636B03172347EA62216BBC3DDA0BBF052C31C089EAC65684B07CA71849CF07809BFA48BAE59EF654B3E300932403A953F3CB2CA82CABFC55B3C4B9BA0420988716E4EC3F6D460B099B2923B517A4462F2113559122898C11024406516049B39E4089198CA85D1B26E01303C2336107B75181A4AA4C39456E6BC0E3D85C46A700F48C0B738703E0B76C86081ED01B6A2911B73B4779EC36354B9639B8A5971DD5623C9965F64283EA578558A49CBD5CB6BA71AF90509E69F7BAE5B2C76BC71700D91CE7B32E127BA73A134190290455179ECD15CCDFD13FCC548159E17B1D2C16AC477AB0C721E55696891A86A7B363D465AF65CA130701A91790420F38AA76788CA0918B5E7A5F1D708C1FD7855C8147415A658CEABB1236A89FE2964A0B8E9B80A926A3756CBBB1B594637CEA3118F23256F1513FC2076F10C19F323CCA44B51B393E2C042B44F9581B719609976A07A768DED88FCA270B7E64480EF18628BC6B463BBF2EE9CEBE6811B2604E3C50757CA658003291EBEC89F8403C79F4CB3A1195C342373BD012D215C90C66842DE394E6E65FC2255CCDC239B72884D63A8EAB8C70632225FB62600BB3282BD82C821867A73C2D2D7BB9BCA173BAB812343015CD811A4408BF637282C5F21C8B843584CB1BA56366AFC029726951370C22D1E79195542F98D43E3D1CB4743A16BBA219D8398BD65065F0A2B4C8019A1740121F3C2892743B087010B5F559150C84651A1B6D12AFC9F684D93668503B518A9070CF6CA3BFD7329A44680E7368F4D785300783F09091820A0D851875DE94CE39E9753A88CC4993C01D6582B1E52B53D51BE441AFA099CCA9B4A78469BCE0B01B3892BEA402ACCABAC6C52C867B8A776613648A963C5C64A978C63F62188A740719A5808273A9BD84532914F783C3B0B1C5E54CCBD45FD257C436DA129030A6A7500B39E220FA67115234330EA330801C1C040709EE5968605AA773EA23E038A7389C5EEB64A645898686322532935FA950BAAADA490B249EC7D5C795113EBF2B6916A0772285A3A678470A42A7F677A53002115F54A6A712AB688C2333B6819472525D339B20959D5634C185976059846AF710C2AA9A127F842D02D54C23D7866453953282A2D4597273E3BB1F7C71E94C3707E38552B2CE6172B989948D61A01945D2599E5A99D05544DBD80A75B273AFC4CC906389EF44164ECB87BFDAB29DF42B498343E89A7850379311500C1CC8094B0227947CB17A3008EA163257926461CB83076B6D09EB80D190287D5728F8F7179D5A3E9D6962CF8878BB695C6F2A2A6EC27F63B87C4AE1143D30CC9135680914A53E9A0777B99BD0F7C031CBC886F9991D67C1169883B086C39D174EEF6A1495409EDD91839D66BD1AD5209AB32A97230BF37129E273726AC7BAFE7CCEF37365581BCCD03729E1235C80B475F7B563AD31C81D49B5BCD0AA24593971E93EDD963D31545418DBA162920FCB3BB1C779212B726823540C1167172E28BBB5714292956C6F5149829A61F1D3A6194A3A7D99CC54E12C9D06BACDD011ACB31A3F65AFA7EB182523937A49CAEA6BB4D7A6983B6334A23C4BFB041AC44CBEBAFC5225EC0951D44973090E28299187414C6EFC9C07763B7BAB5D6790B97987CF8DF2958250BAF24814B5E30F4F9A0A99F03EE39B8FA7498EB7ABA840E50DB2C3AB9D869DC7B60CAC6455F4531F3B57804F414CF0E0CAFC3978846C4C16C0CA3698C5B06519E110C6FB7B2D56D741CE13556D022F9E3775FB96291AAC43E0F7374815B029205150122FC0DB12539110D82A7561C94F046918B0C80DC99F4E41D8AF986687A90ABA8E61C37B6C590F7F2F2F17A6802F4EAF957851DA28366FF95FA05B565ECF7B49B862D5A9EEA0B60F7B4C1B358B94DD04925D84D23F87A62080DDFAA4AFE354C71B17ECE01398A09A50828D4E52CD", "8367DE68855C4267F0152267D9A84CB78354D2AE11429EDAAA8732CF0874F3BA113E750C33FB2B5F260F5074EC357737345E82045BB988AE7DCDF13CEE3FA931813DB67AB6ABB51D152277F4991FCFFFFE12FACDFCC3960FA7646EA287A0089FEEF0608AA689C6DC4E2B1B194DE4AF5C1C05A36AE7E265974709D795D3508741980CF57C14501DF3F11C9B21EBC056F86450CF3CB716B8A7B6532A9F3D8856770A455156470BF85E9E34ECA3569147020BE908473AE44DBC89A726A974956B113AA490CB4F3B3997A81121F7700210C01D6CB10DD18C903CE8127418DFBE03BC0AF678556A038827A35A82396F2F35CF68E7F0B4883664A1A098E9578A375B93D39B07B655F06A83FB3645F56AFFFF975574EE395C09752063BC8EDCB075DC71EF8EF3CA17639CA6191775C1576B5C0A2808701CEF8D7C9A6FC73C8D2B0F0FC3923F5014A8442D3F8CD22A43C4C99B6D498DB3F7136B785463E4138981314DB54B40C3496006E3B9AA840EBC1470732A1682B53573D02BDF5B8E1E5BE76E0472AFAD94C839A9577B5FBAEB1D9389815A60A26B2F8580A3A0F5A304FF6C128D7CC445253FF3C42027F66F1590EDA6A91C697721BEC9A6245FC88429C7C8C12780332505EE5A34C6AC48A29675A2894FB3055709339016DC9584C35A44526C4EF2C5FFBAFA22B7BCE03E5BB20256D24B3A5BA1C41B4648B02D841A55C6FCE58A58F7842969D72B3F2141FBDBE341531205AB350AE77E4A6AB01B997C0C9FFAFF46ADE368206952699186068F5567CE494AB302A31C5BED291CC3DDF81053C27F9BF1F215F15ECB2101464079555A5F332FF862CF600C5BF0A195DD91083B86BC6EC1E08506D44CFDAEE6F3D6D6F086A239E9A89FEFA845E57B04EFE62B35B54087908FA81BDCFC049B46A959F5EC7F4022E58831E3B523AF7A01D1EBC1580CD629E6BA2F690D959CAF01055F978B88CF695F2E5441A688D7D876994463C18A2B409560DCD814DB8FF6D23B6D51FD9F6F20AF63987DFB48D5F9C3EAB09C3343FADBCC5D7192A4B17D80DDFCB3E6D2F6531574F11B3F649D04F7E719BF8E1BE75A25", "CD6BFE40563C026B09692C7BD13181619101E6F35773F0B00C686650DCDC4EEC"), + // modified ciphertext + ("22254A698964FE9234C17549A8296C00E440BC71CE01C9302C4A3D11CC4C86A74846A6C54434382EE92EB985916AF48351125A73900A699BCBF2A152983257B2F28ED16286BC2C1D64247E3FD83FB69988C3752172E4A21C9199BFC05A0B74AE32A0B73E74B0502488DB0C416A0834F4816D682173D32038004DC873038144DAC76C61637E35B3C0F37B8AB5906CF67EAD97C00D57675DD1AA154703DC9B8ED2F8BC57DB05CE47430662553C507804F97856DC4F45422E8B502714CB587A3014637534D7C3742B762230FCBE7668061821A1191A41A5E447DBB560B5267B4EAB4F8ECACB4DD201A2173BA2E55395DBA0993C83A903C328801539F734EBD6232E475135D9AE6774C56B9083FAEBB308D7214F845EE57332D2D06F89C4840782C35471538C8403A6B751298BB1E7274B165C4CAEE50A8ECA2C90DA8A02A25E81228C9281A6E6C82C16F77F3139B6E37CCC35EABC37416FD3F37E5FA1236AB4958E0B7A2F363725911E945778F768CD9F35241180162A841A3B45A4CD2B3A3E013E90B525D7B338C1E25C1D2776D9F6900DD0777C330A4E4B5A90071AFF850A409A361BAA2B0995C41F37AB03DB8AE24312CB6864EE69786C29CDE9E20359571349638C6DC11D574348B037B7B50821BD3CC38607B529400B06F2341B68CAD3E4A62679A5FF6B019B77C64CD15FF830BD5BB841F1FAB141F7609AA9540C2B33D0BA3730B089D7680F06D468C716A8078B36E42A150E775E88BB00FE98811D8A87EC11963F161C76229B42893E81A902F2A755E4266118A7BE4B667C46C56A638A23A3C89A57B1673F92405AD7275E8CB0FFD91B78C2C40EB93DAC278611D08AC11A3278A22D1D555D7B0562226AACD11464D4E67FEBC3A6BFBC41D775AF11383563F963C9634C979B9DB81100052712CA65157C6C308B55062CBAC0BAA7155E263AA6A51B7153C8B6803940871309E246325C99A81241ECF32133767308FBA4B6497729893F2345CA59827F36ACA3BDE2C834949D27535BB20AAD87B91189EA7C67372D521346E32CCC4A417657CC4178C09E5ABC66483B84EBC9C2141C5318F551C5132515707F858460EA953A1F4733A782BC89E3BD44B4B282E10553B4B73366C42464994D435EFF2363D71C50B7D6CC45BA52461ACB59F9677C939D9C7BC7AF992ED03262787BB4CCBCB6A413AF13E18CCA3754264353BAB3CE1A2AC0415508B47B25BA7BBC9EF335827A3099151F03B39996F1CC9FE603CBF9A43FB10726682BE77767A1323AA9A07250C29310A9C4B5AB08A700ABB0E7CF572C81E0A4C28FB76D8B0AC8E3B35D9BC4089930671446548F65BFBB6CA2A7EB9B3CB61643B0A1388804D5C51A707033A6ECB8808B1DCE18B55860BFA1929CB84874E8617A465512691B4F4EB7755B730D3837C8729164C741C14FB7805C936C70FA051BA607B8EB0B0936C3C2701C6B2C7F2542B31C5A53D59CC9F7840311EA3F2E96939557754E665C968926017691C48A548FAA14AB0196D947C415F912E5AC316F81082BE2BD30735289DA87CBB86E71C07162F22952B6406BAC6A528151E74B18163A3D911AAE52057A13F85158C489D3E90BFADCBD48AA4136483DF069C0A6C6C72499B07F21077BD1B4015B50F05B5E2B98AE8FD24B27509379E87C5F308704215DC17707689913A68BBAB76C22A85CA540E8B0267C857AF49BB7242E89D63B697A0332FC588FCA1B987741C05A98A7669DE7B12348472F7EA70BEB68A008C8234B10B18C6A8067607CF451A47430BE3CC01FD718A2C49962E9F90D9C02644A7A2E96731AEB56601D0651638649E4FC299292504743214191312F11B060D56510AA00588B8709AB98741CC5D1126AC0C64B76487716C216BFDBC13A6A4D36D6B55206519618CDB4B2C55CDA81B35558F3D45C3818193B3BC52475521F704DD9947E90280C1FA01C894BBD81EA19B87A15834544CF51232C0A61C5E40684054D84C37602ECBB219C63F7E158137768EDA0650ED266221745FFE5C15F5C3B948915521015E336A724C76182D835BEB22503EA0904C73567FB08E03307880700DE150E0D013D8C1A8E83270035F9249F220043B836C53B6390832C6EDBA52CA131307412248B80FA2162ED54B4EE23853143817F72C6D3A6A1C89AB236085BD9FC9118E6A7A96178D28C13E688E0F9FA82901BA3DF12C3BFD734BB6A5066BADAA40B8375C67CBBE0BBA80A6F16C841CFE33FDE7973C0CBE5E77DE4F995B49155FE393C10008392B890A5D0EBBF3217FB4CD2F002DA3B0CEE11DE3AFFEED891E8DDFCA23A30", "3318D3B70DF3E6D2BBA0972E36E9D5F5FE8902E9D581186C930E091FC038E6E0C13465432B3F99DE5AF86BFC8DCF748AA094C713A72868571C4BFE10D9D3774A7829135C3F5A17CFF577A4EDDB60DEC5B0359CC403C479795B931CFB8B6EFFC323D0B0F4CC503C59225E60A55D4C8AA77DAA69801F26DFE1A5CB003B9206F6D68FF45B1D7E962315E8A9E2078104543289B9D06BA666444C2603F730D327C1AA2099C0356CE81A499FF3B5894363C8FAAE3EAA60A9C0A2BD052877082915E21095E365387E05A6F0DD41B72A4C77D795143156B76F1DA5B779548101686009FBD95EF9DABD470169400393D9B00FBDBD8F37F2DB323A109AF2476604D3CC03F71247ED16CBC6F202E38AA58934A5724E0D0497981C30EE7E340A629884D1808041611FA33548C900EACFA04687F40A7FFD02789DC480CE92B5B8F1F3D6F25A2E5F05F647EC5E718CB7A27EDA0F7B95374238D5D03FEEBA7560C974A5C9C3D04E92D63E3BA2D04A56330ABC5820C6FDDCCF11F21726590984B6E24A95CCBB1C7B577DD12DF391A4900A21232583ABD01630790152ADDD8B50871945D5D36909904D8F669C5788A0C11D9815FB68E6DB7A0D81ADF3BFBF096F926B1C8789280641A2EE9C5F4406E4999C2AED0377DB8FD23FE0D49DF214311988F75E9BF14ECD024615C436908802AB25EAC6D648C94DE53199715D524646A010D7B3AE115DABEE4CEFB2F57917072B40383E00442E4AF27D97048A2749122E0654006CAE385BBD16F8211648AD963837941BB071128EC838C76F80124EE3713773B480E2BCA9459AB4AF3C805706066EC0F69EBF0EA156E6B698B9187001CA3E5A6F55BB25D0FC4A4C14B84F205C4E2A4282F17DC1834604FBB6C7752F878FBC9A5C0ECF496EF782A0844354F5C87331A643A0163AA4153A48AA58B5170A20C9F315421A39C53D91BA0A717AB14589CBC067DFFF9E4FFA8765756CED0D3641AC91A82D2F3C1096E63C0DF97B01F53642D7DC11D9E62B0065AD4BE2207E43CC0E60F78EF0847ED548369AC1730DE5AD31C1E9A3741290CE633C878F8C9F3F7DCF3DE69D0EB98E3F", "DCBE144556BF24ABF89F6DAC5F5DA70DD74B07C60E4E6DBD80509AC80F603783"), + ]; + for (dk, c, k) in cases { + let dk: [u8; 1632] = unhex(dk).try_into().unwrap(); + let c: [u8; 768] = unhex(c).try_into().unwrap(); + let ss = mlkem512_decaps(&c, &dk); + assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); + } +} + +#[test] +fn kat_mlkem512_rejects_malformed_ek() { + let mut ek: [u8; 800] = unhex("28266A088B3482439BCA01AFB7CA5C6136A979B5159985A9484B36B679A5F7B9819EB63577891F7BB9CB98413CCC434ADC79A16D6AB3076569CE6291C59B5D64612A7FB0C15013200BC8BEBB03A570174B5E4363AED86EB02A220D281FB5457F0A549FC5051D49A6B2015259A2C3084F405E1769952260675586A584904059275A265234EF3ABF88C171A80898FC783358BBC9803C8789027D917C9EBACBC568CC18DE84C85454B94249586C0C6E2B8A16FA789C51212DD1728EE9B8C6C40528BF93826FA82368419623032AF27B5694305816811D3CA85805100E9C1A9621E5089E54CB47F5A8FEA0B49EF81C6B5187F48924C7947D6B61697A4A8A18452EF803336AD4BE503275BCACC03C181405F7B1DC9B47FB169EB37BBE27E29C763A4E52B9A42520388CF09B8EDBCDF41CCF6537190E6156C37CC1AAC63C0F90CE78D0B9B190C548D71B6F26CC8F585EA14004B5B30AAA100B2ADC1263828833B24E46163B41446F98C882092A39941867B80632E2097674A793935227DB0B8577E03A69C50A514C7473C892E3FBA7C4316BDABC952A70644176687D4191323BAD93D85A3CA250868C0747E6C44F6126C874AFBEC0BDD4503CB2C59A69816E7D4109941467579A1FFE6A4F50FA379051729DAB6E2F61432F15BE67D667C7CC1054742B2B953078A5CF88D9133087309D88C61DA240D99C59137329907B47865321ECD5564E987333B4CB607B0AFCA86769DC95B2F921357213FCB80C3B152918E9BAB2228C0A1B77897AC68CE55088165F87F397DA9790873B62C5383C0CCC370F0267CBE195651CCF336182C22AC3924B76C9E779B7A271D166B6D24B84242B7E73CC723F764039F6C851744034C3304DB0C091A5764FDC9D593556FF734B82A87CCBC38CA99564D988BBD2D1BF071BB160722D365104FB27610651A8ED817F2742A6B5A1273A61ACAF4460B0AB1456A9922351400A1C7D95D856D6E3370622C9C4164BC6B401435624A98B95CAEB274F34CE92038D785068CDD8CF44C38D84ACB2C466A2756C870EE78C26E738CC451002304EB8C90AB24B6463EB124D779F937A2E3692611D2E34D57B36CC4B2CD3B31FF485C6684D408B972E0D5CA7D2224AAE4E").try_into().unwrap(); + ek[0] = 0xFF; + ek[1] |= 0x0F; + let m = [0u8; 32]; + assert!( + matches!(mlkem512_encaps(&ek, &m), Err(Error::InvalidEncapsulationKey)), + "비정규 ek 미거부" + ); +} + +#[test] +fn kat_mlkem768_keygen() { + // (d, z, ek, dk) + let cases: &[(&str, &str, &str, &str)] = &[ + ("E582B7D75E6C80B05AE392A1FC9F7153B12390FD99930368CC67A768BAEBC8A0", "1CDACB8740C0B87C4A379575F187B367CBFA3B300BF591B109F79816E9CBE8F0", "28C793778741B80B02B4339F2AA4347255B099F17264E1B8CC0A2C7C2A1A79F7997B907FD0496C6E6C8AD7714F5F339D75F11F625591A869BE1175AE47F05FD4313468232BA6957D7807B824F445AC99A0D568AB1AD54DCA8249D1482E61275F52248C77F61A4248753188CD1794CD0A465EC0DC4B025985C461B74E76286E4C37E77405695CC9FD0654374B427A20343AEC0FF1A187768273BFC4905472A1DA387F14559D6CE87313F6A5B6138434539F9A13684055B177E543F8B40F432ABD7CC49989A50A9084C660913F45A8593B17499BC4CF936C2BC1851421CB986808A0EF30AFE97AAB5B8B8EB3F0B3506A95B91563A0E57DB7231044987EF141BDAB3537C316AD16F17805A81F29329879A94E96157E4B7447F7D59603B21BD896CC47B7CD4E232322EB9C5D2215696BCFFCA3A04EFCC4C5D9CC39AC9A6E8700D38C244B0169E7FA1FE81B4B10365E74E6A1F7F756D11ACDC84043F81006D62995376C22535958FEB53F78117EE0F61C4C862640D06DC57A2B8BE62A41A642AF3BC63F6BAC98BBBBFF70570F37B8F8D9572F2735657A6C98F96CAF57A849868720B2640B8BB2732237A1F984C18872D10289CE43C952C9257E06529AEB76AFD127B17596FD25C5216C9CABD9B18EFC50E87BBB04568BB7D5C4E9288C006483AF5912E19108573700BD10CD77224B80659EA75AA74270B33AC4008B738BFEE271E78658C8742FF13C96AD0781A03C7576CA26DD58B52980BA58C0505E446AFA140CDCEA0490DB1F9B18815D4314B2459CACC562441C91F4084E5426C88E632CF7482E79907911D06473260835D7B85E7856A829AEA0381707B939CE86882CC09C4448C6AE94A9C303107C5667EEFB8DF7763CC21189A3C590C40AA51F491503A7935EC08F4FC300CBE607ED8C9100C29FBF45584B13C8D780069337AEC76C36CEB70373E2AB6E7B934B466F53FB32EAF040055496B8540E23A2A277E534468608D5EC0F8D38CEA5BBB806C1BF4F164F6AC826FE733F95461E29DCC11200C0AADA1B8332023EAB329718CE25CC0A09555903F3578BBC863B1752CA94365DA556DF54C3B7E05CBB7115FBC1B6C57A172C31B9906560C8FB54F3C563A2256CC073243B8179B4A28D60E086CF51082EE429272996F0AABE03BA0EAFD3C8E7D954BD0933E2F60ED0C32CEDE7B820A28E48F3CA3C40913CCCAE2337ABFC59843F08C9863325D65A4E9E15C1F46172B118B2B5EB0F1D5158A00134F27B085488C3A0621FE4E5678698250FB74EE5152E3E35A66544A05D279EA99131FBC15165060B90F88EEB7B20892A4DE4CB1683495BD7DA037966B47CC040F1764C5DEB06B5499D4267391CEBBB47F734D8539E39528436A1858182854BF20B1F93279AFB706464C65CCC5AE099B37CC03556C26ABF4C3F8B9BA3A936707211A49A59B268F5284F7970C77612719450377417428C4BA47C9CA115CF95304C4759C5D8859B44985C06A6C924689237BA320D610960D61C53E85431789E67A40113F167FF93429C264F6CABC95448C903437D39A6577BE0CF0012852AA476351A9046A110A1A625A3D74C910B78BCE9CFCA735E4F91B8A4C57DBE489E849446098AACF73070AEE638FCC8896473D3C159D3AFB4B687B40DFBF371A9C2644B605187B71A14BC4C8678FE8247", "3808B98D9A093C7853B0B814D1CA5F392677D3D0A38F81C852F95B9A69B374A24588C0ADD5B510BE567C5A24688EC91ED0F28BB4C86978C09793795C36B94E5DD8498EB4353FF40EA87B17E921B5B4CBA08E5B7BE5A9C8BC69AC5AC3075DD947D04B8695097AC39790A0C8ABE11200C9F136ED7B0C10077E36111C1F139D9BD27142993F5883925C4918413CB8043962A28567397A1C7E003BAC30644055155CE9464E623FC5E2160CD1143FD2C90C03A08D0B333FBB40A308CACD81618674B406809790B2538D30431F064F65EBBA7C41B2A53146A3ABCA66754CAE591534045C1D640D6308472EBCA660A8A7D372470D3869115B8860B577311980B2C66B8352229ABC17B0C31F58E5A5EBDBCA6E3B157D9CA79EE890778A4CF28429C2A82C80DC9C524B0864E385E355BD4E65732D395E464070F61828E57C3E1BE2A50052C4FF1951F5D0C89720987D8941F085871113A91F518FC79B7189056990B2447BF54FC170C932E0A4E0F7A262542F56D1B49EEBC392106D93C77DE9186DFBB824E73B2F7FC796B312875B43414877AFB356214492C19C748DD381BC7A9237BF097CC606A03119A240A5536AC7844F6A79D0E2B821B68D96542450179BCCA231F2DA68C5EB6D118B99A1B66EAA566C78A9009008B0D66155BA4839F8E518C650177DEC170EB09E8BF6A89905320590C642B801CF5B5151C2AF3E7271CB9961C65A5BD17479429A31BE9081F2767D94816C16D1B04772012382B689AB2D3FB9BDC66547BFEB23052600BB369771494D9C914EC93A066ABC9611940B947310DA04197312425D736A1933B785C95DC430791E42CD691C79BA63BE06EC80765C9C07053EAC706697F21720C672B9803DF9935532197B0485BBE42B0DD16561AC0605E9DA73C189C9E29ABA3AECA19C21621B209326418543C44B88843EB3720A2CEC3DB52C59D6507EE5CB03C268B31FC4BF115695EB0B8AD3F11FEDEA1A5D0724A5D5284D0C10E9844D0D32BB5735448421BC5317650BEAC4829B234B787339812F2316ACE8C22C42D6346214934FC0B49ECCBDF8DA19AEE64DE9628C4F7AB3E512A399B0C227B677AC4A69891A6874F6641D2CC95460B751E17E434C924B2947D806856665696E3CC4DB9553B81606C31C3800B48756A073BF685EEA20899C176769E902DB971827D153B9C33516E959C6B469841946C0D921A371A5DE9740C6AB9CA272B5850BA8753CA023A460EBF7BD573C0745F40B90F105EE17C19B6832E019B80F8858BB515F7C709E68F29C1375B22567AFE7B528F4431A94B553DF825CDEB84B4EA9296EAB9AD66271EEC5AEF6F79509A20A182279FDF92F87FB6E7509968D22CA750B5056974841B9654E5716BDC33A2C6A116A4117FA757A1D22710668412B8878E134B70FE32158A2317FBC62F01371296AA42E33C903E0C439F19684B11E2F911F7FB79860C8800F9CA146EABE29DB07237AABB503A9BE2AA7263A0626C162A27537775792B2E7B0FD347929934AD8F521D4159059F611312AB903879490059BE8B38920E2CB4A256B8B35783E909346D13E9888BEB9350369F7C1A8501331110C651621A616B365A026D1CC47DF440C9E650DD0C0BFC8295439114528C793778741B80B02B4339F2AA4347255B099F17264E1B8CC0A2C7C2A1A79F7997B907FD0496C6E6C8AD7714F5F339D75F11F625591A869BE1175AE47F05FD4313468232BA6957D7807B824F445AC99A0D568AB1AD54DCA8249D1482E61275F52248C77F61A4248753188CD1794CD0A465EC0DC4B025985C461B74E76286E4C37E77405695CC9FD0654374B427A20343AEC0FF1A187768273BFC4905472A1DA387F14559D6CE87313F6A5B6138434539F9A13684055B177E543F8B40F432ABD7CC49989A50A9084C660913F45A8593B17499BC4CF936C2BC1851421CB986808A0EF30AFE97AAB5B8B8EB3F0B3506A95B91563A0E57DB7231044987EF141BDAB3537C316AD16F17805A81F29329879A94E96157E4B7447F7D59603B21BD896CC47B7CD4E232322EB9C5D2215696BCFFCA3A04EFCC4C5D9CC39AC9A6E8700D38C244B0169E7FA1FE81B4B10365E74E6A1F7F756D11ACDC84043F81006D62995376C22535958FEB53F78117EE0F61C4C862640D06DC57A2B8BE62A41A642AF3BC63F6BAC98BBBBFF70570F37B8F8D9572F2735657A6C98F96CAF57A849868720B2640B8BB2732237A1F984C18872D10289CE43C952C9257E06529AEB76AFD127B17596FD25C5216C9CABD9B18EFC50E87BBB04568BB7D5C4E9288C006483AF5912E19108573700BD10CD77224B80659EA75AA74270B33AC4008B738BFEE271E78658C8742FF13C96AD0781A03C7576CA26DD58B52980BA58C0505E446AFA140CDCEA0490DB1F9B18815D4314B2459CACC562441C91F4084E5426C88E632CF7482E79907911D06473260835D7B85E7856A829AEA0381707B939CE86882CC09C4448C6AE94A9C303107C5667EEFB8DF7763CC21189A3C590C40AA51F491503A7935EC08F4FC300CBE607ED8C9100C29FBF45584B13C8D780069337AEC76C36CEB70373E2AB6E7B934B466F53FB32EAF040055496B8540E23A2A277E534468608D5EC0F8D38CEA5BBB806C1BF4F164F6AC826FE733F95461E29DCC11200C0AADA1B8332023EAB329718CE25CC0A09555903F3578BBC863B1752CA94365DA556DF54C3B7E05CBB7115FBC1B6C57A172C31B9906560C8FB54F3C563A2256CC073243B8179B4A28D60E086CF51082EE429272996F0AABE03BA0EAFD3C8E7D954BD0933E2F60ED0C32CEDE7B820A28E48F3CA3C40913CCCAE2337ABFC59843F08C9863325D65A4E9E15C1F46172B118B2B5EB0F1D5158A00134F27B085488C3A0621FE4E5678698250FB74EE5152E3E35A66544A05D279EA99131FBC15165060B90F88EEB7B20892A4DE4CB1683495BD7DA037966B47CC040F1764C5DEB06B5499D4267391CEBBB47F734D8539E39528436A1858182854BF20B1F93279AFB706464C65CCC5AE099B37CC03556C26ABF4C3F8B9BA3A936707211A49A59B268F5284F7970C77612719450377417428C4BA47C9CA115CF95304C4759C5D8859B44985C06A6C924689237BA320D610960D61C53E85431789E67A40113F167FF93429C264F6CABC95448C903437D39A6577BE0CF0012852AA476351A9046A110A1A625A3D74C910B78BCE9CFCA735E4F91B8A4C57DBE489E849446098AACF73070AEE638FCC8896473D3C159D3AFB4B687B40DFBF371A9C2644B605187B71A14BC4C8678FE824781E66EF5A7A221619F6A64039CC369843E10DF5C859F6959CC3FD8E5272330FD1CDACB8740C0B87C4A379575F187B367CBFA3B300BF591B109F79816E9CBE8F0"), + ("3E5848DB624613F7AC144457CC1375F006FA8CB953E767DC9E7428D00F5DAD8B", "012DD6C2F0918B9EB6182474EB86D848F65974759D59CE151A396DEEE4CA10D1", "43166BBD4C97F4B57D4DBA6FE40A48498B001A502C28385B74306BBA43C670ACB191FB031C220CA3238AE1CBC961E66898A4C19C0A4BFD076863651FB91A31C75A33DC18B53EC6C56679159783A563C4504CA10D1405C261F8C7FFA87280242EC8BCC3077ABDF371CF55C140D0A131E6529B40788F1943964136000CD7319E05553EA58E5898384822A62B8AB9BD03609F1566F53435C472520D957703090794179EBC73744525A8F002A14270CBD1D4B6E2538353765D49F5651C1C34CC228C4E753C5210BFD00CB378D6A4ECE80168D54A4C62740A0C4A292A42E1621F2C499E4FB94E37912C9CEA7472C65B27F65036E85D5512C38D3582DF290FA819BDF0E37EBA7C2607E6A94A2323A0FB88A6808B2F444E33999F1A10830F715960D563D0A43AAD7AC7F0B4AC4A9A3236286D5249C83DC5C4666354D0EA5FE06530E4081370264566E47ACCBC22380414FF530C394007FE6CA79DB0AC53FB2B06719908D15DDE432F03C84463EC5009D81E9882030360A0FBC34F156391B6E3AC92C795FAA2199BB4A9A33A994A16AEDB6558CE901C4B60295DFA39CDD5B97C9B1E9C3199C825BD64B819D8346B1EAA6A5F5A4BB90A7F32253D7F7BA2778BB796BC707BA53CBA7A36E3682D54C3003F572D2779AD543247C6D78EDB63739E17029594BAC09620651227C8F072C7F51672385F62E06B2C80A4160BC73C8691C5314914F4537266C0D2EC74365B8CEB99C71F076BA772493CB534192A70BD576B1BE0A42D2B87DB45A423309704BA17CBD7CC610095FAB3CA61281AB2949A0D2B548A1110852073A98C4C612BB095D28049201C4F08A6083BBB60A65CBEB81B6D2A40E0D7AB0628B5EE251143812AE9B6524174C420A82D486350954AA1E6442007239FC7ECC969E18507FB55C78CCCA6C18842D745BCD9C425DABA9033B8161374EB63244F2570D228392174545FAA4664A886118A6948451DCDF489A1BAAD5E2B605B4756F3124F80B546FCC61AD30660EC342228ABA6CFCC06E61884F96C7F675711C84451D3902C206B6F1B928A2701C021E42FCB49C28C19414ECB9288C4A51E957B4D742D0E5307D731AE0EEA9D7BC9B48BCB332EFBCC3894AF4986628FF77420B6B8ED75C4F54A063B49265C38CF17B95DE45A64CF30B5EBA5643603B28B2C4948E2A2F1E8C440C54341491B20D378DE5481CDF8310074A69D79767EE1135FD8AAE3934449B406AE447BB33148A08C1C0EE4A615A44C42943CE3D56ED50B77BB7C3D4448C106363472CA0F11F526349C673FC2C8158ACB21BC3BBA9CC39891672FFCB4B3E165E6C4A1D156AD7F7085BBC49AAC5B5AA2682F165560C4132C49E0B12FA4BBF4B40E630B375086B3EFF66B07B0CFBD207D7F1AB67EC580ADA48F36308E1566CF2FF5BD1F84B9161A73014290C384BF9E0B0AA653CE4F8C6D05CB087EF846A1478468E06F81D0B5406B9AC937AFC2795AB4472E0BA964DF4259E849030108D083BA6ADFFC63E4B77126C3BFFE389401E15EC6A88BD0B5B32BD06AEDF80792636F167589C346A6E651009557CA079C593270250D9ACB3A1BB06AF03C65F88D71EA932EB79553D60FD4D34499F23FA3498BBC3A89BE3369661CC2D292221A9B8CF379DF6702808576A37975C9B593EAB54AD4B8936DA5217C6B843AD33D", "0E62A20CC7B3062B2EE82CAAC1005F0E9655B03BAAA905AA59283126149B60969B2720563F1BAF1A93908CF41DB362589E9694C3E6734620AF30FCAF3186B5B42B5376390BEF3B4CD1833BADF19DCCB0334A68B1C6541874659FF14CC49357C2B5EAC09DAAA77CACA604242F85039A89FB456DE488FD06137E7C527E76A18B5CA066AC95A93094B300185B001F44CC2505813534A949F335027B6C12DE7C580451A53C499DE6263117A17B5BA64471A97C0579A4FE3B20F5D24569C85D214155B4E9818D9466184C8E2FB843DF396B2AC18FCA8B61B8387A2B3458E9D0AB9E2B751BE34DD5C70132595835DA55D454C301F13B8BA542EBB6C1E86B2F621312E4B80A54E106A7E5743257CB52603A68B4A9F5D195B1283150E48F9766A1BD248438C92084B451EABA445AFC6BB03805B963537AC8255CBA18D4A9832B2752EBD9B5F7B8AA52F5207B82945C473278DC5D7850C0921423587401D9210053C9C9844460917553A60A099449A7B0D360E00044FD9932140CC1D1D444F14010F8F35869A03728951E28B7C203C5CE3F7AA61205D08A15B181C0445AB27E3F889C0E72B050476CFE29420085A09D203B1C73ADB6B59074893BC3A7533758467D6046E3633044C8A1E9BB3824B72EDC7A8391721E01E4A2A214BB71008B1EFA343B064901831B20698D3958991DD9C22058A0AD4A49657B929AD926EE1B608CB29DB3174B27CB09F7A75CE617A21AB971A28BB6445267EBB838DD3B3BB82759C26A021D0382992A6CAD218ABC44A8D9667A07F5040ADCC1423CAE1363729BE457FD94C025E1B48F071140DA992E1A557A7497907CC729E33E773221F79096ED8BB830373CFBE84F5466A02D1302B1C09215983EC0229B61302F77AB4E698016A600CAB1F9729C320F65EC1537733F3976C4AE75723903BD83437B0A519A53E56B5C49C5C15980DC5341BB45305229536D324F513615A459C5D69019DACA536C42427332279BCB8A74503F76A5AB90EC7476105BA69C518921C62AE043B5E91DC958949D57B0BEF7903CF5CFC5A06C0654A60F48388DA4723B5B3A320184FE6755A881B27C2ABE23A0B63338B1EF778DB47B2CDEE043F0F5390C84BAE4D2B3ADB2B67558B567925DA6E1B33CCB3B87126293BAA2A8B18479D65C4BDA14FFF45E64223EA6E455506164F3F453651439D275862937257F666CDC564FFA255ABBB3B6FC8493FEAB7E76E2A17E849044E62A1F980A57BA5B9397271E2B5D5B5063EDAB2098FC561D10C731AA9D7AC13FCBC793EE659D6994BB00D53323232C6A192DCBE39F6193676C69C256E9050DA78EA442078374A27BE054AA30462374B992C8A32F0440E2DCAA70927D06C97EBF4C04C9557A622203187AB514C0284ED45BDFF560F3B3A15A6A6F0A6B098DF3C9BC083348138710D8B5D97A2EBE2A90DE907658F7B8B67865A4BAA111898BDCB5521F209C5801861D394BEDF51B0302888D39A73BCB07E3E99C2174BD080B1EB4086199B4859FD233D9BA3B34A04CCC67BF59896D98675AFC4CB1656005C57BA8CA5A1B085B686A1C27BE292FD3FC0D7594522C9232DE4B09541C3C1C11A2ABE1830AB3BD80B313369407F66C970FF61586A040F7DA4743166BBD4C97F4B57D4DBA6FE40A48498B001A502C28385B74306BBA43C670ACB191FB031C220CA3238AE1CBC961E66898A4C19C0A4BFD076863651FB91A31C75A33DC18B53EC6C56679159783A563C4504CA10D1405C261F8C7FFA87280242EC8BCC3077ABDF371CF55C140D0A131E6529B40788F1943964136000CD7319E05553EA58E5898384822A62B8AB9BD03609F1566F53435C472520D957703090794179EBC73744525A8F002A14270CBD1D4B6E2538353765D49F5651C1C34CC228C4E753C5210BFD00CB378D6A4ECE80168D54A4C62740A0C4A292A42E1621F2C499E4FB94E37912C9CEA7472C65B27F65036E85D5512C38D3582DF290FA819BDF0E37EBA7C2607E6A94A2323A0FB88A6808B2F444E33999F1A10830F715960D563D0A43AAD7AC7F0B4AC4A9A3236286D5249C83DC5C4666354D0EA5FE06530E4081370264566E47ACCBC22380414FF530C394007FE6CA79DB0AC53FB2B06719908D15DDE432F03C84463EC5009D81E9882030360A0FBC34F156391B6E3AC92C795FAA2199BB4A9A33A994A16AEDB6558CE901C4B60295DFA39CDD5B97C9B1E9C3199C825BD64B819D8346B1EAA6A5F5A4BB90A7F32253D7F7BA2778BB796BC707BA53CBA7A36E3682D54C3003F572D2779AD543247C6D78EDB63739E17029594BAC09620651227C8F072C7F51672385F62E06B2C80A4160BC73C8691C5314914F4537266C0D2EC74365B8CEB99C71F076BA772493CB534192A70BD576B1BE0A42D2B87DB45A423309704BA17CBD7CC610095FAB3CA61281AB2949A0D2B548A1110852073A98C4C612BB095D28049201C4F08A6083BBB60A65CBEB81B6D2A40E0D7AB0628B5EE251143812AE9B6524174C420A82D486350954AA1E6442007239FC7ECC969E18507FB55C78CCCA6C18842D745BCD9C425DABA9033B8161374EB63244F2570D228392174545FAA4664A886118A6948451DCDF489A1BAAD5E2B605B4756F3124F80B546FCC61AD30660EC342228ABA6CFCC06E61884F96C7F675711C84451D3902C206B6F1B928A2701C021E42FCB49C28C19414ECB9288C4A51E957B4D742D0E5307D731AE0EEA9D7BC9B48BCB332EFBCC3894AF4986628FF77420B6B8ED75C4F54A063B49265C38CF17B95DE45A64CF30B5EBA5643603B28B2C4948E2A2F1E8C440C54341491B20D378DE5481CDF8310074A69D79767EE1135FD8AAE3934449B406AE447BB33148A08C1C0EE4A615A44C42943CE3D56ED50B77BB7C3D4448C106363472CA0F11F526349C673FC2C8158ACB21BC3BBA9CC39891672FFCB4B3E165E6C4A1D156AD7F7085BBC49AAC5B5AA2682F165560C4132C49E0B12FA4BBF4B40E630B375086B3EFF66B07B0CFBD207D7F1AB67EC580ADA48F36308E1566CF2FF5BD1F84B9161A73014290C384BF9E0B0AA653CE4F8C6D05CB087EF846A1478468E06F81D0B5406B9AC937AFC2795AB4472E0BA964DF4259E849030108D083BA6ADFFC63E4B77126C3BFFE389401E15EC6A88BD0B5B32BD06AEDF80792636F167589C346A6E651009557CA079C593270250D9ACB3A1BB06AF03C65F88D71EA932EB79553D60FD4D34499F23FA3498BBC3A89BE3369661CC2D292221A9B8CF379DF6702808576A37975C9B593EAB54AD4B8936DA5217C6B843AD33D423290E0CCB74DA11B95294FEF12EC690815FF103075053B89C5021AEFF4CA0C012DD6C2F0918B9EB6182474EB86D848F65974759D59CE151A396DEEE4CA10D1"), + ("882FDEA55C6B497A6F2A1321FC82160C630D9A1F2E35BBE0D3332E3A0CFBC8C9", "3A21F601923C559179A3AA5839A148487379EAAD934BF27F3071E1B1FDC21D1A", "3A7714CDE9B06A8990EAC99A89A12EB606017F841D4BE2100AEB750C3892EF544418216EA437AFFE88059A5A42694089FE7020828160683C0E7EFA4519591A4DAAC30456A5EC2AC39CA0CB3F3920237097E6E5157E1881A09A03AEBA5B9925CC81F20A66D86EC52245E828655032067E8BB813F43BE6DA09FCFA2FC2452EED5CA4DBA87DC306A573911205B53B91187D44214D12CA1147B4174956C2BBF1863D85567B8BA14F24B2B6949F6639288DE34C117391A19B678B3406FF784AB87B66031371BD0073DB89A4ABF597D40213BCD4A93AE834F14A52D527C7486C9DDBF62148036AA55A06ED156A0C0B9366D15B582338BEDB8466317FEC3109139C8CE2C1079A070597C6B712C1B16ACA049F3C8999A95E1DD20163226B96C5724EF6908A9C5DB43637708471E02C31E1B40A226B01DABB057C328717A4BB41F9A32230079A49BEB2E892F9DA40FF947CCA4410497701795C62A113B30AC0A7D143A8CA8C3C0189407F95AB1B05719C1ACCF500676B3596141A409B6C0E952A5F1D456A70492B83C4515DD99B763A24B4C1545ABC8715E61857A89CF6D760D00C7B47D407C1C230C6212215CC805C76979671AF6D3168C73CCA4066233BF66FC1D485E225757783CC0457059CE05B9A2409B7B25415121BB2823B032BB40AD8318A86682A1C9FA6F571A08054939144F1AC2F5D299C97C730F742B75C4521851B8D9FE6AD43121C74AA248B751998A2AD9FB747BCF18AEDB4B4B0E718E979B7F7580C14BA62B42B8997F17E6812BC46AB6799F7519A93BFF43170F2C01B31D96459B7A8956B9DA9B1C2CC7C7E3C849FF326003BA17440539E01498CBA96B0922468DE2345857C3405597AD822004A2B2FBD8723E9F59CFBA30732E3BF68131283B87D05620440A2A7FC3582C20B68D0A8763B5276C12957AFA766C4D0A524800A1D954934FA0CDDD684D85C997BF2C54D8CBCD924B19E731F80B35138752A8E91C0876700F4AC3247BA6DDEBC1657941DFFE96D1803C81B75A6D07CB6CD4C14B8C904020CBB6BDA1D019CC49EC90A12581F944263DFE06DB5E16C94EBA98276B9AC3B13B8A9083AD3190195583EA7A76051A27F2121F46318580C37E50B0429480F7EAC312294CD3C71BDBBB0557C41AFC363A6411805964221D26B0EB7B798CE791EF121BBBAC1BDBB88BD57DCA9114564F4B388B1A2C32912A8BB5C1F21763294498E2B0620F3C085E7C4099D6A3E4FAB7FA12A73E39167BCE99A6F69A293A37EF238CE2ED7A1804A596CA33897F4CB772C4083A55438490D3E1CC1C5E91D9B78A5E67BBE3C7C3B1293CEB0BA7B7E48CB2BCBB30DD70296A17BEF964F265C5A41152964574C0505975DDC10BA9850C6199C83E157F2A74D8FE7496AD2BA6D83586C7624B87C3E00B84B65620DB69643AD9046105B5A6CD8065716C398D0BCE0E1577E3BBE192651692B79DFB747F7621E2C9A87BF2B602105CD893B2BEA50A10D952CC8DBBB1499B9771A3999FB91E0C7438AB641D79A568A375DF1D07E28E2C9C450A72F8C4AA5D71B82A09E1196AA3FCA8E7C684A4DDB635E775FC03604711139F332AE97EA1BA2813B1690BF9CF70F506CBA90641963D5CFAD3698DA30C866C95C66B4846F785CCBF373F4621855FD4815F0920AC4F940CA25DD130DCFA223E9B4", "7A804DFB96B63BA97AF4E35FE6117A82D68509A81C70CB54C68A33280C2640599E918A2074627CC1AC8966A74E08822DC5731E79D82B8DCA0CBBE6C8D1EA6973A970DBB44D5FBA31E14593E9ACBC77F02ACAB7A58D656854F4031DD209A5B33A167943072BBD660B815FD13AAD82A045269A161A86FC5BB72C4CAA704C105C37CF04CC4C8A1643B01C37AF0C8D4D39CA85025665A1B66E907720288BB955A7BBF639123376A3E27D919499BE7A42E1FB09B9AB27029AC076DB2F25147A2BF38F6C5B287BB424BAE821D79875C8C41FB120A1BB957BBB57236684636C758E909120AD01332C60BA5304133CF7837EAC6D31FB7CF160251DA3C4BD1B18B0FA674F1B5ECA1603FCE92928B821A8A68E6A085E98EB2843E722481A0B81E16E2C4BB0F4B0896DBB2C900C8DD7E500A6C84EABB44D4A670D54203E921344C6C1401516AF0168CD1F0821C0FB260044B6F908A68E58170CCB34C4E39919DC747FFA13813240A43631D865ADFE335936034BDA443773B967C8B9636109CCF919A9B6F233F0C3598A7ACC96721CD5489EBDC29549ACC16F0A64C3B0B9C742CD81C504BF6A9898A38D052A36036436655285BC26A249D190E72337D4A0BF191954A5AC76982945205113977A1D6937824C773234CB2EE7716570628BF1048A04B38E5D9C42C3AAAB1581446C3B96CE96674C119A709C0425E6BCE65477252C628C6988DF6316813B6F4D37C9FF339D22A76E3A8A1376498B63AA221FB329EF270614745E94655C6591747DE740586386A6150D66E8557225B38D3173F5816BA12C395C09712A5B289031981C2BC97C51C39499A144C2976305BE1BB08CD6A305212C2136FB8921F337E6D61E8E8ACAA525B8C9C60F3AA66EF9A56DBB247DDD424D67C95DC6B1428AE63CD2A236CCEC911378BAA1E101961864D858682A762113E88F98F25D1ACC9E2E1907536257C3CA97B81091557CA0C691B90DF6B707D55A94413C5382CA4DE2605740156CE738796530781A2E9C629BB1CA0D82B5024A262437243B29E2586F4C8585777EC82898E4942ECD1325D719B21017451B5908691B35722650A81964720ACB595009B99749852035CBD4B1AD51C178E9C97C604FFEF08E7FF4057D274F1E078F46D04298F0CE82A44C8224C31697C25A687DC845B8A55A050D69C6E6A4226759BD0794C5BF0C33CAE843E2FABF738C7957D61206FBACF8B54472C896174195EA8923E3995DE50C9C430C4FD4F81ABD21731DF968E3617575E90A7B430990055068D3390E376FC62490AA97A27F5441A5A149C07344AAB833E19B093FD9BCBC8A05CE1B6FBFFBC7BC424EFC7255178569E2447C59B851E39A2017390B4F230D6DC927BF83BF2F868E104129C5DB003EEC8F9218357A6553EBB41D6CE558CB3B8FF86A3F125A8BED75C675E13A9F343A4B791E7FD87ED4228556A494B5C0CA685AC4AB663527ECCA5E5279F1277AB51186D0EBC50FE5B4B64590F26A87E9049E29735190579DDD194180B60B78579EAD248895C55422A18E59D01E6166BCD19C08D1917280D0265A370D5A34334FC9B29F931C5C68297937198B06791D838492D73C389A9556FA0640E03537F5C744A379892B362B0B1300678F3A7714CDE9B06A8990EAC99A89A12EB606017F841D4BE2100AEB750C3892EF544418216EA437AFFE88059A5A42694089FE7020828160683C0E7EFA4519591A4DAAC30456A5EC2AC39CA0CB3F3920237097E6E5157E1881A09A03AEBA5B9925CC81F20A66D86EC52245E828655032067E8BB813F43BE6DA09FCFA2FC2452EED5CA4DBA87DC306A573911205B53B91187D44214D12CA1147B4174956C2BBF1863D85567B8BA14F24B2B6949F6639288DE34C117391A19B678B3406FF784AB87B66031371BD0073DB89A4ABF597D40213BCD4A93AE834F14A52D527C7486C9DDBF62148036AA55A06ED156A0C0B9366D15B582338BEDB8466317FEC3109139C8CE2C1079A070597C6B712C1B16ACA049F3C8999A95E1DD20163226B96C5724EF6908A9C5DB43637708471E02C31E1B40A226B01DABB057C328717A4BB41F9A32230079A49BEB2E892F9DA40FF947CCA4410497701795C62A113B30AC0A7D143A8CA8C3C0189407F95AB1B05719C1ACCF500676B3596141A409B6C0E952A5F1D456A70492B83C4515DD99B763A24B4C1545ABC8715E61857A89CF6D760D00C7B47D407C1C230C6212215CC805C76979671AF6D3168C73CCA4066233BF66FC1D485E225757783CC0457059CE05B9A2409B7B25415121BB2823B032BB40AD8318A86682A1C9FA6F571A08054939144F1AC2F5D299C97C730F742B75C4521851B8D9FE6AD43121C74AA248B751998A2AD9FB747BCF18AEDB4B4B0E718E979B7F7580C14BA62B42B8997F17E6812BC46AB6799F7519A93BFF43170F2C01B31D96459B7A8956B9DA9B1C2CC7C7E3C849FF326003BA17440539E01498CBA96B0922468DE2345857C3405597AD822004A2B2FBD8723E9F59CFBA30732E3BF68131283B87D05620440A2A7FC3582C20B68D0A8763B5276C12957AFA766C4D0A524800A1D954934FA0CDDD684D85C997BF2C54D8CBCD924B19E731F80B35138752A8E91C0876700F4AC3247BA6DDEBC1657941DFFE96D1803C81B75A6D07CB6CD4C14B8C904020CBB6BDA1D019CC49EC90A12581F944263DFE06DB5E16C94EBA98276B9AC3B13B8A9083AD3190195583EA7A76051A27F2121F46318580C37E50B0429480F7EAC312294CD3C71BDBBB0557C41AFC363A6411805964221D26B0EB7B798CE791EF121BBBAC1BDBB88BD57DCA9114564F4B388B1A2C32912A8BB5C1F21763294498E2B0620F3C085E7C4099D6A3E4FAB7FA12A73E39167BCE99A6F69A293A37EF238CE2ED7A1804A596CA33897F4CB772C4083A55438490D3E1CC1C5E91D9B78A5E67BBE3C7C3B1293CEB0BA7B7E48CB2BCBB30DD70296A17BEF964F265C5A41152964574C0505975DDC10BA9850C6199C83E157F2A74D8FE7496AD2BA6D83586C7624B87C3E00B84B65620DB69643AD9046105B5A6CD8065716C398D0BCE0E1577E3BBE192651692B79DFB747F7621E2C9A87BF2B602105CD893B2BEA50A10D952CC8DBBB1499B9771A3999FB91E0C7438AB641D79A568A375DF1D07E28E2C9C450A72F8C4AA5D71B82A09E1196AA3FCA8E7C684A4DDB635E775FC03604711139F332AE97EA1BA2813B1690BF9CF70F506CBA90641963D5CFAD3698DA30C866C95C66B4846F785CCBF373F4621855FD4815F0920AC4F940CA25DD130DCFA223E9B45F13EE3FD74B1366AFA7F3FFF4B8F645331DB5526FDD7998A4EEB5C27C884DB83A21F601923C559179A3AA5839A148487379EAAD934BF27F3071E1B1FDC21D1A"), + ("AEE013310D04215B77F073A7E22B77BF0221394A08E96BD07E3C99A9AF45F16B", "7DCB9D6A2C3832E5FE9261E29172375A78CBECDA0EF5484C4B3F6940EA20EDD9", "7105C33BA18D19F691968662854A13ECD19E3D93CC15EABB1F4595BC189CB951A957D310D4469CFEB8892021A58D3C023F815D42411C27B0777AF8C25A898C96C2BB519C5830B295EF882ADCECA22D927EBA65A00C815AC8B03B76B4CF56211A32CCC5D9957C52718E6F7240D4E389F0854D160B3FC7D51A9318C743A5748ED06DBC1051AA885E7141655FD378F99C579280027F1242DE118B69E46E2510A5E420B7D8A39E8AB6BBDA342034B1AA3E394DCF02813877A4AC02A76A03BB74804305A21C9261786463440540740A814AB3681E0FA260FFB055D8E7560BA3286E839F44D04AC9C44A6D29BF09127DD0431BF3E379E031C236D44D79C3A48CA7A42064546BC86ACFF82141E6BC17529374DC11CB5AADCD612E7415053EE266494128E87B1091DBA6591CB86C4486587B095F872A70C53DFC3A0D456A0C8865B45E5404A7FC489A8A09641B9A3B149D7261CC896107F000A7A75C6AAF772D3F9B242509318DCB1CECEC4527C92CECA5648441B99D1C84CF472572AB5575ACA40204C434283C1229570012B47829679FC8A63CE1302E132FABB9687F93095E932FA419B22849A4D6AA090FC69ECD029B6F7489B59A43C720C49CC557D8288680F6B0BC6B87D7462AD128022BFA946685A53E2A8C1F6883567B9C854A012475756FFA2A6B2C6040B443F3A425B35A9A53B6BFB029BC08986FEE5B05D8E11CC006544EA96F5400A8879C9E61E310CBF83675070A4ED62BAE9B29D530AE845B3485569ECBA95672EA1A2A713252198DF411CE387888010C2EAA735605D3B9D889A253338276697283443A4739CA62A64883217CAFE5C1916ACFDD689E272CC51AB5C1988C3029B7795B336F8697114DD856A0573D65395A1289B6E285A99016776B97A76E315505190A7BD331E568987BFB53346419097B83962C9D5DB85F956B0B9E0506846416F3D83D4734063B06ACEED16E839957C5A7BEFC2A4346C0051A49CD199A42F8AA2F55687F9F82AA25BA1AB71CA172078F1D2BC75A3B5FF2557167EC3D10E10813D0625D925F1B2253816486F248A2F5D02A06A8343200003F22094D9603AEB35FF9C6512A9C7D21B8C78B0961DD914A95185572730AFB39179E679F45DB4E5DE5B1C97CB85FCB89BCE8CA958654A3293EC875801F8B40A85C9D1D53025A0B28CECB0AE0D0100B73543351949C259956A859D522BD37E96CACC314E52C7B41A76BCC5C88A677AF9EBBBAA6A573B642962EA821E5A516C44B0A7C0B05F68B02EE078F7FC5C7FB02211F7CA52793437F7A9A32DB0F1040A8A01C1779DB589FF550622ACDFEA78CEEF0513A004CB77141F5CC7D163B5038AA5CAD29854BB43109D53EA3873A5F668125C58AA7BAB3C5A4551FF2990A117056536BA4A499548193CE083D8B22303A6AC6F5442F8231CDDF3B62CCA59BD6F402B612316370A3C857311262AC897B290533229AECA42CDAAF33F2943E496FFA371992D8C80AC3C7FEB46C7748910B4875672A8BA4F3CCE4762894A76846A0CCC13149E02CA12FF1776F99B140731E3E78B23799BE84163B8EE399DE987C3CB4253D15AB07F4678BF03766C0ABBFAB39CCBB8D7064C660A66335F926523A161BC4004D746FB4A764F8FF76676AF904F129457550CCC566089B81BBADC284BA712C521BA3EB8AB0C4", "8C50933FD05144630DBD942FCD3C1BF037C176257A332A440C88615D8A613EC3C219D2194F362A7BC3291D3A50C5911C26DA86AF2209A6F7A7DE997892B0149B3ACB5D4C1E2F318DDE95B1D09537571BCE152435ABD7072A60BEDF7A525AB98B28EA6E67E91FB2BC49BBBC90BA476A57DC16B5EC8663C72F2CE06B2AC91B645400EBD6B38662A3AB157190F7C3149C7638F08161185A01A594D2795D8CC6BF40996D776528D018C4603950AE4AB892D45CE3D909F2A962C29C5A99A05FBC4C3DD4C711BB8679D5614CBC039D5D13A9E23C2C5C087B7D6287906A1CAF5105F6EAACB90264824174349C21D1D940882B70D981BB2116906120B143F07E0C41709D0B03D0C21D2047A350CBC0CAB65455389FEE273A28F916A2D69D7A7B2D7A210F597235BF4994DDA3B419704C787779F07B55A2F11D94CA492B147E667BA7E77B6F208CCAA8D6105BD441F9A47E147B76EA46B9D56309E1C224D2593B5B084EFE6ACDC8F5A0D4CCB83BBB0743FC8266D42E065086E5692E4479548C399A62B4ABB246A85E4C1B6AE218D76BA984B020B48981E29156A87341D251B979E7AF8B1687F4F1AAD95C7CCF4BC7A0FCC4C33968878A6912052F16227C92BB7EAD54574E552F7EA05ED474371A3AC690601397536E8280268D1BCB3A8602F21B24FFA64B60A04A7A127E966361CD742B2BE404C14B6597DC4FBE60957724B7E16888A1260DED6268FC743DAE33A50A4532CD9396C17ABE684197907C6743C7883126C77BE58B25B75AF821C1ECC59BB504C2C9D4C45803725AFA3EEDD63A6D391AD047988A987F2EF3624B20BD795C38FC27CB9489487E223F93D228EBE9A40DB61E9F420841E74A20159770302A7A4692649247CFE2A4CF53B00C32376B33625928C4FFF278AC08BC413C1DBCECBF86C7ABE7CA4D7EF074DFF9750708A2A5199DD8598DF295AA2997AE3BC02B4EF5C957B50122E5751B0C2C844979C6E617E50A6E7E0CAC8CE0891F2133EBE59135799FC9FC6FEEE2614633A53EC194E3C9737EF68C5F208E881358080314A31CACA6C78067AACFFB572F4790C7DED53A6C6C451FAAC150203A51D208048486911A3A991627812B1BD74C834B9C08197B37028C716DFACB6BD6AB9973731E0962EBAACB4AE53F8C40CDE680C430B45DF0CB00D60BBFCDB7CAB190A63B027E9F5373CD3097145581BAE7B902D08265C488B363114AF81A866023F4B665B19868FB3A291B1139CA336DFDB6C28A74B231549790548E0C74CE067B594C47880D344A9A833033919FD505868F14108C9671535488E5F52AB38B66C615C2F2D3966DA77118FA72356321E5C55E3B9165E2D374DA05C059B47FA36310018357AC367EB8622BCD1BB2C6410DFEF40AE94A7BD1DBC7E2F01292A4CE4E3BC274AA159BE5644780C1041C0213C6311B79B923934E806586BFC15EB73C7CF0F34F99EB1AB99A5ECBE67547002DFEA019452A7F46FB3F1127A8D3B133992B9A2BAC81B94B86862166BA59B76072385E649EA6577B4FF3624B0426F3F75E493CACE7F515FDA953BA208F5B8A1C22FB2FA84B4A9EE72D9EB059E92569680029B3C90201C82D60272AD2D3C6E1914980836FA1704ABADC7C560A186BDC607105C33BA18D19F691968662854A13ECD19E3D93CC15EABB1F4595BC189CB951A957D310D4469CFEB8892021A58D3C023F815D42411C27B0777AF8C25A898C96C2BB519C5830B295EF882ADCECA22D927EBA65A00C815AC8B03B76B4CF56211A32CCC5D9957C52718E6F7240D4E389F0854D160B3FC7D51A9318C743A5748ED06DBC1051AA885E7141655FD378F99C579280027F1242DE118B69E46E2510A5E420B7D8A39E8AB6BBDA342034B1AA3E394DCF02813877A4AC02A76A03BB74804305A21C9261786463440540740A814AB3681E0FA260FFB055D8E7560BA3286E839F44D04AC9C44A6D29BF09127DD0431BF3E379E031C236D44D79C3A48CA7A42064546BC86ACFF82141E6BC17529374DC11CB5AADCD612E7415053EE266494128E87B1091DBA6591CB86C4486587B095F872A70C53DFC3A0D456A0C8865B45E5404A7FC489A8A09641B9A3B149D7261CC896107F000A7A75C6AAF772D3F9B242509318DCB1CECEC4527C92CECA5648441B99D1C84CF472572AB5575ACA40204C434283C1229570012B47829679FC8A63CE1302E132FABB9687F93095E932FA419B22849A4D6AA090FC69ECD029B6F7489B59A43C720C49CC557D8288680F6B0BC6B87D7462AD128022BFA946685A53E2A8C1F6883567B9C854A012475756FFA2A6B2C6040B443F3A425B35A9A53B6BFB029BC08986FEE5B05D8E11CC006544EA96F5400A8879C9E61E310CBF83675070A4ED62BAE9B29D530AE845B3485569ECBA95672EA1A2A713252198DF411CE387888010C2EAA735605D3B9D889A253338276697283443A4739CA62A64883217CAFE5C1916ACFDD689E272CC51AB5C1988C3029B7795B336F8697114DD856A0573D65395A1289B6E285A99016776B97A76E315505190A7BD331E568987BFB53346419097B83962C9D5DB85F956B0B9E0506846416F3D83D4734063B06ACEED16E839957C5A7BEFC2A4346C0051A49CD199A42F8AA2F55687F9F82AA25BA1AB71CA172078F1D2BC75A3B5FF2557167EC3D10E10813D0625D925F1B2253816486F248A2F5D02A06A8343200003F22094D9603AEB35FF9C6512A9C7D21B8C78B0961DD914A95185572730AFB39179E679F45DB4E5DE5B1C97CB85FCB89BCE8CA958654A3293EC875801F8B40A85C9D1D53025A0B28CECB0AE0D0100B73543351949C259956A859D522BD37E96CACC314E52C7B41A76BCC5C88A677AF9EBBBAA6A573B642962EA821E5A516C44B0A7C0B05F68B02EE078F7FC5C7FB02211F7CA52793437F7A9A32DB0F1040A8A01C1779DB589FF550622ACDFEA78CEEF0513A004CB77141F5CC7D163B5038AA5CAD29854BB43109D53EA3873A5F668125C58AA7BAB3C5A4551FF2990A117056536BA4A499548193CE083D8B22303A6AC6F5442F8231CDDF3B62CCA59BD6F402B612316370A3C857311262AC897B290533229AECA42CDAAF33F2943E496FFA371992D8C80AC3C7FEB46C7748910B4875672A8BA4F3CCE4762894A76846A0CCC13149E02CA12FF1776F99B140731E3E78B23799BE84163B8EE399DE987C3CB4253D15AB07F4678BF03766C0ABBFAB39CCBB8D7064C660A66335F926523A161BC4004D746FB4A764F8FF76676AF904F129457550CCC566089B81BBADC284BA712C521BA3EB8AB0C46E2B36020E42C91F7BE2FCDFDBC37B117984406F46374188D1A2264445D3F44D7DCB9D6A2C3832E5FE9261E29172375A78CBECDA0EF5484C4B3F6940EA20EDD9"), + ("B5DCC09F5402F5DA7695E1D61B4C1CE11380347D4C6BEF40BA11E94E755B1AFC", "AB6EAE982DDA0008CF1BDC59B5C8AC88BA11F169798AA56374ED7A550BC4F552", "52339C216A28924BC691C5C263345BD2600C3080C51B274C119606F08C628AC2C4A49565704BC16D4143EFD78844472C68E56869A599CA3A1B3831B6B170436BECAD6EB880F6C5C787674DE25202DA3C86ACC00B9FA61E0A3B6FD34AA616D95F08F09A12C896EF0C603D639676A658F2F45ACA340BDF05CCB4C72B88438369F974D8EB46732128004B27430A3B73B983FCF4276A25539DD191AE78C897266A8F31A728871CACE5B027D714A85519F57C713A1171205CBD0924CED5D7A7C6FAA0CC502EE59583C5E4848370CA25E55A554822C3C2248DE70983F16E51240D35783061AC2D7883CD90514CF2847A6ED46437427AB5E06AF5561B41AB5189853817AB6062F8BC9509AB9239BEF180319BA98C0F9B4D85D7A258B00AD2F9ADF9BCA3F1D13AAE149092769EEC263733D38CD10B56152A40703B75C0D65ACFE10932827C9B29719C6808F67112206284CF942870AA86DCB549E8023CD47608A98182ACB627DF216FC4423844D746F9993302C88FD796273A7BB18EB31B2F02A9B63CC9731A267B5C544BE82465BA1F41A1171DF44BEF444554CBC2D2AAB729EAAB510C0FDC3A8018760CAA37359A2882E3B286C7582E1EB08558F55207673BD8818E3FD98F59B913349AC15DCACDFCA901F8D63D779774521A0A02E1AB32F77AF11823BA6BCC0E5093B2F7A50099787A719350B64D2860751321AAD6C17089E3408034B708C4390A32C9EC01D094B01A4E40A0F49A5BD649BDF9F0004ADB73611C50C2C927B76C31B211B7381B9B76A521EF876D2CDB280498BC3C4261F544399F1803E2A8950B9B7F056ABFBFCC7BD380A3D4FB14C4894D8D9C2B98DC5E3BBC4057252563C58C1587C7C2F78360785A65C7793E34BAC4F616F148CAD29A6851431C948C6DA3E42B3AB7BDC07A3B0C6A6496963B49AA5F1BE56D28E64D6639CB85A2540771671C0C021098A1A0489CB7C579B33106E2D815FFE6AAB3466742E63C20062857C8B1E770CFE537C1A2E20085D3BEA8FB986C856F3BB3A50CF874034556AA564752579457BB3DDA316A5CF044F2A9B3397BA543B4BF51BA2C640698160AB9D7298753DB22F2B16EE5B433C7D08845BA32F05006AC1C9899371D63A2779DE5803B3307082382EE805E00ADA6B2D0788CBCC3DBB9C003B38AF549B83ED6B119850E4D2B360A0230677A85894A882338781B6BA9AAE3023F5B401460BE97E2ABB95B862C54269161334D5A64557656141C421D497476D81E04ABC47AE1B42025B6B91607DD14277DF333F252AF596606C0777893142CC7BC685C4C931B37A0AFB37A633B2FF7A4B49FE55BD9E55F9FDCB22C45A6DA169A49D44DAEC44567C651FBC368336CAFB82467733A28E94A23E3E1AC6D901AAFC86809683365DA7396678B1FD27AF5B9C52AA7BD02171DB9E80FC0B04E502B723A5C0552947D3FC488181297DCD41D0C42A232034621A1655C36A6973C4D9AD47B60A78F3A680709E83C4D93CDBC96BCCFB06128E9358EB98B41119FE7D29C7F315C92A17B34D666AC5AB49DD65B6021653C558DBE0C79A65BBA23D683687325D7A004AC53B20FB6601D2606AC6872DDAC03CA86BC3DB47F19A828B8A35CE7293E3E1A3B269B4488E1A30B9D15152B039DF36221FB7184EA150EAC2E80F0C5255DFC4C9A542B9953E627", "952119B40B1710433F4321C2CA494BD4F40218233943BC1147552D2DC91B026717215450F96983CF2A58E405AB2A241A58702929FC7B0905A01DC772ABC0C2DCACC02A12CE431099A2725B30A1A731DB62B46A5187040913FB1AE54096D49728F1511747386BA2F4C059F9146FB50E7DF06468676E5025B34A9765737C23719209A7860C982AA634B358E998386B2307DC994F1A4A2F1D182A8EF23429952D663B1737B2B025179FFF67A18357B36433590B8012EEC9B7FA1128B56B2A926B2D9B86A60CF35FD1C44DB8C3062E4067BD815090224DA631A00FC46186230FE52CAB341270390C00E4952557046137957C428CBAC6C25E9EBB3A5DFCCE9C7BAB51A61541E59998760F853CAC6D1478422B38B89557247388A414CE19386F01DC1C19489A29AA1123C04AFB8143E1542B45350E7A853302441D1BB836C6C97F4AA08138338A8A2238C7682C3D1C6EBE63A1E74390E2A47442C3718E61CB869B35E8FAA1D80A027AB585AAC62219A602941CC7EE60B22E97A1851225AF77082C3039E0B84035ECA9C7D2432253B80C0BCAD6E20BEC482A18500BDF75C6820A6C2F2C945A13BD2A0CB5188300D5CBCCB7B3846AF78647413DE8047CF0F9954A8804F889165374A9F5341D93AC6DF3995EF2396850AC793DE83B11A72EBFA673DFF04B868199BDC60FD44029BED01428957D505C685CFC6DFE04AF468735620B96BCA9B1B6298007EC026C54C0CF6476F2B995CC35992C791E80146E957893B744C409564D1025CE8B957FED9B95EDF00CC2B36D0F891EC7EBC545B4889BE31F5052BDE7360B0C69BD280255F5E5C66A655FC857AB5B6230000A2B2A587313D582350A35A0DA4F28F3B738989C22E25490C08456556A5A47BDE0651C9468AB297480CC3056B5AA2326F927BC19B406E41D4663CFDD00CF39A2154FC9439908020CC724C6D67E7CF77248A39C05004FF9F7A8D1569AD8B3866675AC6DF8757CE8C492460227D3A6A7D089D850BCEB770EE8B25276336C488B648CD7284AA54668E074E41A27C6E2972F922705108DF582CC713C3B981110E522A9F3873B3F345D1AB01705535A151BC92A1A0C6D126810D88B52EAA83597CC5EA7286CA243ED1542929CCFC835CC7F279A70119D3C8933015C4DD78A4C5D1184CD86AF06B6806F578AC5C4949F468AAFFC0E74FB989F760EA3525D933A0B939400D62B5681968F0012885B042481724D0DCC1A994336A37B1CB866926D331F705775B0439819393C790C78808385A0E86D92F100D95A5E5C0325F8A42E7243266D477C8FA60AF2D1C0DA05B697B945C2C1BAE9D73ACADA3C73344A7E08793D1186031449B291323A8568EBC450F82B71B7882A76991BCCB55FB70C83766713704094F4F54C0765C8E85966AA95AE0CB02360903BF5D86D2B33347D93C080A575894A2A67F32FD5A279E4B269E1896E788807BF2BACB2B3402F88B3D3193B7BB005D36752D1F47DFDC5804FD2751AA5324BCB360CE87A57B05E25467D8C76A42E574757783DF6AC54A366CFF62B52EA836C833790DA23483FE05A7B2B46DF986BF920C755064358445C7A003B0A4016A8D9597FB60020D348DFE806684B6CD9E8582CB1380DB26B99609452339C216A28924BC691C5C263345BD2600C3080C51B274C119606F08C628AC2C4A49565704BC16D4143EFD78844472C68E56869A599CA3A1B3831B6B170436BECAD6EB880F6C5C787674DE25202DA3C86ACC00B9FA61E0A3B6FD34AA616D95F08F09A12C896EF0C603D639676A658F2F45ACA340BDF05CCB4C72B88438369F974D8EB46732128004B27430A3B73B983FCF4276A25539DD191AE78C897266A8F31A728871CACE5B027D714A85519F57C713A1171205CBD0924CED5D7A7C6FAA0CC502EE59583C5E4848370CA25E55A554822C3C2248DE70983F16E51240D35783061AC2D7883CD90514CF2847A6ED46437427AB5E06AF5561B41AB5189853817AB6062F8BC9509AB9239BEF180319BA98C0F9B4D85D7A258B00AD2F9ADF9BCA3F1D13AAE149092769EEC263733D38CD10B56152A40703B75C0D65ACFE10932827C9B29719C6808F67112206284CF942870AA86DCB549E8023CD47608A98182ACB627DF216FC4423844D746F9993302C88FD796273A7BB18EB31B2F02A9B63CC9731A267B5C544BE82465BA1F41A1171DF44BEF444554CBC2D2AAB729EAAB510C0FDC3A8018760CAA37359A2882E3B286C7582E1EB08558F55207673BD8818E3FD98F59B913349AC15DCACDFCA901F8D63D779774521A0A02E1AB32F77AF11823BA6BCC0E5093B2F7A50099787A719350B64D2860751321AAD6C17089E3408034B708C4390A32C9EC01D094B01A4E40A0F49A5BD649BDF9F0004ADB73611C50C2C927B76C31B211B7381B9B76A521EF876D2CDB280498BC3C4261F544399F1803E2A8950B9B7F056ABFBFCC7BD380A3D4FB14C4894D8D9C2B98DC5E3BBC4057252563C58C1587C7C2F78360785A65C7793E34BAC4F616F148CAD29A6851431C948C6DA3E42B3AB7BDC07A3B0C6A6496963B49AA5F1BE56D28E64D6639CB85A2540771671C0C021098A1A0489CB7C579B33106E2D815FFE6AAB3466742E63C20062857C8B1E770CFE537C1A2E20085D3BEA8FB986C856F3BB3A50CF874034556AA564752579457BB3DDA316A5CF044F2A9B3397BA543B4BF51BA2C640698160AB9D7298753DB22F2B16EE5B433C7D08845BA32F05006AC1C9899371D63A2779DE5803B3307082382EE805E00ADA6B2D0788CBCC3DBB9C003B38AF549B83ED6B119850E4D2B360A0230677A85894A882338781B6BA9AAE3023F5B401460BE97E2ABB95B862C54269161334D5A64557656141C421D497476D81E04ABC47AE1B42025B6B91607DD14277DF333F252AF596606C0777893142CC7BC685C4C931B37A0AFB37A633B2FF7A4B49FE55BD9E55F9FDCB22C45A6DA169A49D44DAEC44567C651FBC368336CAFB82467733A28E94A23E3E1AC6D901AAFC86809683365DA7396678B1FD27AF5B9C52AA7BD02171DB9E80FC0B04E502B723A5C0552947D3FC488181297DCD41D0C42A232034621A1655C36A6973C4D9AD47B60A78F3A680709E83C4D93CDBC96BCCFB06128E9358EB98B41119FE7D29C7F315C92A17B34D666AC5AB49DD65B6021653C558DBE0C79A65BBA23D683687325D7A004AC53B20FB6601D2606AC6872DDAC03CA86BC3DB47F19A828B8A35CE7293E3E1A3B269B4488E1A30B9D15152B039DF36221FB7184EA150EAC2E80F0C5255DFC4C9A542B9953E6272210BAEE60864394042F2E190EDAC61E3C72AC42214318C84151D6E12D56CAB2AB6EAE982DDA0008CF1BDC59B5C8AC88BA11F169798AA56374ED7A550BC4F552"), + ]; + for (d, z, ek, dk) in cases { + let d: [u8; 32] = unhex(d).try_into().unwrap(); + let z: [u8; 32] = unhex(z).try_into().unwrap(); + let kp = mlkem768_keygen(&d, &z); + assert_eq!(&kp.ek[..], &unhex(ek)[..], "ek 불일치"); + assert_eq!(&kp.dk.expose()[..], &unhex(dk)[..], "dk 불일치"); + } +} + +#[test] +fn kat_mlkem768_encaps() { + // (ek, m, c, k) + let cases: &[(&str, &str, &str, &str)] = &[ + ("B649B9AD5A59AA45640B03ACE153499BC1244465735DCA6E5ED0C7116070287758E7A31EE53BA171E7C8964B3615075286A4AF1EA12479AB0218608692A2606A024D12FCAE691C8114828F3547C9D0344AF9920D952BA6BCE6AAE6A47360DA1588697F91AB5475C5588AD6328389A34BA50E41514343C534AD7947C5AA4220C73D335BB24F6676CC2549FD40759CD4B54549B04D8932921B183ECB634B579A54742DD6734C7225741BA32AC196AA68FAAF3D1425D4A44CC563AAF8816A8258BF745842F1CA7D8EDA9A7A6CCD72966ABAB9061EE21EF3D2B1155133F4B8099B653BA8B5224360CF00295F2B3887D1B12D601B18BD407B80D167AEFA0D3F6A906FC2CD08A663B7766815A26C6E2BC83318AC99B5A56D338EC347ADBD9A57EC53359CE898FB637B32FC4A6FC216BFA30EEC501681751BEE46C5C02317C3B3B98F24AC67ACC53941CD20035FE2A59890E9AB7CF063FE07A62703643E0580D99C152343C5BDD8CB9F9C1FD0C194EE7281913A7D1F0473722C024DF76568A731D309CD5FA87FB3A0C771AA42EFD160AF89752C1C3EEAC74A934B163AF92D4EE74C709A31E901045FE6202DE9622B552ACD807829F46AD9C47087E2856F294B97546103568292A4B7462895F161891AF4A66D537E79087F87F63E4E5A7D767A5D6A4A52267C8CE41413EF6C3DC4B1C64EE5AD75D9542099361EF81246A64AD997885FE0631D02919AB6B967B8C441D73B67D52B5FA64AC7789D30E659D776334DA3A65A3B4081014455BC858637B23A991FE8EC315C687C36D81553C79F159C2B4B285604C0541AB62749CBA6C29472B5DC6AB61B2BE2E6A57A1942E729C1E95BA95C8100D4554FCEDC0D73AC8023F736A94AC757B7B5108807A5EABA507B6F22E627EF325C0EF3B28123BE7882840B7A8EFCBA7E0D82434C330B37B7C7F546B123D460A0D0C58893A7E4664F49ACC9150A5DFBB71FBEF44374A987E3192BE4A50FC1F1160A0488844864532689E9F29D55366969E014B19869251977C34049437BB41B334C2DE7A2EB63CC3FF21B042AA0E6839469E4BFA226CDBF8331CD1640E04B4CF2A89BFFC20283DC2D90706604C1021153417B26C650B483856463DF2C2C064AB4A9F316C5BA02109B1023370DDED31AB1DA2EB837BD8CCC52106712EB91A019119BD60951B3662F3F6291ECB76561B253DC4A1CB8E41B3A16B2EC87A252C4B747448823902845527B31C15A3EF18E174B644F548FAF30B3DA5610EDCCB73E3A8714BBBDD668C14A9472718B34EFC545FFF2783F033F13FC1665BC324CA244F1E91851D8CE2DF2B388EA24B2CB8EAB400F5A8AC1D01442F765688393CE21C4C63113BA49480B247C3FB4D49DF82B1F493430BFA78F6D948DA4E927BDD9BD2D18A7F230046853BD8BE51CD59178D0295509213B7E1B0798584DCE835B48312F0257A185D9360E0A702AD8BB0A53C119336889974B8E52B636328556CA1A9EEC413F5259C66503C90206A7857925C727815C94FD545F0112C6A7E89C2EF54AE897A4B0792F98F5710CA174288658F5C8596C7807008369831135E1D50D5AC77F6AE9641DE0622BCA6A8E746700818C4A22A9AD30C9BC660117F3462617BAF392280DE09F5695B3CDDA5E931C5B521BDAA455C3D0F0F7375153A754ED9620DA68DD", "7D5201502FAD05B1463BC2212D6AEC1C8503204C491F12D9366AE750144B7831", "04F4A18C69708A17F561778B2AC10D94380ABEA4A20835939C9015D78DAC41A5012CED1BED948AED6C79193F8B2FC6DEABD3B092EC33AE2F54778F1C54CE762A69521764E20C05BC2EF96992F463CA95D09DD588AF622C297BBD8805113E985388FC9E16FDA06B5EED42DA629D514F86ED84ACFF0A09418E720201B794B49D072DF15E7B7D6EC6D82379A212C71C7603A1C9BBE57FB1CB9A431DE1980ECADA0A4FBF5CACE9AD0CEEDBFDC40761839D9CC1C8590EB6335179075892A8015E04ECADAD37FDCD4644EC2284CF4CBB4620FBAB6055A163E3733E3A7747044B766EBC356436B33E28FA4E67B083592B05811361445C719F6AE8ADD4EF8CE145E3933CEE75D19E98BB964D58044B6DE2B46107F80C3D4690114CC84FB0D3B3D4C3AF671EA7B833746B54FCE5CC761CA4FD20CD163AFA849E5797619C31144A74140ABE1C7540D1A3C557A9F23AF6E6E3523667FFD13B92444CD3BE01B1581CA0CF7A536CE4C073DC17DE955BA22E469BC1C0EC213B3B7CEDDFC47567A7ECFC2A58A6C2A3C2185563277866F8979BBB86AF844349C6021EB9926ACFE0188FD0F809E056A8E0A8AAA2A4208562E775EF60C56CADD6E26A9E52D60187BF6ED0565616020E0C2BFD79D961B1069FF261B2ABF40C9EE2A2C442877F4EDB8D9AD717CB434FED67EF2EACD629DA1CE78023548853EEAF7D998923DB7CEB0174E67875E787F398435DA84C26B478FF6BF785C4714BC6F8E91804E10CC699E1BE342C952D57D3C84654D603709F4F6BB596E022E2E6149C81025226B9925045FF365D83991F7D4C8693544CA7BA6DA60F8E4F6723C9F14AC48882556336ED88C20163544C55AB4238E510AA910B04F445252D507AF02AD24E7467920C81F2D31A71A7241BE2726BB9F8B20BF2100633F616A1233801EB37597DDBE2DEF36EF0727515E7DA178DA7760A41EDF9FFE98FBAA3495A35025F2BD100B3D63E940BA7D997104AC67F653D0A24A2BA2C8A355AF1EE048CB116B1A492577CC7CF61226FBBBABD9CBB043839585F2E00AE673EE6BECAAF5DA7919921C90C74D5B8B173B8A1A650F379B3B5E5F1D04538B936FC2CD0D4F8B9DF9F5052ECD9E66602815B4F96586D038D5BD5A3E44BDE1EF9FF9CFCB6B9AECE3129EF1F026BEFD299A7A8AD324149B156BC5AB868099DF52A2056103432879B495B0655FC1FE8073B502F3F40D403548B1629118CE0EDD41558E4215E8E241A45637A3434BF070F17DAC885ED656F80783A4C47000464FE78B9DB0DBB55895E271D3376BF0C50CEC9A403A8729982DC5B9172B5E80A0EF03FA2A24873188F8022A6F9DA8CA4F2E24AA7E29987B1060ECFE0B08E039EE1F7FB55A0CD35A73B6C25DC26E469BBC2D034265DB5F74E644842BB99199F83947C97BF87532B37A8D40A06F8BC5508EFB117D11DFB07325D9482CDCE60AA34529546D4C8D8F98E3F5B34B5C757075FEE9C3443E0A1109253F5F0A905C571E5343B277E0636A5A46AB36BECF5672E93B712B9BC8E3CD3656CAD1B29C16E", "11B62291B1A9D307C8240D70BE0B45436DB445793173F6E79FCD2B273D7F3B01"), + ("92C65B345762C2B52CEDBA2FA1482CAE931186922095B6CC2495A49A78B77A01B20C8904560696FEB829F0430C2351037D9338584C40637275B62B575F5809673639508CABD4210B676A8A9CD77FCE134FD593C4CCA8460D52CE326067B3C38A8C6C64BCEC23B5746A2965AE8E4622EC5569E9861EFA52A13A21C39683A29DE596BB40304493575D28CB2EB6109E6C1E66A3BABB733E7ED6B01F841A4D895AE084B34B100F0197CA53F774E9DB0B2F8C9F4BCC1921F26BD16544893B66846B3304C71B74F40BC998332A04BA33CBC6B291AA967B922660C855D124A5376CC8047EEE3542057B27B658A215CC990B3177EADC3D214078960BB9B28C132866BA167C07F5E876ABCCA36422B8FC0B781C59CA5D1C7DFE777503D7A6F9084E62519A5F59448C4123FC1096BFEC3D58E81D7E0BC675F106BC0514E86992E3490A0525317EE4C784F96DFAAB8A430A9CDF246B92A36785F6094E2509FCD2A7A4022638B37177468549A04869787E946401DC5CA4DE968796908EFA8B67B9D45BB8A1A00E921E9CBA86C14A96E79C808DD65F04184781C46AE6FC7B1AA159EB55563B72184D12AB81590C86FC914E58C9AE186D63E780ED560A1866AB84591F0557867286350C3BCD4AFAC26799B68EBC60122C85CC9A26A9059FBA3101A0E8263ABB12FA93C5B0B63DF31B563C3086C8BB5C91989B96C3C933B82F529A8E4A02713F4B08F12ACCEBD343CAE17FDB2902C58A595CF2695610BE6208839B719F69257AD42805E892389EF9C76613B1074830B87217C6866E9C943E7CABAC44C88D5520CDFD3A594CA3374B603D2B1173F8D114D742047819C54FD411FFA93B08C482D6933D3EEA6D861C258BD0B4059A6B5FC338FBB003813744896C61C0D825E69B4FB7D56CE2563F6A120ED13CB1CC0BA776415EBF17BF697A95E04B5EEB8A316DF170F59770E4FB20A7221891F872AFD2424794B60F033FC6BA022101B162D6034BF01ADA53B4A7B1268238AE0E65C0D166022126138C3093126C87404A0FC6E20D901C33DC36110B64095F0A7109B2CC08190006E712D6F8B9F6D6AEA3437E7ECC7D2B303FD137A357EAB63CD1B87B0A73E36950E33AA75C3714F98990B62AA41DBB992DCB406D8552AFC687FE220C48455814941D41C675A0D9C3C88068537BBAA4A208555A2319F365D7D8CC87E42A1F488564F84727898963380338F30C4E783DEB161026B04CE1856EEB04CAB63158BD037A6C124522E91FD1F233A5A008EF39ADDD92C15D545D07F532F762A42C4C528CC59F650B305BB1042DA369FD70A226145CDEB389DA185972636F04974E704CA9D28CC240F3B1BFD03157D88018D41B352788A14C9CE9D33F20416B0E7A7A1055CC344397974BBE611A788585AA4126BE3BB461D7403B85F60599796F8AA928A28235F3305DA4315CB50441BA596DCDE15BD09309EC11823D377F1B0A4C8514496240BD7CC04B5A919EB4C0AB9EC1B9EF79C849450D6FD5B6EE188BBEA35C52289C81701542991CB2529C4448269576B3456A0AAB95227663388FEACC63B346A84742D8257428912B35E5A98A98786F04B0B8D72EE901B1F86C0F7135B6C2865174CABA64D76070191D6EA43194A7BD87C395741E7C40A35A57A87C97C2FACAFD6BC804FEB5D5C57EC896AF08B81CCE", "BD8D55A47DF20F6B636E06B0ABA5F258B6E7712074B6FFFCE9153157D8B849B8", "9DF0613102915C62D693A1FA216A588267D923E3B261BC37CC0D6910D3A87C525FEF2CDD80D5A2CDD55D2A30348AEAB46881C29803EAB9F8B53C85D8768FB390FBAA94236181467BE4B0DA9F51568E027AF0BA33E4F290A80631DBE2B102FE4D64B1E87AA5CA7EAEEE0F414D685C06C0754353D94AA7CB8FBE3ADF07E5042CB2E49EB24BF2B1F22B69E97C4D455B8A26F4718E5ABBEEE68B09FFB784FEF4D7AF2E62E835DE09548702B22CD041CF216D19380086196666AA9362D450C9A0DA6ADC97F09A2202157EF5D113CEAB30D261D852A46C531AEFE8F95D05AA6CF8CF87D0FEB374EA17394D7DD1D37D1D6B916B6DF2BA31072F28E8539AB9CD48DB64A14C5E1CE9F5B067C517A8B38C7D44B4DA8CA9429639E0076969F9F24979A1D2B83192E48F3B143DAD0E528ACF1B9AF2386626B51C7936F8F174964BC0913FAB829ADD7AA5B63D47A73B01BA06381FA61710897730516A522EF1DA17A6E21FCF7F6C8135DA057899BB1046C447A91A3F34D5844B7FBD9699C016F10B00BF71B052B88C432F98CA943AFF767A45F18D1476AD989C7F98393364359B8089B4B2949F76E8C93659930F837D1DD3E68C12564F58347AD5D0542A46B974816CC0CADF85B235BC786ED41F4AF242E1C21017144286DE483B287910E242B5991BA1F936C6B51B0673E77D436C77689C0970608A5C7E58AF1AB6AEAD4F93E3FDDA58527BB3C2535E20FB4FA5094639302B8D178525891D7EC052C7D1CEEFB91A40146234FB77633C2D94F463D1956ACD2CB1C77492992E83FABC9B1EB6728367CC64EE96D45475A5209C88EE6F13E0AACE0FCBE503B5948E80859F4520BA238BA8951A992A2CD1100AB53D24C6E702B8808826E206DF141BFBEF0791AFBD7DAF48A2C49A4C3FACF2C125771D1DBFEA06E95D0EB50790994A5E0B51267EE7245D5A6F4F5A140CA7DD6DD44B90B1D1C1B37FBA776089B24D5C466C45A0E10710BB82EE6376F9AE7FE77F51331FCF82F8D442BAE0344EEE2AFF62D4D662A4B00F3492ECD515421A8F49A9504F08D83B09AB45523DE37A9913B48A619ED314C21D5A2BCD7F09E584E7425422D21E72459EC09A6C349D5642C6812845B0D92E473A45FF1155F941E8A544EBC1025BDB671E7C243EB25B76BBFB87118470C12D0C65FA114F145BD7FB5CE2A524F636B2FD3C8B2B7B19A1C2A0E497461623E777052BB8E2367D702EF15448D89B28E07C7B636D51E94A4F714781179275E109167C162572328A2654A8C6D1541B45B291D13FB781F7B3FB3D43DC31CCCBFC1037DCA1D8C0DD3856B3FEC7A38484123E5D93584F4B0C116BCB173B1E2F060E04AB26E4A5F19561A51C7898A68A7BF4137A261279885D81E13EF6937EBF345AF5D301AC1422A4810A883D008C7AEFD7BF0447A0E987818AA4C17B3F57B5F93E22BF2B5A309DA5B8AF0F9BF841E099CDC097720DFD5928B5F2E77F525D66896530FBCA3264B795FE2D1A3FA2593A83D23C760F80C1A4952C78E1B1DAFB3CF5E39A0DBA17699C34363CF7", "D281A3833118CBBB15EF35F846F9358576F030E49527BACDA4FADA9FA2733D41"), + ("69043B0269B4D596BEB41353D51C29DD687E6FF788989107ECB3C99F931AADA6AF8018A6F8A138510960B7419D82014BA6E58BFCCB2E386A278623C9891B4A7BC5B062E8CD40482ACEA10123C85A9E188C70E00FCCBA9DFD485E122C9B4E0A1537A5421AC134E887125AD9B049BB3484227FA0173C9E4B54CE946780ECAD4ED4055AB23EA1CCBF80D11CE0AC85C053B654E629C69B9A935B6AAB2303B47259FB237EEFBA8BDB9694AA521F068A61A1591ECB09439D13908882CC82E42A45BC558F1C5DB7C46262C5515378082BF68E624055029A818FB8B4CC9AB6E41CC8C0F3C66B0713B7674037C8B781214D5C89A4E8271159E08DE2DC0A367817C4E7C8DA3CB69D573F7964B4D7F04602E7382DD12D674BB4CB65209B35574E651FA0B24A0ABB3C64B899D5DB9C663B1D9D282D70D35A38F50FAC111CFA399592DCA8D8F8B7B1D1BE85218E3C07395DC162A85AAB9D267E12C191558849F9188E56357CD8374B8ED20DABC79EAE8512FCC005E15A4262C06D6DFC216B753E4626A3C59538C58B06ED98897677508CF798FE3A94AF19A583263CFCC24302CCBF917ABDE0003C20A39D8F42CACE486200DD7B1F346576690305687FFD877BD6A089C5029F597B278F173A3B41B1DBE63FA7B405ACD0856772A1C902B7E35683FDF64C6CE190677B531C077564D22082C82CB46B77FA33B276E841367600290B7E8CB32B8D1A1DBB450111EB8FD8916C4286A56F7214D2B0B3A89377509C817D4632CC085326301BD13203BFA889D006B7402C866035B01728A2D57C7A47F60512F25E63D4A9E3739A4061CCCBA9250EA8A53C4B3AE52AAF2175C7AAF365C13423FF586A0E3ACE217B08330625A9B1A2402CC0D8B714D380A2BC4B0E5C80AF03FC94CD7030E94719F62A5045DACE70030D3E1598F59AC08B6696D2932E5FF00CB8964916DCC81BE7A0656B8E30837A030672294A298BF58B2AC79B9C56738CB6A63D3117C0CC60E68C6AFAB0765F483EA3B30128970EC172139CABBAE9F5A1C57B0DEA44AB0E40821B18A35CC41374449A364BCB478736BB9A32ADAC543DE71085DAC2FFC6A5807B2D80B2495B3A1122318144E89DE37A893CC24F1204CC22778C86EA58440165487C8A7CD660C11C1CB71BAE86D51BD8BC42C4B811891B59E7F8C4375830155B6F8F558875DA21EF831365440369EC1FAA2294E553CE79655F35EC11058609D21CA8D034C4E4365395E07AD6436BDB4C879A38BC8AC49712B642AF4263B5999A698C062C2A11AF7B1FB447AB0F6810819A4642289AA9871ADEDC425136C52FD8B45A64440D3A57E3439A5D424B7A802E62802A89B9AAAFC027C2C11BCE84B472F084CFD50FE04388D8D559AC6B89A0860EA70198D4360BC7AC175B03CAE9B79D17C09F19914468849F85864A2C2518E6108FE3981B20476A60C10ADF654F616710FAE03B03D8B4DB169B06DBAC17AA033A270CFE3BBD84353B28AB4249A3CDDB81CEE5EC003E08CF392435949524057C4DDFDB67B5E9788835C749C0AE7238CDFB06BD78C51FE3A8B6CDBA141511464AA3A272C1785A0CC2CF169A1FC1830A88BB052B14FD1B51E1320CF1290096B30C2CAC2AF4820CC1D6CB0F117BE6086D4CDA01DCD595F2E5F779198FBB43B083CAF0DEBC36BC734BC4316829F3E2BED7", "035B33594F2029ECB3845CB7BC2059B8A77A660BF37DCD55C97AFE708000DA40", "476107D63A7498AE87E1557724AB397270FA8938B0BAB7E7D78C1ECE337A6D087EF862B7E1A104790F2B5120FD9FD71DF27165680FEFB1A2EB24351EA1327AE9D582F8D120C8DACDBD4DF49C2940EBD40FBA5177F8854055883CB9CFBDF18C1E17C56083FE4166112CD6486BE91A5DD889820F5F677CA850AC45CF1B627A0C6C23547CE85C10E80EC5DE319D483E78305DEB1DD115E38453EEDD78FC5B5018B9042712F52A14F6348959C7B4EF961F6F6789ADA6E7BDE59060636DCD9D590B71490B23083FDD68BADA5F50CBDA437691CF882D3151311274B377FEEACBC47447AF40C1BA96487D72357B22ABAEADD9F4C32F74FD4E82C862CFEA8B5D76B511A56AE68895F43E743C87E7BCDCE38ECC68B12C366B2FD47E5A3630F45ABB9994DB880009DC2292803374446C7B09C45A9259EA13A702844FBFBAB943B3BB114DF7A4D6F8D9F07001EB0EDCD80B8F52FDF60B01729AB12287E01CBFAB3E43B7A69B98BBD0F9D21DF4FB963D6D3C81EED366D47BD761A3239953378761EC3A2CB01A8856B4C57E74082819AEA218F2D7D5FF96E3A80E3179A86716F7174EE6EBC686082A281382A237298BC0013FA685B7B48BC975EE530575BC89090E1858FBD0E1F129392B3038A2C4599AF3A80F09BC8B903F8A7AB217265E240F1314F10FD355E4781EB1EB4DE3B81DC0ABFA62CD008E189864D6CF8B0A92B629B8821DD0812E51B6CBEAC913943953CF6CD2B59E94760395616DF7961BB3D3FAE4A36C517694140F029275D22AA4F72C945B6B001CC71FC77C3A2EDA5CF2934D00C935A33E209018302AA91D5B12905403D358DCD6B57D47F544F6807F1BCAE018934EC7B2C093B202E401D7D8D87B2B42A1D18625FD2D6B9FB9AF92586FAD87006D5CAD37B30985FDF812E489E215815E40A8BC87FDEFA6A11935550765AD39D07122444E63134F6C07D0CDDD6AC854048D907F43A272F88A5C5D93619FB03F0A95D011FE0CEA4A73F2F4AF30B2565DFE5AB0C48FE551587F52C37E9C296556E23CA78EC73B394ED7831A6692A709021ED1CAD78F1556F1D9FFDC18A1D2625D9C9CBCA10896045CEC29A2F1DC6AF2E80690A9EDA18FED5EA3558C0E91A6DC97CEB82FA56C90AADE516A170009F471092630611183221F1B3A770BAED65EC50439B1B447F514DB2042FB6FD7FE2B8FC6C272DC8EE0E00C7A1BBD6AD035295D5F0AAE2F4A09C3F75A0442E48EE826C3611FFF33F1429F9FCDF4F61367F3E95328E8A108A83C8B47420172E8AF8B7D14A5606349C66C9E0D61F9F49D82C334CA43269D07B9E73D8612D158F038D8F824E1C4E51FD5D894E12452C5F940AFE91D90F65591AF8DD161D2E40DEE1FE5DD32E01972D6532E31859127D12084F32077C613AAF1D71060ACA8ADDFF1E56819EB001CCE222F0A2966F9A836135010985353029FB3A638035DCB9567984ED7ADBE7938CC72BFFBC4682230D566804ED1DB47A94C6095848AE219AD2E14600315B333317A7A269906CD5AC4B69FAED011EEA9B2B525A0FDCB", "61338EC538AE04644A7A2FBFCB49C968F68CB1B634DCA5626A9BCA03C4E30F89"), + ("32831A9AD55DDF56618550063F779D5FC0519D1369E50298BFC0413EA042B83891593A5F388700324C1481AA6EBD970264D66A9B244044955BBA2B41BC168C03D28354BA904E68ABCB911F60F189D1C77EE0845E243A9BC87590EAA53BFB783A1DB202BDB663F9787DA7939CE404295DC5710B66C4941B06B18569E4471FD265CA1A50B82EACCC55D4B16870BC949AA9000ABD627B07B19BC039B2C28707081D9B4BCD4C0B398631E835697B0C65B05068D6977C4EA011FF570C983A169A15397B45146FCA5E73D011D4D959BC8A074379C26D182E05A30764D3A7C5094482584D320A0CA885AB0AC41DCB430C01BC94EDF4186788A23E4434602C928B24490B3957D61C48B469C50829B81ED5031209A2A0533A41D878A3CC0690C2204ED30F1C90C2F3F36A78010EFF3C4CD4D76B950B13FDE25A4273313706BEF6165B0DD8B669901D0EDC0540712DB9E9AD34222077564FDE9AB58C225063187E2A472B7EA07713369E17276E6B27552BE0BA65F06B575538B69780B9F2A2D528AD6B94AF9A35AF05D8053D40A129F13CE1EC80C39A51E3DBA1A59C5B803449A3DB0351D3824D30336EE19FB5733B1CC5668E290591DB86729683DA56A223744540B7C50FB3983CF51F5B7012114C050D962A26052F0BF411104566366438FFC8833F36BC26D4ADB2438024755B3015207446286E7B638B8169D83A222C953B5832661F1BA6BDF34B95D383B3930F1997AF29C7A9D28080BB390370AB84596B47551745074567803A497E834D1D8B6136F1B2BCD57D5D9A203A09845B3B3DDD76A097F744F3A91D63451D578BB186BBA6B159BA3042767FC3B26CF211FC0539B29520CC748AB99019FFE4A1BD1886EA00A30C319B0D71C2F967669AEC4AF0DBABBCD23ADB96AB5A916291092CBFE2B36D9616EB25CCE4C676BA65A38AC29EEAA57C1D988A442BC4B0F287368247EC522A1D29BA49F798439C525EAA53B23872430487A1635E226095EC5B3C47F6611ACBA06EA8791CF437AFFA7BB51898817B7E6CEA61BD675C3068356203426C42712DCB9AB3E53F4CB6CF74EC1EA24A24E54B046347A03878BE4228559FE49164E857D7866F084A44502334DF0316DED139B8C45A279C6F748575E2ABB4B0A9AE34DB033046755C1A3EA8A493EF0560B14623C3C92BD03BA930618650486FA7920BB59CBCCFBAC846184B5050818E90902FD101CDCC9C37722E723733A7B348A99C6F1917412F57222490526FCC825BA2A7408A387B31B4215BA803099EF4524BFB141434BA5E069B973105A84AF4C12BA09FB462380F2688B12222DFD77575366F66D042C4E54C70283B70503B24253D2D975784795798B7887C025238E1B0C27C620C323E527B330315CEC7A7CB8818BEC429342CB5C6EC93802C5855359B2C1FD48A0E761B8608A5E8EB55A667C009CCAB28E2CC23B75C97D7BE655B87EB0A8FE5B420591691EA4754D49608414CCC796431E8A59A39C76125A393BE461D7CA2B56DC46E244A8A21205DFDF037219CC26E4234FFE496C92965727A6FC9D660413369AB27797C4B1F88D2578B54721F64CFB574591B40CEEAA5A671CB7ED2AA25A5D733A9472CCB135C8C21ADF968B97C159CCDF78A91567B4CFB7FEB732B6D2CE28924865D4DAE0F034628D662D6CDDAEDD117862D", "BF274241021405CD6CFA844F1C0A24F6EA3A5E6E15040902653E0A17EAAE52DF", "B1F97544D04EDD67B7E1B8FC038F2F36334C9496F5F2A775C098671AF26FE27D79D1FD645EA7968E6FC353BF4FC5944D7D836591342F62C5663403EABF1509C65633D99F3F58B317C3C67500D8695337CB83FF2795863141F5E0393239CBD37C82A6187077EF3AAAC3E81379CEFF229B60128ADBE4C282B8AAFB1447E8B46463CB4CBB081BC4F99EA872A77526E1CF0F7BF492FEE41CEC46446A2D9D7E6179BEE633D6494ACA09D14E7960FEAEF309C907DA4B275503CA8C3DC2E727F071AC5C2A089891E798AEAC3B8E984FE1C03378C63FA1F37A75C7595E5CFA2BEB45F110E3B355AB6644237B86A7F061CAED6DE08647FC0BF70774CD470CAACC5F44E824ED6EE584EFFB94DE9B9A555391006B4766FA64CE93D65CB244A681CEFAB982935110DCC3768E11DD59C955A4E2F45930CAE5034F5D5A5D5DD37C43509E0E5FBBB616953D5309992A9F0EACD1D4C570E5EE8324531EF17500675325112F3ADAC7C901396590CAA0B35F6BE0E9FF008F57995EE7727064D2DF1E1D450E6134781480EB526059D2C37767C7696E484C4208ED3AC4880B34447DE2B577D20A72E0F59CCC29A99FC139B3978DA97F924F1FC6040598417A70DFB8FB528E62669A363506E5CDD037950286AAC1635EC04627026AE97D70759CE62F9811C1663EE39E5FB148AEF6074A1FBD2193ECD4D06421BD0B7CCA0C024BEB1F8441F583E21D638F106B8DEE2E85268B366807E3FC0A23B530C881DD54FEC2A5429D89F9B702A4B248ABE596F021EC8AF984F2F074C994D5C6947527DE0863F51A972303A82D59A88E0FE79A2F284EF3F40DDF55479762A66050FA4A4A40BD51F1526C7BEC652DB46E4B0D6921EF52442C3654F83005CDF394E1E55F8F1EEDB47371C3C4B2F807586475BEE0BFB95E68CD760ECE689CAAFCDB3441536D8A6AA3C7EA80B6DFCFA2D84AFBF187246F32E808F7171CDFC52284B81857C9191B16E01F8C1706CD4C7C647A08D71E732E55FF645AA013DDA9E67E7EB9830ECB5DC244B4850B7329DFDB2B8D97609DC751D9262512E167CFDA29590B7403DD94D3FB49E08A9EED4586E9EBDF5365971375AEF61DF87B10DFA64AC35BF356A1E1C990EA12453378ACE2ACD7DEB1DD2685426557D1F41A022F1F8BD64FE84939315AC081132331C878D3D0DAE2D00B1B3B751822959D473942553742E61F8C618F8AB5067D8A49C836C4EA75356111FE3D1A781E8CE83676557F546B906EA4C40E7908B4473B2620D59C06D23BFF494315F8DC3DC41BC65DDB141CA2EDA86F843358AE92C44197B6F7FD57C7C936B19FC1D96BB12878CDBE9D105BF53595649B84BE94A3B792F1474372EC57A2FBDF36912E61DC1869AC70776EFAC0A00E275C2D4B52345C11CD9A9D4604DBEFAE168513966473A7DFFBCA0479211FA5F5A8B28D0F6140E4353807C6AE43A809AABB62F4896BC0B9E33339C3A1F1A23E65D28030D9107022FE870FAF0C39BE4288C72AD67008DE94C2EC7F502FFF33B9255BA3574DD61863464E44AAD5BBD2", "CCD2596065F728BFF32792DF57BF133F8F50BC8DAB822640CCB6976BFE693E0F"), + ("DBD23E1744CAFFB0984F97A697E7760ACCB421D07EB7E25BB1C91B3E8473E2727600855A038419765C8E86A025794290CE095717D5BEDDB6364A8BCEC06617F66638C7658651067169E3CF69607977E6378039212EDBCFC7926D7C9B50C960758C85A21C743AF6DB5351198636C0072012913B641B6E53AB0594522E5185D0060B595A6F3C628BBF7614C1D88A1D7788C6E994A0C29FEB14A4A0121E155B976035A0A1A121E4D41C1FDAC830837E9071187622C3A128650D91831AD1CB6B660C2A5669CE38C99BC1C546D706F8DB82058C7A5C666F54F5BEC8B1142818686947B511F2132B72815E296BEA04546C9A1EE9251768281436114E4980095D8A25EF048DB69A661CAB717CC6BD3F436A77A19FC1D146CC77AC5FCA3C3EE32D711143D6E8642FF6247D5BCB2BBA511C57415F2C43DA50BFBFB6116E970B2CF589BF2805E42A45D4B60B1B9344E7E3CA77E88B0BB95DAAF96F388C2E410258C61265E2C636684586C40332D318781EA27A5438B9B336AED64B1785DAA44B26092B2A53316387B007BE08019FBDA577687BCA204BB29D63CFEF1255F55A713A120025E94B780137DF5B1148861CE3B86853E61EDC279CE59373DEA5C96B45A404342821228EF51166FF639184024A65F2BBC50C629EF69EE9E8C4149C478683051DE7A9E478908310463BFB72ADC44F91941C1A0C3C945C0A60D08E0E4714A4BC454CF7AE1A888A835CC89D540B83771EF8214A32E0A2B97B8CD971C8FD2188BF311A9CAB1A36142AB73436B68AA22F5A5B7E55138835512FB51319C94CF094893522921615343102C0D1F4C9A5F52AC913760177C61BA467669ABE22136C25EC352EC8025AA6522B47BC4B4326A3279144E53D0B2012ABE8C298CB7A6C9B4AB17B069DCC3A086CC60886CFD05286D33373C3D8A50B302A5561033971CC31D9CE56254BA3C32146F815FFA0C2A5005E78B13A04FC99A0176CD132C750FC7FECF67857A047E174850723B33B1A2984C559C2D8A0E60C55B2D427EF67C0E2396B3B854C86CB09FDBB78FC234FE84C89592520D66B3AB2D1291D5433380576AB74AD74170D805B10CCCC0114F714ECA9B63300C445D82B94051D3CE075F2FBB8F7351EF454B08998664ED14C834789984C939473906582106DDB2BAC7BB2696531FCE5CD580B9DF56B82044029B2895F0F41936B91916455019749886F03886C8A9E2DA147EF5073BD96BFAECB92729A71F2F18A2EF694CAF5674F6736E0F94CFA0B2B60695D64D75A85B14DE6DC7782A351BFF2CC0EC47BEC3374A89C1BF5DCBBDFF782FC0AC98387AB61E7387DC2BCDD92816C13C011E8263AD052A0136FCA88C58AC419F16B12E381A8873044B83C2FEC68BFF82B8499C47621B3C0E6969ACB17658D36772422880AA3485F621933E407381C712FEA2280297507194625EAC3F30014A4A516D6BB8593379BA62733F43203AC23985C17925D428EB84AC3AACB7B80B19B3EE84600078712F72E9D0043FDBCAA949B4AEEF5A8B6C5096823049434ACA7510A1BF9A50ED0C9EBA64A9D53C88079C87174929DF89AB75732F0AB3C138148EB8C5FF92C88E17678B701344A3021428A842C828C28A6C5C15C852D3C4368DC0D6AD8545D45F0A8BC16BBBB45291B092D25F44016462DC96C9593D007794BEDE1", "101F8466E34BA303AB8032030B9ADDAA6DF2F1A7D212BF807426770511350948", "7839E8067401971FC39BFB619EF8935446FB735368689CAB95605771EFC5D30402C6DF666675C0D8CB8178013A70E00EFE321E46DBB4A2BD09056A0219CC8DC1526D6D322750C4AE257AB875C5E1109559B4ACFA5D59E48500C2FD5CA714B08E1967EFDF90C9944F02E27AF389C79CE0C8EAA166CB7A1846A1DB0479EB4CBEC2BC9ACB77CECD42C05540A5290B7DE2E3DA121665CA195767258F76517EDF5D3E30E0D7229A91B2AD47645CD31D50CA1543192A0782F56E4378E6EF29C5B12F73DB0B8A9C2DF495206D8D28C8D0964789480E20EF6E70BC959EC6DA2E7B958AD1EC402090AD8595E8A6F642FAD9C6EE4B9C59F6A4E4EEFA0F205C5B519729D9AEE915C0E3803ECAFBBD031910F3FD1644BFD54ED10B230561259305C7FFF2B7DA35FA6BB19424DECCF2DA47CCB947FA6ECB10CA573F8B3271EA0B635E5FD25C5BF57452285141D28B02885FCBA1F949F8ABFECFD548FFE12690BA37EA43EA7209960F6F25496076A74D195256B5BBA7E62DBC51AAFD5928421B21102F41A9422181D7924731AA385B2DC3741D4BFE6DE4CA5A91057E1F0369742270ABAAE4BA6E5AE28CE200FEFDD06ADC2B2238B2D0973F733B8FA10C62DF6F0429BDA45D83485B528AA5B2C16464B896CEC7DFAD1534B31185FBA569A6D86EB309467A32A496AB93B84A62F781CCE7FBB4A44A1EC06B7F241D42BAD37222619A12F993097B027291D332743E6B4B6CFC940967BD0BC394C3CE2CEF5165706A65E143DDD97B87F1682DEBDFE87E8E73FE684AD203822A9AE02224640CD5801590E24785E43EAFA9C144939EEAE143AF90B2A8720613D16C3103061E14C0D15CC85A45223B9BD013C7D3E0D1902A2CDE44E09F26110421E27AB5654989C2AF4C4980EC3C2A7D9D2DD5B90C90A32DA27D9FBB3EA58F17D321F935BBCC18A4F058513B019FE0279C5F0904204B83E0079AD17B99C37AD4202648F44AA9A64F9F9E57D90191BA861D7089058DBD5FF0CA9C4710E0498DA97422881AAEFA6C32FB20A84D5EADABAEE18BFD1A9D5307513938C2173BFD7C9BC057541B85C313423E0128F49F46EA1D48B0B61EC4793193E18814DC0BFCAE063E61919BBCB4E87E1AFFEE4D07AC2EA68ADF9D05FA7B11257C2B84227B20284B840178C14FB7285E8D54B60C2659545AF4BE8BA71A0866961D2DC36E580DCFDA824846AC2AE540CD99C9B351C4C6387A9C11515E0C91408846464400220876642059E9EE7620095FC440A6AD7B1E8E97CCDBE5BA102913F392477C0C0A2BE410BACC9B5252FCC0E17A18A363A05BD523131BB952E464A7595E8257D2472E92C9198BE488834076596FD96839A3E14991377A4745B3FA4ECBCC4092C2EB9710D2C2CF34581B4A680DAE5929D0967D4F1EC62EC578B324D41EA3E9078B63DC0E128D8779B8F4091593FC31E47D16AB4FAFF6AFEE850CAAD9F48E2A5DD9C8B78AF04A2E56D328A54B9FA769369802F56D5765368C435800B09A52C1483E3A44DBDFA22965E5BA1AFFBB4C2DB139497B43A1CC", "966CF8E6E799440C243BEDB9AA9305BC6261A48A4A046B37B80EE93BAA24FE4C"), + ]; + for (ek, m, c, k) in cases { + let ek: [u8; 1184] = unhex(ek).try_into().unwrap(); + let m: [u8; 32] = unhex(m).try_into().unwrap(); + let (ct, ss) = mlkem768_encaps(&ek, &m).unwrap(); + assert_eq!(&ct[..], &unhex(c)[..], "암호문 c 불일치"); + assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); + } +} + +#[test] +fn kat_mlkem768_decaps() { + // (dk, c, k) — 정상 복호화 및 변조 암호문(암묵적 거부) 혼합 + let cases: &[(&str, &str, &str)] = &[ + // valid decapsulation + ("CD4178D37142A91C7C597854DF1B00E6147DF88C58769B10B2A0B9BBD875FBE05BA228847D94C01ED4C57860339D6735B0F73788C495688395BCD51E4A36BCC15C1F3311162F8525557CBEFA176D0FD8BCFFF79843085B78C0ADE09104EF77179927C37E739AAF7166CF6539C35097C0C3769051186572735B600C3EB07A412932650CA28D469D971B635D789C7FA608CBF819B7B7707291B9D22C25CBB55B0D875EA903CE778C81A2E96BF6E96BAA1A484E69524C644FC3D8C564140E32D4AE0BE1B1CC875CB4EC7764C498E5F01C04C9A41CF26BBDBC08C8E65BBDA78A1C4C0F362AAC60E16F5D225454C2103A99758B0A93864C5F32179808D2A3855AC837424E00F0A0A3055C07F5AB5BD04BC6950603960C29AA2B18C60C154A1022C32EF59BB718E94ADEF990C045C27E3943C115C1D12A96EBA503A01098E33673C98B0421A7C71189530D08B4933912C37C1DCFFB0A14A93A3EA6603A6035CC194C58532D70E2B1B7B0CA5254BD7E010D7409CA58973C7F2062EAC9BA196429D7DA3DD512C1D81182E17B7CEC7C630659602C117F74008070E967F5F2293E9643CF70A1F5A38069D0359D76A84C6846BA1A1BB91B5279864B9671C3896BB2D43278DE11B1A89B8F95F96A9EE815A6E7B063989A13D32080CA55D6C0421E6893B77632DCF96D09F11AC5A740029273CBEB42C7C96FD36C1EE5589F61D70B57E93B8405BDFAA460FA09BE4A345B1D9939466806B9E462929240051B57DD614626A813BAE8248A66C3C0BBA804C52B345924A76CCEED5468EC066DFD5A1770656F3A3B956531A6D4166CB36480235A4E62CC7A9DBA2B45F464EB39002BA71A1211963A86B16880C83B3BBBEFA5A7BC4B31B2A69C09D9675A4B300A829F6A438649E26B82A7CDA8B80CB65986E47BAA2F544CFDE7B46CD17FCF1BAAE568BB28C8235C591AE717BBC05C0CF86344E4627542639FABB11BB5F6C66BBAB92F981CF6BB217942B95F635E7EE75E314CC97F050276801B32BB86CA586E79C40638DB283C3948DA4C7195C4421A1B3BC35886678C33EB0A5A52BA5C78B138D51CAC85704BC0140CA5B830117225CF4B0E363B8155EB4B83C7073BB20AF2798376BA442D309577C62705C7A9172527FAEA9CE21B3863F32DAE7A2D4B2314A376C14917563AE41DE067007567A9E22BB30B834AFEF2B6B07846AC30B54104B8083248CD23AFCCF794C1F59C1C62803B28873013296F5048F0637DFF072529443FFECB57E12A7F0CE52CC8A85D258372DCBB5C0612735D1A6BA7E0AE02215547B28F5E77488B3A241EABB2F3F19F89974B2D822604E74495A2091C363E9E88A7B80769D4492F6B7A7A3BB27DA60A1BA326882B7732C9118E545B6C4B79787225BA598378BAD7B3CAE90EDC06CD0EAC43A2C559CD554C5344A2751952F4B4BD2BB694016B41F65494A1C750A9D9C338A25F5AE26BFE851540E6B84202114ADB75986193737A5F28C2BC2658CD6C960FB0C069C910A8F259593F2770984C87178C29530B6E6424B70BEA514F8568255B62CA6CC39340515EE1968AA800DB4B19AA7676984B798ED5B134116CEF52B5634C7997F33C58E18D1A64499B6450F247A3555BA2A4D44782E02EC6397C69764FD9C08F87E6B802A01BD3E77E0F544CC39944D86A5225878B435B260BD6236F2A68EE1440AB7C06F93991C7FA6AAFF20A1D862D7C73686F071CC7E6624A5B5568E10CB3C23E6FCCCA3FBC63C445472A235758129FD8067B839588A7221C250791243A8C020CCCBD29425201A53BC84621412CDD0BA72AC75D2DA4A13F26A59A7629BC372E78D54776F2C781B99B9110B4833C9E69136A5098BC7FD3A4FA9C3280557EAEEB89D0B15AF4016452E42EB0D608D5850BB7491D8DB6C703BB8D00251D69950DF0E99553160122D4C5FBAB7BE3727F74DA52BA2A240CB5140B9704A7742D17113022B9260A15CC6A9CCBF23349AD94B57B04A29D3859BADCBF979515B433C86008BB43E010A2C7AFF8EC7BFE508B7565B6B3898459599FDB830B7461C1C1C52B145B2829E59F09B0CA28863C70CA1D8A7AADAF6B0FDFAA45096268505A70AF267DA7771555D66802405F72D190189411C5A0BBF7A7185F857EDD5A4AEFC948B6B6997C3BB9FFAC18153785FF752481839CB39027709A8306538687F2501118617E5C8A67E41A2C856DBB274BE4FB17EA47097F81693ED7389B21C8EEBC200F601DD2D78E89EBB919D9387768311E04C9894B08E73C4122B41D334A56E930084645C000E21C7738765BD02FA9F6A7A3188B15739F50AB5EF1713879D94B19964FD272A949A275E289A7C0BA9EB52558926146A280B1036501DC76400993BA71698AFC4318E38847C3F77134D7300F211862F6078BF3685928C5A8A809A945BE058B4A2E000E25E88EB7907BB7977993D9C0183248FDBB8106EA3ADE37B4B7B8A2E139C2AA19088C25227FC5BDD59826F6FA15462301EECA0F4B76BC8992891D066B41700701B10D37899898AC9E69F7257F9330BB0770E0621417AC392D7685C7E47497D096DD0B26161B8A41CA9D9D67664C8120DE5C4ED303C5926126B58979C3F7300EA8533AC0CBF32178A0531CA634634B46B097B367FB2612EA461AC8E04DCDA2C5EB9543457969F9481457188A56E115DD965A810C3CF3217CFBBC51CC316D7799107B20256EE08DDFA87CF318A92D489C030AC57C25344182A5C8B770F0E4B8C03838050CC56A0B91FE84C46034C8DAD78F74161D0E93A9B9C4CFBA5384D023981AB6734977B1E0F900443B4B62F6BDF3227570ACB36B0B2E5D69CDFD9B7A05F45300E5A8918290CB0968FF1363C6048FDCB53F63207BC841BC3C900074E48AB4C73C38662F66C9996B3A926DE61A0B784E2A392E93DB87EA841A190A6002C9B4BF237003E79961C07F156B606AC861ECE75F44AC60717871FB3B0CC2D9C987AA768EAC3497B9893EB809EA4A8B24B144C2BB014A8A7D51D94FA5A05BEF7C47459260D2CCB035806F286793343ACBCFD207FD086B00CD517979ABBA9159C889BC6191290BEB2AA793BA19DA9DDD6275B430182EAC87C5B27115029A696CAE0AFA5C38AA506F486FF52B6A9EAC1414516F7FC34255633B1ABB7252218A67C213966BA6FB237A0A43B59E45C0A34902494359B19304D9F04CD432715A17C70432680DE20FBC1BCBDDDC3C24037573D9A22FDB34F987C610A9799861369288405A138CD4850B14393A6B6344FCC38CBF07875F37A3CA04DA17CF188E017E8CEE406DB439078D7B0E5170AA973C154D3EF4A59628ED3739622864BD402C715EB878247BCAF1F1070AA50599421D778CCE8FFE07BDEE87DD36086FB6FCD1EDB33AA50EA330F9ABD98F1D6117191BB3CAACB0E3F2C139", "997F33A26049E8467F96E36B0F30A68BA7B99CB86A65DEC373DC53388FFCD140BDDB7F966064C3F95766EBB90482089A136EBBC5727B8CFEA68F1A5119817735EB71EEE31444FDB38F70B67B9560D8EF6D807E3339B1C824047233AD57D8D4ADCF2097B017487BFABC85F6D23C6449CD2D0A8DAB7AEE9BFFFC5A2A1FCFECB289D1940A11BF1573945971D2E80D117B00EA95AA06BE7E7A91DEC35B291F971E790B4BFF8A07D6099E40232746E550470A64205DE84911C1E8EB88DCEB615B43792D040031C38D1BFE55108B18E64706FD7E26384E31970655E033C26D7C27595DCDB469A95F5A19727B38D150CD8538B5CCC14936E49A0D92692273C3CA9EBFB2AC760A77B527E3E2B537D50900819F5D1274D4F4952F1C776F0BA4E01DF91DE2D51AFD4E2FE9B98208B60313961140188EC6F9AB0E8B346CB3306A2BFF3734958163A66663B93B7F5A0E3863C37B3D2803B7C29EA6EC49DACFE6DDC589EA41C4D0543C8A8E05092B0EB2956D974042B0CB522DA76F8F611BF9FC956917922C010D7DDB50563ABC7479F05E7D80B583A3335AFBE569F6F3D5683EB131C6B3F2A0B5284EBE03EEA6CC7C3435EE7B47B1680122A562428C936DDD90AAD907989A4BD26730F444900A48889582EAF39897FE19D3CAC3B08D28B0E663B7CF2C30C6CFD6B73DC227E6267503CF8EC2DEF07AF399FA171E31248F23B8DDF33BA95C463DF012E20170204ED8C3A6BDFAFBE82A398FA5EE2E61FAC25A0107614D7545421A4EA899F0DBD2A1C34517004764C15ECB0A8BF41DAD12B5B555ED4A29D4240D6060EA3F9CBC08991F140A7CCD886B2B6A4EE6A0F87AFAA9169AAD9F354851E7513EF9C85C109D617563F42057866B9CBCFBA2733AB06FBDCA8D4350F8AFAC77BC28758CE2435196A281F13096235077AE75AC0ECB09DD123469B645796A8153C158934BEC4C7B40174A002B6993FDBC39F187E69E465CF773515BEADDA077B13271D24D8C2E05F02FC8DDA19F7AB0AD5D8DEB5F4AA124E3EF5288E4C8C46027AB067B94E130C41460B68BE068C6BE2C62A3772E23CD13CDFA36D01B21B62B166C0955C728C234970731D73C1BBC1967178EE146803E81772549377872CC46EBE017ECF40955DFE47A98F6A2B85E51A9E8F9070D844E24C3F4BBFC11392C87DACF1F963CA1C91FEA33ABFC662FDB709B7ADAF81AC17740BBB2699A40D1D162FF55F277B7A9A3BE7E8081A203F4C08284C8637994B0692B66CCA611177BC2FB9850A367DAD0B7ABAB85C88880947BAD9C02D54EA60EC0D9C7C36CE1A62C3CC7FA15BF22CE2B292886485E496AF718AFF7A8C64915D9267D3AB89D47317FAD00DCB2BFB4953CF90A3CB849D87EB4FF67F1850684E2FD2AD26BB6681C50CE8B9817CFE1C1E4F30E07ADC3022020361B1BAD3C98A49FE3D0D75A2A7DAB6E3E665A3E801F6F124A00818DFABE19A22CC684C5BDFB94AF3823294367F95C9CE3D8ACCC91FAEBDDD096ADC90543744BA5FC61BF166B8E9AC1DC03FDA0CE29755B17DDEF3C", "96980F7C1B160A45A8F56FB38D38D7FAEC7844DDF617FA47522CA2998605A71C"), + // valid decapsulation + ("CC918926213945851DCC669396F5CE20845435541567C3A8D7F70110DB0511607493814CA66C51287142E5178C60B23C2018374E273765618F1F413933286D457A7C2F35129BBB78C25473D845CF4996809B2216EC3A0366E97BB0214AAEDC263673B3EC0A0805335CEFFB1A99D221BB471B6647664665C6F11A19C1A5AE7E0344D2D3A61C054B7A401AC17652650B2DC036073766738BD67D033B21ED777A184720BBFC3A174A23A8C0A392E428C448654FDC5730488E29EC4955015B5F2262B048888B977EE2B126BF9CB83175AB22220194055085E90D72D1BC1B5183E2C620AF2B1E6CBC57F03545F7F43B09E9C6A73797EF861BEF6AAE0D20A8CB2459DC59AEF2FA7EFF9580B427795B3AC024227EDB0253F970BF011433BF9623D646BD3C67265523477B847660711BAC503AAB5C4A82A5A2237BC1E2A8C229289996088501E03CE1D052A820C31D641B85B8AE93D333AEB18A6BA5205BB38463BA52D1FC12D8D45E4F780B905B78414294B992517AA0358706B534D6BC6A830D949176CF9499DAF8CB4EA4A52B2A233516416E125815D4B47D9B4A1712319471C81F5926BC274FD746B82100912C4581AAE41962257909D683533369D6D69D32311706535B4A28B29E930908A1B41043A6BF0A8E1D2AC7E0F721C70C53B64B74D64A193692A0CB7258AF7C47D7221E9DC87FA9BB9CDF943CCD235A663C0340C17418D5650405CDECF77AE673A2DE0C159B4873879237CB2422B1625DF773835D30BE55BC5D660805B51C0D50909A34F778DE9267A2002253E00FAD1819781CBEEEDABCBDF0869F4409A3763DC82727C75707F0C578634289D4945E78CA0E6962ABABEC7FC5C56DA2998573C8A87DBC48F35B27EC45245ED5A7107C788B16347758320A556D90B208F5AB7B19F813CF93B5A4791C25A054D9881535E6435EBBCDD70A4466E6A34FD8841A8827FEE68BA67C309C70A66A787F1D205739C16FBA556F7DA053CC201EE45389FF59C19BC45012716E1AE7B460D12119876F2C204DC168941A7C13328B5EA0997B24C1C5170C40442640A1D6C3AD69A46B8397A762BEEAE257273C618D617C2A996E85B444D141C2CB8275DD5B788427CCA4AAAB99D48899230849F833E0D1B6211141F29025372A926C013D012AC4AA2077002C171C9A3C55F09904DA4CEE84220CA2330B7420D45C2D2EA46F4C6C9132AA48BE4685A7F80865B02375659530977A42A661B96A2969C71936DB17D574C3FAC6B5303779282622CAC311282ACFD854380B65C7D1691D2F17B53BB800AA60C83D641941BB7569A22263567D1262C27C6092E90A64C8BCBFBBF09D02D93A8EF157E8A447DEFC67F3C0BD3575973F90CE49475BFEB91622B13CA6AB94741C35E13B5837D2CA5FF5164EC33ED52B8A5DEB398598010EF22410A2B75E57747F1A12F6121FB12B84A43426939071C3A91427B01BADB2A27D41CA6FE882E986086F972F4B5B2D9BC65B82DA71D5C932AF53838567621D9B2B3A8187170C4D16E66BBCC384EA8B5724BA232AE7AC16C397CA7563DB43879C566056B5ADA202A92CEC5A00059E73F2AE948ACF32B6674ED4778BEB992F10A4EB496DA38C02FD9194A1C41EB6DB06618A0BA2918394B9CBF4A87654A5A63BC8BDDAE5A45B9C9B61049CA50924A7D6380A8507A1DA4BC09B3C6AE61185770297E79C1A958689669C03E300F6D5A80A2786DF1C3950C808827AC810FA962D6B57FCF14F3CD95E8D3CB6C069B32ED6012B708D12A823C0D5472D14BC1EE3181C05BF26067BCA30C3DBB96F6520752A03AA086CC21E11BB506A8E3365B041373FA0E3A3E7D71A4BF717619A7FC5E44A54B95FB362CF83B665C8C8217C7547D6321BBE68BEEF52CB1113AAA4B42532032FCF19C119D14EEA89618D41133F953CE0D6ACC5D0C9A8C55923673AEBD633DC4243E3E55C50285295431D015A303ED7B1A13853175A6D644628F8A0C9C7E8CF958920CF572C00633CA4D5C5C7E385919A37B1DB4588FC8F12017DC76C2124BC5CB481BD5A40200901B0A39C7D27AB3C7A8AA4A7E64DEF45896BF6BE77B3BC65622C6C711C604B01A6E5AB251383DA256547FB6DC532BCF014731C51C20C57B12744B64216090438CE34C50FD9CA64048461B3022B1CDABF57EA787176AC6772CCE2DA376C8773E646A2EA80667476CC4C737E8AECC6B56963556061A93C929FC2040A110A6AA9880D306D4985524B3A202C687AF8AA5DC879B1290023FE262E8F887CCC9554FDCA5724896CF1A8593AC68C6DEB5B5CE76C3A11C886C47F69286A9DB76ABF1901D4A88EF5E38C3AE24B329032B3E6773CE7A6E40010AF08002E85AC307316BCD42D26447233010537A036EFA82A7A3889CDC165F10B79CBD18E096CB47CC41F68210CD3B8700474A641362DFF77073DF20DDFE3B33F1993191148D39C9DCEE1668E9A4097B03B36A95F652415A6B4A1856A6E6CC31EF6F62FA8901B8E1048F3C93870950DB1E609660157DA76553B019C7BD65712C88D4BDCC6D75B1839BB30C774AEBEA6793CBB7D887997A56442EA0A7BC024161745733EBC2B99B22D6FC8258F2BB74D618D544408748354688285A48800E8075D38B43F241BC67A24CA429464D041009A791FDF954050E34064705EB692366B1CA1A63C1493382D917A9E182C7A7965515580BBECA643E5E5B7FB544E95D4A0948475AD399E7487C31149C7DD7A69B66C1C1ECCC1940C22D2F538C59133C7D1B55854A8D43261394460C950806D964564D69065A0C710D95D1736567AC21C3EEB67815B53BC0A89BF7A398F9BC6C4DB35FBD9BF69329E0CC6C59173B8F9F85D14E5C2CE10214A310100D58EDBA716B8981456DA45CC521C4A67B1D6B50527691973CAC8E1D2106F69B8C6906CBDA9C368A22FA3839ED6E81E413B359A773C5D056526CB1B6AA84EB7A2B5E6B40EB1456B93987896B294D11279CAB4870FD756745C74765785249112046C9D57EB073F263DDE66A09F59B75413C7F9695A7959CCCD2CA6E30AC3E682604D9A4957C71F6DE4284753342D2CA41119300045B0B7E7554E5A6AFF486CBFD93B7F91B8F2025812385D724056A9CB5B6DF89FB044176E57CFF6D267F8B22C96A34C3D799434C37FC613731BA96427501C7938B9EE780CE5723421B44D6877AFBA9920FFB6C1E498C4E7714BE6BA3CDF337386B2753FE2310873211106C9A3E43068326707345B531C8B72E16AB0D310C25A9E9199304E9730CF3CA91A1938A4D1D388F4D37181A32BEB9BFF80D3411D55882A9AD6FEC1CBC387F5690AE7FAF14DA2828D154418C7C3AD627A49B6D549254A0A113036B9D4345EE6FE4B7FBA212AE9FCCF326870E5E7936274F3D3BEE4D0FF1BD5A0745BA5997667186A3B24", "71DC9BF164275E5CDF117D9D5CC03FED2267B3F379B5878E9E612BD0C77DE6B2346B51B36F1CD6A836A97D6B0DFB853F6E3EE730901D0A6CAC1C7AF186CA9908A535E04A8729BFDFD36CAC030D47A4CB9766819B52D6A76ED0B9E2A550460DFAFA604C5453A811618FCB22D504C062912D59BCDFA52989A8819E05A38E6E500F700911B8C73FC3DF8B0922F6305C18EF97243299643E9628A933DD160D6501B80ED2C05545BFD6FF956A8CA9579C27C09C0CABD49BB53D1E7307B3989F36DB9D5625D09AF82EBEA7B0381C071E7FCEA5E5DC8248440C684A2DEEDB8C824E00E909A7DAB7C7B269E72B65813518C117438F5288E0721B355457E58EBA6FFBA8B2F9E6A3EB19A5CC5816BDA4CAB08D6C1DE81F90D6E7D4F70FE4D91B834247D7E4AAEAE653D9F887B26BEF6D6D446618A7BEDB04F5094B9B5AD1C5C892313EBDE87EBB310E38A1075D6217160CDF51301262EC7CA82D359121A4D248341F285CAE06C0DFD27FEFFA212AD8415AD90DE416D0F3AEB743E62A708B5DAF62F0A53A6F9B3F965FA727D08A1EA3493526761FDA0703C1D46E5CA6F91D383EF6514B6777C08DD8B1201635F23233C49A910E9B9933207392787AEB94899E3A01B1E65D84095ADF3B011AAEA64868160B4925B137A3A47BF1A68916C1CA656AF87906A303CA1720EEA5F42DDC9FFBC3A3C56F86159B93240A6E1D1C24EECE30DD8BAFE45280830BE1A1B42A15605E4A207D5D694798D60628CED3E22E8C8E8E0310177E0077699F2CCDF152CF91EFE72568C43C8D8956CE4570A3B02B7681D04AD9EF701462C4FE95F0BA943B08F4EE43FBE132679580F8DF82C3615751AA6CDDF345B11D14FEF2ECBD16B631F84D6DE75189202E84576725DD02DAC6DE8A1CB517932CE40D9AC34B469367B2A534E5435861F8CD11B7F8AF15CEDD67C9DCDE2476BB1035021D6149392C60550EE09667A1C2933350688ED91B3762E2B9AE3B25D21062058B5D45AB31F7B07753760B9B1C92F21FB02C1481A3AB0AE39F01D60713E7BD67E2B775624458E06FBB69614861DAD78C9A918DBB3146E8C5484FB75C50147D4886766EC62709DADE00BCFBA2E21E26F1449C2753EDF7F3506FFA0B43754D534860BF4907EB68C8B9DA23F5935199D5D4CF4C1D8DF25B59182EDEC27D0FAD70BD6638E4C3CB3C3AF2E8A900887629ED9132A12EE6B693BBB40BFE9A44991147B2922509AFFC8179F2241A895719BD3916587E5642AF812552D3D5513982651A2DA7EB7E88DCB1D30C2EBC0662EC9C9E8BD8CE26C6C846BE802D11C45AEADEC877211AE09B5D663D4851184B44C6B79864AB29C1B6EAD0E82DE4C82FF7BBAED21E89C72C8D718995C19EF305263BFAD3EDA5DE77EB188110E108DCC2D5F6AEE163440F9EDA7405C3793EB4CA3B09D0377EC70ADE3E247AB6130B33DA1FA3B2BF0A3BF7A878A3C633212A09235A93D291F8595E22D9B1124FC5BA306B937183BF8CD5F997113B876B769DD9A78B19F86604549D6EAD96DCA1B877C292EA6B25C04E", "A73828B556426C787F66D75BE272B3C942349344A5EAAF62F2AC8B46FD110A70"), + // valid decapsulation + ("C3DA5C2B2C4E8CE093851782A8E918733731BCCB90B2C39234F164766AA401D326F5E340A121918E5691FB119732BB4AB74CC29116A7A775B7B5A51FCB16A16C45AB7A368DDB2B5C736B29B2630B7DC25F70CA85CB0A0689FAC6A49A29AB292DA052A98FF56C02174E7DA4A2EF2C7E15583358973B5F123F01580A8E0CC2A27267F8DC5679210C3EA6AA5172CA46AB3B757C75684054D35976AA902A1CCCCA53727B96EB8D5670B911F7A7EC8159E60562830BC7FBA0901EF3308711A2FC274A6DE59D180427D8D33C8D24BA647378D2F45229827249BB93FAE5A0CAE54575D61FE74A475BE9858790BCCC7136ADFA0D0640B194B7658DF2A49450130B6B0C2B2B89AD45C2FEE71D360887289404E0FC8FFE51BA13B73268C8C7B72A0330410D38999602594EC1959DD0C42AF3542444120FE4DB4FFA553E8853022841004AAA3F978B6E2C1A4314A5B833C84F41277E9B920FBA7C94C56489801A3AAA511195C44ACC9806A3994ACFE912F016AC0F223C9456CAA157842974ADDDA30B03003A23C3C4D22B26758C361E67B4CDC285B0F9C214F949F44725F4B25D3F1255A32461B9BC7E531910F922C463960CF1F177F4055A2F180ECF9C956700A4C6405B25C081B5D009F1BB62F613388E60650855971A01A39E2A0E3A9C5B95B4322C5B4FC5F329CB64803EF6A55F0178D561729D29522DACC07085C55CC6437007479ED75C452A979DA06AAC3A5F19118431F5AC88BA849BE7AF25E262DBF93C7281694EA8BBCA2721B16A54A42C0851E046AA226456044C51EA0037A88459108AE6A3702026B29CB5C92DA2B3D627AB948738B5841D7AAA18AB74A4ECB00F64ECAA8D80AA6BBBBEACEA2868E264374A1AD23A5543DC1D55687460765469E5C01D12CA94CC86738A896E309AEA759AA74A8461063717522856520E1DCAAB3BF40AD70471C1788FD45A9C24048B5E8C3BAFB1BE9D905F25641100F5627A14C773971BFB402CB8A3B1C141621AC588B832473B0A0A6D937DB13A5BF40B8D3CD71566E58859CA1EAF792CC9368DB1460EAE61A92A32309E406D919878EBD088F6551C994ABAE06973FFA6B901040643C221CB378EE829564E42CC78E67B1E641F8BF30CB5C39D2EC340C3F0626CBBB3D8A722FD5C93349670CAE684E247CF3A24A68805217F91AA3698B9D18369D6F5B620C39739C911239388E0543DF2F137566897C26C08A2F58BF80B68D96BCC8CD28F59F1391997C9AD58285710A96461194F429920FC3837A581A0D38D7B717696C052D6C81E36E70828566B4487192846229B7CCA973A25DD548F27D610F921B2EA83A1050B3CF1D2785FA99004898026B2C8C443CC219199A9B86E2B3C2F8BEB5BC008D04CA40339472E4D11255B61B2C08C3B73B13C3AE39FE8FC5913A974A882C2C74157128CA1D4E8B86289A7266CC668DAB96F6AA5E43C09E8A262B721A2C02042965092CB3A70402449DEB40DAC61A0B0E3A1C65A7BAA5032ABEB165F5628A4C38041349519278272E5144B70913ADC1D3F93B42BA09A0C831EBC7369CEEBAA61F7A2E8208B19AB0F66A130165B2C2BAAA9819C860DD7A7BD829803E483A56C75D82CA96C277FACC4473201C40C080741B61BBE70C944B124C78A91F86A1DB6E9C18DF7AFD642652BE118061AB02B055DA9950B32732180E85AB3B56A79EA2073229AAEAAA79C1518E9F76570B949D04C9771220D8FBC1957A6A2C07855F6002D7EF48B098C627AA2A9C099199D2293AE687066E39CEA9C3FE321BEC52044DE7C0FD6998B4F7C9E8D4159E810679EDAA9F7B32DF0C161B5938229818FC93403CF48594FE5CD5DAA67CE412A9D7C5590998BD296AE9B77A536D023DC63B978D754D624A703435272FA2E18291E9BF45FD824137B4B98460891AEECBF4B97102F547D2BD43A00A57AFD60C3AE7A87083A1F1DAC601C41BE0212B98A4C999DC64FB51C2DA53A0B95C47DB9C9B7815C560226B373A78899A19A2C757CBF02A52025BDCF086CA3A2327FE66E9177864363C79B326501274BC3938F440ACCE63663B8F0704F960FE81C3B31020DE1102724E98150489AFD105111F15C2088BB66B1A05AC67EAC29BCB2555BD7B8763CDB45D3F0CEF417A5A24A62950A9050313EC6E6048E6A52D5ACB4B7041F2168A847FC696092210C848A1F0390A405B60FC83FF86931E6ECCD16199D31457951A249CB0C0D2C66507B7025EE8859D9CB23E733A07ACA1038F0A822717AE95C38128BBA019437F9A63BD6D7729EB4C873095CF1C3C1BD6A91B471B8C1510ACAA7A6F52A9899C180AA229AA9634CDDBA1DFF187D6FE61B8B8B8FA7D38D453471599B6FE4F9429683C0BDA6329F080C9053531E47A0B7B147D08B5C8BEABEE61530B8F88154AB5ABB09377D54A3595C9837888F19122F47B9C959070D8635261AA9099119411B1C54E6E527A3DA336054C4F14904F4280ED8E83D5994BBCD67BC67542348F80DFE300A75DC9926A6786B73245867AC95CA196B44B8F1277B57F5A09BB084DFDA3B7ABC3077289D8EB92A56B95A7A5842E14C425B74ACFBB9B4B49B5BF1908B963915584202E974641C8A7D5A8205677758B3814A0206C990CA1C927A0CAAA10DD0D1753A50B4E8CA01CB0A2291BCBFD4E4B51CEA83BF6B5D3EEBBB5974B689983BB3FC38688749FBC94352E4454CF0A8AE46A237E8336618117A7031BBF1AFBD414A736024636807DEE5C564E80213036630802906EAAE35210EE643ADC6750CB903274395546848C156D29E54FC986AC36E5AF218C32CA2DA3B2BC30B5E322B9128B0955783719310104F263A4BA88B835136665A3E76AABF513B15DE159225F8947664CBAEA9C83111B347AA1403E418EE30040918B9A44CB1755CAB81276A101A2E72EA8663F34CB4B318E5E183DE629DD4F4C3C54AA5E84C64441755A82A3B3949CE217A4F10C89F6AB925A37990FCE681154AAC701527318AC35C7912E001C8DBD2CB3BCCAC727B51AB6A7FE3B623D3811105792248DA05E536C90DA29CEAA9BA30D53795DB47641A9DAF273D52E8987EC84F43FA61858B437A4B0BE5822F456C2767F986F6E6850F177C3DB1A257B4AA24D5296C800AA2AB998899849C3C564966466B6CC38EFBBFB87AA3CADC0154FCAD6DE831765932A705C3CC21B51DEA5268343CAE394177768AC7D4914D6969B185200753415CC28FD1B5C22E3547A12348502C1C5D64335C604D7638722C3B32A394AE1FA1CB69D785A8B76CB039CD646143F71560AA15060C02349CAEE26D854F8C90C5861784C90D410E9F98FBE7D73B373402E60A4C12F1C651AEA71B0E5E947C207DBC41C486DA3ADC460D3319542D5785B039B6C7D3B40CF2CA8C41B404E781BAF86F3FC47C34A02D47924151", "4BCC0DE7F66858B01BC0EDCA0988AD438FF7722DF56DF292D2717AAD9DEF54047EDCCBDF8814FE6A99FC10D85BC96622625E06561925D259D6B26F46260914BA55EFDE4D16DCDBEA3EEEC6B241409D111411383CFE03F113920742C8AE031D5496F671087A08B2A81BC88694680C43C7B294134E657DF7CBE70EAE81EC1E5239EBF1A33108ABBDA851B5396414B63FB6ADCEC6E23206CA8AA938A7657613C19A6FADE8ACE9245065072CA936682E636F61225098BC66E290A19956FEF98E429F2715FFDF546DBEA0F0AB94734E32FBBF9829326A9FF4A3AD0BAC8AE0666C3726EC9701FB3ABDF90FA73C571D550F1273573B904964903940BF9DBCBCA39A8B85C022FA71C1AB87120A4B826A2E11DDF86F4529549B1FB8493D21E6C8EDF8A4AF8D85DE45C7F2D49EF0A29F3FEE950ACDBBB083416C36178110ADE27FDC2AD14D2A7CA71AD7336F0E18E3C6AB2C92CC1C9500745DB989D6FA2999BFAB6228F4AF743F745F403D81A31A61E2FCE1C58C599390108EA416F66B14285E266827E86E8289AE756F8AFE684C9811EAB0EFCB13494872F15B5D72BB8CB80FF7F111B9DF3AB66D0C7CE40F1F688BBACE0899AC2EC85651AD2805AD65A7A451BE2CD9E47CA30646199AF03E0C29B10AE0B7015F0490739B27528198D78DDD537ACB4D110822D2D405917A2D6BCABAFCB1378ED1CE994B2E7A16280453728AE3525D2688F342951505C00B20C1A304CBD5DB81E0A70D9BD575B3D8E6FD4DE2774F5EF97EC7D00F7A6569459ED6E07ACDFD5860FB007E1A39D7501D35FE4BDC3048BE978845AC68FB239A249E8F4EADFB8CB016EEE0CD781CC53BFF488969BC3A483F5C0C919F726A75D546D74BA46A2A14780267D7D760446C393A792A33DD47164A0D7538C39EBE1AC9779C9195E6AE039C877A7197B70FB7DDD1E1247F26E4B45D0F889768BDA43C30165437467171AAB17B1032AAD853630D1EB25896F3FFC95AC45C679736A0075AE7504ECB345953AC81B467DB17E8CE168EB2AA161817DFEA2896E7D027FD69CAA76DDC6C5C0BAE04957F28AC505BB2F49944B1E2AC1C78B0BD5C996D294F23EDC62C0560209E589162138D4DC69024FC0FF925B6904BE1B4B7E3F4BEAAE65C5B7351091C32807A93DF9E4C5C783DB5DC99C8A6DC92C391A4FD692B23C28550862EBD3990BB3F82280F3DFB361F5B3A46A2CA2ED3206989A57ED172FDF3A53011F5F9B79CBCA3B3D3B7E6FBAE5BAF84815ED0AFD6695B9FB89FDB7278EFBE2C368F2130FD48A23A39F43358DA2829D17AA7E611CC29AFE1C319A683F557390C63B102A38F09F8C547C20EAAFB6F8583CAB0960C4C35474E0B48B1106E2D8B1010218FFB0F9B53FC7E91F0D762617C5DFC72CC7D2F8EFC5BA4C6150891CD51C56A699CFE8C3E5A1783F01D43DCB80381C18973AD9CE8E9823C32F73F93EAC679D8BCBB0B137A5274517E3432DEE29282A54EF02D73B11CB5FEBC6CB25ADA82E010320E43E6CFA15D924B1B0F4217C4A779BB1FFE8FE31E28CD40283C", "D284C566FB6528F5F51881592944792309AC950031514025B7DBF0A87F3FB7D6"), + // valid decapsulation + ("5D4A76278400850726C8EACEB001719719C1B8CA4CD23A4FFDF67CE738CE6BE11FD9B9C5867463EDD33AF8644021E2088FB9B23EF1915930C24DE1B5858334556AC154F2468AEC109A5212EEB4145C3A6A4B9C517FE97433247FB652B34F3176778913CDD8BD2C983CB8F723654A8CF65943AA57AAC17935EE5B1305E4B70C711736C27A10823739C57C52EA8135BAA8D0C5BA35661CAD2AB6E17CC1ED38C701C072BC78634A7BBF10389DF6B4A7B41787D833C4F5043635A6A5DA47388BD1126C310A43F80F0B678D7BD083DAE9028179BF23D4C4D548B5BE042E0523988A11A903102F3FC517ADF06AE7E6B585108CFFEB8229E969DCB5C6B521755BE4AEEA922702D188C3554FDAA6AC56317C4F55C945C7290168B09F9C637E286848976A93A51EDBE98184C78FF7035EFED6172A734F23915BFBC9509742687E158E1E91944CE22415F903899BB29D8A7AA34A4871D28FDF4B753547B55D6B89502A66EDF00FE3A370DFE66DD838094A14C39435A3EA21BA93474EE8C356F67471A0F130EF85B00F30A71BF488A139A6832A0A9BE2118296820658C711F53CD596C41753CD0741506EE47AC2BA204DE3732D689C7402CA5E91793C21717804700181B23BEB136B276C96E2103C8723B9F6A60C6B4CA7CA91A2B07F1A4BBAA6742F33836D61A2AE11CA3C0C203F8A331D5122B4128266718C44B26773B5680D1FF4BF3E5B2B61C93996290423B35999F89C393A5319864BAF36C3FB861C733B8055F184A9C43391184768D4AF3C19CAD7A69FE45C16353A2A541B8E0576CDC4B4500F59899A19CFF37436D9E21E076A388B1A77C89B9D7956CCC8B7BC99409FE7B1C2A40712FECC9141C616E2E22E7817756B59870DFB187A1952A6E1C6F2CA1FF79888401667C1FCB145E5361E8276E10BAA62326F9AD1342A60BE391C293AFC035C1998136C3210F2A6DD591A37917376230063B0C43610A6D048BE4E95908C707B0FEA370855495F2C30231754E3AA97C5A908E088750CB50EE3A4A1CC84A09B81789511CE08133932C7659EF1C445885B135B569D2379FEABBA8CDA47AF083BA605397C014AFF278D936525D964637AC9C11B33808D1009A962AC8A0632CE4C0550F5538B8C99BBF5C51D7A803380724AE14F8F1B4A40989E5B220E95B78EDE458779D263FB8993E4734C6282C523AB3341A0BF2365009FE01F0B7C686BF3C1BAEC30C8B68D1A28B06230C971E59BF98B53B800430138848C39CAE288AD39CB37D545BA5B000191855B8B593717A0986FA42A5D56337607385B657643B519ECAB7DBB20031BD9911CE5716B75A26575C009E76AE138CE4E57113A35390B22273BDB50F0E60D6FAA0A0699A12C1311289520938834D796B08D36B40A270489494636649F65AC8B282A1ACD2B298688395060903F86C5EC501A87165583C12F8F8A7636B25AE6A3661191A61E78417B9A1A0BF877197137CB5643E85277AD09D03192CEFF87441F999AF8983F271201F1AB6026837DFB936001A4600D564CEE3B4315622281721416588C666CA88A286110583CE40064D703050858A3C22B09A2E80C36D81FED324D162CC2EFE9796D65069F38416D3680D774B32204090FA719F24C1EDBE1B6B62C465F45A654E06AECC54D8FB32C6C539DFC02230E336DF7748B440900B3A83EE6826E05A8A29E4BBFCFA0CFA4ECA1B8139B2E53405DFA1BC7BA2DA9D5B56C3B5285029CB9D50A905808E1351A6E57CCE037C979D7531F4ABC0DBB76C943B78C473C08135AADC519EFF19140D3272916C4183C2E2EF25BD6C414B9411688B813EE947A48DA72286A8855F8BAB64718B73401F88153B4405583400C91D90171C1C3228258D4DAA1B17B02A1089AE543021A53AFC877AF5BAA3E6E1902B4B98CBABB6776837DC140C42433CB33D383B842A45CC4907E43500BD956D3C0130089AEC3C1CF7E863ACFE4960D790A8D9CB4AE0076A92169FFA1CCDA7A333BCCBCF2948C3134623D54A83D7638C6695C6E9296849904F1744E125B8F3B1943C23318B3A7BDB4D44131A3AA796A24F4833B975C00EDAA085281336BA75C90D21FA7291F12E38AA2D5834B1003CCD6000CF2400AA85CD3974D0BC8C3A7CA19AD0139B5B08DB9224B12461F1C0BC96E3101A516BB5C9682257986F3A98AD2786302B054F12719BA1845CBCABBB1A50BB748BBC020BBFD293A14F8B01EEB0917A7BD6B6682A4389EA62B2A39D8346C76ABCF07CB2BEA4D7E4192B5BC2D9BD9B83F374512B76895C5038373C7A6E27543930C88AB4213C13C21F207D8625784C576B5887508A106FAE6B70DDBC1BC3BABFEB7273E8C3BE893CB116342C308CE3DD1B97920498938887BFA07E12C5B7A69B8E6F7A263E8182E884AD0E99C084431DBF6CAF9A44D13FC00BA58C109DCCE67349F337A4B0F3C6ED4783AFD19B91FB27490A896816474EDC41E6411B5D2AA1388C07556A3065FBBB4457301EE7632A6479AFEB03FA7DA46715443C6523235A25D62FB755063C016B4CCC50AB2E043C8058108B77A58A8E4AB9A7B3E2764C48A3729D295BF1ADB2248DC2EE33907A21693B0CAC248C2CE71D833EC80C97AB2CAD915442AD441BDDCB2EDF5B47D461B6DD83147D42ED86848375278A92B55EC885FDC2881ADFC885FE78B4910CAEF778E147532F9C74F141BAAEB3C2A0C80A870B36213E5A089127010A889EC8CCACAB005B0B2A508078E57A94A57A03ABAD12FE6D8CFFCC95AFCC6AB7F868E545271B840092FDCA5C54BBCB3A8A239D83C63A4365773B6718494D99C4EFD6C4DAEE04B7033C28E7C97F3965960889241E6B255654E1ABC3BC7D88974113891A221399B44CA0A5E3F49C791F490AF0C60785B0B62E10AD1667E8A6B5B5D812AC35159064B6F0B300A06744B0266BD1D589450F997C3DC703A73108CD01E7E79342C525F715C59E479190F53B4BBE6AD6D367027DB6FE5F615E84B2B0CF5889F9B2193D38F28C74DBA699BE15781FEE9686684638A843656C659E1FCC9F1FB0BCCCA1CF36B021C67B855A95B16C8B4D464148D5BB1422A169E68C4FFB186F7E308219255E5309F60B00DF321A3A39711BC019A5CFB0170C476094A6D12C59897769EE4BAB22CBC882B846E8057AA4F63336141B2DCE100A14029B153599558163CB5079D5853F5552321529CA11194D3934EDB98A466367460558A1C662DE04C9B794AA51879AB77019250F39C988A86A59922763C936B901D57150150C3B2D976A26B42CB0AA43BB3FCD71387FF3E755CC13628E14CA83359D65EFF03DCC9A822968982BC76FF866D65B941AC37B2B2806768A1034592FE684BD1B77BD666942077FFD67F0FC9F5DD2413D8C735CBB9603DD69ADA92C59E27072231318C9D798D80997ABA", "A16694D2B041768250DF8BD9BB6918AA9C9049C8C54963FA534A7E867551AA39B4EAD86B8405917145ADF1E9DEBD6DA0A749535D11916F652FCAD1A8F11A921F2FDC4E52267B85A4AD7FC39A977C119CECD48698C96C48848C6D45B023A208BB9277DF8A66D3C0785461E99D2814437B6B470413C0BA148B4A236D1E25054BCDA5C2C8F97076C7D2360FD4E976D84D5081750523E081E4B236B041EF35770EFF5969DD7778654C93B26E83E91292FB2C9DDA23B7D5FB1365363EF7B0D25683F3A606751602853BA9F88DDE18432865AA27E9B5F1170E681B2B86A9F89CDDDFCC2A5D1E05C6E23ED81A794DD0A4E699EAEADF43FC5A1E159D3DF744E28535A395EB521172542EC68E2C7D7B6672AF67356BD61AE2406F49E7360FBC5791E9691F510E034BB79E2FF5081E8052015067EF13E65602FDAFE6995A52881FA5C76659EA35EC5B4A5ED8801166D983E4B74692919ED8CE032471DD0F6DF5953AB47555765124E696BE148467F25700A08B1BAFCC907020AA108FA15CEEE6D71EE596293353A0F26FD387861A98F6CB8AF0A79F1172247114BEB3D2313B57EBEF1156E478BC109ADE1D374B92619DC2DE94CEFBA62F332E5FFCD3E744139F691334A09092FA78438006DFD06C63CA823CB837AF7F1F650D08CE81B46B0BD1B2517D51DB8390BFAD88D3491936F3C9008A8F0627AB432403D51E1AB27D541405A3639366D0305349C76F9C46A5AEE92784D7620B7147721481032E8A5EFF79C873860A4AC7E9D6D3851A105DD93A316C84AC048F4E8D0B31B87DCFB3C6A4E8F41A0303C62C44BB1E8D718FEABDACA9A665B822BB9F60B9B9FB44F5964B41A5EDFB2B7A4334EDF2C19A5D909043FC2D2437298A35AEC686298FDAAE84C5D97BC97E3CD8D9F950DA9AF8731FC3FEF784181E1EF745335B5B3A672397479BD5B07D30735457C39FBA80B65461448581610AB4C9EC084C6795B7E3DFCE83D229C166264ADED2BF4544EC62E9CF4605E5C9BA7C766FE6F5319520E1A2D0039ACEE637882A5FF545CDAF7C9EDDE23D8A22D5D84B70C822C71D1D4BC39EB17E623A964D895709AD62ADAB86AD477424DD04A199B473497A4949ECBC285A6066CEA0E49336FCD2FE6DBA478F08139766B3359213A0573E54E4E3178ECEC39465C01537250661CD68A235E1B622EC028B5BD1F016BAB8C9FB6BDBF2CBE4749F3B3A3F5D035611AFAF136A8B03ACFEF99D7B6EA1353EECF5DA28EC02107D1F9093B78ADA69A7866AF4322FEBA92CF3941402EFDE68443DFDC14D9C94A4FC8118F53740C3C239CD017E1BEB6D41593D27484D692527D59CB10FBC5593398B587563AE36639FE411527A7F6774D843323911911EAFA854AF383CFC66829F7D255079FA11E88ED8F1EEF5D658566BB123621C13044ACBEFC9BC722B99123011576B748A4F0E54CA39F5A96136B3F98265FCD394FC8C743C82F3EBC3CCDB1A0591525F1581688BA93CE2CCFF5AC9B0E109BA774D9035FE744D28397B9CC8446DDABD91A43FD73EA0C8CBF9", "D37D67DE69A313B472D9CDDE6F9540A5DCC04FAE34F3FFB523A29B979453F62E"), + // valid decapsulation + ("EBD9BF0F0C7E35A28EBD907F0A96AF908724E84BBF71A71ED30C0B7EB9AC5B936FF7B02FFEF23CEBD73278B4B1738C70558C0A69058044582C046001008130F8F1017FF9150F5662BDC479AFE43BC8051C657BB7897907D0928611A266E4871801D7C11579348212BC385A7A6D7673CE752D2779B150994064C19B97436C9C15371F468F0831B8A51B7100D46D0244487F92302189761432B865A846FAAC8DA3C9222C5641B93C44E80375F3333EF797197CF890DB44123589B394BC70241C73027B7A417565FD703B8346388C47BD411C257B6976301B7DC20C89752503A00A4153E1112A6031B3204936CA1A70D491B9025334FA9E38223CD2E15259E48E31483344A965052605BCF1ADA0C8CA18E1300D672894B670D5C004025289F7F163BCD533A349C4AAA62854E63906B8638FCA1E755C82BFD85E79A982875B40C4D88B4BA79177922A8CA26FA5D8237AD67DC19A753406BEEDA1812B7792BCA66050B9C532124AF2B2315DF3578A080956FC5F60D854A216CB70348D0B574C7DB83315C7092C02426E67B53175CC671510DEF090C14BA47BF12E1C2548047B43C555036E9ACF6F7440D18750161B7DCE6C942E6B4855080DC0800B4F0BA913AA2E32A47080D5BD04D6CD306A26E68CBC42E864B96090C15185D7821C795A502399435D153883ECC30E3BAF0DDACA3F910301A624E3684A43848E64C47CCF4A7F33170B78735DD6E587D29B943126814D7A965E541EAC78ABD1D9196F651E851A6DE9B638DCB47CF0F6502D71AC4CD4042113B9503690449358C73B5CCF9035FD4A15C4E55269E04A55626F87D60098C9805FA6300797771C21AAA4B4B7FC411573095EF6D08550D9251AE43B0E4359BE7778F595823BF6688561496596788617B019A09C5B71B1EF19231F255C1BF72AADA38D8106B8D902BD70830A331A1E5D581477949DEC3315875292EF7222E40C708C805C8D7A7F531496A98A53A9216256D4CC59B4454470A48A9993F7BA6AE88C3A2669A064E50F4C5690C8810C926C833AEA026812C59D48AD74EB9CBB5367E6040F90E872E976274E69A23A72AD1BEAABFFB4A8D6A4C5008DBC207B4B2D56BAF399BF1EA4364D2179744B926F76137E4541B95362EC5AA3DDDB642980A3A3728E7991558BB15B3EA77256AC4D6DE570A0370C009C8DE9917F8AE663CB58A8C1F0AB339835D6C5A5D328C6F9F7A4A07A784AFAA7A371902661820C840BEB150BD180BEF2E1CEBCB986B7A0C81A44B7C3FA527E648D4152BFD658AEABEAC95CFC9310C580C88A85B0012609250D66B22E376A43F63A12711B275B97C08733A86D079CD2455E9377777BC4435AF8CCA2D531C00B0A6EE92E3194972FE08D0A27364EE068D9E7CA49B6491C2C6967EABD16B0770294A5DF8BA0AEAA7A796A1E34DAA4C028CBBE01650D9A6F5C26A524E76B76F0BDE8CA79B811992E76B696C038C1821E77324DD312054974AD87AA5BFBA6557314524D3274FDCAA95639269F13A24437ABA0419959C1AFD6604DC39137A59865A7E044FCDA114D38BE9D7231415625629397612BCA26774A05BC5C72A258CA2BC579BB0F2ACB700787B4CD8A318E31A48B83618C79A4ACB1C852F350265354B51B9755473597205D52441485326CFA6601554214150A1D2C20902E793376D97C9710255AC45DA5147897FB5C25B5977FFB75107B8072C630AE5B4488316A4734540A462B822B635199ABF7780F1E42BABB2705D7491B175291309C059EC10523B32950A67ABBEB7F37A79CEBF802969532D31654196082890518DB43766B352F9C83BBF74C2BE1C205A5D15D9F8128150356FED68CC5B83C67758D87B6046CF44C13962CA1FB477A856A35B789CEE9757E01ABDA2B15399C4F9AD80711175122309558514D087A576998412A7553D5FB831234078FE3BB83AC0F554162002A11B83998110341910B3013A07FF01616B1F4453A59A0BCEC7948467940512C2DCCBF056749892BC046E33E61687EBA021E64F09B58A056EC170F4D9B7956AC4BBFE99A6A4C56E557C4349C87E557912F879CF11942F268A956A636E8B61FD12058F8D41A868A35083555F89B965691131827C0F3CA2A6999336B823971D237D881A8B1415C2B2396383A2FC036CBF70B18EDB71B3E6C60B1D5AA1C490C99788D9E4B2BFF5857F1615BE961569FA38D71958BFAA9958DF13963543C32F9A53E31ADC433B8509469AA8C8C429047E74C2BD2094836203288B34CED08BA917BA06A14558CC54F62E06121F68B40502417469B93BABAAC39C19C439872975DC155689A41947D526A7B30A1346C3B91F0A272A8B34CBBC127CC4945F5440DC1C97E38276CC754769CA796608AFED1AB483CACC7759495B8739D109ACBA36EDD03696C85C7D219496F84422F3683FE639D3924B93543335F7563C8AA1196932CAD2BC04ED6796DA35CE0AA658B35167492C09428108650194B77C880761C8B94A6CA368287955264723280951CF85AC7E188921AD870AA36A82D62BCF9B78506C065C97C999CB4501A152330A532F2E67B3A89B15D06461DD6CC6E83865CF01AB13B4D890A3D9F573002352BF432A33A035ADE976AF6855A57193CEA9514AA08601AA07CD8560476C2C8E1D8622B36891F8714107C9970B310CCD4078C7090EC90A8035466E1443F59A044747CAD9E27A842893A2AD72F684CB2C7410E929112AAB470A5C86039C4A0FD193BF8E42521A6267557B6F1B7717401BA5AB79436D0508999697EE4590FB569926C15B13C83AB9755E389446290B808054924103B86D4C69DD2AD53285EB9C50BE417815F3B1C0C71A735985FC443796CF5433102A60AC9326F8A1E5F65B4526B225391347BA78D25A010B080C63DDC644FB629EDE699B8E123ACEC10C391A449B886E6997E50382F46C9BA437A36314C39F2B19A345BC9E505B91F9A844A8C6A3AE9C930C09E050202B6447D4B9A3277553055946D259976553468F9CA6235316FEB9C3CE5814B6C48CCBE26A81244225D63C44592ADE0C199EDC18CB3A63323B31389489C4476CDEA081F1A8383CB304067D88DE9083AEBB7822480C5CA1558B250AAE1C6CBAE2482A64834E9F0C47C33AA7E1C39C90264624AA24113A758702C59820011286FB6F0B0C42461A1D070B1C75502EC3A6EE92D1CACA070118D76CC34E3570A3E617B9E4376256104B06A61911856108C1A6AF28EA7A7313A65BD91B1897293275BD93E20B1549A03C50C48A25B0712EA0A9BFFCB6A25855060833032193872D9FD9EF1EC489A46DC5E2F0A66697CDCA86E60EE06B16D0414E821CAED22B0427C650EB0D8349971A6A9B31D59C1512CFDCAB613481B239FD0E65FBFCFFDEA427BB8F9BAEA92394732DCAE11A68957B0A71A11", "DC44FF01668B17FDF5E759C911C44A60F7D4124BEF02F6CAC9D3055207AB607A549FB911A8F8A9034EE3A026AED6DBFE8CABFBA24F9898B415A2FF15FA6A54412F335EF02F27681584584AE9D6AC58264566D750E28B23C0048882CE74F2833F159590CE14F32CCC564CA81C0FC3829ACFD1B22ED3017C3D0C66C4309C2C89F45BAC2E29DE5E351C27D4B16FE3A4BB32791C244ED80AA5903D02097285C5D89C03B998D5DB1721160F25DA76F50DF5907E4C20880575DCD5825ED893F6311204A3049E7B9C9D866719A282545CDA8C7640C26F395E0BBEB26EAECEA1D80BB3C95BEF62C8E3CD05358095C42CFAC2D5DD74C741CDE684752683493AD302E6E3F16890D0ABE0839F9D107599C1789E30AE52AC1EC9E89E077CDB982CAD9280DE02C002740872D56495252FC9B80892BE3BF443597FFB32662BCDE201F704C56B2361E33FD474BCDC438D7D6E80656D19EE068427944C5A8E3F5A42156E80B004B57B5C319C5D6D815A543CE4522BF86D193E42F50429BDE0E55AD5A2914C809825DC4CA1EBD8F295167D7EC8A8652B3331EDD979785CA7053FAF8389B9D6188B62D569C547BEE798AE3AE89FFB0C06238774DBC2E97A2602780C5D7A33C099A91A4DF6FD19D15291F10D7D01675EE249C7A1C991DC352C900705DCCF0AEF2288A9992128525D191AD1D450C7832C913E45E1D2EFE49540E0813AC4D1D75D14631E2A211D3A2D11F0493C6DC1003D02690B583605BC2161AC62D2879AABE900008D287C256465CE3A7AA40A155383F006C786CE4D84B22092D3CD31003F6A7AFA023133B680CCA51B12EF4F49DC2160A6FFBC80F56FE7A5BE0AF9BC0DEB2B17C9F56A4DF1BB544F51F0718D93AEFAAA7D734FCDC8202529C2598B0F0125F1DE7F027DD65B7628A47DDB7619EFEBC1556536E175E956BDCD60559F539F1E9DEECE53F54B2313403E0821C1B61FAA2B2A57F52101A9858911BC4D885BF525048039CF64BD93898D4425DD35B0105BB86635867E2D6F7F219EC5A704839EFE5A477CEF515B8C5F426AB710D1E337C7834B878A7546112E30B8B44839DD7CE9858D350225D36FBC7D1122EE64F5691A754ED3CAD7B9CEC2EF31164D4CD4101097F2787AA6E5C61CE4CA22CE0A969F7641F70903258033897D2407629368DA9825C20CD2A8A6069F0A679DE9F4B8EA9B30DAA1A9EA0EDDCAF7D3BB1FA13FD67429CDF62B140A8106A3B969E96E3120C7B7329FD4D650889AFBD3B89DF961993D63CCD8993073BCD82BF3ED1382989FE43A5AE097744CAB819E3F2DC4450A8961A8491657665B6CA9230B109F6DDA3AACA4C18ADBF032EF3A6F2C7B693062EDEA61040AC3A8B86CC0A76D54F5F42D48378FB4692B2217B33A654CAA983BEFBE870B894494CD496FEF77E1E4796418F65C3CADB970E7BE08F504AF8B915DE8A90398F3A3FC8B184C46EF8C7011B42FDC152FC53DFA98150299171741455328E729F288F41723D4F8B06B07A5F247B3F200AA604B1F672DBEFB5C8D444C6314B2949E6FC403", "22B94A413BE8118640CE786A2107C2AED1461082A1FEBEF610C6C1DA597171AF"), + // modified ciphertext + ("BC4775A9A1393E32C28976CDCFDA0E25F9192FD11BA2175A7B9608B591C63501194FE6606AF76B9F1B22CD2B4D6F86A20FE4039DF0812D78889CA25A31B130A3C9092407448C0318600612CBEB81DDF8A3B760309A5A4C928B0F72F9C8CF530675D46A683261C60108820A9B48751DCBC62526196B02E23892E93A784796B87A99E78C1C348107B68A56EB3874EC63258EAA2B61D70DA2491C0B1C667BF55DD5A373EFD61FBA302281E588F7180B620500D90148E6805BE7E17B05608B2B716017EC87235BA27B0A8BA5F40EE18B0015F9AF1BF3BCD729BD79DC231F603273669A79F5AD09E2A146F2B8782A64FAD55F0E8B6894CBA795F59E9D2211CACA8D39EA6E30DA6C5AD475B2410AA6C73CE0E622A09008ACE68F1E80906B1426B54B723F4B5EC5A9CB831756FC7580B82A149E31485BC84F05B8C37A61813DBC445E56C081BCCBA33147C3A7152378A6AE6A8639029E1D451811E9B5D11C9E80C9026326B9E13066AFBC734FAA8F4F45452B172085A48D9C445C66D6B95B12321D286D4B0B4DF12678E9E973A1FC2BB4BBB579B648766B5E7170424825199AE1A0ADD60C4DA2466302B06A9C4E15B1C423A461F50802E1559D36CC5808D6B2C37A5F5B797B6D24A7D659C7D0A99FD4F4A9B05700AFB90CC830355A5BB73AAB91DBA6CCC5217911725FC5E8CE41D012C3F640093C00649A3373E4CDDA657607833DA75CAB69388D0E40AEE4E8ABBC5750B2701B77BC7DBEA983B42AB0714A8ACD6225A054672CA32938FC967E26282414B06C809827E1C4C3098396BB9DD3DA763E3B9D35DCAD3C4CB20D64C27E15A3203980F754739C091C28E11D0D4CAF6976A81D087B99E84B73A28C52179EA40A8C12C8634E5B8F6DA6CDD8C89A8EA08FB7664BCB9491C90B767138BCFF701EC9CBC4AA91A45B8A13CCC63B6903C8D5E2AEEFB4883FE7A4D5E55E2B2B67A483132A18546EA5986B27C9A34A4404254C2A37744B6914D1194D9AFA4CAF6BB45D3BCA763C88F3E67776A3796E2A9E7D797EC4195F89118C98457B8D94C6ABDB770BE3AB67CB7C0AF38D84BB6EB96C6989221680120074FC3E6126539C93B17411C3000D725354C6A8E582EB76C399D1BE6582B10737BFF083177958130A603DF9B37FF8356C06B1718F4205EB3C6E5DC56F8016900C9A8FCE3092803ACE13EC14B9715A2FB96F2942C95CD4CD6D1A68F16B495826BCF8085620A98B4C10A86D072E68220579AB2783442317581D1FF5CC734164BD08B788514BF171A3446229F949038E814DE557A76359493D193A67B57A5165621A3090BF50C807F68A0F570435A712DB001C8F3078A3041AF3EA64D6901DA5F731D9E3732D34152DD3C4AAEC3B6BA2755CC70C79A906E69860B4D15A5BD62A8D98B41BA39EDDD65618F2327347BA5EA977750A48A85BA249C2B9D080B114A56E6EEC4E9ACB6EF9CAA51B2134B6752847471E23B82385922EFAD11DAEDC882350B3C8E67BD3A8BD7BA052FAE55B91798B89851B1E1B3AEE041EFB66729C508E9CA9C550807EE26619B6C251171C9552D787B57A38491B053C7C72A528BB79C8377ED0B596C57238435ABD1B7458B3B9AA1B70485271E18731E8A3A67ED034DE1B1459E2C251E55C2FA5A0C3842E2BF65C0E6BA46215C0C4F94D6AF8B455AB329C92BF330104993C550BC9149BE0B705407B4D7459E12AA93B337E630597E8D7483FB403E8EC7E3F0AAF19CCCB04C6C29A4A0DB8A9C32AC8729B62CFA90855704383C49C9BD7B0020899A8CA0266E5875EEC2C826A530AF7951ECF280BC33B6158674108D3287D087B656858D22A4063977E4CF04890C18B9AB09E302562E6BAA32F6C07CED990372771D04A13A9F90C8D87B9339C8AA8F47170E5AEB6FBCA56B448DA78122894732F62B5C2E88D53077D91D3C933C741A4C704FA91651CBC81628A3BB2356EC6DA2444F83B2C301AD6CBAE21E0583CB24F05B3BC746805536C3E1F65C1FC0044F2F36230F21232C84A7F2CA325B85AC0C470EA02A6046A4CA22355A4F56715589890C6366DE1972C081E3A7362E380AA4AF3913FD284D8A33AB5CA04886BC7E52B03A6E153C1BB39DE929A5A0339F921BE4BAA63614500EB8497EFA27C69E661BA500D3AC661A135304E425E87B371A3E20D3567929903C8363B8C4F33C4CF1975B99980D60638CAF44800868F508B8B2756AF4572554E49C4C655065CD576E28310E665104AE51109B7B7404018A4A66F49743A1BC182AD32CB6C029860C45FA2292D408934608205124082C4998848517DD8B418A33935AEE37C8A551CDFD1B32F1AA901AC1C616343A93C542BC6CBB147025069491AC14C9BEBC156947D9328A373D2843AB877551A862A8A7A554249668C08CC388333F33BFCA60B4E0ABCEE33260FF3B3D8347D1DCC231A247F88D3C2204B9AA4F429B58015F358727409C53247048B887F213593B37461C7D4A11702B84083400235C54BB79A70E8306CB01B6183649C5684E6E8493CC0AFBA6A90AA9C44D13BC56E71B32B3B7FB965176A96AFC350A39E27BC083064167C5B4D652447815DBB4AC68A7BBF355972F4EA4D0F2099955C40DDBC6AB6C832BAF1C41351CD88E93318CB5926F20ACEFC688DC48247B9027E7800E772CFFCE51EB50C79EE39CAC8784377181BC6A373675C133200484D54230F610799D04A79DA6EC5E82D4EA75A26015E7725392DA31EC1E644D151BD4FC5C808F76DA16A2EB0E7B279227F3000C2A6F3BDCEBC75622B2C684C800848AC55490EDC119756165C5A2560A385781AE34EBEEC3C7718725433B0911AAFC09AB641E5BE03C6C6B5D42485E83D98E79689AC4A7E4A0EA95652583B6DB8465E7A2064528AB62AC555E249CCB5F028682981E3D70EFB64A556462814B094FD5C952E1541BC1CB735AB273B220B0E5A4661E78E02F913BB0875287535BAD77FB82786CF564ED0CB53BBD42249664B1D20122A1851B482B64705165D17804AC43E647728CFE9C87B4A91CCB54700DBB88AF79150C831BC4255EB06763448BA80F0AFD0DA840FE643C59035327A401AF141E2981B0D630A52D633A690B9CC31197AEB78CE5983BFD3044DF438FB983F8FC28C9925C435A5BA796CB776DACCD52995CCC281D007B0D0F1A697E766B4A09327E656C9825CC4D983A0809DC7E000049ABF49ECC337C72255835806CA55E91767D6F860B638030A0A9671FCB7CA8647FAA932A7814127E55268C90E41045A7CF577CE1C5C3D3792B183A5052581419F93CB0B1927C168DED5089B2D0991B923DDD0C6084681A60137CAA6B28ACBB363711316DCE101CCD5C056296502C26BE239B505A01DB7291EFE1F52377053382045516057442B00AA7FB31C1A0AE0718E911821F7044512A8", "38E6D9F5E24CC216630E8B9FCC01FD6E724DD31B55C22C76BC3F4F3A151F6369E313ACEFECC22B11BDEC684C3E7D6DCB7656586CD41C553BD3FB730D17768F89AB9F51A2B18CA3DCED1EA271148F23BB02948F6C38343D33D1DE3E2DDBE507DF7A96D2C6123B9C75E4F96FEA0E8F3A048AB0870ED63E94E5A45DB461D7EABC338F66F0AA5D939B1F1DA50E1172FE01682A29563B2D25B06DA0910F782DA04B7C69BB5796897F48C338044D702B1819C04327B21EB3CE9C2A5533E20DFF5B18EB1310A05CB46CCCDFBDDC0F2E9C348A03F02E01A7425A2A75D97427E570D4C20CE3EC0C27C6228C5076119211E6599A6AA300AD952FE05AC0059A7CDB80A20D6FD91D8D6C1EA05D56098E387E1CABDAD4968AB37D6DC4D26E0CFA953B7EEB6044AA2C52A73F700090D970EC5461E1B4563CDD94D8E79E730AE41A9A1CE7A43E49735450DF09E3111DC2C35560C90F8C98CF3D5235A17679CC7DEDC0BC71933B9F4623436294D06F2F072B31EC9C9B803E8D8D6A7E6D1FC4556B704AEC6A7C82262A1F2A31EAB9DE97416349B7CD7DC34333B8E40E928F94C4DD19EB762CE02A6BEF2D503DB5FC57894E7CF236821274A79262A05527F079D4D0E513FD9A786D826B667C0656681F13EB2599590AD9854C9E5378D8DEFC5D688B861BF5BE6847D475D435F8AC78E3F5A3ED162C6D7534A8FAAF8366C12B095CC1C4724D12DA31B32F229C3CCB328F28E1AC46C48BAE3C9608FEEE472430AA40F9906AEBFEB7650788582B60404586805CE870FBCA22CAD8FF251E6A61384AE1092282D87EEFEFEAFF9DCA2E4B6B1FE7920E860DE8B2232DDA16933323462B1433EC7C2E236B1EB15C0A79DCB264537C42B6818C8D2BFF3AFDA6DEEAD3FD3493AFC799D5A74F2718C15D959819B6A5E8BE3E580EDCEA1A8F96683C7EE318E704F312F9EFC26CE0FDAAA6A389930514A1C97AC0D8DCE8516EC8BCC15E5C6436AB49B819D004CCAA90398AD1C13BB5C2FE9A537A9F6C29692DE939F3C9E87F6732BC85456F3E596D86988B69A4CC813AADF1EC0A2E7D10F60822CC415276A204CD2C1844DDBDFAC8D910C5384A5ED2C8D9B8160A5B74BB3EFCFE7484B4742CBCE861800993D68EB9431F40D495428F9EABE3CC530AB7A9410FCABCD1C9CFAE431E652AF9EE19EAE0F5433DEF0CBA53936E3369BF479A5287B5FD271D0B9F0C294B759DC59F8B0B470D9BF98EB496EE1149ABAC53CA0665D64C16E0069B654EC376C66C456ADC29BF1F6DCAE4AFC020B9783D3F85C48C479E583C477E2218061B9B82EFD0A6A9C9440014B22F9458719191E4BAA1B90C30789974EFCA17C8DD78CB09EDDEE078BFCF5B44B21C7559B9B29A06532D416CF83D3C205E06F0402F89A88E24135948E3A8BDB9DC2E9CD99919CD69B41A51A6211BF2589FD457BE77CA4BCA6C97DA0DCAA1F4DDB886315137212E93877FC8C23E4260A7DFE9679458876026A6044663C42231A1AB5D1BA9DD7BB05E236B80AD48CB9FAE69DA66DDDBBA337090849F4B6916E2", "9652336BB52A7AD8F781E6D8C00E798FEFA7071211D39FC9987779727FD9270C"), + // modified ciphertext + ("2D7A0D99D1B432C255FDC09FE0528D7B83268518626A73443C55CA0F9C39999B4B54544D561C29AA00211857C6EE321E9C0BA69FB8C9FD486EDFE40B1FF9412DFB498FA81FF90420DFEA08F76310A42C2C31E204523CC555545959BAC9BDD64A3121C9835222070AC9A509A5B0FB768AB617402C74B15985D8CBC971B906D9C373886585E4DA9893E88ECEB8B0DB302B9784952BEB1512A1A7AE6850B6FAC8CEDB764D736DC744289600C39E517B44F15F9BB6AD0DBA8EE6237AE63578D8D89BBADB63BFD302AED91408500AA2963B53DA00CF97AA4F578B039AAF72150C9FA214270B5311F341C79BA93DD538E13C693D19CA20C5927C615E5111C78E444D4C024B3B6C8D251C0107217374E9A9CB73917AD94463496884E856B0B74BF3013FDB9848C793764E5003B6FAA116BC173BB63EBDA38DA5F26BC7D899CE8B8CD6D4571F5028132680A81B426933ACE202643F816966552560C600EBF1866A0541424C3BC7DCB00A6C842A2BB6D39770182520CBFABA18CB47ADA4B396518E8E95849A008B789AB4FFFB5B8C7A438AD1AF865ACEB57B5ECE9727329A7737A8502ED145D5F1ABBC5A608161025334BC084205D7219BBFC94E04B2B26509205EF1AACB015D5000BB200983E589654A4417DB107C9A39BAA706AA097A4E55B9A9F2E560402C1545D5B036F01EB3C96719EA0068649D3196A05CE63B0D293C887B97AB9723BADA859C112DDF99C91C090AC1B67F0F301F35919D2B501C4147876A2045025980D30C424B803B9C20364F408A0D4838F35B0AAD04814168B47E381F3AF82644C0B123603A3161CE2A01A157BC262B665FF2E8C7DC48748AE94CCAC1BB0283A9C637A077F19EEA818686A62479B82A63F4C4EB522FE078B9A4A6B10A612831849C51D0C0437C18A151B396A8589899AF69D02AB733CFF1056E664C32A84906C248BFCE69288985BB7DB1BF48255647EA2C3C3AB72FBB0109E75A22F82903CC1FEED348F250CC3178228335933EF2A2CB1284F4E8777F71ABE3E25514EA912A47A888763985A2A1BC962AD330CB39E29D4C620FCA9964828654CB27A4F7950A57EA255BE54AAF22105B629DFCD0076FB21D5FF956805011A1889BE577CF4847C32F6BBF8FD9C3778656E4A0A3A7043C867844B1B221F0925933E698C3B67C7D18C79C2A521613AF07C4A3A37C5E5A260540781DCF4C373D1661EC753A46878A2C61A2F5971182A87786C625AFA66212D81253310C1F398D2546059AC9935F9A933DB6AAA4B402D7A3B9B6388088FA801147AF5E61594AB61465A5CD7B5370A1B019621C8C244481CD452741CB359F196639F1A40D77344AC18B0470648B407FB801205D5A2195232E72115C41237D4995492B77A64A14748F2315306C2DF4787EF9C60A2F0C1DB4FA7E0F3212B6223063691CB96477EAA04019F39DEA2133665CB6D63183A507C2DDD92ED33A3CF095CC7B5A37F144844DDC8BC07A8A8C69388F733CEB659EE3293F245926E6822EA522CC8EBA2788BBAFC962960FFA320A62C45F6B992C009FB2C2CDCFD3758431CC675A7BD0D985D93789E9A74977B434B305BB871A58C007313607CD40E4B1ABD7B2C3CBB512667876215A08F413F0259A9B0360794B6984157D41863C35599B8B13694445AEF8706D43B097E8473EB9AB71551038F306152494CFF6CC771E7437EF013DE1F27071C6775960BDF3E5C189D10605232ABE24071BCA16ED32154E00233C83C78FBB9B1B0C31D53106BFBC50A5E091DAF6436DC320EC8028486223C2EB705334982DA6CD2999B5FCBB9C26E98A20DA272D6B108AD378EC973504EBA7546C3638766F4AE25CA228B81E9B0EF8E35C8BF774408967D8772F6A73BF57F51F2D7C05D0C8C010735C7E0C14EDEB0ADC0A137E94313F8750408A5E6495C9FE84B73064137292891CDC61FE081DF98A406871680E0A5E09519A0C77C5C857BF2A7B8E6C936B08673B213917E28475A934885DD982D5C366DEA94C7792AAD7C316848561503BA7E7EC37D8E31C742B1BD7A5C64FC0791DC669EF1175E29CA91C308DAD0560B3C071F1D16005554FF5DA3E43C17DA89C30C2EC3ED24256706716FF43842160771094A8B31323CF569ADCD98A2F41C2C3F363AB926516078496404429AA5FEDE024FF22489C234AFB422A4841911D9551882519980B379F61099F770FD7230F9F68875163B26522CD134BB9C50225E0C7277BC8A3FD4486D756486FCA6BC5A9A67E35C0BDA8843D0334586CCE210B757F3322EEB28CE6E88BE3075173319FA039C518546D698611CA35977B9C00DD96291AD9CBC85B71C3CB5FA867ADE4507E6AB8C238B3B79C990376A304EF546BBE88A5E5F74FC4465F4C581B92AC48187C084332B2B7614BDD6263F98A33EBC284B0E7CCB6A4CDEED767E45B93B6D1986122264F3CCEF5DCA5359084097313E2C8AE66ECC75E806978F293A1814A6300516AEB25D46026B5F0851403009B0A1FC49C316BBC97371A3475494B779A70C68847CC603F0A476F0C27314C1B8133D03BCC32520C36C0C4B243DB81B684F206FB577BEBD010967012288C605F28231A40B3D2292470D875A340A764923FF6B495C191BBFDC55591B41407137D69978BB4E8BBC1E1173F0161D35ACE60E3408F022C64574FB23368AC333DBA313F414BA99E639793F4B55A775C9AD4998D01B64D9007E42BCEAD0B5E2F3C5B1CC2C9F095382DEB149DFB815C8A253599974C218B90542D586B8D6F051115E88CCA5959A6A41BE0AC0B1CE7636918C5EB63B9BED99D3BF3B9AB1B7358A50B27437B07E01AE62121737B2632632BE27B9C36113D25866B25571DA59BB2DA92C5174814C8A3BA21E36367D630D26156EA935F14C80269E0B90E647DD6E606668CB3042BCFCC169378533E6B79B118D4ADDD9C251DF9C59105A4DC1C4EB3493647FA2902B5389F78B44C2855DAF879262894AE480594F9B58D57802AB99C6DB448A4237C3AC4B859EA299B5514C65615A2F0206A0049524B37EF7182EFB45CE987AC2AA74BE633BC7125C7FB1A0F602802B8B10F3CAB31FA3852737A871ED006840C81F2769B4238B1E2F3A17BFA7BCCD3BB5797BE0FB2337E0343A3479F1EF822905AA1376A68C873805100C70717B37AC28CE4F331C9347F40437776CB5781F833CD236FF2B376C6A0BB20871AD0753395ABA5F4DC755BAABFA09157644724345A14AB4384F14B6207791C82762ED94B472F61C0677B03898371B58CA765F6C1D6DE3B96F38FAFD5E7D0656D1D4F39DDB71676E1E50C745C4E7CE69ED878D9FF0468C5763197DA7CC32A4DC98E1ED5F428EE03B2CDE7806072E4FA38D66BFD23304F321D7B52E41787760D1BFAFCF447640CC723F45286037E08E0715CA4C6ED", "B5B33099285C4CCE37BEB8801A555F1E9179E821606D66336F078C3B43E598DCC61B57DD999DDD0DEC1A39AA894FEE755BF6017D4F94FD9CF22EDE99F0D353F2654904F3E62A55DD861C1DF43D7B6DB3BDD407FC773F6900FD743E440C2CA9B524AF9CE60B846F02700DBDBFF335B06B80085CE1D5643B8A5F9FF24EB2669074AD63E855B102D25E6F97B7485135D2E20E28C0B4E8F8353FEBA7B67F84ABBF4C67366066F047D6625DE7A1BF72F09109F1D57EC206002482D180B0B7E5E388B754C980D9E02D7D46028F214FB5E3E37E5FB74A4F4E81FBC7B14C178EB185358EA087AE471E6FF3ABAF67C9D47F3F575462A9D7F8A9FBE6C32A88D4791D032CAC99BA2AC6021788920F8421767D854D3A8E116401EB14AE9E6A90B773C8C5FB4A3FE77C200813906B0FA472AB834E9DF4B68CDB4FD2A72FD9023424D4FE5C205FA3F5CCBEF5C951C9937AC2220930A3A6EA4B0B8A4416C07D3837C2EB77EEBE8378A186D82089A41FC1BAB0A1D0CCB02EFC23C99008C4BE42ECEEC7348B8F1C32879891223809737E752DA8EF1968C9DCD28FD0ADE1845818AD3B667AB6DA9B06D699516D32FE4D9FE5C94C776C444F8925F3A4CF5F8C1FD86C418481E0457AB02A86335054326FA49F2DE347E2E8AFF25AD7E2FCDFC562A61F7A6EEB5EE9F05F1E5147A152CE06A4F2023D85FA44C7CB5FC4A75374EA5DA42F67A9063C88B5CAB26FC79E5EB5EBBE633567FC61BEB54BBD6EEAAB698F1FB187F3493F25D5243D063AD5B69E42B96F7D754F441B86E7B707E522A43B4BE5EF38AB5DA38F5A851C8BA4238DC0C339310DCA9B5051684AEC5984A65E17D4C49A823995A05CCA199587F75F913A572A3F1FD4B743AB7BF8470B52224DB110F2563396B798B21073292614BF12CBC6DA258265F9488EDA8DF5DCBCA1EC342CA896C6345BA301BC93EDF2FDAB077A06518AC8951DA6C53616A2B991F2ED16CD5D9330B13FEE5425C4B7FF82B6454E38076573DE419977EE02CBB723203B27437E9971833E088EE58935DE7D838099DDA6BFF5AF2F0FB9307A1252621615341D15B0925377CE817A0F6C3238B76AD589C0032A5ABBE9D226D3D298113AFF3B844149227A5F038547F791AB7D4E32F10FE12AB82C622D2C16A1868D804AFEC8600576E9D571A600049BDB140551EA54B73387A48D1E6A5A643D97E32B405789AA14CCA120057EE2ACACD019402DBA9CBF194D7EFE76F4C166134F59ABAACC392EEB6DFB744366EE4C450C327A26B9105AA314B682DBD7C585E595079EF20A48ED3E307522DCC09D5629B182F7D8ED82F8E3F6D3CAE6601D25A657B488017B91FACB3C1597FAC4B97C614CF7EB386152405B9DDBAA7FBA83A5AB5ABA6D8EA531EC26FA157B931C7DA46CC37636FBD7497585DEE830A2476C758D35F8373B03222EE3702228FC231B7B963889BA66F0050B77A0C60A871C6AD084DD05C67FE8AED38A7EB55E060F49FC89C3809FCE97B2A012BC8829C861A3D88DCC3A8D750FB585C5256845CA1F8FB6378F", "99F8AD0CA949479280ACCC3F08B036AEB926E4A74C5188B267A4A7C7D42B0CF1"), + // modified ciphertext + ("F0E83DD90A2966E3C6CA6625FE8392A575B501B760218C3889A4B6285580843085E01732D109B273064361561DA38832EFE42DF982BEAA16191681538D218D11234891C43398F3351E24599F028AF1A507ABA0891A606CC3F987BA0B5F92B0951E322A32F80109365F12A35C8DF2699598B5DC36B5F4191E970589124567BA56A531FA71D37773BBDB89F0F054F5C35B9D62C2C9641962EAAB57AB4FFC91A4C3F715E8DB1FE969B7B2101952D0BA27D303C1AB1A91B12D435C3B0C201A2FE89AA600A260F9BAA0415B45423D08597F2D20316950C6DA11C4E5837AA3D94DA141CBB4959DD5D11C925915B15255151B88A3158318B0CEDEBA0765B17FCB60BCDA4C17BC904FED129058265822BC733252A62796CFB290CFFD55080726678B3675A7EB8E23DC34CAE425F9109832D05DFA68C1EDC13B2EB69F23B23BFF92266CB4B0AB300836747D69D03D45F7CEF1C10F14EC8C927A393600228F4BC2CEE75232D68D4E85084FBA3B3716B110256B22C7318BF3889F015C7D6069958A6E4089A75898AF00F46B9DE6565B241B5E9370DBD4823C3841CC743E9DC1CBCEB71FF20005EA8A23B4BB15566C51E7E67D4407729BE416D013C03F792F26000B71C9849A467361716A00F1628E0B3F85B700C4D53452B09CD4A623AF0BC4A5B192B3B469FCCB3FF68A87A325A25F4B221658CBBDFAA26F9642006A4B25176B278C59EB365159656E32E99D990486661AAF5215A8179400179608B4589D31C3877A25387B645CAA667B1843B2C0F482D158AD6577C5B2E7816311046AC518FC8B8ECD866039078AEEF966120B32608B8D0FD9C058F9926679388B829EE42C17CD8483274064D89304630607437A123AF4BCE25BBEE57325060880818B2C15157D55E8B989565D8D64C80FF0BE18904D842134EDE6CA72A31D50AC6CF205345BB93EB5C806F6F216F8071701B1674A179BE2D9342929706D5944F7FC0E3BE6BF0679A1AADC4F5261A971C61DDEF2B17A31B236EC4649E9A113754F7329BE27151698AA544994BC0B016C7AC65737C24CE815330BFC6D715C2921609923D9B7B6B53DA483C409942246F294E204C918B6353BEC721683236C49B70D66AE1CFC37BE451A7B190DF7579E40515B0043826C7926FA9B5A35676D6F294405B41E5237CE09928C6D70C844C17716C33DA2C4B6C92A219307371626855858761A133370C616024C837A4A26ED6B01A5514EC1AA0BA5A0B8C7986D7C168E8C6160D18229A6A727EB7ABC118CC64C31A605351B972635A6A906393A9282471BF5E3C67B6C8138E93BE276170D844D02E234034352D3086E6AEC30E306B1E3C3A55D2935D02B05D065567ACBB0EE6C7E12A30C22B443DF187310E9C6529C4C5CFC48BBF0358748863A79B3860319C0D9321DA780E5E8AFCCE027DD8777A3A06465B95DA642BE729B12FF63C75DCA6AD253066E9C081808B57E6B1DAA15A3CF37B4FD2B13B684A7A7566F8123A397383905F58C541420AA2833D96233EFE35198CBAA577C693A0963FA2475A73348F15262B9B08B43497613A608AD22B661B3159539355265BC7CD903CBBB841424230E164AAFB538F43582C345179D168BC955B36F93BBB2E9559F0780CB7BBFB6C920B8806ED54252237B3905C07A6EA75797D48DB6310CD6A9448EAB4AA8FC60F78C679F4630AFF35DBBBA1FFF5612DC220BE92C55BC803F75B6B0656080628186ED022229792736CB47F29A4C2E9A4EC3A5583E0B0E629B3BAF0AC2E5F7C0E1529A49B231B006902506C80D7B29B87313F51AB9C7E5C97FC98C3F8A404D559D21DA3AF0E01D88C356284738AD17365EBC997D3B1C515452C3884F27234FB9C351DB1118C6396917F30344BC15D399CCE60C9B8A109D8DD86CB2781673C21F083C1339B4730EB92FA2935AF78C60197233ED955E4190CCB354732D039CB3FBBB0DEBC5BF046A91D7BA67622BEE9A3AB5E3B1CDD64C3C3BC305506A500A51B6B9B249489094677D1A021397610A4171143AD6CCB6769354D5C0ACE964CF552BB4BBCFE4F86AECA44138010A9D0074C9853564E74D46EB88C9FCCAFA24BE5431C0914C377BD4A7A2FA0A44561454925106265283602FBF319AC4284FAD3321DC99C5EC603E388BB60B426733A6C6DEE3463F5071107B061CC01DD3D673C3529495805522BA51EA0B9C4B1271417A06505230579B7BBFA45B1CB6BA127031FBE7BB98A032E6EBBCC1B4C4E19107DF6878D4F35B1D221A600B454230857F42718E2914172151CB435C5C101BC2647FA57390035CC7C4607EECE07DE20811481A2BE2897B6D133E21A2995C2892AF5332AA15116067BBC308234EE74EC1BB6B384895B28092A4DB5FC58A9904C81F0D0C44666817CB622A1DEB709A630B9A778B60E6063B523E9F080B48A0991BA78F28627D4B555AB8BC14EE975A95BA05FE2A76F677BDE402B7BA29BD59CB8AA8F0A485DB15B49CA6AD38A5226689F5EB1A5147344E8427C460674147CA43D439CA73625B0468CF5238029B08C2C543524093829138FA55CBF5C80BEE0635D7FB25B4C5001A8A210DA22BAEE359F4655A8E04502D694AC292C5842A7F2BA07DC27C8DCEC52C2C906349884C976ACBACA833BD6B53E4D8354A0352A2D199F8A57CD5C7B963B62C8E87BB8448527DB820D1335A01BB7DDA871C7B05016B92A0AC544547C1BCBB6A59C9315951017521389AFD2247E89B812E130E9B1794950CD0EF2C3585B93192136EE898A3E4369E515A05D0348AAAD04AFE0032782920F5203C670846F2030D07B68F74C9A5A97C2F16A5C3B9928F60E6BD6E118C8AB9633A6513EE886C18B603BDF62E5A802B58A4ADD29132C3E57A31C1C51C5380C480CF3B057B8840A27F36481E2CCC1004403C6664CB025B094380D9D0060611CB2DF33E35D67C6FD2CEEC16022BA264D4856F7EA8A69A584389DB16FA93AE5AF228EDF565A28358182A2C03E2919CFB5507F95798BB7D12247625283B8C091509646182DBC29C16CFAD045BB1C225EA4850EDA780996A0463CA0F3C9304127288FAF8BB9334CE43C64DCE872B71A938A5447E127630B2D1B409521925B3866E236F8C60B71F96C85067483E73150B50089032A9F998267B7C6AB0FABB3FCB348AB2714E196E6DE31C8A707A3698B26EE016FE6985B848B65AA585C6E5BBAE154681F7B51AB4CBB06B90E5211F79736B41B2502CF87121A915BDD4C145F2CF63C87253D52F574B484D18507AA70840E1772191896E5A536A216AD0172A7C1DF7821AA33697514247BF1C718AC7B9050BAB2844EBFCA3B3DB238DA2A6B96F1AEB46C9047001DB8024923676195CBD98AA02B7CFDA94048E37072B4A70DC622EB8E4D3B1E4C2A5058D379BF69B44B47CF947039476", "AF7272A13BBC90ABCA25AD6F374D039B8C86E67993445A7E45D41989574D7C28322B8D39A7F2FD4866D5E00C8E347D6D54D2D249000E6843E8C7A44001EBBF3224A857B8EDF9875E2EC54064973DCD469C05B2ABC7EDD564984E46DC36959E6220E2369B9229110E051ABC35C5518020D659B1AE482930D0D742E34BBBF9EE1C225481D4EBC1F0E4EFCC88C79A17B436D45ADED652FD1EA9C1F5694EFB753875E056083C5C4A00D4B5CACCF426839114DAC8AD8FAFD3380086B6F5752B45B13715D70E33DB7F293B17B48FDAB58C07CDA4B1F3C4FB6E1157B932D21CE8CE592E2A5306F5D432BC37637899E3CE08D3D87C9A5A4CD1A7D457529C2C9E0D66627CEE04DB037C7EE48B5A90A22855C5CE8C1A1641578B59E7FF4890276D19639DE8BA67915E0AA4CC6C07645F5F61867259357767B726D117EB8C05735108FC329FC687AB74E7D893A84A3B49CC211FB2E373A900E198BBF43442E68D3C4E90391D8F646D0F46309FA1D6F908275076A815A4BA4517AEFC3D11EBC1216E5F981FAEBBE1AFB4199F7619BE5E646E85D64A2D3D56C51F475D9B45012FA7EBF0A1035583817B7DAEEA41F862563F1BDF4B9B08B202145E17F1C113701099C64FFBF9C09436791929B3CE568C2B994567DB27353B05111243DA88FCA27CDC03711EA86D91B43E043490D8056C7E69888CA1FC2AC8010F035DA9B64D6B9DA6DDF94E15F151B68C4D192F3F39E86C573C32E1BA96F2C403A26712AA094C5A4F1F4E766DD4E37D1953FE970270536D1043758D37C31455325DCCEDAB1D98708C21B8A6949E72AC6E82DFD3E8C9805666C0F62DF84AC5D23E55BB2BC6947F10586D1E44EBD0C434AD3BC7188DD9B256A48D774F5FE1DF2AFB379B36F2F4CFF45C323BADCAA36D634C1FAF43A2EF1D698BE57284AF702C4684BE88754CFFD3BD14320D9E8A68F26BFB28287F1484C8557387D80F0BB03E8D088D8E10693DF74B7F0965F51EF85862383FC1E60F6FBC3EEDEE99AEC22FE2852E3858A03ED5D5FE7032FBB7925F9D84FB8793DCAAA6209A551409DC414B3E34A99AFF5BFF8D12911722AF96435710A7803FA56E475750219213D3BE864F03F3B9032F3C4B23E40AF96F420481DB1EF11892B60382655B810DB6041D0152A08CDF0B3FA009E0DEF4777A423502697F43CAEC506F0C5A07FF7F3633A50BB7A70177DDC4FF8627B00F4665717F0B4789212CECFBEB960E3D2A39794048504CEC9502B429C6901C115404FFEB22F3BE50ECE3EC8FA8BE935177996C605DE58C31297F1EC73182F44E1C1453F635979C1D05A6C7D84861C673FF817AB4D64A6C021030C536DB278D9AC8F7831322183C941438B4375A5033169A087B6485CB3ABE85D8B0259C667BA28721928E77D91775BA3B1D534DDCC038FBDEBF559374330667887A838D141AAE9975FB5BD4D39874D6A21A3BB9E2EBB83F8BF590C9DBE913730D8E83D6FD92D3817976631DCA0F6E0E0791EF803AFA30D8C2407017DF85E9256CA171DA3E37EBA869D9592FB42B", "C3CDD63DC36E0312D22C5C7CF6667D2F7CED36F73497D9905B8BA75BB79BE4A4"), + // modified ciphertext + ("5FE5453ED8CBA2A17C2D32836F1B1E9DE665C32ACBE6AC0D58A543535C71056676E031B6EDDAB19CFAAB135346DE73AAECA10BB1299276611F66EB4A16733BA7004E0B24A269C00A6FC4BA4AD1BB393705F69888881B75A5024D45ECC69F50695D641392AB6207839D506C05D5D92646D34AA0AA3D9C49363D20873F8678C5CA77C4500DC9182100D1B04B80CB6ED15DF6752AD68CBB1FCC9456513484941E4E5732F367A5301A98E9772B09A65529F5CE4891BB86F3AB45D623F02A4EBAF74D4576389C31580224374DE79F5F260ED323793E54786AC06E0BD1CF7CC25B1856C04F54B99ADA9B138C53AE84184C383F7ED952C20469C6C4B1758AB15BA42082B7100B296A0167BAC9CA94CE454B0A646E487CC5D244883F7C50DEF18E44A02095B28DD80BBB13E950C0D20EEC821A21FC9B94608CE2C615E222269A6BAD1A09ABD0B70B4592BF052837BC56BE0E7348EFB01575D29F30746F8F496924A198376AB4977491945A539626A4B1880352D88565737FA42310BB645B9C184A3F1C58CA1C6D61223422DBBFA44180E30B841FC9BA11C92275BCB5641B77AA5290B8C288186989909958A3E058CB8BC8D3592F39B260CC382A2DC02C2EB17201CA01C3433A15A47C41527687954CB742BFA2C1B542295320ABC004D74EA43C35745AAFE0268CB5771A51156AB9E153FC905FF8B1C8C74C546EA72347E1A8DA6475F287AD4B5396C2AAB9623C7B60EC53523270809C9870A07D2EAB9262C330F0B551F56A992C1164BFE098059366D2A7C80603CBB3B0130169BAF8C5670206092713C380E33AB6E1BDEB847FE6C216E5BC35670333298194659989F2216B5C2781FB77829AB6B0E5833F6078139A37269AD754D8F97C8BF59F8020C32CB92B7690236F032585342DB542CF63C95BCBC95F0060901CF288E8A90668474ECAFC8BA92A4D70F96E007D800D4ACF6B9AC93A859CEBBBB0D9544B17254901439D54CA77AF7C0A1F76995599021F9812D20852D2A450DD6126F4A0C427E67A8492BEA2AB8814B0393D382ADE903067D4BB90527A07D31CCC30A0B8D4AFB8BC6E55A512345B637DABB8F18B6665E0AE8D23A7CF70110B4B967B691F9DD86758E348AC615E4F33112EB5A173F21430508CE4A00ABCE6B1ADC1C062F11D6B49C20EBA51336531A00B840CD3B1B9741CCE083E0B515F0ED24EF466AE365228F2D854B3F5C579F7C9F407AE94A62504B41B169CC4B3F7B9CCE79E7D625A7DE9025A6041877C9AA1D81F38AA44ABC11469441DB7B6B61E180E3ADCC21D561E2866011E0031F6D26F17A31FA211236AF3AC19A97B71119E7A27577D18B35A975A46DA5AFB1BA40940C58D72A250648B7F537E22101241E0B78BB01F3181C5DA765C75036A8379CD99283F4FB82236EC846C7B11526C09574A9E7E24BC4006A52A635975B6712E7428EC08B6F115352FA42CEC02617274C0C9E06D16EA7E3C294A1AFC68EBDB808AC7942FC7958CB57A5076B1DDE56080B63CCA8BC8E2341698EA83FDAA3394F86AA06BA48A0A697013A59954B0DC83CE28C02A74AC11430C17CA00AF9A7946DB75C14902B3E5611FC2D99755DA4C1F4C3EA9FA6B9983C3C9325CE119AC713068F5F3A025078558469CDB552DE834B48F23661B68A463769EA7509F4BA9C51BEB605D915970C79AF9D19DF84B9090165257495E717541D5D695120B3E42000DB47831C94AA5F485C0EBCB9E583B6181F3CA07904C54911D680701F85947443784E1B67F0568042636B81286781F10A892D60FC25B8E656483CE62524918678ED29CAD3703767851E8F43469C8B4A3F07E61B6A44C418ACA1183DF861901646AC6BAB3BD607847F5A5C2B76A0512750B0908ABB986B2388711542C663B2BA565B3D6D34082B85B9E683583B195865A9889C0110AAA00B87336A767AC81D70E0B422619FB95EE05736CB53A25E43300A953F59CA49AD61C6CC423CA69648D6C18A2B30B5C8432D2E92D403B71D7865EBB3529A4F0641BA817C43C4C90FC18C832699C896FA41497CAB46604643E31979CA75487E1EB0428AB0BE5C09428D3A0F1183064BC1139D163197BB297F3C5DC83CF7F7A577D759D336233E4982BC8029936B34E73F274FD244032935FE5C6B75D99CD244742961663F10B1C12C48428D53E83DB69E1595B72861093492D133B269DE7AD7ECBC6BCF55C67F919ED65CB6D83A239554DE452C65FD472144A5D6544C95429BD4425032BA901B4305FBFE50C645663D3C24471503B32E44A0B0AAFE91C41F6F03A4E04A57BD66DC9160FB6158823BA5B35EA06C6659728670366175E2846C75FD3A547C711E545534A056AE5BC6F5E23BE89D4C69E288C6218800DB4BF47F57082B27A0B07A88B976780C965661B8362672185A94E3031970E973AA8E728E2D3175CE10BD9328ED280B7BE66CB8071A57DC12532721F4C4627930A923B7C9AA169C7C3F63660C18C543B5177166646773F5F464FC8CCA6E0DA2B8B3327687ABDD73940CAD24AA4430D39C7457FD32F789B6C77A3BD94878B0183BB85DC71E7E99CB4E99D4E19433831343F2C9451A74A45FB7366CC7A27A136FAEA49CA676D6802C3C445AB2361469F5431568593B26A56B643BFDAFA6A1EA6B8D7F36637EB2E72C32C58BC8F88B04F3AE8480B971798F731AA3AA74122277AF876B8B51FC823589E73CC3DC2AD3154205228107A42AC0033BB8B794E96A464453CA2683B38E27333425827A4B81367053A25560C4EF41A0879C00CCA2709425FC0B89DFF5223BBCACF2ADB74ABD115ECD214F251730F8BA244E23542ACA707C22ADB4AAABD39A2ADA713095A3E9CFB834F780200909573994E583359A9469130B389681B8A5E7790A2D346AF485986E8B7C5C9B0FC21CAC735A0DDF82D140C24017187982291257C4B993367C07133AFDB9BAEB39664122F4CDCABBC9475F8A77AE522BE8EE639D66A6E2528700882652BF661E510C2C4C31384B556A8F3BACDA57707419299A8BC0DB76D737826CF9BCDB24BB63778389C9897C55235C2136AB8D31CF102990E11063E11036CAC27F11B633A7017701C6163D43A6837A5104A6BE104C6EE28CC6DA72AE318745EF3C4B64289E68CB7AF0C177E9C6D814096FB5416E9EC3B2ABB44141361AF6AB0FA73A521C289721766BB802AE411BDD53B0B94633B998C3E5761C11048A9F2416D51278154FBAE191C8D993C8CC48704B1A380132B0E86DC043FEB6470A45F67E7C19585018E43A1E1C80B6C3C9231BA6BA3DAB2B2EA0E8D5E194BF65B2CDA35C7226B7644FEA747BB4330C2DE4AB95D4730A2203C5F466F50F7038948608BAB6818D712E4F1C858394471CE53E6A718AD5D839F8B22AA8FDE7303FFE3A02EC7789E62121B10FD30", "9FD50E7D85B73CFE4F5D945CEC7E04B09415BF39204179EB8FE46EDC221EABCE1B9DFA89668BDE0974EB23FD4FEDAB0CA3D67862BA9C50FA957FAB872F42BF2C538A04BC6C420BE8133A521006721F6C051B4AB58A407A0FDEB826A643804934E3CB5D770A9804843CAA6D6AC371216A0EEF95AAB97E690F98E1964AB1E2BFB94228CA3C8F1AF91ABF6E57391ACC15418947DE7F8E23E3C789001740FDF2AEAF4CC34CA2940E118617FD5F9685AD89C1DBAEA3F8457D6A5567B752A67E39AF603D7327605E26CBC9CB1D7915504B459C3F22ADC014CA19DA5B8BA5909983EFE8238E28FFD37A074065357A1DB649ABAA9C48B4E33CD87C33F8725AECDA8C828A68AB823D2D34121774F2B9A691502739DB30F29D6324431E44C866BEFB44760A142C9C8945D3C0FD14470A16CE659370391B92B853AA2A2C8B24A8ED8D41CF8F3611C3AACD2F30FFAE25DBFF4A2FA63DC067ACDDD9FC472D873BE44F80D5EAFE9417ADFB3F585B2DDC6BB5A766D6B4D28AF9692614E6C92F3FF7CBFE51D752A3255513BC729825A4F3EC417E4B7F7E0B4FC6E527BC4D67920368D7328D49AF05FD8A632A91A891815B04FAB1E22EB97EF4686E3B39B4646B7DAB4D6B661FDDFB6FF5D4CB428B77757EA79141A05DF2E8943FCA6B7935FD26351E827D1E6C7551AE4AC04E3F99C4ABF5D36F0AA134E06243F7416C667BFF00543A1E2DB17EB9ECDA705A49A4B4C6D272274612D3AF4CAB32B3A59F88EEF845A191CAA53042E00A6C58F856BDD7C1F65841226D005DFC7683CE4866814295769A6B37BB7141308E183A30A99CE95979437B47022056EE6E96424548A05242CD38A9C4676BE085AA265968DA4246B0B964A011615D06480EEFF7B3436DA24B5754859A85EBCB888AFFA8A0F8E514D89D6AB9C2C02AB502C39AFE430D85FE52865E6E6E4D65041CE5F3B60EC4FE7E0A47D05735922DF8DAAD7FD44299BB85ED6B9E8B40918F3360AD741CAB6F7F425D1BD1D6C905E1E968031633C563E52A587973A7729036BF462986CC3E4D31269FAA1907643034E703B62A45B31E6687D3EF0571EA522C2E43449901FA24C2AFBD50E4E350F01A65A6D5EDB7D3C83B1258E8E8FFD63C068BD73DF68BBB16FB1700530EC982247A033595A3B87469EDF357CF4349F02364CEEE63A94C3AE78BBF31BFD57B74FCC2DDC20D230567E4ABE91EC864021D4D6B62D976C6CABBDB92F0B310C5BF00AC1F7CF260EEE566687740DD8E85D271D0B21E5F0AE3E71EE00B1CEB5A63A29AB42F75A25096CEE78067636136701CDFE230C0A61FB2687F3556B53445B2D63F712C7584606336B5CD87793D8D769F6CD39CC7C1D3A86E7F7F71B8DE346DB46B1644EB380DBAEAA3491F6B60DCE302B2F8BA1C0DB608BF6C0E065B22627966EB3794BFCDEC83FB3E8B1FBE164284D32EBA7402853079A8BC0BF6EE3AF42BFCC00B6DFC70C4894EA265D31AA49F80FE9502BA3C37D972AFB8F50E0AE83FA76455507B4F5F0DDEE0AD5F363E4CD161CB7C79EF7858FC", "6DB14F5FE746946F068C779CB745A6AC15F1037D9A85BBE7DC9A66DFF620FEFF"), + // modified ciphertext + ("17643D9422A420CB6561F5A2FCF64B86B39A5AB353AA6A2799F16011528B7FD02432B33CF41602AE297338EB45E6B91E003DCE3041C3F380BFA4224C6D0B14B73C4178A1969F9B6DF2765E0389387566910568B561E7151AA12D3C8B6BA37C8A77A4BDDE28A4DA197CD199AAEC07AE2890A5EDD9694D6C3737620BA52B1E13494E356187AB9029AD25CD4F16CA49882E48DC54C3F355C99569004B53AE7882395324CF1C34CAB02B253B3280D9C0F3AB3EE2627618704C781C4735A780745C1B9097B6A4075DA0A98D18B0244D459DE8A2223B75993AA6530AF4570525623E339698427EA1DB625FC9658A72C6D2A57D0A4018199C84305BBC80C72DB3C3606E7856098B96A5E6358AD19CC232968D8468A5BBC1160ACE74D28FAF0985485232CD14ADCCB2976FE5C38C21C26AF51EE8E13AE4C7764505C282552684D40D1D30163B126EF79C29B822A37175C3D8142122C97AC2AA084F6225F1A839F4B799631946FC08B609C1C57D678B59C8BA24BAAE56113857832430E0CD372543536B40DBDB4721161A91E53E4A35072BB3CC55254C611002A874472A549DD6D77CB7F1A566DA09F75C3D16BC374B2A1F82D12DA2A7B08506C2D6E6912B13C36AE9692D93776FF57122409AD1C1AC1624A5B633750502417F5C8670B8BE798548E4201F4A7BAA55DC0F52B564AE2342A2091E2E35B3A01532336A4A46E49E498C7D5D2B0CF047263ECCC31118CEBC743C7B582BB93242ABC7A15C2A88FCF3722A9612430335B993B8DE99865B6A67F430CE64F77C352A87E1237640C51D8DD9ABD1035A9542043F63AE89C094E3DC9F37A498169137B5A6A7B4083FD0C4033359500798595C703599F4BC442A1221370ADC8BC03C94744B1B1366EA14D8E41CF01C78F69BB987547912047F668B00D35001FAB3091B239CBEE18260B55AA6B1AD3C84A6610B8ADA683256353F67E93884D253EF04870554BB70508A3AC642CBDC1DC16838B457B3EA2CCEEB9332291605049039BDE507193723BE731B428574B7089BCFF047B708213D8BA676E790169492C7A57C0796544F4CC779043848F7C881C6517ED0A37BBA8B3983A76DB56D1164A0BD846CBA551A4C8032E0D35D6D6BB8BE706893551528E0B2F86A4C7B3354F6FC52EB0130928BC5ADE0113605AE9F36BDFC9990B1302E09B44B592B467FE24672E46F5E9B472030156BDB21533C559B161689C979D0D4C5C039447F628537073CF17B8AFDC44735A4C553466F205BB528386E5C0A9ECE76933E665D7A517EB1D5637B949FAE965499293791A63B6EAACFC5D63BDCB0C21C3ACB605848019ACFB1F95B783803776B00F143ABEBE2ADCE561E517AA34197A7B7F88A6A1061E50178C1921A296039C9DB3397807C74C681845999D2073BB8F17F86C16980542282E86148DC6FAC842E9722CBF2BC7182C7B70152856C52B09C748ECED14F22625F52475ED0C559E7688A944372DA013D08A23B99B534BEFC21A72758EA67757576CDD5690873D5AFA388C60A7B635542A02852324318CA92CB899F98BB2AD09034D396F7B9A64B76CA7D86118728085F1A60F2E273EE1401518500FA46068735B7B6F1C075C89A0446A04BA169047BAD23C68F30585EEFD895BB6C1305B325993B69368B7F4B1151E9490210EC18E8090E47F171523A6EDD21809AF6ACBF899CC63A49673B45DF90681414C904B0280773CF0CEB3FDB206BD8CB0CC864A3B8F6C69F3930BE71868028A738588700535663E34642E11176CBBAB5D71BED896683F31A4A849700BC97BBB47540D54439504370E205ECABAD4A151B1AE05428E91FE7C2805A374ADD1C4EF642683FD2ABA5D82E83192BE43A184DB41F9FB8B62C0C90FF0502FC37891F13C6C2752EC3F9636DD6879BF0A6ADFAAA8DB66B0C248FD4A73B51760444F67CF5140EA1405600A119FC086966592660E465B4886A12B03F5749799E31906A6ACCF7F964D1160E1D3B64F55413E4E04251DBB295245BF671442B46C3BCB150F242B545F279624B4C14990EC30A79EF2A815A261E049193B317BEA986675510B6609BB127C0457FF0CC56DC85BE863634851D30D5CB1550116315450D7A82E5248C0CA1B68B1B17793903F67B5F86F83374A2CFD3DCBEE3D1781C7480F7697A7FFC8BF813CB7D2933CEC6CA075C6CAAAC19D0C83324A034E8F94A37E99FDAB7918A8587AAB8AAFC75C5C0147B088407ED82CE482C1AF36911BC701122E3B6D5EB2F4BD806295ABB17E208B2C7218D5B624D39A570BA30B34678B0E5C97247B2C0C18D377174B56977D5D7247F4A8936F9A07DE2553111C0CCFA07B512B0E0A572559AB5CDFC95C99949B6240A118AB9F6BC540B427C8E5A19A95AB1A0DAABA1D63581802F41110FDA1A675705CB1A43790BD6593EC331EE54A303DCB4C82A28B32421D06C453FBC346A97B0A7482F01DBBADB136FB1DB7B8A5B7837A2C988B9A534484BCB5CC3814A438C432E88788C47E40E88735FC40B542631189B3BB8C08C052511AD2D9034D4CB6A0C352590680D0BC70EC4051BE0B25B45D4C8CD645AF6F99A90E780E574544D1AB1A3E3652606790EFA36880C983F864B954179172B90F7BCCB79EB59EA25A4A51B95950745A3CC6BE515CE1174458E8BA64769275283B63842781645187125737AF1CC01B58267B128E2C93E4E53C5F0FBCD6EA8398F287CF2708FB86A0566B118D64984CBC4AB62580DBB9CC3866360B0153F6D135648529C28428A3C297170E32AA0211E6B55734D45037CA7194EBBBF5476A6978488B617887FA2974DB459B1D868B6D4AC21B1ACB0299FACAAC10D76361363082FEB5DA0261886B59801CB324341113942625613A343B4475F11AE2C7C2CAC4739F1F423C6992A5B24B5481B2D8DC72FC4625851D69C249255B6E72D2B141FC1183D051784E6CC5DB627CE0B542060978F3A3083B0E25CDEBC4E77FC549F80162979C83946AD92000E0E531FE5784AD32C75BA23C941E60B81A52488C0AF99C6BA9EA04ECA1BCF017731FB54C0A125B71C20C04156472E697F85D406B9E95374B68156B22935F31AB91585DCF85D18B4C49ADAC4D44015BCF54CBFCC7A8BA93CE7285744DCC57B22303242243FB4247A3113E3DB1343226D914B017341C983516292487EC05760E4FA758D7373AC9937E966348D5309DC078C824382246BC3A81A23AF9A59A0482DD7865CFD73BAB6239F67B4641ECC42E9375CABA5B228F87D610AC1FF9B2F96CA78A84167FAF7898276110D3ED9DFA9C6E07D2A99226F735B798A734225D807F831FE53CE823C0E68D78A7F6DA83D16A655753E24DACB617C25D329B6716FDAC6A334A7F542BDCC7893905DB3D5C01054AFA79EAF44951CFBB6F8F4F68489BF99474A5434", "DBC1AE2567D1980DAB86D677692C088D90626920E556C5C7B79A91899600A6C931915A3908579A3E491DF61668012C34F8CD08D999F86A8D60D4DBB5E01D2B5971CC3EFB60DD33EA13A04C827C8D9E19093DB5A267596579F031930F2EFFAF400F33F37C6357BFB55FAD4D144A233DEAF34ED99635FEAABC0249D7FE6CF7038F434D6C06CD8D3603DAC6C39EDF4798699CE8966B6AC7E8E93A6B04966B543D326782DC4854B59E1D5F39A7CA7FB8BDF297B4F4CB96244A1B0A2DF353C79626868DBC3C310C8C406D1667707FB21C6A3A537EC39EA63E430AC72A48B36C3DB460463E28A5487DC6B68A88119DFAA7FF0E62C01B16D4AEDF3B7622E7A212797545AD1F521DA4FEEBC0DFE5F118466A7F40C32416D2CFE143CB4674298E965F3E104885D5E260F41812E06AA44E6CD6AEB415D5EE5DDBCA1FBF0A78ACFB47C826016A124645395A432F8A2903C48C31BA394C9D2F20197551A96D7794324811CC1A696A00CC11B811837A12D058205690CBD7B64D625573CB004324FC0BCF2AB2804AAF34D4971BD0016282111D480FA545DA30D8A185D8FED891F6A42A66190FD3AF48DAB05531E7E9DA4C5EE03B30CFC96114DA679529459C4854C5E06FC0DE1953181293A9E6E8F3ABE56C58C31BF930237D72F25640714AB06E93A061370C5D2B8A7656C00CEFBDE1B4C3D934F2A57B6BBD78EBA66D6AD73C4448CC4A8DB51CB6DD4C8E1FCDD667DB9D9A16AF69F870B32DEAA611E94BF3CD59E82CE66DE287EAFEA4799D178A6D6BE1C7061EB5DCD93E34195601CB0F48E8C7163F0B3096E5D8FE0397F84BB9E8E849D3E34DC8599514107EDB43D25C69AB8F61431053DC326DF5E9C776F67F1A7B101179CD1B450D0108EB7AAC7BFB95597614D83034CB66B5D878F7200C7158046870B79F5F41024A5654DC5D51011F6C176BE6EAD856404530EF43AC5506A1269F5A3E934DD5F045E60325252DD8BFAE7F10D15B4A3DB919DD493083315E958D3B7ED09904B2B587167E889498025E910A6808DB76FF5A4B6CDDA73D95B4DAE32AF8144D27E1DFF1523A90D48B1CD81D0585B77C1E9723B424F7A380D95046E266DB8A1EBA08F15FD158758852F2BA7A5FFC096106735FF3BCF1E8D8C99049DEEBCE5739BBD3CB28D19DF136AF4C6F38FFB572F72D8B67D969D13889C5F6BAF2E64BAE7D5B7BE2B567335ABE0C6F55C285A6B78BCD2385448F469BF1C6F03124B67434FF2E929175B7D195917A9ED3B8A8806BAD9A6B522A40C1BC3B4F454E3B00A7E745BBB8FF6D191C6EB59CD3BE0FF5944B0E7F3BB3543674699FA90779D089190A482CD50D4B72B9C9B2B43A8CA18DE8C61E23A5E637B61281A333B54CD7A2DE2A249A998D36BDB6F1F85E49BBCEAD253D4447398349706DA1AA4D0FDFE55A283EFBDCCFFBA8A502F9FE5CB3B16176B3B3CA1B5825325809C6CB60A178CB703FF9ABC6BD2A7FC95514A7693AA32F039436BAB30BE90F289D61579B38FB0005632850419AC6E31709C0E8AE6C8EF1905AB5B2A4D22D", "F624D36A15FCA03A832E0C39D0A0019C1CF08A53516080038A086E83F75E8C5D"), + ]; + for (dk, c, k) in cases { + let dk: [u8; 2400] = unhex(dk).try_into().unwrap(); + let c: [u8; 1088] = unhex(c).try_into().unwrap(); + let ss = mlkem768_decaps(&c, &dk); + assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); + } +} + +#[test] +fn kat_mlkem768_rejects_malformed_ek() { + let mut ek: [u8; 1184] = unhex("28C793778741B80B02B4339F2AA4347255B099F17264E1B8CC0A2C7C2A1A79F7997B907FD0496C6E6C8AD7714F5F339D75F11F625591A869BE1175AE47F05FD4313468232BA6957D7807B824F445AC99A0D568AB1AD54DCA8249D1482E61275F52248C77F61A4248753188CD1794CD0A465EC0DC4B025985C461B74E76286E4C37E77405695CC9FD0654374B427A20343AEC0FF1A187768273BFC4905472A1DA387F14559D6CE87313F6A5B6138434539F9A13684055B177E543F8B40F432ABD7CC49989A50A9084C660913F45A8593B17499BC4CF936C2BC1851421CB986808A0EF30AFE97AAB5B8B8EB3F0B3506A95B91563A0E57DB7231044987EF141BDAB3537C316AD16F17805A81F29329879A94E96157E4B7447F7D59603B21BD896CC47B7CD4E232322EB9C5D2215696BCFFCA3A04EFCC4C5D9CC39AC9A6E8700D38C244B0169E7FA1FE81B4B10365E74E6A1F7F756D11ACDC84043F81006D62995376C22535958FEB53F78117EE0F61C4C862640D06DC57A2B8BE62A41A642AF3BC63F6BAC98BBBBFF70570F37B8F8D9572F2735657A6C98F96CAF57A849868720B2640B8BB2732237A1F984C18872D10289CE43C952C9257E06529AEB76AFD127B17596FD25C5216C9CABD9B18EFC50E87BBB04568BB7D5C4E9288C006483AF5912E19108573700BD10CD77224B80659EA75AA74270B33AC4008B738BFEE271E78658C8742FF13C96AD0781A03C7576CA26DD58B52980BA58C0505E446AFA140CDCEA0490DB1F9B18815D4314B2459CACC562441C91F4084E5426C88E632CF7482E79907911D06473260835D7B85E7856A829AEA0381707B939CE86882CC09C4448C6AE94A9C303107C5667EEFB8DF7763CC21189A3C590C40AA51F491503A7935EC08F4FC300CBE607ED8C9100C29FBF45584B13C8D780069337AEC76C36CEB70373E2AB6E7B934B466F53FB32EAF040055496B8540E23A2A277E534468608D5EC0F8D38CEA5BBB806C1BF4F164F6AC826FE733F95461E29DCC11200C0AADA1B8332023EAB329718CE25CC0A09555903F3578BBC863B1752CA94365DA556DF54C3B7E05CBB7115FBC1B6C57A172C31B9906560C8FB54F3C563A2256CC073243B8179B4A28D60E086CF51082EE429272996F0AABE03BA0EAFD3C8E7D954BD0933E2F60ED0C32CEDE7B820A28E48F3CA3C40913CCCAE2337ABFC59843F08C9863325D65A4E9E15C1F46172B118B2B5EB0F1D5158A00134F27B085488C3A0621FE4E5678698250FB74EE5152E3E35A66544A05D279EA99131FBC15165060B90F88EEB7B20892A4DE4CB1683495BD7DA037966B47CC040F1764C5DEB06B5499D4267391CEBBB47F734D8539E39528436A1858182854BF20B1F93279AFB706464C65CCC5AE099B37CC03556C26ABF4C3F8B9BA3A936707211A49A59B268F5284F7970C77612719450377417428C4BA47C9CA115CF95304C4759C5D8859B44985C06A6C924689237BA320D610960D61C53E85431789E67A40113F167FF93429C264F6CABC95448C903437D39A6577BE0CF0012852AA476351A9046A110A1A625A3D74C910B78BCE9CFCA735E4F91B8A4C57DBE489E849446098AACF73070AEE638FCC8896473D3C159D3AFB4B687B40DFBF371A9C2644B605187B71A14BC4C8678FE8247").try_into().unwrap(); + ek[0] = 0xFF; + ek[1] |= 0x0F; + let m = [0u8; 32]; + assert!( + matches!(mlkem768_encaps(&ek, &m), Err(Error::InvalidEncapsulationKey)), + "비정규 ek 미거부" + ); +} + +#[test] +fn kat_mlkem1024_keygen() { + // (d, z, ek, dk) + let cases: &[(&str, &str, &str, &str)] = &[ + ("F3A706FAF090C03DB506863AB0B20BD8A1627956318E88C67EB875E8E7266009", "35D2BC43DD1CC879F765BF2A0C5E297889DDE910E57E2BB0EAE417B90AB7A275", "8D0923CA8A2DA2B4146EC25321122B8A5AA8AFE0C03415273008A46EE83031E98AAAA125ABC75D3B30322560C197E75DD0E48A348099F7B2144D7B8A8660A4A97BCF19C0583BD9BB2123033CD7BB5A14B08B817831A673A28170F5F6443C0551913A327CBA18C3A053C4040250403B70AB9588832403AA0FC37665E04980FE1602E7D2715D9CBC00515DF432A4F5B32B3BC92AE3F31700166D498123E94576509B712B18491B1435EE7AB7AEB1AD30D72348C3CC083ABE24A8B12097BF32F792476288EECC3BF630ADCDAC6ACA7950D9839501A448500742BAE37F109203A809B2B960A307E25347A32C3EAB79288173A878789B296E9E8C1C28C5BB3AC472601C9765F7B77225A810C7B85370BEF4A5B079D2015ADA54236B8F33840675F9B2EB427A1B5974CD5C61B24010886C5A7BDA5BBED974AF7217F3338AD719CB308A8BCB1B6D6ED2A1643736C29095E8A8452A3A36C7BB5AE58CBFDC61529466A90F454ED6895B0861083DD1371999B2F559A3A487CF59A074FB49215EA6A6BE656F9AF17B121A2447CB7985590E9738842B899BA57AC311810AE2D9794F37483DD6BCCB64AF6D56588AE94665961C025C3AA2861974C236BCA4BD8FF5509F7AB774593E7C5549E57C2F18D15C0515094AD9A0DFAA0601E524F8231156B627BB25A0DAE04DACD0A66C041CEF400583FC13BAE640291A39A5C5CA8BCA1AD5C683CDD8290891A76940817DD8C9F52678780548E37A05806600801426DBB950C3B2BA34E24CC77864DD91B39F1408C716A69DF63342854E50A245FB50977B9410DED2C93F86B1F9D5A78B87BF81E51CA620A7E8566B19AB700964A40E3266415228D432156E5CBDF52364A90483A55C39B3FB16FC7465A3F8AC801B70B9FB28B583444BA5C1A73722D417A9D6D9B7DEB08BC6B330FF27CF61AB8831E27758C64AF3B12150CB7B33ABC29858106D63686D8762459ABF9413850AE53ED6313F76F83D0FB8AB34374E7DF693E4A1B3E5A8AD0CE820AFE1CF401ACDE650A8101B0946022D52178E19613C42B88B07CC04EAA81DFB28AC9DC076236B67219A30F8F945DD57BD2F335C52D59372308D38993467DB53DA3382B74867B616481BD0091A2232C1116DC88A589DB9107224A681008C67C589186A6929549BEEF92253DB02B0C8AA9F9C875A670266C72BCBDB4F5625043703C1A0457395832E4C335180462ED2220C59E7361903C107D85457F6CD82EB820D0855D97675C2E0151CDB73C2885DDB7849D74541580124E890116A65BC068093B57914E20C937C60A3EB25576F1A976A9583839B672144CD4A45C3477A45C29B4E0BC2BDBD206585C9B7A7741C8B6B5793A92797A15AE7A5B73A74B2971463634BA52AA792AF05530730B6D0A89A346156B733677932BD36593A7496130CC458DCC5CA987C21960604EC8A8C5396056680CBF3F1AAC4F401AA5029FB2150434BB4706C31A2D54E4297939FA7C9C6F85700613CEB65C7F03AC56EB86E2D27CCC6DCB7B9394DCDB942FF222D86958A996C0CB6A8A44F97A70441C95FA71250116EEC20863C0B5A643458788AB001F8869D909922F51EE547A1E889255B3A0599C65842E5AB8D73872F053BC62392EA53896D328102D460BF1609583C22C3B43780EC6DAD0319EB4A5A65B4756C3CB40EAA935183BF8BD46ABE76BA46E199103A5313C3235F49C915E097BCA804DE680781D8365731BEAC6789A9203FB8787C4C070E00A13A6722A66A28236DB179825653D33CCF898B72C6B8450D97AFD3276BB13340519CBEDA708D12A858F54C49F4547195B7788A9150B2649E36AA394121926D568A488B16D3557B2A32AF57D11FC3373F80A28C0723273D362502E7C428AB44D3CBABF9EA585FD1BD0C9846556A1E196B78CF951592984A0A8487A78C2317D7ACA4118E1049750A0788F0D66AD9E48E34731130ABA0B427360A856D96D80B3F028FDD3ABA9035C10106BA1C0934BED36C6D7C7434249654EA89FC22137F4AB903653B75FB25B6F01635E6CC7D39CF1508690562826B49B6FFC59E0DD35022E541F8BA0D304AA5B4E20606907C424395666C54ABC2B8FB009847C86317685000C231215C8C15945860F6A85DDB98A8C3A527F2749D3C027E694E8F0B0F0FA454913AADB635AADD452F7128BF7752569669A8B93290EB92E78F6ADFF23E89F57F3890753B51F12F3F3A8A654E677847", "6CBBB491D7B3A3439F53C9027BD727A0E1C78004B4CDB66760E50F1BF97FFF0139F82862F1F3178A077B6AF77735CB802E91316BC207B9F1485FF7A868891E003BB9C6558BE6052C0826AF1CB5836B94C7D5D714EEFC7CFD983E9884B71348499DCA05DEF33B49D1C14BBBB95AB195BD72219B18BD0C704275CC94AB06653BBB9E0EEC74181903A0081AAE068B81EBAE8925335B8134B1480A60F758282C2F011B909641266774513A19090BC672EB3A0708CB577F134151C155C1E560D9B335A35A4D75F223502752CD250C9A7778EFC76A52709813A24679B987A6172F1714641EA6902D364F96357E92B44275EBCCE7F6207BB0A215E98B3AA92DAA46A39AD9132F3874C6239391E716BA600E42BC7084425051F865756A89DAB46A76104E183A12031B99BE86257B583C2E88104FEC3BFCAA50F238BEEC20ABF31219685885F2C3CE2F80C09B874E73A8712D35B8EE819D84B1C59A37947C0B9E25F066EE030ED28172FD5914B562408BA9B9DE9BAB2B95A4F30C22ECE458E133371A693D9458058265A75AD6CA5707617B38A1B6F396FB199CC1C77100741DE37500F1BBB10B9204EEB7C3F5B91A1CC91B6D4072AF410957358ACB0625488B9F6A9915758B7CC9B4AFE7D5AD6A95231A7A711B3A8996E23AFB3695B45B6D919C771D7685D90A7550A9B5B1E7538A365451A5A815009E49326C6541CAF0BA685056BF82EA4AB504161B555568583802800554A9480602437B0B9677FAC638A2508154A658E0623C62666E078D21A5A545A818D66C9547B9749458B735A4AD51812950E8A625A1B2A753ABBBA16415006300AD32D34C8B0B3B2AF4D1CFBDE08B4F06C70DE4BA28FB3763745CCE133DD2E488F89A51E7A586A86AC928867B0541438AF5645DE3211AA94ACA4A1095896EBC363C374A85565C1EBA030B5EA21B9B8A140E131ED07580D5B8590446988DA65939A823A84661B26CA12461AFE373519F43C13987A96A9069CD6B9177B205B25B5197DBCDE66C6457892C1DF96FE50AB7EF3B3DAB0A87436B3451476C44AA22AEF918AE5A1A8AE46920263032E3BC5DEBCFEE43B8AA290CEDF06966939EE6CC7C6AF83CB9D90CC889AB8B5A3D4C224E73916AE057BF5455528CACBED31B554043C6B0EA196854461D2644461470EFC55080E326FED49D62B5A4BB06B14F505EB4486C9A1B033B927836DA656C2475E5B8B823986DCA99B298512D94B77FE0588DBB96BA53A67369B82C2BEAB2CCC0103E549317A99DD97B598FA0A506184EB55C96B17302533C7EC3334E3CEC2BBB22BE0F89442840884FAB4A766A41BC8061D4ABCAC5190C729309AC6151BD434B818BC1E7D0CD435C9CA6DC7182228FF93867134A5CC65C6167C4A8BB830E7FEB863C15BF87B259EB0518FF5248792A625F37C589589799746D36197DC63AABD134A00EF1B49C79A2DF67B28E6B6E989CC9D0577EA5AA1B2FE64B322C7A8B241D503523D875CCDC479D28E3C01BD8383DCB2B684281E6949171F518EA97009237B5CD42AC8FC8A571716EB784B336529DB67728499988B73B80EF5C6D33F3B42ADA7A0A36A7C6A8A0DFB804B3D7BB05803957B913827A16F52ACDD7650EE2971A85DC21F6A94A5B31988E4438A9C1733AB8C48C32653FE800BF81BA66658CD2001ACF3A83E3A0426AB6C3B2600A53621277A43D723AB96758B120F120248C15EDF4294FC38F96EC490BD52094655841D9355038A379D8B666875171F39218E21D37023F5108B6D15389F07C1241E15071C43CD7D160EB5341B719C2055424EF6CBF66815F84943C9481CD1DE685A74C5B9DB37A0713952FD4A67BEC8CA7496F6C9C5FB84A8EAFE68D54654C6BAC9C3AB712C2BC32C0CAB072C18DE3CABBD9224DFAA305DFDB3F85019D7B988577B40F27514822DB42D87489502C53CCE6A095B486D9143D6D160B1C7313BF288913657698117EB145C0C9C55D66C15BF1A2B268F6A5FA259139551ABFE3468F1CC49C01B4232543DEF0BF9AE503B998536310751A8BC302B4B4440B835233797C967E7F9308155B27BA63C65E08BDD1946828196B30C042CDA0B56E4008215AC51F373604F5930BB66297C32BE41A162CA21471CC86657B5CB5307D89091DA9A5137F1C1433AB8AC1D59DE421C7C17A883EAB99BA4714F841998D0923CA8A2DA2B4146EC25321122B8A5AA8AFE0C03415273008A46EE83031E98AAAA125ABC75D3B30322560C197E75DD0E48A348099F7B2144D7B8A8660A4A97BCF19C0583BD9BB2123033CD7BB5A14B08B817831A673A28170F5F6443C0551913A327CBA18C3A053C4040250403B70AB9588832403AA0FC37665E04980FE1602E7D2715D9CBC00515DF432A4F5B32B3BC92AE3F31700166D498123E94576509B712B18491B1435EE7AB7AEB1AD30D72348C3CC083ABE24A8B12097BF32F792476288EECC3BF630ADCDAC6ACA7950D9839501A448500742BAE37F109203A809B2B960A307E25347A32C3EAB79288173A878789B296E9E8C1C28C5BB3AC472601C9765F7B77225A810C7B85370BEF4A5B079D2015ADA54236B8F33840675F9B2EB427A1B5974CD5C61B24010886C5A7BDA5BBED974AF7217F3338AD719CB308A8BCB1B6D6ED2A1643736C29095E8A8452A3A36C7BB5AE58CBFDC61529466A90F454ED6895B0861083DD1371999B2F559A3A487CF59A074FB49215EA6A6BE656F9AF17B121A2447CB7985590E9738842B899BA57AC311810AE2D9794F37483DD6BCCB64AF6D56588AE94665961C025C3AA2861974C236BCA4BD8FF5509F7AB774593E7C5549E57C2F18D15C0515094AD9A0DFAA0601E524F8231156B627BB25A0DAE04DACD0A66C041CEF400583FC13BAE640291A39A5C5CA8BCA1AD5C683CDD8290891A76940817DD8C9F52678780548E37A05806600801426DBB950C3B2BA34E24CC77864DD91B39F1408C716A69DF63342854E50A245FB50977B9410DED2C93F86B1F9D5A78B87BF81E51CA620A7E8566B19AB700964A40E3266415228D432156E5CBDF52364A90483A55C39B3FB16FC7465A3F8AC801B70B9FB28B583444BA5C1A73722D417A9D6D9B7DEB08BC6B330FF27CF61AB8831E27758C64AF3B12150CB7B33ABC29858106D63686D8762459ABF9413850AE53ED6313F76F83D0FB8AB34374E7DF693E4A1B3E5A8AD0CE820AFE1CF401ACDE650A8101B0946022D52178E19613C42B88B07CC04EAA81DFB28AC9DC076236B67219A30F8F945DD57BD2F335C52D59372308D38993467DB53DA3382B74867B616481BD0091A2232C1116DC88A589DB9107224A681008C67C589186A6929549BEEF92253DB02B0C8AA9F9C875A670266C72BCBDB4F5625043703C1A0457395832E4C335180462ED2220C59E7361903C107D85457F6CD82EB820D0855D97675C2E0151CDB73C2885DDB7849D74541580124E890116A65BC068093B57914E20C937C60A3EB25576F1A976A9583839B672144CD4A45C3477A45C29B4E0BC2BDBD206585C9B7A7741C8B6B5793A92797A15AE7A5B73A74B2971463634BA52AA792AF05530730B6D0A89A346156B733677932BD36593A7496130CC458DCC5CA987C21960604EC8A8C5396056680CBF3F1AAC4F401AA5029FB2150434BB4706C31A2D54E4297939FA7C9C6F85700613CEB65C7F03AC56EB86E2D27CCC6DCB7B9394DCDB942FF222D86958A996C0CB6A8A44F97A70441C95FA71250116EEC20863C0B5A643458788AB001F8869D909922F51EE547A1E889255B3A0599C65842E5AB8D73872F053BC62392EA53896D328102D460BF1609583C22C3B43780EC6DAD0319EB4A5A65B4756C3CB40EAA935183BF8BD46ABE76BA46E199103A5313C3235F49C915E097BCA804DE680781D8365731BEAC6789A9203FB8787C4C070E00A13A6722A66A28236DB179825653D33CCF898B72C6B8450D97AFD3276BB13340519CBEDA708D12A858F54C49F4547195B7788A9150B2649E36AA394121926D568A488B16D3557B2A32AF57D11FC3373F80A28C0723273D362502E7C428AB44D3CBABF9EA585FD1BD0C9846556A1E196B78CF951592984A0A8487A78C2317D7ACA4118E1049750A0788F0D66AD9E48E34731130ABA0B427360A856D96D80B3F028FDD3ABA9035C10106BA1C0934BED36C6D7C7434249654EA89FC22137F4AB903653B75FB25B6F01635E6CC7D39CF1508690562826B49B6FFC59E0DD35022E541F8BA0D304AA5B4E20606907C424395666C54ABC2B8FB009847C86317685000C231215C8C15945860F6A85DDB98A8C3A527F2749D3C027E694E8F0B0F0FA454913AADB635AADD452F7128BF7752569669A8B93290EB92E78F6ADFF23E89F57F3890753B51F12F3F3A8A654E6778479370FE5B05DDC92C939F62CBDE4C0FEA36F45CD20C5748CF3AC891A4C260449635D2BC43DD1CC879F765BF2A0C5E297889DDE910E57E2BB0EAE417B90AB7A275"), + ("52F2DF12173088140CA73333D1DB4341949BE452049028EF0DD904BC7E0DDEF7", "CC96DE9E1F0506BF490A154A6DB46BD2BD1413C3D1DC163190FF171EF0D11E22", "88C29C2A91CE80B13EEBE182AA709A1694A456B139DEB1977D531AD2722AC7D2B356AA2684D767C60012B1A2345A69AC52C7A814858E8C741A80C4B5E942CEEE0045B957748E2847EA835E1FE17C70456DBEA13FCEE0384BF431225C8A6B499624F086BF71518C8C269640B436E7783057372825768670B2478C96C4A1A2ADCB1F83A057263362476197C5D878D76B025F83AC802CCC02296A99296CF6B23C5617AA95387C2B0071C23AB7BF614371BC7001E29D7109057DC03053FB38C0084C48890612369C649C0BA3C2603E377C84D15B6DB6BB6E9513ADA9A47210861AA055D14683F0556729192CAEA8AE9A538FF810596D64BC0964B380CBC2F5D706A0A8135C1B87C3480C6D54CFC28A53C339666E24213D33A62EC872D803103987A36256840DD98DA8BC17ED42C34F5C008F6C678C748F3F7404D17B46F29012F079A46A0ABF77C04538254ED4B6BDEA929724AA60014674C427C55B1B0192CB284ED02194F9615417B08A9580DCA78944F43094E48AEBBC8BF0126310D9AA79530457E04DBA9B0D02B045AB28391F923C25CAA69AD038B4586350613D9AE22896F2979D56BBAFDAA294041AA42C21B9AC59E41CBEDBC33AD6996C3451B7DF5B9AE2D003F5092B91847290336A62E804B5F167D7CC4E31F40F6C640C10EB99D7F5519C80147B77843CD083B3B5C03718B1A7EA2DBE25ADBD56762E9150B46B6B4E82B769C384CAB39B3610070C6B6E5981C3B182105981A50567514CA15EB44620C231740FEB6BCBF4980F7B1824A5602A982A68BC8961BBC441B111C93B2374EC88FEF30D4931AB0CE3B5BEA680B1099CFA79B3FFF2B56BB3031A8C07519C7DF4F75A8C887C6A1B7249A700717B2798C02F0EA04E720752D8323376E1635331B07435CDD5BA6D7228281612BF060C694BAB6A00A8700977518BBAC8195853FDD34D44E29FEA7286DCFABDF47C9B2F2347EE7B3133CC275E601AA1874BD6974894C99D9659C1910030AD033138A57F8D9B312208873F733DB9C73007F45C49A007BFAB03AAAC3F107C8C0A032CBE24602A2BCAE4D2658D21899EA89EC5E832C0565701E5BAF0DA89E1B55ECAB7802EC830B8268F6D9CA3883C97AACC20FC4861EB59707AFA18A7B24534956301F238DFFB68BD20A8264ACB6BE4918C85282F63CBACA87A3530515041C5A00CADAC1A0B365B8A6CF626B30A49338461E94B44E20C250A23C7473952A20404FC17055E124792621D417CC7E20381BB24A701D428B1643BC0054B5FF46BA9129C0126889156BC6418A4C3B8C6D41B3D8646A915592305C30886B3995400A2A80771A494795D4B4B9EA6C21E0B21E0933F9F7C5FCB3B4C20AA303F655CB003526F8B04B3C85260C00C8E316DB495417DA22BD3A975FE784434C04DFFA545F91555F01033696BC4B87ABC0665B7A5C972D9A08FC8911147DA3CEF156B5CE85391161F32D09F6F8C0F8A845E268AA044E150944247E2B4354CDC72D3EA01B5970A6061B4F83563390BB33CF1CF9F910858DCCDD6C65E463607DF5289A3684440546816D4C2D59B214A15347EA9580310786A182EC6C2ABB73062FA281AC6806A6A042C0E95312027AD66EA6E245A9F2681A24CF505D5DB43F8A8C39BDC12E07C9F805634C9E5725E18A4229A20A681A0CA5144A8611DC372BFDDF102E5254AC1955450B94CE601AD68B5156996AC8C8A1BF1962A2758B4CBAAAB3A341E9C7C19005AB82BD236CB6294AEF22F1A1BBB32DC6EBD1C15FEB409E6C495EF1013E979981DB92BFF97BBB3B34DC385176C440B29637F68540BD10143AC33CDE4D2331AAB1DA5D9C7A41344A073B41FE536D5890239A96A319852593671F6A22E9480775F265C66D68521A73E9EC83DC6AC2963D3B70C3453861766048B18A135374ED53521A458E1FB024100116F434D1A261CA1C579F4311549243AD629525EBC465F8779C7E1B50344009C487D84A7B2683298EC39C27AF80FB3F28C61D388959A99721857CFD30F98F43A19716CD9E698EC946E69FAC477CBCB8E060EB9C96D3EE64B894C748133A9BDB5758CE45DDF791A68D87D1EF70D9AF626C3A909451A132C0702F0731D5D5959A732684C1759D8D31092669E70ECAB9F804CEF4CA416563AF1F27A219B3FD6BC482901201ADA9836B824AF9BB870DB2C9CCDAC525933E58A16C00B373DD81A3B89C39E9893960D7F6CC246A53DFFB9FE", "09766134E63C40A74D70107A121B44651CA43F837316F63F01290286304DA07B45FBCC418092B9FEC00EA7C88A4F20A0ADF9188E23707C88CFFDD6CD86F35A9E9A3D4F842C84A5C8662048395099037ACE5FBA75495A6370B9118D11324788BD4A841C90D219AA6BBC5C829EF2C291AF3AC99CF08672D95225DCC4AF7AAA428B7EE7201EEF8966A10B88067988F62C27FB52470C036535D762DF868BDAD167A383C74246B218CA68EF9B5106979E30BA4B278A731EC370750C2B37135D39B32F811AC667E717BE098B3F278F93815B2270C01723C322F1A3F4D8B1026A39808A9DE51B8453507464E283694BB937778161E604E127CA85F10CEA05CD28458503F100CAA530874BA500A3102CAA3802291DBCF72D457637C557C8F70012EB78B9DF1948F6055974C81AF1FB6DEE601FE40C3D185251441437582B57AB29627BCA1804C3B53EA76325B925982C8CEBF50474A5BD00E43274437229D94494B7536BF473435A1F3C552C45064F4DF31CAE2338394BAFFC8354D1005CDF0CC7D5A8782906135739079DDC727B55A67515C609223FA3C34B2EC24D033397B0D8C6777547E5C635F5E546DF52A207D2CE1AC3A9F85179A8898050078E93093B24122A7F553CEC39BF42B35A07F128288A23C91C9232B835A31288461640CFAB5EAF9A9BAA63AD40044160369C899737FFC8590C83A20CAA997FDA5BE4367DD00B91131C0F4EF61B0D207EEC750A8F4ABD5CACA994B779183051C6F52DCA8696EDC65807758ABABB04203234113052C7F894A478B564EA69D14A1970D2BBB437908F2288D0084EE7D538A3FC628E311D5D558CC2113DD1762DC3FCB336A899B583CEB457A8D55935F6926E1B5A2968352D6B1A35C986915C248E8C193EA3816300C67173930560E8455C560F3817C58848CC9FB3888037B449AA9268876AE4F665B5838329387431408068125F7DA65027917AFD42BD01389FE1F64BEF5B0EC688396CE58D622975D69818888029F6C0005082B9AA14B0EBF9188CE93C3B127237597FEFCC4DCF15B7F70518D8A9B34F1826C5B01CA9F48D1122AD41139C2E625F37DBB431957CD4DB6818292436846745A933014200EFB80FA9B4A4D38824CDD05C1422243A440126855DA2948024E136A23732B98A3319809948B9AE25ABC9B6A5A96AEC8A50167DF81C475EE586DC137DD5ABB7D91A0211258A82982DE2114A808A4DA29B79483A26F3CB73E5985BD68010F233AD7C0464C39CB0EF02938F036E1B099313318E81089CE78C1D008496B69764C08C7BA7DC00328846268B0709104AEBB8C757F22A8A45274CF75E80B1B377A971940852DBE1579B59B7B7901E06ABB7972B60C9357CCC0024C14341EEE51502D79D51D87A6F42897F5955A2B44C73766662F940D47B7DCEE02E22B21E138014D735404A13523EC23ADEF21A3184939AEB0BDA5264798146E1702F42C58185C3B063E1B884455C2B508DE26C5D334C0020C7430D774AAA85B2F9007FA41495A50BAD7315C4CAD429BF2B5B733690A8E422506C36FBC877708BB9F51CB899A48D3885B73933972F97A31DF12C37E2CCFF5608FE500F0B44585CFA0DFFD6932A2096CFE13518FA06B2E93C6F31CFC6E5C1973C2FFEAB73C0D5AA0F7A2C1B922A62065A423187B30B1DD261104933A036A89BF1357961008707D7C4819C6746E137531797E2B554E4C06F57A1811F9B1C0B459B2CBBC4C90293821AC6627B20A1BB8F93752222BA795E0448735840DE552AE26C2B8BBCAFEFF61E0B7898EF606DF88156E9540CCDDB6F4ED180E8E95D2404CD7FC15DACDC4873A2276DCC1892A36B4EB8C973D6858A478FA669052A04CA890C4FEED830553670E5F07FC43C59A0584802E541C26AC4C3C5774F79CF2251274C54BDBFA5B726CA710530C87EE49918A41BC50A3CE5A67C05689646516F2E77A832622EC11C9A69F70B4D014FF35182FD8B6CF2E181710C2EFDA85358712962AA2F04C58360756469AC4BFE861EA019923B68A4B63975064596F39B83FA7083620C20F578B7C96CCCF4FB5D485726DECCBDFBBC6EEEE2185FB46379575B5A875B28653A6945A7409C0402EA40A386A4F0F47D5BB1C972E96216384F9C48489CBB9DEFA92B2B983E11B796F0F176C6D5264290AC88DB13AEB93C022BC8C2266E88C29C2A91CE80B13EEBE182AA709A1694A456B139DEB1977D531AD2722AC7D2B356AA2684D767C60012B1A2345A69AC52C7A814858E8C741A80C4B5E942CEEE0045B957748E2847EA835E1FE17C70456DBEA13FCEE0384BF431225C8A6B499624F086BF71518C8C269640B436E7783057372825768670B2478C96C4A1A2ADCB1F83A057263362476197C5D878D76B025F83AC802CCC02296A99296CF6B23C5617AA95387C2B0071C23AB7BF614371BC7001E29D7109057DC03053FB38C0084C48890612369C649C0BA3C2603E377C84D15B6DB6BB6E9513ADA9A47210861AA055D14683F0556729192CAEA8AE9A538FF810596D64BC0964B380CBC2F5D706A0A8135C1B87C3480C6D54CFC28A53C339666E24213D33A62EC872D803103987A36256840DD98DA8BC17ED42C34F5C008F6C678C748F3F7404D17B46F29012F079A46A0ABF77C04538254ED4B6BDEA929724AA60014674C427C55B1B0192CB284ED02194F9615417B08A9580DCA78944F43094E48AEBBC8BF0126310D9AA79530457E04DBA9B0D02B045AB28391F923C25CAA69AD038B4586350613D9AE22896F2979D56BBAFDAA294041AA42C21B9AC59E41CBEDBC33AD6996C3451B7DF5B9AE2D003F5092B91847290336A62E804B5F167D7CC4E31F40F6C640C10EB99D7F5519C80147B77843CD083B3B5C03718B1A7EA2DBE25ADBD56762E9150B46B6B4E82B769C384CAB39B3610070C6B6E5981C3B182105981A50567514CA15EB44620C231740FEB6BCBF4980F7B1824A5602A982A68BC8961BBC441B111C93B2374EC88FEF30D4931AB0CE3B5BEA680B1099CFA79B3FFF2B56BB3031A8C07519C7DF4F75A8C887C6A1B7249A700717B2798C02F0EA04E720752D8323376E1635331B07435CDD5BA6D7228281612BF060C694BAB6A00A8700977518BBAC8195853FDD34D44E29FEA7286DCFABDF47C9B2F2347EE7B3133CC275E601AA1874BD6974894C99D9659C1910030AD033138A57F8D9B312208873F733DB9C73007F45C49A007BFAB03AAAC3F107C8C0A032CBE24602A2BCAE4D2658D21899EA89EC5E832C0565701E5BAF0DA89E1B55ECAB7802EC830B8268F6D9CA3883C97AACC20FC4861EB59707AFA18A7B24534956301F238DFFB68BD20A8264ACB6BE4918C85282F63CBACA87A3530515041C5A00CADAC1A0B365B8A6CF626B30A49338461E94B44E20C250A23C7473952A20404FC17055E124792621D417CC7E20381BB24A701D428B1643BC0054B5FF46BA9129C0126889156BC6418A4C3B8C6D41B3D8646A915592305C30886B3995400A2A80771A494795D4B4B9EA6C21E0B21E0933F9F7C5FCB3B4C20AA303F655CB003526F8B04B3C85260C00C8E316DB495417DA22BD3A975FE784434C04DFFA545F91555F01033696BC4B87ABC0665B7A5C972D9A08FC8911147DA3CEF156B5CE85391161F32D09F6F8C0F8A845E268AA044E150944247E2B4354CDC72D3EA01B5970A6061B4F83563390BB33CF1CF9F910858DCCDD6C65E463607DF5289A3684440546816D4C2D59B214A15347EA9580310786A182EC6C2ABB73062FA281AC6806A6A042C0E95312027AD66EA6E245A9F2681A24CF505D5DB43F8A8C39BDC12E07C9F805634C9E5725E18A4229A20A681A0CA5144A8611DC372BFDDF102E5254AC1955450B94CE601AD68B5156996AC8C8A1BF1962A2758B4CBAAAB3A341E9C7C19005AB82BD236CB6294AEF22F1A1BBB32DC6EBD1C15FEB409E6C495EF1013E979981DB92BFF97BBB3B34DC385176C440B29637F68540BD10143AC33CDE4D2331AAB1DA5D9C7A41344A073B41FE536D5890239A96A319852593671F6A22E9480775F265C66D68521A73E9EC83DC6AC2963D3B70C3453861766048B18A135374ED53521A458E1FB024100116F434D1A261CA1C579F4311549243AD629525EBC465F8779C7E1B50344009C487D84A7B2683298EC39C27AF80FB3F28C61D388959A99721857CFD30F98F43A19716CD9E698EC946E69FAC477CBCB8E060EB9C96D3EE64B894C748133A9BDB5758CE45DDF791A68D87D1EF70D9AF626C3A909451A132C0702F0731D5D5959A732684C1759D8D31092669E70ECAB9F804CEF4CA416563AF1F27A219B3FD6BC482901201ADA9836B824AF9BB870DB2C9CCDAC525933E58A16C00B373DD81A3B89C39E9893960D7F6CC246A53DFFB9FE7708C928659DFE465AE813012DF3FF6EECB1E081391B0A94172AFF81AA3296C4CC96DE9E1F0506BF490A154A6DB46BD2BD1413C3D1DC163190FF171EF0D11E22"), + ("F2A1E83CAD0055A85EB52F1BC1B9EE24F7F58D9C2FD099B5D38C9F75AD657578", "26D7035CD963C74AB142936A91EABD3074C583654945F9F5FDC29F754B06EBF2", "9331AFCAFB47F0E805E29B98A2A3C2D9D49AFEA806FB1CCD5E24105FCA6C3CCC1551B613927AB4CEE284CB53968DB3767C2154295CC479F06A4560385BB16B29273386320FABC520E5B36FE4D85814079E7E835D7935172DC893FF9277B177B7D8A9ABF9C66DC6A52C25852639540220FBB4E2E576440351F281712910CBF6A02F5A1B786B006DCE399794191D9F6BAFCEF1545E3A960CD204868463FF2B8A20944211408CA82AC182D05AB4C32D6A556FC36381BF163F69377E0028C8E8C70F0AD7A526CA274F9B65BD3ABA9164BF34F74B47C56966E4BF8E1B889E54CA29A037E67C28FE2A2B6710554D3813B55131B2B75DE0A419AB8888151876CD7B476CCA91FD83480D150CB3206F6A40B95E3412F9E7288EF051D3C99ED40B9E29A0A65CA1739F88122C72CB15950EB99302CCF18EDD17446F165930396F85E84E52E221B5668431B55CFA08D048A1386D450938D48CC2A94B38EC1F3F6AB2FA5565BF3B4BF1638AEB6A2440E5CD840C564183331E1AB7CB114A409CAB6D88C84C4545396360B1FB37B36CC405451D7FBA970FA7433B879EA8CA0BFF7C4BB0B98B255412AF695511A9CE168BC4FD50C27E121CA0221DE918778B941019D32020EC2F306C7BDAB38F9518BD51FC374F84B5E4B700F05785AC39CF9CBC0DA4C52150C9485D4A6BB4CC3C54A21DF63C2044B97C8709A5DAA82933197F1EA24A994370A2C9495E0B6660A10AA62BAB0BDC4DA284CC39B7C0E0464798A7365EA106793815B7B2BF02EA70C5B506CA343C38386BEFB69D6E31A1678BBD497BB1E64075B4158205280802234AE612A588CC8B2EC24AF74C5EAC085EEEDB13B827340AE29F348A9E31A20244D20A4B2B41F46A6A4C8293A2B8C5E3D3C49CA1AC0724969360BDD8A0822DF35D16A7CA48F90BB2C7972CD70B8DFB6CF5F1453F69266B04C2C32987DE0565BDA632F4B257CAC7CE7953A70B29681BC4602F3ACC3CFB3BFB63A135A758E461647E344A454AB3A1A72D234268D8870ADB84AAC250127E2AA593E79828848C112C26592A89C4DA358BF20571144C3C715449B8AE46606A51E150F4CA3593BC9ECC8A0382532610328253F488E733965C1742A36626524809C548397A922E79B82FC35B1D0EE41975CBCFF6F38BAAE28F0B452BBF792CD9023EB893767FD36E855774F1600399DA75889B03A95A21F32C0162540CBAB08B82B8346CA43B3E07B44F8C5AE6255CC379B3FE631A434349533B85C164B44D7805DE49442D113D238A9856E1897F0352130369387C3F2BE95435885A117072EC762656BCAB6EC1BE33C80C7835C4670B1D142767E23BC0AE6247529A3C8F348BA37ABE3507AC93C939560A445CE696881248F2A6C0B19B3900DB2E803A4BE0D4231F41A65702502241BBC724AFA441991A0A46DE7C62F6A220E457B2D876C44BB0A5F3116868E2B6D63A44A5C55F53F83E10E534606B76108CA4BF12A1B97A61A28B1BD184A9C2057655D460B65C407CB93272F44509C432DAD7B03A7393CFC160861CAAD4E9C81B9CB1085539D8A2C1855421346ACA46F46C3D12696237B237398B4015467CE7A6030A9666736717E6A740152BC2939998724018525D74724C5BD35B2D1887F9D2C6733A1173129DF3AB625E48C6664207C4D4931AF4130CC1CA0EF9160898890B49674F92A9464837F5597A7CA26BA854A4E0FB756572766DE2B9FAB3820818CBC600AEEF588B59B416F9568E19FC4B03902CC13547DC18197CB1AB23190424EC80633A5D58D861D9BCA5450102E52247A74594936153723C0A561B64F4E75D90E805DD7087FFD3420B4A0CC54CBB32944693A393511CA69E2C2B7A3A5B58784F2E999840BBA8C2C994852151569CA0949616DAF73328B220DBEB7D43794B1373CA3E8AC69178653E889D67E8546D35A40478B9C6410D9376CB28D6080AD41FE834B970403B8687C29011C6C8E24137F22FB7192443190BC2B095594C85C55389D4D49722124F07067FD9F34319565392799C27550934127538524E1655A03F697313130CB0F6799DE565A5DB8B7C23C7A8F53AA7714C7094A86E67894A75A489E6C414B67C52030F40EC5C599969D8E00B7C03492C8CA6DFBA8D641307921712C2840591FA9A847B39794BC22747830FD956C0C17B5C974FDCE96F565355B500A70C3D86EA32C9290514B1373184AAEDB75D90976FE6765F4747BFC946F1", "8EC50C8143608F9186988602849A965D48645CA331C1815E506A1C51D73AAF18BF92D39BB18A5E681447EB78883CE48E6C375AA8E36CA82ABA3E188D8D795DDEA50B655B2E9409C35931C934BCB5820C6D2CE975FD955A9BF40695650004B4C62447AF3E1082DEF45A4AEC121DF9C8D36C8264034E55F926BD5053421C809041C1701650EC0959E4EA6D09898B8BCB90F95209C3397525D984F6079063901BB82478F9B1CE08875449355FB16A3921A8CCC9547349C5650BF49D3D9BB523092607F2B4BB437B1A969C35525F321333E9436B66A08699A9CE4017C6D7485145C408FD412E69463A558927F0763ECD8B24C344640535CE10DC41780213A3DB5930DC40CBA012CA5B22B2052C3192B31C9230F4D12FD5767EC1700775C478E263220E40C65EEB36FAA1C18B530B6C5ABF6B11397E694763C8C781CBB3C6C767F24287BCA224370C9D5E9066609078BB4B7BAFA46328F9C203D7AB0419B9177295DA08B802D93A97DB3F1FE964BA7B78F4A5404657603E5A7B95971A1DC2A1B00B54E7FB1C180CCE49917278C961FB61009200070ED90BB752A55661B011E35382A01C57793AFD864AD8D3ABCD752F78168EF12238CC520A6845CE3CA5771D207B3A2A594E687867783E80B057423118753BBCD3926B6640321938A40E793BADA8476D64858296348BD7038DA24E65D4B6FC4C907ED69E0159C76AB07F9111035B6187A41024AB26966F7B64673860EC9AAFEF29C0D7DC8A4DE31344C41D84D07B3A6525A5A884BCA8870E758E4A6C02150505A7587EB6288C80910C0E999C0309A34DC7C47053AA1F01848BC15059E65307207F4C4974DE0ACE30C2A3021B5927B1B0787002AD0A4CABCCC2D30865DB9150F657025658C3A14CC7D088BB1247CB2097A521FC46E75B158C55509D0A6DC8FA6B53016F6C28939C889943075E1B26646E3BAE5B0A52D3553E3D24AD62396AD18CB53C896E8FFC148F57CE11A39677052C134B8D0BA186B6F9BB94A2C96D06627ABC7E29660B9DF9C9B9E1786B39B9148A982B456F4F426368845D0377C043036252A4BA1B61487958A9E05CB272072CBD0927B4667BA7E4837E4B7405B9CD3EE43F7575B8209BCF122622E7E99B414C3AA32C9DEF1B200444B82BCBBA8EBC079FB42E00A830D921C45387BE4F86BC05D64BCE8B948BC61D6FB0CBC9552591F99622C50C498376A106414A6109354184EDAB57D8BC27F6761E18E12A12335BE89944B737C3B3BCC3227148A5A60EA32B83C8C5692B35BD68D72DF7CC264B519C52437B357899B3409239C02251F83460F6AF9B7C73AED1B4EDEB322F794555A0CF5AFC233260A10C6529B5AC55582B153461C25401027748B39260015E00184D553F45372613631E2ED0B4633A0CC52B6F62454DA414A49B17BC88225EC9EAAFBD121494652C925BCE19CB53E3216040B879F409049B780098A189668A7C46751C714B38A6E59EE4D7620AA3B3DA3BB3FF3C4C04E45F3ABB3CEC417CD80908EA701D90A21E22210CB7188D739CB13DFA03E35327C1912453C5709C1B6FEE2CBC5F8714E1A43AEA7422D6E63F247C8065A0CDFB782BDD2CAA781B4B8B76BB7885C7B446BB40A52BB2C8112F3C5DDD077FAC625D54DC26B0A3B52109C8C80137302738B511377376A74E07C1D2C3108180C6ACE97E61F6A343D6C1CFB76D5B18B756179A66D51483B904E5B88FFD700971A37E913829A2F366B8386BC342BB49411890C958040B7E66CCC5FF9C55CBC85209784282F865C7132FD0F82A4B016A9CBB64B03CCDA3F64FE6E760DFA764A7D2A17027B18C8909FDE5C9686608670871CD95B2B1DCCD99C34B22034147F1471BB38D3043ACA4C727BD971C4676BB7DBB987E015084F16E05686994A776704A161C7407007623964359E0F77214B03898C51B4EA6474A7B0711E505F0060C220A80A9C591481643A9796C2AD651ED7783F38633C308B33490867DF28DFD054AE9C49CFF78B508234E17538EF58172E92642F43A5D421B2929210BBEA122853918EFBB91A0446405519D43DA86031A6DBB3A9E24BCBE4E686EE9EAAD71A42A4D27907E89326FB811A5663D18002F7294CED499A29EBA328F5460F80512D42AB48B726E798778A2C2692BCBA2A0052F9A56C1A0631741A6BC846A8C0BC309B7E9709331AFCAFB47F0E805E29B98A2A3C2D9D49AFEA806FB1CCD5E24105FCA6C3CCC1551B613927AB4CEE284CB53968DB3767C2154295CC479F06A4560385BB16B29273386320FABC520E5B36FE4D85814079E7E835D7935172DC893FF9277B177B7D8A9ABF9C66DC6A52C25852639540220FBB4E2E576440351F281712910CBF6A02F5A1B786B006DCE399794191D9F6BAFCEF1545E3A960CD204868463FF2B8A20944211408CA82AC182D05AB4C32D6A556FC36381BF163F69377E0028C8E8C70F0AD7A526CA274F9B65BD3ABA9164BF34F74B47C56966E4BF8E1B889E54CA29A037E67C28FE2A2B6710554D3813B55131B2B75DE0A419AB8888151876CD7B476CCA91FD83480D150CB3206F6A40B95E3412F9E7288EF051D3C99ED40B9E29A0A65CA1739F88122C72CB15950EB99302CCF18EDD17446F165930396F85E84E52E221B5668431B55CFA08D048A1386D450938D48CC2A94B38EC1F3F6AB2FA5565BF3B4BF1638AEB6A2440E5CD840C564183331E1AB7CB114A409CAB6D88C84C4545396360B1FB37B36CC405451D7FBA970FA7433B879EA8CA0BFF7C4BB0B98B255412AF695511A9CE168BC4FD50C27E121CA0221DE918778B941019D32020EC2F306C7BDAB38F9518BD51FC374F84B5E4B700F05785AC39CF9CBC0DA4C52150C9485D4A6BB4CC3C54A21DF63C2044B97C8709A5DAA82933197F1EA24A994370A2C9495E0B6660A10AA62BAB0BDC4DA284CC39B7C0E0464798A7365EA106793815B7B2BF02EA70C5B506CA343C38386BEFB69D6E31A1678BBD497BB1E64075B4158205280802234AE612A588CC8B2EC24AF74C5EAC085EEEDB13B827340AE29F348A9E31A20244D20A4B2B41F46A6A4C8293A2B8C5E3D3C49CA1AC0724969360BDD8A0822DF35D16A7CA48F90BB2C7972CD70B8DFB6CF5F1453F69266B04C2C32987DE0565BDA632F4B257CAC7CE7953A70B29681BC4602F3ACC3CFB3BFB63A135A758E461647E344A454AB3A1A72D234268D8870ADB84AAC250127E2AA593E79828848C112C26592A89C4DA358BF20571144C3C715449B8AE46606A51E150F4CA3593BC9ECC8A0382532610328253F488E733965C1742A36626524809C548397A922E79B82FC35B1D0EE41975CBCFF6F38BAAE28F0B452BBF792CD9023EB893767FD36E855774F1600399DA75889B03A95A21F32C0162540CBAB08B82B8346CA43B3E07B44F8C5AE6255CC379B3FE631A434349533B85C164B44D7805DE49442D113D238A9856E1897F0352130369387C3F2BE95435885A117072EC762656BCAB6EC1BE33C80C7835C4670B1D142767E23BC0AE6247529A3C8F348BA37ABE3507AC93C939560A445CE696881248F2A6C0B19B3900DB2E803A4BE0D4231F41A65702502241BBC724AFA441991A0A46DE7C62F6A220E457B2D876C44BB0A5F3116868E2B6D63A44A5C55F53F83E10E534606B76108CA4BF12A1B97A61A28B1BD184A9C2057655D460B65C407CB93272F44509C432DAD7B03A7393CFC160861CAAD4E9C81B9CB1085539D8A2C1855421346ACA46F46C3D12696237B237398B4015467CE7A6030A9666736717E6A740152BC2939998724018525D74724C5BD35B2D1887F9D2C6733A1173129DF3AB625E48C6664207C4D4931AF4130CC1CA0EF9160898890B49674F92A9464837F5597A7CA26BA854A4E0FB756572766DE2B9FAB3820818CBC600AEEF588B59B416F9568E19FC4B03902CC13547DC18197CB1AB23190424EC80633A5D58D861D9BCA5450102E52247A74594936153723C0A561B64F4E75D90E805DD7087FFD3420B4A0CC54CBB32944693A393511CA69E2C2B7A3A5B58784F2E999840BBA8C2C994852151569CA0949616DAF73328B220DBEB7D43794B1373CA3E8AC69178653E889D67E8546D35A40478B9C6410D9376CB28D6080AD41FE834B970403B8687C29011C6C8E24137F22FB7192443190BC2B095594C85C55389D4D49722124F07067FD9F34319565392799C27550934127538524E1655A03F697313130CB0F6799DE565A5DB8B7C23C7A8F53AA7714C7094A86E67894A75A489E6C414B67C52030F40EC5C599969D8E00B7C03492C8CA6DFBA8D641307921712C2840591FA9A847B39794BC22747830FD956C0C17B5C974FDCE96F565355B500A70C3D86EA32C9290514B1373184AAEDB75D90976FE6765F4747BFC946F1249DBFD72887FAACDB1B900CFCB5B3955EF374D86072D38CE29DD9E906AFD6E326D7035CD963C74AB142936A91EABD3074C583654945F9F5FDC29F754B06EBF2"), + ("145BE4E69C65F82CB31290C14E279EB77246AA0DEEB4729E7B21DB0ABC4A15B5", "E6748703B1B4742E044D723FADA7E44091113AF9D0FE599C5E969A1194FC7925", "F2F223B0E4789C392E69386897E95825B0B6E38C821F806C1890CBDF500FE1A5B8AA139B4189A7D720360852B514CAA73BE8CAEE5C8828027F945768595280A4B869CF154E2507D09855B9D3897FF1587C30162DBB49C443025912D3A950A4711B06BC64EA2216D81C7862066061A33F776A0F9C69B52C8EE328B726A6BA415376D5AC8E36503E51067F19A53D601B0ECEC648D1A256DD452472DA94C6656DD68CCE745C4CC04826887340AF46053592CCC701630AE172B522725F546D4E244A3421079456AC75745C4AD289557249E9F29A5194677AF159E824AF31687D09876277A3643DA93B6103A7FAE9644CA127B6FA9BC80A248575253F4C813F900856A4445741119E729E29466866BC14E26527933248B286AA4D872EBD2CBFCF1A67A5D90240CCA09255C811D2B97B3987C4A472D552AEAC6616E122427C365D435501EC66C451826ED7719D8888241DB93A7FC82FC43908F70151243A30B9308C600A61F6B3248657838DD74B07255219D32903E8A850B997F8A329F56026FA2C0C43E322D4A5A5727100A89542E8D57DDEC2B02029306FA108B5481570D98ED9D29FB3025F5160A6F83C8FCCF509890AB6A3B6403920894DFC9F7B50CF50883448A41DE86C381DCCA7F0F431CD12494D515938358A39216549C804D3F91C3236019AA805B4FAB7928482B27125DEE89053201ECDF0B907786CCFB11B9E24890F099E4711950D00BC126680A7F45CDDF4115310A42B813DE03C6B77743BD66473C587C6FAC769B293645F601150D42A9C741F740071146A942AEA330D78132A6813BDA844ACD73A2CA311B1E84EE3B093E171382E7B353AA542D3BB3B8222B256D7AD3A21042B68BFA2668867351190E4CFE60C22DDF8BD922A77933CB4F3A5C012A72CDD42C686815F876407ADAB033BC742CB811CD82C8098847EA44853AE71494B63CBDEA66A8A08A55F75AE873A0D61F487A94987BE25C0E2455593F01ACBE322DF69532F5308C1076FB228610D6AC8CFE45F390957B427A4E95A6FF5A494A2FA2A0F92A05EBA713127CD47A53F4A835EDEEB702412C8EBF1BC18C2009C7A8BD8995D7E804F35964E63AC2EA51922E1564868A95D2B99AC887C8DF985092E0ABE26B4A468C01C3653B34A097F0489142245836F2CB504AA7E0A6536B46A8569C3366FC9460D838CE40AB4322006E841445754A00957667156633DFA5F3FB6A9B6D28337A85B9C34B41E37A0DA8956D3640E97337354841205C98316242A22D81F979063FE1CB02688AB4383577F022B55581444EA8137696E44B6AAA4D079BF6381EE55253CE1023BEC17A7325B04B11D8AA55F6F1429E661961C92B88A104322988624DB6155E836979B3162377D91B10C58D2265A32165B7509990013CDE36136F7B86299C6A0F06C037C35E54164580961218787B8D46C56C84F0C6ABA065C6E4B6078DF35BD8604B9097668BC4460EFAC7790D50FF605AC3AD108EC045D76962C4E3C6E59C292F89A5CD0832BDCE59BCE898391BC71D5353BF89B366F28BA1C4934AA1041CEE3325389A10B4A5E558C3F2BE8988A6C60F9977661D39F38F663B1AA9C26479BFEA67EC17048E3D3C236522BE6535C1870CE0BB721249205546BC00FD2B86FD852CE0647D6882393CB11EF22A5CEBA1CD5C2A7F0B0174DC52D66E8CF1C567C89E128C2B29EEADBC7B8E2704E80398DDB2B89D1CB6B6570C15826F5C855F9D24FF25937233C84EF49249A9A739C540592C618D63CBB83E2BCA108ADA6AABD3E93AF538B5C6EC29FDC146C0950817CC80C2258AA4BAC0A984B621305C082903643851D623C8B8D6A90FD269B65524B8396A0EE192CBB4202634209223241DEC3BC66054446F12AB6DBC9BF27BE16EB993E0552BCF1C4CC8C5AE4449D49E508DAE71FE13A7876352C58230C5EB2861DC237DC799CFD3588E2A6AD06D1A665881FB7620C2AB50BEC766ABD441790E23C28D75FFF43CD88AA405D5B607E551E0B182D42136A16508EDC90BE9AD159262436F406803AE580C4C0B4552AC6267712E30A39F66396DCABB4E864CBAC9AA9F5226F8B421842811876F39AC648C73D282A45C4A99BC90F2E52563881C22BC3858B928382D5416056B969191316A7356A07B46B146D2DB360BBD89C63399585A7CA72279938A2B22FC8CC7F6E7F3A757BC90E65D8A8F8F49202F696137573B13BA0088FFC801833FBCA9E", "821865EAEABD3E684D94942229295D93B09DA166B59342C25287246B0102075A3C69476570239F9DFCB6A8F16F06F45A9BC2A225A47FDAD6287115CA8524C8402C04FDF3AE9386C31B7707CE863F068872182480EA3243AA654968D49C1FE52D5F8CC0128C1757B9CBF816420FEABD17233AEF955B4C77A6777A16BA35745EC6587C37376F042ED3DA49CDE7BA8A31960CAC4266D51DE764CB694880B0E5A2EF0789791962E67CAF1C152E8ED05A63B0B77F56CA99490D321785C540A2C814B77291165574460D908F6826BCCAD3B33633A2693A2302E34B5BC442ED8744FEA370054A0846B9B09AD111F38A3E46015FF431064F586B8C43939979437F59AC401C6C6D3B4E14A9675D54CD1DD2B2437345FF54AC616CB10501656810BBE085AB75B0CC24124BBAF1B3C8504C46A44A8AB4215CC22DF5644294F3A2D3135AF558191080A49AE7CDD711C503FC6DFC2771D2FBBED7A6B5435C07AE290677656E9524A26C9455B21704C653A395D34521E830E7514D7B77612AD2CB8EC6A7AB6889F471740D4B943D3048412655D2FA32451742CA8B4312241FAF151648D65028D5C08E717ECE286B7157BF05B8A1956C52BA215D40273238999DD8B7C9171293C1D82095ABB00B09C99ABB2A8B402181B23F8EC668BFB2C33A656A767BB77D8316517208CB44426A94B642154E7A5A73E62AB75B000DD479C13FC7A3CD5322F1BA2D970B05FCE795E41BC5A8135A37A85E01E777F452BD7E0284D07C5D41D34D4698C510F674E3A9AE59833960F02D39B9B5E4EA2B73378157783C8556B5DC6215087C4A73FB2ED5A62FDDB23006E05FF14B87E6CC4D63F379E7358641B69D9FE48CA01AB2C210B99C436D89D84A2DBA4CF43122B9912478259A4F5B7B1E6CAC6934975A82588071C079689686BAA105E3B1CC436A7268B211BBA7833B2BF4C85E99D5CC65A40A34949138DC14DB83418A8A68AC74C993224028159B386055EA7597905995A52989D865BD54C358A19519F07051BCF97BDFDA2A5FD50CEE13CE6D2484CB297D3C2238772B85354B906E3AC9502B7B8DF4C32FAB4DC5553228D71EC8FB86089243753A7D7EF29398B92744ACA4B25819D4CC1FAF4BA5C09416FBE8C6893175ACC3C242D136F85636FFA1A2147216F2D100807877DB7628843336BE3176FBF0A4B73AB9CFB65284C38F1C883AA251B72CD46E7FE6CD117891D9294D8000388FBA12DA851A945C497AA2A797F25103DB1B68996355071A06C9AB0E122F25748FCE59A22F68AC8EB4635195BDBF07120AE27C9D857B4111C9DF825B1513CCB409CD02A37E6393025521492CB9707BD059E7C29672449478B17DDF57B8E4913F5DC00D0955980F7708650BC22E3588FE6A0F2CF016BD73BB86C7CE7F39381F20A4884A3FC1DC5397A4BB30199B47F73908D58ED40491DDEA36CCA15AC9CA337D5C05EAEB4647A9B88635451CA58C99C67DC5A5168E0C44751857C3277BE9DAAB366755DBD1137C4CC8CF97A0E5E99816D96FF1984BEDEC44867B1570C50772BB704BF54AE3F5150BD89B7DDA095FB939AAD0C3FA1CBCF25B93B40B1EBD26B8074871B2210B48F28FE4C384AFDC6ABC531150E81516376406600C181C4D200C5B7D23003DB65FDB401C8094C110C53E3998156294514F990249269C5D9A1077B2C180A97F0D833FE085A5FE48102EC59B52929E249A82518C2FA6EB5D45941DB3971CCDBCB60D69097E12AB226965FE33ADFD407102C052A4478325271F3E28C450B20F51EB1DD70358F8F18C260340C2080CA568A10060A2A4903796A39E4FCA1084D1859C66722A41AE14456DCE2BC753D29732F29984B85E49C60A1C595794FCC6CD593E84F7B1ECB40426836ECC9394043900C286131A785B5666BA6C2B76764C4C6BEBA7F5C12690FC844AD4559F0222261A4F3961753C7A1EDB1619ECB73FC088180ABA68CF623D09E5CB1B144F0F9BBB2DF9C4CCC430B8383480500959392EAA1C99DD16C4774650410BBD9328BC306746432B50C9A4B2B0A57E1D0B5BA92963DC055F5009592A722DA9504212EC4533D463C3F1850E41A4F2D6092D87C1434B7BD4416A109621BC44129BBC0F3D972882FB50F8DA3585D7C0920A1D3C8B5BC3B3BD0F948C2D550765DA6A98579E7FBC29FAFABF0D20872DCA1BF2F223B0E4789C392E69386897E95825B0B6E38C821F806C1890CBDF500FE1A5B8AA139B4189A7D720360852B514CAA73BE8CAEE5C8828027F945768595280A4B869CF154E2507D09855B9D3897FF1587C30162DBB49C443025912D3A950A4711B06BC64EA2216D81C7862066061A33F776A0F9C69B52C8EE328B726A6BA415376D5AC8E36503E51067F19A53D601B0ECEC648D1A256DD452472DA94C6656DD68CCE745C4CC04826887340AF46053592CCC701630AE172B522725F546D4E244A3421079456AC75745C4AD289557249E9F29A5194677AF159E824AF31687D09876277A3643DA93B6103A7FAE9644CA127B6FA9BC80A248575253F4C813F900856A4445741119E729E29466866BC14E26527933248B286AA4D872EBD2CBFCF1A67A5D90240CCA09255C811D2B97B3987C4A472D552AEAC6616E122427C365D435501EC66C451826ED7719D8888241DB93A7FC82FC43908F70151243A30B9308C600A61F6B3248657838DD74B07255219D32903E8A850B997F8A329F56026FA2C0C43E322D4A5A5727100A89542E8D57DDEC2B02029306FA108B5481570D98ED9D29FB3025F5160A6F83C8FCCF509890AB6A3B6403920894DFC9F7B50CF50883448A41DE86C381DCCA7F0F431CD12494D515938358A39216549C804D3F91C3236019AA805B4FAB7928482B27125DEE89053201ECDF0B907786CCFB11B9E24890F099E4711950D00BC126680A7F45CDDF4115310A42B813DE03C6B77743BD66473C587C6FAC769B293645F601150D42A9C741F740071146A942AEA330D78132A6813BDA844ACD73A2CA311B1E84EE3B093E171382E7B353AA542D3BB3B8222B256D7AD3A21042B68BFA2668867351190E4CFE60C22DDF8BD922A77933CB4F3A5C012A72CDD42C686815F876407ADAB033BC742CB811CD82C8098847EA44853AE71494B63CBDEA66A8A08A55F75AE873A0D61F487A94987BE25C0E2455593F01ACBE322DF69532F5308C1076FB228610D6AC8CFE45F390957B427A4E95A6FF5A494A2FA2A0F92A05EBA713127CD47A53F4A835EDEEB702412C8EBF1BC18C2009C7A8BD8995D7E804F35964E63AC2EA51922E1564868A95D2B99AC887C8DF985092E0ABE26B4A468C01C3653B34A097F0489142245836F2CB504AA7E0A6536B46A8569C3366FC9460D838CE40AB4322006E841445754A00957667156633DFA5F3FB6A9B6D28337A85B9C34B41E37A0DA8956D3640E97337354841205C98316242A22D81F979063FE1CB02688AB4383577F022B55581444EA8137696E44B6AAA4D079BF6381EE55253CE1023BEC17A7325B04B11D8AA55F6F1429E661961C92B88A104322988624DB6155E836979B3162377D91B10C58D2265A32165B7509990013CDE36136F7B86299C6A0F06C037C35E54164580961218787B8D46C56C84F0C6ABA065C6E4B6078DF35BD8604B9097668BC4460EFAC7790D50FF605AC3AD108EC045D76962C4E3C6E59C292F89A5CD0832BDCE59BCE898391BC71D5353BF89B366F28BA1C4934AA1041CEE3325389A10B4A5E558C3F2BE8988A6C60F9977661D39F38F663B1AA9C26479BFEA67EC17048E3D3C236522BE6535C1870CE0BB721249205546BC00FD2B86FD852CE0647D6882393CB11EF22A5CEBA1CD5C2A7F0B0174DC52D66E8CF1C567C89E128C2B29EEADBC7B8E2704E80398DDB2B89D1CB6B6570C15826F5C855F9D24FF25937233C84EF49249A9A739C540592C618D63CBB83E2BCA108ADA6AABD3E93AF538B5C6EC29FDC146C0950817CC80C2258AA4BAC0A984B621305C082903643851D623C8B8D6A90FD269B65524B8396A0EE192CBB4202634209223241DEC3BC66054446F12AB6DBC9BF27BE16EB993E0552BCF1C4CC8C5AE4449D49E508DAE71FE13A7876352C58230C5EB2861DC237DC799CFD3588E2A6AD06D1A665881FB7620C2AB50BEC766ABD441790E23C28D75FFF43CD88AA405D5B607E551E0B182D42136A16508EDC90BE9AD159262436F406803AE580C4C0B4552AC6267712E30A39F66396DCABB4E864CBAC9AA9F5226F8B421842811876F39AC648C73D282A45C4A99BC90F2E52563881C22BC3858B928382D5416056B969191316A7356A07B46B146D2DB360BBD89C63399585A7CA72279938A2B22FC8CC7F6E7F3A757BC90E65D8A8F8F49202F696137573B13BA0088FFC801833FBCA9E105F2F6732DCA0DEF151216C79A5370A63D0C239D8FC99819CE1FE5A4984F08DE6748703B1B4742E044D723FADA7E44091113AF9D0FE599C5E969A1194FC7925"), + ("FE8C4F8EEF555AA563EB748B5F55D72CF68AB6C81E9D541A8651ED7EF2EF1352", "BEABA3A6A443866994CD9C1C2B70005D87AFF90F4D00A7A232F3D175BFEF72FF", "D9B50B583278A2EA947B027BF85885CA3398471A81CD153CF9675481EC3D657A623852C8F72A02EB3A950F493471334B871915022B1510506418849C6E52529335B00969B63C6A8F1D8727E0FB89D7E61D6D5C4765B6A3593C3948AA2B2C2B909E1CC766A879EA278A921C2BC876B572F3435CC39CD541C390109CACD51946254CE6E62A3698AA5AE1C01DD220777C9DB8D89F30258A856332C4F540C1CB4CA67B6C3CE2782088B7DD072BDBB1102F403BC534C7AC2A1628261B0EB9A19452118E791220680AF79189002A9E9FA7A54AE38FACCA03E870C48C8B9A58F4060AB60322509E568371A9B3A959E5C55E5B99A0FA9ACB47C422336431D08915046644148ED3F87F873309E2FC684B5AA6BA627E883093D90351CC737E26364AD3251F5E320FFC905F2134825B7C7AB87651A656A9CD49ABEE5772745B798E08B00DC5C41B3C535890433DEB7316167C5873724B7B730923A92C87316EEC59A4192441E1A7EA88762D797D73F6193309B47AA0589707C342450A79550C67876D952A3DC6A65C4A7A3820116BD5F70B8081AA5EF8CFEA09CD4B156808582CD7D078FA2B13CFD5771AC18B76022AB72364111235841343497468F2AB3A134016041437D02237C997095FF0A016E6C48566241ED873DF4C47A05904A8852E56CC82000C5DF3F2769719761D3165942C3D7D3B39FF151C69793B33F699791A4691A934E98386B330340A396C95947DB2017DD2862321D53B88014066697614865F75512E08C63F52002FF96999D5CAAA31128EF89270DAB59A1E909991220C2E9C66C7563A7B572544871F29170D71E29B7A1433AE239F76B21FEB6B9D49E863FFD8463EABBAE5425C0E527A36FA70D4464324AB0CA35A3AEE68369491A434C3243335909BAA6B689B228D9540733B6DCA05015C927ADEBBA90B17014D5268DDF8A06D9AA979FA0917CA4A4C871C4CE6A3908AB158664AF5B5AB6FAA95E0E593BF55A65B710824FB64D7B7A421431A75E78EB7D61BC899628994935D4778CFDA77E9168307B2B6CB0164085634D94CBCA4C8B53E793A00DD8D2FF6A800C55E028012BD6912C48B13432C485B8AA0EE61555D5699AC5AB6944120D1EA030FF2CF785573B0E84FB772B9F3E473CCD47921B6111AF6A690F088C58C7267978BC5757CA9E542A441983BA113654A8448D7025AF4BBBA0306D56403A12568FCC8B892D9AAD06B809D5681B949A19BFBA4C55A26E417B164309E1E48AE245A81734C141D639D89677A29E231B5255072A37C90925AF0D36EB39737396C1A02B513EB98BC110870CB9565F2898690ECB0DB7807E3E858C57BB642CCC5EF0C8F00D733B3581382994E0C2C74A8B140D6EBC02405835986C2D913077B6C7BFFCB230BA6181D765620B0AEC43B8D15F64F51ABC92DBA2E5CD53021537A6BC06091D2C1E47B2E049A1207617D9A6703196623A0CA85054373D4283EAEA0A867B308F362BA8B26652FD346B4386685E6169D0946233BB186646D3B322EB726142A4C6DACD9AC8E866EB11708EED22ABCD86E2008814B96C976FB67A9399CF837529F18461ED9047F073B0DA1C0DBC3BDE180BD4AB75A81288B1D64474F4797AAF9170C674188061872E4B1BEB2CE82FC968EC9442DE65B59972FE55A56C81CB6B6584F86E566F5202CFD6400579C8242DA9CCECA68972563F155A9CAA2790219846ABA47000927B4231FD4E1131EA23A5B595B4760013020BE7DF38725CC502C2B0C33CC2593B44543F06829AA12B8CB6836068FB0E697FAD760F0251318934A1355AB75497C88D2BBC0F9678E4CA6E930CB7D445B530C7061488999C530B5325CD3F16824C034274B77320CC4191663C1E58A2A321952FB6A4350624AF068B9D95FF58A5E247887BBBB275D65C417357A0C29805450B2F2B859BCB612F6D3401FC2C4A93091C582207AA8512551AA33E4CB896AC42443A33DE84919F131575A3EA1607EB27B6E570A889FD6C38029BDDB6A0E9C77690C9855DFDB483CE35855A484BA791B151759D272925505765F546A67490859E28EA625A252C1C7F80686042B1BE9931E4C7A33166729481644E546BC45293786C10524DBB7DE822E51A44A64C51EACF0A743CA8566310BDFE61902FB7CE2A46950055E1869BEB391695E48259E11B30D34109D63C1D41FE491A35244C5AEA207E40081576B15C73098F375C0A5ECBDFF849EC24D79", "1BF4705C214F0912CC183A44471542F69602C5B544EBD526E472CDF6A04F9F856E4931BA4D818753D3ABDD92C4AB72B03A43C36A969B2FE92938D27BF4452323B973797260FD93745CE95237B4C4853BCFB49B70B25A7E4F54B72B49CF313889DB8C95A1512AC8F516AC22AB3CC59B2E9BA59508CE1A1526846C22AAE6A63FF89955CB48EE5063711559CDC36C64131DD0A97F16854FE4550FB115A57454CD8D22B8E7634E65D421DF6603E3244A0E082247B4C25DA971E0C5193D77B11A661C0E150A7A614196911A95D89C0E5919FB5AA2B3A3CC5C068730818CAE910A1650060026B3B4103ABDF6B1788656FAE87AA5DB97BB6B6F87986617C1986E06A0F055BFBD73923396C334E6AEDC2C018F5A8EC282C980B95E73CB33DB85556894A5855207780816912B1FE35BCF02754671778B00CD9F03DC21439A83EEBC4875EB5290A41BB6075934C2735C054A16E83E77A4844324A5A7E92E1D681D4396B0F600CF7ED694FE56635F51193DEC76B4BA4887F53A107534EE318137C38F02801A15848F1AD79CD1E75E889C2BBCF6256FE239F04B34714053F3D25D5927B42798BEBC6430D541C8E7618160B133E1D42A4E069B86B47469771F0EF54B22550B5868479F07275D297EC233090CC133ED388AE239BD990A1899E778BAC687361B3C275C1ABC4C1046900D80EB5B94C1868A2870BCD12E36D3AF37150E6FE3B2718419D5EA1F4D81CFD152740B51CE77B8125A2229A6204BEFC44C14CB193F438F995ABA01931095433E24250802113A63E15F52B370D78731E7B1551B33BE53E80E780B499E91452049BE2EC33016F268E9A525A8E4253BB592FEF952954B7794F4B3AEC4143235BFBAD30BC22B78CE5BB39CA840A2E5B49DBB355D700314D8893D71A48EB3AF3562944586372677654A948151A9CC8C88086755C2CC175637068545323BFE2388B08899FC2810433231FF98CCF7D77FD00919B0B63561924267327C8E0C614F0476A99727FE51BF6FE9CEAAEB9D8DF13CB50C70A5C0AEE61CB77155B9E6B010AA34C84B6898DA28C5F8A2B0B7B63247A245E1FC1551A529B7B498C35464A4305ADBCB79688525EF0216963409CBC4CB5C37313E5CB1188401259439A1F15A1BEA223C4641340BC408E043D48A80B0B5B6BF180093230960AC8EAD883283120422D58E102242B548A65AD537F3D008EB0397818A2776B3A2E6AC9D363372D899980418AB03B3C6BFC40AD6222CBFD39C22B62222A3B5A992A25AB828C233C6ADF9412B4A7EEAB68B0772635A1977071A831B462B40D84CF87792EE9B0A81D7BC6B2205F09137D0232F3D2AB140C5C86510B07F506A9FB6200F795027866144C419453A3BF2F3CFD2737748AAADCBD64E5CF85CFA202EB808B0AFE73BFF626B179C86524068BC1C799C41558BEC0CA2E4CEB3DBA3F93A9D6D137DEC573FBC127576D6C0E569B4451BCE7D958CC32750625B1185765666774602938EC1F08E0A7A0EC91BCF452165002D422FA5A187F0345F1A43589B333DC70E0860C24C1779A5EAC9B9F863CFB2ADC976A8FFF0C8AC37CFE1370E1EE235D40503E8A52C4954836EEA809AD7CD4E2043011586E6A8BF3E76551D576D60F741AB6C5D3DB2B99527775CBAA2CCE5A2B2570BC3F759565409D19A9A604712AE80448DA8A7DB80413A177137915F4AE413EA050AF8FB2FDB4B6C99B8A2F3401C84409C7ED39BB6A45B318852E3F245F8596177C369BCC22E95F828FBB7796B127E1D6B314F30291B57212661B3E23CC349FBC05984C9F38C7A1205C8C5E86E7BC3577AC59ADD11B4ECB038FF74BB9853B1825B63FA842CB1C251C25BB41974C8C753A25017219B0816729C09905365565939DC677AE0788950789D66C7766A035000688AD401435E762ACA4527BD67965C44966130B7254B8AAC7059710B3B9397860EA243AC567869424EA8D5CA90A240E7D53ACC8693EF452884223BD72A61D6085FF2D6209E39163317949834579A833207E65ACF2458B1F71715379CBD828F0FD26864663D0268A6EE6723E9B7B1AFF4A4B6A61DA07887626B8F35F0342097C25CA5B8BCCA2A7760871CD8C6DCB346D006A489094A78E368FFF9450C6A099E9CADFD0B861077C80FA8BF6C7C191CA2178532B7D92347F77225FAB1669675B024E586D9B50B583278A2EA947B027BF85885CA3398471A81CD153CF9675481EC3D657A623852C8F72A02EB3A950F493471334B871915022B1510506418849C6E52529335B00969B63C6A8F1D8727E0FB89D7E61D6D5C4765B6A3593C3948AA2B2C2B909E1CC766A879EA278A921C2BC876B572F3435CC39CD541C390109CACD51946254CE6E62A3698AA5AE1C01DD220777C9DB8D89F30258A856332C4F540C1CB4CA67B6C3CE2782088B7DD072BDBB1102F403BC534C7AC2A1628261B0EB9A19452118E791220680AF79189002A9E9FA7A54AE38FACCA03E870C48C8B9A58F4060AB60322509E568371A9B3A959E5C55E5B99A0FA9ACB47C422336431D08915046644148ED3F87F873309E2FC684B5AA6BA627E883093D90351CC737E26364AD3251F5E320FFC905F2134825B7C7AB87651A656A9CD49ABEE5772745B798E08B00DC5C41B3C535890433DEB7316167C5873724B7B730923A92C87316EEC59A4192441E1A7EA88762D797D73F6193309B47AA0589707C342450A79550C67876D952A3DC6A65C4A7A3820116BD5F70B8081AA5EF8CFEA09CD4B156808582CD7D078FA2B13CFD5771AC18B76022AB72364111235841343497468F2AB3A134016041437D02237C997095FF0A016E6C48566241ED873DF4C47A05904A8852E56CC82000C5DF3F2769719761D3165942C3D7D3B39FF151C69793B33F699791A4691A934E98386B330340A396C95947DB2017DD2862321D53B88014066697614865F75512E08C63F52002FF96999D5CAAA31128EF89270DAB59A1E909991220C2E9C66C7563A7B572544871F29170D71E29B7A1433AE239F76B21FEB6B9D49E863FFD8463EABBAE5425C0E527A36FA70D4464324AB0CA35A3AEE68369491A434C3243335909BAA6B689B228D9540733B6DCA05015C927ADEBBA90B17014D5268DDF8A06D9AA979FA0917CA4A4C871C4CE6A3908AB158664AF5B5AB6FAA95E0E593BF55A65B710824FB64D7B7A421431A75E78EB7D61BC899628994935D4778CFDA77E9168307B2B6CB0164085634D94CBCA4C8B53E793A00DD8D2FF6A800C55E028012BD6912C48B13432C485B8AA0EE61555D5699AC5AB6944120D1EA030FF2CF785573B0E84FB772B9F3E473CCD47921B6111AF6A690F088C58C7267978BC5757CA9E542A441983BA113654A8448D7025AF4BBBA0306D56403A12568FCC8B892D9AAD06B809D5681B949A19BFBA4C55A26E417B164309E1E48AE245A81734C141D639D89677A29E231B5255072A37C90925AF0D36EB39737396C1A02B513EB98BC110870CB9565F2898690ECB0DB7807E3E858C57BB642CCC5EF0C8F00D733B3581382994E0C2C74A8B140D6EBC02405835986C2D913077B6C7BFFCB230BA6181D765620B0AEC43B8D15F64F51ABC92DBA2E5CD53021537A6BC06091D2C1E47B2E049A1207617D9A6703196623A0CA85054373D4283EAEA0A867B308F362BA8B26652FD346B4386685E6169D0946233BB186646D3B322EB726142A4C6DACD9AC8E866EB11708EED22ABCD86E2008814B96C976FB67A9399CF837529F18461ED9047F073B0DA1C0DBC3BDE180BD4AB75A81288B1D64474F4797AAF9170C674188061872E4B1BEB2CE82FC968EC9442DE65B59972FE55A56C81CB6B6584F86E566F5202CFD6400579C8242DA9CCECA68972563F155A9CAA2790219846ABA47000927B4231FD4E1131EA23A5B595B4760013020BE7DF38725CC502C2B0C33CC2593B44543F06829AA12B8CB6836068FB0E697FAD760F0251318934A1355AB75497C88D2BBC0F9678E4CA6E930CB7D445B530C7061488999C530B5325CD3F16824C034274B77320CC4191663C1E58A2A321952FB6A4350624AF068B9D95FF58A5E247887BBBB275D65C417357A0C29805450B2F2B859BCB612F6D3401FC2C4A93091C582207AA8512551AA33E4CB896AC42443A33DE84919F131575A3EA1607EB27B6E570A889FD6C38029BDDB6A0E9C77690C9855DFDB483CE35855A484BA791B151759D272925505765F546A67490859E28EA625A252C1C7F80686042B1BE9931E4C7A33166729481644E546BC45293786C10524DBB7DE822E51A44A64C51EACF0A743CA8566310BDFE61902FB7CE2A46950055E1869BEB391695E48259E11B30D34109D63C1D41FE491A35244C5AEA207E40081576B15C73098F375C0A5ECBDFF849EC24D79666D7CA68B92E449435BA817F8C3FA9AB2E0EABA14809C7B4484DF747A446D36BEABA3A6A443866994CD9C1C2B70005D87AFF90F4D00A7A232F3D175BFEF72FF"), + ]; + for (d, z, ek, dk) in cases { + let d: [u8; 32] = unhex(d).try_into().unwrap(); + let z: [u8; 32] = unhex(z).try_into().unwrap(); + let kp = mlkem1024_keygen(&d, &z); + assert_eq!(&kp.ek[..], &unhex(ek)[..], "ek 불일치"); + assert_eq!(&kp.dk.expose()[..], &unhex(dk)[..], "dk 불일치"); + } +} + +#[test] +fn kat_mlkem1024_encaps() { + // (ek, m, c, k) + let cases: &[(&str, &str, &str, &str)] = &[ + ("F6690E77251A47A0126F3CAE222B8873EB0EC9669AA7B08F4854AA72C0731649CE60EAA8B08198B8712158238DBE83CAEFD7641434CE1A1346F6900A53C105462B4F1E909E680476000C8E8FF43DFF9AC7F36657BF852262D383E68CA2C8764B59441523F009B5A752278491DBF89192FAB92BC873DDFC5B5FB49F9160769BD228844127FDDBA4949A1CE958B6FC1388DEE6CAD9F549287B7E0C85300D636AC1A68C1F0860AF00CE50963E3DB200F651147C4425DF62467FB4BFBD3B89DA337AA74444409BA3FA73B8CD1454F7E7C730352F894387E79C7547C077C19496322817D3D540E496922F1063235B78EA66BA23918E79115277965E67760A2FFC91530AA85575474DC344F11130FC77CEEC03ADF4937FD7FBC838C05BF88263A9903BA10718B6C43E97047902827A491ABFA8D83780191C96FC8924817FA3AB625C76132CEA34273B3D74589E91114216989C8E98C5B4EC2E5AA01805BB598CB7137E61CB6907952A51BA54C52E9E8C629305B7982779D0CC9D43E14C254277A5124E06257CF7A61EFC186118A808CE4BA0FCD31B34559D77643BE90BD017B5C90C235615B61258D6432EE6138A90A0B3DA38B6C8C20157644C010B6BBC327AC4B515E2AD5C46AD63BA0871946B6455A303448662A1CBCCBA1A462389B7EA4AAFD4A411E2A45D92146FE4508094C8E4F2747E918A2E57874C934A5C654F137695C67084A3EC50F3E7C777DC42AAC0AA9727015546CAE376C68504AA99A8AE585C2BE6E84E510124B5EA9169C356BE0239CD8C900DA93203716C3557BAB802C846C31948B7B2EB064E2DE01E9FCA837D4986E43A019BD351DCE5218FC1386A46684F2347F6EA999C07586AE66DA4A129FE3C36E1435B0715B155477000768EF7877C618579CCD958DCF74323E9C065A04F0F1B23ACE41B0FAA69DFD53EA2C62AF467B0DF64800E79C4FE72419958AB1D470250B5122A915EB00A44F4545DC83B78A0942702B71D9339115E007FAB9684FB3880B0E97221D5867D4CB0A4921FA29A81A939231F2C1CC5D329652052C3484602316B792ABCD55036E1F41FDEE702BA1216AB71918F469503EC85E9321AB8D4287BD537194560E1F140FE3900D1864F588B4AAC648D1647612837CD11EC6B730768528A96D825BD06033A4734B04B2AA6F26586A345605E59781F046738C51747564183B139E772B94194B3EB64407C462DC54C14CFC81BAB301F9091950B5A97BFF836990C0A419AB1C8F08A8D200AA3B968A19224E03B140C998A58B86221CC258C407F0DDA752A890D58425D6269B4CB03052DA31CD0ECB04B36A42D30A1EDF98570A31CEAA81EC9382F6DC012F63728F38A5983B50DF7C65491934F2CBB9CDD714284C0706FF79BB1F879B6EA423C71198BFA0AFB628469BB0E6F79C174463FDDA6BA4A78A886EC690FDA63CD93B66CB01E384395FE0281A7DCB55661B2DFAA7990CC19BEB96BCC29740F305FF3C72281A5991F3B39135B066389C1DAB7B0A2BABBEEE76BD99B394C82A28EBB275643C288E421BFFC94AB317DD5191220E9527549670C67AA4D26CE8A6AA1C8629601631B35FB9FB881C1FF03CA68C2BE4D913269274B04668305A6ACE7129654C51DC9C78EA9B07B489241A0E4C83FD5B2857A13AB4AA9D7139202AC08E2877624D07AEE9C6954DC7E0BA693F260B714B80C92F6B411D510E6276061451D99E54AD55954993B9624FAB31DEBB0FF700B10FA3D7C3952E48082F4D87DDB703E55CB65577450A375C56644501364B871766DDFE57CEFA187745CA3B6F87890C2AC65B89DDBB9AE7AF90F9BAAA353043CD382A4089C5744D11E78D71A61D01F4C345C5CAA9B385C7AF35A72545C677BCB363B129A0392033956064FC199CFF4BAAFD03A3413C65B025FC186475690AE93699DB6D0B71B285ABA96201D149968574613865CFA26CFEAE7C249FA5D5FB18480A752C368681A5969AEE20DA1F0ACCE6281852441862B1B4C01A1C0352211D49204A46701F72C52287E4C8A8FFD8668E0227329E84B1E0A8D47418A4AF44A6E80CBA470A4A04C1817156A87F4AB7055389AB3B51DEC6CD64CB8D7890B2D4BB85E4AB20AC3ACA6762B0100640F92040BE73906E68E8103A57669532120312FE50DDD47AC0B19151FD6137B16A09324948629B84FA2E7C8EC150FB8C7DB1D800C0825AC18915BE2529771B8390945E6DA65F324", "BF233CF6121D41585B4AF0EA74B35DF7ED52BB5782107A8259CD4AECC3587E61", "EFDB7F7E56302A199A7BE79C00715F670A5024C0FD357BFD446B4A9371079C5A04964F5F4D0C8FB3D12AD1F6F2007D26F698C49DA91E3B107D2332C7612DF3D7A216FE4F07C5AD8A1E9E2DE80E3ED3B79BDBA3E5388FBDA4311F79401798D193082B17692BFE3286CA442C5F7D4D63327CF40D7B0C420084C636A283E83483A469CF88E669129CF868F013F8796FD76AE474ED75AF8832BF0A3658A4EDB3FD2F37DCA44586C78728834226E5DED1732A2B3ED1C0DBF4A825818C344CFD60D4DB460E2E5B2B1931500DD22C098CF45F407D73CB04E83CA866090C244A11D08613BA174B06F7C58633BC01629BE2F41A695E8F9AD2E09C2BAB4ED34AACA0303B58F3B3E55D16F4A84B4BCDC6D7C4C20170DB0DD0FD20F4B7FCF8E6DF8C7BBB75075EEA2BA10EAFC071A83DF4B2401CADD328FB729CC5071C1C869DEACC3FCD8B60FB157B1C424491D3AB36DFE8D15CF3DADE14A109A014D157CAE91F1F7FEE9CAFAC003F2A6FA9F3E4C6960299C71BDFC168770A7D034FF52120599CA005886EE97FD4F15AF42BA209052A6A93069CDCCD91A380156A0CE7BCA73851D37B916C1C93F5A45AB48DF44DD2B1D6B23C91DE70F480C397B4C05A8B93E199D4EF247BE533FCED03CCEF20D313E2FE313967E4F4DE65817710D4C6947FF06A9508777712F4C66987883BCBA57509C588E8A71C3A8C7D70CF50A945DA7BA742B16CFA756856127E04D0AE0E82CBB39AE7ADFB8A688ABF811D08928EEA3304753596BA04471B8E3CA5F330E98CD3CD9EE6EBE9A12B9D58445894D5A5C1846BDB20D322545ACEC6972E829CF671B81B79FC0B140EC5940DB91C479C46B7E92572D8A77064784422E04E273375AFAB75982F6444092A2BC507097BDC84312C27F72158F70C3679FBED151796E8086825B15287857E19640459119732FB4469415B96DB7DA85462C2D269174BC7BF4D4B992D0B6A4DA92BAA2BE7F88AD0AC06BFBED1A5854F8348BAF9946728CBB42BB61FA288C80F07CFE669F9617747C975C835EA476C4E8FF288F65BA82F50AF0B9E9340960BAE41C6A6A6FAAD0FA15785A2CB77CD67AFE488902C1DB7FB2EAB64E1657DB282533DC909CCAA267A16502E0025A8B4C45BB6BFAB262E67B388FC62530950D93699FF078D21C9621B4937CDAEB45858959D64999F393BE59E8304DFA079663B9B6E126966F6DD39C54AA193C9DD8785EC4E5C13858E2914DECC4153C99B1AD8B8211C1DFE35858CF9736A4CA5B259D0B1AA528D52181DFE54061D960C939F6BE3EC1E8BEA3B9AD579B1EC13D4CC7FDC3166AE370C49D45FFA4AD483DB3071865007E2DD079518A2FA738DCFD94D79D36AF57C2DF26AC4DF777579E18DB9C107CB0022D0249D2BDB7B4503CB374E4A1A59061F05CF0EC6020AC0FA1965ADA850CAA5B89822E0FF50AC8AFA464B73E7CB8BFDE7CA305245E867ABBB35D3E4086E64355922B977CBDAECD0CDF272506A50F2E1D97E8DB39C6443F76A0D7EA0B4E19EBE7EEF0851C45714B59F74171114D2D5D589DA46E4D5DE7B83C484531AB0D4B76B786CE7CA99A22FC70F8CA901579E38CDF7443B44B4CCCC70C901EC3FE034B64C78F6B12CB5426E74724EAA75289FDDCCDC653D998A3A00C8C64CB47B5814130AACBAD79F55B14A0EF81E440D4E2FE1C8EB4D8888FAEC6898E23AEE45B4B8F6E5E64CBC38E498D57034DEC3F2D44426824635CC624A4B49BDD745BC52B1F151C193CA6855778FEF4F39E72C576DAEDA9F6041682AA1BBB2F134218028DDFC82589A2179A9AB70ECF613593EA6E04A47E83DD88235578610206CF6034159F376530266A461FAF89A6363269F6EA266F9A406E852F3B2081F1D36B6CA41D2DC0C4C644E4263AE36EE79081B352700A2D1C7A7BADC4BA24291271CF4AD3A02A8789D25E16F9E3755E2F61D9F018D79D4C7F2E5FD640E738CE055535FDCFF651B2FE70344168619BFBDB1F5ACF0222FD689070D0AD758C11D485121ECC0D2D04B1D91FDBE8B1F315F991CEC0E5CD0EE3065B0BB3D9627677A7D4D4CA0D539E7552D6B2D89758D8EC4A9AFE41C1FD896D4912F71CBA5E49F550118EABD0C67497C162F42052C4A6AA4009E2A210C5D4A3AE2165DC44D5072123354C542C80E57F86BFEC55500181C7E057D1884047A5E6815A9545B642BD631C5A46579A507253FFAFEC18581381D50FE491D319A1AB4A0D19689", "BCF2EFED1E45C35C5FAFE170AAC3F4F5B3EF11220EA6B9A254F0B90EE8D56B94"), + ("E877B205F0147851540681B6D2477F3D265BC02868E7868405BA3083151FBFE1AC548060B555005F4610FE37235E253E2B78CAE5036AFA411E6DF3334929595A019D0FBA542AC52D2572035A3C08B0572A2DAC4FC8C0022190C72354A35655732D57214E2202B570277810B76E56283F358B6F34AAAC70337E72A11D1AAAFAF7AAEDF8002D0BAFEE6026CBE0B216B8BFCF031DECE9157476B57C29BD1368A620828C9D71C9B8740A3C7C1BFE10CE23D82B44EAAB3287CCC09033D2EB9971B04DAC2859BF00C02CD464B82C44AB1632F53C625858A36F163F3C4065AA0693D96896BE4C04E51024C6089255B82B5E19479F13C836998A016A2A38EBBB36839903A3CEA6B0766503AB3FD86BA7F890AA831F449AAFB87C6A404A51FD50B68A4C134FB520BF437DB5D402BE5342BC463E59025F8D70C52BD5C714C0A09AC07DD44B77136A37C7D7859DC49DF85A5F2799836154973205AC75CB5635E5B7A9F94CD1A77C1F16CBD360A51C5B06869CCE70A1A92632132C26064B3A818C73A50D15421833C1D8F07D7D26CFCB2C6881F4C41E2B0C9C0A8EE205AF66D26FB1128461ECC5FA5283D03161AFC77F35E3383AC4499F48A40B72AF393A4A346005E5C6659BC90131301374921849BBCAF569AF99F401BF98380F99878D51287B876C4D7639D66611B820CED1C979BA399C6A69AFF436196014C465A2596CFB345F06B677966A2E476E2341703734067C32B1C8A16A50C96D4F552529D4AD65C25DBB293D223BAE9C1A2181E3234E64241A860F03112C0AD81E761A0443AA69E165B996065C98D15DE3BC7E5302C0999C82E1457762F98CF30556503C2BD2C1099BDB29AD9CAD83A150C2B92121E005CBE52BB8006500776CFA7C18B9B763F902A465215628B03E41898EA73AA5865965F52B0FDA601EE59C962E7461EB3751E73C148E00541658AA6DE35A80B35520056C4FB17AC0633E271C3529F574B39608FAFBAD8472BC5677C1198A04FEB9A623A200EB42988D945F6033BED6790019EC6C14299C7F5941FD08D043A2859A456DE4EC8A15090364048BB3924C7294A6E2B14C116349216396DD8280ED0A97B2930970C9BB545345761B7E64CC809A4BAC062631C7953CCC068A6133999120CEF5A2CA7FD452E69BB62FECC1CE665D9A62BFA6489482E4628F967E4E0150AE8726AE979DB805C9C78A3E48698B83F5032F44AAA2E2C768A8B2D641961AE48A1D04760444AA0504928A681A08D832E247953FA67382CCA563B76009C05CE3A3A06579781B30B944596C034C6C0F94426A70C7B2766654E4A2A6C6CBF3AC8E20C52A2554A23CB8B8355B76DA6C60AA5AC186F5612EE466FCF543C3493D79D4A0D8F3AFC66706033C13501352074052C44511B5F17B21CBC2D4899B61D31D32EA1A08B17C962A0F3A7B386DE7654E3C4DE7B0160B1C6BBE527C595640FCD8601E7A8728537469102734D98A97C86961DA2762369640F18EC0A7476BF95D96E7C9CFA3111585B2763A3E6C2694D96C0A08C4460AE4347C6C7B29748A18E3A52BB13F771843DB7BB503D82924636E8527009052AECD97701C97BD66890199049440498F01D1577346448D14A34B4230DBA54FDFFAABBBB462D86BA3BC5AC042C912FF4816091A3CF6260663E5A7AC9C9854316AC94C7C0915A526C809F6F1A90DF984C07C8486FB9B2156355CCACBB9B85294E8A584FC0AC02CB2B41A913A9895ED44011C8C99AF5869B1AA9C2A5C723D560E5ADC55E23292010407532147C1771B8125BF11B23B64EC16C299C25B40C0977C1A71C444A130B60FCA6F8EB98173287CAC7634F4E7693AF9AADF049E7FF86A777266AA3973A98C09AFA74F4C2463A64C8CA02796F17A11A907257F684185067F2C13643A72137D85092E880DFA83554A181F3F45B5F2A3131AD479CF788C3DE63C8A3C233909A5A9CA8E93B0A67F77556E8C20AF678ED7E5642176ADDB2B3462801BFE561073B42875130735E261131141B382BD80C7B11B007E22B979CCE1C1A08A4EA4F0BCEE75B047F58CA22C8727B57EC632ADCC64969FFB794E6932C6F74C37BBBBC18518D22767D112B262991D8BA77C3F3364A95728556754EE537C084CBCF185B068AA9FCCEC1899F6204B56628B592925E570A83775BB6C13239C5B9C61ACDA9B3A5028AE1979E83517ACCE3AF699E51F74A3D50991FD864BDC2268C6B24E0E4C01", "6E2DC22D76B36E1FC7978AC7678BD7C381E6B65627B72CF3E4077A86FC75949B", "526C69DD30685A694D35131C5A8C409F94659F33E29BA7F95054FEA09223F4B5AEFDF8463BC80A8FD1FD8B5E044EE8351A84027677EE6AC185EEEA7A70860F790075ABC13A8F7251A118D78FD8DEFCC7451823DDA959882AFBEA1D5817EB339F367152FD7E3A29E024C89D89053859D17D42AA1A78F30C135C01414C4E330129065970D87463BE54F17CB9C53DC25F8ACF8B556B177C0DB19CA2483563098108AA2E5207BFBF6DDA093CC5F89C583F7207F112805D29B4782CD6FA606B52356954A1452E3E0DBA5F225F32418047906CA389B87AC3CACF74355D1A97236CF082C7BB383CF6497A36A4A00640B79307C1F86462F100967F21D081593DF20D925EE88490C9C6720C2199577A48A7790647581459E6EBE303B623F6B0618C63CAF970CDC2946D7A879C33DFF88DB4548FDB9427D129C7B6D4DFD9BEC056F56C8D7D914F9E218CFFD89716C3DA29515272FA944696B50D37A007DAF4306F4189DFF3E0D96660E407E2C71CF26AA58DAD99FE0182789768E2EE24DDD3FE84D2EF896DE4FA81D39906A03EC7CE8780FD084CF6884B4E1CCD34CCA16D61BFF6C1B508EAFFDCFAB62AA6A34106944A0B5001C80DF368C0BB050B8CEB46F0601176DBA177BB83F1150A88C4A1AF3C0D8589F6BEA3ED78F60A290C114C74895069EE7B976FDF21EA9D6A32C8B4C432017A534DC818DD2972E49DF45A800BCB57392B7187498970F07AA4583F7FC99250F735CF3D390EDF2059AEC57AB135BC8CDC10B1FBD16D6D247F2DD2C524A3052C9EE85F93149FFCE8D95D4CA3EA4AE7B75CADD097C756C595B7901DC05AA8EAAE7590443CCFB12EA42F0893B21050FC286694E0990E02FAAAC9CCF762D298D5EBC7401FABED89625C3DF1F9D55860768782EC34B2A71C06E5E74582EAA846B5C1160199A9052188A5ACAE4C0C5257FA864E890EA562D396CDB35EC60864BB8FCD8C70A9009AC6913524C1FEE7036E0CEB7832E76C56C3715D152A3224D7EF51F427CBFD39286FE3D7FCBB9F4515C0ED3A874FF26F22B86BDC21622B314AA46924DC3DE6A2CAB5945EB34ECE830BAB383AADF6A20B852519EFD2E0CFA6E14E89C49FA99F28A234A91BF648471D93C3DFD7456A8EA347DF9A2DCC98F44B9DCE277ECF0FD46FB3A8F71334267AE8A35A564A17CEC14561ABE358350DBECB145FAC4FA8B01BBE0A5C119CE9BBE99F4B0A72FA7AF8B59F60787914E89068E492F1C8422D0641D17110F6BB3B3E5794BD4E536683538C4758273D6B25A16004B825F17720639F3974A5439541FDA29A501670F4AE549CDC33AB048ED15141A39D52C4F933FDD6ECEE573C70DF657677738F6E95A6C2727A192C6196CF6F5908E20CBE178F5CC4DA4533605D6ADF22501964D20183B877940F886CDC925EF61B797A35F5D71C39141888819763CF8201BFA236C50AA38935E5FA2569CDF8A092E043FA9838C00C1ED91B93963CACFD8103A9603BE5CF0B07541C1C31D6AC9404F369044446E6B853127F1BA6782D25CE1E2F97666D6572D7F4C3F9B5EC9D50F712F01CC46D3650522CDC6C64BD89087B459592CA9403016B0B4FBB327C1B5CAB60C834FE2810B3E9046F1BBA60B20C31B9D61E14754EF08D91073AC098D6BFFB9BC5959B412BE9FAB115C1ED59D6C592B82AD4FEFEA935053C3CA96895EF9657092615F00C5475AA9685E96E23EAD84B957CB7E82B0F58EAEB8699E9F3021333E93E33DF27383243D51CB991178E7287C17EAA5AA174BE83BB81B9E7F781E2B85CAD4F9718A2B2C90C2D1F2F860550BCAEB15F5B4E44B2696ABA3FD455CDFEBAC66863B1F470FFF2F629A07E306940210E8478FFC9FB6A7894CE292FBA95BF07FC3555DD0B8A41102D2DFB5AFA8E638EBB466C9EC432D84FB5FDED2A47BCB61248CAD67C8578F4E5978CF1DD19E81977AC4C4518E9E1D6BE17D5E18F30BA6E67F78F2EB9C65B36B0A80AC2BE0D8751826F5DC22C9C51FBA01D2B3740BFCEEE370207DCE7FEF8622CD072551896A4E0ED66BAA12F7FEE6EAC04CA511D1C8A5797252A71BC49E2354A0892201570C9A4A96C992E3E6452B21C841EACBD0F78217F78DE61D877B7D5CD49986C8B2A73A78A004E92F973E6043D5AA657AA1714E8BDC87383F1CD1D5D1318C0531A1C7C08BAD0E6B8C19837FA96E893DB134609FFB7551ADCCC10F6D5E74C85EC8F97FED1F9F248CC0F221C6F211B", "EFE60E221C3BAB00072536698A43C8D03B093F44FE9B3B8DE0F56B48C2570C38"), + ("9520B1AB029949EC4F0BD73F2DC7B511F242583778025B6FD0E7CB81958FFE1086DF36BB7A39696808883B5A16F076618444846B307AED6961AAA27DE8659E03E677E19217D0D05D80AA46ED532684493A7DAA2DC9FA07BB2287B4A07FE00B5AD7994CC0D604AA30686922C56CB992516C9FBCF87A31C4337387612A500BEB9616680B2B1547396669239899BE7AB3685443CE9D5A40AB32501898C98FD173682126C268A19D24813171031032A8E17780ECB9CDE40C36D3223998FA7D3B77BC99E945C2990523F1C07748048123243E26BB0278A588A7549050645F315852E891E2939619A84D18143213BA64E4E116315CBFD66A691ABC29FABB973EB0AA18169631976CF4CA4D36FAADD06CCE8BF2B15D851F74E622C924B921546289459C4F680401162D7BC10E2333B25C34BB4F8038E4EB2E6F2174580A76086CCA02C0503F506649E17C3C867A05DA66844402BC55AD6BA521D509059E6907C60A43E13B3AAA572048978E052179367C0E682C9C465A22F9B8363F634E857C094162BAAFD90E0177442AD81A2C83C1B872165AB5B5D52BB8C194A6A074B3C7DB1E7066925C3988615992C5041FAE24C44120CE6BCCCECB977E5F39C957570EB0972719E1645B80A6F9F071F7722071DB0706129DE5A1523B1A641B1502750842BE8A8905923DF4E77755869F55776BAD53947E5560735CB71643259E56367D287D48A1353D88224A280A0537643EA66FA133837C0165EFF6022CF634861299C0B3429CB20477517C51F60C127344E58C5B2AF7C05B141C73E8128BD5839FCC4F0DC3AEA1C283EAF3C15AB82685F4C8C89B73FA3C5DBB128B524C1D801BA080C93F8A0921848C5725EB46125844B5F1488749B85207A16100A4B2502F41054FC5C4B5433B6E5D37AAC7F3334192AC6ED1924347C0A4ECA08CD040DC15C083859C55241BF1AB9F5F43547D728ABA853EAC12A83019072A993D83179FF5E9C4D926C2E4C8A3EF9983D4401871F7109FB12251B4C865A987BA584E7E2221BAE52593269F7E149BA9FA8820B85EED036028092B0557A7BE27CF9B09CD1A1A5CEB768BAFE39E617A661C3B8A0748109002A21368616C67C211C252B3DB377BFC947EB22010C9C36297B45CCA2F64D626E43A9AF646C68AF5875099141D02701A94C03B980A37B09071F20DA6A1C707993C5757B5BCDB02B5F01498A761CD7439B8DA9880C3651D00356BE84AF95052941805EAD56479A66A4BA672C8A7A629429DA75314E5933C8C149033969AF46CC99DD4B1FB7B52DDF519BAB02799869EF14B83E085667716825F771D9E80B2B19B3412FA850FA9B19ED71B99DA4B1F6BC80B0AA5FFF77A53E77F014449877C1E7823912B979AD2516821A349D071B69E5382432BBA7E10C9B2975D80EA9236B35F57C869A21B5C194BA32DA3142FB58B9ED78608B3C30E8C29EED254CFD4BA012B7630725B5E6A15352C80B8DBBFDD62048C9695264C5E1864044ED79359666A9CB2642ECA5D80C9058AD70AB726000CB342334074C1967BCBE2319E83AADCF07733C13F25A4859A0034CB016725C81D0053A9E1FA605536489F23C71604A086CA3C60220DD9236213A8C103B9CE41463CAA792A5A320FCBC83B4276A1FAD411E2D9168ABBAC22F1932995C9D99420550484F014370C9186BE1C3EDF1C40C1A3411672B08FB104A5C551E20A2D3F4B791EB3A4B8E1CD48AC0C7734C4117B53CA7C8B1F0C697D448F7BA19EF4E90381C94257AC370FC15468C86213B7C6094704EA317EA7679858562F3B45A412B1C92BD37934951D2DEA27D3521391434A143CA58A904FC91743ECF47CFEA14B67B773CB7989D281C61D88751AF9C3A0344156085B7EA74CFBEA67D862C0561ABF70E17719C44483957D52580100C997F4F0099A98204E6BB7CA08BDEAB072DE920B70226037C31728F18AA19504158A46FDE9C42A9272B5CB525A8259D317157CC802A002C464A2CC68D3BE6C790F52D1C3BCDA495D8C5DC6F50B8B64481B70213DD46CECF33908785E80EC9FF5D561436B7B3A1963CD3C460089678F0C08998C4A8CA37D5B1624E85C1CF2911DFF7239E0328A76FB162811CF10904E949B09A3499DB3C9BD0FA1B0D935B7C455C8424330B7075DA6B51A17E283DD8373527C770F193116D08254D87F7AD7F57F47BFC3833D8C6CB0E549E6505914D07EEE50DADF41B39D3907E38E3E", "85962FDD6D092A8551968684B9C9EEEE736ADD8877D876C8EA62A2D5CC83E147", "851F92892D2E0E11102DBF8C99F61A5B7710EC70E41E677732DAA983CE86D0C396A1729D72217BA452C4592498024490D73EB7AD2D2F2E95BAA1B204E621EAD69513554261461D6740CEC83EB5344B55F5D11CBDD98752DC471C20C69F66AD51523428B19E56F7DA32F86E9662AF37CB65477B8058599D2077743ED4BA28E4CFB3B9D6DFBC895B11C7C81E0761D1AAA56A38CE776DF99064C1E3676C04D1E116120490BDED5AE01070A8614949F48DCA91082A9A809518040ADDB1AD7F5A9FA1BB5FC704D73CD66A0FE6C0B34AA39EC3FCEF531F66303AC672F5C9C6ED013A45167DA34553087AF1E78C62E2CD214D34536297E2BF3EA97B258F261373E936EEB6C4D17B7994542E4158BBDA941F5C107BA14B2C819E2AB671DB2A5D99A20294581B3F6D4983B820A4FDCAAAD28A9859ADEA0CC2C4CD4A415A91342387F7909C6904897EAE97F1A79D07D93F9A0597B89E3E80768A6C4B1B56025D56374E3AA67C1515B7FDB872F100CB844CD54B368FF5FBDE0E476378F2A683BF3875EA22AB4B46A83372AF9AE6CBE41753F69C98C65E07E7B9C4E8861CC2504916EFC70672CC567006342264383934BCD1000ABA678548F4C400E37F50E948FABD67895C3D6CCAA48DA27235BF1D7BAD8804FE0F32F66CC112F5171139D37562349A6507720DA44DDE0133122275C1905613BE83A083CBA43D1BA240FC7BCB8AB589BE229D3A219E029FB4306444BCF76BC0C9B6408C1E228E58B8B5872FD582040618B056277B659637938BCC2DEC198D27F2FCDA216C8046D74B3D035332CDCA51356BD275EF5FF9AB6DB9C2DF2151D9134EEB12421B427246F287761402A0B4D13D9B176DAB7E91D36723D0DB6A9D0FD25A4D2059327418BC3087ABE250B2A0ED03765758531F522C74A38EA705ECAA1100AA385B8FAA9AE4CC56D8E039543A1F51B6AD4A749F8C5B2A8472C6D4833A0E51E2475112BF9E079DAA72B36594DB6F5845123576DDBCD0AEECCC9E2DD4108FF2EF6C4701EC385A6E5F5412974D9DEDC635C3496EEC4DE3823CBF3EAB43798E37791F65585313E34E704A88B677E900AB8615D4DE9ABC7C3F450A4F71EA7D533570229FAE1C4151176FF303849D6C422A7CDC94D5152A17907E1D1C966B8357F59512BFE1CF1F93F6B15705C43579FD10C6C5527C07BCE41826C3B007688E2595E0FCB980602DEF50EE0426AD34218FF43EB5F1A049D33479A9C4A60F70900AE8F8226A4CE8BC6841ED50E375E563FB66EA358229BF2BD4A310EA58794AFF3572FDDEE92EF5D15BAAD8BC702E0ECEAEA0DFCC8B761FD671E75D3354548E031D16A5DDB45EC7B570A4A169DCE11ECB7E9A5F71236DE5837DAE1A075C258134C281DA01C7FCF15D41605F91F9D16C4B8BFC9291040365A623F61407F22F6143222C1B16D0D6C5E2C1F243E241D76BF3608FF32701A93F4C50F27D6FD7EBF8F502293433310598AA16D1C0C1C453089A622C2E6FD91D17A217853F942692B5748CE80839AA05E1BB31D2CF9AB3CC9BEDE428374DD9E99DA6441BB6BFB50FB5E850FFB463B7DD980C2DBB8A5CCB96F669489E553BE84D233E0316C3940EED75C172451CB0AF4EBD2B9CB05E3CFE292D0F106533DDEC77CAA49358FA79F3E9B5B609AFF64E3D1085A4C7D1E25FF47035E3923DAA1E9F5ABF700663D2A5F1AC9728FFD552404B63EBE406207ED8D1E0D8DEF1C761FC87432067885887F4E05136BC497B1BBBD4C0D36BE054108E90AF353E4296AECED037A7EC2CD99FAA76769283524324651CD18CACB401FA7C556EEC851B3F4DE090397C1D5893EBA09ACB224F18A0BBD5A5AF04D2BD1911288FFCF3B0C774EDBECC3C9E7F38723A07AEC75EC7F4055C353599C31B84878CBAFAE305E6DD9AB3B60A7A2EF0186639245F8D8EADEE7B54CA9A52ADAA9930B43CE4A7ED5AB1B2440C488D56B00A7C758F329824BFB7928470A3E3D0E8DC21955BD978BCCEAB0DF503EF4CA56DDBCFF8794F17D9A24F19173FB6804DA17BBA07DDBACCB483D3AE1DC8C4108DB95FD4E2B7D62B047C9859232E8BE14CC84B42A14F6B63450D981C30DB3FF0A62EBD7BDCCDD728D94CBF946B143BE39D73E28AA1EC0734E28E6D22AA3BC9855235BF6EB041A94C91C4B5FD274B4D09A284ABDD623D94AA6225B3E090195681784ED210A19744DAE1053A2AA6E3DC5848964624FE1A7CF73E73025E9F7A", "DAD9ADC089ADE86D6457A8833D41B235DE0579273F24874D689903199C296842"), + ("F89A32FDF01452B2ADFF6028F73C16510347DEC0400E16242CDAA614FC9A5EC69373D2A99C500414026B5CB2B67995B21C528E43A1B945D7A796AC2C0EB3BEA43665909585D3452F898374727A9DB92322A8116E18B20E0A3328B029995BC4B320F930318AA4966A125E3A42A4A3624C4939D0603E8ADA4863B9200D0A78BFC5AEF42A96EF188F30B7C153F83695B0CE226C2DFDC84CD724AF310733E4A420BFC4A405248647377EA71A2D3249BC11D15CF0C75FF083B453C338A2C26978B1607F5CCA9CD93315E2AF0631786B2424AB9B69711B7BEB790C51314C3147865C8910D9B22233636478F7070AD6BBBE983B51798A0211CC56A89BDBC2C3A0C80E47030FF074CDE1F080481809989BCB3FE05348D717C27CC32A4C7760BBC9EED52CAF3099E4F9B43435A673AA80895566D3DC8B9D1427604054A4932F9D68C115CACFCF3C2CB25B22EACB45C57064B9BC0D55D508BFA101BA01C06DA07450810E1E7CA9238A9358AA8592E7A89EC25CF8B32ECB81A38773489B4A6ED6F85603358F8F58833600044B488401EA9D60F99F4F58A292337124F15F47EC8C67DB7AA1098551880AD1D21736F83246AA921F18AE1D0529AA274CACF703C1E9394044B0DD3A0C2F776989161026729778435FB72AABEFC71AA96CB53F5411A4740202A177FF13AF362024B53AAF43395B53DA505FAAA1CA42297E968D27F1338DA6698C153A340A85EF6831991BAB36E3CB5B02A33B6C47B76989615C1DBE080639CC2A3C1550DC5C83A982A9F8A391071C6D74CB25C4A7BD7815BBEA662F5C51935ED501F8169D3005BA5E51C54053851545A4C22A8AED2AC7EEE60D4CB494EAF30B31504AAE91A085AA5F6A34ABFC2741C414952446BEBDF289C76C3EF6D25D47AC2D7281690B1051113A64536B05DD1936469811FC5ABB4714C9BAA371B00413E672AFFFB9AFE5A61B8B5758FADA01C681937BB2C23359A0EE9A0A9968994AAB8C7052A89454724409D05AC18ECC2151C534197028CABB521D3E929C54FB9AE61873C7517120766725B25078F414E8B2A672D85F23577ECAE849316CA660BA09EF7300F7D77E01F513BA4B628F12639DD9700EECB79997500D2AC36399917B05A433E51C28929C4C8A091F3C3239B6891DA2759EC004E6D9CDE60C527A31CC04B2261EA686BCF86191B18A7FE97BC4D2083FD71A914C52A36564F6C41BB509C76ED34C3D7C9502BB1570916E255532663939534A1A986C8F297ABB10799A0CA508E2B7358F961D0EEC71BAF67A32F953511499773651FFF526F4408EE11939D739B88BC97551C21BC2C665EC49BA5E258C2C3BC4763C361DDA35D0F35B83CB54B12C15BC894DF1B399EFC193CCC175448C71ADA17E48828BDBF09ADA393DC9F90FA0790FD9B8ADACB566BB23B4298900625C78147C2CE5460B388739223AAF2267A3309C635DD9486894263AC40F86B1222961C717C341BA514029DBC8DB0546CEEC03FE4A733E17345B240437E38D6A6676C5613480FCAEB29A5492B7839C000EE6720A72FA92582745499A4531218D92CA480D5565E15B3B65B01939636F2BCA54B4A855991664BE4A1293424E710787C6966F61C586EB559885FC819699088E525B815653BD431308424A50F30844161A01C331505C3F2AE6C95CA8ADF323310CF43965B2AD0CC770CE291827FAA218CB273947CFE7CB0838150C204520CA92C7B92373868B0B91C141D96B67F98C0888EB4B5A8695839C3E52317D94505BA4E654B6850C1F9A7F0156566FE462FA6C417AC8604F6B42C5B49C14E08CAD344FE64C7D787AC3FB03757D651AD49898FADA528D9407AE2024018C419D3C514DBC11099418C93235D4F823AB5A086CBB77ADBC92AF69C415A04BDCF1B82CF35B45B04304613767020372A145BB109FAD2442C495A650651F74C293D9E16F1E57485875A6E9FB5E16798090B41B5F30072DCC3C8A25BEB4E2B771D48C95B79BC481A5330A3AAB329C257A6126D8881FF075DF6C4554D621BA5AA865770A1B1CB24896B15E5A34D6B34F20D769F7FA293C13013643C89433980E8C9DE1A7CF718422814A4EC44B1658FC69BCC825969BC3AD9B7C8474511A58252246A7EAE2A8520B47C18A812A9B794F80392372340E564591201365FC9FA2D62926957F6F0158F7CA8725A61ECAF6F3465947A776EB3EE8202C718CEED80FC1925D5EAE3853A7A0C3F4", "8C65EF16D1258A6C0705225DB7A5AC1956DD97D4768C5CD3FF9FC52998A9DF02", "C923048E8DBFF58C2504A2061D9A8412C8A070B1AB07BD7B7CFD010DBEDDC6FE2AAC3D157EE7C56FE934759032DBB053401308F0956F7986A2EAADAA2D543E1DAA8496D4C3F43086DD6EF523D16E21D28EB63E0D4B7A22BD08AAD709B9BBE5D513E99BDB4CFC93D89B5638BFC0A58AF607109D273866F1F29ED2B5334ED884CF6716E3086AFDDA7740FB395057DB7E9A4AE16F87FD3A0ABF51055DFC2F9CAE85E5B264A4C0475F6DFC8758BFB19CB033D071E6538018101F3990B7BA0B951A2ED69707DD61AB3B27E70C43F31E3870416AE1A5B27D2083F544E3F327DD68AE18449D035A0809A59702FB7AF84302A9B1C3B1626951FEA1F918AE20551DDD938A842449FA4F475387085C2725BB282640D235FA8C9045990E72AA68B11F403C40AC4835050830800B2673068F1E164A2B0CBEA439989FBDB8EEC4F86BD147350BE0FE378C2A8D6A07333BB004EF367EF49E76832E758471291CAE425E8CE2B5AF5BA239CAB8E3E6A8A0191D0C9CDFC325466F02832059C39813D602AEC76B27C3A6238FA59A78289213A7FDFFFE0F1E48287400B347DEA82189FB0847D538D08617F5344113E4E3D255D4A1C3F55D10FB0403B3F9DF5F859E93E2FFC5DB530E0E44A52AD26022E40856D55714C580C7B9B893DC1D7E9FCF3FD4550F2D746E4CC281E151E944FAFA2D867F79A10B458B2CE0BED298CB2A2080CC51F6CBD83C02391BFFEB472940FABBC3FFE73AAE6C5A1057CDC4B984DA49A2EC9312594DAD1CBA7A21057117A0039F97FA26824FA9E5AD8350A4022A677BBE8BD423E81CD5A217FB2FAB858B745C9F3882F7AEECD141918D72A8F708B33D2E1C7F2FCCC9AB62AFD1BF6DFCE09262FD1E1F6B33DFFA19AC556F8168ED06CB39110D5141EDFC4274667E7F8D99248C7B73D001A31E0557802FD9915932AA029DF07C1B67DC429306AFFED25CCD2C230E5758719AFB043B742281D4EBE08BBE4CBF86E47E81E10669C992CF94EDB30539946859931C5F4F7FCA855361C081957A1871F695CB3902DDE061610FD1EB3A2B110B954A99EAF083ADCCAA47A521027076A05C20D325CF21F7E8D6AC6DA58438EECC5F03133B7F53F37EFED2604772A8D807CB182B81BDC77C0B7113DC5677954F58056B0D22C876FAA9712EADA80D0A6165E2FBC09882E4CC4EC7F222A88693D58DADCE94F1C1C19C4F40DAF7900E869741593458FA685B41FF9C667790FB096A60D98CA2049F42EDE9245F36E52722100855D1F3A31C8265450C9189F75A2986090FD324F62A43CAF94D494376A4B99C0C6DE5D77E316671AEA390CB412E094372A8F966D7A4631D42A44C8EE4E5D6C6ADCE93C573BB709AE24D6B7C6BD8E3DADBCD53DFD10C6BA7EA4887DE7E676E2FEDA3589EA35022D15B04709FAB44580B10D04C40C3519C5BF6D963A7929CAFF87413BD63EAE1D463C29BEA36DFF36C4D46477052B0DBB43ED78D3C472FE90AF0A1C8C0770B7F689B028F5C06A0E4EBD4A110C23BF4C1CFC171BDDCC16153D0FED91282007EF3924050B728610982D753281C5D0BD395123952C6B10EBF98F65E08741400AA09436D7D0015B71B0E7851E8D98EDC71281B502DD93C95E520F11ABC0D78D87F45E20E046411368CE2B494B765D2F6AAFCF1F89F4A8B6680265F0C5C597D3D0031BA33BCCAF421DDE1C6064B19F8EA4E93372B6984D525172E976C2677D8F178E72945235530A0CEC6676CB3427ABD090CA8870F3291D3BD10820F8D47EEBDECD784C06554E9A47FCCB9261FE09AE13A32A677DFDEC7206670BEF74DD09145F4AAF038CF12F228004677822F5BD24DD833DB12B9298655989DB96B6147106776B4BD7B8E4F547363AB5850BD685B13497DC98DB454701258E08C6017EB66C15CAD5D472E09A72EBE2738E1791861E831690830DA37F090C156D5424706736E53C1A94ECDA15C5C00C7079AE018CD708D14103EA224F3F07782DD59185C17D1EFB39984A1EB6A86D5661C4B8AC328602E16FEE80AFFEA654A3F9D68F4CEB440B81E27976F4A4520D72E59AA69BA38AEEDD2C01495C2CA736202B4500115262787C6E8F179D1D32BDB168E66F820210D8028AFAFE2058D1450F71DF21EB16E31986ADEC5A1A30FBD919486EAB01C8F232B91D32D52F61961485E08EE23EE3AA533FF92041778005D147B2A36AA0231FAF1CAE710DE693C280694FD44203183FE90581A", "640E9C78F8CC336881BAE27ABAF2B6A8761C3D902A7E0658D444F36D19C489E1"), + ("88DC9E381C952DC3A5ABD9B485D9198B644D97AA7293A70B76C986A58B673964050737810AC77F658B30A5DC4D6ED0533340B40CDACC8593A7F3737CBA319A25015C1ED3C25E0277B725495F47711E78BF3B76CCE7268E1694CA8EF80F3043BF4E854157375578916D0F0C6FB4ABCE3B730788B75C1FD0076227CEA787AF7F04B680A4CEE0E4A97821C5185126B8763562B0A4ADC45E57A217F6D250065A18591C0D7BC39582059B8AA5907B2C10E393B73F423964D29775367CC772892ECC11A8C81C35749E69BA15DAE56867C90819D0043652A2B706A55DFA961331876862049CE854B6E379CB94A849D170B5038B61693A254B076A55595042B74803475496723E4790BACA6AC3E93628464F6677BA892235345CA41C624E3B9A8EE5A890157B6517B075C65B6C0A8BA946FB524FF64AA3360BC0F4C258407A697A77BBE36CEC5C00D1A179C5C34F542C497499454CE1618DA95D6C478CBEB52D4550586081813761A245775538764F3EC42DA4CC01F93945C186B6C46C2BA503529F42688262120ABC27551C04F656515B7C161B9775F7296408C898CA6AB86FA1781F5988081777FF9AB929D717B0C63295637255A369B93656CFB9464CD260F458B9838040EAFB381829929DB77EFF79791D98AD61AB98D8F6694DF5014E6B00757A448509481E06361D3031678607DC7764D76225CDF5202494C43EEB861AA495AFB1BEA346CAEBE2B3E0A5069A3882DBC6181B5575948C76D2176D1B6746A6D8BA23A405D651C3294BCB3A33236D9068D416012F225C0FB62F9427B298B33C0D217919F534BFFBB7752674801B3946173B7F007B32AC822DA3A1CFE711636246B48AAA9F9C40370574456A89EBFB45565739868C810E3CC5728CAC33982024292033D34FBBC0B16C25A4A7C3C0DB085EFA293C3AA525FF1856D9E8676E8235958C3F4EB6A4C8C86B97C78E5D7022F74C085EA6925EA78BAE44574BC7CB2709AF112B4DD46AACB20453F3DA1CD5D651C5B37B46E5AC46591767B9222711C8DCCCC2B49723F351B4F6519871200EFD3796C502135F4999F74379A176B35F2B761FC2ADCB698227C0C93E4C45FB270E28FC3558120B92D52002CA38B4E7201C38346857890A2B1337B9B14E3A4EAEC98D221B038249A133E732DC224BDBE7967ECC5809C91490A90655C38A9CDB99CA2B552DFC3A1A230A7C0918FB08743E11346FDC353C663024FA34591C5002797E1BD662F9FC3685333226188F2EB1C599D2C8ACA8BBF53A9248C9A677E43180A27363182BFCAABC5065CEC7C257F300C1F6A2A0B160C4DD6A90106289F508BF26D0A3C0206CCD41A390C5452911C03AEB0B43E5628B67B89593122583C68AC847A1CA4048F31146E1B39F26AF91ACBD42E8496A3952F6015247142468C42B9D75550A684D0A2B5508508B0D312D81B40C08A06C8B6681D5CB697CE871AD15C3987542C92AC4C71C685D802E50F7471791306C29507795AB2C90996933C1CB576F680C86E7A321C5B829AEA9924362B4BC61B91DF16A97025C93A8544DDA6A3799AC0D81C9525AB7601382444B780BE4578F573DA2AABC5E032201B47E2798C815368F3AA4B17970747506B4CD861291F280BAB83BEDC253FB3164DA84455924C724F24E99B2A8EA61AD9D775D3EF516D5E65BA8304FE9660D829AA793D4C7C09804FED3B5298C16313A216276A09AD1185BA59819B65814398CACB1A2EBB9BAD874224BD92E79E693AF9487AE964D599186E449821DD081C16C6B29C95754740F86B07A433681C06B695EBB0189659F3F02A96BA9A792E22BD2856E5B4818FED815B40092CF4B0EEE20288F2266D765AFC8658913D7CEFA353C03C54A5BC671A8091C77CC67ECE5487193685D58C5B66C50A434BE96542D6202B51134107219B6C6E636F2F96521279648F1261BDB900D607303015D05C4459667C91C3539B08B8D667980DE1220C9D7722CEC5488A442E835B7E29000CF908D5C7587CB47BAFAD144C3C79828B9B4DCD140E9583E47F32B0E86A3DBF6087F40042C2A9612F73969080B35A0193D3A58E6F891222B28C351A2EA802F27AB79D5C76D29895AC6695A472615D07852E26A15840739E48C6C69CAC6EB1544D2D957B192C630B4A61E2527367B130B3C55AA6B51887BA1E8D08BC2291A57ECE2E03632B65EE430C5E77CE844C1BBDA7FC5DB7813E0E4A79DAE35B4DC59", "2C29A4DF2135AD034D6172BEF7A0A30906A82EDFD7E1E6FC5E5A00C18B90CAD7", "BE180A0AB0F9C995BE4A132A8AEB443B06871F20BA11E523B8D773E969F01993B736AA164938A19B45CA65CE882BE28737536850299094466CD896E913D1BFB19DEF04AAF36ACA42C7905593C7A985593B4975547110A26E278EE15848C6EB53A4A119686F8AAE0ADDC948C8C3EA4B9C57C3CDDD0D2703E6DA55E48CD014B65E17189E95F8EB1342BE96C69903B687FD904A0A3545927A92022CD564C1BBC81B907C88A2518DDBA16FD3617EEEAC93CF24682365686E61E18285EECEF9844A4E2A9841FBF3606B84ECFF32AC3540A36F986EC4D8DF50688F80CF73896E2380FF2E5BB2594A8F1F51079EFC121ECDD50E18943109BD60CCB25529ECBB495AD312636CBECB51696388F811E0EAD5FFD6FB17F12C2C41CF0039098F77ACA2C2C18E84474C2AB4614070145E786F4F31095DAF16D1A01E3F32A0E39A7B4A3050C530CAFF4ED10B188D7DA519B6C79441896E3CF954BB6D055C59E13D307B90653D1E712E1679249F4B0AEF1F8C821109977F1FFD8FEC705F18B0E98768BD5BE2F1E822608E01438A23706823A9B7AF23349C75D96D143A7DA1F10753967614D3C61BB4422DF5AE2B5555EEC97EF35CB859B108637AF1514998B7C310404794D2E992843DCE5FD7002A09A9183A0B4C3BCC4100A26C45D67C7B0B035C4B38CCB27BE3060D862E888BC0806D66CF78DFB48BEC69FB3FF477A2E78DF79359E4B740AE8F32F97EBCAF0DEFE57E80760F991884F564C2BBF779AFB5C0E475C1BCC7B904F2503B4D1D467BC40DB7857FBDB5D43DC8F0DAA5DA2F09A743546C6D75DA831549EAADD42B9A7C8F690883035B0163A811A748A7F8438CD053AD66B78CC8E68C44207620FC52B878E0854E8B32F14C1AF93D84B53D33DEDD915CBB639C70E57B8C36E2707FAE9BF0EEB9CBD92C381C2DB04F24197EC03999F6BF6696932D2A24DB7821808F80F324DBDD1E54C8D1F557FCAAAA16E6EB0C3A07B61FE130BF5313CF9285D7DE7FC0DC53770F3F76FCA52538F128A0CECBF16B5C67544AC6998088F5047A2074691346A457C0A6C06EA571F881A573F20571F0BA4938997D4B7B46BE93FAA96F984206F87CC0DC47DE66E84D0C66DA2585CD6F721B9BC0ED71EE491866FE54E6EAD69F666C7B7D2C4DE76E2F1EC41B7BAD71A9876F4D002BC263D113E479F926051463F5F51642D396428A3295486502DACA6C6EDC27AFA2F9A1590C3B436FF23DCAD070922AAE811A408A3155AFA02C4EE738357851FA190FC42F2CC46F88EEA43A565CB88D432EBDC6772416BADE9D49B68B53D783F4FCE20F7E7FB0533039C63A9F46C073BC2B811D730ADDFEDF24C59024D246993C0D8E6465FBC3AE84011FB14923493EC110F94393A4CB3D1A475F5576BB78C0DF7744009FE534753772D88892C71BAADAAC58CA9C038CF43006E1AFF5B3FF1FE095AA6F0026E76DC15FB1B95661B7952C52D535D18C9BA1832921002DEF8F1BA00C7D6396C0DE3E851A43ECC99268C169450FD96EA8BD988032FDE42C3877119AB8AB6E3115534C3F212F4F4EE984667F51FBB6C2C4B1D7AE8D4AC9C977514943B26C23E30E48D45415879306B41253F7C4D807601C096149ED9619005CB5C4D6A12B8282C5F1BA3F94D0EA15EA96FC63973A11227489E0D5C3CCAA4662240279C87E9F2DC498FCC4AF71C7569D2A15481EA98BD9FC6043AE722D1EBD0920F145E34641950C576D11385E6AA10942C3DB0C6C961494C3552BB533AE90EBE4CB5EA114A999475A0CCC2250C40670D0061FD08B16B627F5D6B44030A9C4B5DB9CBA6D7F8156F807FA22459791F763035844F1078E06E595731C487BF165F59910A294C9A8FB42115BF495A5E1EED6FEB180F36B766CFB6518F91A5A9111D5E1CA1D56471FEF1A8DCFB6ADD0A85A63A953673BEDFF64F93E0E7D69CEE76E07FF3054C4D8BD3DF0B76D3251448F6D1F89D7AFB751910627D88DA9A3166A5A847D274C7EDCC44956EC4ED73B762DC2931B47782E580E080E30AE50A539446355342571560D283D0F51E0728A7284680FA6EF8D4B22D15D48CBF93E631E642EE7A3366D3908C5343BDC00D251296E55C237943EADC3290B1023C8797F81F7ED47A532CD7CD3786BFCB9CFFF954559369E30EA309B0A7A39E8D2492DF848733E95164DA419328069E98CACE7F04CFEE37BCF8508E83ECB3CF091734DB8039551BFAF71A6492924E939", "27B870279BABD8038A4B68BF3E1529FB88EDE8776388499BABF6CD371419FF20"), + ]; + for (ek, m, c, k) in cases { + let ek: [u8; 1568] = unhex(ek).try_into().unwrap(); + let m: [u8; 32] = unhex(m).try_into().unwrap(); + let (ct, ss) = mlkem1024_encaps(&ek, &m).unwrap(); + assert_eq!(&ct[..], &unhex(c)[..], "암호문 c 불일치"); + assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); + } +} + +#[test] +fn kat_mlkem1024_decaps() { + // (dk, c, k) — 정상 복호화 및 변조 암호문(암묵적 거부) 혼합 + let cases: &[(&str, &str, &str)] = &[ + // valid decapsulation + ("E4B354B3103658F58F653293EE21661041C205B83F65236B4BFA3B2F4142ECE48F0057117A95119B0B381EB04D3134BF5D7112E7305E32AC956F18B9D2C687DA9A38853154F9DBB00C371EEA194061048C882958990866475A12E00843C3388EF9C57A5A7A7111156B71C07C8C146AE810A20B330412E11DF6F815C03981733B5ED47C230AEBC76AB671B02CC8360A9E30F162A57852548B5B83E32F55A77BC6578C0985BAAE813018137A5880BE719C23AEC5864EB96E5838B129E1762EBC92DC2934CE522F89709302F534D8758DC3A263D0A7CE17A965CF549C861389FFE2CEF21C3ADEFBB658778640588BC73452D5640ABA4C7262F6C314DB1D9B72490CD7B71234CA081388FACBB6C8B8B6222616D6E12CF7EA3449E853EADAB85DF712EA2121C3E39774695966DB4BF3E1C70EB6BEC1B06AEA3A8D2B44895F538CEC7B3305559EE6128DAE02815547AEA76123A3D0C4C9C53B7B25ADBA9485D70661FECC8CE75C88F7C242EECA1CA58C3804EA2C78904BF009810495C37777C334C18B156128D215C943853EB4A61F6A6B53AABA0901A0580FF9A34D028833080181518CD720211C2B7FEB0CC7F3CC4A3402BE3067212052C7D1933E22A893B6400FF457CC9347078FDC49DB4A6BC435497AA1888D2671D34550774C5EA0D7B7F28A322E376A8190C807B35644BC912DFA145BA2140535BD4A91062EC094E7E76D4586665A2C0867451D76D1B0F17264244A52C6FC0495B8533FD9219341567D3971CD0434D4C176AD72424D612B0E3503FBD986C255C994B7C42D46C5C8254055AB86576314C5B9677A75353C11AAADBCC590E4BB7268AC6319C0FD564BE5B1A94ED9012CEA934BCA1A8C038487E5A636210B9BF23E1D9A25E1ABCC6DC619398A0D13B63509C6690C2800278B7D29043B739229206839A4A5555DB694B0995979925D457C3C1151C95630C3D050AE8C79385BFC4E4F515342788260482367DACFAE98881B73A5AB616DC5920414343DEB8603D9296C44CC292FA5C9D4C590BE547326290199C939C73546B6CBB954937B625B28D3D017EC5098792534ABF7C3D3A1BDC4D22D79633EB9514C27A6B81B21076C392C35700218766B7F94A02D948AB63002D5700FDEE34A98002F5441CACCB3862C724D912B7DB5F847F0BAA69E714E34A6BAB270121975A7DFB17681675FD3CC7A614A78D9F322FC335BBEAC38B02A19AF163D06F74200F60FDA372492537159C0AB2F13692A574F6C089F562444FCD60F111A506C75B18E96A395150FA4AC775356C679C08BAD138517A9B994630060AB2CFF631F35A690749512146ABDD9C8268DF0583FD75FC0D31A15813238361CBD112F78D4CA272561C0345948965F2C8A13A0018663D3B28F2C8A0D2627FACC5C591B40C82685F4100832496225D19DECB838D263AEBF236E59B33C25BCCE6FDBC795B643D03C2DEA5C82E82172BBE903E4F2B53A86BE81DC41D74A663288495AD6271D6231C9F06B4BD9A121765DA9A8397F0747C57488DFA3507E0569D6D39990537012B26C298B3E7DA27081D0AA5AAC0885030C94BB418F463E98E2124F7721D2418F2BC5CA0858AC49924524873C1EC20376D5099B27BC835AB424C46B2FF142F316AA6BB1C0A92B0F67B68707C2863A5812C6CC0C88B92B3C8B4CC1EC34A2766E5B1B9EEA4B367CC51EDB5B2E359A563F54848B7674A33513636172F166BC8B3211140C89DA979DEE0CB6E60031FB4907D0B81A8E0999C7C301A1094CE0B8612185BCE6A8308FE10378F5AE6FD413049833E263287898BE233B7FAB6B4782AA19D063A3DB126EBE50A7F458A3DF3511FD315658B2417770A072449C16A89DFC20AC6B94C9F83430B25894F9891D718328DA407BB1A478072C04EF4318B745CC394B12AC0C3E493342F4C822DB117CB99C5BD024692C769D545B398D207096436123BCC0C3F611D065BC049CCAEE90566D60B5408A863F444329863547D331CF9638E25054B8A92E42038CC1388FA65947D0A57AFE54C893E115304C3A78A807872C550B7774DD22BD60806C0F57C7422CB247719CA47B65A6B3C0FF16B022B7815FA4A022C66BC3536301D91F90000CCF5963505769C96828EF013E80356C1C0694DF3A6F1BC8794BDBB00764C414736E153B4D35188BB4B93EC95C37BFA70E8F55239B6A1A6DB5230DD11D44FA8BBBD5C8FEA397736A254B86A458288ED23735D7CCB69FB7752F53722EA16DE41C5F3C71CBFF5A1A92B728C750520DC877A3DCB3855BA51C740F3E2769EFF2050A433EADAA364C4145EC20999BC70A5E73C69FB84307488CE2C116E6CB1C4190A7728A6647927E016225EA13A4669CC5A089452AF94DED501EF3C72341F4C2E868C1B6846C3D86CEA575B5AF5273B0A4591C7849CA88CA93CCB83E5229B2363C2D33436CE4781BA2134C3047F3246AB9AC0A739054A444CC60D724263A52AEB92BBE6CCA490A2240C76FE84A58F0DB3CF2625F4347991FF72319A642717A5556C73B97185DDB805713712517291DB52C2A98E468A852C0D1B527831AAB4025BBC6D1BF2A6B2D5A20587B327DA81204B56771131742B88C7CB907A3BC6C2845FCA65A609E5B9B132E6A6004AC4074CA0E1953212AB62B841842A452AE2DE0588B22BAEC68B8CFA428DCB993AF056AFED8745DF90737533467C194CD72AB39F694DE930C5A971109109EF3011DAA8A3FF6DC6CEEF03F57987F09B23D576C4F55E576F849AB41C4187F3C95CD7084B8D24FC6601746C73990CB7DA8507659E3409B709E4836043E473ED3871B953846A4666EE1D29253325F96028AD2B8B00770A2144A32876856E1D34DC251CA6B40C637FA46CD0A0232D2B8D5B94AFFD3248F7879CFD3048EA4CCDB51A20BCB9D1EF88F323CC24E909D29CB295D854120C00CCDE0883EE4BFA486889D05481FE658FE958273A89C06E6897C546824FB8B87F06AF5487884B885281A17E9FAAE223A6CD8667206271869232E084806755B11DB924B2EC99C26BB0285711D74D1C7208229A6E82938A03497E1016FBA846D29B79131CBB5686C1E465E90E33A0AE71990329D0C488612C83505A2B599592096E84E2A579C09899DC8405256C03355BC89A35A9BA075B596A644BD825280066798C50E437BBD556562A85485F7CA66A14B8084540E86D58722BB4013CA02B4428AC48724CB68B77E415B157B5F4134580212387B31BE7A76B5B9869B17C424C2DB76EDC81A46547E19DA263CC6983AE36FB0D07D972AA3CE3534C6C6418F684FDCF59A1CBBA81F229137A79516D2098979014C274B830C9381463A9BA4496C623E18A36DAE521A0C183C25470DE64CBD4A7956C69234F7616E00CDC5F5C836FD806BC0374C9A209F707888C953C2C4F3318C197119D0926B08A8CDCB0C5E848EA89381B2E248872280B6B28CF5917BE56B1E0FC3A291F37CC3387CD7E715CF8C2455449EDC103520C4B59E39AF1866205EA9A6A0C12A1F5AA5E9289EFE4984070BBF553B18F3593009F19AFA2460D75BB0EFB44657DC2F831B791833A2DC6BCA97417E5592882ADB071F28543CF1BBEDFCA19D7ACD363C4BD1029E3D659D89598E5121236403AAD7326B3385700E840BBA28B388E5A16FD882912A54997ABCF215255F024B6DB62A4723B228D5519AD7731B7C64F8F5322D628E4E9B03E0914D628C73CF812BCF0745615B70A0B704831306E032C4678C9978134398A40A0A369B9CF6AE2E054913981C176B4918AC7D59C19CCC7805A865CBEB8194223CA6C4A095588A54853487A4C74F582454AFAB306C02880B2C0896A522F5EA3F93D9A1692356D9AB9BE1284B8D6271892584635913EF08928F50AF5000BAB12CCFABD0C6CFE6AC3A3202360B10A6767BC4A281B9C27780A38AB370CCEE65C40493A9CFD56B9AD047BF483F52A6155C2C3588039E0A7B0D6DCB3239C366EBC742EE275A0805C52734CB7D5727A6512315249D31FC2147B82E2ED07C0D31033C194B3D7C78DF98A060603A2F3A126A493A9E63311EAA5F63FB26211410F0D52304B35B9C104515D23FD8EB485132B5D55AA05AE5754359763FFA1EA4208868E6ABD4EA19CD052AE5E490A5D7B309B88FFF788318D060D4841C7B7031F5726D3CD8066F702BC070A2FCC91809B17708FBA51227CAF2F97522F3CD508A8FF460BA1D453F11790BCE7604CE254935A2726352CE7A14B1CC26342F6A8F9DC341B4EB7D6EB00C84C56EA6E8B948F9B10498683B3B737CC367FF54CBF729984E6C66A2E00130F28DE5E2AD39471A4DE910CA9A1C386618B5E6A8806CCADAF230D6D765C7120A68833DFA5A45E6F6787E802266740333685384772D5924F4DAA74B421D42C31A2448EFB0B174EFB7E3EA56A579F85625F1589138BC6A1077BBDBC56551E9852973E8C8D78765A165E2E0400B555957865B794E22CA92EECC1857A6C4049902F0F5C3AA95DD774D628B67924499A23C6F1CBA", "91B411C6C8DE923DD689138E85D8919624987853AC214C821E7A9221BA6EB24CFCA62D997E9861FF34BD9C1826A5933E326CFE627080AD0491F62845C03B402977349855D79C15551BE0170C9A2A93952A9D4E3EA039E1644567D1CA6017BCDEB89343DB32793355A0454CD6777CC08C47146FF02C9666BF9687E032F6CCCD1C44718031508B03A3FF0648FD59B7CCA2C09DC4F3DE90A61BC73731F37C01870F96F69DE032970C2A8CE803A76EAA514B64D9174E65C6FFE0DEFBE34E0DC5C5E90359B2205431EB2DDEE399BE7C24DB27321502D0798459A77444B9AA68FAF7E9E35E11A3799B1D01D552F7CE0EB82E71130D52DA6D1A9AF51EC46D94327DAD227595ADA79A13094E8CDE25441D2ABE3D2BE3514BC2C4DC25B5C0FA08C2E5B0ECCC0DD5C565D2DA22DD70120DA74E938290989225D034AEFE483D650152C877C37B66B9EB145367D23537117F52A58C4E6FE747036C12235D921472EFB4232687BCA48783DDCEFE436256A1A37EBD894DA918B5117FDF80493266411BA9DAF489372282DC99824F9782F226FFFAA905FD71B4BE1EB1E18B308AC9A601EC2E0138E077E0D18F76D6907BE0340CA00516D3F0C2C260A12C4AFBD3955B5465E72416709A887DDAE8F412FD0F894101C82C8FB742426A5941053AAA715F89753D7FB5AE79625E58B3ADB943106F0701B5AAEF7AD359B3DDCC963B446D8D8CA091FA13F68A2682FD3FD04EDB7C2B12BD791B39113FBDFE2862387D61DCD88CD47F240C293A89F994DB2FEAE33FE226ADC2C1B81F55290F9F8FA44782B009AC84F4FB35E20876DEC20A4C152ABC853E72BFEA5811CD02AAAAB5BC9A6AA2EFF4ADBE9CEADAE1FD3F55CEDCE3E20F95B3A05A0D3A112C73A1BA99457A0ECFB4049CB88565556C5A2F67B1DE90FECE15DFEB01A6AEC912323B506D03D3D174275A2BE623893EED0252AD97F15D83D850CBB38C8C901E9370C8E4591F4C29199388D581AE7474674FF25FB132DB0D31B82FF7B504EE95D6999972BA3A7EFE180C3BA9A78DF242DA7AF6AD4E35F9C0639629835ABFA123C065DEEF398E4E2D0D2ABB59B9C26192A2FB2D9AE018A92D120297BD88527D6A84E89A1A6D1550D77653289E72DB935518CDD69336013040025C72DAD96F316EC37618ED6AD626C83E0DF50267C20DFF55D34E27CF359F77FB19C9C80D6569961CCC62273931F1E835CAC96FC60BF5BC94A38179E7517697C5A345018743AF1512D298167F6EADD9D6A0C4C6D026F798533221109869CEB9F0BAF5FEE25426D970131E119EC07217D7972C82AEAA3CB6987FF72687E1181CC4A09904837E5DF55D587386F32784A9A698139C247045E2DE3DF9678CBA80368695DD8F2202B25837E396130967AC97DB24FB6C0A3E7E8AF729DCA6702A413E3F91D043505C5794C0D7802213C5BC2BE75437F2435F56500B5B13EE5ED02E403F8029B73534D22212882CD4CE47D6BF3C1CAB6307A54D4173F3E8460E4C88911607EFD9DA7C211D771F70A5603F737902CEB54FDD9762BBA0D1329F5D2500865E21D642842288F9B436C5659609501C9272637ADE2F8554D13E298580590B9BD671CADB32726F345FD2BBDEA1D10DA1550D15E05C9EB993C6EAAE924C675734D6E0A327E0B16D67663066FA4ED40C6C80BE8034020D9269BE72FD495601DCAD2B0E81A4BF5DF23B105BA02660D9B44C3DCB0909644252628DF97ADC86A0FDAB739886145E0C8F9267B80E1DC22360F5F31693F9E7B1B7D61492D61AABE7CAE2F4016BA3A955B223294894BD76811B257447442BFC16712B6D567A9FAB49022865D01B1AC9CE19B692633759BFB021273E1768D53BC7A4A77F146733BD8B275BB72D05E07590ED1E682EE17AA867DBD42969E49CAB16521A6E8D9320C1F9F3F0168D02029AAB43F2EE970E2C45A980368DB21E88E9647C430107D281B0AA1F8D45B3E3AEFCDBF8AE7CA416BE952BA117BABE736E766F0D3FBE460FCD2E99B013153CFDD8ED40C6ECA243D41B6F80B0EFD03AD458CE0B70BF99961D7FB4AAB9B1A6E4C1FEB18943292923E8462353EC2A85D84B2B386CB75507D01D99DD9504A4667438663E27774CF7A888BAE219D11ED51919D643F7AEA8213639605DEE93A77FFC0F1E13F13F374BA6BE0B7421A71906C05C15508621A5FCDE99E62E63FA0B83BD590490A589B97BCFBBED391D28E07F76752CDAD3F6", "2FC3B59E84AF82399B9BCA78A25E779857F2E543C4711AB974E28551FE28C048"), + // valid decapsulation + ("B4306A44BCA1166395EB988AF887C87DB1A1B0E1A3E5B176D9831D02793796A02F1E23535521264529339116B4011BB8582145FA81AC2826A62EC526CEBCC7088724E211B2C4674AF04C014956B90CB86AD80B84C2EC04017279A9785B1B502817C5AE31FC772EE9CFF6243A3B661E7F8B298772A2CF7C6A93054A6C5C2D3E9A62E4C783551414EA7A3124952DE61B495DE75496C9000DA647B42BA0E9E38C5A66C57F3584F65B6851509F0F03569D23AD78D3C476D7740F0C9D2FA4814127BAB780B1FE4C505D96053175708932BE963416681189A5E99B708BC287668794282935BBBA958C401D46708EB8595BB58A7335672DB2883A04533D50B90E622EFFD0380F0778021624D82260BA5B30A9C276EC13500DFA5328115FF6F80F86048E23C2CCD0495551253CDC737C1B182346A64DDC79AA49A32FB3360EFC31CF9BC21255B5CFC3B4C65CB82693205DEE46BE44722048EA0B8EC699F0DB514B50CE694B5471EA903771606069848C68197A0B5C5CB94199B66E64113A3C40A35F65B7A1870B4FBA5439B099F5433C54291AF03B64FD2B0F2B828E01A1AD36C6427CD6932AD65D6325AB53178619427E6AC4B4E514110D332516A782E4E138E2B181B9049F8F4991BD4142C2E330591C8A669C32F1E913C957CAA7163CB3FC3C2890485CB5CCA337441A12998EDC77BF672D8D8160981BB12BE35D2C78AA3CB7882B00944351422C564BEDCCA480033D660C94A9A05A35C991A88935BB084768978E2C408AC51BAB47C28D6F01106BB8A233D3A871E6A24F6443ECF0B8E5901526C8A45C1143E5E5AC8322C2019AB059CC9A9F37AB21F93207F65A1EA991CD960A6E3437D9F325C9714AB1F1C7BCFA7EFD1C45E13CBBC322B9CF9A841124612CD88DD79525E379B48BB8103FA0B2041B515983C12581281ACA4B9DFA9EF93C8E59F8301DD61CE470BFD306A092C434AF02B778F54D2E7243E53922D34B9A1BF991E793B33F1AC691D9B2D2F7937668B3B87C47F4375968D93F75A03D3815C9499361301869E18322685322A6671C45833DB1401E37C7638ED2B870111A2C5923C8F97B8F66C7102CCCCB0A84A87477025CC209227F7587C16282B298AA27609364928A561BF7354924C3B85BC62923AB81841FF7F68BB8F99C2E8C48451C0C25C677B9B8C208A07AAD936A1516A5EDA52F1FBC57BAD12A50746BCF0B06D7CA3BBEB0CC4843A9586C9608885BF9C7A93408593DAB2AF5DC12DFBB1BDD9B0EC83C62D09ACDB076060035BB432333CF0324C2EB97C9926AA7520B7BBBCCBC95A4C0F1754CB85ED3440B0732CBA1AC9AC58BCF38E446FA16B44B1485888678143A93D026B1CCE20C8D0A5483A9A2CE514623142382D8C5D49B45301AA540592EE2776EC20889249B204371780E5AAE15151CC15AB91232CFAE272FE061007EFC4C4537087C4C9FAFF97D59C6926CFA4BA23964D99191E84218FBAA513CD16B7BCA1219C377B57C34CA1B5D6571C0D4A659BFA8699768116314191EBB5F96320758A5A160A35CB54161B4C8C877FA65EC4BC3D8150B49A223E56078FC573521A66C118291612354FAA71B3647C0CC6C7E6D531F7F7C1F0B8923099706CB01CC980A7964D759D3473C8C4458D7A0C62B192E3C052F0A905D663C23F9A2556C1975621A9C9F4A8D7205BBE20BC2FBB217AB69A8C21B9D5B8A4F886546258241EC107E491CB6D6880AA4C1C6523A902EFA672EA1674A5384C436691B02618CA98FA1A8CE3632861D881DF1856FE2C82A60D2AB36076401991DF874BD57E72140F4AB6DC5718D536036FA373B137E56043526D2416CE828E0D13029B64F0230774C092AB8A3CC0C348752F5925E78BDDE7223C0B11CF35936BFB5C370E353E6A49F06D23E3C14BF2153995854A57A0C89FE365576957A82C77D1493071C60B77DA40820483A33B413694A67EFE50E90B75595D083E4BC47F68A3A93EB7B47824C5FA586C82594E93487FF151F9A774B91DC754BB604DAC34F8A4157FEA51EE33627E824339BC34BE5ABAE248CCFCBE185357958ABAB68E352C88DF04D4419A5B2F90BFB8C5B041928ED4A26C5A92CB264BA42365A55B34111F910B66B4AB72C3F7B56351CB6B0D6714B4A014D2EEAB5B65259D2409F7E0395A44194E3366B87ACCB8377A18DC7C27C455A7155658456CD8735851C53C5E794BEFBEA7E04245874C438FB56125C9679D7580113D1A10440ADAA0B4383C404FFA369E5F4A063B45C8EF2784E0CA3290582FB680A2C80AD5AB1CBE61BCFBF032C5CC535E539A59BB82B26ECA8CD74BC240BB14E803A1B35C058753A55E0292FD671B383872C0290FBB01F72524AE5EBA649723FA6B09B6DD5C6CA800D1E65840A0980E2517223046ABF8A2E7C5369315A0D400804D813A27A2276FC6543816081B1997884470E2460244AF392B0968504A70420334D162ABEACB61741E1CA7B63494F39084CC431668864B97402B16780A989A7176AAC4D549E5DF7945B000B797625959190D8A6904973A89465644F98A511C1445E2341333578BA984F5670B8E35002BB3175A2421B3402564AD5210DEB04C1CA91F7CA9DF2ECCCC29A2E96204E857A85333260FBC899331AC72118C2D5D3A65D7363BAAB3AEDB31D08D922AAB026869C9A7B823302801EEDE61BC8D055D99667829C32459402FD1C2C58E458BC1C16D77124F7362B3607189AD83E00810613E97067CA9205924D0B3431286640C0E4406A2B14D2C097078A3AACEC8188B06B4BC0BE96D36E0428059DE82447588975B17EC43C3B218420AB9359E637814B841E6DCBB222B04094759DB54A9DC66BAB07567180F13F9C594821B404B4A726529C57307A79FC074CBB74A7DA8CA0BE441B27935EA16252B3091D86C79C8FDB0661B37973B15F8905001C2399E4D38AE6F071559B334D155A1461CAF0C17A4A2149F3408B3B19B407361E9FF4825CB4882C738DEDB58FCDDCA165D62FB9A06D1290C687E18C5FB2C18F2A964B532A7CE31ED3178DCF898A0AD46B78A75AAD4768DB0246AE39862BB71D11FC97B5682349DB2ACA337563292B8BE3A7AE067D2DB7476449060442CDA95B366D551E6CF83FB348CF84574E4DEB84FF07BE20AB484EEA426E18B1AC87C5E89405D218B2992C1FB4633AA43BCE61D3196C5C0E5B14CF8E936AAA0413EE6AB4CC2C9F164995BAF643D6B2441473185E356CF201882FD95A4644BF2A8B560019A1C751125C5B4EFBCCA9DE5A70AA32AFA5A57C7843C953F80B474A59C33077B04172B1B05A2F895AC1D74463569FDB50C747A3B25D5690E3775CC1C353BFF30696A2AFB1310ACD0B61C5E39EB2992E3770AE3DA3016186211B8B9207B075E9C12EEE4C602E341A95742C3ABA8833563AF1970D367472BD821D65C951C65AC92123A0DF4C171B2733569810CB2285DFF7408772117422C59924291A6337FB3864882B27A2555F7BD0BF6D8A17691BB0ABCB1B0300CB378392673441A026259EE08DAF7A62A70B6D3436BFB46948A319B8630873880C8A13F7ADC0EBBFE3C217C4463EC64596EEC5B1AA622B1F33211F399D8E5CA6CB9C7D0CA8CBDC5976B50626E6A07E10861A5819772F6199786B99D75B4461212110115117121CA6217C6F3AC51B6344970990E661C94696CCA04856F1A6707AF319DA63903BE9384FB9A51FA6464B6825DD47B9879C4570B919AE3BC9765484F8394F99EB6D5DF5464C2B810B9108E8582043C3035B746195957A0F2C900A1AC0D06892AD9613FCFA246A117760B78C4C2A7E1E0306E3F462BBA337A36C7FF6173AF5B15448A13D89C42BC6C1146DDB65B6096D90C9341ED1B4ECB8CA13EB10036566B889191A6855CAA959B6B40D2C818A990433EF4BCAEA9CB71ED34964B39F37A78823E467297668DCE00ADFBB48521A9E9A4B8B68E74A5CC6C80116CDE764356661AA0B47A2A2167D23E6A9129254C78BA32BC46F41862EBC69084F97954FC16E0292CFE1840CBCA13ABF192D81A5175AF6CF23B04E11FB24F5385CB397A8D1219DFBD44CB1305CE2A0B7F4B25A2CFA3A2B3A9C34A08C1A712838E39D5FE73F6A5AB319130CB4D29BDC448E849A1CD9A96BE8B1C6640C96227043B14C50629B608BEC7EA5E13C21FA1EBF328FD498975CE61B3EF87F9095AE9008216D36090DF71E921146FE4923112895BCB64B4D3A27A75506C3448F3C3A3456324DFE1C075DC3CB04C9141C9AA572595C8F078FE6A8C3B5BAC2D987521ED316DDD3434B8774F1098D875215CBB671FAC19F270492FDDC2663F3A2EEC7C9F1D839D3EBC0E2459DACBB572CEAB25389AA2AB1CC01C78876D270E3067F710B47D6475AC33821EC5DD4F34CB3DE989CBB20F81CCC1735DE903B5F92F9405E7281E8B16B88108F3ECA7BCCF4A296121CCD35D8AF1E9892ADFDE9F5DAA565877023AFD05D7A4C88A5422EE9FFE6D708D96C311C0B75E31A7D339872A4FCB254C2C7A8C23E", "B38AB6DEA7699D86673A0CA79722F5A3C063407D38F624084B548195ACCAD81AC9272510EEEB8EB5077DAA74BC6D3A81AE6856F3B9B328BA87951C003F6BEBAB1F7D82E593DBB77D720A47F2D302C4217345DE18ECDE850CCBAB5420FCC9E794750F8718AA015F5BED946266EE1E197E6D71D4145C0A990F6B57941C3050D8CD215B9BB29569EA46A9DE2CAFA669D53F407A339DC058493FC9DC805CD62C046BD3A92ECC390ED762261E82D66B304E14930E445AC84C91E312A819EE2C16363941F8436C71C080CE2476B7F7802320F680F6ABEE1647B24604AEF8C639B36AFF3F98EBB2AF9A96D2F93493EE19F94F20B6861EBFCBBC32198665FA1452466437E0372B9D84629579C55215255CD88EB4D5DDFCE01FA3990905CF19C3484E0BBE08F8E696EA338B670024061BC137657D908A2C9DF20FF36B961BDD406F13E2BCF2B009842B3BF1FF522BF080EF1CFEFB70E03B572CD25999EEAF9644AC375E44ECFA72FAD4F1F35B38787B27518F611CEBA8A3E1A35153E44819E24B0CA63105314E140BFCA81244DC2938A3125B51ABEFC96BEA2E20C7A61654856AE7C113D5AA59C4780DED19520EF47E7E8BABEB8DC9D724244FBE7E8EED986A7E75B5C97B254A2773E4FD087AF505B7C294F30389E938457790059399377C547890EFB9434210E46C1CC35C7EF452306AF0BB63FAF93D511788A446544C85375757242F87265ACF11F21E1BB2D7C564F767C86225AB6A3E5A4645C8E52BEB6125ED4A6A09515B8BC3CCE7A9F54DDE4BE85CA574C6BBC849E8A4C22A85B201B295C62C5BC394F4159CCF1BAEC594F9013AF623976EF62AFE5FC87E27E91A6F4131CE41ED196FF62841167F60F7FDB84A32D6289AF323A5AC791A1D64516FBC98BFF53FB72654CE3B1DE1D52D8C8CA57A925DC7C6A197238F32E22EB6678AEB7A77A986A8D1AB8B4F5CE080144699E982B651B2F73765A61BB85CAC9CA2DFAFE9655DFBCE73AC92BD735502B0A11DBC46DF6927C1AF154A11E66C741B45A5CADB4EEA646E1B3A81CE082C59FA0C51692ED8E55C8F5DAFC108941955F6872712FC80F930DCF0C0E6E228DA8801B63520D743FDAD7CD2448D9432077E4EF4993D2074C65B69B98869825330F42D384366F5A08F1BDE05DF2C5AC27F29965B8C9482CD7793226A4A1DFE67EBE9BCCA19E24A27DDA19414E9E9D21E5D8985FB8EFACFF343E7FA5416D899742DE5E006B14C30EBF264B9AEE843426D4639F71C32527BCEFCF2363DF881687AA81D17FA58C9CC5790FD23C6D49894DF0B75C8FA815C437CF4FEE67F12A519644674747E2E6220D0D388B651C83AF3EE269CD726C6F8BA7232646A98B474252935D6554A011ED1E50CE402E01A8A7A0DFB9AF46907634B32024A1005215CEC5A926F5B2751429B6E65568355DD8414D0855AFBB55242A150D2A7034486CB68408333FECC5E4C79C0CD1D06005DFED6464AB782E9D097AE1650EF54FF860EBDE385506BA8EF064AFACAA5193C0D1175A6098D5071B5A7BFBCEBE23F68CEDC71235958BC7F250122F6E0341D6A3519CAE2302902EF9E1518B3DD5862F33D0D5A953D9FDE9E9224B44F50B0B55041DCDFE3513CEDD7EE155B78638851D5DD444CC82D5FA19AE5FB275E452BC0849CBDC14CEAD4D848D8CAF4E8262DAD352BD4F7175C6653339814BD03D1234A0E64D6769254493EE84637F540FA8F95007688197CA2AD0931F7CD305CECBADC92C78DB5218BE04614C28E1A7675E0D33352914061A9F515A3752A18363A9FC276D988D9A8D567B833EE3FB419E3B21F84DAB6C24DF6045C072CD8E957608CAFF2D6FFE630BBDB3CD3E827413CB6B87D462BEAF30AF55D7C5E739AAF015E7D746F76D8BD066B7B7F8229DF9D97C0D25919F2BB415326FE1B9AD7197640FCEFF0FB3832EF5EB0E37C50111F56FF0AA1E885E0E80648E983B88FAAC15CEEB615325D4ADBDCD957843A30630FBD59C6AF4ABE46DAA6772A86904D4125687A6FD3FBB0015BC280F4CEF6E0720DB418B82BCF66008AB049E3BAADBDA0BCA04C8B725BA5C246D99C2708B4FB9207F1B8F4B6A40B8B9B458CBD0B0D56D3C34C20D3C20669A615C7104F1E3F2B6C77A8EBD8C37F5CBB06998608D0BE18079469F6A82B59507A2181D5C289AF0512EECCCE1F81DE52BE3B1322D72EF64BC88C1D60E86A338E09F8D7DE7409A0CCBA9C897971052EB9", "DC8F97D3C3F8B2744F66F336E73D7240F67626EBF61591C771A743FAF2AE9B35"), + // valid decapsulation + ("6089C5991C02AEDA3E58C1641BAA81C60892A10629E685B8D738076B5C7CF5CC7C22F7A5B9160D753A27A6A89E09447EAC260F32A52090D81CD8FC2980802786117803B79C34B3355DDAB65188440696314B50B01FF666251B4890A351103B6550CC8E4118AFE7A5373BA04747712B5C08BED91A207D241E0E52686261A1B70C0699004B1FF5573976ADB44170E8141EEEE8A60F18B5D7AC5649F134CCD34066DCACF9116CD4F73880B74978F3B890985C42175DBF8681461A27E21222D3D908352902C515226817BCB1C1845CD6CED354A8C42838B5F21D56D484AE085BE0AAA6B40917B34A86FBB28CBD8407C5911194191BD568AFD5CC998000878C922D94A7092EBA29E48909E53C8DBCF3CF438097573442D9E42AD35C08FBA201CB8B4E9A167F33988DCE521E14902B8810B05D0C5690F86E0B01A2DD604001438BEC40BF957468C4032D0CC71AF2D18716387A586215B4BB6940881AE401CC054010C74634484B86D4441F92F0230794319D14A0E1D220AAE02B132A74943C2936AB3E7E2626F71512BE180D63A0CA1BA5294DE77C5C5A12EFEB546BF48BE8BA6A51A015420249A25533DF2A7917765779C62AD3E0B472E87AE9B3406EEC22CE2478128002E0478B8518BB285034ACD141AD817B05036A9317BBD198563BC066002B7D68716ED4AA138C271B556C9751708C807685AC304ED4574B0B477A4BD3088096B51CF9B8985A482C6288B1A128F603B3289116B4E047BA86956D426C38FA1DE57BC267136E41901CACDC5ED34729A1FAB5C00829A2697C368148B92C7775EACA83AB3C5AD5637D7C2084FB2FF3526D5BF9086DAB1AF58A3E531B50DA3C765FE9894CB516045A8B7EFC0013738F978120489B72BAE41DFDA636E5E208BDBB29F17BA6CB510A3EA2ABA59A0703EC1DB9058D3A9750B267348FE77FD76B9E24755A5D9433766C359A71463488B55FF2390C39A842FA63B2C121FF0A76F6E97A8AE2C7B6936E662C4976092896A500F533C4079341A39CB43BAA89BAE16EC7D5818DC442E982295A9A10B2E9473061B7634204D0B37FF4935E64878D3F601F80C686E3FC0A0523154EA04C6C548FED3A1772399BE67780E944449C217B2CE0B9214920E67BC5AE8A7345CC9C5D001889D8883721A693A72268154D98BC965C83BE7AB952A4651F3719241A2CCEDE48A6683685AE123989530F8160A9F3E368DB7163A3AA1AB98C0045110FC83A73515921377710445441F1539CD07129B6560867B5A7A562B597D5C8CBBB3043927874468EDF7258934C7A851AAE9E91292827269F3B8C308874E4316BA936B43DF89102D63BABD8327D204F27B81BBEC28C72EA0F23068FB64CC7992A410717AEDC3CA093FCC8F85810FC1AC80405767C003DEC097F0F3260BEA578E82415C6A0AE7DB39C96FC4FF15BCA5DEB4AE66949BD120B7BBC14064BB2D1687E5E896643DA1491037C00B7180F342836271A0CAA1CC680C9FA361986D1B3C6E9C588808A92D44DB00C5E8C569E45BCABA025C3CF477B6303CC48641A2BC241B57778C7773257F389EB041BF9A5A4AE9842891BB9211C1D960A61D48CC1BCF59F2179692111814EF87B42A4BED6653AF787A04E2AA719588637D126A6598B5EE3AD39F92139943D373C91B979B6A5D43C5B64CBA1563D0BE124E271C19538596FD1991FD022AE967D4F0C0B31CA8A8D87599F929784AB02E6956873504D7602B2181A360323AE50586FA5F8BFBB1AC61581C535955FB572484284A27DC856C5F77F3EA08DD224407D458C66279E466CC9FE11B3555073279B86DA992BD2122E58F1BA0C29CC1522225D0B6BB7656EC120935752851D66C2D9A6A3EFB46353F93B9C83C6F13B9038855B2C15BA4C2A938D1CB550A1541DC630C2096BB5096C1FDB739CAB67B049A3DB005552E65E64A0C3CC52ACF7B132EB7613230A64C82684C8246189034FB7565BD280A2AE1040A6221A9878CAF03216CF9209D467BF4254AF4A888159418A857053F661810D66A155D199CE2B3D4974C32FBB0319531770CB0D65C54851A058DE2983E103A40404531B18908697C0145565B05C6FA5A2635C5BA185427BDD0499D281BFFD38C7533C51E839419B1081C6ABA0BCC4612AE46E4E661CA6C506E3285C6BE68F7552BACE2B88736C7DFF15283883C23A055376021B4F763D83488C6BE84706E66DA73637BC818E53394A07A977E4FA58B21C2CD8DACC9EA6BB8DF16807F1425B6C36B4D3091810A6D9A815E6EACED1A548D0D988F9E42299D121F148CC30D0188821A808B672BD881029E471CAD6CD8E4AC918F448E525C7414131E749378561185D05CCA73A21B0609CB65A62129ACB16647153A41C366A4D2D16C9D5D4A2DB322808E13E7B383D6700A1845780A0869B069A5A317187AAB336420A938A71631B28A72A2A7EB2E5B360046AD975209D3A8C489110D9A7124FCC5298B5C8BC489178CB5C1BCA5D31F32127AC6E69B1754647B11561C0A5270AD7E3618994CB8057453DC4C678C088A323A5B6C3CE350C08EB984540DAA53FD51117B17179F3C477FA7E0B5A8927F88A46030B1CDC73C2C467860267021283C1E5BA1F54740B925904E09CF95A7F898A65AA454B68787DB967C683480D1287C9B0270377FB5B8A463185C80E6A2C8124130DF3616A624575E106A2E25A919EF05319B0A9B92C7C3DC15D9F80994B7A7F180C74FD081A62485ED4549D16A9C7F9C9BAC2B1ACBC42664C99AA41640435CC6498CC3AE8B29ADAD5C24E88A8C359055C1814B40746698882A8A34FDD698C740709282B4D330BB8555009EDAC11ADBC2B05A56DED17850304A7DE7B324E7736E71A58B1385EAE97AFCDE64B76BA0B48D15BD53A10C65CA2359273A70C69F499A75D0589B3602CC040211D327699C62AEFC6AFEF4B20A4FC9B277A494CEAC316D36CA1A8A406C9616C13B4AB719E2D42163EB64CFBC773E290777D1AB617C0BCEDFB22D9F4BE5BB735E3583C8BBC96B8C680C3D87BD490513C5472B1654DC0403094F137B7C8BE7D101708D9CEB5673E78EA8415773CA7B858E90601DB439F8D3A5C33F0721348C6767391D5039C7AAB2107B54D3837CC3779B533678A73D20D229577DCD170B471A89B95467CD609B000B956F034A9FC5D08D36F1E119181743696A52AE1EC07B26CC7600806892A91C9813BA2DB9BEB32733EEC9BEE87262D4C70578C710804B7C131929C8786213960E5C021D5F3C1FCAB587F408F57A0C5E5F1127DA4B12715BC73208DFE703B44DC33B0DB22E6615B8404A936D97868C146473538B63421EE40BD4094C6164B8CA0D3C7FAF41108C62547023689CB8B83F5B8807532277A45C109D06F905692A605DC0706FA50A04E3B0387C62601E23956ABA544484F5566B4AC61091F42637C6C6D58286733B1C2F7C79CC3F673EA3426B2B8375D680480DB8F4C65ABF1E116D6B5340EF27FC912BB05307D5F5CCA35417E76C699E5809C34947C6817265508417BD61462970248A758E8911F50623BDD9725B4C46AC970708DC727DFB25ED2976B52125417AC9106DBB8FF464D6C692BBD65C960220A658671FD800B97128F5FBA9D9AE39FC20062B20866D9681E8F38003B0C7618DCC1A53A6520752EE6500F56D4041F8BCA7D879640B13C77A38A7F17705DBBBE890CABBB312F7CA3C74DF67F2A7B311876C629A00D79442EE000567A73A20D59210DB928C3E5BA1310BC39412D6EA02507C80EDA114D4BE3A12A475A5B4976CC1B76522949B7208324111015593FEDC4A2E070A8DC10556CC2A4F8F2C808E39020B540D806AA340A186EF42452E99BEF230E687B072E15ABECFA6A70B84AE7749E5EAC77AF19CEF3F6B6145161A1B61B03B72E287646142268C12C7427D8A79BF4A94BA59B2764778DB684032265B578BA917C31620715292B87B32256BBF82E5C1A1B624B47D3AB3702A8B7B132CE85A3B00454BFFCF07784CB791B14C4A53B5B3DE55DE1766EC5E81437BB7259D7437842C819CC35310431CD6A5BC8688A018584CB1C2E1261B6D0EB20E4D740AE9173B803CFC358A3550956E1D2036DD9B277BAA5208B0F88BC3D3743A78C10B7AB901D44C841D09B955D18C05F51C7B60BB555EC2A960A09097C8ED90B56106AA3643CACFF722AE98395EF0808A5ECAEF6D7BFEA8081E7BC28ECCC070A7956FE91375BB1B5C53C74AAC60BC845985DC555274200C931C005CACEB3D001E6372E5BAB6E8FEA357B412CD670666021693FE1BB287B087EECC484F3CC1734B79AA9A9F83B395B771BE29438F09A243E7B1CD746BB3EE25FDCC78DBB4C56604C703B4C28FB636CDA877291994682E1A47EFD79F0472AC5B19D9ADC5FDC2A044136758E93FB5B60EC50F2EBBD9D85E718310067347921E764D25488E072E7C540352C52E83641AD90DD61708D243ACA3DA15A0CD58AFFF73AA0D30E40D593181DF72D08E083618496DB0EF23E4E2D3E06", "BAFBDA04E42603DB0AEF4D066953731882C2CA44028B616CA4283D4E898DEE8F3CD6759BF161EA58887E37C182BA7B4B4C1D07727553FD260E01E1EB8DDA0043B81726F6C2CCED79209B6CB7510672D627FAD4D173BD73FB56A539BF1C7A750170F1308DA2CEE2224323520B389055EC19EF7E316DA4192E9C271A79D2FBB31CD436527B09888352BED7C70DA43871FDAB2752B6A00AA04FD873C8B1E817AD212DF28810B137E2D0A4826B49083BF30725CDE1C3662A073D15128F0D42A90BDDC9DB0DFFA5D3D7FFAAE3F008021BCAD97F3AB1C0D533C69B58E0F6A1A33A4FCA2BB3DDDEC97AE5529B1D772BA59DA60D79656281E9570AC871626704C88D8089B584C2483C56665CD2B1EB2187A52D9824663238284FC34A929855F450F52DAD62A2793FB5A219EEA4AA070252DD674882C88B4E1F66111B1264AA0FF885D0B6B28085FB8A140970B11DB2EC89616E74365F1CC1A97783B1B5AC9AF92D62556E214D1DABF1488E2AB22FAB3E284D424A73599C61D3C0316881DCBFE2ADC555717C41100EF4448C3BC52592EA0A31344B85D21FB323940C847A2FFF14182712C5FBE81E604EE417434067F8A167D226176125001CEF46253453121EB33B825C45C499D43223FCE1E0840CEDAFD168003191A2A63CF190BB615687D796B6DAE66938189F792CB088C630AC7DE0D477B552191092B39A62A4C4503830C06D1CBC304B292974518AC18A058CF8E90AF2E3504060B19BAFF0E4F81A055253750574168EC91A66FA0907C710CCC251E09C14EFD92708D853401E5ACEECC45C9DD7DECF0BFEE6642F5516D8E61257A07612DBDDC906D6D76F76652027E242F9D303BB6A7163E43E84BFE1DB0D35C046ACDA50EC0CB1A3246543258C2F96BE8F2A64086846FDD405706E2089F831690F6032796B4D6159E75A1D9950F70E5D0DCE79E39D49F8F8E8BC44BB64A381CA871C6951AD046BF6B3EECDC12CF830EE7D7866F9BD7C5AC8DB05F0A83EB83FA79AE3DF778F3F6171A81069EECF2D893E0ECE89F20032F85FB128FABB1809778350AF007DB3A426664804D6EDBE314C156430A93DD933D18AA84D3BE0550713C8888AF5F144EE609279E80B7FBE7D5B021A9D6A0BFAB74EFF59756829763EDC9EA83D863BB2BF98640451D7B40A35C4532E4FA35ED208B4B6B9656D0FF230058A27DD0A36AB9D2FEDF738F25ECC71A63A041E50AD2B843A5994C13E846A2F1EDCA9A4FCD875BD1EF43E05C98B0F9D9EB78E3D4A592201DED27CEC2471A600E34DABCD8B790E3F914BC03A9687D7D5F519A17727F14EFD421104F418AC914663E100C1EA2B9711637E94491741F852F65FDB49DAD006604B5892319CB472A1DCDF36D7E696B39F874C9598F493BA5E61F4F675479024425CE8A273031E9F04320434A4FC6FFE765521EB2DB3BD8DB85BAE25CC109777793F5A9566EF5020A97C5D13EA8027BC1276AD3C1B9470D05B148582B92DD01C706BAD780264470317C7BF92B267C34AD4753B25C31571B203DCB8F0328854EE4C488F4265161514D19C7A4DD31D26A4A840EF57E847F405D4D81EB59E240CA27538C8259DE934B111FBACBD197FFB3DB4E0E46D3983ACA2110E5B06A3B4E286A6AEB51AF8143214F8B116CAEC619ABF45180150C34B138BB953F128F2286AF51B35ADADF695627C7267B15389A2940C54A9799796FCAF47033924F6192EE81A4A91B1A6B4660F0106E541582F3D105386DE518E7274317733619BB5D3C5458DFD32F21832CE57199E13581D422D90198D252B5A2B02BE72556DDBDADDF804485155B8828925DED0C5EBBCF75123551CC95271EBCDB9C0F6F5EDCA28DD4A4E84DE1D3B5A70021FBF3E3C2AF90F799E2C869BB84D20178A97CA62A3F1A626BA4B1F69979278AB8F1DB88E66B3DEE136A8F6E5EE942F2897C5D2CA81B5A076535D55DA5AC6C90E75C4DF33D919160B39441D2F7FB37B66A635F1FE30A73F29A3974E8DEA34DA7F19F328947F68E4C7F9DE6D11DB1FDEBF528FD29589B84FA5E0164D6982E134D9C0F3B110335DD66881B32858E9F6518CA4FE7C225258614EE258D2BE076CB07C9FD51F49BAD6B9423C036DA919E0816AF6DB7EA4FEB51C4847287EC0F0432E2A9661D6AAD442019EA206D6B8A48ADBDDF09914E2C39CD58451BF5A0E8924E3ACF729E8EF4669A7B9676BF6AED9371AEB82AEB82108E05B45E27F", "57ED4E4F37FDFCB169BD0A749D40E6D58654534342E065EED3D273981B9B7053"), + // valid decapsulation + ("48EBB6D98494EDF34B2C941BCB61366B80C185863146543C1F76CC5027AD20BC8814397D03E26DB9F4844CEC1DB7B9B6C8B14C65829052AC6F9D47712802094E978BC9A5989F747B599B26057A04661027B7FBCE9DE019D384B2FD206DC0780F58F0AC9E0B1635F93D77C7C20563C0900803E119B011D750D4C9AF6C037C6E32250EEA3CEFB977AF934207870A0BB965C61C490EF82CD12AA1E33897F3F4C643B1A05F91BA0BD420FFAABC44D38F66F9956D777256D726CA01278FE018BD0713462AAC61AC5A9F5734988153E523090A89265A811B4BA26F01B3B056EB32879743DD583FCE067F90EC63B80B97EC985FE78A739BEB0B0643910D618135CC23C600BACF5438AD899CAAE3A9CC2AB8DC30732B678585D3B55228967310BEEE4CC269658992C7B2CD359F7ED630F0552170D702D6A646ECE00728CBBF09EC11E9F1999052169347ABEAE482F1B13EB8B439CA018E6AB4763C1AAF5848C1601451CB93550957C435FA16B79B504D4172319B459B86C868E981870200FB21522EB4647DE52E2D68835D6546DB18AF048284232C39415A8AD86929D2381128856CD6845691212E7148A9C2B95D468A176264655663CD495632C480C654051859BA020CA01DF1B248914C90319069F17B6EDD9C58BFCC49B911C2890BC58946ACAAB40BEC05294FD5B6681CBB398AAE59AA208BE24E5DD35A4D39BBA28B35979359E93A69D9E5AC098076E3D17B97A5BA8796316A627C83956B12616AA8380C45D845C67658141A5BA6A83D6E96246BE71ABF93A014332CAEC404CB7884B88A23D92BBFDE7B22F3A89B0E5738B7A0979EDA99501B1C12A882368897B7919A5F70397384659603259A5847B11B20E4924432B2A9AA885088B40764B1A548A256DEB659D994BA1725544FD29B11B78FECEC0D07A031694465FBC6082510CE41090792785ABBE607E33603EC1A5902C51765179B3F78B7393A93D189AD40F73A5E5357A4A35FAC3B0561EA9A5EAAADE6B16313F90569514227C52A362A52645B52F875C4FDBC001CA65ED05616DB563BE4027F5B1850B1A529EEAC5552891E169663CFD3A4CC2ABD13C37FF6A7082480B9E652313275A26F671C9694A241CA79B3373F02CB698CB609C1D9551B054D4705874CE452CB8AC7E5A38271970C6E3B9137A7865C858577A656181072FD65961391BB34177455371DC81032DE824ED0BC21B2B9AB5E0A5D615B23D5C850191C7BDA998C4C445A439ACF2392A5879CBE1D39638A26534ACB2BD5CBB04ABB1E618740A2CC907F514B95EC4986704F5E8788E862960DE766B576BBA8AC0EAE34BBB075AD9DB09D5E529FBD1A4EA7E6213C4B2835E449A05138DCAB0924D060AEA337BD781E2CF1B3E7BB372AA2292E048B9ED415AD4AB1080389DE762165EC95A4D4691465260B9CBA9B98684C755981D42E287258C3E427652365DA9414897A603AE02861D27967AA6A044A787B53B6D20215C083570F5058DEABA170E38FC3677B22ACC5B5D17AA3521DCF528D564C9303ABBE87C9BD115A6AED2C5C5764BC8E4B7CEFCB558357B4FAD3892E71BF27B87754FA0C519415960A6DF6EA5C6E59B521704FD4A67076271D087CB5F5B51D5D020DB13926F1406A69DA0CA257084838465EE152B9794AC4567B53246526438D0B6845FEA5202AB868C3C35EC07B3A1DEA558BC7BD7A05BCFEFB5F6C9AA1CFB3989EC59667169284B22A42B49A65929140BAA0A8552706FA76E66B4267F074C934B39C1360D82962AD331CBDAC30F8B27C0302B586640A3492B341FA3D967B5E1C328B88EA7669337AB0EB3E862898F746305A73A9EE77B03E776D9FF02DFCA8BF8763182C65A90A478129560A59CCB97FF186478327CB534C22142836B21AFBB558FBD527EB9A3A8249C3CA3A299544169199B05347C04938B558C22103A64F04A4C74A698B5484BC65A16D3CB573179188DE3ABCC879A2C0150D92554E0766A7EC084E4A0B0911F9729860C2CB02CBF4CC05FE9429BDC979779080391CCF5B4A4088175A2372CF43DA804AE473663CC55CCB4331969FC42ACE54EBB24082473EE95E9552C03364AE70D4CE6CA114F8577EDE550247F859FFA679277C82E15822FD87890A6737C5C2A273036F343198F33395426709201699E8345C23B2933F05115350C45232A6DCD8C5F07B9FD3F07157762384A11C02D00A8789751B08CA158A5015DA200CB4652A565959E15ABFEC60B6B6AF601C882FEB0148D643D43433FB768735C314B71CA1F1CA3888D47F6B9050306633BEA2AFC8F8CE704A5FBCA947E40657C2BA6DCDDA9979EA326080648BE5C2B2828BF9670FC60528678A21E8334CE6B0A292B94833C2128A009DB77221CF69A233624D25AB5CDE3A6C80D4C8B6A7830E7B81CCA0955FD9A863A90A0A9B8A705408261076D4E89D99757157B1BD99F41D3B220E85395129B8564EA95B0526B9BC3C0CC4C374B4427E8756A22BD03EC671B1FF899055DC56CAAC8AFA0993E5D58DE201C4BFA4C1C586A0C2AC79DFA385132380C4395B6DA128D86695100C178AF4499B2439270951509C11E1D01061640F4AB67C3FA10FCE37CD21E67385E3AD189C4FB4289200751577C4256511828FA63AF4C352AE7B46AC0A2B3AEB35CBF859C7A43C4AE4206F639A3718170C697CE0BC24855A4DE2153480B89D3DC6317F391E978589C953A8759260C50835FC340DB89CB176440F53D580B3FB2685102E4FF201A45BB736005FE10744BF76133696CCC70448D17013CA173B6380485A307CA02531619A20AC057AD5662A5B6A9DB420122C5917AEEB14CF164F337439A4A816D3B00896524A041BCE0AD65DAA27A9C287170043B0C8B86C5FC207EFAA5DDCD238E31AC9154420B69C7E60E2B98F5752B343336C112115FC6F3B479E63898DF6593BBC355631762D11E3BA7DF7B705FB1876EC0F309701EAB02C01B6BD935881CF7074CF6B3EF5BA6210C2CCF0543AB6811496969F75E4452841210137101BC45B7A8B5E7BB1A58084903E6076B8F10DCFDB175112B89C58CE16F3622FB5AC00C4778B300243438BFF37CF1A41CF09FA4726C311B65B0E5ED56BBADA4847D7A4E8F4135ED1B6AD917C66261ED6261E88FC52375055EEDA7A4B2A7547025E48D6499E2867CB3339EE83B2637564B1A1BE574928DD838514FCCEFD6453F3C30BD050226AC1B7C21356E1314D1A72324D843C13D355E9F745A1053B64C3B2002BAEB4786C8778A36AB0CAB3DA7407426EA557259507A5F7A622A0095DBEA07200D204A8C7A3F9C5B2421952DA569966DB2E08577A3D94A947963E75C810FEB61E58816A3E9901B09CBD75E291036302D572199DD12984BCA14D38B65CB5A8155C5228B099DCA122FDC7734F976A9D8187FBA1B0B8C36B2223C5D971B9A77B978DE967CD35ABA457446DECB3E5085C68B31DFE2038A2C0A0E0000737976227769F36D04E5DDB405683AA4E6B6CAFEC5BB56381A835967C7A49D6CC4F1BA85F7493893544766C0C6B72A935957BC070D309C8E7B5F1648282361FA2908DCCF99A19046175A91B8A492E678A67A8CA1DFE132539170FD08A6910001F737484BBE107C8E5BA16E5920675CF59E6590253739B918AFF83337D6ACB1FC965FAC58325918695F01C4567253CE87844383998B5297AA35A2683761BD1783D37C504C51E072312309606EFBA759DA8505F3C16D36812DA070C7BB771E3C4A7D2E33B79BB4ACDB35F4FE97A5442B073203810012DA4E98A1C70329D036CEF462A2AC239A1DB38E1550F2884ADD3FB8FE999412E86A96A9B6DDF24A94A4A309CFC4B0C0A2A13D93CF97570FD485E08F7A681C33F2E6632DBCC2DAF552C7DE4C27ED153AEEA5E8DA07E458110CA4C7516244D4D6915D0566AAF615C6FB682D59728B14A0170E4366B85C2EF5C8596269EF9BA73947A6D4EC6530BD556374470AD472BCD495C68CC95E7F806B4B121B6134B092C73B3682FE0214EE2A276A82A44DA277E5E898A1D15184909440D81A4FB3BB9C6016285A05E421CC9531897493ABFE4D71B0D3B1559950D178A2014EBBE3E24B2029A9EF00036D5789F1D3348992C3F5E633A2116BBC54786FD865E07D7BF8A4B014EA6582FAC1FCC973BE9981FE7390FD27774DC87743A539552B854010382739CA3F4619A1B329AFD70670502B50C419983E11E98F86A38A2A141164DFF19373AB57EC69640965821C97671D5A15C6C901F42865601686466F78177AA7A07AC0D2ED47969D31C6EF8CC0AE409ECD11D2218276E0668A8B695AAA72305C93450659E681B31D3D5BA7F282AE21444FEA9365008434B97755126C7B9B39E89C402D0F46BF13D37BCA014ACEF97024244958D3FE0C6693E7623F85915AD4252EDF029363DEDED6EB939D9224D09BC547B224415C8D66C307D207043865E10BD4ABE38DC14701C818DDE435C49AE143489E2AC84F1CB4648881455B0E4B29CBEB7D91379CF", "A675E7A91ACAE3A48801C9ABB1EE076D420018C2CC1D845953F8CCE50358EABEC28724EC974F3D400EB8B95F8B6D6E4CCE6E6E343344B85FF3009A8D070F176FEB29FB225F246C7FE6B3228C20E13F37B9810B3108DBD12343277822DA97849FC1360FCAE325FD5EB6B9AB958338EF1A60FDC5D237F3CCD2D8F9AD5755AB2217535A3A42C660DD13EE50A9AB75A2838DD99820B296742E85FA0687487E4F0D6D94B634070F54805B1E8672B4287EE92197E57B6E391A7DE7EE5029B7B2E7F270E7D5B906453ED3D693FD21B7C5424A00678C0EA5B51252ED613F9E1D4F08FDA8388F2E7E38B7DEC158FFD8B9445658CAF129AA15C8B9361967096C5B4B3BF7B4F04DD246E985FFFBE00BDB5E0705CB7B7D39FC65B574B8A3D9221C4A4A3F93BE2C5BE670B5C450EA5040599BD60A57AD8A293B659161416BD61E5A9A9A11C8C8C1280F02AA98C62BAE7C60E5F5F79FA0B5C8A6596695506520CA933629A8377350769EA66E7F258C92421BFBC49EC13F7EC678A7F4A91E77E04B45B6DBB68F8B78C7B38982FA666E23C8E5F5B06438196416710EE02E1D834B191DC263133C8F31A062F8377A513D0C47569507F7332B09D1DD2035910621665A9AF641C2BA04E3AE11F61DB20883BA3E700C4C6FB0427D22FAE5192062024CF36BA99FAE7C198932E05A77C8F191DDC3F3EB21482C7E5F2805E1EB818763F70E2F510D9FBBF4011F242B6902EDD9117CBCBB89F3708D6F98C5163753CAC8DF8179CCA270BEAB19F451EB504D8A73F610FCA02F61E6DABA6F08703C2EF130B73A79A91F94D30865D3C38F85B34CCDED331249A4F2ADA591B1D2FE012F14547C0ED8AF91DBDDA42DB729FEB5453BA8AFFDC08938425C6BD796B6811EAE718B86AD90B6C6D42BCCE0E4648A5F37CC43184148F88C092CAD5234BB2B17EB035BDAA8200574CBD0C1F622C2712E9440AB127A9409421E6ACCB93740B8B956A093A414655AD96FF4D19CB0A324550AF74A14E4583F8DC140B3A0D7AFDF4B7F4A46476942D8010B42C6D40CCEED6D20C01B256DFA74D16511FEE9951FC9CFB9AC4735749D287095184050718203734C00E780FB94F49D2DD22DA630AE698EBC0F9EFDCCD9F3BFC6BE7700EEF1ADE288602C77E9E44B5D53F4A0A45E6EF6803030BE8C353D43D704F307CA53AB623245DADED26173E75EA6C93F72A1A50FBFD5B6C91CD1A7EF7142A88CB263E8F554E2C34C28FE8691A7F01BBFB751FEE5C5369D49D456511C3EA6E1232EF85AE89DB45844DAD959B81DA0663268BAC3BA9CD1CC170D6175495EC77230176D740A284E0F53B7F6002B238BD6516C4ED2CF050FA7AD5405AA085D4759672278CF6B99D2C292F1CDE7AF00DB21EEB6AB5694E73C7EC058D4CE4B506D436DC0DA748994543F846EF29A2CA790F2C8882CA59CBF22EFBFDE86AB15E7422806A6D6A43F1DD6C6BCCDA77A69034FD4860D69E969E34AB891DBE139412F6B82A95BF04260D883ADF6FDAEF5CE0E68E40E749FEEB5F413A5EA8B2B8A449A527D0451A221AD2F314D5BB141DE57A9ACCE35EE50904D3BA238CD99387DB657456311ACDCA9B8BAB42FFF4716E59AC2247CF6C13E170297C11534AA17E5FBC7162C74E5E8E07A22C4717C0CAC287043F8BE194D53227DA674233FFF0B9D48B83A56659DFB64AD498B2DA63D4B7A38C826D06E834B23D57EE374AD4A6F3AF1E7E8628C98763252613E7E7B34A83D763E883DBB9BCBC903CC3E59878799A935425C540D3DF70DEE2D1A8BD5D2BCB541588AB0DDA0BEBF8DE7D6C2322A8B186DA3CFFCF9C17DFBD10209721F2C1B88D3C7366F8D23DA8273775D7C5E9F135022691036385661D8A2E1614988089640C6B3803D9C9D7C9C3E87DBB7F6AB289BD2399E8CE4CB3D6AD042E1783C9C39E620CFD718D312AEB5D780EA6B409C69F6091F83B081F2BFB32DDB8EEB6B88C91B5F340737C8601C2295D94CFF384A99C7F49818E0BF571FF325132A34869A0A6F060003A2DE43B3986F98A263228189F617F30A0481589AE5E396673FF1C412C18AD181C11D6C52B8B4EDB47A10BBF2E3E83AE368FC55F88B7FD16A9EAE8154B7F7AF88A10CE3DB69B7A23048458764622F8C25CDAC47B8CABAC8A5F72B03FA8376EE03D937827EC3D3B56CE3819BAC3603AC5E1D2DFD0306E4B468C8BDF1C24BA6C0145D57839D6EE91C7DB40DA88A54EE8C736D1C", "235360D7AB0AD5022F1DB7F526206F4EE98199740DA36EBB5A77BB78D3FB9195"), + // valid decapsulation + ("9AA71AA2A41ADC5799CF8598440A0B22F66BE33203411B8D8CF63863665D3297973C034125FA23EBB6279EE9A2A9C235DFE12495B0C5B6B822DD971102632E27678A619994A2BB9AC28516995335E0F57D925357C9A0289CBBBCD74060CF8B9BD77508AF520059BB243F8C99D85A37298908BF988C514457320200C444C00DAB905C61842B92AD331011C4FA45410A462C9AA5D189B5A69820988A08563ABD81D520E288462E7596E922579301A817B440396B89F8FBCDE2A794EA25798E436F94D43434D7267AD306F1009B50D46B53158E7921C50D5407D4D266C62C9CBB0086F41C6CD5A252648B97468C4DC16694A4253CF79141952447CD7AB88179C843BB5A7B158D4E5919789987937ACD5DB73CACF65A4B359975B4780D574A47424BB14904E6F16D17E255FEC75EAE4660348564CA8261AD826A00B6C845E1871A8C059B5A8607E8CAA3074EB654A624AA1FE0D9CB9C7C09483025179B9BFBE3392AFA627455460CBA582B11373CB0BD0BC16609725F453B4C585665A3F32D0A692DFF567FCA659356CA6A56F204FD3435361AA57B57B414B92A5EF14D575A07EA768EAD04A6F066573D21792768AD6219A308E52B925CBE5743510ADA541AB440D2F69B062A83E117CD840791AA55702A211849F2069DFAC98D35567812755D264CBB828D8C089E180640698A3E3BB737CC9C79391BA83248BB1F82AE12D80F38B5678A9A369E19AD60B3A70F447581FC75DA0291B9E18BCBCC81485A1CB09210BDAABCCCBC07C5B563C0F84F210B9198DB1AC4A92FEF4504A788CE245954881A94D0F825E1D722E68C1300CBB6C3E828691C69ACF6C2BB896805565A7C3504CC347B2401A2AB767F26EB1C63352377AC2AB05A2AB30017C4B35D16CCC9858542276407C9B7C86BD37585B384B66CA158FC540431621CA40CB9919C9694522ECAB7C8D9A7E01413F14CACB44B1207919D8EC808F1816E61235F0AAA110AA5177C5184E220C89C66B8F80C71AF2B7C67834DBD8709B03844399658DB19CBAF6A8298F8402B4A2EE5A6244C639A4456146029A09AF3C2FC1A2BEFE87752180240934D5722734D1BB4E6CCC7C9634C52A83EBFF20674E9982A93AA32121CBCD52153B9B03CC6A099FB77B6D3C15D66B7D945C1D3E524D02712A5733F6FFC3322A808D1833B6D78CF3088A3818038316639950C4B14464F55B1642AB59693601958CC4DD50C3E33626BFA83ACC8F82880A86290F60D23171B81F9B7FA304181AA91B6B8AC4F3132FB58660BD06FF982A3925CAAEB1867E2B60A52905E07196592A0A2E1DA636E56296177436F017FCE2B3878C98E4479A5762AA7C11928C9298921C3AB3427A9CAE828CC67CC08DB754DBB2F485905B114B085A3B6146728048714C8A0C8A8CCA95D6593937B916BA2A8E6A6287808111B828686427955C741CDBB4E6C5BB720873370D089CE3753418102B0BB5B95648CF42936610399A528982695BCB90182CE31C5D2256852FB1FB85B549387294AD1C78AE455B5586F5CD24EB3493ADB1682D2169C34A7B117A9759533038A85AFFB40C222FC9C1CC96A09879298671E6EF0BE221336F1505C1B3275319A056191A99B160010C51D93492123D07E5DC6B7A851ACC63C58E2FAB86A893881F32725AB8F4BA8262BF21EC4AB5A7275B17914B45E102E5FC71602D48142B6745F9BCB5D871932627CA3A1721FC462BED89E90CA92A25A87DE762C90CC5E7DC9514DA74D971122AD378C866B6C058CCFF1BA5ACCF5AC84BC2A4CCB4FD4F577EDEC0B11D55849E6A6A1505797C0218AD7A84A4C759845A1B6A44DADF4C05B88CD1689C59BCC078896C292EA960C6C1E30DCB7CC4C9D5A96257385B48FDA842DD69BE60B5C15272A26683A1B214C1B1C3C9D55095E7A39FFB038AA827250C5AB229BC6BF47A7BA0ACBAC552AE6516E5E3B9DCCA4B788C39AD7F5B8415B149021C231282E6A012F06E02843A5B389AC225C871D333C8E3F8C311DB81866E49A99978BC9C7910114C58F179B667AAC17ACC676954A77C916B5B4616E67BEC103675662A7186B17A7F4B05B00A572726E4AD40BEA38BFB2C825D9525AB0B099FC07561F7C2E44C5810F8531BF634A71EC83FF85320C1592969451C0346916A497E0BA374DFB2020A68EC156BD9E3926F4B4A26C5404E4446B42F90263611F706C1860F093BD195599B761BB0B73AFCAA8E78167202747C3696D68B3854E4A7657290710686D31F8838342BC0BB2333C112805BCBAF8C70B0B108FD877CC3619B4665751A26B315C9C49CEF4199EE55464522E0CC7C324E7C9289ACC1A2163DC950EAB210BE14461F823395D66849E5B73B963126C6A95B3CCC75454325768B72957C6BFFC62B36AB20924B0E7A88C6A403D70343F6F23182210116BCC8FEBAA8861365129226B0D2BBE366AA92D7A365E4B2B4CF2BC32D62A000CA3F501BC1D7CCBE6A5B872C65A7C145ABDA15243662838568644DB8F889C445152C49C47C16B902A797AAFA0608C8377097A627DA66A37D77ABFD3B86CD5B265101276E7F79ECAC1A7F038367FF001F6516A5FC13AF95059BF530EBADA24271B1B914095B5DABE4BBB7CFAA9AD95B976A5C1BE62C35E8D60127960204D5B71CB707952F69CF1AB8FD95873C358AE9ECC34557ACC6EDB45FEB640E9B23DB3F438B079ACDD0038A547CD928496ECA44C10576A9A9C5B7810483DB1661397A0816A5D01253657E12CC867AA40B0A80F5414FB30B5805A4BB01806A185BE9B7B8B30C8704FDBBC80B5C6EE914165F16DA9F613C4A391611A8CC895AE42789FA58C873E10812F237FC5A4C374E708181B66DB99547250373BC842313697F64B50D177C9467A83EA909F74BA0F6A387352031CCF2B7EE26255816629E61314B6C702DF825CD603C5DAF4700D3948AACA815A7663098214A9C91849047AF07BA1703A4FEF4391D0565F28C807C4F57300E4728FA2456837773B7C5A911C75ADE8C268A16898FB60A6A66B6E083D9D8BC6017627A8C2732C05C526F822A68B60F16024DC33C7D93C6F3B6A325145B71D6C28C82556D695C279F4988525C98273833311A7A06674C5299CC3FC5E0A25408B51C5D91ACA7BF81DE5C5923C7282F6D82E78BCAD8CD8CE9244AC5D8756ADDB8A28440055F253FAB00979738C60A44C86CA4BA3F9C31716C3B621C9CE402CA827B097EC1750B3A158AA681E1C1093A84793703C7CC0AA43D37F91C0683B206ECA2168CC392ABE42BB32F9C9B9451C377A6A9C195845ACC4B2AC80CE7535467C3D9595C1C7C121F459BC73C3A4F4C069B3215A02BA4D45A1ADF6DA4C9D59AFB73844D86BC1ED3AB0FC62B5F685B83C79C417D7408B562AC4B344C9638BE54695CABAB21AB262E470570D842A4F958DF7C8B1FD5CC0EC0287B094810F148831AA424CC930A0B7C5AB21887E363804B26F985BB673169C4B86AA0D66AD044327697C45CC0485F3DB40EFAC2852C26E81F70687CC3D2774AEB8033FF63C0846A2862287621DF8B5BE08956381AE18A20EAB49C3EE3A1C2A82B54B86696EB3B6400BBBB8F52F739B323B8BBB9C1C269FD488E22B9A8D41227556168F253278D00475A07B7DE404EAC80118696F98E77B65C515F643C8E2ACA6786157DBDB439EE48D17865B59BC3535336CEB9442A6D5C8491095879AB90B5CA372B18E7359113DEBA51AB2C2392B92DA062E1B250DB639BD709543DC4703B3521A5613C8E89AAFF76CB8F93373D840A3BA87B56AE10995CABC86AC2B5892C343BA8835A433E32B59CBF90BB1CAC0CF9BBB9B0CC659D0681840867F9187C5260D5069C7CFA617E523947495A278D6AD32F9B19A276B1831AA5EFC0445209D2CC3CA952B206F86B93767201D271130E2B495D1A66F22392A58160A314FAF423A3060014A159F124053ED79B2AE832B1703CD5D76A67AD64E1992394ADA88FC68A8674A20B09066F12736F665CDBC30CB51A5A651EABD2DE987E2FB0CA1B121B0277BBAE3BE230637DBB6AD55CB927916CB2E8869A80C36C8242F6A46C5B4B57094F76A6DDA1438DB975FFCC2EC9CB0BBB78376798297747104189AE4DA13365B093268A41BD304FCE5ADD66512CC076F8DE7791DD9ABFA756E6D1C04EE1A4FD7C141028A7B6F3AAD3781B280E4252DB85D0D7795EF77C4AC391355EBC180506A94766B4D13594F50666199A949F694C4571A93C6952DE2C8E55335B8607DC0F84D1D62B8B9569597448B9EFB5A6EA82C39B5C6F2581F6B3109C673A4D28A86FF6298AEE75A01ABB15039BAEDF4C2E347A89FA614BAB3A8B338C5184A993CE82CAFB842741039F45725713030E923118459CF5C5C2B0603F02C089DC58D7C7AFCF9A3DBDB7CBF2FCC47046FA4924A893ECEE82853F3AE705BCBE4507ACB82D811FCF2667F124208BAA6F3165FD2F6A77C62C392B20FD160AFD9A075A59D74A9C1122F73700049F1B411D124572E9029B2C3D57D7E28", "57C9278E2250D688E41E9F98CBA4074DAF22128126E0B09D7826D982461828CCF5B4D8FCA19072A6B796BB4825B4E7D9F3E2C922836ACEB6672C76B6E7603EBD665DBC30FFBA215CD317E0D16C2C892BC56948EF165632E5C2544A55B119005F7768BF1A635949A61F00D5904621C4DB978A7AC566216089C25C32AECC265EFE13030ACAFACED4E3DB63C93FE7849AD1FA842A1C3D77DDB37073AA593AFD15D46EC86067CED8DA5C5BC771ACC41A950F98E50DAF35A86951C4D928D3C1D05251E533B0BEC57C74C58DE7EE2429ED08E2E8BE5CB1132A69DFCD1255720FAF9BFDA2552B049DF4A7CD7B5B12D66E7584ED527C0F6B3E7C38143130FA98EC2C94E309DCBFFC56C6CDC15934D1FBF74DEAFFA6EE46C0B7424D5949EF084238180B3FE8C9058D48219BD08D4367B00B74E28AE54AE8C64C329296A1A35AFEF745F87CAB25BCCF38394E91BDAA15C8D711763D58540660EAE86BC9B6704879AA4FBA36613B166880788F8139507711D1D4A46A070A0B5EAD8CE70825BEE250478C390D5149B7D7613E9D6D2B3EEC0ABBA67C501EA67BDB1EDE0112599BBAC417E58B88D8973C26078466EA11CCFEE22630B37620574CE4A0412302780CC546C9405D5B9EE9193D31645303A31392D05F3AFC6959F2D0335ABE5F5EE0F1B81880A0695825913ED6D791DA8B5DCB8A86450825F661B5774DF45F7FDD7D5F8F80DDD3C135A619C271E3ACA5715F2A81D8DFA4B4397FEFE724D2A92B7D4D085C84F7A71C226BE195416F998131B2EC983373DDBC20248D82E2A48250D37BA76384E96379B1A41FC159952D37D453BDDDF24D85C73FEDBC2BA63BAED075F35C77A26803ABD014380B4397EB202FD796FCBDD3F68DF299349C019BB14A497DA31150047601BA4A167AC74A79C53EF32B1698BDFB31A5EBF2C8F3FA80AC67DDDE04CD545F7B8AE2076F95B3CAF9B396D323E29C6314537CA02B668DC719AFF7E688DAD24EADC1A866AD7C32F93BE2DFFB51C344D8415636A96550B0512E0491E480A893B638B3F0F645B1473E83CA424DDD7B58C6F62AAF7D8736B31B6913272E057261F222BA59343B25D63C311C4AD984FB336D14D354BBA085A27884D3F0830D972235BB8FA95B97F7F57F5D4706DE378621BFA3A2995A03626F9608EC48819786C89C611A4E3366E167E7F3ED5A19207E7098E49935EED462D21623230AD670BE46F207705FDC64190D48FD7B9EC34F37133E94D672B995DCB08362002E3943E6DD9AB7D146862E0CF3DAA0FA80DB65E7F4C846534910B48D79E892CFB4CF1388F3A5CAF7E8E1C5338D6D7D7EF3D96F4F89A1B8D31EA11750EB7985998543516B5D75D744B843818398737CB62D24BE242B6273DA9E23EC59DF862EAA7A8303FD093B7977B3E3E8DDE64E2AE7025377462D65230572E0AC9A84D81001931995D9FF84ECE6BBF4B172D92C3164C1B67C30A7F6C007E59C92DFDF37670B0C4CBCABC0DE454844B9DCE266B1EAED757AD5518F6496048366962A0B9190AD2A721597C98B77769D7FBA9CF993B7462E2492D5062D3AD7D3D452F1730F51EEBAE0D932FC09E9ACB21E547E87ACD013E8E82AC9A52AA16D8AF0F956A769C6E575A59A294993FE5FBDBE46E4DFB11A6CC393FF4EC236C8155C0C1026AF72FF519EAE5D2BBAC2B24F94432F1098FD4A3AA6A6BE75F4133CCF1A94EC22DA0AFC29F67CBF11685F63CFDB827801AC60A0328B9F1D3404E63AA255B1F44750FC9C1B7535D8358AA1AD27284535D0FE9C127738880055C0713D265F7117D835CBF65C9DB7879E36D3E7DFC1BD61EE6851B62463FBEEA44F6E706168AEB7B15A52FDA2B385D47DDA96E2FD12D98BD0F19AC87747FA4BB88A4575DE60789BD44CD8EC2D15D547E8E11AD2A80BE7F13AE9C18DDD7F570C9928F478D6D72328AA7F1F7939D6B8D1C64194DDA26A20C819C478023377E207FA3CBBEDCFFF19DEC883F1D38F5620957E0C7D66829B7A2D490927106F7C2E425684F20C5F91FF763F35A92F4344343C5AA9783119EF7BACEE928A756E51CCF05BD607D1DE15DAF663B2C8F26E83A4B19CD97E20EB3D89E131B4B15C5B362046443E795D47B0D9032A693AD5AE938FC166792E88B943EF9FAE6A36C7CC9EC889570C5D314A246CED6648FB43DA6877BD8700EF40CEE8657E6640E984B96351B7FE265133EEED5AEED0C096AE94B21DC42947A043BA", "A1CA2B2B5C45CD210FF16BA9CC49BF1F6E34C0E1E1DD3C8DEC45532074DF3069"), + // modified ciphertext + ("5D75CE24C45E5C5C6EF29A047A632869E8001D28CEFC0A7B15F8A7ABB027707183C007506BC2A2C4966C5B5BB993176EDFFB50BC41B8A7FBBD14C0180937C9B35224795343A0F782DFA118444338E7C20E1C7A4D93751B0598C87914C26C8304A011C42692C76028670AF61474A40AF93196DF037A32053FE88C173CB878BF467DABE7973397BA3D667ED2BB38672975FE6294F41077EB9B78DA999B3FC25CB753251DF09E32131E5343B8E8B5898548C84A336A796447A0C5A2DC1CC2F6F15DC5D480CCA771FDF668DAA367B78AC079305E16387EEEF58079A9070B020DAC4CB1AA25B6E5D19DFE4BCE6C4303A2469B9A850D35B846BC29AAD8455ED5E1662D56B82F3606CDE7CBABE8840E26487D9080E0134A7A4B0D79D69C53059F6449BB4FD51A99F9313E9446124218A30738FB01CAE8C58545E22DA97991500A781F810437BA4904FA964DC916D75247CF686091C180DCC87861F7B7067B6D08283E3641BE61531FBA8B4F6D1B417E8B312C093FEA0C17F8F5BBEAAA2E118C6E51F52698671C93C5BF3E36BA3C4596F5A33D460C163A903FC76872FDB9075A593BFC05D0CBD1BCDC276C453C2775497E77B093D1034330231B9FC8BAB4EA7EC9832E5776677325397A1608BCC2B60D573D28A13452495D6F8302D6A6B5C72C6D44A19FDB9488CA580F63222F6D15A1F190B8FABC555248AA3B4B84DCB5C811E717CE636B86EBA7BCA0B92139C01F49AB26322E5771C35ED307A0856397285DF6B5CEABA543A67C33F8AC61365691BDBA67003C1B04403450C63E790B9ADC3BA6EB861A5DA04898EC5C349C9C46D542E1413DA82137FDC2A53A483B924709D0D0CBAF18C39787584C0CC3C4F05F943C89B59089BCD7A160D04418B27916946DBADB813DC9577FA837A037CEDA32BC47E9ADC385654667904EC429198925EFFB7E0F4591CF4439EE16330F5A7F6688248A416B79316231426094A990E3F44C49A11C5B15191C08B914D42A96D04B6C3651131BA5EA80291F49A06F30A07F944E81A9A7A7642BB8D711BDE11F57F0A7F1D5CFC853046338CE8BB36DA038AF1CE5C308F36D4F02AABF9ABDC4F13377F755E3E2C1A1FC542544199078922BC110A8E96A8C235E50689C63343513685BA21B9F806C729293365A2C7066773B9A1972413A342BE3029CB61B8DC951A53423F621CE7FA16072D30F675C63538CA5891085610155D2211D2B25C7B7F347E08566D7328E7E62B8A6CB62240BA74F9751B69544E937BA9611A4214472BAB01B5D53A070E32082C057A1773B149C9BF1115835B6497B2187AD7C4B3679296F016C3B2B25EB451D2B976DDDF73195026DB23A6DC87B4108D1961BCA1B4A6885CB54C2BD737ABC390CBF69A356A452740CB323F38995857DCF2106E567ADA74281863C8EB264CC17221481B8228978246C2C83B9E9064420A64FB7A30D061B9FC610C7762412E475C8318559B108B7895F20E88C8B09839EC88CD6140F5F9AC46DD08D6B62C1F82B3909F68B8F7C85CD7BCD8378CD7C1B5A34D74A10F3CEABE98C2BEBBCBE9C82B46314C9F0288CE50FF8B36E28677A6CB311ACB7A4A873AA257544562943306964A1753DB2B57CF0641DC111B1227342A64B47B88B6056916521D82F75E9A6E2D8150B102E170200BE1A27177AC9D4C49BF1AB70966109FAD83E448384A6FB3C126A5F19C0A484B3297A329AF32A31FC52691EAA447F081A34F1086CD4B1E959089EF64DBE8756BF52A81F925E0D06B222F3C2F767050D7B475E5C2FF1F62C68F05596A96AD9E3269CF0666CB50A4E421FEFA61B255A9E65FCC0149B736BC163B4A76E98EA6EA0ECB08EF364E6A455E0C47B2E29B6C429C11DF04DDA036554AA25B51A5F795C6B2B54C8799460CEB171C2CA6BC5405B919933217B16AF189C47C940E0FC7EF83051411A440CE9C4692B4607C672CE9B52C7F03336711E088173DB4649D16058517461A162188C489623FA0945EC5EF9481CF5D7AB175C9010E5CD98B11F736B9C4896539FA32C7BC79897B0BC6C062B0CBA0E5431A2ACE28E8AB09AB27AC9ED4535372026241CADC23C6A2BB4564F6B833437AD300A039CA81377F1A67D4B55215735FE776FE23195E71B665F0B5D694807DED1A55706AC9E3184F24657450964DD834515BAB90A2421A11B5EB141615707B888AA3E6FB89C0C70BA66348E59BCA5463B0AD051C47E50944142A6A10B176FC8ADB9EA3D61C570FD2AB344AA3C4B7A7C15C0B333477A8EE974EC832023A5976A443282FC2C384436ADD1BEB5F09FC8BA63B4207B74FC0CA226CB22585C74E02537187681717808984E52C30734730CFDA686F9B463C69529D42622FAD40AF3899F584AA57685ACADD221068858DE6CC99E22B602AA87FBB079BB1C114E9370D7667DF5A38B65773FC3C0A29D48AB2E78853133049336B99CB3CC3B849793A50EFB587A640858CE4A203C1410660A3267E570BB5A2041913C522CC9E8F772F42C8B6B75A900ABCF4B850C3FC260A5CCB29DF87122362CB7379CC0B7AD6D6CBA07D0BF674B330136547FB7AED92C4DEAA33F7B57A11840324014AB88C077DC933154A576DBA70C9A5B8ABBC5A606C52DEDF12112C7C086D8C01FD59C6DDA7A77ACC94AB182BD70273D3A63B53CBA6A489DD890CF635438734916D34BB0FBA0290BC57246967B774105F8F542D716899E456C26B89E8C1B5A4A0211A2E11E9FBB9321387CC0A85417DCCD5A49A9F5000C77E26EE68BCF2B2AB12D966E30BC1DB5C42BEE2905758798A7A611E217C391ABBB2F953AB2E1C48E19B274468C7FD51F681A2AC5E0298E18693F892ACF746A8AE1B3D974134E55B25EC97D75D192A266564A1A25BE6C53EA34C0463A9CD324824C32B5D24B513476C606DAACBA506892B0827786065915A10F628D68E66837092EF4446A85E99F89A24EDA7410391C811CD41737D763C0A825115B81477843A4F75572E09D96D0945C7A871725C37F9C3D865487BE7C9529A0BD1DA9AFA4D0790CD9C98155BC3442AB990C077ADB40F10C66A3365D958BAEEF5CBEAD780C8C769595017FC6AAA74E3774B7F6850D8B0D560061F70A10CF39A77CE498042A04C6748F25478553F2CE959C30C0D541D23B8233A1CA3F4C7611413D357702E5E80A6856A589F75D9B5410D9D2BE44A9000DB6C8A1EA33CAC5B96854C0637236A22A555CC12100D30AB3D62DC9C614ED806C6B848EFD650728F95DDE762F3DD04B2CC756E24B893D640D85B5B7DA466FF32283FB03C4917858F5B99552E0147DB52880E4825CD6A83AD23CA5FC62324314009CC66F6C32F09C8C2DAAB96969CE457B5B0F9A13B329C13DE02D6573ADA3C67789E4C8CB12B509513B664C47FF769ABF626631398AEC44584FDB2C0CBB04252083BD2A94DED51BF26727E395C21AAB2C1D95CDFCB541F4E5CBF2B0B1716CB24872C8A7E04D38656EC438533450B51194936C3073F3CB36BB20BA69E73B16881A7284B2AE7A5C682439792B1FD5548DFA3954A7459ED08B7387A5169AB74354A916F3EA800DD54C1600061DD7CAC51020E61AA6A1B05E67C196668133EED1C540DA386B8CC2A4DA2CAF6458EA545AFEE0601D55617FF1422D09B04DFCC45F511174449EE701CCAB390B9C4749CEF942C4D14B480AAA656A684A4464A9519F1150B6D0861874A2AB687796EABC2A1E19032B8A1EBE729F6B7AA28CD7864DA1C679A1C17DFAB80DBA8B467714BD05ACF8E5BBFC3848CB715F9C9C2E93628479B9474CD83EE1E7BDA5EB530A590D22C1572EFB9A91595A91D4289865C9D36BBA0D422E622898EDF10BFD0CB7495A9A5705538B9B0E8A41860B13B4600373B0920964F7B4FC6179FC3736404108DD8291F7082E1A6C12C8C8876EE69AE03C2DB3B23AA0A5B5B1D7B62BF72FF165CF8395707325B5E699C8CB33118383C3058C2469009B1500A4AD420D390B9DDF7293CA4471D5B7CAAC6B2600658C39FB899C232602893476262A2D2461C11B153C958EF882241779806CA52A0ABB07AEE02E642B0FB8853119A86EFF2330CA81CBFDFB1063E244941339F5095BB1299D8C2042602B7EB775C4F910A5E1BA2C54E95BEF761309BCB38B0A3ECE189F2F3B96CB24337A152C5503AAC5468B88865E34A89691E7C0B7E7828B37C96484154CC9A880F057D49205D1F90168A8797D8C8766D359CA8651A6A0AF78D664AAB6A6FC11A36D163C489897DE46BED6A1C0BAC85EC5F655DD554EAD258D77B246535CA9C9C3C5B5820B80283E6DF085D7210F8980679332707785BE8C0B06C7BACA199C521D2AB782E00B51F92650CC4F8FE344284504E80909BB908AE1A2C83EB98042FD77ED5ADEDB4BE173D3F447AF43618F28CAB933B776E6E5D9AE3125F37631F8637A3F654CA48E2CEB4B8B545B234EC16CAA1B038DECCA14FE422461ED4FE90843E6E2FF29BCEE59D578987568CB1EA598F98827948703D7CC47B66D", "957F5498662E9DBD8572E38A41425944A689DBA3B33C4952402ABA16A85426C21B3F546D600F1538B253DB963EA62213506AD803096C8260E91EC1A6B86CCCA5360BDD10F04E2818F3F5365556C24BD21417AC1709FABC6EC0EAD0A3E2DAFDB99085F122CB29D7BA5C4896FB2877D4049663131BAC5741C20D7ED5BC7582CDE842B6E8B8559B21D724E863B615F487D8E3C9D45374761370DA4AE1C759B8650A4364A3ACA55673B0E882F25AAE83A96A9DEB90BDBC52D45730E29A7D5D3596240A1B47D108CC88FD8C150DEA42F8AECDF310264ADFBE1D405A89E504D9ED58F2A78D213DCDCA3108B6B5A1D2403F41A0EE5EDD70B38DF4EA583EA05BB13A054FA9B71911B5569943B023B11B446F99B2E90523146580BD60EB8E3D29A15ACDA34710715CA8410BB5A1D33E517FE41603A11D4E4400020D4E2A36E5D798311164E99E9886525505DABD3815BA49A1D62AC87F57CFE24E0350CCAC4A0D6E0D0B130A3DA54502D50A737EE21B7A4DF2176F5D3A98799543EE0EEDCC3B27E89A4CF3130EC39DF7D354A54C76AE997D0137489BBEA6FB3718FBE71301687AAA5EC7DAA1779559A1A98D3933E18CA643E4B25EB38824AA8C6A2CEF5B1D971D8D6E415FB37FC5299005E77E6AC58DBC6C10797D592C12AFAE542ED85269F0F82843E2D7EBC7B4A85BF1F180886ABC04E8500597C185196BE6034908DFB056E1EC31BFC0E444AB86DC466CD42B8E80B7FE9B39A6BD394EFF8F2E209A196FB05441D176CD6D6F270C3C0D9FC5196FC50439EB7DA149F7C90644652305741EA91F95895A4EEA851AC81F0313C7C28932EDED5B431E4A58A0EA0A12B72DE9153A5048639C7C8948409AEC78B1CF12FB7FD4894FFD35BE583E343626BBEE46556648EE200FA737994BC6995FCA11AA3CD1AA9A699CF3AB186FC38C6D12B8CC81CA5D8E74DD207254ED10EF08A7CB047C1926F4CEB596790E3BBE674CB89327D45F12E722DDE0D5C2B156549E441C1C82EF4BEEE3810A759E2D77575BE64D3DDF424E8BC760C9A44DC6B7A244A192ABD0CC87BB036CBCE8EE88970B07865C778189DDA7E7B9C4BB49D14CB472AA6470A4A77BEE5B486FEF967D92B5D9571B6084E13A2C5823C8216B900BB667E0DC11E3E32057E309861A6A772B2A36FC8D302882ED2A73AC68CF24FC1E3F478F466A479D167B822E2E4288DB77D63D21F0390D91671C1D81A9420B0B6879593A78613582E77CBBAC50C721FE48C820D062F809CE24A664921777C69D9936035F93FCA198C227149DA84683A2A63DFAC89FB8C75230EE03ACF32436E66619C3B9D217B71990EB932BE5B78F6CD7D129B2BC654263E01F2CFD0248C85D46BA36BD1E3B33A0077E20B101AD95C3B52B3F7689F0076AD7833F26F5FD98898428E5434F78878EB67E19C6C8C5A2689B06778DF0182F5C8AAA6CDCB93E2618EFE368A589037D9DAB880F18C48CC949241C248F2FEDCEDE448F303120FC80E49CB64D8C6E9C2519BC8DA87B66C6831A0E74CC8606A00296E548427F9278160AB662F38FCBEA07C68889DCE7F226CEFFD864D5084FCB37904C78A52DEAAF8F498DA1227EED15CFB4D127187CFEC58152A4A484BB743DCA8A76DA3DED32507C4B54328275308E5DB31AB1568A6811C903BE759D552F1B071264523F62CF74F6C0411035E5B8D6C2F84A1B5D7DCE5541B01D8363E09F4F7DAFD92EF56DD2A37B3B844B1E37C008FBB98B43C1AA057BE68891FEEE396AC409D108DCCA43D7BA59EC02124FF7D76C348B6F5F8945ED2C8AE1453EDE13290F979F61B5D8C859959F5CF8FAD62CC9C6EDABE6283AB8DA5A74DA0F6B2B56F93AB56931EB3512413B20224355D4F2AC37338A012E04E654904C91A977DCD4508A23C9A450FCD1D712066BE01B8418560530C4AE3108BD0FC77798FE11BE866EC6A8D4D60AC8958459DECB1386AB1831C1A01A681193DF12115A231F57ECD18FC56EEDFCA7BD3AB1C2CCB87559A8EA06717A5850B32408DB58CE17AEBC9A2DF9DDB6096E53242473DC799826C6D9EB9FFB541858232CEA0CC49872BDACD0F37FDE76AF707827D70A5E78C3D573D167112BD76CE60F00720113F8539EFAFF253323AE0CE0ACAAB6D6AA93EC538E4C614AD9FF2FDCA7E2F8280DEF5C1BEB7913CB55D1C7323365AD86B4849B6F919F2CD6F0AFCB2578BD8854DED96462DA8D29154E9D421EE4648C3F", "B5B87791EAD3CE7F695BE94D4CD34D5F33F031492DC765A4EF6B3C94539DB193"), + // modified ciphertext + ("26973201103B1C61BF9BD2AA805C0CA5C261605937DA88B24BF0973FA21B3DC94AACEB4D6199C113C61B690B0DB0713E42436A6B5C1CCBD53F3496C3DFD6CE6ACA7275D4866D46C29E25809C1826C27449C9DC64CB52B02CA53F5B8A9FC565384D331B1D1151F1F9A5177C086D2A3BF4A4B77FE848F841215C18A2C83511B9614A3CF212C9386DD3BA424F815EC0843CC6305FF97342FF9642DA6BA638C38EC30875E3FC3F2F3C4111C35AF3327453DC223F9294D3CA4722E07164561F4DC0765DD57B2C2CA3B3649B914B9C403011543C7B71644E3DC1040F7994FCB41BA3EC36ADB5349AD195DBFC4051606AA242B3A78A41B0335A29AC3C26375885F5101188A1A628A6206147F46037C8496E73D353EC10CFAC97920C156B499CC800C9090A29ACA2D7C41BF7892D25B933DA7543A5CBC0842BDA45861170C963558636A219241C52190723C133641CA43CCECA008015B866FC533B11007A85B648A24F7A933C2D31321DDA531ADC9847089539A57A4358826C08C7E23B8018584F70E361A57C60232A0058E335BB87A8E5141F14E57CF72446CD412792F8876063661C616F48FA06361479675218C14A5A87C91C27876DCD25C2DBE79CD625042BFA5709050D360C95A8F7B1D30881F6C0C68D4A6612BA8914CAB9E587340AC789EB851E721746E71578A295C407F78C5FB72E4855649BF72A0B12BEF08047444551045253CBE27039C54F2C72B6A7C9ACB2299B9A0515B6E152C3ECB3B6D12366B652B6C2CC70DB088D699924674ABA777DA6888114156E67C5126704B922223AC5000BE1E23F966A26099C664293C9CEB8CC49B42EBF4A2B1CC85271D556EF1231C933C45BCC8B1AB5BC6BCB014C770B25CB6DC769945DB2CC119B91AAB65427249E1940C38C57C9DD64CA7FA55CDABC2D1F07862281865EEC479BC29EE9067F47A6237B18525C39295D996D77736ECDF5780E76820EBB77EF403DDEC18979890FBBE99537D3CAD3A77B252A81D8E6031CB254FA4735654C74C59C5FBA58906F94657B42340AE11A527317FAB8AD3B380658EB47BD99619A4211DD791055CC59B83B538F170E6BEC0053DCAFCCD9BBA263783822CD93733F4403C1D011848467493FDC3F2728CC93D66BE34434C43B0C9AD72570FB46783283132116CFDB17DD36683EDC2FF175CD2FAB7D6CD768432C6B4FD1394DA77A45F90AF3E47905553FECD421DDE68A3071A8838703569023AF1ABC223273D2C392C3A3818B494E04D49FDAD957AD2A93DD96684C1289CA9B03C1F7BDB0953787BBC94BBB228AAA27A4C72E9A3B1923DC5A1F2B535CE546E900844E008EE8A9392F317DBBE90FF0475FDB27BE9984C4666472FE38A649DC70019361E3F042B28A224E567608F346E89390D243B185139A5CB5A99809CDF35485EB5AB92D1C0817C01F337569C0EB50FA5C60CE30841005C63D217933F84B70A16162737C6A11C62E017610D30BFD6C44C74906B1806C72C7C0C7EA2DA4A3B838900B1611124468A4DB459541560C9C1A759836496099572310BFB873100C02C10B6746F4E0CD51921FE4B99316811CA49A031ECB296E94CAF6903A1493B749225F19D526F2B5987EE82F1AD29A8E002CE69AA74A418F51A4C2B6E65BD3925D927B433E43026A069D6B170AD6A57AD0D0C1C32376837CAEB1698A5E387A4909B1D4F2CFE5D4A14B36772F5BA75C282422BAB0C1F43C57BA9A9437618C10AA39449275178031F0C0029174A4C8A5562076B4F40401212E84254D048748D3086691542CCB607A15F818FCA12ADDCB4DF73C59480C5E0B4CAC73D64D73A3CFC3E1031DD2A3DB20CE902924C9A8C7F64106AF3181BF978029BC3CE080C4A4613602C0AA0C6109E5F3A813771A54F7473F393022956B673B8DD0262905A8C2CE50CE2F09483BB337E647C9CE772A79C529C8134EFE47CCF6D67054F64D020468E5B695FF656D1CB4B1AEA1A9DDFC90EE0BCE897556DBC158DD6830C4B8C1660CA86D8B3C663214C69B39CB73A74C6890EA6279D14961CCFABBF197207B146AF048C2D8236039BA6B4244530E88229FE96614B834D0452EA19C2B0541C8461990DE9C3AF162B764CCBAA02A5AF389992BD91B35D9581FF76FCEB9817C16B2287242A78142C9C988C12356BCE63FF7585953B0111C127664C26882B97596628D32368F51EB9777517BE1353C52CC864F6372B78192C9FC865098B48DE4071A573348812F92D201144CCF216240368983027A303A54B4F15614AD67158B46AC6E03530016B907109B02FA274B8BA2F0356BA722404F919E03B3761EBB1ECBF25CE1B98EEB21BE7C5B03BA1631E3A774B488362515563FB312A07A197DDB4A8CE1CC7C006085541424A813CF2115E8A9B51F064A61C44B670B80047035FBD095F4ABB8ED760E33F90E5D90040FF0768D990282F58FE67989893C7A1F66935D17CD9571612C2BB98707C707EAAD6CDB982295BD7538ADC744149813A4D9730841337A58EAAEB95C2D4CC52A8C19ADADA071E068172CF285EDAA44F72261B37044203A5CD171A81417A4AF8196D9A86B0E5606B802C44A8B6EFBE894842B8CA15619BB73CBD4CABEDD83556DC80028295DE580024699AB67D6829FA362D710475BA922CBA322CF777A5191403005A86053A26C707F76D97040070E824276B4A4BB789ACE86F5A18BDB2C5B21B8E2A7A0533ABF1CACC050F01DA8DC55D8DBC269E975BD552184A56A1AE45B872A9184E5C5D34210F992B424167817F89CD14CCC284CA22B83088BB5B4B48A9C9172AA7CEACFF093BE6C846F7FECC75BF726DC5B2AA4214B1FA35B834BBAD2998D6F680E7E666122C3324F8C54ACF6C2944948BCF3512E3927976C4B9133B8C1EA16C1F94C4D155144805379D451767B1C042A6E0A576A15FC7B5A9B84DF32012A2B2F2012113EA4827448A854D457E1D880CAC253C01883B509BC1C2783F1F814A785921BB70AABEB05D9C3B21A5A5611F6992EEB04E018B6C4BA9BFBDB5F138B599C3C2872202043183609A76AC622066FB5BCB20B557480289A0695156B327C48CB6D536A67DA4F86B9860F150D82B72648D1AA0213552F27512101154A4045EADC9CA394C0DA8877EDF3766472C67844A8B8A9B5D7F4C4ECA597ABA46306262646A5616C092D29152281B787EF873DF76B4BA1257F43F96B033C3829231DB8A1BB2676B7ADDAB1C7DC91B9936E54926FDBE85C1CB09287F44DF9BB0E2BDB61827137BDFC6CBC849CD73B062D8C3A2A473AD599629BD6A072E4C9D49B356E712982879188C5B2D5847C870779E748B1AB382999221B7153C321B95FFAF7122A1A67DC688D34239CE43618A84285BC86B62DC9940BAABB08C399B66A0010316F8692C511D74B1CC810DDEC509ED095C5E2922272C636BA4301F416B0F4883441570DF4663438BEC7355E0F826B47481B532AAEBB06AF985B57B6DA04BA89A0F8B4ADEE69BFE76318B2246B3A405AE106AE63B44A1D0B644991B8F24B2F80327130586A2DE138E34150158C0DDE0106E064971598AB1C400029D18C7CA48DCED35244A3AEBC0C2BFCBBA9F384AC462C7447495C601C7463C039FA2A633695691596A8CE948BDAF8B5C8E937A8E37A7F7B20DCC7A25F07A527E80A71CA92FC128771942C18A9319545B59117B629198DFEE0457DA2B513572D0F9189A44743A1183FE78A97A254B4C3A722AC52A03289135BB17084DC3F056A0635081A8F5862F36A47B740090134C6DD315A3A6245710C519E811BC3095518965B9BFA9BD9E5221AC436B7B5667DBC06768B2F929A6B35706D4A238DE158BAF1451944B1BDC9EB14546B266FA3AD2F3CA3AD874121E54E8FEC6A121B3B6722B947B80091873EDE468AB9083884C7B5ADDC93A4AC4C5D477325114E2E04B8C0EA6D5736833EC4C462361F1AD21839C59897C2B749C14192463AF547612D980CE6084696A51674B57A3250070AF2045F2697CD5A38080B4429D9869C31A18F996AD3832227592C7C768EE9DB342440C88CD98E22D730FE037123C306736A2D4D2602C0AC1DF35931F2E8CB66B8A22A9A010D89007F36CD4719A68C68849706A0344CBEE36342A7A6A8B8C81971C8B08C8B4C718517C6065D6D729EC3765391705A4E8C84F6ABBA345A50A8FA5B47751E0A3C61C1A56D413A5CB57425AEF4075D46534639BB62957434DB405540269349B9A16B8013002F2B051E7C1CBCEA19737B8C8AB8CBA1A87A9B2D46180B783C353B369A58173F7139A383A31EF9C98F06495FB45EF20244AC04BFED903A8CA978321819B5E3ADA52A77F3ACBF8642C570072AE9A30C218118C544115F026CF6257886C1440703DAE33C9BE8B8132E9AD514312460757C3DDBCE268A08FEC902D41139788F020BF6135F3C786EC0A1C21B3945EA2AF22A0DD9D29E6EB48CE816BC73708B2F73C696DE5528B0A05FD1F75F54E8CD022B4DF4B74DE767F1E8B795F42AEA9CA9", "6E90BDC6477D2F2A0806CA44E1AD7189B06ED9A51752FFE174E74BF9C16325076BB59D2E85DD74B7F4D377FA80BABD334FE85465377CAC0128B037D48FC3674B2E48D45AE03B0CCEEDB9C98C34F3A38C8FD791165CE9841131439ABF3F0105E1D8B5AE8B14F17211272B3FA4A78F8038C9368A88CB33696597594D4543FF23D29FD79882BDA21A92D493C9790E5BF5DA93D8DEBF1107076E8B50459DC6E26EB27B22B01D4C96F02F9C1A31D589FE9CB083A403CCBE56DCA78D96C3F1702BECB4D28DF89654F4F3F4287F26665FEC25B4E1426F40364EEF0914914AF3666B098A9527458FB7E8FE4F0FAF1CD77F036D23B3239DAA701C2B5BADDE0C0B9370BC17C2A74B282B94309C83D31991C8F30D41C580E503F0613D59DD5FF651D949590A8C8F9A4A30DBC241DB68E749E5D3BE6DB4051C283FB130CCDFFDA160BB5CA574110583E0DCDF1087A2D83B27B33E9D3865A88E8E4FC31D174DF468B88631B579F2773F9CBCA9712A840FB9437BD639D611E020809C0C1D96536493FE4B0CA3DBD2041C207C6C8F23077F53FDB78A18F59F30CCB304A73059F49CA4624BC9A40FD7BA17D9903B7F5E534913113EA28F40A11E1F64C18B5F676AD71F183AF87C23DF02CDB33DC10875408B8A7C335A2BEEE144E2B83B0931C8892D6C6F61AE22BD76F8C293B5153C6CF6FF11DED7ECA0EC5F1E51024DEA7FE85248D36A8A1C58EFBBD092D66B3903E1B8AEAD44BE2728C5D0AB142B1ED2BD32791C1C3F7443386B239FFE569D3119A18F497A069EE671617E6AFA3BC1038B954CD184B896FF3D21FAC0DE7E49A55A87C6648E4174B960E73AE8D73682CF08C6E58DD834C8B897FA991A4D6C966CCE9900F9B911B6E9519C5AC052232D6B9A43A0425D56EE054B93B4A6033EEE05FD8604ECFD7F43D407FE5FE40A468053428E610C2DFE0A15B76759D769269FD7964B1E382E654E0B229D14295662EDDED202391B0438796AC27EEAF1E6C7874D06E6D72BDC5E56310EDB1299779E56E1795E4AA4E3AFA96FA3D5C03B6C3C562D4F680BD367CCAE9D0C8C6153C84623EFD4D4479F95E603270782656D9F5F94EB1E1C51CA44B495C4AD8DBE83E57879D1DDD2E9CFEBB4FEA3114C223ED4BACD36E4C5F09457106601AE4CFC9667EA6A0BCE36D3CF2C5E9622A1BD3628AFD9F9357F2FA02D352B56F0DE2D73308041E6AA84B543EA858970FD0EC5CFE935C206FD3AB7031F410D417A427C72B2E42BBDC504276AA0A3836CC1B9294D5DE7A959522BCCE471BC3C3DCE7D9FF0AA4A5138D6052B09C7CAFB5308F8C92FA0BBCAB1D2ACA0551CA41CAE040705FBA6084B5FB92C8AFB35AFF2627273A6AABB43F9EA2CE86C31B6E00D60A837A0F5B016CD811062D5E647EDE2686DED705F1FCB034DC32E01B4AE167DD823AB5A1B25997097969CF235460A114C6BE6CB1482C5B550B2DE35FAE7957E425D7C7BA590EEB0675E7C819B487AA7CFA59CB7A52FE443E6EC8AF9114600A1F7A9CAABB2AE3481DEFDE3B3E7DB595CD12DF20D02D4D74157477390DCDEE70BBA98BCC88DCD03AEFE71F4BFB817901EF1147F1F481267788CE540D191F0F706DC3F28CE35E571F61CAC7BAD74A8E0282AB91F9400B3B6637336DF339E0C1BEF7AC7ABADA3B1A9E25DA4EE5BA390BE761F15D390A3ADE467822D55AC1C38CB176B8E22CA4E60AEF53075EA83C91BB7109F4387A49B097EE2DF365FCE5BD8CC7D1A87BF391D238A2ED7C64DA6CC2DF988FDFD899B11626BAF823396642586BAC1A4BDBCA92D508F2D72A466617B2D71D574B718EDC5498A83A7A2C5CD5CB08462210EF225D080062283F14CBFD3E81A995252DC31F0A45417EB97E898E05C5CD535564F026DCA06F5B07637D686A57B937ECEAF407FA0CF90EB6D9B33A1A65D7922A4F5C62152E049BAADA59CCC202A3F089476BF0A96E037C621890C765899A44D5E494C268A4891B3CD08AC4A0C10BD44D21ACCA0C79BF3BF1CF5E9D1EFA387067432E7034ADD4379E833E4D25B7D4007E44438CEC98D15F7926A676EB263BAAB1DB2DCCA55CE7F3B02E5F5C6CF01AD3C71F2BD6240FE124A041EFB0ACC3CDB14206C24F71253DC0A07596E50E84728637A6FAEE440E7BBA374914306C2CE8630CBA58F1162E7724AB52E232F490BD16B9187169DA935E706870D6EA62B286C7BA9F1AE0D9DCAFBDB74AD6B8EFF0FF1CEBA42F4", "A50BDEAA22355A6D1AE1025FF97A2EEF84CAE20383E395BF2636757E1E3826AF"), + // modified ciphertext + ("044843AECAB03CA3AD8387106D4587446199A01101088A95A6C98FC7726BA642A2D1E0647BDB88D941606EB67E4BAA89D951658A9127B3B08A52E29E5F76061DE0BD8985C43A0B9C57202B3C59BC0DA5223436C5BC83718E8C5EB3B0C4FA977FB52C77C88743CE3006807C6913E05A516906B0828F3B91812950A1CF703E89560DCDAA2CA800AF5A44B995F7C64650CEB6477273F1B6AC0B35FD030D0EF24D4E7132F2788EFDE0A9E215291859426F70B7E4550D17F815FB71CAB59348B6C4278180A4AA28744EF2B195FA4ECEDAC8910C4C041958A702518833A6934A446117BB45159ED4BA42D1421D52071B0D9BA6879906CF582971EA30F1A63C64A52222F8898E080B56A98A33287963B4C4D11916CF716E7B079D5CA33ED5D7C75138821706475BBAAC396BA31D17CA5705A9B04044343064BBD5CAF0A6C57A45CE3B216CFFE47345F97DE9440048E175C6324959423534548375378933C29B2DB02779AA5F6B92796D61A18F89917228A4AE8113E9744C750B6EBABB4ADF1B46379891ECA75FFC291A56636496B97C27BB5F8E118B84DA5B3194146B375B9E210D1C1B2969B53C34E889B49C481819442848BF30E70B6ADB751D098A0122951DE6C8833081B5B047D0517EB3A819567729292C09DA47B3F529C19D6630C5A9330FA8AE6E176D3C4A08326B1915DC0D00A68E01E10972E8B7459B41A30205A0168F266821BD17A3EB63C1830687A3EB6880D3352A7785424C064D4A9549B40203B3B43C34064FD1986AE6516E10BCE0662022077DE2987A48557091B3863F1B741BC03D70E4C219847D54B414009993D0E60A13A61E0C651148998D23B12B2C05AAFD15B76EA03503277A4A0368B063027C9AA303693A3DDA241019B5C850C4D16745D31C6E421614A4C092634383F2327B25A29EAEECB7B9E81B35940EA3234F043164D7658440DC1967518E79801D1814811DC2BE50CB3129D984EEF44CDE0549BBC31E147B1B5330A0BE001E865414A1CA98FCC557EDCA065DA97BD1F46FBF64A77D95A7E3FC6C02B74904D3932C86198B73B3B13B667D6C3B9D1B305D9949E590B9863956C6567C3338A51B5C376481A971E7BFBCF8BAE9793B87D2B94A32A250B98A982B413CB33F8098598FF6888F31A3C7E6A548F05B24A77254F6798EB88FB838B09241B92EE034A4E16EF0048B37291E36631F50359EAE237BD7B3382C7B4DE3A95845CC6E8D5642A42B0A3BD4A27CD3B5B4078D4B427FA5070289D5CE8F022956827B1158594BB238C23B70F2CABD00B882A194544248C7D997792E30727D612C04689ED716353ADC4D93D1BFDF2B91E767A87B779F81F4A295A112D1087B03D71E4D9AB2C83B96632128A154396621459012B5DB39136F96826DABA6EF68627223684FB13F4FE7937EE859B22890874C811A32460725920111BFDB5C4882524C544277FF72CF127A840D07AE93FA526F7432195316CDBAC5C93822AF11C738755C3AAA37AC8C24BAAA51EE049BA17C543665486FD50562DBC5FA14C65F63A47BD1CBA840CE4EC57CDBA0B49E020ACE272A95B64A4B37583A6861DC8017327C4D14EBAD5C47AA28F6C524E961047C3167C934F411A7A9E75C2382654225753F480D717B933A4997FD45773C09AE01B5CFC212AB91341387E18B18472AEFFAA9A2986D0429B756A25DBE026E0137339EF82D10B3618B3C5EAB4240996274896174E6FA7443B753DDC27B15909B25959AB376437CC316D9561B91310BD7C575DE15C7E0228BAF8B4695495B36D056CCE58732D40823DCA59597980A168054311D32B19C154989B867830AF61C78988640A47C1FA71058213A4D93B6899507CED783126C88CC2524FFCB8E93FA40BCA06BC9F2372D92622BC04ECE69CC64042F66ECBE660BC72533C75DF0BEAB4B9EF91431CA251A06D4173CA9391F718DB2F36C1DAC70F9651C7F122988B78D73CA65DAD2983417C89BA36F9CD711023A7561E7492752A4749CACD59ABF1763C404452FB3E1B63708A0D561B5549CC3D35A953E497852A6138D9B367EDC09F0A23B9F11B66EB29A401907B57BA46B3BA411A04FE706BC342980232AB225E81AD6B4B5A95A0763B30BAD646985995F8B779B015A7B53714EA314B2BA006561A40195F4CFF8B155025BB289E05C760253FD761623106103767B75F0A08BCC593AC5A5BDC114F89C3DE7005E044A167874C549E0A9CD387C8147179C52A81F028B6161570B68A9A9236B05CAC19E037BECC7425D5C35DBB671FEF281ECB581BEA03A13887246191E2F1302A59BC4069297C3224723C3082E4424866A77DD0BB48CA5B0BAF7C54B816E6C2140968AA16EF906474C26581010D15B79899CAE94FC149E15421EE23DDB00A1D90A1061DA94E8DA8389F35EE2E990F5188127332C81E908DCB51DF501A203AB25708279AC4663241916F1499F498C5BF019A31618A2D2BBCE12B613FF5791E0925922226FECC1BAC3BAB0474B06C6044A26A2B53E4362AFE55198420A4C0240E3BB28548C7772D999A1155A6AB36567E463C905317C67C0A1274024CC05C002B3E1D75FD0F2C25980961F6A2329ECC71244684A0B92B0829D7CC2BE6AE3102ED7B3F9BA2F867B1F4A4B97F44C0C9A01B0E9C3C6CDF1528FE8220952742326647BE33A9459888D14C26E2C0E778C9314B845E1001699B0655FB1379EF29EC7492919A162AC900F816986B1B814092A9C16BC37DEA359B9D6B6A6B8670E242EE4147162A80A0E73053928156D1333E4D3ACCE0A084D2B6FB4501BD1A90B9731345CD823923A068940505E830A2207A1D1060824108E63909EA3B0055ECC83255565D644A91D047AC69426B6819BAC5A880BF9815AF0B57BC3615161A455D7A7E33C05F61461AC2C1886C47B3A9C51805AB2DE3AB4D89CAAD618274988BB26B4B789CA7C6236A5FDD1082B87BBA523BCE4906BF14A31E96A7D7B466471F233EB70A48BAA481B61480E726875C6CA95678EDF79B4CFC12A81C3A99BA255A32A2DEC84CC3E47A3A0444A8D378C8DFB4866E15DCFF229D4EB2A8E083BDDF68DA6336960A9C2A5714E3EA69CDE48958A37CDD26CA7A762A41CD3C194C4C039DA7FB0433BA2C8892C8247601368AC1965EB085D38C363EC8179C55A835644B7233C2173D6231EEA81DC64CC6E4330F3955F1564464F664B4267805DBBB2A0E98DD6259C7D1507EFE8C3C8876CD68AB3E400A2E3C90F5967B10B9CAB0B82B6222108E1F9CA670AB0DB49B227433A8FD6862BBC866D23615CBAB366B972E227CA8E3B156081CC3E74128BC1360E750F35119CBBE2C950B8C6F785B2AD6A58611716EB9C5534B683549003A3613267847B2B8865BBD2BE3EDBCF1FEC41EBA0BE97000E20BB41479991DEBBABBA19CBF7152E1311741C99928883313CA747E108387B648A4E416DE4CBBCCDE40A91BC6009376D6D9CBA77E7A810D7B56A21B1142C81DDD4123532CEDAC6A304394BFA9B1312A40D9560C8C010935C6AABC9C16EB30659C19A9AFA1593A0780E138411DB9B9D0532496788679A0A8595879C14F95C234A6FE20200605841E741792C6225CD2C15E868142731BB102666B5B8193FC0C45C2903E22C9119208D2BC671C5070A06335FDE9C5F8D2A3BE0B60A57A12DC4DA0BFB31A10F3A684061192CC02A3A91120DE6C873514081C515A2C001C8EC4738608FCBEC374C5465F41C558768AD3BC269FDF835B604C1C369BEBE0CB29A4AB5CE2C7F40D232D62C2E93D230FBB361F119C025C3143C841CC1E17DC3F1B9FF5199DED839C044887E256A0A01145453817234816FF45462005A8A064ACB9AB692832D55EB0715C35228906DE053BDA1B9486D272A78637B78635070587771805258F823D46047C3D69611E327AC288549FA7AFD41046CD90BBE535D8BDC698259B2EA691EEACA0BA2FA2BFC38A12D62154566A3C3054B81F3CE46337F1FA4836BB45167D9937C53A8D0C6585A36AA1E1128D8642339C8745E4943B9164CECCCB1B1AC6E17FA22422B85FFC9211AA875813C097838AFDAF74B496A9503D63289356DFBBA499B703136070C78E193237638F80844B4C6BA9B4B85A36007553246C9C543EC8AC4F4E8B0412084EA83A68EAB52F3924CD9DA9427285E25F5A68D097CAF262372663D41A681F2101AD7795E75D123619491262C72CC80C6EDD5AA7A4823B65CCBC4979E02236C02D206E1EBB63232AE1D28AC492AB8CC7BABCDA32D6ADA6E8ABA9DF53C6B63F61CEA1ACF8C7C5ED4DB51DDF4BE3570B225521B1B92C7ACFC010283070D5C7D3562CA45B48911770D5AD06939A61D6B98629AF14CEDD744BE8317C4636808242D94A20031B07F75B6322620A6D992DADB43FB2F7A2FBAFCEF48B3B8E1D80D3E4ABD720CB72988D306183A19868C108AB6DE51C56FF5A1593FCF8D74090736A253EF8661106C168684E2948C6EDFC4C555517825EC4AF4E33836BA1F51888984D0FCF5D5", "4FCFFF02345D88F72600A06D9023E2B4728C9615FF675E97FB31F418ECC450F7187DDF6CD2FFA4AC97B501780C81A21CE6EA5401B84E382B045BA6DD7ABCDE9816CE4639D4C787A74087E05C26745F565A533DC2686C2032100E4DD0795EC2EAE8ED0E3BD781603C50734C51BE3743131F238CAE2F6A41BE4D333A3E4BB2EFAF857B329C65C58708FF757D9186A0BA021DE5C8F83157EE91B3B41802990B03FDC6839321607B7F3E77D622BF771F05FB3B424F312F83BC7BC5D57EE6E3D2CE8D8CCA2A1553CD56F58D2D730F3FD94565A3ED8C00783AC3EC7894C3B1C5C80EFBC2FCE36744F5A8ACAE654FB8239DFB841AAE3761C373200B663D94AA8445C97C0ED59EFEABE23BDAFBB59FCF932081A7F8711E2F97007CB8D44290AE5A562AF1FCAB608C13ADB02E9A86C6BB3122BB753C193AB24DAEF459CB55A9F82AEB025B77405F6F792FF5C155DA36576019D13C2D9F3BB349356E2D4F66970884597821FA2F8578E777085EB3657711AEBBB92C19F837E73EF169252C5CDA16A249FD2AFE0E643CE513A1EF9CB8B921F5F2C92B75510EA831008681D01DC7755861874DB8F4A4066C4B3BCA6C309E0987BB88BAC0EC5D903CEC9D23981DE54BD5947FDF8448E9B39CC5DB6972A976097AE432DE4E5749151A0B1552FD4D7D8FD8FD255F9E781BA60E768534A87F55F2F0ABC5BB62C44ADF6F3CF8EEEB24AEB01073E3ED6A7D94B7284ABF0789A9EB3ADC7A1B6AFBB8EA64261EA3B260270607B30E2235728BA0B3C0DBC7093FE88053EC651FF98EB06C7CD25556FF815A92A9C43A11C540FA17DA529C78500FB77BE69A80C88E34D377D0D3A817FA6EA783C4A0FCEF69B689FF2135A80E871FC7260CD8B143CF0D3A251723ADCACE2DFD93BAEDC6D6DDEC139E56620DC7863EA41C4CEAFDCFBEEB8799DBD69C486AA32AB0A0EE7FAE05BA80B76627B7EE9DA48D7A69C586577A897502177A236EE3FAF1D6B1BD50DD050BA6F7B1ABE2D7D987FAD1692950A2D07EA7678C13A6788DC668235AD16A0A775E79D88B165E7439AF9ACCAB04A577157AF4183A438BC2D045AB50652D320848BF7D4D83DFC0CA140ECCE29955A0CDFE4E5BF9143FEC9503E6E6DC35747261548FEB5B9B148DB989CC37FAE7DAE57BE1B0E181C32B831E3698DE7D95B638401D6403E092455D18F23E45B63E1EAD2ADC2DB3C01B8DFD46E97D13381B3278D85BBEDE057554D6271063B36F962E4DFC1904DDA5FB46278E3BDF17754E737DD00B7A374BC5DDD474A6392E0630765287C50E13E0DDAB6D1B244B13C59AC6BD925C175970B678BE7302806BB79D0E96118A5E80F6C585B8B5C1792DD295D1285AA7BB4929A88A2E93C58ADF713BE38E3BAF5E20D630AF697232A11CE1FE4B3207FA0FBBE135780166CF9329D5D4EFC777DF35C79DCAE1940533CAD14FCC3C9F044BAFDA64FD8EDD81BD24B388940C9130AC1C57560475AE5C9214D824274F1B9EAFB7BCEF0761DB486357B010D5A1D9D997BA77FD3128A22105ED604A862DFF6FB40CAF0C2E76613324E9F4434CF6E6F06326DBA3E66840F9715F0762D4D21C70D87A55D4D28264F0DE34F072C7B9A42677BFC3B9450DBC7BF44E8728B97AD2B4BA0439113F3173BD470312B0AD3B9AA7D0AD809AC4C26B6F6BD5EE0AFF7E415A043FF255073066EE11F32028CCD068C0CB6B0923AE9B70072777597E00767701CE132E13552F16B73C56348119D28F34D5E5E1B05E28F20EDAB616D01DDC2D3651EAF0D8DCE3DE710BBFF7B2A0750FACAC8429FFD2CC848891F50BAD4C37C739ACBB7121864E4C139C5533E3F56861C0769495941603D172D2992EC980C40083A398BB6DC3693D66BD8F58028D1FD302F47C68E502BC3D34EC29934FDCC6907F0184B3DB61D0F41F5C10F6532C9592AC25FBDA069609A91D74781F69F3BD2B931453B9B4F88C12397C398E1C1F16C9C2B1C7E290721B9C89B919527DC3231193271C1552236F468744DD930013D172802D1EF3B603DBAAF2A36C8298A59C56A4731CCEF5E403E8358C3A878C5927EC4C322E61AE0AF4BABE85D8C4F4A71893AFA1428B54D269F4C9CDD95962AB77E14050A224E33922C366407B329B00153D14FE6CA8DB365D9AA29C1DAAF50644F825DBAB5ACB30EE6328AE585AE4986D38BDAF932B08F390E079BD00D2CCFA9534DB862621D30EF56EA663EB938F2BFB213167", "6F27F20F7092A1CFA3C21542CA2DF326BDEA92D1745B2CBEE8513ABD55BD970C"), + // modified ciphertext + ("715A321C531ED2B3CF63C018FF403FD51C41E9212C73D3BA70694D548910EE98CA382120364347779155C937C01D858D63E0C645A804B22960FDDA499452C169F48422BAC0E17743A11699CB884AD02C379E96C8E2985E79E1A85C316FA872BAFA8A1F599B36B4250ED556896D66BB16F70A9A3912CC509D8FB4B0393559CFF75702125ED006D0A0E8C6039015791A7A2CD0950EEC7326F472594B13C7CC0C536C23B9EB06D504A37F31A4920B6E7F67C2F9A36EF9BB0A2A2447E32071C8DAA0EE3A38BDA28D31F024927C364B99C2F71809420301A5F70AD4B91BA533364307194F94150E9B2B1EBCC10EF04ABFA478DB559082E41AAD8477C1A541D74A091CA3988D128FDD6568D52CC12B016DEF8C8748AA3248954BC3A79977995EA444BDAAE879FFA793F7D024B59CB5CEF600ACF79DFE1A3FF9726B0E5B0729CC91DCF6B11014B9D7281E6D848E22820B44D257BFE16E3D16796B98CB560B7A7A1312DD6A07762B343E70C677E76D01A2A36C2712E9D59480E14284FC099AB79C5A47585B151D0CAAC0E01193AD139C9C37A05BAB317DC5B83A504F7D999C347796D6A584E6229FB401733C44593894423C60A1B2B11047E11858F1838059AD56951F27F77E85303FF9A46FE5D490C9009513F2AD0B635A30EA43E6FC321F33932D4B6DF086A56AE87134820D406030A751782D68068D726A68A35B54A790C7EB38B1D73B8715409B4B79F29BB240830CA8D195FFE7098810118ED14B01184CF08537B4B178621B84FBF093252687775C1940DA9EF0A209C3D8A0EC5C174B01A6D1858BA0E86F30684093C18E0A3B135C303782C804E8E9B67E24228830816B750E5A247D76D4483444BB1A6254985A79295B431A16C5AC0AB9B7A02EBCB505B5E7AE6DFB1D722C5AA4F4CD9F131655FC00C20484A3F7CA15EAC37E1C722CC48BAD325ABED4966666CACA039D40F23F24E1A7AFC4CEE076999A2BCF6675C27CF654D08998CE64954C835109C65656F08A6062B0A6C820231A4F1F3C2828E8A2C1606FE4E2280A6A0047153BCFA20D16C501C6B9BB62780365B412CD2656285A3014821F7E7A26F7CB0B4A4B13F3F24858A0A349E385612A9B05D15F17824FF8D901C8C3BDACC3166AE785A0031CBD8B12ED942047572C8798C1995C7332AC57D8726944C5CA75244012C889C673AD5E8A9F8892B53A7A6C9CB49199812C39389A8033077F069287622F8DE0C1B647C9025A13300631DDE5A780E50E0A193B845A16B01384FF99A5781B070A962ABBC8BF794A47815AB29C26475E72760B967974E26523564A4E54C139324808B494EFD9640D4A287591AF1B414E77894BC61A09C18520971714D47AC6E736BF9B91A706A05125195FD104C74A5534C5E21C7DD66D0FFA0C13ABBAB447BD12C1B654F4BFB510896934AD050B17F490374F81B62B192C5F1653879B1D098727485C558698996C125796E3C1528860CEA9B3CD625CC9E2B7A7484C87D655861944BDF59132C20BEC705F0C79B4FE9838BF6B48ECF7CF8230C33241C57F82C1F3F31554C13607790AEDF759951A5579DA3FB1018F3F28B934158F6F36AA1516A260A0BA5F822155E18DDA6318A6832F002B43C4B274C482CDC314053C5809D8181D68311BE5BA5A9922582717CB9B751FD946589DB313C7551C26A7BA9BFA8C97D43FE8D6AC2E2B8B4AC802F0C33BED275D242673A1912EE1C5468573A1DDF68D63E05858C15A8D6C69F604CA134069B53A3F4F0765E814654612B19261198A25C15353229D4261A531CB7EA508F1D72319C119334AC9A6FC4B8398606198896EE37AFC4BC71801C16E97B016DB9799E750FCA836FC1099DE6163F3B9BFBB8864E43888C9766CD022CF203B76E937947AEA5D0FC08A5A75CBF9610B3B4B92E8E534F3B4343A4109D10631A0909239B878BEC48C2F202F41A562ECB594D74A1626ABA554D2349611314529A293D0A8F8D69C045149D21A35875CA53E0A3D84A771DA39150AC572AFEB7DBB1C4A755646587635DBB449575494628ACBE9090B50782CB3F2528D4C982DB95C664376A6245241605AAFBCC0D1D3C4AB5C5B8C54969C977DA8F95C4C93A02562AA5F5BB3E2058E56B0BCA002CA3C7A88790C5B9BC60D2B46A80D681CED7283D1A46511E55569EB5BFE06357AF020BB19484908601FA981F86842D850B79D9B2702B7527E55CD948BB7ECF069E9BA289C8717C6D3C64A3AB1FDEB88E97010E2F5089FF13291E85DD0C8ABA58BC935CB1B98D779BC00BD4D0AB894845B5AB101800336E99A99341845CF731A40337FB7D3B78FBA276914C41FE6BB1F47A9895B9B8849CDB07A63983A869C252209C858F6A267F5408533A25CEAFA908248A46A4821357801AFE6BE53F5135975B5CEC1316D46A8AB195280FBADD0855A6DCB5FB3761E22780DCB3B3134E9AFBA09A59FBC74FB41C76FE73709F02793446C8CF578FC9408F053C28EA271F304A507092ECE34823E19086143A42666632D6193CAD57E2ED370868C0E6E794E5D432CE07B9ABFD9ADBB900DC8B51B301BC2F4D89D1BDA34DF8889B2E22CB34BC4AB77778B0A8303A9C8D8B4309295548FD83555A221B1D86337468FB7A698921420FE538EA772CF6CC655B106250B0B3F7C4C3CAE404BECD46C80352D893C9F0F67306572B89AA3C9CF92A53DC941AFA27932B24A1989B8DBB8756F298CA8A93EA3CA6711471068817B9DA78DB7773E06A4CC75B3541C4287C3946D44572D43FC58F104AEB421AD3D21BD0C567368E60D9772B73402406939117C45C2AF0508A9005F42C0852D52B08850CB23C2660D5C5C5C5881374B12A75623DF343B6C336921B218147B28E244AF9E6C866133CC6C589882A29CF1CA07261562D946ABCB66734C8685B60C5A27D0480CB99BD2864085EB57CA982197066412434735988A12B5177949B71542356439483E8A7D3E54025F821DCE34C110FAC635806C36FA9690E11801B08EFCC920E7827B3F7959FC580BDE654EEB5512CD1C3CB5029BC8D0CDF201976FF5991DBC5950392999786F9E62000CB45C3C8B7EA2DA6C9F0618741BB1A1B3B9D7B83D7390B1ABEB6C74099129CA1ED1D230E0C1C676F9313AA32A3F25B9BFE07FF3B485A67424627501D2371DEF9CC349DCA5187578BA3A1B9BB9649068981FC75E21F17DE24CAF0896CA5040238E5CB76762BE3391790C7B6C4DF795E3A1850DC289E45C0B4C66931924126A6725D7E551A82B33D700234EA5ACD00AAE1F844F28173B6F31A8BE870D53D230EBF3C8F202C1D0B579CEDBA9AE61A89B94CC3034375E086694D6A59EF176156C52D7E458ABB317D0C417EAE7625DC21D5409293E35AD77AA347789B69127AC9A975B5BF74A48D82D819600E69A963FCCBC36E071CDDA374026CF8E5C3E4D97341611CAA52A5763BA5D0C9028D073788B3809CEECBEECA8172B37511826AF08BC3CAD547D5F9A8F3256B3CAC5015D5ABD9BD26CF9431C4CBC498BC015A67052911776810A5CE84041243BBE16634128FBC1395B045EEBA274B22097E2982A767353B0535A90B0A2D6CC6394B177A17C568CC2174911043A4A3C92467FD34C7A3C0E4851625E862DB9B876335684E1ABAA5D9B98E952A3CC8B6A80254B3251637E40BAA47B5C386C345E5B37BF992B6424CD4A699E2135CA96F2A03D05C5934384B3B03067565DAFF43C0D884EF8149551AA46A01A870B7B266F26072E7B8106EB399AA22E950429AF522C03F57CA7D0784CF790BABA1DE7B8B85BD24713925DED64C824E3C163C77C677B4BEB9BB25077851A76412C8608E9547CE2A93D271ACA89B0A1A205A1E6F06A8276296C704BBD8A97E96C1E101363AF2724DCB693F255AE466A3CF777C166FC7A5B211EA6E0CD5FF77D2564ACE3A3C69051951A6B243F7C7E3C8B00CFD80ABFC4A6357749C6FC38C926A268ECBB5A585F364A9938D100CB125061430554DA0468A59709143AD7F930DB772F61984A29EA9FC16C7058FB7118C0BF0FB91DEAE8CFED9B9C0665C147E37EE7E7AF0F2BCDB0C07CBCE448B15A910DC83F71678DBB794927E0A75EA4914ADC59CEB527145C37CC03199326149F5559CC54381E5786DC73C7B3302769FA4AEE488F79E06069F06C29D84AC1939070FB6B05824FF717BD5B7C5C81DB0F58F9141D32BE0DAA8D5C69A383181F2BE14400EA79B3C321C6A87FCB743D1FA15C1C625A6F2C205E47ABEED35CA818170F15417E908DF0EC88BB1B29820484C5A14892502E13F2AB4F85811B0B3512E66A6F2505C76409FF36181BA0BDDE8B3E4EDC723F073B9B244E1D86AC241752979CA9E6036A78C0A2E0D6CEF3A4726848133166BAE2EE8E8B26CF1AE15B074C62CE7CEBCFB77640E471DF9054A487F7274D7A16074F00F74E93BB83EF23A8C8396ABF557DC52BDD3304102142A46ED26C49BE675691CE797D22DAD9754A023521341437B8BB438AB6B75D8ED29258F7B045", "E36A4E6574282A20D31E5C7901C5CF76FAB307935B10D47F28718BE945DBCC69F6DE7F904AC4C0C312014854D262044AA4258BBB4AA3276AAC69C9001509CB009C69634B9D1574351E3A1D10EF9FDF7DB56DC9387A6F4C545F29798A7714635B492FD81C678FE58C5BD1710DD91B4F841657B6CCA691F360A49304AA12DACE954613CE2333356C2D90AF8851D58BBF5CB792E3F3322944026B78EB5BD2F5BC10499A7CC07F53CB6ADB9B448046A432FC90125919FB7A71ADD744B997C66CC817EB61CD79B27DA95CF84FA6E046A2FA66CD3A91CFC27D5F9DBCE86825F5AA657D5C18C0ED3E506A0892FCF21826B7FD31F4CF5F37A198CB18AAF2F85A64BDCD859F1B353449AFC3D80CE73A7E1518983205A32ACAD694C67B292EF0FBFB2BC89D7166A3F7791EC5DB5AD2FC1466637F7922162331D645E00DD14C1E6A209C5FCBDBDEF2A9FAA97BD6C3D9EE81E6E5CF96855B64C9B601CF864F535DAA5211A58B6FEC8FDA0F38ACD98120EC178CAF41FDD4C756D665AC532D40EB7BD2CA187360AB665EF2C1AFC7AFE13E0BC3E6A64A8E92AF0582F29ECF3147A467F81AD04669C194DEDD94540CA79A6205513CD410352D4E4C0EAFFEF84F5C8836EA402325801DC3EBBC47432C8FBF34E50CC3AB3743EDEE3889B97BF39FE52D9A3094FB532FA128A3260F27A01459AE3F92E5BD3741FD9FCC9C6AAC158E352B0CAF8F444C4D935A63454617410E2CECCE7034662AF2DAA8BCC4D36F5C07F69FE5A638CB406997ACC02B686AE4878F3D036524D7C2739B002DDE011CEFC4A163A053D8327D490239AAE2A8E84EDB45A3678E2FC2BD0A5F073C232F3718D1462D421554DF3C0E83A7B7BC528C5F5678CDB59E1A09E1511F7B95744EA087E35F65CB053FF5CBAD86E37A65EEAF2FB15F5C1ECF632002A4F15637BA6F5A79526C51D2268DCDB1E7C01EF333B8C8CAB1A36D6E9291092C50D4DDF1A601498398BBF8CE06A92453394F237B4C068EA27258AA6E28A92DFF1F5518ABD343DA2345F3E288F21B87DEA760510D6646FF80894E87C153B1C522396A7D1B5A01294E8A2913E6C10A88C8081FCDE677FA050B5D8B380FCAC6E76338A4F0E05C5E10E914746575413A934D21BEA2C34AAA56DEF2E0CDA2569319D3DE6B780D9079A47173CF232B82102B6865581A9B4A6DFF4BF96DB1F2A996596DB01B70CF9A53C3515CC08CCD880739A0229F032D38102DA631A7F117F1449E8DBD345F87946E6D86AEC9C3C6B5784268CFC983C966185EAF4C7D8FA8288FED7597692008B8F962D7A26C6F7F4712008AD49A441A42D0B512564068ED455F3DE379C54221CAE6485745C435A28C274AB6DB1B7249EA148C2532FD4BD990C9C9F0BF6361791D2B0C6086A881857EB5DF4BDCC2C538CCF5BBEB9105BE762DFC965C253A7B99144BDA0D8A8BF8060E391215B9B93B7395E53F6CF9F7098F61EED7B2EF7F324CB56DD7BF4BCA2626B4D423316AF099F6E48CCC068013668E89D6B814CD793201844513CB60A6ECFB3426D783C4FFF63CFAC7007708C044E07D052A65FE4A5ADC6A78F60C48E53C63223E58E5DC32722620F3DAF9FFD0F43BB86A2BD7CF4FC9F0F67F097907E74906515269C2519D282C77BAD0B4C67191C9320E807E4F89B20A83AAF218269A37F32BA3EC209E868833CF3969522D41AF3EE7163F4043B08C1C5FB270C0DC7EDDE21217CEEE35A845123EF813466C387D67C60704A6564A24A0C0607A8684771CB07E603C26B37CB94AB836880E3D1F2D2F63264E2379A02FAB78BD3BBAB535BEC2E21F876E4F88707C720FDAA833DC46F0F1FD29D22025111828DF4E51744F2A2D360B22394267BC6FC145D1BDA0B8912282428DF6E72AC8CF21F648D063BEB5F09AEF722872A9C5CFEDCACF1BC0FB82367FE92A65F8EB30299414985CAFFEE56E459DDEB0564EB4BACCD6B45AF8E059E9FEB33D61C88BC1C55D3645A5B2A8C4588C5D4931BD13696F06F0A45CFA72AA923D06DBC916A8DDC0856BA7F597DF5D3EC44E6EFBE067B7AD896E56D6326F4E275F93C8A03526B2D7E96D7ADD5DCFAF59E17FCAD5E6B98F8F91A6E363889536010AC2E008229462783748D38CF1DDE993B754C8E46403CC378E5F5CB322CDEDEAE30648484AF545BCA574F4B157CD178FD17FF1D854FFF04DA8D764497F86CB403B189C3E8812992D327CAE26D8BAC35A56E8A5E830", "32B938D50D2F513CBCF2B3087548923CAF777163B05AB4C333F23CDF1B1E64D7"), + // modified ciphertext + ("56BA88526AC0BD62310EFAC21B138E6A540340271D16F832A134BCD011C475CC9B2F199A4803870DD158F8314ABCC5C4E313A2605C29AABCAB898C45E5F5A8B08A92755402726ACD20102B35E501055686B71AC4A7C02209F186A449293D28A5C1A8B6DD450F22B0BF57A6681B0696128CB40678373EC57FA7991A47B3299D4790F8F08E5C353D1CCA6D63D14B3CB610F8774B955C15AAFA7039874C9ECA88B6EA5FC604A31888CDF7859D28BA0D9C907AEC2742CFF505B9140B6AB933A3B608E6323D5CE4449C03720C63C799C383A970973989CE87135CC9D8BAE9349E4225090B6C739B630D1808860526B9F9AA0758F5CD6E63B603B68C0580C7D4F7942BC2858E40A8CE6C35C1B75979230D88177B0D3918D5C3C11D6286BF653E00634B8AC76176B79DBC002EFF37AE2FC9A4B6D5CADA9C9A1378A10A84A49D42751BEB8B4A4CC455E64FF35B2F3A024DA76906A9152D98B1902D97CBE35C576EC2A8A148C988A1C90EB18EA0DC9C7C3026324A975A5468DC56B958247C915C4255F3718584BDD5810A5B671BC8423CF62463B0B1C93E6104DDC1BAC0518576F3A7F5E4AD86345A7E11659BDC16EB002A7D1B02B7615741C5C742E2BB0C33083857BB6464072C07C771C224C008CB50656C7F12B0B259881D2101B0D98CEFB3B4CDA02BCF38044E895B48F09DC2854F2F969854463D828709E297181DE2179CB18B99959F3E001A55683E8EF75296430228A442477B7D93F504B2B48EEF0688B3729204563A1121C5831531D54A229E4AC611F77DC1806D1902388D4A92BD7352737776CBB2B0DC7B4C63C0B7C3491CA6A094E58C219999A145D53EE1E5686D73B3B248551B44CFD65689814764BC2480795B5673D11E2CE50626621B2B6C8A0EFC2BE243C8C9E871E1BA5FDCE59BE7C8B2DC1298D2193850A6AD6F856FDAE9C119D2A4FBD6A2A0A48A6AF2777A0644257256B7F02CDD75C36DB971FA4C7A69911E6BBAAEA2067F8CD22E9B198EF7B49761B224A506329C14BB14D10797A81F951AC7BD1553D4A3363FE196795B1F69B42369D4816D4467E8E2820F94CBFDEA97C3F1AFB440C66C085AFB347F71FA057AB0C49B59BE1A899EC8EB282A79CFDACA3B179B162903D060886B27B9AD4F159954564D7AF44BA842367F4CB3BAC6415DDA1A67F78E817308E6D227A4637A853BB6DED24B5E3473D86070A9CA0D23879E51251F0FF86094BC5B48058103E6388654801FBBABCB416ECBC249640B6263661E3F0539D3B01FCE39925D321ECEAB438DC936009C1D4DE52F23326B9A056348569B935604F26538027B777DBA46C1D900208739EAA289066B754B4CB463381F756C4D5DF5A87BB85EBF6A97A83B37E1261E1330AADC6C3F75A2BEBD7935578BAB2B07BD5704379A74B0B2A88E4197482B0CA32BE1043A635016924822A4CEABA54842F8445B3A5E2A78783F38968DCA18F70331C8C1317F73B36C3390C0EA6221A953A578CDB0186D9D45CBED841CCB9453E6AAA46CB019B660788568715C0A854F4005659629661094B5379338C11584B06E71373FFA6B78F87964DF474860F50AEB3544C463739CD74B4ABC20ED7B940A78721F307B08A44EE6BC2660367AAF510975B0265606BA2472C7BDEC10C4F654376B75F5E8B471BAB969A78736D9314A254D0A433B3D44B7C1E09EDC2A426A067F5C950B81F13D4285745C88253F068A8AC346D453499B525610D9201E1B3010874983D37151F299C36B3C6FB02B6EF687B01B9CEB55AEB38C0F6068975FB71143BC3B86C641FC72BC74F67BED967F75113A85204E146A920F1894DA283C81E42147560306AAAE89694ED37B3AC5A1B56FD8B0FCC859D9AB4578E35D36812BC368984B126B3CDBC4D9195445218E07B74BFB07A9887754C4A03F39B58F07344456C51E832578CF152A2AC9C363F775ADA8277965C758F7759DCA303DD310135144DD8CB640F4396E2404AF242440101FD6EA817C43638E74874767B8BF75B0853AB1D434886D2A015661880D3C459E6A293701747F65A1B87333C53AC580B29112C45383EB66970C0AC3A6C2AC5C4B83EB9ABA29A7DCC0BCF8577F2C4C244B024914A79B761B3378FA0DEA260153F837E340688C39B4E0540DF8A5BBD270B3646460F66C4AAC13521B20AB192B75E8F4AB16199547DBAD46C29EE2AA9C3BD21155A1CDE1185A6FA4CEC65871C9082008FCC2D6462E9688A08F21A91C70C2388587263A215992771BC37002C4344BB77A3193C148769624C971525A50CE681C7D4734321A4834CA85A4503704F0BE80F342308C7C34099D0F095521F877E6C01BD5317B555CCED34138DBC45CD9B34695EB3AC78969164274F821AAD04C38739A6CB6D23473802655FC1FB32856698CC93C6B7BA9A59C8F7BB4FC1CA9805B60E8780C89338190963BFEE01E489CC5D7C110156374194B3989D7C49E829C7C402C9D449792923CE810A3D7FAA1A157BDC2F7BDC6963175F2308F5A1972FBCA31C44D69B400306B254D3478012680F3191DA9C6515376C2AE0AA73AF90301ACA5CFB37A1238C7672C5678A01991C773136A113A49A847CAC37BAB8B05AB60B6A66EF5345EFA3486C97BC1E11A51A476BAE4122E0253380F39CDF0F458A6005D64498F6F7CB4475001FA7805F8D66FDBB245689705CF172D7AF4543B573C256BAE89F33391B3600E86B42BFC04366C2F33CC5951F99ABCA4851CD3C84C660D09EAB5ED577C407626BCA69426530FBC61589085811DD066E118B8B2EAC79787184F21C64F182D82DB587AA4CC6A76529C57482F283EC7707F615C7395B89BCBF641565ABE32533F888325401B69EFE5219BDC43A471C18D8526D6151FCEF501B8AA6B200C632B6A93FA308BFF637749459C514949B113C3EC76C140255F923932DC2C2C6067C7E25C244C81B4D0663A6788A9DA105B6119B44AD118079631425B639F9033471186E233C4C9B823F2F370831B50C61CBF66912C4A11B63A34703111A3A1C198ED274CEA03885E7A7F261893835B8812A7B146D80F8DD13D98EB6C75FC9B99E3A63039A7F7A7655042BBCAD324528AB4B9668FE59BC42C193DE8237AE9D8B3C438BDB6812914F17DEEB64C3946608178A2254132275BBD0841692DC4519408A1DF1564772942E04B9FF0AC279680AD4D46145532638FC0648F4065DA663C14D6788D9C1E6189460FA676CD766A2DF42C7408356732331D915D55E147C4D5C54E470516B91A8B965DA6481E3A9B4DA2E5C577B61FFD0C8B6A88928B174654449270C0CF6D92653370290D985F42F1BF2BD633D0613BA4B35C4B85AAA9DB2784C5B7519222A3D55EB9C63F5E630CDD7011B0092ABFD1BA5ECB17FB0132E165732CD6C519D0680F6B9F717330DF16C55EF6CEB7111EFE0763119C0C117A42872B700FEA551D86A555C3BEADA9157D24095400160CD0901C209D6CF64CF0940FB4E27C80659155D25288F91843389689005FF582796E627042D320D148C1779C825ABB114F07C9C0765A24901152C5312846C5B1BB2B0C995AC5B9BF354C1E60459FDD8C0CA99068B635CAB0B87784A528ADA543D17CA75A92A16169819A4B8F335468B2F22B64C33C89597FA5279891F78A5BC633D4642FD877881D4826D5BC14AD6819C19157C5D374F2F112DA276A2EB8A3344B13DD6372B0747AB2A09A17764C591B1589DC05F797822B0727703B1708E0799609B609E169F2D80B6871CB149B447DE702A354B1E68607C3739B67B1C7BE6A8905040816E182FE765446516C25A61CF6F041FC702EC443B3F029CF96464DCD2A6A7038AFBA15857DFA0DA3955A8300694ED2167019B7925BB9B1E8C9EC155C41DA44EE22516D766B88B06BD246352294A865339E796B01EE64AF0B58C2D574225FD1CDDCA36639BC877FD1CE2232482F8AB2F15097B720A553237C30129E0235904383735AB838235745606933D9665573109288E2B28B6627C6DA673B9711BD338D36D2BB4EE28068C6B980B543EAD3241DB93F0DE70E331A9BF5528A5952623F8B41B079BBE1CC551E686934ABB3D4279136F30980A753B4976EA4C1A0C26A309DD08F115C03FB8A5F77E08828D02927973A55960C8F048128A868276C08F308A70A97C137295657654EC7F715B560BB6FDB3A886A17994165F11122D5C77B437C186765C292BC5341B9A7B82A3640F84EBF1A3309B8846A0690401AA8660509F870AF5F038D1B735E82E76DFA702E4621C0FD17567A9194B142379681AE50C8456A829BA04C86F93C524D3C2323394853BA2CB1697797C00BCA68740E4738D9D7ABD0B7A372FB7DDEF05DE800C0EF1B5C5C0C6CB6C4BE063C172165264A8E589512243A985811CFBE3681B283F38E936AFA5BE5CCF6556FD19377DBBEF56EC1F9C8164BA18E0B551E143B18D953AB8D8E28A714B83ABCCE6578A603830622C782A328ECE04BF9F81956854B87723D9CD989F7D5986622A0CC4E", "571642393727EC2180916934F8934B29A6F53D717DB0A5C193A965C306DEF344C123558E6B6A213970E1BE2CBD2A59C7E028F036E1F1F475D21AF822D597C8BC3BE7E51AB6071C6287B0F4F4BC7DE80BBFB57868D89539EC30DCB22AA6950563C6439190D134A1A5C3EE74E189F097554E9A06987F4D9CDEDCEC264FA68081AB9B3755F8FB6C09974E8B4188E1DDEFACB973CCDD39C3B9D83490A125AE2A86BC2F433900893E8EFEA8AECB0B76C12A1D163223014414FA01E4FA4DC2334B98DF9F2BDF5BF28CE94B3FA40E116214B34F4479AF2326EF5FBDCE45688C94374583A98BA741D726D30522FD7FE20CA114A3E07AEFD27BFA25AA07F4C8824BDB819A1986D2D569E69421656A49DBA6649CD3C091E5AFAAE4E0F0B4CDA4C9DE0C53097068796403301844978F36B6F4DFEE07DA2F9BAB283C8E0ED3F4F81F3D7B0D52ED18DA9CC46310EA506284681CCF4AD9AC89AB78C2DE4FF41CB26CD44A9580568B177D46C360CF7AA4C37287CCFE5F5C4B623B800EC57C86986C7AC11FE305F46AA987F06E1B0C2E8302B0A395D13FDD6C5AE9F9C188D3EC94963CFB024343954CE0D066FF9683D2325788903F741AF1E8F5B30EC909AFF0C31C5F0B1194086D2D31275C90E398EB05BF6F0833B49F23AC93537E3E23385923BEFDB7EC484094CA482FE6B2F8E415406A0F9C99FF0FEBBBD3642D21F9A356768605B9D8DCF7B92C7011477043ED041F299D291B65891E34BD7A8C9D7CDA1DFBB39D3FD02E02A34AF3E935E611A353C1F32C2A39270ABD25E6B935039E15A599CADDE031CA8D44EE3698620ED27180D096E4FE7C284E07F1E90A3E37AB3DC4604E84880EDB44232CA26EBD8BE554694C73EB7CC0D61F5DD1A03367E25862302113629E1AB3265D5F8F372E87E71A38E9AE5360F3EB32C836A3CFE896634731AF3FF020B82A5F90081A84A3E11F473E61D2F6C55DCFE304547E3C67DC66C2118DB4AD4B3752A95C8241D20F9234F4255D5A2B30DDBA1D7FA019F37E4D3439287E18074E0209AAF00EE83A3D962A5F35DBD51746016FA0C6946FA5A3E1AE4B58869CCA0E6075867B39CA52F1AEE845A509CE4B06CDD74F44263EED2C9F5EC4866C3A3AA1172C9CF1451F70EF50555F0978F8B2F840FDA2966ABBEFF7233F3291169A69742B899BF8985C65169ED782E581E9540A79C9355521C5C50AF563850B4374BC835A1080CDF00C37209C1A486FE3AF7DB911DB536D195CD4FDEBD1A27A0A06210D804D938A4249DA55DFD1DFDDD9C2CA9A086E3F0DC74910CFBC59AB3CE28A4CFD17FABDE76FFC99AF5320A977EF20CD779AF82CA76385C98702B186D58F664DB5867B1F9525679D4EA7DA3F50D6CE73D2AC2E552CB3BDB6FA9E841774022E93EF4FF594C12FC18F8DD79EF4EDF498FD351BFD22287D9C62DBDC1F1DCF9972D584D757DA5B690F1F3CCC9345E9AD6BDDD2E7924F127F94E5FD56CB84998059BF458218FAE4E50C09165E407823C6E12085F425247A87DB26DE6C1EFEFB52B529D014C6DF1A8FBA5B3183EC2640E23380DCDEA413E622F2EDA9E9FC34378FEF5DE3575C3AAA63D4F3BB524221E355E62EA93E8C8C63225C3FACDB9B3EA280432A274D188CD5FF462D4AEA0B5D56177EA8A9D12C147A32C54911729A50DFC2D15F2C1841592BF64D2AED354DD50ED000AF092EC2C1A185D267087382DE2409679EC6A21645CD1ED615F84DE2FACEF49594A61AE53A0C81E04659A2458B1195E7E8C1DF202746B9D470DF490E7A515541E495CC15D4545C4920CC612E7E6804D0863313CE37846086A55496033EBCA5494B99A30D1878646432911B7FD823450F26FBFB8FE1FE6A5417CD823858CF14C8B29D6C4D5BBB9B1BB1BD9C141EA9B6C8BECCF8502840D412010D47A58589054B965345E37B60F8D9A42B833A1AD6BCA62DF3D3A06376C3DF61AFB4D5AF87A3EA57E429043BB1B1907AADCF075EEAC6A672795FF2E642D26D253EFBC6B41C469A906673230F7CA41311E602F2D7CC9F6F156EC4305EEAEDE9FD1DB895180DC0EF50BFB1C0338B72B11FA82A0BF313497FDAC38DCDE0E375AB6DAF2DC62DB3EB63A59D9B7E6115F60A582D9A7DF0FAF5E362FB34D2B64D63C07C6E606B47520462004AD2857121B374F044FB8A31C945822A61BCF9985A155F850DEDA2FE8B890C9325A6E1DC5EC8B79B88564ED603564890083B27F290", "933D0A130BAFD7E072A625D971F77FA464E5FFBAB9E6D7D7E4FDA08FF685095C"), + ]; + for (dk, c, k) in cases { + let dk: [u8; 3168] = unhex(dk).try_into().unwrap(); + let c: [u8; 1568] = unhex(c).try_into().unwrap(); + let ss = mlkem1024_decaps(&c, &dk); + assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); + } +} + +#[test] +fn kat_mlkem1024_rejects_malformed_ek() { + let mut ek: [u8; 1568] = unhex("8D0923CA8A2DA2B4146EC25321122B8A5AA8AFE0C03415273008A46EE83031E98AAAA125ABC75D3B30322560C197E75DD0E48A348099F7B2144D7B8A8660A4A97BCF19C0583BD9BB2123033CD7BB5A14B08B817831A673A28170F5F6443C0551913A327CBA18C3A053C4040250403B70AB9588832403AA0FC37665E04980FE1602E7D2715D9CBC00515DF432A4F5B32B3BC92AE3F31700166D498123E94576509B712B18491B1435EE7AB7AEB1AD30D72348C3CC083ABE24A8B12097BF32F792476288EECC3BF630ADCDAC6ACA7950D9839501A448500742BAE37F109203A809B2B960A307E25347A32C3EAB79288173A878789B296E9E8C1C28C5BB3AC472601C9765F7B77225A810C7B85370BEF4A5B079D2015ADA54236B8F33840675F9B2EB427A1B5974CD5C61B24010886C5A7BDA5BBED974AF7217F3338AD719CB308A8BCB1B6D6ED2A1643736C29095E8A8452A3A36C7BB5AE58CBFDC61529466A90F454ED6895B0861083DD1371999B2F559A3A487CF59A074FB49215EA6A6BE656F9AF17B121A2447CB7985590E9738842B899BA57AC311810AE2D9794F37483DD6BCCB64AF6D56588AE94665961C025C3AA2861974C236BCA4BD8FF5509F7AB774593E7C5549E57C2F18D15C0515094AD9A0DFAA0601E524F8231156B627BB25A0DAE04DACD0A66C041CEF400583FC13BAE640291A39A5C5CA8BCA1AD5C683CDD8290891A76940817DD8C9F52678780548E37A05806600801426DBB950C3B2BA34E24CC77864DD91B39F1408C716A69DF63342854E50A245FB50977B9410DED2C93F86B1F9D5A78B87BF81E51CA620A7E8566B19AB700964A40E3266415228D432156E5CBDF52364A90483A55C39B3FB16FC7465A3F8AC801B70B9FB28B583444BA5C1A73722D417A9D6D9B7DEB08BC6B330FF27CF61AB8831E27758C64AF3B12150CB7B33ABC29858106D63686D8762459ABF9413850AE53ED6313F76F83D0FB8AB34374E7DF693E4A1B3E5A8AD0CE820AFE1CF401ACDE650A8101B0946022D52178E19613C42B88B07CC04EAA81DFB28AC9DC076236B67219A30F8F945DD57BD2F335C52D59372308D38993467DB53DA3382B74867B616481BD0091A2232C1116DC88A589DB9107224A681008C67C589186A6929549BEEF92253DB02B0C8AA9F9C875A670266C72BCBDB4F5625043703C1A0457395832E4C335180462ED2220C59E7361903C107D85457F6CD82EB820D0855D97675C2E0151CDB73C2885DDB7849D74541580124E890116A65BC068093B57914E20C937C60A3EB25576F1A976A9583839B672144CD4A45C3477A45C29B4E0BC2BDBD206585C9B7A7741C8B6B5793A92797A15AE7A5B73A74B2971463634BA52AA792AF05530730B6D0A89A346156B733677932BD36593A7496130CC458DCC5CA987C21960604EC8A8C5396056680CBF3F1AAC4F401AA5029FB2150434BB4706C31A2D54E4297939FA7C9C6F85700613CEB65C7F03AC56EB86E2D27CCC6DCB7B9394DCDB942FF222D86958A996C0CB6A8A44F97A70441C95FA71250116EEC20863C0B5A643458788AB001F8869D909922F51EE547A1E889255B3A0599C65842E5AB8D73872F053BC62392EA53896D328102D460BF1609583C22C3B43780EC6DAD0319EB4A5A65B4756C3CB40EAA935183BF8BD46ABE76BA46E199103A5313C3235F49C915E097BCA804DE680781D8365731BEAC6789A9203FB8787C4C070E00A13A6722A66A28236DB179825653D33CCF898B72C6B8450D97AFD3276BB13340519CBEDA708D12A858F54C49F4547195B7788A9150B2649E36AA394121926D568A488B16D3557B2A32AF57D11FC3373F80A28C0723273D362502E7C428AB44D3CBABF9EA585FD1BD0C9846556A1E196B78CF951592984A0A8487A78C2317D7ACA4118E1049750A0788F0D66AD9E48E34731130ABA0B427360A856D96D80B3F028FDD3ABA9035C10106BA1C0934BED36C6D7C7434249654EA89FC22137F4AB903653B75FB25B6F01635E6CC7D39CF1508690562826B49B6FFC59E0DD35022E541F8BA0D304AA5B4E20606907C424395666C54ABC2B8FB009847C86317685000C231215C8C15945860F6A85DDB98A8C3A527F2749D3C027E694E8F0B0F0FA454913AADB635AADD452F7128BF7752569669A8B93290EB92E78F6ADFF23E89F57F3890753B51F12F3F3A8A654E677847").try_into().unwrap(); + ek[0] = 0xFF; + ek[1] |= 0x0F; + let m = [0u8; 32]; + assert!( + matches!(mlkem1024_encaps(&ek, &m), Err(Error::InvalidEncapsulationKey)), + "비정규 ek 미거부" + ); +} diff --git a/sha3/src/keccak.rs b/sha3/src/keccak.rs index 9a8ff37..a768791 100644 --- a/sha3/src/keccak.rs +++ b/sha3/src/keccak.rs @@ -249,4 +249,20 @@ impl KeccakState { self.squeeze_into(out); // self 드롭 시 KeccakState 의 Secret 필드가 자동 소거 } + + pub(crate) fn into_xof_reader(mut self) -> Self { + self.pad(); + self + } + + pub(crate) fn squeeze_block(&mut self, out: &mut [u8]) { + let rate_words = self.rate_bytes / 8; + let mut word_bytes = [0u8; 8]; + for word_idx in 0..rate_words { + word_bytes = self.state[word_idx].to_le_bytes(); + out[word_idx * 8..word_idx * 8 + 8].copy_from_slice(&word_bytes); + } + word_bytes.zeroize(); + Self::keccak_f1600(&mut self.state); + } } diff --git a/sha3/src/lib.rs b/sha3/src/lib.rs index dd96fc7..f8e37cc 100644 --- a/sha3/src/lib.rs +++ b/sha3/src/lib.rs @@ -179,3 +179,22 @@ impl XOF for SHAKE256 { self.0.finalize_xof(out); } } + +// SHAKE128 증분 XOF 리더 (rate = 168 바이트 = MAX_RATE_BYTES) +// 무한 거부 샘플링(FIPS 203 SampleNTT)에서 필요한 만큼 블록 단위로 스퀴즈 +pub struct Shake128Reader(KeccakState); + +impl SHAKE128 { + #[inline] + #[must_use] + pub fn finalize_xof_reader(self) -> Shake128Reader { + Shake128Reader(self.0.into_xof_reader()) + } +} + +impl Shake128Reader { + #[inline] + pub fn squeeze_block(&mut self, out: &mut [u8; MAX_RATE_BYTES]) { + self.0.squeeze_block(out); + } +} From 0325c44b3432af2dee52ff79c1a27840a67bd368 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:50:20 +0900 Subject: [PATCH 30/65] =?UTF-8?q?cross-confirm=20ML-KEM=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=99=84=EB=A3=8C=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cross-confirm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cross-confirm.md b/cross-confirm.md index 8c69eae..2dcc57a 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -15,7 +15,7 @@ - [X] X448 - [X] X25519 - [X] ML-DSA -- [ ] ML-KEM +- [X] ML-KEM - [X] RNG - [X] Constant-Time - [X] Zeroize From 98f74f05025d53d14d871a45e7cba0edd98c58c1 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:12:34 +0900 Subject: [PATCH 31/65] =?UTF-8?q?chacha20=20RFC=208439=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=ED=9B=84=EC=86=8D=20=EB=B3=B4=EC=95=88=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=EB=B8=94=EB=A1=9D=20=EC=B9=B4=EC=9A=B4?= =?UTF-8?q?=ED=84=B0=20u64=20=EC=86=8C=EC=A7=84=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EB=B0=8F=20AEAD=20=EC=9E=85=EB=A0=A5=20=EA=B8=B8=EC=9D=B4=20?= =?UTF-8?q?=ED=95=9C=EA=B3=84=EB=A1=9C=20=ED=82=A4=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A6=BC=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20=EC=B0=A8=EB=8B=A8=20?= =?UTF-8?q?-=20generate=5Fpoly1305=5Fkey=20=EB=B8=94=EB=A1=9D=20=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=84=B0=200=20=EA=B0=95=EC=A0=9C=20-=20decrypt=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20=ED=83=9C=EA=B7=B8=EC=99=80=20Poly1305=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EB=B9=84=EB=B0=80=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EA=B0=92=20=EC=A0=84=EC=88=98=20=EC=86=8C=EA=B1=B0=20-=20final?= =?UTF-8?q?ize=20=EC=B5=9C=EC=A2=85=20=EA=B0=90=EC=82=B0=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20CtSelOps=20=EC=83=81=EC=88=98=EC=8B=9C=EA=B0=84?= =?UTF-8?q?=ED=99=94=20-=20RFC=208439=20Appendix=20A=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20KAT=20=EB=B0=8F=20=EB=B6=84=ED=95=A0=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20cross-co?= =?UTF-8?q?nfirm=20ChaCha20=20=EA=B2=80=EC=A6=9D=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chacha20/src/chacha20.rs | 26 +++- chacha20/src/lib.rs | 34 ++++- chacha20/src/poly1305.rs | 194 ++++++++++++++++--------- chacha20/tests/kat.rs | 300 +++++++++++++++++++++++++++++++++++++++ cross-confirm.md | 2 +- 5 files changed, 480 insertions(+), 76 deletions(-) create mode 100644 chacha20/tests/kat.rs diff --git a/chacha20/src/chacha20.rs b/chacha20/src/chacha20.rs index 144e505..fd55567 100644 --- a/chacha20/src/chacha20.rs +++ b/chacha20/src/chacha20.rs @@ -86,10 +86,11 @@ fn chacha20_block(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u8; 64] { pub struct ChaCha20 { key: Secret<[u8; 32]>, nonce: [u8; 12], - counter: u32, + counter: u64, } impl ChaCha20 { + #[must_use] pub fn new(key: &[u8; 32], nonce: &[u8; 12]) -> Self { Self { key: Secret::new(*key), @@ -98,21 +99,33 @@ impl ChaCha20 { } } + #[must_use] pub fn new_with_counter(key: &[u8; 32], nonce: &[u8; 12], counter: u32) -> Self { Self { key: Secret::new(*key), nonce: *nonce, - counter, + counter: u64::from(counter), } } + #[must_use] pub fn keystream_block(&mut self) -> [u8; 64] { - let block = chacha20_block(self.key.expose(), self.counter, &self.nonce); - self.counter = self.counter.wrapping_add(1); + assert!( + self.counter <= u64::from(u32::MAX), + "RFC 8439 32비트 블록 카운터 소진으로 키스트림 재사용 발생" + ); + let block = chacha20_block(self.key.expose(), self.counter as u32, &self.nonce); + self.counter += 1; block } pub fn apply_keystream(&mut self, data: &mut [u8]) { + let blocks = data.len().div_ceil(64) as u64; + assert!( + blocks <= (1u64 << 32) - self.counter, + "RFC 8439 32비트 블록 카운터 소진으로 키스트림 재사용 발생" + ); + let mut offset = 0; while offset < data.len() { let mut keystream = self.keystream_block(); @@ -128,7 +141,12 @@ impl ChaCha20 { } } + #[must_use] pub fn generate_poly1305_key(&mut self) -> [u8; 32] { + assert!( + self.counter == 0, + "RFC 8439 Poly1305 키 생성은 블록 카운터 0 에서만 허용" + ); let mut block = chacha20_block(self.key.expose(), 0, &self.nonce); let mut poly_key = [0u8; 32]; poly_key.copy_from_slice(&block[..32]); diff --git a/chacha20/src/lib.rs b/chacha20/src/lib.rs index 50fbc66..b1f9178 100644 --- a/chacha20/src/lib.rs +++ b/chacha20/src/lib.rs @@ -12,6 +12,8 @@ use zeroize::{Secret, Zeroize}; pub use chacha20::ChaCha20 as ChaCha20Core; pub use poly1305::Poly1305 as Poly1305Core; +const CHACHA20_MAX_INPUT_LEN: u64 = (u32::MAX as u64) * 64; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Error { InvalidKeyLength, @@ -26,6 +28,7 @@ pub struct ChaCha20Poly1305 { } impl ChaCha20Poly1305 { + #[must_use] pub fn new(key: &[u8; 32]) -> Self { Self { key: Secret::new(*key), @@ -43,6 +46,10 @@ impl ChaCha20Poly1305 { if ciphertext.len() < plaintext.len() { return Err(Error::BufferTooSmall); } + assert!( + plaintext.len() as u64 <= CHACHA20_MAX_INPUT_LEN, + "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" + ); let mut chacha = ChaCha20::new(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); @@ -89,6 +96,10 @@ impl ChaCha20Poly1305 { if plaintext.len() < ciphertext.len() { return Err(Error::BufferTooSmall); } + assert!( + ciphertext.len() as u64 <= CHACHA20_MAX_INPUT_LEN, + "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" + ); let mut chacha = ChaCha20::new(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); @@ -112,10 +123,13 @@ impl ChaCha20Poly1305 { lengths[8..16].copy_from_slice(&(ciphertext.len() as u64).to_le_bytes()); poly.update(&lengths); - let computed_tag = poly.finalize(); + let mut computed_tag = poly.finalize(); + let tag_ok = poly1305_verify(&computed_tag, tag); + computed_tag.zeroize(); - if !poly1305_verify(&computed_tag, tag) { + if !tag_ok { poly_key.zeroize(); + lengths.zeroize(); return Err(Error::AuthenticationFailed); } @@ -138,6 +152,10 @@ impl ChaCha20Poly1305 { if buffer.len() < plaintext_len { return Err(Error::BufferTooSmall); } + assert!( + plaintext_len as u64 <= CHACHA20_MAX_INPUT_LEN, + "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" + ); let mut chacha = ChaCha20::new(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); @@ -182,6 +200,10 @@ impl ChaCha20Poly1305 { if buffer.len() < ciphertext_len { return Err(Error::BufferTooSmall); } + assert!( + ciphertext_len as u64 <= CHACHA20_MAX_INPUT_LEN, + "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" + ); let mut chacha = ChaCha20::new(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); @@ -205,10 +227,13 @@ impl ChaCha20Poly1305 { lengths[8..16].copy_from_slice(&(ciphertext_len as u64).to_le_bytes()); poly.update(&lengths); - let computed_tag = poly.finalize(); + let mut computed_tag = poly.finalize(); + let tag_ok = poly1305_verify(&computed_tag, tag); + computed_tag.zeroize(); - if !poly1305_verify(&computed_tag, tag) { + if !tag_ok { poly_key.zeroize(); + lengths.zeroize(); return Err(Error::AuthenticationFailed); } @@ -221,6 +246,7 @@ impl ChaCha20Poly1305 { } } +#[must_use] pub fn verify_tag(computed: &[u8; 16], expected: &[u8; 16]) -> bool { let mut eq = Choice::from_u8(1); for i in 0..16 { diff --git a/chacha20/src/poly1305.rs b/chacha20/src/poly1305.rs index cdff384..5394dca 100644 --- a/chacha20/src/poly1305.rs +++ b/chacha20/src/poly1305.rs @@ -1,4 +1,4 @@ -use constant_time::{Choice, CtEqOps}; +use constant_time::{Choice, CtEqOps, CtSelOps}; use zeroize::Zeroize; pub struct Poly1305 { @@ -10,6 +10,7 @@ pub struct Poly1305 { } impl Poly1305 { + #[must_use] pub fn new(key: &[u8; 32]) -> Self { let mut clamped = [0u8; 16]; clamped.copy_from_slice(&key[..16]); @@ -22,85 +23,101 @@ impl Poly1305 { clamped[8] &= 0xfc; clamped[12] &= 0xfc; - let t0 = u32::from_le_bytes([clamped[0], clamped[1], clamped[2], clamped[3]]); - let t1 = u32::from_le_bytes([clamped[4], clamped[5], clamped[6], clamped[7]]); - let t2 = u32::from_le_bytes([clamped[8], clamped[9], clamped[10], clamped[11]]); - let t3 = u32::from_le_bytes([clamped[12], clamped[13], clamped[14], clamped[15]]); + let mut t0 = u32::from_le_bytes([clamped[0], clamped[1], clamped[2], clamped[3]]); + let mut t1 = u32::from_le_bytes([clamped[4], clamped[5], clamped[6], clamped[7]]); + let mut t2 = u32::from_le_bytes([clamped[8], clamped[9], clamped[10], clamped[11]]); + let mut t3 = u32::from_le_bytes([clamped[12], clamped[13], clamped[14], clamped[15]]); - let r0 = t0 & 0x03ff_ffff; - let r1 = ((t0 >> 26) | (t1 << 6)) & 0x03ff_ffff; - let r2 = ((t1 >> 20) | (t2 << 12)) & 0x03ff_ffff; - let r3 = ((t2 >> 14) | (t3 << 18)) & 0x03ff_ffff; - let r4 = (t3 >> 8) & 0x03ff_ffff; + let mut r0 = t0 & 0x03ff_ffff; + let mut r1 = ((t0 >> 26) | (t1 << 6)) & 0x03ff_ffff; + let mut r2 = ((t1 >> 20) | (t2 << 12)) & 0x03ff_ffff; + let mut r3 = ((t2 >> 14) | (t3 << 18)) & 0x03ff_ffff; + let mut r4 = (t3 >> 8) & 0x03ff_ffff; clamped.zeroize(); - let pad0 = u32::from_le_bytes([key[16], key[17], key[18], key[19]]); - let pad1 = u32::from_le_bytes([key[20], key[21], key[22], key[23]]); - let pad2 = u32::from_le_bytes([key[24], key[25], key[26], key[27]]); - let pad3 = u32::from_le_bytes([key[28], key[29], key[30], key[31]]); + let mut pad0 = u32::from_le_bytes([key[16], key[17], key[18], key[19]]); + let mut pad1 = u32::from_le_bytes([key[20], key[21], key[22], key[23]]); + let mut pad2 = u32::from_le_bytes([key[24], key[25], key[26], key[27]]); + let mut pad3 = u32::from_le_bytes([key[28], key[29], key[30], key[31]]); - Self { + let state = Self { r: [r0, r1, r2, r3, r4], h: [0, 0, 0, 0, 0], pad: [pad0, pad1, pad2, pad3], buffer: [0u8; 16], buffer_len: 0, - } + }; + + t0.zeroize(); + t1.zeroize(); + t2.zeroize(); + t3.zeroize(); + r0.zeroize(); + r1.zeroize(); + r2.zeroize(); + r3.zeroize(); + r4.zeroize(); + pad0.zeroize(); + pad1.zeroize(); + pad2.zeroize(); + pad3.zeroize(); + + state } fn block(&mut self, m: &[u8], hibit: u32) { - let s0 = u32::from_le_bytes([m[0], m[1], m[2], m[3]]); - let s1 = u32::from_le_bytes([m[4], m[5], m[6], m[7]]); - let s2 = u32::from_le_bytes([m[8], m[9], m[10], m[11]]); - let s3 = u32::from_le_bytes([m[12], m[13], m[14], m[15]]); - - let h0 = self.h[0].wrapping_add(s0 & 0x03ff_ffff); - let h1 = self.h[1].wrapping_add(((s0 >> 26) | (s1 << 6)) & 0x03ff_ffff); - let h2 = self.h[2].wrapping_add(((s1 >> 20) | (s2 << 12)) & 0x03ff_ffff); - let h3 = self.h[3].wrapping_add(((s2 >> 14) | (s3 << 18)) & 0x03ff_ffff); - let h4 = self.h[4].wrapping_add((s3 >> 8) | hibit); - - let r0 = self.r[0]; - let r1 = self.r[1]; - let r2 = self.r[2]; - let r3 = self.r[3]; - let r4 = self.r[4]; - - let s1 = r1.wrapping_mul(5); - let s2 = r2.wrapping_mul(5); - let s3 = r3.wrapping_mul(5); - let s4 = r4.wrapping_mul(5); - - let d0 = (h0 as u64) + let mut m0 = u32::from_le_bytes([m[0], m[1], m[2], m[3]]); + let mut m1 = u32::from_le_bytes([m[4], m[5], m[6], m[7]]); + let mut m2 = u32::from_le_bytes([m[8], m[9], m[10], m[11]]); + let mut m3 = u32::from_le_bytes([m[12], m[13], m[14], m[15]]); + + let mut h0 = self.h[0].wrapping_add(m0 & 0x03ff_ffff); + let mut h1 = self.h[1].wrapping_add(((m0 >> 26) | (m1 << 6)) & 0x03ff_ffff); + let mut h2 = self.h[2].wrapping_add(((m1 >> 20) | (m2 << 12)) & 0x03ff_ffff); + let mut h3 = self.h[3].wrapping_add(((m2 >> 14) | (m3 << 18)) & 0x03ff_ffff); + let mut h4 = self.h[4].wrapping_add((m3 >> 8) | hibit); + + let mut r0 = self.r[0]; + let mut r1 = self.r[1]; + let mut r2 = self.r[2]; + let mut r3 = self.r[3]; + let mut r4 = self.r[4]; + + let mut s1 = r1.wrapping_mul(5); + let mut s2 = r2.wrapping_mul(5); + let mut s3 = r3.wrapping_mul(5); + let mut s4 = r4.wrapping_mul(5); + + let mut d0 = (h0 as u64) .wrapping_mul(r0 as u64) .wrapping_add((h1 as u64).wrapping_mul(s4 as u64)) .wrapping_add((h2 as u64).wrapping_mul(s3 as u64)) .wrapping_add((h3 as u64).wrapping_mul(s2 as u64)) .wrapping_add((h4 as u64).wrapping_mul(s1 as u64)); - let d1 = (h0 as u64) + let mut d1 = (h0 as u64) .wrapping_mul(r1 as u64) .wrapping_add((h1 as u64).wrapping_mul(r0 as u64)) .wrapping_add((h2 as u64).wrapping_mul(s4 as u64)) .wrapping_add((h3 as u64).wrapping_mul(s3 as u64)) .wrapping_add((h4 as u64).wrapping_mul(s2 as u64)); - let d2 = (h0 as u64) + let mut d2 = (h0 as u64) .wrapping_mul(r2 as u64) .wrapping_add((h1 as u64).wrapping_mul(r1 as u64)) .wrapping_add((h2 as u64).wrapping_mul(r0 as u64)) .wrapping_add((h3 as u64).wrapping_mul(s4 as u64)) .wrapping_add((h4 as u64).wrapping_mul(s3 as u64)); - let d3 = (h0 as u64) + let mut d3 = (h0 as u64) .wrapping_mul(r3 as u64) .wrapping_add((h1 as u64).wrapping_mul(r2 as u64)) .wrapping_add((h2 as u64).wrapping_mul(r1 as u64)) .wrapping_add((h3 as u64).wrapping_mul(r0 as u64)) .wrapping_add((h4 as u64).wrapping_mul(s4 as u64)); - let d4 = (h0 as u64) + let mut d4 = (h0 as u64) .wrapping_mul(r4 as u64) .wrapping_add((h1 as u64).wrapping_mul(r3 as u64)) .wrapping_add((h2 as u64).wrapping_mul(r2 as u64)) @@ -108,23 +125,23 @@ impl Poly1305 { .wrapping_add((h4 as u64).wrapping_mul(r0 as u64)); let mut c: u32; - let mut h0 = d0 as u32 & 0x03ff_ffff; + h0 = d0 as u32 & 0x03ff_ffff; c = (d0 >> 26) as u32; - let d1 = d1.wrapping_add(c as u64); - let mut h1 = d1 as u32 & 0x03ff_ffff; + d1 = d1.wrapping_add(c as u64); + h1 = d1 as u32 & 0x03ff_ffff; c = (d1 >> 26) as u32; - let d2 = d2.wrapping_add(c as u64); - let h2 = d2 as u32 & 0x03ff_ffff; + d2 = d2.wrapping_add(c as u64); + h2 = d2 as u32 & 0x03ff_ffff; c = (d2 >> 26) as u32; - let d3 = d3.wrapping_add(c as u64); - let h3 = d3 as u32 & 0x03ff_ffff; + d3 = d3.wrapping_add(c as u64); + h3 = d3 as u32 & 0x03ff_ffff; c = (d3 >> 26) as u32; - let d4 = d4.wrapping_add(c as u64); - let h4 = d4 as u32 & 0x03ff_ffff; + d4 = d4.wrapping_add(c as u64); + h4 = d4 as u32 & 0x03ff_ffff; c = (d4 >> 26) as u32; h0 = h0.wrapping_add(c.wrapping_mul(5)); @@ -137,6 +154,31 @@ impl Poly1305 { self.h[2] = h2; self.h[3] = h3; self.h[4] = h4; + + m0.zeroize(); + m1.zeroize(); + m2.zeroize(); + m3.zeroize(); + h0.zeroize(); + h1.zeroize(); + h2.zeroize(); + h3.zeroize(); + h4.zeroize(); + r0.zeroize(); + r1.zeroize(); + r2.zeroize(); + r3.zeroize(); + r4.zeroize(); + s1.zeroize(); + s2.zeroize(); + s3.zeroize(); + s4.zeroize(); + d0.zeroize(); + d1.zeroize(); + d2.zeroize(); + d3.zeroize(); + d4.zeroize(); + c.zeroize(); } pub fn update(&mut self, data: &[u8]) { @@ -150,8 +192,9 @@ impl Poly1305 { offset = have; if self.buffer_len == 16 { - let buf = self.buffer; + let mut buf = self.buffer; self.block(&buf, 1 << 24); + buf.zeroize(); self.buffer_len = 0; } } @@ -168,14 +211,16 @@ impl Poly1305 { } } + #[must_use] pub fn finalize(mut self) -> [u8; 16] { if self.buffer_len > 0 { self.buffer[self.buffer_len] = 0x01; for i in (self.buffer_len + 1)..16 { self.buffer[i] = 0; } - let buf = self.buffer; + let mut buf = self.buffer; self.block(&buf, 0); + buf.zeroize(); } let mut h0 = self.h[0]; @@ -212,21 +257,19 @@ impl Poly1305 { let mut g3 = h3.wrapping_add(c); c = g3 >> 26; g3 &= 0x03ff_ffff; - let g4 = h4.wrapping_add(c).wrapping_sub(1 << 26); + let mut g4 = h4.wrapping_add(c).wrapping_sub(1 << 26); - let mask = (g4 >> 31).wrapping_sub(1); - let nmask = !mask; + let ge_p = Choice::from_u8(((g4 >> 31) ^ 1) as u8); + h0 = CtSelOps::select(&h0, &g0, ge_p); + h1 = CtSelOps::select(&h1, &g1, ge_p); + h2 = CtSelOps::select(&h2, &g2, ge_p); + h3 = CtSelOps::select(&h3, &g3, ge_p); + h4 = CtSelOps::select(&h4, &g4, ge_p); - h0 = (h0 & nmask) | (g0 & mask); - h1 = (h1 & nmask) | (g1 & mask); - h2 = (h2 & nmask) | (g2 & mask); - h3 = (h3 & nmask) | (g3 & mask); - h4 = (h4 & nmask) | (g4 & mask); - - let f0 = (h0 | (h1 << 26)) as u64 + self.pad[0] as u64; - let f1 = ((h1 >> 6) | (h2 << 20)) as u64 + self.pad[1] as u64 + (f0 >> 32); - let f2 = ((h2 >> 12) | (h3 << 14)) as u64 + self.pad[2] as u64 + (f1 >> 32); - let f3 = ((h3 >> 18) | (h4 << 8)) as u64 + self.pad[3] as u64 + (f2 >> 32); + let mut f0 = (h0 | (h1 << 26)) as u64 + self.pad[0] as u64; + let mut f1 = ((h1 >> 6) | (h2 << 20)) as u64 + self.pad[1] as u64 + (f0 >> 32); + let mut f2 = ((h2 >> 12) | (h3 << 14)) as u64 + self.pad[2] as u64 + (f1 >> 32); + let mut f3 = ((h3 >> 18) | (h4 << 8)) as u64 + self.pad[3] as u64 + (f2 >> 32); let mut tag = [0u8; 16]; tag[0..4].copy_from_slice(&(f0 as u32).to_le_bytes()); @@ -239,6 +282,22 @@ impl Poly1305 { self.pad.zeroize(); self.buffer.zeroize(); + h0.zeroize(); + h1.zeroize(); + h2.zeroize(); + h3.zeroize(); + h4.zeroize(); + g0.zeroize(); + g1.zeroize(); + g2.zeroize(); + g3.zeroize(); + g4.zeroize(); + c.zeroize(); + f0.zeroize(); + f1.zeroize(); + f2.zeroize(); + f3.zeroize(); + tag } } @@ -253,6 +312,7 @@ impl Drop for Poly1305 { } } +#[must_use] pub fn poly1305_verify(tag1: &[u8; 16], tag2: &[u8; 16]) -> bool { let mut eq = Choice::from_u8(1); for i in 0..16 { diff --git a/chacha20/tests/kat.rs b/chacha20/tests/kat.rs new file mode 100644 index 0000000..70afad9 --- /dev/null +++ b/chacha20/tests/kat.rs @@ -0,0 +1,300 @@ +//! RFC 8439 공식 KAT 검증 테스트입니다. +//! +//! 정답 벡터 출처는 RFC 8439 Appendix A (A.1 블록 함수, A.2 암호화, +//! A.3 Poly1305, A.4 Poly1305 키 생성, A.5 AEAD 복호화)이며, +//! 벡터 값을 소스에 상수로 고정하여 실행 시 외부 의존성이 없습니다. +//! +//! # Features +//! - A.1: 블록 카운터 0/1/2, 최상위 키 비트, nonce 경계 케이스 키스트림 검증 +//! - A.2: 부분 최종 블록 포함 keystream 적용 검증 +//! - A.3: 부분 감산 미완결, s 덧셈 오버플로, 131비트 중간값 등 캐리 엣지 검증 +//! - A.4: counter 0 블록 기반 Poly1305 일회용 키 유도 검증 +//! - A.5: AEAD 태그 검증 및 복호화, 변조 태그 거부, 재암호화 일치 검증 + +use chacha20::{ChaCha20Core, ChaCha20Poly1305, Error, Poly1305Core}; + +fn unhex(s: &str) -> Vec { + let b = s.as_bytes(); + assert!(b.len().is_multiple_of(2), "홀수 길이 hex"); + let val = |c: u8| -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + b'A'..=b'F' => c - b'A' + 10, + _ => panic!("잘못된 hex 문자"), + } + }; + let mut v = Vec::with_capacity(b.len() / 2); + let mut i = 0; + while i < b.len() { + v.push((val(b[i]) << 4) | val(b[i + 1])); + i += 2; + } + v +} + +#[test] +fn kat_a1_chacha20_block() { + // (key, nonce, counter, keystream) + let cases: &[(&str, &str, u32, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000", + 0, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000", + 1, + "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000001", + "000000000000000000000000", + 1, + "3aeb5224ecf849929b9d828db1ced4dd832025e8018b8160b82284f3c949aa5a8eca00bbb4a73bdad192b5c42f73f2fd4e273644c8b36125a64addeb006c13a0", + ), + ( + "00ff000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000", + 2, + "72d54dfbf12ec44b362692df94137f328fea8da73990265ec1bbbea1ae9af0ca13b25aa26cb4a648cb9b9d1be65b2c0924a66c54d545ec1b7374f4872e99f096", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000002", + 0, + "c2c64d378cd536374ae204b9ef933fcd1a8b2288b3dfa49672ab765b54ee27c78a970e0e955c14f3a88e741b97c286f75f8fc299e8148362fa198a39531bed6d", + ), + ]; + for (key, nonce, counter, keystream) in cases { + let key: [u8; 32] = unhex(key).try_into().unwrap(); + let nonce: [u8; 12] = unhex(nonce).try_into().unwrap(); + let mut chacha = ChaCha20Core::new_with_counter(&key, &nonce, *counter); + let block = chacha.keystream_block(); + assert_eq!(&block[..], &unhex(keystream)[..], "키스트림 불일치"); + } +} + +#[test] +fn kat_a2_chacha20_encrypt() { + // (key, nonce, counter, plaintext, ciphertext) + let cases: &[(&str, &str, u32, &str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000", + 0, + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000001", + "000000000000000000000002", + 1, + "416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f", + "a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221", + ), + ( + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "000000000000000000000002", + 42, + "2754776173206272696c6c69672c20616e642074686520736c6974687920746f7665730a446964206779726520616e642067696d626c6520696e2074686520776162653a0a416c6c206d696d737920776572652074686520626f726f676f7665732c0a416e6420746865206d6f6d65207261746873206f757467726162652e", + "62e6347f95ed87a45ffae7426f27a1df5fb69110044c0d73118effa95b01e5cf166d3df2d721caf9b21e5fb14c616871fd84c54f9d65b283196c7fe4f60553ebf39c6402c42234e32a356b3e764312a61a5532055716ead6962568f87d3f3f7704c6a8d1bcd1bf4d50d6154b6da731b187b58dfd728afa36757a797ac188d1", + ), + ]; + for (key, nonce, counter, plaintext, ciphertext) in cases { + let key: [u8; 32] = unhex(key).try_into().unwrap(); + let nonce: [u8; 12] = unhex(nonce).try_into().unwrap(); + let mut buf = unhex(plaintext); + let mut chacha = ChaCha20Core::new_with_counter(&key, &nonce, *counter); + chacha.apply_keystream(&mut buf); + assert_eq!(&buf[..], &unhex(ciphertext)[..], "암호문 불일치"); + } +} + +#[test] +fn kat_a3_poly1305() { + // (otk, text, tag) + let cases: &[(&str, &str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000", + ), + ( + "0000000000000000000000000000000036e5f6b5c5e06070f0efca96227a863e", + "416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f", + "36e5f6b5c5e06070f0efca96227a863e", + ), + ( + "36e5f6b5c5e06070f0efca96227a863e00000000000000000000000000000000", + "416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f", + "f3477e7cd95417af89a6b8794c310cf0", + ), + ( + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "2754776173206272696c6c69672c20616e642074686520736c6974687920746f7665730a446964206779726520616e642067696d626c6520696e2074686520776162653a0a416c6c206d696d737920776572652074686520626f726f676f7665732c0a416e6420746865206d6f6d65207261746873206f757467726162652e", + "4541669a7eaaee61e708dc7cbcc5eb62", + ), + ( + "0200000000000000000000000000000000000000000000000000000000000000", + "ffffffffffffffffffffffffffffffff", + "03000000000000000000000000000000", + ), + ( + "02000000000000000000000000000000ffffffffffffffffffffffffffffffff", + "02000000000000000000000000000000", + "03000000000000000000000000000000", + ), + ( + "0100000000000000000000000000000000000000000000000000000000000000", + "fffffffffffffffffffffffffffffffff0ffffffffffffffffffffffffffffff11000000000000000000000000000000", + "05000000000000000000000000000000", + ), + ( + "0100000000000000000000000000000000000000000000000000000000000000", + "fffffffffffffffffffffffffffffffffbfefefefefefefefefefefefefefefe01010101010101010101010101010101", + "00000000000000000000000000000000", + ), + ( + "0200000000000000000000000000000000000000000000000000000000000000", + "fdffffffffffffffffffffffffffffff", + "faffffffffffffffffffffffffffffff", + ), + ( + "0100000000000000040000000000000000000000000000000000000000000000", + "e33594d7505e43b900000000000000003394d7505e4379cd01000000000000000000000000000000000000000000000001000000000000000000000000000000", + "14000000000000005500000000000000", + ), + ( + "0100000000000000040000000000000000000000000000000000000000000000", + "e33594d7505e43b900000000000000003394d7505e4379cd010000000000000000000000000000000000000000000000", + "13000000000000000000000000000000", + ), + ]; + for (otk, text, tag) in cases { + let otk: [u8; 32] = unhex(otk).try_into().unwrap(); + let mut poly = Poly1305Core::new(&otk); + poly.update(&unhex(text)); + let computed = poly.finalize(); + assert_eq!(&computed[..], &unhex(tag)[..], "태그 불일치"); + } +} + +#[test] +fn kat_a4_poly1305_keygen() { + // (key, nonce, otk) + let cases: &[(&str, &str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000", + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000001", + "000000000000000000000002", + "ecfa254f845f647473d3cb140da9e87606cb33066c447b87bc2666dde3fbb739", + ), + ( + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "000000000000000000000002", + "965e3bc6f9ec7ed9560808f4d229f94b137ff275ca9b3fcbdd59deaad23310ae", + ), + ]; + for (key, nonce, otk) in cases { + let key: [u8; 32] = unhex(key).try_into().unwrap(); + let nonce: [u8; 12] = unhex(nonce).try_into().unwrap(); + let mut chacha = ChaCha20Core::new(&key, &nonce); + let poly_key = chacha.generate_poly1305_key(); + assert_eq!(&poly_key[..], &unhex(otk)[..], "Poly1305 일회용 키 불일치"); + } +} + +#[test] +fn kat_a5_aead_decrypt() { + let key: [u8; 32] = unhex("1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0") + .try_into() + .unwrap(); + let nonce: [u8; 12] = unhex("000000000102030405060708").try_into().unwrap(); + let aad = unhex("f33388860000000000004e91"); + let ciphertext = unhex( + "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb24c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c8559797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523eaf4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a1049e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29a6ad5cb4022b02709b", + ); + let tag: [u8; 16] = unhex("eead9d67890cbb22392336fea1851f38") + .try_into() + .unwrap(); + let expected_plaintext = unhex( + "496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d617920626520757064617465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d656e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f2075736520496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f7220746f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67726573732e2fe2809d", + ); + + let aead = ChaCha20Poly1305::new(&key); + + let mut plaintext = vec![0u8; ciphertext.len()]; + aead.decrypt(&nonce, &aad, &ciphertext, &tag, &mut plaintext) + .unwrap(); + assert_eq!(&plaintext[..], &expected_plaintext[..], "평문 불일치"); + + let mut ct2 = vec![0u8; expected_plaintext.len()]; + let mut tag2 = [0u8; 16]; + aead.encrypt(&nonce, &aad, &expected_plaintext, &mut ct2, &mut tag2) + .unwrap(); + assert_eq!(&ct2[..], &ciphertext[..], "재암호화 암호문 불일치"); + assert_eq!(tag2, tag, "재암호화 태그 불일치"); + + let mut bad_tag = tag; + bad_tag[0] ^= 0x01; + let mut pt2 = vec![0u8; ciphertext.len()]; + assert_eq!( + aead.decrypt(&nonce, &aad, &ciphertext, &bad_tag, &mut pt2), + Err(Error::AuthenticationFailed), + "변조 태그 미검출" + ); +} + +/// Poly1305 update 의 내부 버퍼링(부분 블록 채움 후 flush) 경로 검증. +#[test] +fn kat_a3_poly1305_chunked_update() { + let otk: [u8; 32] = unhex("0000000000000000000000000000000036e5f6b5c5e06070f0efca96227a863e") + .try_into() + .unwrap(); + let text = unhex( + "416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f", + ); + let expected_tag = unhex("36e5f6b5c5e06070f0efca96227a863e"); + + let mut poly = Poly1305Core::new(&otk); + let mut offset = 0; + for chunk in [1usize, 15, 17, 100, 3, 64] { + poly.update(&text[offset..offset + chunk]); + offset += chunk; + } + poly.update(&text[offset..]); + let computed = poly.finalize(); + assert_eq!(&computed[..], &expected_tag[..], "분할 갱신 태그 불일치"); +} + +/// apply_keystream 을 64바이트 배수 경계로 분할 호출해도 카운터 연속성이 유지되는지 검증. +#[test] +fn kat_a2_chacha20_chunked_keystream() { + let key: [u8; 32] = unhex("0000000000000000000000000000000000000000000000000000000000000001") + .try_into() + .unwrap(); + let nonce: [u8; 12] = unhex("000000000000000000000002").try_into().unwrap(); + let mut buf = unhex( + "416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f", + ); + + let mut chacha = ChaCha20Core::new_with_counter(&key, &nonce, 1); + let (head, tail) = buf.split_at_mut(320); + chacha.apply_keystream(&mut head[..64]); + chacha.apply_keystream(&mut head[64..]); + chacha.apply_keystream(tail); + assert_eq!( + &buf[..], + &unhex( + "a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221" + )[..], + "분할 적용 암호문 불일치" + ); +} diff --git a/cross-confirm.md b/cross-confirm.md index 2dcc57a..6bc57bb 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -9,7 +9,7 @@ - [X] BLAKE - [X] SHA2 - [X] SHA3 -- [ ] ChaCha20 +- [X] ChaCha20 - [X] Ed448 - [X] Ed25519 - [X] X448 From e83123b5c93a3bba4418919ad2292b56cfeb8447 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:13:22 +0900 Subject: [PATCH 32/65] =?UTF-8?q?mlkem=20x25519=20cargo=20fmt=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlkem/src/kem.rs | 3 +- mlkem/tests/kat.rs | 405 ++++++++++++++++++++++++++++++++++++++------- x25519/src/lib.rs | 11 +- 3 files changed, 350 insertions(+), 69 deletions(-) diff --git a/mlkem/src/kem.rs b/mlkem/src/kem.rs index 7602570..53daf63 100644 --- a/mlkem/src/kem.rs +++ b/mlkem/src/kem.rs @@ -125,7 +125,8 @@ pub fn decaps( let h_ek: [u8; 32] = dk[polyvec_bytes + ek_len..polyvec_bytes + ek_len + SYMBYTES] .try_into() .unwrap(); - let mut z: [u8; 32] = dk[polyvec_bytes + ek_len + SYMBYTES..polyvec_bytes + ek_len + 2 * SYMBYTES] + let mut z: [u8; 32] = dk + [polyvec_bytes + ek_len + SYMBYTES..polyvec_bytes + ek_len + 2 * SYMBYTES] .try_into() .unwrap(); diff --git a/mlkem/tests/kat.rs b/mlkem/tests/kat.rs index 5649913..1749494 100644 --- a/mlkem/tests/kat.rs +++ b/mlkem/tests/kat.rs @@ -35,11 +35,36 @@ fn unhex(s: &str) -> Vec { fn kat_mlkem512_keygen() { // (d, z, ek, dk) let cases: &[(&str, &str, &str, &str)] = &[ - ("47B893474672BA92E4B12EE44FB32953AF8E8503B5FB471D1614FB8A021A660A", "1F8CB39E9E30BC458A0DC5408884B1187FB217018DF760FA57317703B844A0A9", "28266A088B3482439BCA01AFB7CA5C6136A979B5159985A9484B36B679A5F7B9819EB63577891F7BB9CB98413CCC434ADC79A16D6AB3076569CE6291C59B5D64612A7FB0C15013200BC8BEBB03A570174B5E4363AED86EB02A220D281FB5457F0A549FC5051D49A6B2015259A2C3084F405E1769952260675586A584904059275A265234EF3ABF88C171A80898FC783358BBC9803C8789027D917C9EBACBC568CC18DE84C85454B94249586C0C6E2B8A16FA789C51212DD1728EE9B8C6C40528BF93826FA82368419623032AF27B5694305816811D3CA85805100E9C1A9621E5089E54CB47F5A8FEA0B49EF81C6B5187F48924C7947D6B61697A4A8A18452EF803336AD4BE503275BCACC03C181405F7B1DC9B47FB169EB37BBE27E29C763A4E52B9A42520388CF09B8EDBCDF41CCF6537190E6156C37CC1AAC63C0F90CE78D0B9B190C548D71B6F26CC8F585EA14004B5B30AAA100B2ADC1263828833B24E46163B41446F98C882092A39941867B80632E2097674A793935227DB0B8577E03A69C50A514C7473C892E3FBA7C4316BDABC952A70644176687D4191323BAD93D85A3CA250868C0747E6C44F6126C874AFBEC0BDD4503CB2C59A69816E7D4109941467579A1FFE6A4F50FA379051729DAB6E2F61432F15BE67D667C7CC1054742B2B953078A5CF88D9133087309D88C61DA240D99C59137329907B47865321ECD5564E987333B4CB607B0AFCA86769DC95B2F921357213FCB80C3B152918E9BAB2228C0A1B77897AC68CE55088165F87F397DA9790873B62C5383C0CCC370F0267CBE195651CCF336182C22AC3924B76C9E779B7A271D166B6D24B84242B7E73CC723F764039F6C851744034C3304DB0C091A5764FDC9D593556FF734B82A87CCBC38CA99564D988BBD2D1BF071BB160722D365104FB27610651A8ED817F2742A6B5A1273A61ACAF4460B0AB1456A9922351400A1C7D95D856D6E3370622C9C4164BC6B401435624A98B95CAEB274F34CE92038D785068CDD8CF44C38D84ACB2C466A2756C870EE78C26E738CC451002304EB8C90AB24B6463EB124D779F937A2E3692611D2E34D57B36CC4B2CD3B31FF485C6684D408B972E0D5CA7D2224AAE4E", "89C31D05611AAAB258F78BC2DE0A80D5914BF80C376A990D33CB97F4F2077CE12D2DAC559DE3400B0622754A2E814730BB7C2B401A076CEC9524654DDAC661B2F2123EA64D3BD727CA42C8D2725475BC0AD4B698E61001D031105897CA746249D24538CD63849E874EB9449FBAE979CA2A3F357B7D87F112FB16AB8BA20BDD315688231B21A4083277663113BC70A806E772917FA95743A01F138BC7BD5CB3B21CBF8EE301EB0BCE71A4BAAC3907B469CBEDE767A55AA194930B4A2B227E633CF33A3FC715454A32873B717FE6A21C01989133E42113D985F46807A26B0DE105CD9A897334D9816075C3149A1919CC2DBED56A23F3192CDB68F6E305852B8B864BACB5349CFD98419FFC9486C6B07F788CBC79B7680441B0DB1DBD92AEDFF49DB6F2AE52B95FA9B966C6CC08714A131770903859B823D2CF9731A36D8795B60585F8F1BF1A2C4E544407D5C1359810BF99A7374511A663EC1B192B83E27C4F4B6C3387692E4D756A235C14D151056E829FB79936A0C8A52EE128F0F369F8EB7390783788037CE32B021E57C45EB968CD5B9A04B02F89149778653F208C14C2E786D4544749572BF0D9A31B13BBAD60B6756B9379FC2E1660C58ED373C957309EA213213A5988CC59B1C37153811F7271BC1E729AB1629714D1955F706796556213B08E3F766867D1CC02648BE0574BB8818D0978578C562FFB6795DC065037129C5E4738B5960FE7E2A22570C36571A4A7A0A5187609F8D27714CA299309509C7C8AC2776E6814A262359B971B1A153CAB81858D96350F9F2CCC9884C8F5509E237655B4B2B22E75C6919A82EB56B095E3C52B55583F6855E186CB75DA3A20640762922062721D5C1AC51B87B10633BF43860814C89CE16BBF66667C2101BD27BA80969C78DC487678989EF5AB22D9B42679590E1855C94A9A90B4E021333A4532C7AE238358646094B598AA431486D0B7BBD82C5C4EBB6195680C3E5AA806B155AF7004318867FE55B1996353495034DF459624F4B04B51167A5148499846A36C605AD85B997674DF241D8EC59E56E22CCDB342F535292AA444B69C3E7A252128266A088B3482439BCA01AFB7CA5C6136A979B5159985A9484B36B679A5F7B9819EB63577891F7BB9CB98413CCC434ADC79A16D6AB3076569CE6291C59B5D64612A7FB0C15013200BC8BEBB03A570174B5E4363AED86EB02A220D281FB5457F0A549FC5051D49A6B2015259A2C3084F405E1769952260675586A584904059275A265234EF3ABF88C171A80898FC783358BBC9803C8789027D917C9EBACBC568CC18DE84C85454B94249586C0C6E2B8A16FA789C51212DD1728EE9B8C6C40528BF93826FA82368419623032AF27B5694305816811D3CA85805100E9C1A9621E5089E54CB47F5A8FEA0B49EF81C6B5187F48924C7947D6B61697A4A8A18452EF803336AD4BE503275BCACC03C181405F7B1DC9B47FB169EB37BBE27E29C763A4E52B9A42520388CF09B8EDBCDF41CCF6537190E6156C37CC1AAC63C0F90CE78D0B9B190C548D71B6F26CC8F585EA14004B5B30AAA100B2ADC1263828833B24E46163B41446F98C882092A39941867B80632E2097674A793935227DB0B8577E03A69C50A514C7473C892E3FBA7C4316BDABC952A70644176687D4191323BAD93D85A3CA250868C0747E6C44F6126C874AFBEC0BDD4503CB2C59A69816E7D4109941467579A1FFE6A4F50FA379051729DAB6E2F61432F15BE67D667C7CC1054742B2B953078A5CF88D9133087309D88C61DA240D99C59137329907B47865321ECD5564E987333B4CB607B0AFCA86769DC95B2F921357213FCB80C3B152918E9BAB2228C0A1B77897AC68CE55088165F87F397DA9790873B62C5383C0CCC370F0267CBE195651CCF336182C22AC3924B76C9E779B7A271D166B6D24B84242B7E73CC723F764039F6C851744034C3304DB0C091A5764FDC9D593556FF734B82A87CCBC38CA99564D988BBD2D1BF071BB160722D365104FB27610651A8ED817F2742A6B5A1273A61ACAF4460B0AB1456A9922351400A1C7D95D856D6E3370622C9C4164BC6B401435624A98B95CAEB274F34CE92038D785068CDD8CF44C38D84ACB2C466A2756C870EE78C26E738CC451002304EB8C90AB24B6463EB124D779F937A2E3692611D2E34D57B36CC4B2CD3B31FF485C6684D408B972E0D5CA7D2224AAE4E3A389831056ED8FD81476869245782689C84B3CE90FE6A9E78D0A380FD6A15731F8CB39E9E30BC458A0DC5408884B1187FB217018DF760FA57317703B844A0A9"), - ("3DEF73C558E1F5F2BFB4F6EAA36EE200CDF54580958CC2176282628FF508A016", "B9958575128F97022525266064F9422E83F75FBDCD0F2BC2648655F0E31A17E5", "CAB840FD02818B280C161B2C815853580A92DABB3A7A070026128A75D86CDFA3BC012263E571365B4489907A7ED56AB2891381111131D040587E66B8F870A17A36B73AD8B35C02ACE0E7AC274644A6DC23A7401EF0E547CFB18E9C969164B78C36D06CAEA48670E63B187925EA208757B98BC2272C5DB5256A72BCF2130B9B93B2E793222BBB97AE464B042989811B37BDD38610895B65712C619C9AA1E78D872680B8730D1B7BA79121CC798B3E952C3181F071F19707D7BA1FE99C56A5BBACF4A170E24422C3478C84CB95D0167196A47CB78A17A0E414A1C37F0816594FB0888000C3242B2820BA4D65A15F1E7633117736FEE790F10A5B4567CCCB191466E40CB5E5661D433000E562A0029108D4CE91218625083602CAA7C13411DFA7137DC631D77A82DD4A05AEF81A47B77E3D6998147A7BCCC929F75548E866A6D1D9C7BEB70E63D706622B05A863A9C9D700A13B6426D34374D8BB0E007D8AD7CC1806B152C38DAEEB75DC0C5791C03485B8B45E7ABA834026F2C1BA5554277F636F59005F99D16F3D6430C37C2D24C7115EE04648C88DE90925D5B529AD0B7C3213AC9A6516D395239B91CBEB46547053C98B1C211E64CEC295178A1B2D194A3511B82B21C651E77C7BD1A5131599C5AB4C656CF4BD3763CBD01912C2713F438A463BE24B462B473CFA7D7CD58EDA3C05256A2DB4C57CF8FCC0D2FA6249CB9A36D44F9B8C9DE9E84CF3F49F7D8C08DE9230766C1776929E2F00453D28470A350AEE005A4602812AF31926085DC9FBBFFC3A1BAB2A2297C4C98346B7D48A5D3EDBBBB666AEA8A39E3E96885F0AB67CA81BCAC292A15C6C4789A5DE863676B123DF7B3FBE355448D9AB89C5014D0CA282ECB2504B3685A239DE6603FCAA755B5175A41CA876707CC56A31E3DB98D9BA612A3009121576449BB4E5ECCDC57330813C7CF692B11037122D026BE300942A8A217891672CC41FBA185A62B3B9D67419AC15B472D6C355F830D8EB56D5C892DC58BE0ACA8C6B304CC86A56F3CACC392B1503EA73E22863E6F9C3171A9E18F0331C4C5BC9553CDB4655926A6C4E1989A330B5323F422B9FB1329DB7BBE340AC26F11C6299252EAB254260453FF7B7F4ADD0F4", "3594A400134AE29552DB981BEBDA33EFF05BC5AC7FA0B539F92216CE790957EA34592011AF42AB6BE51311208C5A7A787AC84A330A28DCF6A69DF879CF876401FCB0AF84569C4B1FF556556BAB21F3728C8F172CEBDC3C684786DB0255746C582C3729BBE97B1405378F75979A9B07FDBB36A1B28E3C8C96431CBCE301510B7BA596F34E24F99499B0336F23C0DC5380C044B8395201491CBB85AC11DA6BB78E3A66545B40284C0A64EB8BDC63BA351B95F5EA29E6051C76FB0F7726BF3EF16D531C7CD7CC49ABD584CF237DAB0014DB49C8AA970EE6F39770E48CF44B0E64B86A9D4824042556B3331BCF245C42EAC9E2BB862615230D19B854B55BE721C2E23947FD7C74F3304155626731B4AD38064BC0FCB726E48F316AB24A4A8731DA0B28C0BBED56AF00909C1D0B5F86CB2105E37DF1385A4D8A038980C833B52FF6A1086589173BB96601713AE94C0EF48BCD567B8836985A21650AB72021E675225BEB09DDEC05FF324BC90840FEB8AF2046543F8933492558D6F42552553E477C29AD601EE2571DDE98399D77914B098E07F291167B041ED60AD8D5794FA65AC7E7663FC962AAE22F4375793E7A886278751ADA4E791743CB12CC5F24BBA5D57F3C3B61CF48315C6226DA27C8B7931C3D304F3156B47A07B0EF4CB68937473C982208D5AAFE3CA2948822EBC02BF5A01F62E09CF1B4AB95020306559677FAAE0A677574711CF0A92FA1A5240BD6323DA812AA437A449058589745790037EDA03F19D88AFC5A3D6B18ABA4A2605CE42568ABB7C1F303E508534FB7A10B77C273419C93F418801A81C4F3AE003BAFDD309381DC4DBE8065F978B3EB3B19E03597BC979C0CA0B36E830868E289E2CA54C2196C99922B9047CD4FE7B259A4447F41AE120185F9141EF1B186F68A869EA4373EA654FD6386EF2CA63E7CB68F8454506891C0BAAF3A66953FB26DA03C04F3A27B1F1956E4922E336314AD360D114C395C89810197BE73B8CEAAA1B00AB079A2D33C215450E4FC33F38383E2834F0E143173C1C1ECE2AE19BA48E0FCCAB0CC5712C9BCD8229C2AB00729F37B3BF974D2F718CAB840FD02818B280C161B2C815853580A92DABB3A7A070026128A75D86CDFA3BC012263E571365B4489907A7ED56AB2891381111131D040587E66B8F870A17A36B73AD8B35C02ACE0E7AC274644A6DC23A7401EF0E547CFB18E9C969164B78C36D06CAEA48670E63B187925EA208757B98BC2272C5DB5256A72BCF2130B9B93B2E793222BBB97AE464B042989811B37BDD38610895B65712C619C9AA1E78D872680B8730D1B7BA79121CC798B3E952C3181F071F19707D7BA1FE99C56A5BBACF4A170E24422C3478C84CB95D0167196A47CB78A17A0E414A1C37F0816594FB0888000C3242B2820BA4D65A15F1E7633117736FEE790F10A5B4567CCCB191466E40CB5E5661D433000E562A0029108D4CE91218625083602CAA7C13411DFA7137DC631D77A82DD4A05AEF81A47B77E3D6998147A7BCCC929F75548E866A6D1D9C7BEB70E63D706622B05A863A9C9D700A13B6426D34374D8BB0E007D8AD7CC1806B152C38DAEEB75DC0C5791C03485B8B45E7ABA834026F2C1BA5554277F636F59005F99D16F3D6430C37C2D24C7115EE04648C88DE90925D5B529AD0B7C3213AC9A6516D395239B91CBEB46547053C98B1C211E64CEC295178A1B2D194A3511B82B21C651E77C7BD1A5131599C5AB4C656CF4BD3763CBD01912C2713F438A463BE24B462B473CFA7D7CD58EDA3C05256A2DB4C57CF8FCC0D2FA6249CB9A36D44F9B8C9DE9E84CF3F49F7D8C08DE9230766C1776929E2F00453D28470A350AEE005A4602812AF31926085DC9FBBFFC3A1BAB2A2297C4C98346B7D48A5D3EDBBBB666AEA8A39E3E96885F0AB67CA81BCAC292A15C6C4789A5DE863676B123DF7B3FBE355448D9AB89C5014D0CA282ECB2504B3685A239DE6603FCAA755B5175A41CA876707CC56A31E3DB98D9BA612A3009121576449BB4E5ECCDC57330813C7CF692B11037122D026BE300942A8A217891672CC41FBA185A62B3B9D67419AC15B472D6C355F830D8EB56D5C892DC58BE0ACA8C6B304CC86A56F3CACC392B1503EA73E22863E6F9C3171A9E18F0331C4C5BC9553CDB4655926A6C4E1989A330B5323F422B9FB1329DB7BBE340AC26F11C6299252EAB254260453FF7B7F4ADD0F452BE25FBE4D87AA01253DA8891D2592A416E2F791479E50ABB19E8F215DD6216B9958575128F97022525266064F9422E83F75FBDCD0F2BC2648655F0E31A17E5"), - ("309398D92B95B68A725ED55D3E72AA96717FA08A9B919DCDABE7D893A73AD7CC", "C2FE9CB7F49BF7296785A1824CD798C377BFF4E1836473CD4A8C7423BF1D0F13", "5DFA5E5FE41C3F4064EF11004E943E7913458659CA5A0455D9BA880815B062879BD903136D3A73D021A812A9669DF0A68A04CFB11742307919103C8FE3710A95491204970583D49726A324A6BC240272A5CF26B56C1656F996B37FB430E27CC7A30742513439535B3C5E29C302ABC00265A2D7512027969F8A50CC2B22901D16AA719906D5094C8A91A3906CBFEB852CC3A581FA33B47685957A831AF48AC244F766DE32A4A0190ED48B336E4155A9795930DC4209DA637DF59CEE3033250083E8AAB97C30C9EE75196148CE6CC556E8B54AEAE743FF9A0CC49B00D7B0AAF35387A22403A4956056FCA423244877995FC272A9A623C176C911AA9B0227AC57FB204600546E6E10C843FCAAEF8A418E6739EE4669DFC577BCA851FCBACEA3A699A25BCA3DE397ECBCC9D31C3DBD0CB461A07DCB976F900BBF44CA3127121D48127D5FC3325A0643F0233C6C45583C217CC9A17A4882260544B43CC0972B730A2948639C29B5A0F383023112FB106CD9700D011B091A83CB099C21F2B2C08B3994FDD5369D4C5AF61896BB00922B113F80699423B1BC4A5885E95A33DCBA6E8165C4BA0A7B2BB16D00FD32B20837D717CD55C91189734D889C09416C89CDE21DF4BC7787780A1B4B50A3AA66ED38539B71596E473965165BCFA701A0153A46C9AE0B6B588A136BCB814FE0A8CAE7D63D2BF3AE61EA2FCB14036A76756B0387DF7123A73620DE7ACB82AB23991508D03078D83B221C047F864C15D7BA75C722988305798203849FA0BE732B23F41084032C75F097BDA90B2FED6C51ADC56153951F0F96BC9951CA34BC1B4C972F4515234ED664738CCE4330681C36BF773C4783216F1E964A0DE4B1A1923183E232C45B6992EBB8E0D5154261274DDB1AA6D2AA37D836D51BCB77DA1934B510A22A83072707412ABA89AC81F7D9A5B38B84CCD03B56B7133373991419CC09110FA1DB151D453BEEC20CB46163DE304723D947858299615B9E9CB15553207F09772116C843A315C389F160508A001BAA76F92759DB79B4613379AD66B7986303BD17C872DC9D276482372861F06B805B03C55F251A812137CDBA5AF5C65C2D795EBF84EE48F10338AF317CC5C961AE4AE8243C5F0E", "310AB0E7D75FD82CA23D0BAE25C4935465BA8596C6D08A44BBD6A9AAA51F741607A71238A9A24E3A878C388A8099D66A61A4474876197BD671D25A939D66B7D811541381C7C2694C53C8399F40780F5C3960366D70E76353676E0AA024ED8B9EFB81893FC1A7A06CBAD4BA2D6A16828ED5002B808ED52124BB8B21CAA53038EAA15509C1FAB3478CB39A4EA4A9644174A0F1A46E707240EA6C1B32CB90B0608370C238F96DFC404E00251F8E809DA6B72829578495E9236E1019B651B49DF920CA2697B8051C7D973538998D13A867D51AB491841BAF91A9C9F1454057937150361D7CAAFD9A9A2CF51158F659D3296BB4E30A32AC5E4AD23B3552AFD5C7CAE9F338A0191EFD4624EBA24DAF48C7453CA87ADA377C69C1DA2ACEE818CAB702434E5716695B2513F830CE797EA9EA0F5A04AFB84777ACDB7399D247797A5FD6C62368D8622B773B1C22382B5C1EABB3B09312BFE60C7A9972BDAE9AC737697B6CFC0582F938658A60E8D015F670B373D2A00021A890B656051B98E7182E08454E7F31355ADA6C56C7CF9D528AC1409A4A015B471472316610240C4570D6A2F7F297ED76A2EDA8B703427A22C056D4A549BF859612A56A5AEC469147118DEA0061E7620CE30355EAB40BA1B6AB4B39EB77061ADB9C0EA148B05BC0C7B6BF08FC91B20971AD8641051B230BA9C1A8D5C8C500231C88512361937C36CA9EF488913747C9E77346505007B55147F3540938983C4CCAC0733101C46B1A138AF1DB42F926C18CFC86A65B40ABBB25D3C4A51BDA8FC5D63CE510207B1932DE31CA0651323EC6C5F8826720C09413B6145E54CC18F9AE41B71F369B7F07207563707651797EE88A92544A46BE59063D95C1E88B7D4846C894338BDED33E1B088D81F48332C2079504147D493755D15084463D23175CFBE7A293424B66A61A71155C8C176FC7528C8A576418D577E9858E3FCBC6CC51C65040847ED6404B7A44B9E82F321C28D6176CF31C7F722B9AF7B9598348A27808A50A0374D581343EDAA74BC37187709879D17C8B853FD659645289B929603BF245AEB5EB92D655A7B3C42285C8235DFA5E5FE41C3F4064EF11004E943E7913458659CA5A0455D9BA880815B062879BD903136D3A73D021A812A9669DF0A68A04CFB11742307919103C8FE3710A95491204970583D49726A324A6BC240272A5CF26B56C1656F996B37FB430E27CC7A30742513439535B3C5E29C302ABC00265A2D7512027969F8A50CC2B22901D16AA719906D5094C8A91A3906CBFEB852CC3A581FA33B47685957A831AF48AC244F766DE32A4A0190ED48B336E4155A9795930DC4209DA637DF59CEE3033250083E8AAB97C30C9EE75196148CE6CC556E8B54AEAE743FF9A0CC49B00D7B0AAF35387A22403A4956056FCA423244877995FC272A9A623C176C911AA9B0227AC57FB204600546E6E10C843FCAAEF8A418E6739EE4669DFC577BCA851FCBACEA3A699A25BCA3DE397ECBCC9D31C3DBD0CB461A07DCB976F900BBF44CA3127121D48127D5FC3325A0643F0233C6C45583C217CC9A17A4882260544B43CC0972B730A2948639C29B5A0F383023112FB106CD9700D011B091A83CB099C21F2B2C08B3994FDD5369D4C5AF61896BB00922B113F80699423B1BC4A5885E95A33DCBA6E8165C4BA0A7B2BB16D00FD32B20837D717CD55C91189734D889C09416C89CDE21DF4BC7787780A1B4B50A3AA66ED38539B71596E473965165BCFA701A0153A46C9AE0B6B588A136BCB814FE0A8CAE7D63D2BF3AE61EA2FCB14036A76756B0387DF7123A73620DE7ACB82AB23991508D03078D83B221C047F864C15D7BA75C722988305798203849FA0BE732B23F41084032C75F097BDA90B2FED6C51ADC56153951F0F96BC9951CA34BC1B4C972F4515234ED664738CCE4330681C36BF773C4783216F1E964A0DE4B1A1923183E232C45B6992EBB8E0D5154261274DDB1AA6D2AA37D836D51BCB77DA1934B510A22A83072707412ABA89AC81F7D9A5B38B84CCD03B56B7133373991419CC09110FA1DB151D453BEEC20CB46163DE304723D947858299615B9E9CB15553207F09772116C843A315C389F160508A001BAA76F92759DB79B4613379AD66B7986303BD17C872DC9D276482372861F06B805B03C55F251A812137CDBA5AF5C65C2D795EBF84EE48F10338AF317CC5C961AE4AE8243C5F0E7FF658BC933FC07E44F8B4B3C3125FF493176D18380C6F7816B229D6D2F56EE3C2FE9CB7F49BF7296785A1824CD798C377BFF4E1836473CD4A8C7423BF1D0F13"), - ("FA58CAE89E664853035BA4EA8723C83225B89C8365024FE73A3E1B7AE01E6648", "ABF5D4E34BE28DB2984CAEFD09F06095921B3C991EF8C4CE3B16CA2C18E0A0B2", "40E90144374B8801711753236C08520F53BABDB82EA11B3E092B461D837C0CD95D90F7BCFF16956DF280FE69626825B26DB54E28129B07A0A32CA7726F7A93E2261EC5D604463C64669B7A7EE24A9BE822FAAC57837A4274B106C346143B0087FC5381C54040D4D897506C9C56385147F49D3FFAA51B9759B2E6A119AB7C421A8B48E0AA1C1C3C3F636349887E315A02A0B67135B732AA406090B09FE6B000A52C0E9FA737ACD1C8F4E2C31A17349AD60193950100506B957A6E381578224B37A2452B602AA9738884579B1D6DB6886364C07648B4EE6B7FA99418A891BEFDB67B7F76645F902DBEB117191073CAD23F9E227AC589708939B0FC6C60EFF7A321903224F342D4D29D78C660C1178204B80B0FA477F3F83FFDDA0AA213368EE3BD75B0C3D2C8533634AF9D94660B232775DA0E1788BCEF99B9642299775655AB63A1CC865D863834CA933A39E865198A8A93514DA351278AE67649203D773792AFC551E22367EAF48FCFB4507FF3ADBE217F4653642FA05BDC00AB3BCA8627F1ACF89040277C0DCD071136F97FBA75B59DCC3D0D22355E54335823BAC3904606D79EA845BC448C49BA2224DCAA7DDA9A583A16AAD382A8E15653AD19085FB9A2A70A1CA82ABBBDD6C05AD081E4C7686E917AF652AD5DBBC19FFC397BBB13EA7C82ACAC1A6695AABA32B39AC593774B1BBA46044459B11A7B134218C89CD24C22A49077D75BB3681F70059558916631911A05758068709671B02F04D0004FF31AB3D08FA68C63FE9BB95D4360810280DF384ECF773D0B3C97899596EFAAC05B8C816FB7877446366140C4B4150EF3F5A742076AD1A0A54100AFBCAA8531B1AD807B785F833C6F819C6DB679B0F0B087E555F8893C2ED7B6DF945DB623B21E78540F424FF66329464A8A59BA7202D8AD85131D81060928A45899F06332C0779D45A4D4D25EDD81138F9B70D3F90CDDDC2845557502413DF58C2E95692CCDD61A369A40A830A4B3E629B7BA81A1E873228109A5A24E4D6613224193201198FE6058E009BABC0757919912C0534A726304B8118831F73D66095F4E409941AB2595F994D84F674FA40EAE3C8B8F1E46F114141E6241795B3BB324521BD8E331A6B78663", "41E1C66EE49564867327963287EA72B0347DEC961A1F651A454771586C238398AD28730AD66B9AB5433E26AC0263A92B2A233785525662191507822FA546CF4ED82493353DE20B0D9A616A79BAAFB6252A812B9363A039B7E2AD61F417D08230E247A518E022B41A63FD47874DBC5274D0C332DBCDC335B5070419D999CE8AC5077C85691D167144488EFFEC5C16482E8D285DE572980E877223903A36912F4B0A409FC17501B8A299447D6D6518F04945E209C8985CB6C207042B446F74D5AC1B7A8429A5AC60F20BEF28926CD618441A5363C2B25231989F814732D948350C481738BC567190678310ACAB5665F9C09028038B2A1CAA27709ACBA80B5487918412458472B9E8758C2051A88240FF3340A8F8C5E44664A23A0201DB9D5DBA09C5345A11EC03FF48AA651A5BBEDB32D7DC4750A0ADBF81C64EB43032E6C828698AB97BC8861A101B05937C23C6DE419EC5F635C949A865731FB6834164F04BF664848C04B6C6EB8664D02492C64D9AABC504583531B4909E492FA1B726D5BB8CE04457E6038396F352770C11ECDBCA271CA9E6704A64B7749EE8A8DE925C831C31F70550AB781768CC8713F820E291BA39C0BAE2D3C195934429F5B322E8017A584482C608655A93111583D5AB5FA887556FDBAFF6076BEC4342DD9647EB1B3091BA559880186B9C0F63606DB3B390A2714546817C0D80290188751B4885F71362A097B7A97925969B553874081C57382FB92508E81C29E7A311741C6104C3D51A7A9A8B3FAA4703F8A94899317AB5A017A211A4F369C2ACD2235A24827E6012214BA87927179678CC5A8BA309911EB1D701767B94535532882C00C99123E4818BD968B8DC4CBA6D68972AB80FBA50C0DAFA2716A6B79BF021FCCCA4E5585CBCFB77F32A2DDCB06A09F37362D83FC2179636C8A720679C1EB604811C2403D4BA8005B11B75950346CE09A22306533F0F61659423AB449A03AA6A89AF2B7B90739F7FAB0D4DE9733227663DF397A0A5771BE893EEFBC7A1570C7D206CA6143C2DA3429D1C10A5F153F512B58651AEB3EB72CABB437B19A8AFA7BC21A8A527B07440E90144374B8801711753236C08520F53BABDB82EA11B3E092B461D837C0CD95D90F7BCFF16956DF280FE69626825B26DB54E28129B07A0A32CA7726F7A93E2261EC5D604463C64669B7A7EE24A9BE822FAAC57837A4274B106C346143B0087FC5381C54040D4D897506C9C56385147F49D3FFAA51B9759B2E6A119AB7C421A8B48E0AA1C1C3C3F636349887E315A02A0B67135B732AA406090B09FE6B000A52C0E9FA737ACD1C8F4E2C31A17349AD60193950100506B957A6E381578224B37A2452B602AA9738884579B1D6DB6886364C07648B4EE6B7FA99418A891BEFDB67B7F76645F902DBEB117191073CAD23F9E227AC589708939B0FC6C60EFF7A321903224F342D4D29D78C660C1178204B80B0FA477F3F83FFDDA0AA213368EE3BD75B0C3D2C8533634AF9D94660B232775DA0E1788BCEF99B9642299775655AB63A1CC865D863834CA933A39E865198A8A93514DA351278AE67649203D773792AFC551E22367EAF48FCFB4507FF3ADBE217F4653642FA05BDC00AB3BCA8627F1ACF89040277C0DCD071136F97FBA75B59DCC3D0D22355E54335823BAC3904606D79EA845BC448C49BA2224DCAA7DDA9A583A16AAD382A8E15653AD19085FB9A2A70A1CA82ABBBDD6C05AD081E4C7686E917AF652AD5DBBC19FFC397BBB13EA7C82ACAC1A6695AABA32B39AC593774B1BBA46044459B11A7B134218C89CD24C22A49077D75BB3681F70059558916631911A05758068709671B02F04D0004FF31AB3D08FA68C63FE9BB95D4360810280DF384ECF773D0B3C97899596EFAAC05B8C816FB7877446366140C4B4150EF3F5A742076AD1A0A54100AFBCAA8531B1AD807B785F833C6F819C6DB679B0F0B087E555F8893C2ED7B6DF945DB623B21E78540F424FF66329464A8A59BA7202D8AD85131D81060928A45899F06332C0779D45A4D4D25EDD81138F9B70D3F90CDDDC2845557502413DF58C2E95692CCDD61A369A40A830A4B3E629B7BA81A1E873228109A5A24E4D6613224193201198FE6058E009BABC0757919912C0534A726304B8118831F73D66095F4E409941AB2595F994D84F674FA40EAE3C8B8F1E46F114141E6241795B3BB324521BD8E331A6B78663C8C4EFE33D072A1F4DDF63EAB34ED9F1D5B34E15A0BC3981BA572545E773CD76ABF5D4E34BE28DB2984CAEFD09F06095921B3C991EF8C4CE3B16CA2C18E0A0B2"), - ("FF466BC223EE9EBF1DF06432873A6816BE864CFE408B2D676945311E31B7BF84", "482303CBF9E3963784E1BAC39086AADF7DE6DBB45E61A8021265A5BA2522A1D8", "64D24F31ACBCE41742EE149B50A331F9951D92449ECEB58C85520496770C543A8E4903574700083AF5CF85E7075AB494D406C44A106CDAE48F8F5C37709CB6F8A448957991FBBAA68A7710080A9680A3527A9B2036617EF91585369AB1A8C865D58B1F8AAA6F53BC32FDDAB1AD46B3E5274C6C09654C284FD8339F2A883118E69FBD262F057A4B1B2BBE5EB8B6D1988034A6184CF881BD642CB39059B6021257E37218E4ABDF6AC13200606423BCC96B3A7A03CC5F915D3C578158734FA748BFBC1712BCCB9213BA49ECF4181B2C2DD8007DCC347CBD3C4A5FBB833468523E6B58CE607E4AEC716F8A44EB211718C89899E5AF9396BF3AD50397B2ABDA921B19CC438E890D82C14D6053C735C4C3C3165D4D58CF74775D478C4BAAF2015795B061B0C25E52B6610448F8818C00E04F5825504CA995043288F4889CFDE1129646A304A1C583358534FB0A8110995AF7434A5447E6B31448C50D3C87481B626DD17B95242935A2C5373C8A9E1207AAEB60AFC4C96AE1B790899CB5C28055723547594B918A84BA82D885B0B41F4DF755134602750233BCA285295C3022E037CBD03CD2D7B141C06015E8A456C2A373CAC509805472E57A04186BF3A6CF36C7BFE76812B20CBC60F59CB5C0B378E1609F514FE7894852A329DEC82FD763684772B0666055C512A35457287D73B557D446CAF2A3BE425F152A62B91893CEB72FCD52156C22294B6421D53074788A4C0A23AFBD1A8C5375118AE2879912CB5B198EABE46E5502197D23315730458119BBFFA64EACC12C883A545A67288CB87D11004A4EBB1F692A127BDB0CBDD54F2E83951BD77498B90F8EA192805819F8582B91804F7022B5A0B75D88792EB7A94044D4BC0A583B45A1B2E2F6024E99C7959963F1C54478181822058E55637F755978B8F46A789BAA99EB88B70AC2056A5A752AA8753B8511AA77980C4BA1392D1D9BB77216089EC0BFFCA661998801BF499E7A4B82A2035FAF273D2B28322DD71E5B342E50016D29D91B81B405C365A9EE0CA6859230B3188D4BF9BE89EB651D7768D7C4807644929E964B17E98FF5EAA35A9816BD09B7490066720FCE37A99C7F1CA6EC12E3EDE24DE9BE115230F6E56ECE1FA2", "4B95C17A934210318070956FE0831459483F97F5016421790D6387AFE4A7FAE52461AC7A8B84425BC21A9AB9CF7E778023C69986693509F62A30617DCFE6805CE38CC034587A326692576E3857A581B79C2EB0C6CEF13217A44268036A9A911C9E52A521B03F22EB405DE4AD255211F210553587C20CE34A54738117689DA806B3D8C1AC33F7A0351598C86B1C57D97781A9B63C31C50D832FA4A25C71475CA5D03031C316E2ABBC64967B3E9A2B49F3686073CBB1D9CD02950F4E08597C423BDB386E06E782E10BCB4CBA9ED90ABBC8CAC59915CAB644329526652FFACCEB03AE02928CFD3B18DBE12C4D478FBDD56ED43C1F8305067E355D3209314BD6741DDB89811B541F2073600319F37002297601DEC38DC2E0BC5E87BAE0093C7DD27DF0A10BED4AB42CC884B19138895A6200DBCCFE2B0A7542CB27B7026FE544CECA213E78B0747373FA001E2DF3C354CA27A9818D6DE7B09AC970084B237D1791B6892EC9E8B632A167A23217A8C3981AD009CD127C67A40E3EA90D4967535BDA4A246CCDD0C103C4E23BAD2A4A2222A277A3BC2FEB769DB09D13EA26D20718D9E98E05C4B51523B834D9691B2969ABA3AC0632ABF3C26711531BCC6A54636A3422065B00F07A7EEB96124341E875B859738785126CFB2C4652E832841B38E3D50946C47B67746FF69C4855EB6D805879ACE8464880664D8BB52B35CBEC3C7F4528A826D92D7FF153AE7046E0EBBD89A16E73E554428903FF77000E70147BC7689EEC40207965AC105C1BA95CBFBA584DFC6431BA82F8F849B87CB7F55978908450958B578E3869C9348B445945E8635571CA1D237B9F2FA25BFB85477BA45D1DC974027BC903BC0B3DE111EC98722E3AC4E25497DEA304F8B4022753ACBA83BFC0187FE93C6D8271CAE796C4C701B60B4A434825AF44CB1C807C44F7655BAFEC2F4365B1357087E1096E6A6A49D2C686C52A7E0C5C07FAB6B1AB668088E9CB4B3C7316421A3281850EEB358B67A77AC40CDA765975D342B5E6BD1854583F038A56D82FDB2C8698180E0037722B2A8616B96DED13A2FD387E9B357615849C8F09B464D24F31ACBCE41742EE149B50A331F9951D92449ECEB58C85520496770C543A8E4903574700083AF5CF85E7075AB494D406C44A106CDAE48F8F5C37709CB6F8A448957991FBBAA68A7710080A9680A3527A9B2036617EF91585369AB1A8C865D58B1F8AAA6F53BC32FDDAB1AD46B3E5274C6C09654C284FD8339F2A883118E69FBD262F057A4B1B2BBE5EB8B6D1988034A6184CF881BD642CB39059B6021257E37218E4ABDF6AC13200606423BCC96B3A7A03CC5F915D3C578158734FA748BFBC1712BCCB9213BA49ECF4181B2C2DD8007DCC347CBD3C4A5FBB833468523E6B58CE607E4AEC716F8A44EB211718C89899E5AF9396BF3AD50397B2ABDA921B19CC438E890D82C14D6053C735C4C3C3165D4D58CF74775D478C4BAAF2015795B061B0C25E52B6610448F8818C00E04F5825504CA995043288F4889CFDE1129646A304A1C583358534FB0A8110995AF7434A5447E6B31448C50D3C87481B626DD17B95242935A2C5373C8A9E1207AAEB60AFC4C96AE1B790899CB5C28055723547594B918A84BA82D885B0B41F4DF755134602750233BCA285295C3022E037CBD03CD2D7B141C06015E8A456C2A373CAC509805472E57A04186BF3A6CF36C7BFE76812B20CBC60F59CB5C0B378E1609F514FE7894852A329DEC82FD763684772B0666055C512A35457287D73B557D446CAF2A3BE425F152A62B91893CEB72FCD52156C22294B6421D53074788A4C0A23AFBD1A8C5375118AE2879912CB5B198EABE46E5502197D23315730458119BBFFA64EACC12C883A545A67288CB87D11004A4EBB1F692A127BDB0CBDD54F2E83951BD77498B90F8EA192805819F8582B91804F7022B5A0B75D88792EB7A94044D4BC0A583B45A1B2E2F6024E99C7959963F1C54478181822058E55637F755978B8F46A789BAA99EB88B70AC2056A5A752AA8753B8511AA77980C4BA1392D1D9BB77216089EC0BFFCA661998801BF499E7A4B82A2035FAF273D2B28322DD71E5B342E50016D29D91B81B405C365A9EE0CA6859230B3188D4BF9BE89EB651D7768D7C4807644929E964B17E98FF5EAA35A9816BD09B7490066720FCE37A99C7F1CA6EC12E3EDE24DE9BE115230F6E56ECE1FA2B8DCD1A8B1B0A8CD37A0F09ED734E53BE83FDCA95E8680EE9816913C5BF47638482303CBF9E3963784E1BAC39086AADF7DE6DBB45E61A8021265A5BA2522A1D8"), + ( + "47B893474672BA92E4B12EE44FB32953AF8E8503B5FB471D1614FB8A021A660A", + "1F8CB39E9E30BC458A0DC5408884B1187FB217018DF760FA57317703B844A0A9", + "28266A088B3482439BCA01AFB7CA5C6136A979B5159985A9484B36B679A5F7B9819EB63577891F7BB9CB98413CCC434ADC79A16D6AB3076569CE6291C59B5D64612A7FB0C15013200BC8BEBB03A570174B5E4363AED86EB02A220D281FB5457F0A549FC5051D49A6B2015259A2C3084F405E1769952260675586A584904059275A265234EF3ABF88C171A80898FC783358BBC9803C8789027D917C9EBACBC568CC18DE84C85454B94249586C0C6E2B8A16FA789C51212DD1728EE9B8C6C40528BF93826FA82368419623032AF27B5694305816811D3CA85805100E9C1A9621E5089E54CB47F5A8FEA0B49EF81C6B5187F48924C7947D6B61697A4A8A18452EF803336AD4BE503275BCACC03C181405F7B1DC9B47FB169EB37BBE27E29C763A4E52B9A42520388CF09B8EDBCDF41CCF6537190E6156C37CC1AAC63C0F90CE78D0B9B190C548D71B6F26CC8F585EA14004B5B30AAA100B2ADC1263828833B24E46163B41446F98C882092A39941867B80632E2097674A793935227DB0B8577E03A69C50A514C7473C892E3FBA7C4316BDABC952A70644176687D4191323BAD93D85A3CA250868C0747E6C44F6126C874AFBEC0BDD4503CB2C59A69816E7D4109941467579A1FFE6A4F50FA379051729DAB6E2F61432F15BE67D667C7CC1054742B2B953078A5CF88D9133087309D88C61DA240D99C59137329907B47865321ECD5564E987333B4CB607B0AFCA86769DC95B2F921357213FCB80C3B152918E9BAB2228C0A1B77897AC68CE55088165F87F397DA9790873B62C5383C0CCC370F0267CBE195651CCF336182C22AC3924B76C9E779B7A271D166B6D24B84242B7E73CC723F764039F6C851744034C3304DB0C091A5764FDC9D593556FF734B82A87CCBC38CA99564D988BBD2D1BF071BB160722D365104FB27610651A8ED817F2742A6B5A1273A61ACAF4460B0AB1456A9922351400A1C7D95D856D6E3370622C9C4164BC6B401435624A98B95CAEB274F34CE92038D785068CDD8CF44C38D84ACB2C466A2756C870EE78C26E738CC451002304EB8C90AB24B6463EB124D779F937A2E3692611D2E34D57B36CC4B2CD3B31FF485C6684D408B972E0D5CA7D2224AAE4E", + "89C31D05611AAAB258F78BC2DE0A80D5914BF80C376A990D33CB97F4F2077CE12D2DAC559DE3400B0622754A2E814730BB7C2B401A076CEC9524654DDAC661B2F2123EA64D3BD727CA42C8D2725475BC0AD4B698E61001D031105897CA746249D24538CD63849E874EB9449FBAE979CA2A3F357B7D87F112FB16AB8BA20BDD315688231B21A4083277663113BC70A806E772917FA95743A01F138BC7BD5CB3B21CBF8EE301EB0BCE71A4BAAC3907B469CBEDE767A55AA194930B4A2B227E633CF33A3FC715454A32873B717FE6A21C01989133E42113D985F46807A26B0DE105CD9A897334D9816075C3149A1919CC2DBED56A23F3192CDB68F6E305852B8B864BACB5349CFD98419FFC9486C6B07F788CBC79B7680441B0DB1DBD92AEDFF49DB6F2AE52B95FA9B966C6CC08714A131770903859B823D2CF9731A36D8795B60585F8F1BF1A2C4E544407D5C1359810BF99A7374511A663EC1B192B83E27C4F4B6C3387692E4D756A235C14D151056E829FB79936A0C8A52EE128F0F369F8EB7390783788037CE32B021E57C45EB968CD5B9A04B02F89149778653F208C14C2E786D4544749572BF0D9A31B13BBAD60B6756B9379FC2E1660C58ED373C957309EA213213A5988CC59B1C37153811F7271BC1E729AB1629714D1955F706796556213B08E3F766867D1CC02648BE0574BB8818D0978578C562FFB6795DC065037129C5E4738B5960FE7E2A22570C36571A4A7A0A5187609F8D27714CA299309509C7C8AC2776E6814A262359B971B1A153CAB81858D96350F9F2CCC9884C8F5509E237655B4B2B22E75C6919A82EB56B095E3C52B55583F6855E186CB75DA3A20640762922062721D5C1AC51B87B10633BF43860814C89CE16BBF66667C2101BD27BA80969C78DC487678989EF5AB22D9B42679590E1855C94A9A90B4E021333A4532C7AE238358646094B598AA431486D0B7BBD82C5C4EBB6195680C3E5AA806B155AF7004318867FE55B1996353495034DF459624F4B04B51167A5148499846A36C605AD85B997674DF241D8EC59E56E22CCDB342F535292AA444B69C3E7A252128266A088B3482439BCA01AFB7CA5C6136A979B5159985A9484B36B679A5F7B9819EB63577891F7BB9CB98413CCC434ADC79A16D6AB3076569CE6291C59B5D64612A7FB0C15013200BC8BEBB03A570174B5E4363AED86EB02A220D281FB5457F0A549FC5051D49A6B2015259A2C3084F405E1769952260675586A584904059275A265234EF3ABF88C171A80898FC783358BBC9803C8789027D917C9EBACBC568CC18DE84C85454B94249586C0C6E2B8A16FA789C51212DD1728EE9B8C6C40528BF93826FA82368419623032AF27B5694305816811D3CA85805100E9C1A9621E5089E54CB47F5A8FEA0B49EF81C6B5187F48924C7947D6B61697A4A8A18452EF803336AD4BE503275BCACC03C181405F7B1DC9B47FB169EB37BBE27E29C763A4E52B9A42520388CF09B8EDBCDF41CCF6537190E6156C37CC1AAC63C0F90CE78D0B9B190C548D71B6F26CC8F585EA14004B5B30AAA100B2ADC1263828833B24E46163B41446F98C882092A39941867B80632E2097674A793935227DB0B8577E03A69C50A514C7473C892E3FBA7C4316BDABC952A70644176687D4191323BAD93D85A3CA250868C0747E6C44F6126C874AFBEC0BDD4503CB2C59A69816E7D4109941467579A1FFE6A4F50FA379051729DAB6E2F61432F15BE67D667C7CC1054742B2B953078A5CF88D9133087309D88C61DA240D99C59137329907B47865321ECD5564E987333B4CB607B0AFCA86769DC95B2F921357213FCB80C3B152918E9BAB2228C0A1B77897AC68CE55088165F87F397DA9790873B62C5383C0CCC370F0267CBE195651CCF336182C22AC3924B76C9E779B7A271D166B6D24B84242B7E73CC723F764039F6C851744034C3304DB0C091A5764FDC9D593556FF734B82A87CCBC38CA99564D988BBD2D1BF071BB160722D365104FB27610651A8ED817F2742A6B5A1273A61ACAF4460B0AB1456A9922351400A1C7D95D856D6E3370622C9C4164BC6B401435624A98B95CAEB274F34CE92038D785068CDD8CF44C38D84ACB2C466A2756C870EE78C26E738CC451002304EB8C90AB24B6463EB124D779F937A2E3692611D2E34D57B36CC4B2CD3B31FF485C6684D408B972E0D5CA7D2224AAE4E3A389831056ED8FD81476869245782689C84B3CE90FE6A9E78D0A380FD6A15731F8CB39E9E30BC458A0DC5408884B1187FB217018DF760FA57317703B844A0A9", + ), + ( + "3DEF73C558E1F5F2BFB4F6EAA36EE200CDF54580958CC2176282628FF508A016", + "B9958575128F97022525266064F9422E83F75FBDCD0F2BC2648655F0E31A17E5", + "CAB840FD02818B280C161B2C815853580A92DABB3A7A070026128A75D86CDFA3BC012263E571365B4489907A7ED56AB2891381111131D040587E66B8F870A17A36B73AD8B35C02ACE0E7AC274644A6DC23A7401EF0E547CFB18E9C969164B78C36D06CAEA48670E63B187925EA208757B98BC2272C5DB5256A72BCF2130B9B93B2E793222BBB97AE464B042989811B37BDD38610895B65712C619C9AA1E78D872680B8730D1B7BA79121CC798B3E952C3181F071F19707D7BA1FE99C56A5BBACF4A170E24422C3478C84CB95D0167196A47CB78A17A0E414A1C37F0816594FB0888000C3242B2820BA4D65A15F1E7633117736FEE790F10A5B4567CCCB191466E40CB5E5661D433000E562A0029108D4CE91218625083602CAA7C13411DFA7137DC631D77A82DD4A05AEF81A47B77E3D6998147A7BCCC929F75548E866A6D1D9C7BEB70E63D706622B05A863A9C9D700A13B6426D34374D8BB0E007D8AD7CC1806B152C38DAEEB75DC0C5791C03485B8B45E7ABA834026F2C1BA5554277F636F59005F99D16F3D6430C37C2D24C7115EE04648C88DE90925D5B529AD0B7C3213AC9A6516D395239B91CBEB46547053C98B1C211E64CEC295178A1B2D194A3511B82B21C651E77C7BD1A5131599C5AB4C656CF4BD3763CBD01912C2713F438A463BE24B462B473CFA7D7CD58EDA3C05256A2DB4C57CF8FCC0D2FA6249CB9A36D44F9B8C9DE9E84CF3F49F7D8C08DE9230766C1776929E2F00453D28470A350AEE005A4602812AF31926085DC9FBBFFC3A1BAB2A2297C4C98346B7D48A5D3EDBBBB666AEA8A39E3E96885F0AB67CA81BCAC292A15C6C4789A5DE863676B123DF7B3FBE355448D9AB89C5014D0CA282ECB2504B3685A239DE6603FCAA755B5175A41CA876707CC56A31E3DB98D9BA612A3009121576449BB4E5ECCDC57330813C7CF692B11037122D026BE300942A8A217891672CC41FBA185A62B3B9D67419AC15B472D6C355F830D8EB56D5C892DC58BE0ACA8C6B304CC86A56F3CACC392B1503EA73E22863E6F9C3171A9E18F0331C4C5BC9553CDB4655926A6C4E1989A330B5323F422B9FB1329DB7BBE340AC26F11C6299252EAB254260453FF7B7F4ADD0F4", + "3594A400134AE29552DB981BEBDA33EFF05BC5AC7FA0B539F92216CE790957EA34592011AF42AB6BE51311208C5A7A787AC84A330A28DCF6A69DF879CF876401FCB0AF84569C4B1FF556556BAB21F3728C8F172CEBDC3C684786DB0255746C582C3729BBE97B1405378F75979A9B07FDBB36A1B28E3C8C96431CBCE301510B7BA596F34E24F99499B0336F23C0DC5380C044B8395201491CBB85AC11DA6BB78E3A66545B40284C0A64EB8BDC63BA351B95F5EA29E6051C76FB0F7726BF3EF16D531C7CD7CC49ABD584CF237DAB0014DB49C8AA970EE6F39770E48CF44B0E64B86A9D4824042556B3331BCF245C42EAC9E2BB862615230D19B854B55BE721C2E23947FD7C74F3304155626731B4AD38064BC0FCB726E48F316AB24A4A8731DA0B28C0BBED56AF00909C1D0B5F86CB2105E37DF1385A4D8A038980C833B52FF6A1086589173BB96601713AE94C0EF48BCD567B8836985A21650AB72021E675225BEB09DDEC05FF324BC90840FEB8AF2046543F8933492558D6F42552553E477C29AD601EE2571DDE98399D77914B098E07F291167B041ED60AD8D5794FA65AC7E7663FC962AAE22F4375793E7A886278751ADA4E791743CB12CC5F24BBA5D57F3C3B61CF48315C6226DA27C8B7931C3D304F3156B47A07B0EF4CB68937473C982208D5AAFE3CA2948822EBC02BF5A01F62E09CF1B4AB95020306559677FAAE0A677574711CF0A92FA1A5240BD6323DA812AA437A449058589745790037EDA03F19D88AFC5A3D6B18ABA4A2605CE42568ABB7C1F303E508534FB7A10B77C273419C93F418801A81C4F3AE003BAFDD309381DC4DBE8065F978B3EB3B19E03597BC979C0CA0B36E830868E289E2CA54C2196C99922B9047CD4FE7B259A4447F41AE120185F9141EF1B186F68A869EA4373EA654FD6386EF2CA63E7CB68F8454506891C0BAAF3A66953FB26DA03C04F3A27B1F1956E4922E336314AD360D114C395C89810197BE73B8CEAAA1B00AB079A2D33C215450E4FC33F38383E2834F0E143173C1C1ECE2AE19BA48E0FCCAB0CC5712C9BCD8229C2AB00729F37B3BF974D2F718CAB840FD02818B280C161B2C815853580A92DABB3A7A070026128A75D86CDFA3BC012263E571365B4489907A7ED56AB2891381111131D040587E66B8F870A17A36B73AD8B35C02ACE0E7AC274644A6DC23A7401EF0E547CFB18E9C969164B78C36D06CAEA48670E63B187925EA208757B98BC2272C5DB5256A72BCF2130B9B93B2E793222BBB97AE464B042989811B37BDD38610895B65712C619C9AA1E78D872680B8730D1B7BA79121CC798B3E952C3181F071F19707D7BA1FE99C56A5BBACF4A170E24422C3478C84CB95D0167196A47CB78A17A0E414A1C37F0816594FB0888000C3242B2820BA4D65A15F1E7633117736FEE790F10A5B4567CCCB191466E40CB5E5661D433000E562A0029108D4CE91218625083602CAA7C13411DFA7137DC631D77A82DD4A05AEF81A47B77E3D6998147A7BCCC929F75548E866A6D1D9C7BEB70E63D706622B05A863A9C9D700A13B6426D34374D8BB0E007D8AD7CC1806B152C38DAEEB75DC0C5791C03485B8B45E7ABA834026F2C1BA5554277F636F59005F99D16F3D6430C37C2D24C7115EE04648C88DE90925D5B529AD0B7C3213AC9A6516D395239B91CBEB46547053C98B1C211E64CEC295178A1B2D194A3511B82B21C651E77C7BD1A5131599C5AB4C656CF4BD3763CBD01912C2713F438A463BE24B462B473CFA7D7CD58EDA3C05256A2DB4C57CF8FCC0D2FA6249CB9A36D44F9B8C9DE9E84CF3F49F7D8C08DE9230766C1776929E2F00453D28470A350AEE005A4602812AF31926085DC9FBBFFC3A1BAB2A2297C4C98346B7D48A5D3EDBBBB666AEA8A39E3E96885F0AB67CA81BCAC292A15C6C4789A5DE863676B123DF7B3FBE355448D9AB89C5014D0CA282ECB2504B3685A239DE6603FCAA755B5175A41CA876707CC56A31E3DB98D9BA612A3009121576449BB4E5ECCDC57330813C7CF692B11037122D026BE300942A8A217891672CC41FBA185A62B3B9D67419AC15B472D6C355F830D8EB56D5C892DC58BE0ACA8C6B304CC86A56F3CACC392B1503EA73E22863E6F9C3171A9E18F0331C4C5BC9553CDB4655926A6C4E1989A330B5323F422B9FB1329DB7BBE340AC26F11C6299252EAB254260453FF7B7F4ADD0F452BE25FBE4D87AA01253DA8891D2592A416E2F791479E50ABB19E8F215DD6216B9958575128F97022525266064F9422E83F75FBDCD0F2BC2648655F0E31A17E5", + ), + ( + "309398D92B95B68A725ED55D3E72AA96717FA08A9B919DCDABE7D893A73AD7CC", + "C2FE9CB7F49BF7296785A1824CD798C377BFF4E1836473CD4A8C7423BF1D0F13", + "5DFA5E5FE41C3F4064EF11004E943E7913458659CA5A0455D9BA880815B062879BD903136D3A73D021A812A9669DF0A68A04CFB11742307919103C8FE3710A95491204970583D49726A324A6BC240272A5CF26B56C1656F996B37FB430E27CC7A30742513439535B3C5E29C302ABC00265A2D7512027969F8A50CC2B22901D16AA719906D5094C8A91A3906CBFEB852CC3A581FA33B47685957A831AF48AC244F766DE32A4A0190ED48B336E4155A9795930DC4209DA637DF59CEE3033250083E8AAB97C30C9EE75196148CE6CC556E8B54AEAE743FF9A0CC49B00D7B0AAF35387A22403A4956056FCA423244877995FC272A9A623C176C911AA9B0227AC57FB204600546E6E10C843FCAAEF8A418E6739EE4669DFC577BCA851FCBACEA3A699A25BCA3DE397ECBCC9D31C3DBD0CB461A07DCB976F900BBF44CA3127121D48127D5FC3325A0643F0233C6C45583C217CC9A17A4882260544B43CC0972B730A2948639C29B5A0F383023112FB106CD9700D011B091A83CB099C21F2B2C08B3994FDD5369D4C5AF61896BB00922B113F80699423B1BC4A5885E95A33DCBA6E8165C4BA0A7B2BB16D00FD32B20837D717CD55C91189734D889C09416C89CDE21DF4BC7787780A1B4B50A3AA66ED38539B71596E473965165BCFA701A0153A46C9AE0B6B588A136BCB814FE0A8CAE7D63D2BF3AE61EA2FCB14036A76756B0387DF7123A73620DE7ACB82AB23991508D03078D83B221C047F864C15D7BA75C722988305798203849FA0BE732B23F41084032C75F097BDA90B2FED6C51ADC56153951F0F96BC9951CA34BC1B4C972F4515234ED664738CCE4330681C36BF773C4783216F1E964A0DE4B1A1923183E232C45B6992EBB8E0D5154261274DDB1AA6D2AA37D836D51BCB77DA1934B510A22A83072707412ABA89AC81F7D9A5B38B84CCD03B56B7133373991419CC09110FA1DB151D453BEEC20CB46163DE304723D947858299615B9E9CB15553207F09772116C843A315C389F160508A001BAA76F92759DB79B4613379AD66B7986303BD17C872DC9D276482372861F06B805B03C55F251A812137CDBA5AF5C65C2D795EBF84EE48F10338AF317CC5C961AE4AE8243C5F0E", + "310AB0E7D75FD82CA23D0BAE25C4935465BA8596C6D08A44BBD6A9AAA51F741607A71238A9A24E3A878C388A8099D66A61A4474876197BD671D25A939D66B7D811541381C7C2694C53C8399F40780F5C3960366D70E76353676E0AA024ED8B9EFB81893FC1A7A06CBAD4BA2D6A16828ED5002B808ED52124BB8B21CAA53038EAA15509C1FAB3478CB39A4EA4A9644174A0F1A46E707240EA6C1B32CB90B0608370C238F96DFC404E00251F8E809DA6B72829578495E9236E1019B651B49DF920CA2697B8051C7D973538998D13A867D51AB491841BAF91A9C9F1454057937150361D7CAAFD9A9A2CF51158F659D3296BB4E30A32AC5E4AD23B3552AFD5C7CAE9F338A0191EFD4624EBA24DAF48C7453CA87ADA377C69C1DA2ACEE818CAB702434E5716695B2513F830CE797EA9EA0F5A04AFB84777ACDB7399D247797A5FD6C62368D8622B773B1C22382B5C1EABB3B09312BFE60C7A9972BDAE9AC737697B6CFC0582F938658A60E8D015F670B373D2A00021A890B656051B98E7182E08454E7F31355ADA6C56C7CF9D528AC1409A4A015B471472316610240C4570D6A2F7F297ED76A2EDA8B703427A22C056D4A549BF859612A56A5AEC469147118DEA0061E7620CE30355EAB40BA1B6AB4B39EB77061ADB9C0EA148B05BC0C7B6BF08FC91B20971AD8641051B230BA9C1A8D5C8C500231C88512361937C36CA9EF488913747C9E77346505007B55147F3540938983C4CCAC0733101C46B1A138AF1DB42F926C18CFC86A65B40ABBB25D3C4A51BDA8FC5D63CE510207B1932DE31CA0651323EC6C5F8826720C09413B6145E54CC18F9AE41B71F369B7F07207563707651797EE88A92544A46BE59063D95C1E88B7D4846C894338BDED33E1B088D81F48332C2079504147D493755D15084463D23175CFBE7A293424B66A61A71155C8C176FC7528C8A576418D577E9858E3FCBC6CC51C65040847ED6404B7A44B9E82F321C28D6176CF31C7F722B9AF7B9598348A27808A50A0374D581343EDAA74BC37187709879D17C8B853FD659645289B929603BF245AEB5EB92D655A7B3C42285C8235DFA5E5FE41C3F4064EF11004E943E7913458659CA5A0455D9BA880815B062879BD903136D3A73D021A812A9669DF0A68A04CFB11742307919103C8FE3710A95491204970583D49726A324A6BC240272A5CF26B56C1656F996B37FB430E27CC7A30742513439535B3C5E29C302ABC00265A2D7512027969F8A50CC2B22901D16AA719906D5094C8A91A3906CBFEB852CC3A581FA33B47685957A831AF48AC244F766DE32A4A0190ED48B336E4155A9795930DC4209DA637DF59CEE3033250083E8AAB97C30C9EE75196148CE6CC556E8B54AEAE743FF9A0CC49B00D7B0AAF35387A22403A4956056FCA423244877995FC272A9A623C176C911AA9B0227AC57FB204600546E6E10C843FCAAEF8A418E6739EE4669DFC577BCA851FCBACEA3A699A25BCA3DE397ECBCC9D31C3DBD0CB461A07DCB976F900BBF44CA3127121D48127D5FC3325A0643F0233C6C45583C217CC9A17A4882260544B43CC0972B730A2948639C29B5A0F383023112FB106CD9700D011B091A83CB099C21F2B2C08B3994FDD5369D4C5AF61896BB00922B113F80699423B1BC4A5885E95A33DCBA6E8165C4BA0A7B2BB16D00FD32B20837D717CD55C91189734D889C09416C89CDE21DF4BC7787780A1B4B50A3AA66ED38539B71596E473965165BCFA701A0153A46C9AE0B6B588A136BCB814FE0A8CAE7D63D2BF3AE61EA2FCB14036A76756B0387DF7123A73620DE7ACB82AB23991508D03078D83B221C047F864C15D7BA75C722988305798203849FA0BE732B23F41084032C75F097BDA90B2FED6C51ADC56153951F0F96BC9951CA34BC1B4C972F4515234ED664738CCE4330681C36BF773C4783216F1E964A0DE4B1A1923183E232C45B6992EBB8E0D5154261274DDB1AA6D2AA37D836D51BCB77DA1934B510A22A83072707412ABA89AC81F7D9A5B38B84CCD03B56B7133373991419CC09110FA1DB151D453BEEC20CB46163DE304723D947858299615B9E9CB15553207F09772116C843A315C389F160508A001BAA76F92759DB79B4613379AD66B7986303BD17C872DC9D276482372861F06B805B03C55F251A812137CDBA5AF5C65C2D795EBF84EE48F10338AF317CC5C961AE4AE8243C5F0E7FF658BC933FC07E44F8B4B3C3125FF493176D18380C6F7816B229D6D2F56EE3C2FE9CB7F49BF7296785A1824CD798C377BFF4E1836473CD4A8C7423BF1D0F13", + ), + ( + "FA58CAE89E664853035BA4EA8723C83225B89C8365024FE73A3E1B7AE01E6648", + "ABF5D4E34BE28DB2984CAEFD09F06095921B3C991EF8C4CE3B16CA2C18E0A0B2", + "40E90144374B8801711753236C08520F53BABDB82EA11B3E092B461D837C0CD95D90F7BCFF16956DF280FE69626825B26DB54E28129B07A0A32CA7726F7A93E2261EC5D604463C64669B7A7EE24A9BE822FAAC57837A4274B106C346143B0087FC5381C54040D4D897506C9C56385147F49D3FFAA51B9759B2E6A119AB7C421A8B48E0AA1C1C3C3F636349887E315A02A0B67135B732AA406090B09FE6B000A52C0E9FA737ACD1C8F4E2C31A17349AD60193950100506B957A6E381578224B37A2452B602AA9738884579B1D6DB6886364C07648B4EE6B7FA99418A891BEFDB67B7F76645F902DBEB117191073CAD23F9E227AC589708939B0FC6C60EFF7A321903224F342D4D29D78C660C1178204B80B0FA477F3F83FFDDA0AA213368EE3BD75B0C3D2C8533634AF9D94660B232775DA0E1788BCEF99B9642299775655AB63A1CC865D863834CA933A39E865198A8A93514DA351278AE67649203D773792AFC551E22367EAF48FCFB4507FF3ADBE217F4653642FA05BDC00AB3BCA8627F1ACF89040277C0DCD071136F97FBA75B59DCC3D0D22355E54335823BAC3904606D79EA845BC448C49BA2224DCAA7DDA9A583A16AAD382A8E15653AD19085FB9A2A70A1CA82ABBBDD6C05AD081E4C7686E917AF652AD5DBBC19FFC397BBB13EA7C82ACAC1A6695AABA32B39AC593774B1BBA46044459B11A7B134218C89CD24C22A49077D75BB3681F70059558916631911A05758068709671B02F04D0004FF31AB3D08FA68C63FE9BB95D4360810280DF384ECF773D0B3C97899596EFAAC05B8C816FB7877446366140C4B4150EF3F5A742076AD1A0A54100AFBCAA8531B1AD807B785F833C6F819C6DB679B0F0B087E555F8893C2ED7B6DF945DB623B21E78540F424FF66329464A8A59BA7202D8AD85131D81060928A45899F06332C0779D45A4D4D25EDD81138F9B70D3F90CDDDC2845557502413DF58C2E95692CCDD61A369A40A830A4B3E629B7BA81A1E873228109A5A24E4D6613224193201198FE6058E009BABC0757919912C0534A726304B8118831F73D66095F4E409941AB2595F994D84F674FA40EAE3C8B8F1E46F114141E6241795B3BB324521BD8E331A6B78663", + "41E1C66EE49564867327963287EA72B0347DEC961A1F651A454771586C238398AD28730AD66B9AB5433E26AC0263A92B2A233785525662191507822FA546CF4ED82493353DE20B0D9A616A79BAAFB6252A812B9363A039B7E2AD61F417D08230E247A518E022B41A63FD47874DBC5274D0C332DBCDC335B5070419D999CE8AC5077C85691D167144488EFFEC5C16482E8D285DE572980E877223903A36912F4B0A409FC17501B8A299447D6D6518F04945E209C8985CB6C207042B446F74D5AC1B7A8429A5AC60F20BEF28926CD618441A5363C2B25231989F814732D948350C481738BC567190678310ACAB5665F9C09028038B2A1CAA27709ACBA80B5487918412458472B9E8758C2051A88240FF3340A8F8C5E44664A23A0201DB9D5DBA09C5345A11EC03FF48AA651A5BBEDB32D7DC4750A0ADBF81C64EB43032E6C828698AB97BC8861A101B05937C23C6DE419EC5F635C949A865731FB6834164F04BF664848C04B6C6EB8664D02492C64D9AABC504583531B4909E492FA1B726D5BB8CE04457E6038396F352770C11ECDBCA271CA9E6704A64B7749EE8A8DE925C831C31F70550AB781768CC8713F820E291BA39C0BAE2D3C195934429F5B322E8017A584482C608655A93111583D5AB5FA887556FDBAFF6076BEC4342DD9647EB1B3091BA559880186B9C0F63606DB3B390A2714546817C0D80290188751B4885F71362A097B7A97925969B553874081C57382FB92508E81C29E7A311741C6104C3D51A7A9A8B3FAA4703F8A94899317AB5A017A211A4F369C2ACD2235A24827E6012214BA87927179678CC5A8BA309911EB1D701767B94535532882C00C99123E4818BD968B8DC4CBA6D68972AB80FBA50C0DAFA2716A6B79BF021FCCCA4E5585CBCFB77F32A2DDCB06A09F37362D83FC2179636C8A720679C1EB604811C2403D4BA8005B11B75950346CE09A22306533F0F61659423AB449A03AA6A89AF2B7B90739F7FAB0D4DE9733227663DF397A0A5771BE893EEFBC7A1570C7D206CA6143C2DA3429D1C10A5F153F512B58651AEB3EB72CABB437B19A8AFA7BC21A8A527B07440E90144374B8801711753236C08520F53BABDB82EA11B3E092B461D837C0CD95D90F7BCFF16956DF280FE69626825B26DB54E28129B07A0A32CA7726F7A93E2261EC5D604463C64669B7A7EE24A9BE822FAAC57837A4274B106C346143B0087FC5381C54040D4D897506C9C56385147F49D3FFAA51B9759B2E6A119AB7C421A8B48E0AA1C1C3C3F636349887E315A02A0B67135B732AA406090B09FE6B000A52C0E9FA737ACD1C8F4E2C31A17349AD60193950100506B957A6E381578224B37A2452B602AA9738884579B1D6DB6886364C07648B4EE6B7FA99418A891BEFDB67B7F76645F902DBEB117191073CAD23F9E227AC589708939B0FC6C60EFF7A321903224F342D4D29D78C660C1178204B80B0FA477F3F83FFDDA0AA213368EE3BD75B0C3D2C8533634AF9D94660B232775DA0E1788BCEF99B9642299775655AB63A1CC865D863834CA933A39E865198A8A93514DA351278AE67649203D773792AFC551E22367EAF48FCFB4507FF3ADBE217F4653642FA05BDC00AB3BCA8627F1ACF89040277C0DCD071136F97FBA75B59DCC3D0D22355E54335823BAC3904606D79EA845BC448C49BA2224DCAA7DDA9A583A16AAD382A8E15653AD19085FB9A2A70A1CA82ABBBDD6C05AD081E4C7686E917AF652AD5DBBC19FFC397BBB13EA7C82ACAC1A6695AABA32B39AC593774B1BBA46044459B11A7B134218C89CD24C22A49077D75BB3681F70059558916631911A05758068709671B02F04D0004FF31AB3D08FA68C63FE9BB95D4360810280DF384ECF773D0B3C97899596EFAAC05B8C816FB7877446366140C4B4150EF3F5A742076AD1A0A54100AFBCAA8531B1AD807B785F833C6F819C6DB679B0F0B087E555F8893C2ED7B6DF945DB623B21E78540F424FF66329464A8A59BA7202D8AD85131D81060928A45899F06332C0779D45A4D4D25EDD81138F9B70D3F90CDDDC2845557502413DF58C2E95692CCDD61A369A40A830A4B3E629B7BA81A1E873228109A5A24E4D6613224193201198FE6058E009BABC0757919912C0534A726304B8118831F73D66095F4E409941AB2595F994D84F674FA40EAE3C8B8F1E46F114141E6241795B3BB324521BD8E331A6B78663C8C4EFE33D072A1F4DDF63EAB34ED9F1D5B34E15A0BC3981BA572545E773CD76ABF5D4E34BE28DB2984CAEFD09F06095921B3C991EF8C4CE3B16CA2C18E0A0B2", + ), + ( + "FF466BC223EE9EBF1DF06432873A6816BE864CFE408B2D676945311E31B7BF84", + "482303CBF9E3963784E1BAC39086AADF7DE6DBB45E61A8021265A5BA2522A1D8", + "64D24F31ACBCE41742EE149B50A331F9951D92449ECEB58C85520496770C543A8E4903574700083AF5CF85E7075AB494D406C44A106CDAE48F8F5C37709CB6F8A448957991FBBAA68A7710080A9680A3527A9B2036617EF91585369AB1A8C865D58B1F8AAA6F53BC32FDDAB1AD46B3E5274C6C09654C284FD8339F2A883118E69FBD262F057A4B1B2BBE5EB8B6D1988034A6184CF881BD642CB39059B6021257E37218E4ABDF6AC13200606423BCC96B3A7A03CC5F915D3C578158734FA748BFBC1712BCCB9213BA49ECF4181B2C2DD8007DCC347CBD3C4A5FBB833468523E6B58CE607E4AEC716F8A44EB211718C89899E5AF9396BF3AD50397B2ABDA921B19CC438E890D82C14D6053C735C4C3C3165D4D58CF74775D478C4BAAF2015795B061B0C25E52B6610448F8818C00E04F5825504CA995043288F4889CFDE1129646A304A1C583358534FB0A8110995AF7434A5447E6B31448C50D3C87481B626DD17B95242935A2C5373C8A9E1207AAEB60AFC4C96AE1B790899CB5C28055723547594B918A84BA82D885B0B41F4DF755134602750233BCA285295C3022E037CBD03CD2D7B141C06015E8A456C2A373CAC509805472E57A04186BF3A6CF36C7BFE76812B20CBC60F59CB5C0B378E1609F514FE7894852A329DEC82FD763684772B0666055C512A35457287D73B557D446CAF2A3BE425F152A62B91893CEB72FCD52156C22294B6421D53074788A4C0A23AFBD1A8C5375118AE2879912CB5B198EABE46E5502197D23315730458119BBFFA64EACC12C883A545A67288CB87D11004A4EBB1F692A127BDB0CBDD54F2E83951BD77498B90F8EA192805819F8582B91804F7022B5A0B75D88792EB7A94044D4BC0A583B45A1B2E2F6024E99C7959963F1C54478181822058E55637F755978B8F46A789BAA99EB88B70AC2056A5A752AA8753B8511AA77980C4BA1392D1D9BB77216089EC0BFFCA661998801BF499E7A4B82A2035FAF273D2B28322DD71E5B342E50016D29D91B81B405C365A9EE0CA6859230B3188D4BF9BE89EB651D7768D7C4807644929E964B17E98FF5EAA35A9816BD09B7490066720FCE37A99C7F1CA6EC12E3EDE24DE9BE115230F6E56ECE1FA2", + "4B95C17A934210318070956FE0831459483F97F5016421790D6387AFE4A7FAE52461AC7A8B84425BC21A9AB9CF7E778023C69986693509F62A30617DCFE6805CE38CC034587A326692576E3857A581B79C2EB0C6CEF13217A44268036A9A911C9E52A521B03F22EB405DE4AD255211F210553587C20CE34A54738117689DA806B3D8C1AC33F7A0351598C86B1C57D97781A9B63C31C50D832FA4A25C71475CA5D03031C316E2ABBC64967B3E9A2B49F3686073CBB1D9CD02950F4E08597C423BDB386E06E782E10BCB4CBA9ED90ABBC8CAC59915CAB644329526652FFACCEB03AE02928CFD3B18DBE12C4D478FBDD56ED43C1F8305067E355D3209314BD6741DDB89811B541F2073600319F37002297601DEC38DC2E0BC5E87BAE0093C7DD27DF0A10BED4AB42CC884B19138895A6200DBCCFE2B0A7542CB27B7026FE544CECA213E78B0747373FA001E2DF3C354CA27A9818D6DE7B09AC970084B237D1791B6892EC9E8B632A167A23217A8C3981AD009CD127C67A40E3EA90D4967535BDA4A246CCDD0C103C4E23BAD2A4A2222A277A3BC2FEB769DB09D13EA26D20718D9E98E05C4B51523B834D9691B2969ABA3AC0632ABF3C26711531BCC6A54636A3422065B00F07A7EEB96124341E875B859738785126CFB2C4652E832841B38E3D50946C47B67746FF69C4855EB6D805879ACE8464880664D8BB52B35CBEC3C7F4528A826D92D7FF153AE7046E0EBBD89A16E73E554428903FF77000E70147BC7689EEC40207965AC105C1BA95CBFBA584DFC6431BA82F8F849B87CB7F55978908450958B578E3869C9348B445945E8635571CA1D237B9F2FA25BFB85477BA45D1DC974027BC903BC0B3DE111EC98722E3AC4E25497DEA304F8B4022753ACBA83BFC0187FE93C6D8271CAE796C4C701B60B4A434825AF44CB1C807C44F7655BAFEC2F4365B1357087E1096E6A6A49D2C686C52A7E0C5C07FAB6B1AB668088E9CB4B3C7316421A3281850EEB358B67A77AC40CDA765975D342B5E6BD1854583F038A56D82FDB2C8698180E0037722B2A8616B96DED13A2FD387E9B357615849C8F09B464D24F31ACBCE41742EE149B50A331F9951D92449ECEB58C85520496770C543A8E4903574700083AF5CF85E7075AB494D406C44A106CDAE48F8F5C37709CB6F8A448957991FBBAA68A7710080A9680A3527A9B2036617EF91585369AB1A8C865D58B1F8AAA6F53BC32FDDAB1AD46B3E5274C6C09654C284FD8339F2A883118E69FBD262F057A4B1B2BBE5EB8B6D1988034A6184CF881BD642CB39059B6021257E37218E4ABDF6AC13200606423BCC96B3A7A03CC5F915D3C578158734FA748BFBC1712BCCB9213BA49ECF4181B2C2DD8007DCC347CBD3C4A5FBB833468523E6B58CE607E4AEC716F8A44EB211718C89899E5AF9396BF3AD50397B2ABDA921B19CC438E890D82C14D6053C735C4C3C3165D4D58CF74775D478C4BAAF2015795B061B0C25E52B6610448F8818C00E04F5825504CA995043288F4889CFDE1129646A304A1C583358534FB0A8110995AF7434A5447E6B31448C50D3C87481B626DD17B95242935A2C5373C8A9E1207AAEB60AFC4C96AE1B790899CB5C28055723547594B918A84BA82D885B0B41F4DF755134602750233BCA285295C3022E037CBD03CD2D7B141C06015E8A456C2A373CAC509805472E57A04186BF3A6CF36C7BFE76812B20CBC60F59CB5C0B378E1609F514FE7894852A329DEC82FD763684772B0666055C512A35457287D73B557D446CAF2A3BE425F152A62B91893CEB72FCD52156C22294B6421D53074788A4C0A23AFBD1A8C5375118AE2879912CB5B198EABE46E5502197D23315730458119BBFFA64EACC12C883A545A67288CB87D11004A4EBB1F692A127BDB0CBDD54F2E83951BD77498B90F8EA192805819F8582B91804F7022B5A0B75D88792EB7A94044D4BC0A583B45A1B2E2F6024E99C7959963F1C54478181822058E55637F755978B8F46A789BAA99EB88B70AC2056A5A752AA8753B8511AA77980C4BA1392D1D9BB77216089EC0BFFCA661998801BF499E7A4B82A2035FAF273D2B28322DD71E5B342E50016D29D91B81B405C365A9EE0CA6859230B3188D4BF9BE89EB651D7768D7C4807644929E964B17E98FF5EAA35A9816BD09B7490066720FCE37A99C7F1CA6EC12E3EDE24DE9BE115230F6E56ECE1FA2B8DCD1A8B1B0A8CD37A0F09ED734E53BE83FDCA95E8680EE9816913C5BF47638482303CBF9E3963784E1BAC39086AADF7DE6DBB45E61A8021265A5BA2522A1D8", + ), ]; for (d, z, ek, dk) in cases { let d: [u8; 32] = unhex(d).try_into().unwrap(); @@ -54,11 +79,36 @@ fn kat_mlkem512_keygen() { fn kat_mlkem512_encaps() { // (ek, m, c, k) let cases: &[(&str, &str, &str, &str)] = &[ - ("86C64C4CC2B50533235A338A567B089E57BB11160254B75B91A45D31A59DDFD133CDE0B5D2776BDEC5CEB392566243CF0796809D75C429AAC4DDF4A9E58CC13999C90CF3C03DA754E6AAB9A9F22625DABE8F5C0030C796A73AB114D905FFDCBC0D135D34F79BCFA2CC550036466415E96257D56A85DA1C294C7938F3C92992B55335F3C1BDEA0350345A87E600903A3F9327858EE0C4CD20C47588B8EC613AC8076FB1483160D356C1442E9400B210B07AF2BAADA476A01D6583C23A882B889CBF055F2A31CAC0574E0423C2D22A178FB233969857742739D01A71B4903655031740065C1370464683B3916BB1F9133264A447E3E92111B99ADCF5234B1403FD8A570EA20DF29C43245A4298B20C83B9B2FAB930DBE818C3940E1A5805770874A140B12DC3BE7DF9B0FE546222645A01B93DABA66D5648C064FA89E32792FAA12C5F76747317A3304164583691AD98A01854422B322488BA6F077825CDB8C0754A3937422B16656C3D80001D760EBDCC8A3E5980F6C353CDBC89BD7C10D4A24B3614AAC13497416977F9BBB73CD742BFC94496549FD4FB4E48AC91B87B432DB64E51377A1B54C5A54662BABCA2FD68B52DE239098C067D544B7DD69FA6AB16A22143965C24AF3B0736718C2F9230E2534DD9BB9E190462577856D19C0C309931993C942ACA682CB030DDF36EE0784333593B501466B9090923190EA707313FB6179F58C42BDA8EB16C141973CE45A577F3A96E72CAABACB7B5D056BE10DA8199B28B86F58C819CAFBF6C7CE2D5B45F4023C6391E1070774F1C033AF830A8D2B1B81A8B07727C117AA3812812BDF768F25CCF06BC1AF5C058221341C4F6B27A6B7BD1177BD55323239A57DD5021B6A92485553A8C30917215BCEEF97968A11A42FC0DA997C0984C7F9C277112DCAC0FF83CCB6517D54C5EB6E2AB9520AA39E260862B0B99D40E701064821576D78AC4908B1E8FE8C4825603F0545F2059674EF6A04CAC900AEBA76BFA8D21D7036F679DCD146D0AE7C1F4C340F41B2985F32344E5BEDFFC6C286AA2EDD252B78C3A5F3576D1252C2A96B91D23453E2C9476747C845554FE4D1DC2D29FB6D8F3897FD8ABE600CF428C76E77F3AD0A034EBD24845", "19C44D35AB9EF31B1360F0BF33CF63D80E405962D698415C5888F0AF385DCFF4", "A87953F9DC2996A8DD40BE55901417A933C3D36EC09ED8A6B81C684947086C23F75172E4380023FE3C120FEEDFA142F6F149619790B64C1D5794E24C83F99ACB8BAB2F8D21FB113189E66E666DB3D154B4D51B74B0180FD14E15039FD7869371DEF178DFA8A222C779A1959C3BD34A27F52ECD8B67E614B6C39FC4106B9835D8035716EC8F20E5B266506121CB1D61519001E4DE0774B521965506327A84AAA6515C64556283B13C342E553AD30DE725DFA33C8259BD09DF059BFEB770CF1DDA98E4CE1CF7DECB19125DECC2321BFCF87142C5CA001C64E69EB3AE2C607306BB0081E34804B70850005A378C4AD5675549414122C501499FD1E72FE3D340E5B9B7E31EED7025989162BCB44B4EB496836979712F3B342E10070F3C80F66BDBB9A0551375D78905395584ED9633396EA71B76354522583DF0D26B41406C80C7C16B170595F6B467159E8E3F8E85B3D0E9B090FF52841092D64A3E970DF47EC8DD4E0574D5D7AD2DC87526F0D932C9748E4763CE8133A3862A1D1DCBE2B7F7F5B7A3800272B9C0685EB01200AD5140211A93EC47EC7FC8F652B7CEAA52F9554B6E9E49675DE47EE2AE7864AA791ECB7B7AD9DDEED8533E2493DAB662EF278340A477CCDEF6FFC1AB492DFD5D70ED17158B3138E2233C37D264D59DDFD665EA049C109CBF4BA826E097B47748953905DF8510960AC2DBD020FDE56EBF2575918706CC98DA87661F2140F6674924DF7EBF887FD6BAD36857D5C6BAF541C92D38A46C8AD84CCB636C75AD7D9BA2A5056D9F1A5BFDBCD0DB89DD9827448AFBAC84642FC21C4352C82FCA17D547C350B2B48ED5A33BA2C11D97B15B9D707DB087875ADB217EBCCCB16E84D88269B6E28F1AF93BF92CE5CC390E0C7278AAB127CBB4A8122FB2087DD6AE73B8EF69A724B55DFAFA29A25EC59B96D768931E6E6F86D4F8C42DD1F2A768C032279BC65B3C224D9680E38B4DBF1B9F5565284FFB52DE69218A32AF7CFDD10BCBAB97AA09C79FABE8E6FBAAA041AFF553C8CD561FD5CE7E619D535CB0F53062546D0A6D0E618011144968A58EA2E25ED7FE4C125EBBEDC6E1D0", "4B7B1514D1BC9808F80E3BEE7B528E13B753C99D153F7EA116A5887063BFCACF"), - ("4F601C25349B73A7CE97F53AC9736FB150C2353922861B5D69293D5A6C8E8CE29EBE5718FDCC32595B83996A43CF54BBC9184D2AA9790D23CB3101207AB190009DCE7F7682EAB1A2CDF779A23A577A6C758776CC595916107728F71A45D4C745A50649D4CB387C406E1CB53C14BCCA5C56101B8C7F63181B8D9C2730B795688A64B24A23ABB93D505493F1796469E89E9D906C90683EF76B2EE8A63E24162EC1532367CA5301857DD20940BC7A733CC8A877708DD6116F1048A0F40952E1C32BC36364EF1898EDB0296019659CC59679C259A07577984BCE54569A4A7BA14ED56B48916DFAD3338601B81209BC028CBBD1CA975158592C6C8EEDB3152E44693A9704A904773AA16A05E4361D2035761322399CCD23F3AC85AC0EFBE7C5CEA480C74045CDD03CCE56856ED5C83E520DA9D5B23B04C270FA1731E631ED23802451C536FAA3DAFC3403087063C95C902C3E5EFC150692AE2A6940AA26C887F657739573DC2248551B110CC4020D121BDA897308819778345F5037877A3246B228339D7B947B7A394D9C0A74B3423543AB43B2B53143CB849837EADB2ACDB65682EA247266CBE45435FA245F05B0A913A9B984F3B575D283EA383EA422CD6FB57937B578BD89335FF39C1BF83EDBC63D8477657D646F33625CF47A3038916984AA543003CAA384279D14595D99CEB7893818C02F3F4C6628C69C4DF490BD48074C3C5445168692EB72A2A0B992108DF67473210C883C079A4674CEF1A0BA9EFA69D2686DC9562EAEAB23B76713C4C531D330042ADC64530717353C8C1268B90D5B6FFD696AD3EA663D3CBF6D08B3D271B753D15AC49371FC891AEDFBB61FF91183D59CF8CCACA1F22D55270817D280EE9808804A0E8999C022F4ABBEA48AF649C87B8717947AB19CC28C253A5F66927998B551BA75C5D56B261D9BBD65261128D840708646EBDA299571A48FA01F8048CC67E61F96C175AAD223C9EB51814407E3D71923A4471691BCE6921FC5DC2672D859E6104A25BBCB03B78F56356CB013458FD7A9B92351113C8A42063FAB504AA33AA38D5144A1300E5198328C729439EA53EC2F5939FDB2877D542D8357E777009516D12ADAAB19B12E957A69AC6B02474E", "F7F7E81932219A3713D1DB2C9A3FF2C663502B63DEC74E774C178233BFDF92F3", "D88071CBA8DC30519C0974486140744D7E5D777400BA4462BF60844073CBEC44BD349FEACADEAEC93DEC77B21727E606D401591E36E069551EDB42A35A28D86B1BAAAD1CD0C4F311194BBCA5103980C9B65F1C798077D4AE3ADF3FFCB301024C4987A5F3825C2D561D0CB00EEF3CB078B7ADE3638BF340A71E6DF6E2DF8900F3B986E1BB0B9F14167A7AECC855F429E90EBB7785660CCA32D60C96CDE69B28B54001D25B22CA4BF1D66DD245A7292F98A4155D3887BD2971B8575403EA4EB4C77D8BAC8DA2FBF5732F1C1FC22C32F3E9701D92595E6BE185008F1881D569B8421A9CC71FAB467AFA9F2F802D014307817F225655588BC45603661182F9CAA1445E69158AF0FEC173F8DADEBE06204994915181417062511A845E683A217BF682017C72C41923BE29CAA2A3048F51199702C012CC0593D8D2891039020DD2BFEEA5C46C8E70C489BC287A134636F9578D503BAEC4F4646377312AF78099F434D502CCFFE61E861C365C38CB6151C059DE22661F03AF2891D69287F04315604CD8F00C46C422F6E3981E141EBCD8D9CDFE9E95CC0EF0C6630A4EBBEEC3BDF26E1F95A38C514526C8D2539D9E0B01273544D8ED645EFF30C15534A6EDAC1BD585EA927F3A3BD4F24E202FC49510945CC98C492687037B202F5A5ABEE1F4631FBCDA98EAD48A8038EEFC624D32D56164C54D44B4EC7878561FC2AA6EE28343074C7630669A7D4D3B099ED3E2F72551F9DF8228659378DD7663FA7BEBA45F2A6DD420C7E30C640CA49A755AA4E11EDD4225781D62FF3A89937D4427C044BAC0DE56509A5B92EC078F66EA6507D80D304E05B5A3424E9DDF2C45C9CBD475602A129C143F9C6EBFB0F446C6BABCADB9F5D7FB2F12CB3549BF2BA12F89209B1D68DE077B6CB274E635725153ADAF24B4DC9F6BB96EB934335564C08D7ED8171DF05F44A5019767C880D71CDAD9EEFD318D009FC185DCA94C213493D8D3A8BC979687728EEDB7E34A1FBF50814AF6836382A78C534871B9784FADB40F86C2FE2486F55D3A90222FFE3C6FC991646F7EFDB8C9C1C330EE07230DBB4A2433358088E4DE1925", "2F361290F7F289EED298858F894380233FEA7BD2D049346B3D975DEDF756A9DE"), - ("889538C5751FE8EA834B31CFAA172720A1C9F1B379DD4A70E5A4AA51AA6FD75B955C30077CE39993B214255A0AA9D0319EB0A9544B549BBB079BD06D6113CEED441B937B2CCC5702D1F354C7170B3ED62C635548A5DC14F7E026F0EAB84C5913AAF8A639CCA3891B8D36C97717A4A0CCC31223E80ABF20C3E48C6F8F7A09BEF5205829A86A241DCE8308BB53781ED96D61F6B0FEF5CABA932421153F755C0E868B9DC72C9648B95D631C5DC7C304C493135BA1A9CF855AAD14B4CE7992D0E58A253A452BB12A790C739D7324FB0641CE2947C69C803BEAA15B3A40CBCC61E8D26F7E5512DDA8BFDE8768F12B68E567547C09B2191AA2AC191C81D91EF2C772A838406F0494EFBCA0ACC941DA7A2E332501F49203544727C2C664364693DC33A097830F72E62BE82219AD120CA469784A64414A1604F112946B48C5B8B622FE777487E1ADB08A8D219C74622980EE88132BE4487912CB5394B0F2B9B085B138F4A575DC314ACB33C5DDB81CCDEC1FDE1A2350F5014D96AA4FF96335309688E13DA1EB90258A049BF19AD35184594AAB88813DCC517FE7394236104728FB00C54AAA950AD061D242DCA00E39108AEA90A3653265D25389D1861AC735B53BC23173E3BDF7007300C87FDD3686265940AAE061A80236275451D1C7666DABCEE95059B61C320430CB33AC90498C8160188E2BAC3F46C57A420702A2868E7E4947553350CB6711D1BC4B8D55C4719113141C749C7BC2EFD5B665C200DB65924E62625F0C07F127C646AB6551069D95AA94C3071612B3917A79907CE0C30BE7295861AD45BC0F62C34128E6538A6302B1F774FFA80D418A376B63AD36175283D7A3B3049490C605D413CF15C4077EAAAA7E067AD68A704DF4B15EE36F61F88B52E99B04EA0434782DB652C991764CB65107E2F2804B18AE6D81524569B1A01AB44C63219496BE463C308AB66B240253F7230536A84A21EC66C9148D2F22500938010B1614F3804449E18BA32B35E893AA7DD6684C15462C927CE30161ABA86FBF47519022505B5A3B33F449CBD88FE280A863E04314060585BB4B4CE1590DBBCD9AA9B01FA545E7BC294B3C9D055672AB024FE63E5398FB32888A07B6173403A375DD58", "B6BFAC644646F211A2E067003E596540E8A5389BCAA96D110FC45BEC9EC16465", "CC8D1D67DE7966C3E2A4164437C4050B82F3B8F73DF2A67233712FBFFB3EE27166958E98F09D6905506C167E7F41BF4BB201136079DAB8F60A06A49373935E5D699C47B38290C21772E689D668BBF5126BEBB7B7F6F280B092B3A48C44F8910117FF95151FE2A8E030EF2043F37B63DAC92BC7E3EEEAC7EABB60547F7A140973D8167BEC991B30F8E1F6ABEA34931CC078D1CADB0D8928633C7D370B6B61FCF85B6668518CEDB38BF6E4BE28E6329A9572A0799BCDCDA11F9BFB7F5B6E1F24471AA71D0A6E0E020D2752ABCBDF9D4272651553BEC662D80FF3F5BB71C377C03C4887F29A06A944B7D2B0E45E93E192656C08D8D4880CADD34C2339F0A19EE2B5FC05DC08582B8D67A5A461F01A5071DAE764F0589B85280F3FFF2962DDB8A0E7C9CA16CCC96A46382512E8B44C56FBC1DE2847AD40F4420996DE208BAD322C4738E20679FF19D4923F8170A6BDE9510E358ABE7A6342FAF3D1BC61086313FDA4DDA2BEC416A227B84586171359B8E24180055C6D9FBA72918EAE143BECF9289081E524DAF70199DD36B5C47FEB29EC32198ADE2DCB1BB8F8F246BD915BA8F532719B82958BC20C1AFA9A84B3B2933964B0E52254A6B7D539499E7DF69477869B0EC779654B60F751013297FC96F852F4FF605EA0C7BE5405FB99F8A53D3BACBAD24390F241A6FE19896D057FD3E1202064C3C039326424D092FE729E0557783E08F3F73E165D4B58D3C7CD778716427E31DBF8FC5F5B1477F88E5F27FFAE9774CADA3EFC56892C92839CEC1C88FFCE80BBF28711BF952E9E6344E6FFCC57E6CB2A4FF0C19EA7B414DDFC37ED915AEC580719EC43DC9DB50703CC87BA1CA8B9E87F18314BBED9A4E665BB366A5590951026C47ECB777C9E5C6C7532847E0253A63EB081382242E0D364C70C45DDDC6C758A35059498902A70592C68BCDA19BE1176F9A0F32804FDBC503653B1C3A354D2A4CAE1A2CC9DA2F9B667A3C148F8FC71A48F56F5A83ECC9A760E7896A5CA3A3F3F5EDE2C8013FCE2A2DD09690BF436688AAF3D3226B20D8CF17544E43DD1134E105B7EFC6C997DB254AA29D60864C0C3", "ECB3CE59B3DCA488E67E76350F45C88ACEC0D2A46FEFC5546BF181679BA5AFC9"), - ("402874933B4B8F9509785BC8B2C88B76A705E1A686CA2B1DB89A8CB006C92CD108BA6B6F36C2B4DA4381B26A2B578B5B12844EDA93A60CD01C39FC8CF78A563FDB57DF9453731716A5B9442274B9DC662860A644AAB6A3F2E095B8EB0A4F41957E9138033C16FADA0CFE336A7B43178C48AED4A069B038978FACCC57618BDBFA2342B44EE80372BF629205EABAC9AA261ED318B04A3A350B8FEF208F118BAD2C584846B57FCE5B239BC33692BBBD39774E1C0B7A7356BA9D53C3912B23F4629A32588C7D385960992715A67322D8091F75B6FF244AD528A3501ABD6A5C1D9510B3A7696295EAC1F2C403D7B135C16B76C1E5C682C496610B5D900B14E40485EF2BB9488A1FB1D5392E62A3C878C5CEF439CFCA433C05CB39A00283E651B0964A4580BE3FB2BC95674F2F218EDEABCA6DFC074F492111538B99665792C125A0C8073F3C13F9A5BE5EDCAC726A4DF36C8A506008E9BBCBF8D83404E2CB0EF56C00B90814546EC75444CA764F5B83AA522CBE2C3CC255EAB5FDFC681840379DB96E431848B1658FA0A2631696A9A5115BE0488DB3B6670B13CB8FBC36C4FB9CF737BC911B2C05591D6EF2B2F6A109F11129142C15BFE1045D005E551C5AEBECB05D646E6CECB602E833D64116F0D68EA9AB02A112B411D2089F93A490CC6E04684693E1CA18E84DB57024F55594CE99923D714144C8A7B4401920E5159929395E1573F4815F581429F9861F0A04A576861B20F089FE92843B47B3B02B69FAECA98E866D16638BD0A006EB642564F1C314EBC488A53DAD44A41A97A6187A829A560BAF1AC77E582D8C40AFBE511FCF068B84167E49D03F84ACAB4E05AF737598FFF3132A4A8FAC55C954D64BAB315A73CB44D9A534BC7BA78737691390BD04E300C43156240B7995F54C02CC1025608B6EE1541A9234E4B9AE368A6522E2C3D1862ECA10165A79839EC35B8E67563716CAC90B3EE2D316D22795678A151928212BF6A59D593B1E6615951B9AA48026D8B73069135642F596FF8A30E4D4813386A268063FECE408B501358CF9C11AA2A3E5D89F929643493C2513F1243C6C7C7746BCC7078D04789B0CE423B431A5916C6A4F78891AEDCD19BD0DC9806C5D8C9557DB", "91386C8B1E53CCF9F2E721CA6638C53810E36593391EDE7EC1C4C51337E81135", "1799AB6D0014EC540F42CE0E2A924D4FE7327458497FC05E555891707AFB020A41592FC104AF7675346986AF2A06DDDB39A633F2E0D57E17B657DE463A02DE5BD6FB65B2420EAFE9F56EEC9AB9625EE800E8C79CC0BB81829633C7C2779616D96C33CEEF3FE21680B5F1E801BD1C304835C1FB4D127CD8A0D2361FCE6DE73D4DEA7B6A6C15B4FA8C1D982F6AFB4EFD2D4AEC3408B6BB0FEA6263E0BECD2BD078C28249615938366316387C1E49D4BEDB8A8B781621BF30BFC185104657B04C770568BE663EBED75CB2E8E59C8A6AC4A816A059F39B6C3FA0923D1A99339A9D23674CEBC12EC65EC0B1693C250B95DC688912D4DCC3EEAB1E85191CD1085BE71860126E32389F162AEC5CA5DB0625F09FA359BD16009746212E2FC6B74A9E1812D03B8A669EB19E2D22AA81551EA87D1542B899DFACC3B4B73FEBB120682DAB1DDBC2B6C278B803D923676B5F553708CC27442A9C61E9A043A10AF056FA5864A44383ABFA7C6B1DFEA1AE16065CDCD163B4BF2CDA2D75E02768923007FBA8B4EB1CBB58100DC6132144A0FE21C1EE1A0D4D7FE26518A7DECB20B827ABC44783C6AEE158CCF20DE0C18A32AD79628E6A52033030B13CBC018BFF943465D29E07D237D5394D09A3A748B944E0F625DCD828926473D21C9DA97995699EBA32387343B41E2A20E4E98E37982C97D8664EB134699CBF2441BEE6DA19AEE9F7417D9A2602C198AF7996253203194E67E978BA508D5220B7B465BC1AFC1DCC5AAEDE0AA1F85F41E68ACB57FF5A384B906A496D562184A185E65640CD10357882E11D78F27AA64CD6F83113BE197C4DE04292257208CAD7D7A5C4AC0FE1B240A432E8338E9AF22E3C8CA3CFDD557A95616382F77498037FB40FD51268BED8560E21DD00980914FE432D6C5F383FB2E5C3F4CEA0AE5F34167C52A98A033B6A76988DC97688CE9F4057EDD5A761866F33B8BDD4D1CA327B7B9E9BD880434334153AF74483B92042E46900963E42480E714F87D951EB5334C890DF3515880F51A55C59CC456DAF38FCDC9AAB37660082B899F88061343E0C7850746F6E000D6F8AD5D675FF59", "224716A4671C4E00188A2E11B40BC337C68B54FAF32DCD3312C606E8D62BD36C"), - ("B24B8656777875B5337608AACBFA9C7AF83B01CA84640C2E0BF0C91B8B1E8B79321C1B3F89116CC9E4AA9C72B9188A30165C5B90C12B55E9B4DB8BA02837AFC2A5893B574436F879A842ABC0D5CDB5645130696ED4A332DA780FEAE3831BD88110D3202384344C857437E74733E00ED061958630A837758CAE088215F357D7FB5CA7A71EB4E84A306A56FF66091213AC399098B386CD7D229E0DC6C2FBA23F6C4737495B70A1D2452EA6B249F31DAEF3CF8BD98B4C8242D0D03E0B0828ED383F71163F84387583527EC164B8F4000A44787D10F0A3145A478ADB598550094B3C172C3276D2BC0F9501BE83809A2181B37B612560638C81A458B73A67189C2E4A93CBC4DA3EF2DB44B9522B0151A11B0A33DEFBA65E66B5F7225938A74E88910A46810121955AE1C02E66C347221438C79075D90896537A0E0EC741DA4A40622C8AD30A0999354D965B301D1AB74950C6A610CDC6A7C19F29761F8427DB113C6858C4FFCC11CE993736209E82DC9845E1701B460208343FADF25F6D3AA4D0FB5A73B38B01CC2C1339567EB611568815700AAD1E5134DA92201F4B79E079570EC2C73A2C39B34B3C254B216C220DE9F9635566943044978BF67DFA44B19FA642E0A91A7A31ACA729799594B72FE1874B9434904A8342883707E9A8521736AAFB563BF27CE2335E688CCE8B0226722AB8327C06806515E5D7A67F98158C6B1FDAC051D5E0C8D3F09F8EA22667D21ACD733313E73CDCBA6AF5D53D1F49CB4A100C2AC64BED907A99ACBED9979AD431AFE7C6C48F597C7150A51572C0297019A12755F64A50CB04216989B05496CFE9CA8DEFF131AD64713EEC4C9C78A95CA6A0C44AA0D6183D681A663E519B4589942DEA3491F80304523E0B074B19325D9DC0C82AC83719DB300E787437524DED9429424749ADD037D08039B9B33969C89672737F6E804C27C064311B0BD7BA04A95BAAC9545F29847C5390B2D6467B67756DC4B47964324A2046833C99829A8B9F550C017B2BB44426267E49BFDB5ACCED7CA7BD8198A232184FAA8FDE193A129B1546907918D11E8EF0B69ED0A48FC1C1D79258D6C03F833AD3D3F2D69A9CCA751A7276AA55EEA973F18DDD1924B0CA73F8C439", "F6B342A07A2B183D2304E7E1FA5EF054A7B4CBC377B72913EE1E085B92E4453E", "3D01A48287761DF9E36B9FCA1B0D9FF3019210451C4217045202F6FDDBBE67BF32F76F6805EDD9C98CC046BB6BEC3DA56F27C9D0C70FD92F2631ABCF57D24D73B40900B525CF2FC60099D09579668F07DDECCC9CBC109B78AA7FAF4B1448A3CF3578D77AC63EEAF0B1DAEF8D878AA1E569FDDDC12088C663A848C5DB24074F83A434DAD5B2EDF6D90BDD0EB64E6FC8C8D44DA972C475C068C5990D9A3BEF2D8489E4E70FBF397F1EEEB9615D2BA7E3FC4250F61AA7012A6E323AD2267600A4BE1285AEA9EBA1A07711FC1A896430B629B9E01F23658B4DEABC97F47CAC15208863366534EE0038BB5C1120AB277CF37A1B6A8B372942AAD1C5E5C6810B80A7BB9BB18827B2DAE757AA7A589988B4D1BE001114CE4E2BD16371F0942329D06E8D69B2CBA3D7E83077EB531647E9E9057486DED2F743990511CE3787E75E74C3D205E2449974E30895C32497F0F9E2BB967C9903E96EEB30EFA211F1F1B38B09691AED0300245108B87D6D67F0CBE00ECE2AB1F033B4889F6B8BECB452138221C226793D91A11030C4B26077FF61C4722C52E1C74D19CC4641FE8692D28D073D4237E4FBF73EB21BB5D372D6447DF35A63EF6C5591D25DC0A0444AF0F1CFFD4D6AE90C68CE83D9BE30E6DD0CEBAD247390BBBD533460DE309388A8237ED4DE2044E816AED8BD056B1067029ACA87C80FE7A67B507F2528471060504F0C50359516C17502CCCA43F67A9A3EA08123A3E4474406BF91FD285777970F9B0EF2FF30C397ABB5EF9FE334327AB22BC340931B74CE8FBB54B420A6F259B46C0C5DCD7D936A4F030F94EC5FFA96C586549A0FCA6D6FBAE0FCBCDC200A231092B037D7EF7AC2CD5820E6D8D2B4FD26E72CB3BE29B5A56F8059FF16D41283069CACF58DDC7CFCB80C05C556F913D95B0D48268923524B7F22A3D664CCBE8C615F23E311BAC018BC678E00BFA4C43C6370A5003EDB687BA688661C1540BE59D123D87952FC01ADDDD0F1C5B9E930DBDF9402E306A1C59F386614A798AB6F57F3511484338389E7CBA85843FCDD363C000DCF3F787B019377472374872F8A1415E1540E14C776", "32BC11E65F42683B177FCFC62DB781C41E44C5B852069A27B5BDDF7B093AFEA1"), + ( + "86C64C4CC2B50533235A338A567B089E57BB11160254B75B91A45D31A59DDFD133CDE0B5D2776BDEC5CEB392566243CF0796809D75C429AAC4DDF4A9E58CC13999C90CF3C03DA754E6AAB9A9F22625DABE8F5C0030C796A73AB114D905FFDCBC0D135D34F79BCFA2CC550036466415E96257D56A85DA1C294C7938F3C92992B55335F3C1BDEA0350345A87E600903A3F9327858EE0C4CD20C47588B8EC613AC8076FB1483160D356C1442E9400B210B07AF2BAADA476A01D6583C23A882B889CBF055F2A31CAC0574E0423C2D22A178FB233969857742739D01A71B4903655031740065C1370464683B3916BB1F9133264A447E3E92111B99ADCF5234B1403FD8A570EA20DF29C43245A4298B20C83B9B2FAB930DBE818C3940E1A5805770874A140B12DC3BE7DF9B0FE546222645A01B93DABA66D5648C064FA89E32792FAA12C5F76747317A3304164583691AD98A01854422B322488BA6F077825CDB8C0754A3937422B16656C3D80001D760EBDCC8A3E5980F6C353CDBC89BD7C10D4A24B3614AAC13497416977F9BBB73CD742BFC94496549FD4FB4E48AC91B87B432DB64E51377A1B54C5A54662BABCA2FD68B52DE239098C067D544B7DD69FA6AB16A22143965C24AF3B0736718C2F9230E2534DD9BB9E190462577856D19C0C309931993C942ACA682CB030DDF36EE0784333593B501466B9090923190EA707313FB6179F58C42BDA8EB16C141973CE45A577F3A96E72CAABACB7B5D056BE10DA8199B28B86F58C819CAFBF6C7CE2D5B45F4023C6391E1070774F1C033AF830A8D2B1B81A8B07727C117AA3812812BDF768F25CCF06BC1AF5C058221341C4F6B27A6B7BD1177BD55323239A57DD5021B6A92485553A8C30917215BCEEF97968A11A42FC0DA997C0984C7F9C277112DCAC0FF83CCB6517D54C5EB6E2AB9520AA39E260862B0B99D40E701064821576D78AC4908B1E8FE8C4825603F0545F2059674EF6A04CAC900AEBA76BFA8D21D7036F679DCD146D0AE7C1F4C340F41B2985F32344E5BEDFFC6C286AA2EDD252B78C3A5F3576D1252C2A96B91D23453E2C9476747C845554FE4D1DC2D29FB6D8F3897FD8ABE600CF428C76E77F3AD0A034EBD24845", + "19C44D35AB9EF31B1360F0BF33CF63D80E405962D698415C5888F0AF385DCFF4", + "A87953F9DC2996A8DD40BE55901417A933C3D36EC09ED8A6B81C684947086C23F75172E4380023FE3C120FEEDFA142F6F149619790B64C1D5794E24C83F99ACB8BAB2F8D21FB113189E66E666DB3D154B4D51B74B0180FD14E15039FD7869371DEF178DFA8A222C779A1959C3BD34A27F52ECD8B67E614B6C39FC4106B9835D8035716EC8F20E5B266506121CB1D61519001E4DE0774B521965506327A84AAA6515C64556283B13C342E553AD30DE725DFA33C8259BD09DF059BFEB770CF1DDA98E4CE1CF7DECB19125DECC2321BFCF87142C5CA001C64E69EB3AE2C607306BB0081E34804B70850005A378C4AD5675549414122C501499FD1E72FE3D340E5B9B7E31EED7025989162BCB44B4EB496836979712F3B342E10070F3C80F66BDBB9A0551375D78905395584ED9633396EA71B76354522583DF0D26B41406C80C7C16B170595F6B467159E8E3F8E85B3D0E9B090FF52841092D64A3E970DF47EC8DD4E0574D5D7AD2DC87526F0D932C9748E4763CE8133A3862A1D1DCBE2B7F7F5B7A3800272B9C0685EB01200AD5140211A93EC47EC7FC8F652B7CEAA52F9554B6E9E49675DE47EE2AE7864AA791ECB7B7AD9DDEED8533E2493DAB662EF278340A477CCDEF6FFC1AB492DFD5D70ED17158B3138E2233C37D264D59DDFD665EA049C109CBF4BA826E097B47748953905DF8510960AC2DBD020FDE56EBF2575918706CC98DA87661F2140F6674924DF7EBF887FD6BAD36857D5C6BAF541C92D38A46C8AD84CCB636C75AD7D9BA2A5056D9F1A5BFDBCD0DB89DD9827448AFBAC84642FC21C4352C82FCA17D547C350B2B48ED5A33BA2C11D97B15B9D707DB087875ADB217EBCCCB16E84D88269B6E28F1AF93BF92CE5CC390E0C7278AAB127CBB4A8122FB2087DD6AE73B8EF69A724B55DFAFA29A25EC59B96D768931E6E6F86D4F8C42DD1F2A768C032279BC65B3C224D9680E38B4DBF1B9F5565284FFB52DE69218A32AF7CFDD10BCBAB97AA09C79FABE8E6FBAAA041AFF553C8CD561FD5CE7E619D535CB0F53062546D0A6D0E618011144968A58EA2E25ED7FE4C125EBBEDC6E1D0", + "4B7B1514D1BC9808F80E3BEE7B528E13B753C99D153F7EA116A5887063BFCACF", + ), + ( + "4F601C25349B73A7CE97F53AC9736FB150C2353922861B5D69293D5A6C8E8CE29EBE5718FDCC32595B83996A43CF54BBC9184D2AA9790D23CB3101207AB190009DCE7F7682EAB1A2CDF779A23A577A6C758776CC595916107728F71A45D4C745A50649D4CB387C406E1CB53C14BCCA5C56101B8C7F63181B8D9C2730B795688A64B24A23ABB93D505493F1796469E89E9D906C90683EF76B2EE8A63E24162EC1532367CA5301857DD20940BC7A733CC8A877708DD6116F1048A0F40952E1C32BC36364EF1898EDB0296019659CC59679C259A07577984BCE54569A4A7BA14ED56B48916DFAD3338601B81209BC028CBBD1CA975158592C6C8EEDB3152E44693A9704A904773AA16A05E4361D2035761322399CCD23F3AC85AC0EFBE7C5CEA480C74045CDD03CCE56856ED5C83E520DA9D5B23B04C270FA1731E631ED23802451C536FAA3DAFC3403087063C95C902C3E5EFC150692AE2A6940AA26C887F657739573DC2248551B110CC4020D121BDA897308819778345F5037877A3246B228339D7B947B7A394D9C0A74B3423543AB43B2B53143CB849837EADB2ACDB65682EA247266CBE45435FA245F05B0A913A9B984F3B575D283EA383EA422CD6FB57937B578BD89335FF39C1BF83EDBC63D8477657D646F33625CF47A3038916984AA543003CAA384279D14595D99CEB7893818C02F3F4C6628C69C4DF490BD48074C3C5445168692EB72A2A0B992108DF67473210C883C079A4674CEF1A0BA9EFA69D2686DC9562EAEAB23B76713C4C531D330042ADC64530717353C8C1268B90D5B6FFD696AD3EA663D3CBF6D08B3D271B753D15AC49371FC891AEDFBB61FF91183D59CF8CCACA1F22D55270817D280EE9808804A0E8999C022F4ABBEA48AF649C87B8717947AB19CC28C253A5F66927998B551BA75C5D56B261D9BBD65261128D840708646EBDA299571A48FA01F8048CC67E61F96C175AAD223C9EB51814407E3D71923A4471691BCE6921FC5DC2672D859E6104A25BBCB03B78F56356CB013458FD7A9B92351113C8A42063FAB504AA33AA38D5144A1300E5198328C729439EA53EC2F5939FDB2877D542D8357E777009516D12ADAAB19B12E957A69AC6B02474E", + "F7F7E81932219A3713D1DB2C9A3FF2C663502B63DEC74E774C178233BFDF92F3", + "D88071CBA8DC30519C0974486140744D7E5D777400BA4462BF60844073CBEC44BD349FEACADEAEC93DEC77B21727E606D401591E36E069551EDB42A35A28D86B1BAAAD1CD0C4F311194BBCA5103980C9B65F1C798077D4AE3ADF3FFCB301024C4987A5F3825C2D561D0CB00EEF3CB078B7ADE3638BF340A71E6DF6E2DF8900F3B986E1BB0B9F14167A7AECC855F429E90EBB7785660CCA32D60C96CDE69B28B54001D25B22CA4BF1D66DD245A7292F98A4155D3887BD2971B8575403EA4EB4C77D8BAC8DA2FBF5732F1C1FC22C32F3E9701D92595E6BE185008F1881D569B8421A9CC71FAB467AFA9F2F802D014307817F225655588BC45603661182F9CAA1445E69158AF0FEC173F8DADEBE06204994915181417062511A845E683A217BF682017C72C41923BE29CAA2A3048F51199702C012CC0593D8D2891039020DD2BFEEA5C46C8E70C489BC287A134636F9578D503BAEC4F4646377312AF78099F434D502CCFFE61E861C365C38CB6151C059DE22661F03AF2891D69287F04315604CD8F00C46C422F6E3981E141EBCD8D9CDFE9E95CC0EF0C6630A4EBBEEC3BDF26E1F95A38C514526C8D2539D9E0B01273544D8ED645EFF30C15534A6EDAC1BD585EA927F3A3BD4F24E202FC49510945CC98C492687037B202F5A5ABEE1F4631FBCDA98EAD48A8038EEFC624D32D56164C54D44B4EC7878561FC2AA6EE28343074C7630669A7D4D3B099ED3E2F72551F9DF8228659378DD7663FA7BEBA45F2A6DD420C7E30C640CA49A755AA4E11EDD4225781D62FF3A89937D4427C044BAC0DE56509A5B92EC078F66EA6507D80D304E05B5A3424E9DDF2C45C9CBD475602A129C143F9C6EBFB0F446C6BABCADB9F5D7FB2F12CB3549BF2BA12F89209B1D68DE077B6CB274E635725153ADAF24B4DC9F6BB96EB934335564C08D7ED8171DF05F44A5019767C880D71CDAD9EEFD318D009FC185DCA94C213493D8D3A8BC979687728EEDB7E34A1FBF50814AF6836382A78C534871B9784FADB40F86C2FE2486F55D3A90222FFE3C6FC991646F7EFDB8C9C1C330EE07230DBB4A2433358088E4DE1925", + "2F361290F7F289EED298858F894380233FEA7BD2D049346B3D975DEDF756A9DE", + ), + ( + "889538C5751FE8EA834B31CFAA172720A1C9F1B379DD4A70E5A4AA51AA6FD75B955C30077CE39993B214255A0AA9D0319EB0A9544B549BBB079BD06D6113CEED441B937B2CCC5702D1F354C7170B3ED62C635548A5DC14F7E026F0EAB84C5913AAF8A639CCA3891B8D36C97717A4A0CCC31223E80ABF20C3E48C6F8F7A09BEF5205829A86A241DCE8308BB53781ED96D61F6B0FEF5CABA932421153F755C0E868B9DC72C9648B95D631C5DC7C304C493135BA1A9CF855AAD14B4CE7992D0E58A253A452BB12A790C739D7324FB0641CE2947C69C803BEAA15B3A40CBCC61E8D26F7E5512DDA8BFDE8768F12B68E567547C09B2191AA2AC191C81D91EF2C772A838406F0494EFBCA0ACC941DA7A2E332501F49203544727C2C664364693DC33A097830F72E62BE82219AD120CA469784A64414A1604F112946B48C5B8B622FE777487E1ADB08A8D219C74622980EE88132BE4487912CB5394B0F2B9B085B138F4A575DC314ACB33C5DDB81CCDEC1FDE1A2350F5014D96AA4FF96335309688E13DA1EB90258A049BF19AD35184594AAB88813DCC517FE7394236104728FB00C54AAA950AD061D242DCA00E39108AEA90A3653265D25389D1861AC735B53BC23173E3BDF7007300C87FDD3686265940AAE061A80236275451D1C7666DABCEE95059B61C320430CB33AC90498C8160188E2BAC3F46C57A420702A2868E7E4947553350CB6711D1BC4B8D55C4719113141C749C7BC2EFD5B665C200DB65924E62625F0C07F127C646AB6551069D95AA94C3071612B3917A79907CE0C30BE7295861AD45BC0F62C34128E6538A6302B1F774FFA80D418A376B63AD36175283D7A3B3049490C605D413CF15C4077EAAAA7E067AD68A704DF4B15EE36F61F88B52E99B04EA0434782DB652C991764CB65107E2F2804B18AE6D81524569B1A01AB44C63219496BE463C308AB66B240253F7230536A84A21EC66C9148D2F22500938010B1614F3804449E18BA32B35E893AA7DD6684C15462C927CE30161ABA86FBF47519022505B5A3B33F449CBD88FE280A863E04314060585BB4B4CE1590DBBCD9AA9B01FA545E7BC294B3C9D055672AB024FE63E5398FB32888A07B6173403A375DD58", + "B6BFAC644646F211A2E067003E596540E8A5389BCAA96D110FC45BEC9EC16465", + "CC8D1D67DE7966C3E2A4164437C4050B82F3B8F73DF2A67233712FBFFB3EE27166958E98F09D6905506C167E7F41BF4BB201136079DAB8F60A06A49373935E5D699C47B38290C21772E689D668BBF5126BEBB7B7F6F280B092B3A48C44F8910117FF95151FE2A8E030EF2043F37B63DAC92BC7E3EEEAC7EABB60547F7A140973D8167BEC991B30F8E1F6ABEA34931CC078D1CADB0D8928633C7D370B6B61FCF85B6668518CEDB38BF6E4BE28E6329A9572A0799BCDCDA11F9BFB7F5B6E1F24471AA71D0A6E0E020D2752ABCBDF9D4272651553BEC662D80FF3F5BB71C377C03C4887F29A06A944B7D2B0E45E93E192656C08D8D4880CADD34C2339F0A19EE2B5FC05DC08582B8D67A5A461F01A5071DAE764F0589B85280F3FFF2962DDB8A0E7C9CA16CCC96A46382512E8B44C56FBC1DE2847AD40F4420996DE208BAD322C4738E20679FF19D4923F8170A6BDE9510E358ABE7A6342FAF3D1BC61086313FDA4DDA2BEC416A227B84586171359B8E24180055C6D9FBA72918EAE143BECF9289081E524DAF70199DD36B5C47FEB29EC32198ADE2DCB1BB8F8F246BD915BA8F532719B82958BC20C1AFA9A84B3B2933964B0E52254A6B7D539499E7DF69477869B0EC779654B60F751013297FC96F852F4FF605EA0C7BE5405FB99F8A53D3BACBAD24390F241A6FE19896D057FD3E1202064C3C039326424D092FE729E0557783E08F3F73E165D4B58D3C7CD778716427E31DBF8FC5F5B1477F88E5F27FFAE9774CADA3EFC56892C92839CEC1C88FFCE80BBF28711BF952E9E6344E6FFCC57E6CB2A4FF0C19EA7B414DDFC37ED915AEC580719EC43DC9DB50703CC87BA1CA8B9E87F18314BBED9A4E665BB366A5590951026C47ECB777C9E5C6C7532847E0253A63EB081382242E0D364C70C45DDDC6C758A35059498902A70592C68BCDA19BE1176F9A0F32804FDBC503653B1C3A354D2A4CAE1A2CC9DA2F9B667A3C148F8FC71A48F56F5A83ECC9A760E7896A5CA3A3F3F5EDE2C8013FCE2A2DD09690BF436688AAF3D3226B20D8CF17544E43DD1134E105B7EFC6C997DB254AA29D60864C0C3", + "ECB3CE59B3DCA488E67E76350F45C88ACEC0D2A46FEFC5546BF181679BA5AFC9", + ), + ( + "402874933B4B8F9509785BC8B2C88B76A705E1A686CA2B1DB89A8CB006C92CD108BA6B6F36C2B4DA4381B26A2B578B5B12844EDA93A60CD01C39FC8CF78A563FDB57DF9453731716A5B9442274B9DC662860A644AAB6A3F2E095B8EB0A4F41957E9138033C16FADA0CFE336A7B43178C48AED4A069B038978FACCC57618BDBFA2342B44EE80372BF629205EABAC9AA261ED318B04A3A350B8FEF208F118BAD2C584846B57FCE5B239BC33692BBBD39774E1C0B7A7356BA9D53C3912B23F4629A32588C7D385960992715A67322D8091F75B6FF244AD528A3501ABD6A5C1D9510B3A7696295EAC1F2C403D7B135C16B76C1E5C682C496610B5D900B14E40485EF2BB9488A1FB1D5392E62A3C878C5CEF439CFCA433C05CB39A00283E651B0964A4580BE3FB2BC95674F2F218EDEABCA6DFC074F492111538B99665792C125A0C8073F3C13F9A5BE5EDCAC726A4DF36C8A506008E9BBCBF8D83404E2CB0EF56C00B90814546EC75444CA764F5B83AA522CBE2C3CC255EAB5FDFC681840379DB96E431848B1658FA0A2631696A9A5115BE0488DB3B6670B13CB8FBC36C4FB9CF737BC911B2C05591D6EF2B2F6A109F11129142C15BFE1045D005E551C5AEBECB05D646E6CECB602E833D64116F0D68EA9AB02A112B411D2089F93A490CC6E04684693E1CA18E84DB57024F55594CE99923D714144C8A7B4401920E5159929395E1573F4815F581429F9861F0A04A576861B20F089FE92843B47B3B02B69FAECA98E866D16638BD0A006EB642564F1C314EBC488A53DAD44A41A97A6187A829A560BAF1AC77E582D8C40AFBE511FCF068B84167E49D03F84ACAB4E05AF737598FFF3132A4A8FAC55C954D64BAB315A73CB44D9A534BC7BA78737691390BD04E300C43156240B7995F54C02CC1025608B6EE1541A9234E4B9AE368A6522E2C3D1862ECA10165A79839EC35B8E67563716CAC90B3EE2D316D22795678A151928212BF6A59D593B1E6615951B9AA48026D8B73069135642F596FF8A30E4D4813386A268063FECE408B501358CF9C11AA2A3E5D89F929643493C2513F1243C6C7C7746BCC7078D04789B0CE423B431A5916C6A4F78891AEDCD19BD0DC9806C5D8C9557DB", + "91386C8B1E53CCF9F2E721CA6638C53810E36593391EDE7EC1C4C51337E81135", + "1799AB6D0014EC540F42CE0E2A924D4FE7327458497FC05E555891707AFB020A41592FC104AF7675346986AF2A06DDDB39A633F2E0D57E17B657DE463A02DE5BD6FB65B2420EAFE9F56EEC9AB9625EE800E8C79CC0BB81829633C7C2779616D96C33CEEF3FE21680B5F1E801BD1C304835C1FB4D127CD8A0D2361FCE6DE73D4DEA7B6A6C15B4FA8C1D982F6AFB4EFD2D4AEC3408B6BB0FEA6263E0BECD2BD078C28249615938366316387C1E49D4BEDB8A8B781621BF30BFC185104657B04C770568BE663EBED75CB2E8E59C8A6AC4A816A059F39B6C3FA0923D1A99339A9D23674CEBC12EC65EC0B1693C250B95DC688912D4DCC3EEAB1E85191CD1085BE71860126E32389F162AEC5CA5DB0625F09FA359BD16009746212E2FC6B74A9E1812D03B8A669EB19E2D22AA81551EA87D1542B899DFACC3B4B73FEBB120682DAB1DDBC2B6C278B803D923676B5F553708CC27442A9C61E9A043A10AF056FA5864A44383ABFA7C6B1DFEA1AE16065CDCD163B4BF2CDA2D75E02768923007FBA8B4EB1CBB58100DC6132144A0FE21C1EE1A0D4D7FE26518A7DECB20B827ABC44783C6AEE158CCF20DE0C18A32AD79628E6A52033030B13CBC018BFF943465D29E07D237D5394D09A3A748B944E0F625DCD828926473D21C9DA97995699EBA32387343B41E2A20E4E98E37982C97D8664EB134699CBF2441BEE6DA19AEE9F7417D9A2602C198AF7996253203194E67E978BA508D5220B7B465BC1AFC1DCC5AAEDE0AA1F85F41E68ACB57FF5A384B906A496D562184A185E65640CD10357882E11D78F27AA64CD6F83113BE197C4DE04292257208CAD7D7A5C4AC0FE1B240A432E8338E9AF22E3C8CA3CFDD557A95616382F77498037FB40FD51268BED8560E21DD00980914FE432D6C5F383FB2E5C3F4CEA0AE5F34167C52A98A033B6A76988DC97688CE9F4057EDD5A761866F33B8BDD4D1CA327B7B9E9BD880434334153AF74483B92042E46900963E42480E714F87D951EB5334C890DF3515880F51A55C59CC456DAF38FCDC9AAB37660082B899F88061343E0C7850746F6E000D6F8AD5D675FF59", + "224716A4671C4E00188A2E11B40BC337C68B54FAF32DCD3312C606E8D62BD36C", + ), + ( + "B24B8656777875B5337608AACBFA9C7AF83B01CA84640C2E0BF0C91B8B1E8B79321C1B3F89116CC9E4AA9C72B9188A30165C5B90C12B55E9B4DB8BA02837AFC2A5893B574436F879A842ABC0D5CDB5645130696ED4A332DA780FEAE3831BD88110D3202384344C857437E74733E00ED061958630A837758CAE088215F357D7FB5CA7A71EB4E84A306A56FF66091213AC399098B386CD7D229E0DC6C2FBA23F6C4737495B70A1D2452EA6B249F31DAEF3CF8BD98B4C8242D0D03E0B0828ED383F71163F84387583527EC164B8F4000A44787D10F0A3145A478ADB598550094B3C172C3276D2BC0F9501BE83809A2181B37B612560638C81A458B73A67189C2E4A93CBC4DA3EF2DB44B9522B0151A11B0A33DEFBA65E66B5F7225938A74E88910A46810121955AE1C02E66C347221438C79075D90896537A0E0EC741DA4A40622C8AD30A0999354D965B301D1AB74950C6A610CDC6A7C19F29761F8427DB113C6858C4FFCC11CE993736209E82DC9845E1701B460208343FADF25F6D3AA4D0FB5A73B38B01CC2C1339567EB611568815700AAD1E5134DA92201F4B79E079570EC2C73A2C39B34B3C254B216C220DE9F9635566943044978BF67DFA44B19FA642E0A91A7A31ACA729799594B72FE1874B9434904A8342883707E9A8521736AAFB563BF27CE2335E688CCE8B0226722AB8327C06806515E5D7A67F98158C6B1FDAC051D5E0C8D3F09F8EA22667D21ACD733313E73CDCBA6AF5D53D1F49CB4A100C2AC64BED907A99ACBED9979AD431AFE7C6C48F597C7150A51572C0297019A12755F64A50CB04216989B05496CFE9CA8DEFF131AD64713EEC4C9C78A95CA6A0C44AA0D6183D681A663E519B4589942DEA3491F80304523E0B074B19325D9DC0C82AC83719DB300E787437524DED9429424749ADD037D08039B9B33969C89672737F6E804C27C064311B0BD7BA04A95BAAC9545F29847C5390B2D6467B67756DC4B47964324A2046833C99829A8B9F550C017B2BB44426267E49BFDB5ACCED7CA7BD8198A232184FAA8FDE193A129B1546907918D11E8EF0B69ED0A48FC1C1D79258D6C03F833AD3D3F2D69A9CCA751A7276AA55EEA973F18DDD1924B0CA73F8C439", + "F6B342A07A2B183D2304E7E1FA5EF054A7B4CBC377B72913EE1E085B92E4453E", + "3D01A48287761DF9E36B9FCA1B0D9FF3019210451C4217045202F6FDDBBE67BF32F76F6805EDD9C98CC046BB6BEC3DA56F27C9D0C70FD92F2631ABCF57D24D73B40900B525CF2FC60099D09579668F07DDECCC9CBC109B78AA7FAF4B1448A3CF3578D77AC63EEAF0B1DAEF8D878AA1E569FDDDC12088C663A848C5DB24074F83A434DAD5B2EDF6D90BDD0EB64E6FC8C8D44DA972C475C068C5990D9A3BEF2D8489E4E70FBF397F1EEEB9615D2BA7E3FC4250F61AA7012A6E323AD2267600A4BE1285AEA9EBA1A07711FC1A896430B629B9E01F23658B4DEABC97F47CAC15208863366534EE0038BB5C1120AB277CF37A1B6A8B372942AAD1C5E5C6810B80A7BB9BB18827B2DAE757AA7A589988B4D1BE001114CE4E2BD16371F0942329D06E8D69B2CBA3D7E83077EB531647E9E9057486DED2F743990511CE3787E75E74C3D205E2449974E30895C32497F0F9E2BB967C9903E96EEB30EFA211F1F1B38B09691AED0300245108B87D6D67F0CBE00ECE2AB1F033B4889F6B8BECB452138221C226793D91A11030C4B26077FF61C4722C52E1C74D19CC4641FE8692D28D073D4237E4FBF73EB21BB5D372D6447DF35A63EF6C5591D25DC0A0444AF0F1CFFD4D6AE90C68CE83D9BE30E6DD0CEBAD247390BBBD533460DE309388A8237ED4DE2044E816AED8BD056B1067029ACA87C80FE7A67B507F2528471060504F0C50359516C17502CCCA43F67A9A3EA08123A3E4474406BF91FD285777970F9B0EF2FF30C397ABB5EF9FE334327AB22BC340931B74CE8FBB54B420A6F259B46C0C5DCD7D936A4F030F94EC5FFA96C586549A0FCA6D6FBAE0FCBCDC200A231092B037D7EF7AC2CD5820E6D8D2B4FD26E72CB3BE29B5A56F8059FF16D41283069CACF58DDC7CFCB80C05C556F913D95B0D48268923524B7F22A3D664CCBE8C615F23E311BAC018BC678E00BFA4C43C6370A5003EDB687BA688661C1540BE59D123D87952FC01ADDDD0F1C5B9E930DBDF9402E306A1C59F386614A798AB6F57F3511484338389E7CBA85843FCDD363C000DCF3F787B019377472374872F8A1415E1540E14C776", + "32BC11E65F42683B177FCFC62DB781C41E44C5B852069A27B5BDDF7B093AFEA1", + ), ]; for (ek, m, c, k) in cases { let ek: [u8; 800] = unhex(ek).try_into().unwrap(); @@ -74,25 +124,65 @@ fn kat_mlkem512_decaps() { // (dk, c, k) — 정상 복호화 및 변조 암호문(암묵적 거부) 혼합 let cases: &[(&str, &str, &str)] = &[ // valid decapsulation - ("BE26809AA451F5925CE5B1311789AFE966C34EA9B677EC50624109CE3A85FE560478E09B99D16C57D612CD8BAF90486B48170ADDB370A0C613CBE3BC04785F8B2B688215BFEEC192EB39CD0239751484161685A8197987BD600D72A0BE008C4BA9C56B9C495952435F29716C3C69660BDBBB4F66652A3A7EA08C6B8175C0D7C563C4060EADB1987322A2467C7E672BB54B2187E37952B6BC6C001B5DAA969E86F2120284213156A80F0BA258545549778D6D65AFBAA9CB073549AFF577CAB079C625236AE5C003158D0ACA16A91A9F45C32FDC182B800798DC2540A7011294D81EA3644467238C78C84D892A61133016E4768042A14A2C13598B51600B219C0C1B4E88C979DDBCBA52B836FA4095C2E47943A27010A58BB4065929EA6E33648CF9D2A121871838B39FFB3291E5781A06B1266542CE2A85A8BEBA7287703085C4B808E7C15448272FB37E9A04297CCC26EAC658CC30A4FD4C72F00070F04AC4BA30486D8396C149A4053C39A2C00A33CBC41FB563E7B38AE1913032311617D45BC83C59DC6845CE991D65BC442A39AF5C7A5638EA9BA93678E28A73EC24C032B4796F44385E2492E02734A5EA3972B5CA726A431C3C41ABFC1B637311F781629E69C0291A5AFF8AA8258129186CAF17AA4BBC927F76045076853437B95B0FD531C488C336D43504F95C82B970DD19202973C8CB12C468A9A17D644EAAD75ECEE7440B9901DE7998BDDC737014C1461137EFA28D32DCCF4AFC9B5089B6067A3D727510085950446AAA2A272CBC06B3D0E236DB180377810C435AAC5A60553BD0AE140969D8D2CDEB78B3D097540C6A9EE2613296A5AA4D6BCAFAFC4580896529E15308BC05BDD36A5DD9B6EF989080C2431ADCB35D8357097CAC6EB7BDAD905FD668A3ECF690DA814456933E6089641F252DAA90799E39AB6F037F79C5B13750005E202B98BB50C4D94B512533F4C2A67B3C31B2E03E691AA41867B8D0C93A73A27CA49A2DDCDC0188FC1965B579E516AAB43527D1D69DC723BCA288C5FB73C99A399A7D4107C0E76E00C7C9DC52095BB925E24A2575420BB3F4CDEB798CF377469405C22FB76F6C154B35E49DA6119D72DC139BB52A057B6E4F4A177FE88568B06D94C0A59562BC2BEA86EA7CB1F078223E7743ED8B6DCFA2CBE64699C773A5ED0C0935917B9E3015DB102C8B98A05559211B563EA1B0B7012C6332262FFFC09E779A1068380E9FE1B0E0948D00B8883F96A3D555A52D18687491916065A9E56A075464B625507560829CB94A2FC57C7CBC96864C0201F1D34BB21B0133A50C213077C7DC4CA0F871D1FB4AA8C99D7CD245607954A3E45DFBB24C320B97DD1231CA5A44EC041BE9D15504A72DB425CADAF57AD0E72CCBAB789FACAB053377F56B891FF7932F3A2A0B618544CBCB99158246F183E313443CC35BCEC71E9D30954C404D84540BE4152B3794A582953207813685A05F52624BD7926CD3812D53CAAAB2C392FFB26BB3A06F7A1AC1D0F145C9434B86695B58A673B4955ADA329D5B572152727CA3963F54898D39F38DF578965436025BD8C1C6342629BBA98B300FBE8AAC379BCE1212A247BB452701A82810B825066BE058CB78DB9E83A3ACC6637955411A7FA3C2AB7692A279A210CB9040CA1B36B65041B1A8AF874701DC3036035A96D22B38F4CBEDF72777C61C3248328B51A191690A9508B6A1185F0B890A91F0CF43E4127C51509E8B5AE6588863F72595A7524708A2A6A385A385BF750BBCD3B7326BA99F37F6402E1978AE963AE207CF1FAC8028024AF63CB6CA32298626CC8D935B72D8556024824260BA2E427C3930002023B7EFD41693E44D4482AF9A4463023869DE85820C848520D2B465905433848A3589ABFD4C39DB311B93B34086D12C4C8243518A75C14A69DA9235A6FA305258BF7BB5ADA8A428F6063FA1D437C4B36394727FC06957B8D6BB21F409969290E1346698A25E61937AE1E23BDFE09A53B437F172A57BB61FCC569746054601415E6E315648E03DA71239BF869E1F669B8838A486FB2E4F489DC8E2747DF4849B83B165E95D23E4CCCCB5C414934549B9046F359E06B6ABFEFA0E4745A346451EF8791D5582AB2DB22EBDD1126568930264C19F1BA747296B40E284A45459398587975C88071CBBF6C9275DC8C81C661A020443C39208359AF0A721A303479A6DA0357402827F09653C7A2CCDDD78688324A1678F678D49AE2658E0DDD0DA7FD25FF8375BD9708415D6BD80AA8C23425D1B45CFECFDADFEFBA627B7F88C03E56DF0227510BC47AE6EBA8B92C1A8", "FCDD461B68BB4A153F9203BD9822D4C4F6305ADA8786C564DA4F31518F8D4AB51D9485673921777D439482DA99D70482D0C3EAC998541CEC67759A19865D3EB42F3B7BA83180F6E9968EA84747A8B0E7C5CC3159A7BE5D471BAC5313C055BBB731CC73565E90FC5A64DA299C38C632C054A1E3EC1F13D0D332F8437F273F149CA419A922BCCB0AAF454162282A0080C6C31530C9A620A682069309B039264FB7A82057047ADEFE3C598C8FD17BFBB48CBDF7DF4326C69519615A6D81A797107532FE503ED0F8C7FEAE197E329C05D4041DCB6B3DA4428112FF2702994682BFF8FB7E6268162D3C85BB40574D8FC256335DA513E5C1BC06271DD2219674864BB1F51BD2CEED41ED425BF27A27B9CE312EA9AC0753D102D15DE9B3A668C2E154817A8AE7B6F2F5D0A38A0E6A46E06E1309CC9FF1879F6A5C8D7F3D56EBCA1BCCEF42ED57EDD349F321C07DA2F5D9242E6F31B9A0EA25BB904E19E7B771068EFAE868CCC4EBD72CBA16865698AA64DCC33D7BFAC4D8C472D31A7AA65733B54CB41E79CF16BBFC930EDD78CA21817DD5A61066CFF379363B5E43DE320066736879D5C4F16F4431D71AD9EF7E18D3166AF1D0A2C7126BBB542F182F20FD1A4AC88A218A46A849797A6C459029AF2E61C9707C7F142F333D3ED061872449D00F9744693BE1118F0597A61F1A808976504A3FE1F9A4582547AD780EA05A318FDDEC654F0826D1344BD986238A214840257B6E857C9A032579179FC374174E640AB5540865DE606948FB3A02EA70742BA7A08617B6FB01239F4D5138BB7A28836368E1E6CA63BFD9C3924261289BF6C1F9C3C040595CD52E4643F6C2726F3102E308494F4B33E312D59345279333A2AF49C157B4FE55D42083CBF436C66E0031609ED4BDED3DD7A54C98938BFB41A4F4DAF3A96B20E29F54BB7CC6742AB563467DA923D682E91ABA279A616E52DAAC56D07919697A5CB173FA24F09BC778AB1103F6F03719D088C549B18ED72ADF6738BC82BF4370AE9A7BBBCFB1C78717E11354DCF42654675AA0A45108F5B67D2CC77DC200A0F97BAF667A242627298E57BE6C761EC4", "9B0E2A90368B07C5A94CD4E880EEE7814E1703D09369DCB9D3C2AA079FD9F75A"), + ( + "BE26809AA451F5925CE5B1311789AFE966C34EA9B677EC50624109CE3A85FE560478E09B99D16C57D612CD8BAF90486B48170ADDB370A0C613CBE3BC04785F8B2B688215BFEEC192EB39CD0239751484161685A8197987BD600D72A0BE008C4BA9C56B9C495952435F29716C3C69660BDBBB4F66652A3A7EA08C6B8175C0D7C563C4060EADB1987322A2467C7E672BB54B2187E37952B6BC6C001B5DAA969E86F2120284213156A80F0BA258545549778D6D65AFBAA9CB073549AFF577CAB079C625236AE5C003158D0ACA16A91A9F45C32FDC182B800798DC2540A7011294D81EA3644467238C78C84D892A61133016E4768042A14A2C13598B51600B219C0C1B4E88C979DDBCBA52B836FA4095C2E47943A27010A58BB4065929EA6E33648CF9D2A121871838B39FFB3291E5781A06B1266542CE2A85A8BEBA7287703085C4B808E7C15448272FB37E9A04297CCC26EAC658CC30A4FD4C72F00070F04AC4BA30486D8396C149A4053C39A2C00A33CBC41FB563E7B38AE1913032311617D45BC83C59DC6845CE991D65BC442A39AF5C7A5638EA9BA93678E28A73EC24C032B4796F44385E2492E02734A5EA3972B5CA726A431C3C41ABFC1B637311F781629E69C0291A5AFF8AA8258129186CAF17AA4BBC927F76045076853437B95B0FD531C488C336D43504F95C82B970DD19202973C8CB12C468A9A17D644EAAD75ECEE7440B9901DE7998BDDC737014C1461137EFA28D32DCCF4AFC9B5089B6067A3D727510085950446AAA2A272CBC06B3D0E236DB180377810C435AAC5A60553BD0AE140969D8D2CDEB78B3D097540C6A9EE2613296A5AA4D6BCAFAFC4580896529E15308BC05BDD36A5DD9B6EF989080C2431ADCB35D8357097CAC6EB7BDAD905FD668A3ECF690DA814456933E6089641F252DAA90799E39AB6F037F79C5B13750005E202B98BB50C4D94B512533F4C2A67B3C31B2E03E691AA41867B8D0C93A73A27CA49A2DDCDC0188FC1965B579E516AAB43527D1D69DC723BCA288C5FB73C99A399A7D4107C0E76E00C7C9DC52095BB925E24A2575420BB3F4CDEB798CF377469405C22FB76F6C154B35E49DA6119D72DC139BB52A057B6E4F4A177FE88568B06D94C0A59562BC2BEA86EA7CB1F078223E7743ED8B6DCFA2CBE64699C773A5ED0C0935917B9E3015DB102C8B98A05559211B563EA1B0B7012C6332262FFFC09E779A1068380E9FE1B0E0948D00B8883F96A3D555A52D18687491916065A9E56A075464B625507560829CB94A2FC57C7CBC96864C0201F1D34BB21B0133A50C213077C7DC4CA0F871D1FB4AA8C99D7CD245607954A3E45DFBB24C320B97DD1231CA5A44EC041BE9D15504A72DB425CADAF57AD0E72CCBAB789FACAB053377F56B891FF7932F3A2A0B618544CBCB99158246F183E313443CC35BCEC71E9D30954C404D84540BE4152B3794A582953207813685A05F52624BD7926CD3812D53CAAAB2C392FFB26BB3A06F7A1AC1D0F145C9434B86695B58A673B4955ADA329D5B572152727CA3963F54898D39F38DF578965436025BD8C1C6342629BBA98B300FBE8AAC379BCE1212A247BB452701A82810B825066BE058CB78DB9E83A3ACC6637955411A7FA3C2AB7692A279A210CB9040CA1B36B65041B1A8AF874701DC3036035A96D22B38F4CBEDF72777C61C3248328B51A191690A9508B6A1185F0B890A91F0CF43E4127C51509E8B5AE6588863F72595A7524708A2A6A385A385BF750BBCD3B7326BA99F37F6402E1978AE963AE207CF1FAC8028024AF63CB6CA32298626CC8D935B72D8556024824260BA2E427C3930002023B7EFD41693E44D4482AF9A4463023869DE85820C848520D2B465905433848A3589ABFD4C39DB311B93B34086D12C4C8243518A75C14A69DA9235A6FA305258BF7BB5ADA8A428F6063FA1D437C4B36394727FC06957B8D6BB21F409969290E1346698A25E61937AE1E23BDFE09A53B437F172A57BB61FCC569746054601415E6E315648E03DA71239BF869E1F669B8838A486FB2E4F489DC8E2747DF4849B83B165E95D23E4CCCCB5C414934549B9046F359E06B6ABFEFA0E4745A346451EF8791D5582AB2DB22EBDD1126568930264C19F1BA747296B40E284A45459398587975C88071CBBF6C9275DC8C81C661A020443C39208359AF0A721A303479A6DA0357402827F09653C7A2CCDDD78688324A1678F678D49AE2658E0DDD0DA7FD25FF8375BD9708415D6BD80AA8C23425D1B45CFECFDADFEFBA627B7F88C03E56DF0227510BC47AE6EBA8B92C1A8", + "FCDD461B68BB4A153F9203BD9822D4C4F6305ADA8786C564DA4F31518F8D4AB51D9485673921777D439482DA99D70482D0C3EAC998541CEC67759A19865D3EB42F3B7BA83180F6E9968EA84747A8B0E7C5CC3159A7BE5D471BAC5313C055BBB731CC73565E90FC5A64DA299C38C632C054A1E3EC1F13D0D332F8437F273F149CA419A922BCCB0AAF454162282A0080C6C31530C9A620A682069309B039264FB7A82057047ADEFE3C598C8FD17BFBB48CBDF7DF4326C69519615A6D81A797107532FE503ED0F8C7FEAE197E329C05D4041DCB6B3DA4428112FF2702994682BFF8FB7E6268162D3C85BB40574D8FC256335DA513E5C1BC06271DD2219674864BB1F51BD2CEED41ED425BF27A27B9CE312EA9AC0753D102D15DE9B3A668C2E154817A8AE7B6F2F5D0A38A0E6A46E06E1309CC9FF1879F6A5C8D7F3D56EBCA1BCCEF42ED57EDD349F321C07DA2F5D9242E6F31B9A0EA25BB904E19E7B771068EFAE868CCC4EBD72CBA16865698AA64DCC33D7BFAC4D8C472D31A7AA65733B54CB41E79CF16BBFC930EDD78CA21817DD5A61066CFF379363B5E43DE320066736879D5C4F16F4431D71AD9EF7E18D3166AF1D0A2C7126BBB542F182F20FD1A4AC88A218A46A849797A6C459029AF2E61C9707C7F142F333D3ED061872449D00F9744693BE1118F0597A61F1A808976504A3FE1F9A4582547AD780EA05A318FDDEC654F0826D1344BD986238A214840257B6E857C9A032579179FC374174E640AB5540865DE606948FB3A02EA70742BA7A08617B6FB01239F4D5138BB7A28836368E1E6CA63BFD9C3924261289BF6C1F9C3C040595CD52E4643F6C2726F3102E308494F4B33E312D59345279333A2AF49C157B4FE55D42083CBF436C66E0031609ED4BDED3DD7A54C98938BFB41A4F4DAF3A96B20E29F54BB7CC6742AB563467DA923D682E91ABA279A616E52DAAC56D07919697A5CB173FA24F09BC778AB1103F6F03719D088C549B18ED72ADF6738BC82BF4370AE9A7BBBCFB1C78717E11354DCF42654675AA0A45108F5B67D2CC77DC200A0F97BAF667A242627298E57BE6C761EC4", + "9B0E2A90368B07C5A94CD4E880EEE7814E1703D09369DCB9D3C2AA079FD9F75A", + ), // valid decapsulation - ("7950925EF032BF6CC75C330365606AB5C4955E124636FA896833932D16444A2C393975592028A3849694C2D59A6AB5B6167A983762B94717345BE71DBB07409C12609C99BE4D863BA5685A326B8D14F6B68DFBB994633669933FB97354C2E1C416F1A68295838882B4D6903FFFF9AFFEE3CBACF4A3C0D3BB15246631EC851E750FBC784ACA065610970996CB058FD0A97D9440A61C5370F54EF90777E93B73C3325730B5A86543B5994B304BC000CE08BCE1FB2F84BA1A868090D43224B82368888A3C22502E31D664044144782B3F82F927B9C8C59326288124C4EE0482FD011A098B0464358C694A28064178FF622A7152024BFC9D4B308A0F92347C6BB8A8671E723BB32F22B2585341BEE8286DC50AA8AB705A85B3798029AE168AAB23790A9C64B2693F4A001777F94EB7E4096866422F4903B474256DC244A6EB80C1C736C2206C0EB121A5742C2247212AC95F8D7B82F375300CF0015746902A3C5E5DC88C8C57BBBD372148328D59460852B72A896411F6290554F3594893CE2E37066000963D394C5AC85F7E261FBFE32F85236D338A82B5BA3A48A87B7DAB24FC164AD33A05ED1B6BE59A17884B79AD8C5F60767F11341F6C40AFBB92A05F39537C4886FAC92186E407AB63246E928244FC4DBEE1AA8E745DE195B53CA694B675CB75378D63956C26CB5AF4CA24613478E4B844D0925C3EF1C8DA98B265C31794D264A4B87E524C5F18E2C2C6F9B0DDC5957ECB9AD4F9AD961692EB044B665480DC6604E3B96128544F92108DB1CBCAE03AC2DC8A46CA16BBA3784563CB69C34B053EBC1FC2A590A976AB4E20939D604428044FC70552411C0AD5220B46AA09FAD75E16173976460A99C632FBD01633A7963D0C22AC91985BC20F16A36A2FE5A1CB6886AD736833F20941BA426CE172A2429E4B39A960C138D9E18254E7835ED91AFB7BCBD613C509073A1699026A381F585B1A6B457F13D9BFC9F4433B069878BA561145737F0A2E3D0B20ACC39EAA5597065202C316BF445697F3E9B71A2AAD49617161C39571920438B88C7B5A9B3B85C2ABA799F8CC5D99D6A91CF7506E15BAF9F042F3089E7CE381133992D984856F5968F0870FDAB7481C217A0EE50FDC0A694AB194D8223CB7541451CB666BC08B410AC9ADDAA778CB068B10056F28773E4231F0406D488B3E3B398AAF6128E3C781D271A9B0E68F64A5C5C276532E3C7F5DF20694429E69047D533BA2DAF135AECBBD5CF647266584BBDA9B2B17B732B4A0A6F732A753626442005D91B1485C6EC6E7ADFDC54A852942CB29355BD665432946B9683C85F63193A61ABE6B03C70385E8516AED4ACE9C4A7B6BC188A2C1824964131CD322620BC6FB393465453BEC3284A8E25431C626DAF3BBF6B085600772F4786833AB562B6337AA016B50304599E589AFA490B219CEC74657067A1AB5E5AD7AC2774155BBF7BA87FED18366F7769A7C160D86617F0A4D5DCA445A3C9D329B755914B8E9B3BEE22AA817014DD9473768F8133C837895076528486CC6A4A9ED5679614502781993AB477E50C613373218F3C488DCA6CCAA162C2D965C95851BF8EBC864B67450DA62F67901EA2A4114117E96C91F39122D39BC03D8B03CBC12B1ECCB07F53473F6D714A998388388B628B659C156658AC2A2F7156A60ECB44D930A5985AD58255E72666B9301879DA0047808A31DA2C14178C15464967EB2542629A772BA4F324BB6D605790D8AA6E86802337A34097BA778D409988408FBC977921A06E3425930461EF0CA3C0B23495DBC530E692EE100305F099F23E69B5ACA990721BA642C744BEA1759A5484FC5985ED506A3036C88E7A68D4A50C4EBAFD81C3D798473581694FE6A8B985C0F617011AFA4B1430BB984C3A96B800EB55BCAE91C0BB111317BC9CC92A4C78463B5C27267FC15408671158C47346A99818BF70ECF6912C15AA883795ACDE69FC772B879E12FE7A91872A5BB86767DAD1C84A476207A19B47236C2DDDACA75536E05A96C109B9F21C6B9E43170045CA22387452B4B21D5515624753698112BBFE563E7A8059601C86E3030B36B8DA1BCB1B4155C83FB9485D70E181B946F3921F8F7CB4649742BEA4D2954599C3A498430899590C83883A2AA8075DAA81A11A1A5E8B37903D61A288B8D84328724068538CFD7199A9B8C3008474F01297BD3B7F6A3CFE284A0A618AB4A8B790CCB690A3D93A6FF6D066B48CB9A9AF45F8D5F165C72CA6DE8C202A19729F46FF824B6400B689B27F86964629F3D9DA45D0026DA4F0C41878EEA5BEB8A36113341DC823C", "088EEC18E0639F82AF4A8C4E577E90C31716693C6AF31BE04A69B2751023DEA988991D919FDA0BC84E746ACA5CBF3D4A335C8718F1698F89E379F38F4AB02199F761CE4E1A80BA2B25289578F642614A70077735A3FCCF5948FFCFE2B8AECA166141432653D8E2E6E5EAE0BF455B48E5184285BE0685FE446F9ABE7FD0B721E0A3D34A30E984CC51FEBD52AF0CC58F9D8E65C23CAF01A0F90C0F36EC782E7F7143E387F97ED2332BA10F4AF29CAD22633883BBBAD58267B39C1A6F780DD6C55C6DC3627BFB4B81DA52A00DFE1E995081F5FA46C1CAEAA204B12B127E2B2B9D714A97EA2C941DC483DE3A8E6CC02435D22976FF9313A30C7B0A04433CE6E5BBBC9DF83208F2BEFA36DE3812BF8B3BFD9DAB4CA0EDF1D570E37E166B031A68F9135A01025B21D4833F57EC1906DA73BAE21FC2DCD18AE8DEC0E1E40CE66F0AA39014AF53C93509EE4A6ECAFECFFE668A8C8679F1D57E0865F12106718716E56F16069792F2D0419A70A3B0B7A2025E211CF4B7C5228685887802D3FD65E46B28B3350D318486D3881C6C84A1E0ED487C3D77A56F34C0A005454C5DAE77716C744DE1CC8EE96ED963EEAAC584B5B92D77D1F2DA7655AED5A1CDBA3E45FF9521A89F2AC05716CD5B11D91F7E2E1BF3587647566ADB251097C398A1E47ADFBE0207D5A44C6E35E4838D12424D97872476ACCF545B4E7B28046F5EC6C8DAA03140107AEFE9BFBBC1DF101A343E9D00B6ECC46A1DA91D1242DABB7070F06C5F72574FF2EF171EE9849A5174534CDF13F85E2D12ECE4BB358F9F5E5EFAD8EEA86CB01082922A4092878AB7B7E4C72E78081896E2634C64126512152CAA0D4B448A1BA357712DFAE926B6CE15DD9349F66DEA45DF545D4532072BD198CFD2F4533F2C85DF017B1133B5D41D012194CF5EA5302F6FE10D638F462FA4AA5429BA453AA3F5CC4BEF22B5CE7C005BA1DA45877F64EEF3D68AE271DB2865F2A0BB560AAF8127895144B30F4A3338029B589CF23F9D652E6E6F2DEB8858396C35AAD1FEDFFE843174E0AA86B1E4E98ACCC50D1C1B64A7B48E93C5A7A08B2B6D9E700F1C1B9479D7", "C238B69181CFB9E4C08D2B886B716301B4B216D5213BDDBE158C778D7C90FBE1"), + ( + "7950925EF032BF6CC75C330365606AB5C4955E124636FA896833932D16444A2C393975592028A3849694C2D59A6AB5B6167A983762B94717345BE71DBB07409C12609C99BE4D863BA5685A326B8D14F6B68DFBB994633669933FB97354C2E1C416F1A68295838882B4D6903FFFF9AFFEE3CBACF4A3C0D3BB15246631EC851E750FBC784ACA065610970996CB058FD0A97D9440A61C5370F54EF90777E93B73C3325730B5A86543B5994B304BC000CE08BCE1FB2F84BA1A868090D43224B82368888A3C22502E31D664044144782B3F82F927B9C8C59326288124C4EE0482FD011A098B0464358C694A28064178FF622A7152024BFC9D4B308A0F92347C6BB8A8671E723BB32F22B2585341BEE8286DC50AA8AB705A85B3798029AE168AAB23790A9C64B2693F4A001777F94EB7E4096866422F4903B474256DC244A6EB80C1C736C2206C0EB121A5742C2247212AC95F8D7B82F375300CF0015746902A3C5E5DC88C8C57BBBD372148328D59460852B72A896411F6290554F3594893CE2E37066000963D394C5AC85F7E261FBFE32F85236D338A82B5BA3A48A87B7DAB24FC164AD33A05ED1B6BE59A17884B79AD8C5F60767F11341F6C40AFBB92A05F39537C4886FAC92186E407AB63246E928244FC4DBEE1AA8E745DE195B53CA694B675CB75378D63956C26CB5AF4CA24613478E4B844D0925C3EF1C8DA98B265C31794D264A4B87E524C5F18E2C2C6F9B0DDC5957ECB9AD4F9AD961692EB044B665480DC6604E3B96128544F92108DB1CBCAE03AC2DC8A46CA16BBA3784563CB69C34B053EBC1FC2A590A976AB4E20939D604428044FC70552411C0AD5220B46AA09FAD75E16173976460A99C632FBD01633A7963D0C22AC91985BC20F16A36A2FE5A1CB6886AD736833F20941BA426CE172A2429E4B39A960C138D9E18254E7835ED91AFB7BCBD613C509073A1699026A381F585B1A6B457F13D9BFC9F4433B069878BA561145737F0A2E3D0B20ACC39EAA5597065202C316BF445697F3E9B71A2AAD49617161C39571920438B88C7B5A9B3B85C2ABA799F8CC5D99D6A91CF7506E15BAF9F042F3089E7CE381133992D984856F5968F0870FDAB7481C217A0EE50FDC0A694AB194D8223CB7541451CB666BC08B410AC9ADDAA778CB068B10056F28773E4231F0406D488B3E3B398AAF6128E3C781D271A9B0E68F64A5C5C276532E3C7F5DF20694429E69047D533BA2DAF135AECBBD5CF647266584BBDA9B2B17B732B4A0A6F732A753626442005D91B1485C6EC6E7ADFDC54A852942CB29355BD665432946B9683C85F63193A61ABE6B03C70385E8516AED4ACE9C4A7B6BC188A2C1824964131CD322620BC6FB393465453BEC3284A8E25431C626DAF3BBF6B085600772F4786833AB562B6337AA016B50304599E589AFA490B219CEC74657067A1AB5E5AD7AC2774155BBF7BA87FED18366F7769A7C160D86617F0A4D5DCA445A3C9D329B755914B8E9B3BEE22AA817014DD9473768F8133C837895076528486CC6A4A9ED5679614502781993AB477E50C613373218F3C488DCA6CCAA162C2D965C95851BF8EBC864B67450DA62F67901EA2A4114117E96C91F39122D39BC03D8B03CBC12B1ECCB07F53473F6D714A998388388B628B659C156658AC2A2F7156A60ECB44D930A5985AD58255E72666B9301879DA0047808A31DA2C14178C15464967EB2542629A772BA4F324BB6D605790D8AA6E86802337A34097BA778D409988408FBC977921A06E3425930461EF0CA3C0B23495DBC530E692EE100305F099F23E69B5ACA990721BA642C744BEA1759A5484FC5985ED506A3036C88E7A68D4A50C4EBAFD81C3D798473581694FE6A8B985C0F617011AFA4B1430BB984C3A96B800EB55BCAE91C0BB111317BC9CC92A4C78463B5C27267FC15408671158C47346A99818BF70ECF6912C15AA883795ACDE69FC772B879E12FE7A91872A5BB86767DAD1C84A476207A19B47236C2DDDACA75536E05A96C109B9F21C6B9E43170045CA22387452B4B21D5515624753698112BBFE563E7A8059601C86E3030B36B8DA1BCB1B4155C83FB9485D70E181B946F3921F8F7CB4649742BEA4D2954599C3A498430899590C83883A2AA8075DAA81A11A1A5E8B37903D61A288B8D84328724068538CFD7199A9B8C3008474F01297BD3B7F6A3CFE284A0A618AB4A8B790CCB690A3D93A6FF6D066B48CB9A9AF45F8D5F165C72CA6DE8C202A19729F46FF824B6400B689B27F86964629F3D9DA45D0026DA4F0C41878EEA5BEB8A36113341DC823C", + "088EEC18E0639F82AF4A8C4E577E90C31716693C6AF31BE04A69B2751023DEA988991D919FDA0BC84E746ACA5CBF3D4A335C8718F1698F89E379F38F4AB02199F761CE4E1A80BA2B25289578F642614A70077735A3FCCF5948FFCFE2B8AECA166141432653D8E2E6E5EAE0BF455B48E5184285BE0685FE446F9ABE7FD0B721E0A3D34A30E984CC51FEBD52AF0CC58F9D8E65C23CAF01A0F90C0F36EC782E7F7143E387F97ED2332BA10F4AF29CAD22633883BBBAD58267B39C1A6F780DD6C55C6DC3627BFB4B81DA52A00DFE1E995081F5FA46C1CAEAA204B12B127E2B2B9D714A97EA2C941DC483DE3A8E6CC02435D22976FF9313A30C7B0A04433CE6E5BBBC9DF83208F2BEFA36DE3812BF8B3BFD9DAB4CA0EDF1D570E37E166B031A68F9135A01025B21D4833F57EC1906DA73BAE21FC2DCD18AE8DEC0E1E40CE66F0AA39014AF53C93509EE4A6ECAFECFFE668A8C8679F1D57E0865F12106718716E56F16069792F2D0419A70A3B0B7A2025E211CF4B7C5228685887802D3FD65E46B28B3350D318486D3881C6C84A1E0ED487C3D77A56F34C0A005454C5DAE77716C744DE1CC8EE96ED963EEAAC584B5B92D77D1F2DA7655AED5A1CDBA3E45FF9521A89F2AC05716CD5B11D91F7E2E1BF3587647566ADB251097C398A1E47ADFBE0207D5A44C6E35E4838D12424D97872476ACCF545B4E7B28046F5EC6C8DAA03140107AEFE9BFBBC1DF101A343E9D00B6ECC46A1DA91D1242DABB7070F06C5F72574FF2EF171EE9849A5174534CDF13F85E2D12ECE4BB358F9F5E5EFAD8EEA86CB01082922A4092878AB7B7E4C72E78081896E2634C64126512152CAA0D4B448A1BA357712DFAE926B6CE15DD9349F66DEA45DF545D4532072BD198CFD2F4533F2C85DF017B1133B5D41D012194CF5EA5302F6FE10D638F462FA4AA5429BA453AA3F5CC4BEF22B5CE7C005BA1DA45877F64EEF3D68AE271DB2865F2A0BB560AAF8127895144B30F4A3338029B589CF23F9D652E6E6F2DEB8858396C35AAD1FEDFFE843174E0AA86B1E4E98ACCC50D1C1B64A7B48E93C5A7A08B2B6D9E700F1C1B9479D7", + "C238B69181CFB9E4C08D2B886B716301B4B216D5213BDDBE158C778D7C90FBE1", + ), // valid decapsulation - ("7D3C9A9E5A8C69843DA2C4803F42CC058B1823797BBFB695CB90547A932833E3B5AC09935E117CA1C012F8A573EEDCB2666274B25425C1826ECBB83C0415BA3E8AABAFB18DD6DA0AF107604847547D404A8463C743241AE6D03B95E5490F140BE0F8095E74825EDBB6BF040F5D61372F416D2E565D116766F0156EFFA2C59EAAAFD4F695F7E0888749108D967755E84B6B7379D90788094B6053F21BC3536F2903C816D21756877673F529BCA61BE121194A06BF53825982C76D10BB5A772543E44A005606B631835499CA06C1E6C4A967CA2D008EDD5A86A37335DE0C303A19947D7320CFB4ACD0CB369A80B6FF66788F97B293E7004C17B9DFE55A9248381B898DE2FBA92AF7ABE9B749B5868852A564E24A54C822CD740AC7DB9C46C204C1AD435FC1B89C378845913B7A49F912BDB60F7A57CA261C25A188981007745FB7C71443850961C729DCC6885C989DD50B226B15275635991CC58FC88179966AE9C314AFD6413BC1579CF2CCE0216F636636734C2A87996171E7B35F50C00E19AB18A11BA5E492DC85AB82805408B551531544241CBE43A42102764BDEC83E88D210FB2B00ADDA8848C520A347903C6304F76AB2CB311AC650BA16A28DD605C8A92413A3A43B81819242B74089F5B4F14BA49E2642C8FA13BC06A0D9323E49B05472CA0BD3CA20CA548E2C8457A9445B77E318852B9B32EC6C956A242E07B58A183EEA68978389412E3B7D7EC22788051E34DB1ABC1B3E2427872626CAF556B05BCB634160830B70A3CF84CA4F8757668020B9A55DB92221C7B02CBB96703BD12EA9755254EB80C7A6C45B8B8CDECB4730F86ADEE24A965BAF6076535FAC13346407FCB1C199F2C8D3010EF2CC1E7F15ADEB622F37804CFA6612C2EACDEFEB284C588F5E2B5B07A158BDC6B45DA90D825B20F0DB1551245FDA7C7B73C7C6DEF65A89828EC8701CA36CC10B4279EA9BCA19CC700FA92E83D09BA7E92253C9CC07BB3989EB6DFA668279B71D40F61D13417523638F98D7700F448E14DC1163A479953B5724340A446A6A2CD48CE96B048CD01255B850333172FE860938598170DB73BDFCB1D211209CF13D866CBD971818AB1110888B25A74B27FF37774951996DA25F7170AFC98843D7F48BDD1041B9D2A70D0B8989E03D8FF3B931F8BE07E118D546445857C28378BC52985E66DB50A7B632909338579279D8862047EA3A1061357E9534F5E04BBB001A87E3BE084890C5CA171406C68C9058DE0415CB823ED2536B0BFC466A7455FC227CE8B2AC758A00E16380BC82737F3B196817204EEA531D8245AE51445754897DF78E9EEB73753BBAA658087977627F659332F5AA8F838A0E7960CAA1BE8B1BC2E661347EF5AA9B945CC3B49AD6352FACB486EC108027DC712289667E98B17666C1E3129C5E5C14C2B1221577C784223393EBAFAD1AC5E1281B6805606A76CA719B3CD9FB0B53856CCFA8A484B4313138564A5A835DCB5043F729B59606D71756E6790315E07E8C6A72816C8CCF2285C1DBB61D6A68EDD065DEBCA9CE5C54860711B386BC62E63C0AAACDD36BAAD690C6D4B7696681464C27984C902BA2CC03A07B7657A12AAEA670E4839F4544B2008481D32043FA5B7A9C4C9912E3CBF82715260A3E39D9AFE3F06972D44707A4B65E61587D3C650BD04F09BB9B0BD6745CAC69A5053734477A857B469910A11AFACDA38B2B77489BEB0B3573C163B1A199DBE85742468C1C5C29FA272D3FFC181802B76DD994C228255CB1512CCA4B26A60B1F40B5B2E3898EB4927A039A9BE7C1CD91900612C00028A55585835A5A846A162B3909501B07C76D48407A922CDB4A483957594F949B59A3CAD7E40AAA80BB9C88AABD15BF600B59B653B067B363AF65858F822BBA02411CD10FADEC5515900465D3B8EA1035AE4BA20E396BD0980C1D37C8BBF79EBB5741E3391DA4FBB55F6BB63A7C1EAE327685D953456A39B3E416557C41BA8237F8C7624402C992C37C86C5B73EB7B4CF34750A5A705A2B1EC137428938B8BFDC0FCDA27E7FE460EC316C05FA15BC19144A088ACE681DF636829F204016491978B4A315682763B32A88D08AC31C4F6EA1C2A2C93B3AE5894B4AA6F0752D70B59412533690C64FF97CB62F0B84B328C9AD287C64B6437EA12FE7D24E2E068C5C9B189F972B25F2BFF1B165BCB569768A1A1128D348947665F386F1BA674D44CD2BE8B54C233C4FE6EEC1160B9FBD4603702809F490273505FE2B746D702B189120806B9376DF9B7146A700B6F9CECCEFDA52BB64A8C34F1D139B21864C68B0C3F2D8", "E5416C65B1F75EDAF39CC240B4FF6239889F62784EA2A45D13BFB0A3221E58069028CE710050495FC50C402990A19F54845509610DD8FDAB7028D9C980206A7487865A6EDEC55CABEADEC8F1BCF75E9DF67F7DA0F62787475E95E719A562BB0A54631D333E5CCDDF10CBCB2F431719855EF68C149713276DA41CCABE67A8A57765E02B98D7117494D0F13AD04C161D4ACA485310070D34A280AF99DD687DC0DF116EEC29C03F3A5E0CDEB9F37F583BF818A68E410684229B03E6B70A21EC297460A411250D44CF51E34237A41AD5CA438EF0E6E2D17E0ABF80C7077456EE2B7B787B78B2FF1BCC8BC07FB220A99AC01447E8F3E63A2E99AB0AEC5E3E615786433E9CD336EFEC7152CD85DBA3A8F1227A90F81FFFC75E48C82C43BF54321868158137DC1464F5A6BDE2D3C2E9BAE1E3862BC44585830C4CC31703D96E38536CD3F3B16BEE749EC318DBE982FFC6EE06996E8167DA6D46A052FC0CADD06A6E18C5202379068E6ED0EEDC6B242010C3848ADE2955C25963CBA56B26372FBE2B262E6DC07FA046C08E2F3EBE66C95FB172C0F123AAFC6A0DEC44C2DD493899A9B98A6BB9B51A074D5153621297580B3103D542D4078B270E7874A839E6F3FCECE4D34527EE80D3E3AAA550BF73AE7C7388BF12212A20A3B4B8BCAD265609CF476D164FC65DCBA0D82B4469D2617E8D2A0F1F09EC3F17997A0C555BE924A778EA63FAA59B505881DC267EB415388F67B3BF54D180C8AFA7BF49888D59C7E0B750DCFBB6C7364432CCBB5B628208466726C75ECA8B58AFE0DA8B4ACC1CC7EC58CC78E6A95B812DAA638EB9CB93F0EA2870E39A3BB1FD1C818741B94E55783160EE5168FD6AF194C1D2A502D698FEE396575DC95DA36182D2DAB495EBF1F84B1A349A528BF2FE9BBEB8F8B2B94EA80EE9D5B6823F48A2B547469DAF656C12D0C0BA30D0D69CE84FE5CA168D2B61DC4607487E8B925E1139F71078728B052FA08AA39A4FA0C3AAD2A55BE4D7937B47CE1739E27B57DDDC91F7666E0BFDD9FB04BE2F61886103E23EE8826E983C9042733F8D59C4C877F467198AB09823BB091353C857B0", "64FA974EB768A0646EA038266B6A79814C9FA2E2C7A25A903FC37183E3C71FF8"), + ( + "7D3C9A9E5A8C69843DA2C4803F42CC058B1823797BBFB695CB90547A932833E3B5AC09935E117CA1C012F8A573EEDCB2666274B25425C1826ECBB83C0415BA3E8AABAFB18DD6DA0AF107604847547D404A8463C743241AE6D03B95E5490F140BE0F8095E74825EDBB6BF040F5D61372F416D2E565D116766F0156EFFA2C59EAAAFD4F695F7E0888749108D967755E84B6B7379D90788094B6053F21BC3536F2903C816D21756877673F529BCA61BE121194A06BF53825982C76D10BB5A772543E44A005606B631835499CA06C1E6C4A967CA2D008EDD5A86A37335DE0C303A19947D7320CFB4ACD0CB369A80B6FF66788F97B293E7004C17B9DFE55A9248381B898DE2FBA92AF7ABE9B749B5868852A564E24A54C822CD740AC7DB9C46C204C1AD435FC1B89C378845913B7A49F912BDB60F7A57CA261C25A188981007745FB7C71443850961C729DCC6885C989DD50B226B15275635991CC58FC88179966AE9C314AFD6413BC1579CF2CCE0216F636636734C2A87996171E7B35F50C00E19AB18A11BA5E492DC85AB82805408B551531544241CBE43A42102764BDEC83E88D210FB2B00ADDA8848C520A347903C6304F76AB2CB311AC650BA16A28DD605C8A92413A3A43B81819242B74089F5B4F14BA49E2642C8FA13BC06A0D9323E49B05472CA0BD3CA20CA548E2C8457A9445B77E318852B9B32EC6C956A242E07B58A183EEA68978389412E3B7D7EC22788051E34DB1ABC1B3E2427872626CAF556B05BCB634160830B70A3CF84CA4F8757668020B9A55DB92221C7B02CBB96703BD12EA9755254EB80C7A6C45B8B8CDECB4730F86ADEE24A965BAF6076535FAC13346407FCB1C199F2C8D3010EF2CC1E7F15ADEB622F37804CFA6612C2EACDEFEB284C588F5E2B5B07A158BDC6B45DA90D825B20F0DB1551245FDA7C7B73C7C6DEF65A89828EC8701CA36CC10B4279EA9BCA19CC700FA92E83D09BA7E92253C9CC07BB3989EB6DFA668279B71D40F61D13417523638F98D7700F448E14DC1163A479953B5724340A446A6A2CD48CE96B048CD01255B850333172FE860938598170DB73BDFCB1D211209CF13D866CBD971818AB1110888B25A74B27FF37774951996DA25F7170AFC98843D7F48BDD1041B9D2A70D0B8989E03D8FF3B931F8BE07E118D546445857C28378BC52985E66DB50A7B632909338579279D8862047EA3A1061357E9534F5E04BBB001A87E3BE084890C5CA171406C68C9058DE0415CB823ED2536B0BFC466A7455FC227CE8B2AC758A00E16380BC82737F3B196817204EEA531D8245AE51445754897DF78E9EEB73753BBAA658087977627F659332F5AA8F838A0E7960CAA1BE8B1BC2E661347EF5AA9B945CC3B49AD6352FACB486EC108027DC712289667E98B17666C1E3129C5E5C14C2B1221577C784223393EBAFAD1AC5E1281B6805606A76CA719B3CD9FB0B53856CCFA8A484B4313138564A5A835DCB5043F729B59606D71756E6790315E07E8C6A72816C8CCF2285C1DBB61D6A68EDD065DEBCA9CE5C54860711B386BC62E63C0AAACDD36BAAD690C6D4B7696681464C27984C902BA2CC03A07B7657A12AAEA670E4839F4544B2008481D32043FA5B7A9C4C9912E3CBF82715260A3E39D9AFE3F06972D44707A4B65E61587D3C650BD04F09BB9B0BD6745CAC69A5053734477A857B469910A11AFACDA38B2B77489BEB0B3573C163B1A199DBE85742468C1C5C29FA272D3FFC181802B76DD994C228255CB1512CCA4B26A60B1F40B5B2E3898EB4927A039A9BE7C1CD91900612C00028A55585835A5A846A162B3909501B07C76D48407A922CDB4A483957594F949B59A3CAD7E40AAA80BB9C88AABD15BF600B59B653B067B363AF65858F822BBA02411CD10FADEC5515900465D3B8EA1035AE4BA20E396BD0980C1D37C8BBF79EBB5741E3391DA4FBB55F6BB63A7C1EAE327685D953456A39B3E416557C41BA8237F8C7624402C992C37C86C5B73EB7B4CF34750A5A705A2B1EC137428938B8BFDC0FCDA27E7FE460EC316C05FA15BC19144A088ACE681DF636829F204016491978B4A315682763B32A88D08AC31C4F6EA1C2A2C93B3AE5894B4AA6F0752D70B59412533690C64FF97CB62F0B84B328C9AD287C64B6437EA12FE7D24E2E068C5C9B189F972B25F2BFF1B165BCB569768A1A1128D348947665F386F1BA674D44CD2BE8B54C233C4FE6EEC1160B9FBD4603702809F490273505FE2B746D702B189120806B9376DF9B7146A700B6F9CECCEFDA52BB64A8C34F1D139B21864C68B0C3F2D8", + "E5416C65B1F75EDAF39CC240B4FF6239889F62784EA2A45D13BFB0A3221E58069028CE710050495FC50C402990A19F54845509610DD8FDAB7028D9C980206A7487865A6EDEC55CABEADEC8F1BCF75E9DF67F7DA0F62787475E95E719A562BB0A54631D333E5CCDDF10CBCB2F431719855EF68C149713276DA41CCABE67A8A57765E02B98D7117494D0F13AD04C161D4ACA485310070D34A280AF99DD687DC0DF116EEC29C03F3A5E0CDEB9F37F583BF818A68E410684229B03E6B70A21EC297460A411250D44CF51E34237A41AD5CA438EF0E6E2D17E0ABF80C7077456EE2B7B787B78B2FF1BCC8BC07FB220A99AC01447E8F3E63A2E99AB0AEC5E3E615786433E9CD336EFEC7152CD85DBA3A8F1227A90F81FFFC75E48C82C43BF54321868158137DC1464F5A6BDE2D3C2E9BAE1E3862BC44585830C4CC31703D96E38536CD3F3B16BEE749EC318DBE982FFC6EE06996E8167DA6D46A052FC0CADD06A6E18C5202379068E6ED0EEDC6B242010C3848ADE2955C25963CBA56B26372FBE2B262E6DC07FA046C08E2F3EBE66C95FB172C0F123AAFC6A0DEC44C2DD493899A9B98A6BB9B51A074D5153621297580B3103D542D4078B270E7874A839E6F3FCECE4D34527EE80D3E3AAA550BF73AE7C7388BF12212A20A3B4B8BCAD265609CF476D164FC65DCBA0D82B4469D2617E8D2A0F1F09EC3F17997A0C555BE924A778EA63FAA59B505881DC267EB415388F67B3BF54D180C8AFA7BF49888D59C7E0B750DCFBB6C7364432CCBB5B628208466726C75ECA8B58AFE0DA8B4ACC1CC7EC58CC78E6A95B812DAA638EB9CB93F0EA2870E39A3BB1FD1C818741B94E55783160EE5168FD6AF194C1D2A502D698FEE396575DC95DA36182D2DAB495EBF1F84B1A349A528BF2FE9BBEB8F8B2B94EA80EE9D5B6823F48A2B547469DAF656C12D0C0BA30D0D69CE84FE5CA168D2B61DC4607487E8B925E1139F71078728B052FA08AA39A4FA0C3AAD2A55BE4D7937B47CE1739E27B57DDDC91F7666E0BFDD9FB04BE2F61886103E23EE8826E983C9042733F8D59C4C877F467198AB09823BB091353C857B0", + "64FA974EB768A0646EA038266B6A79814C9FA2E2C7A25A903FC37183E3C71FF8", + ), // valid decapsulation - ("3F6361FCD422DAB61DAEC1B1A1007CFA943D79991BF76C463D04271CF41973860F2CE81ECBB20FA2DAC145F711D923C8F2606B6CD30B62979E24619BB66403B9B25C09B1568310873485957B3BC300E59AD41C1AB13C010FF12C4B4193B7DA75E71072529027F0F518393A76CDB3C8566A0190798EE02399D3FC19451849C75242378C22D898A8FB8742120B834DE6027D8CAF52506A28823C78914C25398C387BB7CA3A8D7D3B98E44CA817B76AD1095ADE01BDCF35992D10775387A1617969A96957D11CC8E7273CED7BCC7DB0C056C1815F14676B17B5AD86B595AB6BB8D55C8CF64AB7D89CB35569CA9017E84AC173397E78B978DBE5B5C197B39823CB5A331FD5A6C5C1F95A5FF142A946342FF084829616AD571F8F864C1A1540CD8A4F0FE92C919AC37A222966E57CAE63B920F55C4B464BF0E83901B32F01300E3CB6B692582F96E906176C6FA9E34711C4C2C38C3BAE730E42C980D5079A07340E50A097CFAB1F90502BFC92A9DA62A1D8393B15C335C4D3B14CE1379096C9464A8FD3E551D3C454D34C9986D3CA576730406535B8385949D91D66980BEBF66F08B6385096C0F706A8549203B3A873A4D4381E1B973996A116182C81E43B12D9BF27612B50BC1DA20600B4796E83938506C2CAC350C611482EEDF693B76AABE8A90DDBFC66BC5C8CBA5071CF6A9563EA2ACE5994552554B5ECC49ACC4D62464FA512CDF39940E2087138F87F8F978B37874D5FB709C947082C1B29C8040584639AC4BC291454164DB6BDDE556E73A184B699A26D485F3E4A61DAA202D1E253DBBB386260387D47603BD94D33FC2D2630022D87C8C7F01E33175294719B0402A66C3885B92B396B8272546C5FF0893C6221CF8A18232DA439223A3768D3A6B7D579BD27898AB669F71555BB9A8945912125D83B323165B1B7B7289CB577A1A2EAD081E3C30DDD76A252BA4DCDA0799B36CCE8516C9F576842A92A046B56E03C80B97449C475699A45A266E0BF33A0B32CE3B6C069B79D11521288BE98D9B1A8451B8D1697A4B90ABFA2C55B4040FBF3015BE51B4F7B7E416C763D542B95607B79A43B2583B26B080F4FE7150E6618DA8513A9648C02516EED3300E7F22BBC38BD9EA06E8C9A52091CADC6A2993E5245B98547501C68203A23C10A762CE9365C904E67E06B56046CADF44F00BCC817F57E2911304EF398E74882353C26F7B57C49A5379138845203A987A41DAAE6583D0C8F32B6BB98DCB3376533E6B2A478542144CA379EA5C82800C4C2E11A91BA5BC6D3A50D2454943BC919294E63E4C2DA4A1EC92591AD63896CE1569F368FF2F6322CEA5740C39CBD52BE7706047CF26A74297B2BCC2CB6C847748C8FD4A778731506E9256BA9691121792F309CC61E894C682A7AD5303914347D44393C105C13CA1468106837B421310362BC521068555ACDD98C84BED2A4598267EFE0645DA08ECBE63C639C8AF4AC8E08C25D3C9A1BD398BF1EB5ABB49ABC524C392797658A34C4BB1C295F0B72B088A83489732C3A5D904A0C2EE4A479F66E120ABDF0649A4EF9946636914C311B4D06196193914F0B1A4FF271BDCC314BA0AA1126A37C6B30A78B70CAA092E3A70451B605D28A6E99B63E3460192DF5736C362EDF09763FE68D2E0BA8EC952E176CA986116DBC9A4F5C4880D7ECBB3E407190712143A5B449E045EB9C0F732C3EB754915BE85829B21121E003B293BA4AD62804FC9EBB258BB0791D36232390DB7AC6148EDE099733637550A49BB385C469B4AE95158097FB15E46B7AB43306E01848A1D55D32F1213623534E7863586523A62093CB239AE226CF6C50A9AD4BCE792504746AB45FF0A06F8A569B04C25333A6B755AFBC166F8AC879AACA029D3CA9E938ACC9948F0505BCC32A8604657827C76C2240CEE762A708988255903C752BA0EEBA9D41E17F0EA97A8D0B9E6F690A0CA45CD8C11256B21C64B477B8573856F130944C96C9CB2C34696F47705930D848B8B81D49BB4D510CB3384087FD9225486B73DBE57169164918C6B877833177E659D76C86EDC27ACDEC6D44B83BBA10708737686EC815AAF76F741104B573A72D5808D0786B1785936F140A3C0585023AB30D85314F25BFEFDC3868FC182BB7438FDABF15270B3CF47881483A6EF9C89DF56C7AE10CCE8C5F12EB4CBDE53CBEDB732454AF31CF6DF8C527E5B0BA5B6FF73FA6DD6EA1B4BB5276140980C35D2FFE312598FADFD252A81DFB2B921F786DB4BC436631D8EF3D34E33872E4080786D0D565361C2B4C405126A567A05187E4941ACC7FE692EB648A", "A8EB0BC44EE33E8ADF58058E35F70DDECD13B0E90E6CFC36CB2FE4B84642D83C5C91F6D49CED07C2A8EF19A4A3F4A3460A4E099E04BC48507F36C8309F196BE71C2CF93FB699B7F04488902A61E8AFE6596CBB7981F6B9AA414A2F7BBAA9ADD3108D1A1EAD73219C595E2D00CC0403236F70883FB961C72D291DC41E1FD2EF96AA45B2F738632989B7A19AE08E8E2C098BB388D5C690C1E5E4E0A8BEB2CDB2A5476BA5FACD4054EC0C4EAF8ADC2182EDE5C39FC178CDAC1C177C60DAA0053F03FF602471A8094B6BA3D76BBE195D037F7A74D41B643E23680136636FD301D698BA1A9A2C526C23237312E5118E32558C4AA9FC1730BBDD5B6AEB71FF9448E06793159FA179D9B4FF8C300EBFE1629D58D1D1803491211D43D91D432A3D0A42DC2C3857C0045C0BDF0841E3257A1CC5E030E822D039EAB72AB6605EF8C57D78D9815D7B4DFD75E48D451E550B5C3940A2B6C56D8D1E8D603F566D5BA30415C8091EB62711E53262F0C3D2F13C7A71326A3F6C6B5506CB301DDB2702F8A73C547D0C5AC0C9DE03741F7C73A1A1F8D5BFBE4D5124C0A6931E9120AA35FDE7E1BCBD0B558BBCA6EB30B6B442221A5D625EEABF41338DF4D7E74C616FC660B8530B1C4CBD70A338EFAFA53EFBDF2CB57EF27454DD3201D4D14582322956E651BB848833EA780227A0DDF955F6040ABFED8D5F1DD0655A5326F959D9123859E4064C2AEB538C8B3D68F8A3F439669FEA8445A5BDFD3D9C87DEA90D1FFABA5489B8F40D906D7DCC51D7E237AABF4BD9B04A7B4B81D9A281B05E9FD0D9BA0E15BFEB15C85F2AA8E707413CAC05F54200C37016E775F38E47F14601B1D9B82E39BDC1A7FBE16FA2A722642395DAB19709DE110CB45BF901EC3476F112AA56E6F8A5D8CB6A32E406A3DEAC61CBBD762C5342849844C3B09975AE8E7B82EA47237DDFE9BD98D6E6EC1E63FF02818B1F1BE20E6DC7D9EE78E27BA2049E8372148817B9DBDE1480005AD734ECB4F0E9ECC2813E49E2832616574276A40CB2F8B7CD89C3BAE911C55861CE329AF42140564ACAD2644C3CB08BA9D6DD84498E073A105760BF00B4", "6232F7746C31F86370F1432C6A6A964BAE457F999142D8A121F8FFA5D809640A"), + ( + "3F6361FCD422DAB61DAEC1B1A1007CFA943D79991BF76C463D04271CF41973860F2CE81ECBB20FA2DAC145F711D923C8F2606B6CD30B62979E24619BB66403B9B25C09B1568310873485957B3BC300E59AD41C1AB13C010FF12C4B4193B7DA75E71072529027F0F518393A76CDB3C8566A0190798EE02399D3FC19451849C75242378C22D898A8FB8742120B834DE6027D8CAF52506A28823C78914C25398C387BB7CA3A8D7D3B98E44CA817B76AD1095ADE01BDCF35992D10775387A1617969A96957D11CC8E7273CED7BCC7DB0C056C1815F14676B17B5AD86B595AB6BB8D55C8CF64AB7D89CB35569CA9017E84AC173397E78B978DBE5B5C197B39823CB5A331FD5A6C5C1F95A5FF142A946342FF084829616AD571F8F864C1A1540CD8A4F0FE92C919AC37A222966E57CAE63B920F55C4B464BF0E83901B32F01300E3CB6B692582F96E906176C6FA9E34711C4C2C38C3BAE730E42C980D5079A07340E50A097CFAB1F90502BFC92A9DA62A1D8393B15C335C4D3B14CE1379096C9464A8FD3E551D3C454D34C9986D3CA576730406535B8385949D91D66980BEBF66F08B6385096C0F706A8549203B3A873A4D4381E1B973996A116182C81E43B12D9BF27612B50BC1DA20600B4796E83938506C2CAC350C611482EEDF693B76AABE8A90DDBFC66BC5C8CBA5071CF6A9563EA2ACE5994552554B5ECC49ACC4D62464FA512CDF39940E2087138F87F8F978B37874D5FB709C947082C1B29C8040584639AC4BC291454164DB6BDDE556E73A184B699A26D485F3E4A61DAA202D1E253DBBB386260387D47603BD94D33FC2D2630022D87C8C7F01E33175294719B0402A66C3885B92B396B8272546C5FF0893C6221CF8A18232DA439223A3768D3A6B7D579BD27898AB669F71555BB9A8945912125D83B323165B1B7B7289CB577A1A2EAD081E3C30DDD76A252BA4DCDA0799B36CCE8516C9F576842A92A046B56E03C80B97449C475699A45A266E0BF33A0B32CE3B6C069B79D11521288BE98D9B1A8451B8D1697A4B90ABFA2C55B4040FBF3015BE51B4F7B7E416C763D542B95607B79A43B2583B26B080F4FE7150E6618DA8513A9648C02516EED3300E7F22BBC38BD9EA06E8C9A52091CADC6A2993E5245B98547501C68203A23C10A762CE9365C904E67E06B56046CADF44F00BCC817F57E2911304EF398E74882353C26F7B57C49A5379138845203A987A41DAAE6583D0C8F32B6BB98DCB3376533E6B2A478542144CA379EA5C82800C4C2E11A91BA5BC6D3A50D2454943BC919294E63E4C2DA4A1EC92591AD63896CE1569F368FF2F6322CEA5740C39CBD52BE7706047CF26A74297B2BCC2CB6C847748C8FD4A778731506E9256BA9691121792F309CC61E894C682A7AD5303914347D44393C105C13CA1468106837B421310362BC521068555ACDD98C84BED2A4598267EFE0645DA08ECBE63C639C8AF4AC8E08C25D3C9A1BD398BF1EB5ABB49ABC524C392797658A34C4BB1C295F0B72B088A83489732C3A5D904A0C2EE4A479F66E120ABDF0649A4EF9946636914C311B4D06196193914F0B1A4FF271BDCC314BA0AA1126A37C6B30A78B70CAA092E3A70451B605D28A6E99B63E3460192DF5736C362EDF09763FE68D2E0BA8EC952E176CA986116DBC9A4F5C4880D7ECBB3E407190712143A5B449E045EB9C0F732C3EB754915BE85829B21121E003B293BA4AD62804FC9EBB258BB0791D36232390DB7AC6148EDE099733637550A49BB385C469B4AE95158097FB15E46B7AB43306E01848A1D55D32F1213623534E7863586523A62093CB239AE226CF6C50A9AD4BCE792504746AB45FF0A06F8A569B04C25333A6B755AFBC166F8AC879AACA029D3CA9E938ACC9948F0505BCC32A8604657827C76C2240CEE762A708988255903C752BA0EEBA9D41E17F0EA97A8D0B9E6F690A0CA45CD8C11256B21C64B477B8573856F130944C96C9CB2C34696F47705930D848B8B81D49BB4D510CB3384087FD9225486B73DBE57169164918C6B877833177E659D76C86EDC27ACDEC6D44B83BBA10708737686EC815AAF76F741104B573A72D5808D0786B1785936F140A3C0585023AB30D85314F25BFEFDC3868FC182BB7438FDABF15270B3CF47881483A6EF9C89DF56C7AE10CCE8C5F12EB4CBDE53CBEDB732454AF31CF6DF8C527E5B0BA5B6FF73FA6DD6EA1B4BB5276140980C35D2FFE312598FADFD252A81DFB2B921F786DB4BC436631D8EF3D34E33872E4080786D0D565361C2B4C405126A567A05187E4941ACC7FE692EB648A", + "A8EB0BC44EE33E8ADF58058E35F70DDECD13B0E90E6CFC36CB2FE4B84642D83C5C91F6D49CED07C2A8EF19A4A3F4A3460A4E099E04BC48507F36C8309F196BE71C2CF93FB699B7F04488902A61E8AFE6596CBB7981F6B9AA414A2F7BBAA9ADD3108D1A1EAD73219C595E2D00CC0403236F70883FB961C72D291DC41E1FD2EF96AA45B2F738632989B7A19AE08E8E2C098BB388D5C690C1E5E4E0A8BEB2CDB2A5476BA5FACD4054EC0C4EAF8ADC2182EDE5C39FC178CDAC1C177C60DAA0053F03FF602471A8094B6BA3D76BBE195D037F7A74D41B643E23680136636FD301D698BA1A9A2C526C23237312E5118E32558C4AA9FC1730BBDD5B6AEB71FF9448E06793159FA179D9B4FF8C300EBFE1629D58D1D1803491211D43D91D432A3D0A42DC2C3857C0045C0BDF0841E3257A1CC5E030E822D039EAB72AB6605EF8C57D78D9815D7B4DFD75E48D451E550B5C3940A2B6C56D8D1E8D603F566D5BA30415C8091EB62711E53262F0C3D2F13C7A71326A3F6C6B5506CB301DDB2702F8A73C547D0C5AC0C9DE03741F7C73A1A1F8D5BFBE4D5124C0A6931E9120AA35FDE7E1BCBD0B558BBCA6EB30B6B442221A5D625EEABF41338DF4D7E74C616FC660B8530B1C4CBD70A338EFAFA53EFBDF2CB57EF27454DD3201D4D14582322956E651BB848833EA780227A0DDF955F6040ABFED8D5F1DD0655A5326F959D9123859E4064C2AEB538C8B3D68F8A3F439669FEA8445A5BDFD3D9C87DEA90D1FFABA5489B8F40D906D7DCC51D7E237AABF4BD9B04A7B4B81D9A281B05E9FD0D9BA0E15BFEB15C85F2AA8E707413CAC05F54200C37016E775F38E47F14601B1D9B82E39BDC1A7FBE16FA2A722642395DAB19709DE110CB45BF901EC3476F112AA56E6F8A5D8CB6A32E406A3DEAC61CBBD762C5342849844C3B09975AE8E7B82EA47237DDFE9BD98D6E6EC1E63FF02818B1F1BE20E6DC7D9EE78E27BA2049E8372148817B9DBDE1480005AD734ECB4F0E9ECC2813E49E2832616574276A40CB2F8B7CD89C3BAE911C55861CE329AF42140564ACAD2644C3CB08BA9D6DD84498E073A105760BF00B4", + "6232F7746C31F86370F1432C6A6A964BAE457F999142D8A121F8FFA5D809640A", + ), // valid decapsulation - ("0AC64A3013A72421AB1591CE52635A37BBB805449EA437A76074B6E78051AF379F12F61FFCDB02C83522F7AB4345D8280443310BF43D493C5D41C5606AC1A8D4C5C0298634EAF5C2F082173ED41A7A7209DB6CB0C5B37F71F6272C15843A181508812A38A08DBF067C173BB7FB669BF53AB527B95DE777358528B890116FB9C3CEAC87A383692DF7ACAB4E734AF059154D090EC3A545DDC70696B1CE0EC9A42902B24792C96EA2218F01492D833C6B7875E969C14B109215D836D655488BF620814C6077CAAEF626C1F4A958DB81A806E06099D190CA0C42427AA2F8B5BA30592B860562B37C7C05CA7C538B88F1BB84E6504AC6B30AE9A1B887FB94CBC623FA51744B59ACCF39145ADC93311B134D497F282C3D6171A96C736AD6BCCCEC741C9738A839007599C03CDFF1B0D6121C574643DDC57466345530445F4095412457419A153117BC049C2AAC0DE9B0718098DB4A0A30590E5AC15676BC17C03CAE83E52F19506469CCCEC96789B938661C157050777D424014D16773A1D4080EC16917D399B2410C666C1BA85B76101197B152AFDCA940A12692ABEC5ACA3B635D7193D06A5B82A53E43AC8813C30C40AA7848BBBE1ABC6A761174D9619CE6F4BEB4549BBBFB0DA2CBB5BA66B9A9514B52F8AE0AF55D3AB704DA599B47E6B7E79731A2EC7E4053CEA0A79D38A380D8109208515528D41D31303A2F49B5C1F5B5668849D2A30796C2A8141A480B1383CFC70852B980B5138DAA96CE05E143ED3504E2DC6DC903ADE23936DA14B31ED619D9842AAD68234DF95260745B99388213B9580FE2319A2152E939A4EDD13B456B42FF48B53B4929E58A0301D1B01A8796435CB8BCD3181152474C2CABB6452CF63875883429FA3B8A4F8805AEE303683B29C5D59498C5B0F4F546AF358D36F294B422025A876868C923A9717955308AA1B188F7593467EBCD2FB533421241DE9AA59324CDF3F80A77485E187ABFE7E6BB49548AE957BB1EA89F8CD37A70C558E9CB8F62C32C77793C2A92A5C1E27072481DA502AB1C157F2A0A615C9222D81C1711F70E8EB19302637215E408A4857BDD4B9C405608E3E95AC8B13C69E72A43A8116F90AC571BB924EA1C3E994BDB30B9913A1C3B80B40392C4391B131A163401645F9A45035779B240491949E8595462CECFD161D012173464579137360B68A60C52AC3985CE82192D45C3CC358A5B8DDA2B76939446115C5B0677393C30F0B46F5DD1B94235C82C473BDF338D7AD529E10519B5668603AB1988602666090C8805BAB05A53953BA79F024FAE6A13D01A1FC70895C27664B089C69FE19009A80B45194E445B64BFB6A515A20D67A7CD5D329DB1B60929E0BAAC9B67297774E259C8B3F3A606494B21417B7469476CB76A84764EB1A63B57E20BE4FB4FDAF43CE9B633D9F0C93668A11E3A15171B0480E18E4C55390F43117C37265C5806A4C393AC2A7A5C3B581169C8CBC44DEC319A47D20780288C507CAFDC6B6D549650C8988E6A9723D8395923943BD3AA386CDA3320632A4A809FFA89837CF11AAAB98C8CF901328507CB944D84E91E1305449B416D03916196C03CCD0A86DA978AE9CAAC92342ABF0AAE8FC84413A3C3D68BAFD9A371F758BC1A03238B80657D7212A484A2BC85745D12624B038379D221ED512879BAA91FF59A26E471AAD875A1B686B0B16E58F3A7BFD84C29769201EC4F4DE3107F1547BD50B986017D2C9A29687923FEFC4709B73493801149457A8B37914413012EBC37FE700D12A231B6A321CF7BC3C59220ACA80CFD85B648C50BDC7BC9F478BCA7608C4EE6A9C451A87DCAACCFB185D5666B2830A695082B24B3B24357065C41AF438A5B13B169C0CC35D653C5D4F174C03BB9FF43124747ACC9723BD959CC94180E134C679C3204BABA8AD43C7FB455468CC8533E444CEC311857143F41F913CF460447D487115530FCA3AFF13C5C31A5635C923F5D278166818E3980C939112E18315ADFF845EB645FA75CC33435294606B0BCE1B0510AAC2162913E4677BADCAD7A7049FBB857F58C5FBCFBB20F90319911BE22B0B8425C2BE15A6755962349C21E55A823A8A77DF0326FAA3A6C9091CB57C5446883608BE2A2CE7AC0CE0C3E106547CEC54398E29342B4A19AE9A47C11CB50F232026051FFB0CD3C4A596DA9C0CDACDF47379A9DDAAD46D01B525849F62871783288068EA28A19997C7A03BE4A280F032731ADE86900F1A0D8CDA6758BB7E3ECD290F8969B058F20EDD71120EAC448D59C6EE64AD1C9628937FEF4275487BEBA177249D44E773251B441", "E39B4C28935E605361E62230450410B04B36FA339DCDFAA8471F4B4274F6568F39B4B7358B2ECDD54F4B000273B8F83D0F6EADE8D4497958E357FAC7DF67924F06C62B59A37DDBD0373A595A2C3EAB451C83023E0B5BB009F8073B934D0C46BAD1EFFA079CA378AF999A926E8DE404A48AE3B60BE0E63348FC80AFA29A5D48DD99F58E57902B3A484EC2DBC32B5B7AC5956B5F72FD8E8511EAE315D09FC548FB499669A2C2D7E548333A1CD3CE314E5FBFA98615ED79A26186447FBC52DA56421130971E7102ACD53EA7C29A30EABD6EACE75A79D3643FBF4FB44AF0C4C53886E4CFFE73287C5E070B44B4AF89348D921F080BA246C08DBB81E15F90B4344F1AD0CF3131C2994B203CD5CDC1E6B8B0ECC1CFC85A789148017098959AF216F5FA33B050B2A2FA6F0931E78CA3521DB52E1D330BF4B3A117220C962BCC62102CEA5814B3CFEF670BF8A00A058EF53D67FA28BD5403B68E59AD0289C479CAE5154F9102CB5501DF6F2593FB000E7AC71CFA95364184E7BDD141857681599A561D74CA063E3661D066A641F2FF608C393FA8A9DE3019BBD6FF85F6A2583474EA1A141A2723F43A022C1E92D037DC1BA844475045B8A879E577988F8CEB7AC5340EE5846E6CC19AC87EEA0F3D8553397B4A3E7B9B1BD4E920A829BD550D8369FC6A3B81901D46DE4B75B45932CE43801455ADADAA3EA47E313D77090ED3F8238DA8824ED0B1D90BD83708BA467FFE17D0416EDE29EFAC3850EF80D818C1D5AD1BF5EDB25A562DC0BE95EF886AFC79A50F12E6F35D4635A70377326EF82A1CCB6A9827E43D9999A26B8952EEA3001DB8E6D21AC20F69203DB66945D05C5CF3B67C66A29F7E99994B9125731582F1B5A1A5544E9A6CC83179F247492BC653807FB7AA29127427E69FACC906723C54D2854AD0F365921DC345426817F0481B52B5A2977620168168C921480335392DA75BDECB4FF3DD0F722B7E03534EAA31A066B89431706253FCACEA4BC1E5F7194BDB5034D6578D5E75C4B4516B907BADC907314DC451427A68819A00E705464D394CC850CE71E07EBF17C7768CD9198DBC940EAB1D", "19CC79A25E516B49F045E0E0CD715426A5E44CD8C8DD0A2CE7477451AB1EB1C1"), + ( + "0AC64A3013A72421AB1591CE52635A37BBB805449EA437A76074B6E78051AF379F12F61FFCDB02C83522F7AB4345D8280443310BF43D493C5D41C5606AC1A8D4C5C0298634EAF5C2F082173ED41A7A7209DB6CB0C5B37F71F6272C15843A181508812A38A08DBF067C173BB7FB669BF53AB527B95DE777358528B890116FB9C3CEAC87A383692DF7ACAB4E734AF059154D090EC3A545DDC70696B1CE0EC9A42902B24792C96EA2218F01492D833C6B7875E969C14B109215D836D655488BF620814C6077CAAEF626C1F4A958DB81A806E06099D190CA0C42427AA2F8B5BA30592B860562B37C7C05CA7C538B88F1BB84E6504AC6B30AE9A1B887FB94CBC623FA51744B59ACCF39145ADC93311B134D497F282C3D6171A96C736AD6BCCCEC741C9738A839007599C03CDFF1B0D6121C574643DDC57466345530445F4095412457419A153117BC049C2AAC0DE9B0718098DB4A0A30590E5AC15676BC17C03CAE83E52F19506469CCCEC96789B938661C157050777D424014D16773A1D4080EC16917D399B2410C666C1BA85B76101197B152AFDCA940A12692ABEC5ACA3B635D7193D06A5B82A53E43AC8813C30C40AA7848BBBE1ABC6A761174D9619CE6F4BEB4549BBBFB0DA2CBB5BA66B9A9514B52F8AE0AF55D3AB704DA599B47E6B7E79731A2EC7E4053CEA0A79D38A380D8109208515528D41D31303A2F49B5C1F5B5668849D2A30796C2A8141A480B1383CFC70852B980B5138DAA96CE05E143ED3504E2DC6DC903ADE23936DA14B31ED619D9842AAD68234DF95260745B99388213B9580FE2319A2152E939A4EDD13B456B42FF48B53B4929E58A0301D1B01A8796435CB8BCD3181152474C2CABB6452CF63875883429FA3B8A4F8805AEE303683B29C5D59498C5B0F4F546AF358D36F294B422025A876868C923A9717955308AA1B188F7593467EBCD2FB533421241DE9AA59324CDF3F80A77485E187ABFE7E6BB49548AE957BB1EA89F8CD37A70C558E9CB8F62C32C77793C2A92A5C1E27072481DA502AB1C157F2A0A615C9222D81C1711F70E8EB19302637215E408A4857BDD4B9C405608E3E95AC8B13C69E72A43A8116F90AC571BB924EA1C3E994BDB30B9913A1C3B80B40392C4391B131A163401645F9A45035779B240491949E8595462CECFD161D012173464579137360B68A60C52AC3985CE82192D45C3CC358A5B8DDA2B76939446115C5B0677393C30F0B46F5DD1B94235C82C473BDF338D7AD529E10519B5668603AB1988602666090C8805BAB05A53953BA79F024FAE6A13D01A1FC70895C27664B089C69FE19009A80B45194E445B64BFB6A515A20D67A7CD5D329DB1B60929E0BAAC9B67297774E259C8B3F3A606494B21417B7469476CB76A84764EB1A63B57E20BE4FB4FDAF43CE9B633D9F0C93668A11E3A15171B0480E18E4C55390F43117C37265C5806A4C393AC2A7A5C3B581169C8CBC44DEC319A47D20780288C507CAFDC6B6D549650C8988E6A9723D8395923943BD3AA386CDA3320632A4A809FFA89837CF11AAAB98C8CF901328507CB944D84E91E1305449B416D03916196C03CCD0A86DA978AE9CAAC92342ABF0AAE8FC84413A3C3D68BAFD9A371F758BC1A03238B80657D7212A484A2BC85745D12624B038379D221ED512879BAA91FF59A26E471AAD875A1B686B0B16E58F3A7BFD84C29769201EC4F4DE3107F1547BD50B986017D2C9A29687923FEFC4709B73493801149457A8B37914413012EBC37FE700D12A231B6A321CF7BC3C59220ACA80CFD85B648C50BDC7BC9F478BCA7608C4EE6A9C451A87DCAACCFB185D5666B2830A695082B24B3B24357065C41AF438A5B13B169C0CC35D653C5D4F174C03BB9FF43124747ACC9723BD959CC94180E134C679C3204BABA8AD43C7FB455468CC8533E444CEC311857143F41F913CF460447D487115530FCA3AFF13C5C31A5635C923F5D278166818E3980C939112E18315ADFF845EB645FA75CC33435294606B0BCE1B0510AAC2162913E4677BADCAD7A7049FBB857F58C5FBCFBB20F90319911BE22B0B8425C2BE15A6755962349C21E55A823A8A77DF0326FAA3A6C9091CB57C5446883608BE2A2CE7AC0CE0C3E106547CEC54398E29342B4A19AE9A47C11CB50F232026051FFB0CD3C4A596DA9C0CDACDF47379A9DDAAD46D01B525849F62871783288068EA28A19997C7A03BE4A280F032731ADE86900F1A0D8CDA6758BB7E3ECD290F8969B058F20EDD71120EAC448D59C6EE64AD1C9628937FEF4275487BEBA177249D44E773251B441", + "E39B4C28935E605361E62230450410B04B36FA339DCDFAA8471F4B4274F6568F39B4B7358B2ECDD54F4B000273B8F83D0F6EADE8D4497958E357FAC7DF67924F06C62B59A37DDBD0373A595A2C3EAB451C83023E0B5BB009F8073B934D0C46BAD1EFFA079CA378AF999A926E8DE404A48AE3B60BE0E63348FC80AFA29A5D48DD99F58E57902B3A484EC2DBC32B5B7AC5956B5F72FD8E8511EAE315D09FC548FB499669A2C2D7E548333A1CD3CE314E5FBFA98615ED79A26186447FBC52DA56421130971E7102ACD53EA7C29A30EABD6EACE75A79D3643FBF4FB44AF0C4C53886E4CFFE73287C5E070B44B4AF89348D921F080BA246C08DBB81E15F90B4344F1AD0CF3131C2994B203CD5CDC1E6B8B0ECC1CFC85A789148017098959AF216F5FA33B050B2A2FA6F0931E78CA3521DB52E1D330BF4B3A117220C962BCC62102CEA5814B3CFEF670BF8A00A058EF53D67FA28BD5403B68E59AD0289C479CAE5154F9102CB5501DF6F2593FB000E7AC71CFA95364184E7BDD141857681599A561D74CA063E3661D066A641F2FF608C393FA8A9DE3019BBD6FF85F6A2583474EA1A141A2723F43A022C1E92D037DC1BA844475045B8A879E577988F8CEB7AC5340EE5846E6CC19AC87EEA0F3D8553397B4A3E7B9B1BD4E920A829BD550D8369FC6A3B81901D46DE4B75B45932CE43801455ADADAA3EA47E313D77090ED3F8238DA8824ED0B1D90BD83708BA467FFE17D0416EDE29EFAC3850EF80D818C1D5AD1BF5EDB25A562DC0BE95EF886AFC79A50F12E6F35D4635A70377326EF82A1CCB6A9827E43D9999A26B8952EEA3001DB8E6D21AC20F69203DB66945D05C5CF3B67C66A29F7E99994B9125731582F1B5A1A5544E9A6CC83179F247492BC653807FB7AA29127427E69FACC906723C54D2854AD0F365921DC345426817F0481B52B5A2977620168168C921480335392DA75BDECB4FF3DD0F722B7E03534EAA31A066B89431706253FCACEA4BC1E5F7194BDB5034D6578D5E75C4B4516B907BADC907314DC451427A68819A00E705464D394CC850CE71E07EBF17C7768CD9198DBC940EAB1D", + "19CC79A25E516B49F045E0E0CD715426A5E44CD8C8DD0A2CE7477451AB1EB1C1", + ), // modified ciphertext - ("05C52D7B334DECCC71E083A807F14593A2487B57585EE4589FF607916939493539E121C43D958CB926B720AB46C1100922974A06A7772C485072C97427121A318C5AE2C8A47DF579F75008BA1B8FA6CA5BE19BCB21C55E161BCBB83B9D44F4BB9B3606F8E2730BCB4FAA1BA94E57AEE50052A3F653FB440023AB2F7C7CAFE096BE9844AFA39282AF24046C5735BBA80AF0680B32B25CC7D530B2ABBAA28329DF34CFE0317B0DE34D0A0248BB60314DEB2F80831575765D19199A234B3BB246813D67764BEC958258246BC936D8940F68291D6BD4707CF02AF26052271C0373F825F3B5631F9186926820FB831D0F413992169293B847B3A04742900FE2E4A7C6541F5CFB7287F851CAE79A320114B84391009D2CFC0961629CAA44D9A98148A72DD7CBA8B0A8105C7BD6466E25BB45B4857E1D097534E24180812FEFF4C56FBC6CC14317AE11A381447149A6463DF582F765363E0656FB0402E5308AC62798098CBFF83C6A37C51AFA92BFBD162D8209D02E1313AE3BA215FC267FE693FBEB809B453A0E3AAA28388DE3556EE46793FE59AC60A85D0280552B775EABE2337B0AAA79F238F2F727C5D5A2628B3101E511EB5696CA3A7BDDB2C1F6077C44814D8ED53FC38151A1AC47C37A068E3C222ABAB3D3A2CEF24799B78C73E42316F5866F0D71B5E5C35745851719F0575EC70C03A9C355CC68851B1735F37B845285826A0E55059E87024D3BF02DD14A135AD8B5494367CBBC5E4BA76ED97333D0BB900266203534A11F91124D947BF251C85A4048CB5BBF558125D4C088EA70C3B9771A5E1720C7CA58FD22A8A3428654622981BC663C547408441ACC1683C415163D6AB98CB0A659973081D10EAE875EFB787A08969FBBAB6E35292FFFC7B17266B7506AABD67AC1AC8803772B2A598BCF4B2B2AA4446BE3301DC5B59A1BB2467864A8C7FB8F88736CC265BBC81097220573C5E57A87951582B28A3AB839EE9323B3856E6B9115FCFC79FCFB0113E57B66514CCFA891FBDB137CF4416AF463C1FB48C5317430F92FA82B83FF60917F0CC1B8A60BEAC41A406796E7EB5266656616D85D3EA25A21E97EEBC469B2BBA977B97336223202B072D382B90E0986A1241011E7C85446CD340850F9E20C66BB70F686C8187C111BD3486E581302F28083D7840446359F9585448448E4C6C191A17A05301F19FB9AF37989664A096DE43FF5EC9390F9330EC87342D9614995521A1AB0A387110D183C1636BED16B3A0632AC5F81BD4F18033DBA2CC0D97092722B12F227678777F67CB601391D04223D3CC6526DF4361F7C555A1AC30649721DA548D5C4754668138D294D8316AB1A7597AEC041C8FCBE63D469DD54A45F876F3EF10C70EB0E400CCD9C8AA399E7B377B1AB8E45CBCE34312CBA97F9C3A8BC191EE00C384163023DB276BD6C8F3AB666F60252DD8B5E88A1B1A2A31ACF851C5AB448EAA74797FB5A4DB6BA7824789931501C042EF1D6BB63EBB6B39401113BCE1D6944A0D596699717E315B42DF86CC7008299C36C4B4B884324C6B6E8B780BB4E3EC74F362C90A9451B13849144D95F7D084180BB78E619792B5562C52CB29F6BC0C1803E5C5CB0C74A141BDB02CFB2B05CFB6C6D1B0BFA562745B984164C0D8749B931B76456F086B7052FAA60A11F04724ACB1770080EA3DB91ED5C6C77A903A3FC1ABD51206C4C5DA7760FCF988AE69C52BBE86EB7CA10705BAAB1D59383D875F55081FEF34A07025FC7F14912D0A72BA54414493ADB600A7D104E8602C3DCD686778AB22B4480A06C98C514300BE2C9DBAA33195BC8731536878A7A5CF783B6A5BE8B55B9D4DA00FC258F07223F43A40D9730AC064576683A9E1D786B0DD094BFA6795207705F0A6F9808BFE3B00AC294137093AE45A892F3918D4A8BB302CA067058C274E36677882B5F53A8B6952AE2C483B1F3BCD3A3266DD21167025E27600EC7748BD62C2AB3C09449129AB4B8775227565B2511F51457AAE36495D5862A452767A1422C117977A85F3AAA503B57017DC9B4AA09406B1A18E2ECAF8C761AFBD54427240E442860CFB50111867B856465C2D8A7B428710953BD1062877E2946E9A02334C461954272F3EABE0A6712F527908039A4D890138A641301969FEAE91C348878D53CB8C3D9170A07823CC05B638997928A0C593EDC86BC93C98C59B39B2E602B89129FF40A7771C3CEFE36589852794DED3FCE5A81A20A52C286EA35AB530DCB83DAB5C77B78AD795746D27F10B90D85881E1ED7BD51BD6AD69F7D2EEC361086A4B087F4A76067C53A142C73", "0CDCFEC9D62E3C8412849705B87B4EA72E6D5553B2EC6F2AB980A9AEFC330224F2E7C8B969DA9658DB470588023F3A2F7DEF4C3E955C34B594F991E55CD7C26C5AC624906D2C23955F104F4577E40F51746C7C738BCD15C34B56A949529A8F0D5DAAF902D4149EDA9CB134D5FD57D7795B52A38E7C6165C22C0AF4B13A127A2A2087F0B144C33464281350D4A4AFD39901CB1D3764A7340D712AA6CD8AB0E1464D84D8CDDCBB74A01C98979E1AAA21F5D86A7D2C882D5F02549A75E1B91A8FD6AD02A0A727B3CDAC1E2CFC2F5DC9BDB9B86DFB76919A42BD98DEEFCF8F9B322E0AB887F522A21AB2234F91F9055D32A5B4E6F78B52C281E3977B4102350279F87653B6FCBE5D1F150FCF6D37EB73F8C823AF15D39A26C6B78C5E6140317CCB0FD929BAE1A7C34D0FEE595776C755661DF749D1FA268E67B0B2F3716DCD7B0C4B6258C6BE84EBD2C037926183C6306420585E9362478662055132308A61012B324B5DCF4EE3A8A25060139E297D9D183D881AD9420676B7F8546A0CE8CCD92C3B89579492F39BDF0148923F15C2B7F3A77E6942B8B45CEC2307AA935F7A0A2448F4F3B433888036FF4E5FADF50E8913419CA17EE5A12453658FF8AB3B89ABBFEA35BA945800EE237725209496B2B5D598F1F1CB9779268340164C8690210139A4A07CBE0759A1413E0B28E9F88E51AEAA13F1ACCD9748E576866C37D8108067CBE12B1F6BDCA0B98FA8A0FCF457A7757390295BF11BDFB637918DF8868FC4191D94D49FBB1958880434E4C8B74EDC0AEAE415A29BB9F6BE5BC57B9D6F2750B8C435FDFF47F3F22F099B6EBA506B47819624414CF5C857DCB780510F5BAAFAE242FFC4FC66431AEBB85D5EB23745E4F74A49A5F098538D653660CF38BC567CA8D9E33D9B54BB224C79B5E78F4592BE4D232E4D08B64E11DD27E78BC526ED91D016D93F33946F4FC814853168831F3DC868C6372EC955CB40CF4902B4E02078D2C824325A30A9B9FB3A04788C51A7562B17F04A65924D8BDD1404867DC8D703C53933F4EFF7404D0CAE3EB5D6FC85DC90B47CB345E5FFD5A240D350F07566155E40", "3DE98CA3A5795225FBB69C5C80277F9B5AC7D370A567A1FFD130D5C1FED0F588"), + ( + "05C52D7B334DECCC71E083A807F14593A2487B57585EE4589FF607916939493539E121C43D958CB926B720AB46C1100922974A06A7772C485072C97427121A318C5AE2C8A47DF579F75008BA1B8FA6CA5BE19BCB21C55E161BCBB83B9D44F4BB9B3606F8E2730BCB4FAA1BA94E57AEE50052A3F653FB440023AB2F7C7CAFE096BE9844AFA39282AF24046C5735BBA80AF0680B32B25CC7D530B2ABBAA28329DF34CFE0317B0DE34D0A0248BB60314DEB2F80831575765D19199A234B3BB246813D67764BEC958258246BC936D8940F68291D6BD4707CF02AF26052271C0373F825F3B5631F9186926820FB831D0F413992169293B847B3A04742900FE2E4A7C6541F5CFB7287F851CAE79A320114B84391009D2CFC0961629CAA44D9A98148A72DD7CBA8B0A8105C7BD6466E25BB45B4857E1D097534E24180812FEFF4C56FBC6CC14317AE11A381447149A6463DF582F765363E0656FB0402E5308AC62798098CBFF83C6A37C51AFA92BFBD162D8209D02E1313AE3BA215FC267FE693FBEB809B453A0E3AAA28388DE3556EE46793FE59AC60A85D0280552B775EABE2337B0AAA79F238F2F727C5D5A2628B3101E511EB5696CA3A7BDDB2C1F6077C44814D8ED53FC38151A1AC47C37A068E3C222ABAB3D3A2CEF24799B78C73E42316F5866F0D71B5E5C35745851719F0575EC70C03A9C355CC68851B1735F37B845285826A0E55059E87024D3BF02DD14A135AD8B5494367CBBC5E4BA76ED97333D0BB900266203534A11F91124D947BF251C85A4048CB5BBF558125D4C088EA70C3B9771A5E1720C7CA58FD22A8A3428654622981BC663C547408441ACC1683C415163D6AB98CB0A659973081D10EAE875EFB787A08969FBBAB6E35292FFFC7B17266B7506AABD67AC1AC8803772B2A598BCF4B2B2AA4446BE3301DC5B59A1BB2467864A8C7FB8F88736CC265BBC81097220573C5E57A87951582B28A3AB839EE9323B3856E6B9115FCFC79FCFB0113E57B66514CCFA891FBDB137CF4416AF463C1FB48C5317430F92FA82B83FF60917F0CC1B8A60BEAC41A406796E7EB5266656616D85D3EA25A21E97EEBC469B2BBA977B97336223202B072D382B90E0986A1241011E7C85446CD340850F9E20C66BB70F686C8187C111BD3486E581302F28083D7840446359F9585448448E4C6C191A17A05301F19FB9AF37989664A096DE43FF5EC9390F9330EC87342D9614995521A1AB0A387110D183C1636BED16B3A0632AC5F81BD4F18033DBA2CC0D97092722B12F227678777F67CB601391D04223D3CC6526DF4361F7C555A1AC30649721DA548D5C4754668138D294D8316AB1A7597AEC041C8FCBE63D469DD54A45F876F3EF10C70EB0E400CCD9C8AA399E7B377B1AB8E45CBCE34312CBA97F9C3A8BC191EE00C384163023DB276BD6C8F3AB666F60252DD8B5E88A1B1A2A31ACF851C5AB448EAA74797FB5A4DB6BA7824789931501C042EF1D6BB63EBB6B39401113BCE1D6944A0D596699717E315B42DF86CC7008299C36C4B4B884324C6B6E8B780BB4E3EC74F362C90A9451B13849144D95F7D084180BB78E619792B5562C52CB29F6BC0C1803E5C5CB0C74A141BDB02CFB2B05CFB6C6D1B0BFA562745B984164C0D8749B931B76456F086B7052FAA60A11F04724ACB1770080EA3DB91ED5C6C77A903A3FC1ABD51206C4C5DA7760FCF988AE69C52BBE86EB7CA10705BAAB1D59383D875F55081FEF34A07025FC7F14912D0A72BA54414493ADB600A7D104E8602C3DCD686778AB22B4480A06C98C514300BE2C9DBAA33195BC8731536878A7A5CF783B6A5BE8B55B9D4DA00FC258F07223F43A40D9730AC064576683A9E1D786B0DD094BFA6795207705F0A6F9808BFE3B00AC294137093AE45A892F3918D4A8BB302CA067058C274E36677882B5F53A8B6952AE2C483B1F3BCD3A3266DD21167025E27600EC7748BD62C2AB3C09449129AB4B8775227565B2511F51457AAE36495D5862A452767A1422C117977A85F3AAA503B57017DC9B4AA09406B1A18E2ECAF8C761AFBD54427240E442860CFB50111867B856465C2D8A7B428710953BD1062877E2946E9A02334C461954272F3EABE0A6712F527908039A4D890138A641301969FEAE91C348878D53CB8C3D9170A07823CC05B638997928A0C593EDC86BC93C98C59B39B2E602B89129FF40A7771C3CEFE36589852794DED3FCE5A81A20A52C286EA35AB530DCB83DAB5C77B78AD795746D27F10B90D85881E1ED7BD51BD6AD69F7D2EEC361086A4B087F4A76067C53A142C73", + "0CDCFEC9D62E3C8412849705B87B4EA72E6D5553B2EC6F2AB980A9AEFC330224F2E7C8B969DA9658DB470588023F3A2F7DEF4C3E955C34B594F991E55CD7C26C5AC624906D2C23955F104F4577E40F51746C7C738BCD15C34B56A949529A8F0D5DAAF902D4149EDA9CB134D5FD57D7795B52A38E7C6165C22C0AF4B13A127A2A2087F0B144C33464281350D4A4AFD39901CB1D3764A7340D712AA6CD8AB0E1464D84D8CDDCBB74A01C98979E1AAA21F5D86A7D2C882D5F02549A75E1B91A8FD6AD02A0A727B3CDAC1E2CFC2F5DC9BDB9B86DFB76919A42BD98DEEFCF8F9B322E0AB887F522A21AB2234F91F9055D32A5B4E6F78B52C281E3977B4102350279F87653B6FCBE5D1F150FCF6D37EB73F8C823AF15D39A26C6B78C5E6140317CCB0FD929BAE1A7C34D0FEE595776C755661DF749D1FA268E67B0B2F3716DCD7B0C4B6258C6BE84EBD2C037926183C6306420585E9362478662055132308A61012B324B5DCF4EE3A8A25060139E297D9D183D881AD9420676B7F8546A0CE8CCD92C3B89579492F39BDF0148923F15C2B7F3A77E6942B8B45CEC2307AA935F7A0A2448F4F3B433888036FF4E5FADF50E8913419CA17EE5A12453658FF8AB3B89ABBFEA35BA945800EE237725209496B2B5D598F1F1CB9779268340164C8690210139A4A07CBE0759A1413E0B28E9F88E51AEAA13F1ACCD9748E576866C37D8108067CBE12B1F6BDCA0B98FA8A0FCF457A7757390295BF11BDFB637918DF8868FC4191D94D49FBB1958880434E4C8B74EDC0AEAE415A29BB9F6BE5BC57B9D6F2750B8C435FDFF47F3F22F099B6EBA506B47819624414CF5C857DCB780510F5BAAFAE242FFC4FC66431AEBB85D5EB23745E4F74A49A5F098538D653660CF38BC567CA8D9E33D9B54BB224C79B5E78F4592BE4D232E4D08B64E11DD27E78BC526ED91D016D93F33946F4FC814853168831F3DC868C6372EC955CB40CF4902B4E02078D2C824325A30A9B9FB3A04788C51A7562B17F04A65924D8BDD1404867DC8D703C53933F4EFF7404D0CAE3EB5D6FC85DC90B47CB345E5FFD5A240D350F07566155E40", + "3DE98CA3A5795225FBB69C5C80277F9B5AC7D370A567A1FFD130D5C1FED0F588", + ), // modified ciphertext - ("00897D2F39CCAE11888BD6AD0EA8C6301C47144178525092A14B821D004561F1450DD9B2DB99A85E75B1AE401EE6295572318E196260AC425860748937943DF646B5673AC1BAF9B45F9AAE8F676BAF60CBE8255466AC91AF67B398B07BC16B11EA78066B14023A276F1C84348AD82DEBA62599EA83C7435BE9395B437154ED789DE5649BCD12AAC480C25C670E70B890B2777B8B6AB432F778110B1C685B6A00201931A9947E9C2C7C7C08F7708C1C7A7A13BBC71BE5240F2193600731A5443A40D7CE27A34F9B3B689D4C751FD42FE78B6CD45BA2D1755E137B2EBEFC884FE3B04A16674A113CD791595E16B7058C8191D30AB3F89AB1E53C730C05A0268CBA55475634BE3DECBF3FC93E33864D10569AE5C9A613CB8214431FF9CCC499DB0471035936E3B1B0FA7A1AEBC7CD778A79A863B2C1383D5323F43169AB999BA1442649F532FBEB0D407CC2372ABC7566027178BDDEF73BA68956F28827D11BAD3E655045186DA43396F2C29C02062932E9B7EEF46C7AE50484422CC7778AA9D0B8CA343A45A623DA15AC20008E186065DCB7B71EB41B0F7A1E5D3B40E1416F768868BC6837E7845C7819963154B5B41208B3D75F96586026053086A35BDFE870E9952865E33189852CE487038E687C2721AE1C6A1A0F722BB473782F040ABC3743C0D86DA708201A235C2EE9120C04AEB01CB8E36C7C0E27718903A45CC97406499A60857DBB749A1E1CC7E3D5919E017D0F4CAA3E1888BD8B3EBAB035B4161E39568288793FD78420F6D15D0925587C95573EF182DC83150C79239A818A05926C28B5B5C079CFB741C361177CAE058A3B76C177C82A447549A37A0DA28244EBC6BECE59BE5D27B6E79047D1E0498E870244A7A0DE8334C1F3BEB48219F0064DDAA6AD9812994C06A6837CA156D616A76B0B1189949C954136038AC9255E4BFC6979CC2D80B5AAABB93126B64D6E4C7EAB06256F86B4F5B1CF1728303091C9C8A90DCCC73670F839EABA1B137BBE1C9789C1B0509DFA22E9FCADB36A6B3D0863CC6874D525A19A950571FC267DF7B2AE657BE972124B661026133154C1A39B377D3C8854A120508E702F958492DDBA6C4F200D9FB3919796540A20C16E684DE3E9AFD4BB9325158931D23FC2C36BCEFC8F9A71208A8C1F2D4C590E58812D0B93FF227FA4E09AA680A2693461DB86454FA921128CBF54623DE4E5577AA818896760FA642D592A20C992139E250731B67B69770048C4135E220D88DA4293D28D304199B255AC07A32BCA637CA6B72A8CE1930ECA9F9556185894201E1C657568351C477BDB1B1EAD9BAA69524466A39B029954497291279B39261AC3C18C87E24B1D2A597E86112768C310B981C2DE7CA3FF0767D84C7F6BF63DE57171AC697772635BA9C9589773072C63528F528BBA898C00076A5EBA6A55CA621B04822EDCBDBB80B83BF8785D05790A8714C2B498585A52F55C8A96E41D9EBBC60148C300EC2CFD32590C42BA026754F79570F0E1A481A41CE300728C75AEDB9556142187B1654398B12E64535D8F103BFD360B63F4CB30D7BCC4C7AF32E48025F490B9B7C89E302D767C27BE144E228778427B977F411BE4547CC805057AF8A51AB1A699534DF734A230EA8BA035ABBE5B76C07AC516785DA2806BA9C822BF0A92B2E0672611091F386A4D9B261B572421E65F1BD1AEDDE8740490B8CE10567D4816C0C711D3D03A0E049B9B6036D882BABE541E0FE38C1EEAB73DEA9F66773414C503D8E67A79383683454333467B813B3774BBAA3A9072C60637D073157344C4BF12B426D8315484438A01CCE9591ACF2361BBD535DB63212A613A88A024B5B0423340324E2C94D8A8B8022B8CB17A749F7310D40C84654B031396B4C0DA3853D0C6C0491164338D126B8D7F3A470AC7B44426C3D1193A7CE243673C0D05374324CA9043C96C4DC23385D90960999541367863EAC311C96740A0687D1664759663158AAB5C143BEE1A44DFD44AEE6A91C2366B25A5C2B08143C0AB475CA205B7C1C1AC2313F4A1080ED007375585B4D42578355C56869B9FC97D4E7676477476C15AA562F702C6650CD8D88A0C276772C12231B0AD641633A62C8CEFD082A5B5ABEEF098394BA2C4E55D7070A1D799C4186053206C90645C4A900CA2069A23FB5C909DEA7F20C2D35417F046DD937CF7DA8D3E764C526473A3D594D53652C29C492B6034B6B30889AA6652877A84739FB51155858C6B16177C948CE8112422D28B82FFA9BA4EE3EA4D5C93F896623DC8706BEE4092D2DBE6FB277A57085C074136FB4B93CA", "BF78E75459EB54C961443A39375270B876E13B90F676DA2A85A545B12893E6E9678A28BF4ECEFB226679FFA469C028BEFED7DCA3EFD1B59801A4043AD05AA450BA24BB2E336EE9CABAFF39CFF8B9E55F57137271AD356EEB2CDBED441F30BD06B62E00809B311CB86F01F5152ACC1C2D847C908447D9B71201B30E654799CE50076AD481C9B1DB40EEF47992D455D8F5B2E88F9F62AC223D87395D580C0E2578F136CE6F91F880F9627ADE5A9178BA5C5C0B1FC93E6E081FD172131568AEDB72A99C88110172357FF8E89FBD6F0665A65DC429C5FC2DFC24328AA15234977D922DF0E9D78D8405402DFD2DD5FDD8AA16A7C3FE3420339731917189A4C39179E75804211B072C747113E4DD6C23D967D3564CCF6E47C4F1D3EE52F02A6D2E16C9AD74042AB468CA355942B115A5F5D57A95F0CA578E9A35EEF2303F547BE81161EA5C2F7A024488992F1F0CAED68AA24489F45885CEE0CCA063B7D3F85E7305EDBC6B0CFF73624677D92DC8DF66A14D1FD05D43BF41AC9A23CF40CFF3CB21C7A5B0D6CA20BAFC02F7F2B58A45801CAE3C7BC46141425B15B33EB5ECC8CE5EAB005616B91985A32573EE23064C4F46E0565729583C055E992DEBFB090CBC2E5C59643EBC597DF01FEDE1097DAEF224D7CEA21D2BFC76E76EEAE11396A2D37B4D7C02A2D154480103882D9928E380DA4AE69B00393CF5E0F431E9DF8C0A34B5F549471C2802E649AA01F68F08B0FE1E50504EFC0EF9D45D3921969C09D2EA4333D93B9CF4B4CEB06554E4FABCAD23891189CFA71A71062C9AEA0F80043E692D721F0C22509106E114B7CE611523C367A5088CF638271613C7DBE58C8A1B97AB72954A5AAAD391680BA467EB41A96B9E5084ED4ECCE9BEAD8580C6605683516A0BF51BF37AE875CB076FEF5311AF4BEF5F753387293AD2BBE0963FDD16AA8183F4CA7B1D2CA0ADDCDAE784F25ADAADD285894F09CA431408EE5EFB8524107577F76A47B0B0BA40E767B07A1C5673B5F2467250C05B1196641ADD5E25DE9C9A767B40D2F65147396E30C75BC006253E0F35A8972976951E2DD16005B417F1B94C1013", "36A5BCBCD7DD3B6DE9F8875262DFE47C4D1B75CDF71A56CFC64F54E64B92A161"), + ( + "00897D2F39CCAE11888BD6AD0EA8C6301C47144178525092A14B821D004561F1450DD9B2DB99A85E75B1AE401EE6295572318E196260AC425860748937943DF646B5673AC1BAF9B45F9AAE8F676BAF60CBE8255466AC91AF67B398B07BC16B11EA78066B14023A276F1C84348AD82DEBA62599EA83C7435BE9395B437154ED789DE5649BCD12AAC480C25C670E70B890B2777B8B6AB432F778110B1C685B6A00201931A9947E9C2C7C7C08F7708C1C7A7A13BBC71BE5240F2193600731A5443A40D7CE27A34F9B3B689D4C751FD42FE78B6CD45BA2D1755E137B2EBEFC884FE3B04A16674A113CD791595E16B7058C8191D30AB3F89AB1E53C730C05A0268CBA55475634BE3DECBF3FC93E33864D10569AE5C9A613CB8214431FF9CCC499DB0471035936E3B1B0FA7A1AEBC7CD778A79A863B2C1383D5323F43169AB999BA1442649F532FBEB0D407CC2372ABC7566027178BDDEF73BA68956F28827D11BAD3E655045186DA43396F2C29C02062932E9B7EEF46C7AE50484422CC7778AA9D0B8CA343A45A623DA15AC20008E186065DCB7B71EB41B0F7A1E5D3B40E1416F768868BC6837E7845C7819963154B5B41208B3D75F96586026053086A35BDFE870E9952865E33189852CE487038E687C2721AE1C6A1A0F722BB473782F040ABC3743C0D86DA708201A235C2EE9120C04AEB01CB8E36C7C0E27718903A45CC97406499A60857DBB749A1E1CC7E3D5919E017D0F4CAA3E1888BD8B3EBAB035B4161E39568288793FD78420F6D15D0925587C95573EF182DC83150C79239A818A05926C28B5B5C079CFB741C361177CAE058A3B76C177C82A447549A37A0DA28244EBC6BECE59BE5D27B6E79047D1E0498E870244A7A0DE8334C1F3BEB48219F0064DDAA6AD9812994C06A6837CA156D616A76B0B1189949C954136038AC9255E4BFC6979CC2D80B5AAABB93126B64D6E4C7EAB06256F86B4F5B1CF1728303091C9C8A90DCCC73670F839EABA1B137BBE1C9789C1B0509DFA22E9FCADB36A6B3D0863CC6874D525A19A950571FC267DF7B2AE657BE972124B661026133154C1A39B377D3C8854A120508E702F958492DDBA6C4F200D9FB3919796540A20C16E684DE3E9AFD4BB9325158931D23FC2C36BCEFC8F9A71208A8C1F2D4C590E58812D0B93FF227FA4E09AA680A2693461DB86454FA921128CBF54623DE4E5577AA818896760FA642D592A20C992139E250731B67B69770048C4135E220D88DA4293D28D304199B255AC07A32BCA637CA6B72A8CE1930ECA9F9556185894201E1C657568351C477BDB1B1EAD9BAA69524466A39B029954497291279B39261AC3C18C87E24B1D2A597E86112768C310B981C2DE7CA3FF0767D84C7F6BF63DE57171AC697772635BA9C9589773072C63528F528BBA898C00076A5EBA6A55CA621B04822EDCBDBB80B83BF8785D05790A8714C2B498585A52F55C8A96E41D9EBBC60148C300EC2CFD32590C42BA026754F79570F0E1A481A41CE300728C75AEDB9556142187B1654398B12E64535D8F103BFD360B63F4CB30D7BCC4C7AF32E48025F490B9B7C89E302D767C27BE144E228778427B977F411BE4547CC805057AF8A51AB1A699534DF734A230EA8BA035ABBE5B76C07AC516785DA2806BA9C822BF0A92B2E0672611091F386A4D9B261B572421E65F1BD1AEDDE8740490B8CE10567D4816C0C711D3D03A0E049B9B6036D882BABE541E0FE38C1EEAB73DEA9F66773414C503D8E67A79383683454333467B813B3774BBAA3A9072C60637D073157344C4BF12B426D8315484438A01CCE9591ACF2361BBD535DB63212A613A88A024B5B0423340324E2C94D8A8B8022B8CB17A749F7310D40C84654B031396B4C0DA3853D0C6C0491164338D126B8D7F3A470AC7B44426C3D1193A7CE243673C0D05374324CA9043C96C4DC23385D90960999541367863EAC311C96740A0687D1664759663158AAB5C143BEE1A44DFD44AEE6A91C2366B25A5C2B08143C0AB475CA205B7C1C1AC2313F4A1080ED007375585B4D42578355C56869B9FC97D4E7676477476C15AA562F702C6650CD8D88A0C276772C12231B0AD641633A62C8CEFD082A5B5ABEEF098394BA2C4E55D7070A1D799C4186053206C90645C4A900CA2069A23FB5C909DEA7F20C2D35417F046DD937CF7DA8D3E764C526473A3D594D53652C29C492B6034B6B30889AA6652877A84739FB51155858C6B16177C948CE8112422D28B82FFA9BA4EE3EA4D5C93F896623DC8706BEE4092D2DBE6FB277A57085C074136FB4B93CA", + "BF78E75459EB54C961443A39375270B876E13B90F676DA2A85A545B12893E6E9678A28BF4ECEFB226679FFA469C028BEFED7DCA3EFD1B59801A4043AD05AA450BA24BB2E336EE9CABAFF39CFF8B9E55F57137271AD356EEB2CDBED441F30BD06B62E00809B311CB86F01F5152ACC1C2D847C908447D9B71201B30E654799CE50076AD481C9B1DB40EEF47992D455D8F5B2E88F9F62AC223D87395D580C0E2578F136CE6F91F880F9627ADE5A9178BA5C5C0B1FC93E6E081FD172131568AEDB72A99C88110172357FF8E89FBD6F0665A65DC429C5FC2DFC24328AA15234977D922DF0E9D78D8405402DFD2DD5FDD8AA16A7C3FE3420339731917189A4C39179E75804211B072C747113E4DD6C23D967D3564CCF6E47C4F1D3EE52F02A6D2E16C9AD74042AB468CA355942B115A5F5D57A95F0CA578E9A35EEF2303F547BE81161EA5C2F7A024488992F1F0CAED68AA24489F45885CEE0CCA063B7D3F85E7305EDBC6B0CFF73624677D92DC8DF66A14D1FD05D43BF41AC9A23CF40CFF3CB21C7A5B0D6CA20BAFC02F7F2B58A45801CAE3C7BC46141425B15B33EB5ECC8CE5EAB005616B91985A32573EE23064C4F46E0565729583C055E992DEBFB090CBC2E5C59643EBC597DF01FEDE1097DAEF224D7CEA21D2BFC76E76EEAE11396A2D37B4D7C02A2D154480103882D9928E380DA4AE69B00393CF5E0F431E9DF8C0A34B5F549471C2802E649AA01F68F08B0FE1E50504EFC0EF9D45D3921969C09D2EA4333D93B9CF4B4CEB06554E4FABCAD23891189CFA71A71062C9AEA0F80043E692D721F0C22509106E114B7CE611523C367A5088CF638271613C7DBE58C8A1B97AB72954A5AAAD391680BA467EB41A96B9E5084ED4ECCE9BEAD8580C6605683516A0BF51BF37AE875CB076FEF5311AF4BEF5F753387293AD2BBE0963FDD16AA8183F4CA7B1D2CA0ADDCDAE784F25ADAADD285894F09CA431408EE5EFB8524107577F76A47B0B0BA40E767B07A1C5673B5F2467250C05B1196641ADD5E25DE9C9A767B40D2F65147396E30C75BC006253E0F35A8972976951E2DD16005B417F1B94C1013", + "36A5BCBCD7DD3B6DE9F8875262DFE47C4D1B75CDF71A56CFC64F54E64B92A161", + ), // modified ciphertext - ("BE862BB7632AEDB569346A4DFBF44DD3B76416F8A546A3909516B0D8E25CA1116A61E254CAF40BB52182A2B87E5A7C7B157A07690533927B303B0698D93406892212846307EACC26BCE18F4F34B230A098C8444D5EC687F9D3BF8FDACB1EC9865080C2F9C77CEF2657B0812F6AD56EE8E786089A45A6162F1FFC92E5259347374EFA5B417BF1CE1528570D582EB61106EBAA72937A095EB9A374F69A16E85F584B5E1DECA20239A6312909E6535B9993094C512F77252B9B4292E06A4CE84A2993070028623511CA0351630A5B0193A4A477C8A4BB4BA9BB2DE876F9533B8CB77232384277B87EAE6A8F48EB28D8813A25750738348659F78E2FDB87C646BDF9FB254559A0FE38C6D1F6794A6666D6147E256706F4405E0E99AF7D745AA77A7B66E9301ED866BD9A1586D047AC9C2786B763057BC33CB70C628766F912055C40AA4A7423711C4E07C9A1DBC6531AE96137E0B5E4B811A53B9CF6E8ABEB82228F1718353C28EA7C80F4D871B565AD339254DA946EB67A89B8A505534BA4DCEB7F20968969848B8653C7AD4590403CC628E10A29F1B4A95BAD448A0D89B96AC12A7F5644C2739496EBA9B30CF616F27B42023A7753335178140E65886381E685616181608561BE82592840368D2B6DFB20245A623537A20B81D798CCF5761A87BF8C34800B7B80B4B6430B058C62969C2868CD60CA57C4B29E2C2B1AA1D5C7FE665F17AC30FFF5428B373E0F488CAC977D1F62BF3C113FEFC5931A70A3D71ABAB75C3DC306847E075706CB6E8A137012138D65F79F6CF014FDA9261BFB032759261C057BB6E509CD5549C42C3A398C43D61815165873546B21AB7B0B29CB5978B06F67B3C7795A367DA21DA11C3BB1A6A8BA949AF68B68DBD3AE0009AC2530B539274F7A7611F6979000C7593B844ABE44CFBFE6166FB69438969096080322629550966B22A40FD3D84ECBA45C512A3172CC4A1712283897805EE712B18B70EF1790DCFC94B191B0ECCA2A14911A0760C42A852FF7D31E8118BCAD459393265EAFF3C61C66618BF70CCC75BACA898061C46478274986F2346256343F20C058F994FB547A0BE68EA99938A7029B9B86A9F38687A17B1950728B204CB70549B115A17EF31321A7235933615FB8C7913B4BB3B85BA7873447810631AB480CC73A087992B36906AAD899BD68A26A137045E2520484A51618116AEAF2CC6A7CA918E45D2C405AC5512E9289A089D74B01FA9227CAC3F20783D6C099715681229AC4F97390FB63635B0755588803808B6FB0C2AEDB0665135122267927361B2D66BB1533436692F980DBE5AF926C2A96703E0CBBA855758422B4CE0E019A5DC4C7B3110E3D519CB68BB00A87905F068A8C689BD871CBAB2C6341C008346091CF1702EE733A8D83A56D878DB20B24BBDB9897E36FE3310950AC19A891499C089759F24DA5D190108C55ABA1CBABEA355CD15E7BA8200F67B43CEC79B0F790E3F4ACBA165A2DC7235CA9C10061034B425743A2988A207047132071398F22D6AC68002716892A2272B4D12481046709F1216E25C90CF3B72FF5AB9E3B71878D75AA6639975AA21ABECB83707A1E48AB55EFC563B2A00C22F34273C91231A94655199CD8E2420A109121A268EA1787DEB467940C5697AB5BDC0C3B16316A50B96A78099E10D54BB763BF15A76552325028E5AE1780458C3AC1831A7967C5260592B1BAEC14CFD952B8251A31480DF7147DA35592D38592B748A5F248C16B20C563C35B9F96AD92489F2DC109971691099231EAB2BAB11426DDA8A1041C601A579D68C07AB8F102E1C1BE889A563852BF59A9BD2442CCEE1957F65963062C5333996CBA731343839D250751E9294B4DC83CC0152E4C2790ABD8698DFB90C609A5755A3E5CD259CE37897AB04DECA0068E81767383996E622DE5B53E02BC0EDDB4B6A49C1364A12FFD4B0161219EC8945112A0CC76D1C436399D44B719D846318639AD0BD592D30C4491D19D74A6B3C897A7BFD954CC912A60933B3AF0B280F40252F70E4E858FB738911909532C048C2EC25491E6B9DB8ABE6DE0A722468A3A544F02A7CB16E731B57925B49A9F48067ED5D80F8E97C266E0020953319C1B773CFC74B2321754511CE8B597B1E708A9582839355C16EA8F4DC6605A086DDD22936A0ACEBCA76B5246C06BC846CCA8DEB9E406B923099784F17FFEF95CD35B8DAAED33172D83B31EFB2277EC16A29146FCBCD797412D05D2CECEC79E8521EF5E326FFDF527209190D6C93E21623B0839BEEED3313D0061E298AFEA9EEF6270F73E26E3E513694B", "4635A376E61E4A6FE9E462337229DCDE81D11F87E38D3A37B2B3487BB9AC85FFC9D45881F10420D2953CEEF13715B2AEA5F3E3CA4F6757BC27CE7AAF07998969DACCB093C2A584C21DE4D6E3D1BFDFEED91550F4D2AC5972B1A581D02C1EFD9E28FDF32B906F1DF0FBF73527DB5CCF326931407A56A64035554CBE3C9E3CBCC2794D9B0022A87A31C322C083B8A923C75DB70485AB2EF4E89929DF369065D3352EEC7F6D3F8607DB77F1E25AE4F4C46FA724793772175D48439AAF13CABC7CD70BE5FDCFAAE1079EFE14AA1F1A89BA953E5918142C245D14B03A5BE58B696F80F07E44065FCB38E63A80559063E0001606C048F1DCF29DD02CABAAEC247B36F132B4A9413D4FDBA79C2068459505B42B3EE719B9E4B785E1405BE2843A30B17F351B98AF012091B91C414F1B8DCBE92FF47C1F979D3FBD4EAC19092A6B7F54CD3ACE6925114659CE8C84B44C5B577B1E95EFA7F5EC57752AA03F10781156FEB3514034BD382470DB6C85AC9E6AC63C45D5AE51CB620665C53F5E15371F1F4888B6EDA8C7F064A44DDD065DB2E57ED7A7BD096E51C7670C9442DDAE096CE0A0BCEB1711E720038A3437AAF4F2E343DE2504AA1C3993F0C2AF6567C36376F0E863F6A42FA5151A3A9C2B4CB72CFB9EE286608AAD9B8F57F7C2162EAA074A1F09DC66119A08FF2C703A1C8DC43C309F4E6B10C5A96BA87AA1635D2293B12365E6BC2FFEB0D15ED8F4360B9B0B790DF575A1904C53B40F15E301F78B38235D8D959C7DF7990459E0C08249F7AB50D23F940A1F511F61D9E404F8EB23DABBF0D4C82432E0E043613B556B96DACA4094BFF69BC989078D7F3D396ABF0ED719E19EAF4C80B985F0AF88174FBE17F8C666C054DDA825896ED0CBF4001F846A7CD74A0AF1487EB22A6692B5D4824A5C3B1F5739CABBF9DEBF53521B375A83C49A81465F6DD7CA562244A137F68CC8C9874EE0EADF84A2087F055669616D4EDF52A05FF271002A1416633045A04B3BEB20119EA05E9EBC2924ED42B74D5086CFA3D52EECEEA0E7D7D49FDEE8BB06BFCF997CE992644D510982E25B1A74F69FB5A9AC28E138", "E127EB4228395463C415F558464E7A91A84AA92C2F476D8AB05BCA3F5435599E"), + ( + "BE862BB7632AEDB569346A4DFBF44DD3B76416F8A546A3909516B0D8E25CA1116A61E254CAF40BB52182A2B87E5A7C7B157A07690533927B303B0698D93406892212846307EACC26BCE18F4F34B230A098C8444D5EC687F9D3BF8FDACB1EC9865080C2F9C77CEF2657B0812F6AD56EE8E786089A45A6162F1FFC92E5259347374EFA5B417BF1CE1528570D582EB61106EBAA72937A095EB9A374F69A16E85F584B5E1DECA20239A6312909E6535B9993094C512F77252B9B4292E06A4CE84A2993070028623511CA0351630A5B0193A4A477C8A4BB4BA9BB2DE876F9533B8CB77232384277B87EAE6A8F48EB28D8813A25750738348659F78E2FDB87C646BDF9FB254559A0FE38C6D1F6794A6666D6147E256706F4405E0E99AF7D745AA77A7B66E9301ED866BD9A1586D047AC9C2786B763057BC33CB70C628766F912055C40AA4A7423711C4E07C9A1DBC6531AE96137E0B5E4B811A53B9CF6E8ABEB82228F1718353C28EA7C80F4D871B565AD339254DA946EB67A89B8A505534BA4DCEB7F20968969848B8653C7AD4590403CC628E10A29F1B4A95BAD448A0D89B96AC12A7F5644C2739496EBA9B30CF616F27B42023A7753335178140E65886381E685616181608561BE82592840368D2B6DFB20245A623537A20B81D798CCF5761A87BF8C34800B7B80B4B6430B058C62969C2868CD60CA57C4B29E2C2B1AA1D5C7FE665F17AC30FFF5428B373E0F488CAC977D1F62BF3C113FEFC5931A70A3D71ABAB75C3DC306847E075706CB6E8A137012138D65F79F6CF014FDA9261BFB032759261C057BB6E509CD5549C42C3A398C43D61815165873546B21AB7B0B29CB5978B06F67B3C7795A367DA21DA11C3BB1A6A8BA949AF68B68DBD3AE0009AC2530B539274F7A7611F6979000C7593B844ABE44CFBFE6166FB69438969096080322629550966B22A40FD3D84ECBA45C512A3172CC4A1712283897805EE712B18B70EF1790DCFC94B191B0ECCA2A14911A0760C42A852FF7D31E8118BCAD459393265EAFF3C61C66618BF70CCC75BACA898061C46478274986F2346256343F20C058F994FB547A0BE68EA99938A7029B9B86A9F38687A17B1950728B204CB70549B115A17EF31321A7235933615FB8C7913B4BB3B85BA7873447810631AB480CC73A087992B36906AAD899BD68A26A137045E2520484A51618116AEAF2CC6A7CA918E45D2C405AC5512E9289A089D74B01FA9227CAC3F20783D6C099715681229AC4F97390FB63635B0755588803808B6FB0C2AEDB0665135122267927361B2D66BB1533436692F980DBE5AF926C2A96703E0CBBA855758422B4CE0E019A5DC4C7B3110E3D519CB68BB00A87905F068A8C689BD871CBAB2C6341C008346091CF1702EE733A8D83A56D878DB20B24BBDB9897E36FE3310950AC19A891499C089759F24DA5D190108C55ABA1CBABEA355CD15E7BA8200F67B43CEC79B0F790E3F4ACBA165A2DC7235CA9C10061034B425743A2988A207047132071398F22D6AC68002716892A2272B4D12481046709F1216E25C90CF3B72FF5AB9E3B71878D75AA6639975AA21ABECB83707A1E48AB55EFC563B2A00C22F34273C91231A94655199CD8E2420A109121A268EA1787DEB467940C5697AB5BDC0C3B16316A50B96A78099E10D54BB763BF15A76552325028E5AE1780458C3AC1831A7967C5260592B1BAEC14CFD952B8251A31480DF7147DA35592D38592B748A5F248C16B20C563C35B9F96AD92489F2DC109971691099231EAB2BAB11426DDA8A1041C601A579D68C07AB8F102E1C1BE889A563852BF59A9BD2442CCEE1957F65963062C5333996CBA731343839D250751E9294B4DC83CC0152E4C2790ABD8698DFB90C609A5755A3E5CD259CE37897AB04DECA0068E81767383996E622DE5B53E02BC0EDDB4B6A49C1364A12FFD4B0161219EC8945112A0CC76D1C436399D44B719D846318639AD0BD592D30C4491D19D74A6B3C897A7BFD954CC912A60933B3AF0B280F40252F70E4E858FB738911909532C048C2EC25491E6B9DB8ABE6DE0A722468A3A544F02A7CB16E731B57925B49A9F48067ED5D80F8E97C266E0020953319C1B773CFC74B2321754511CE8B597B1E708A9582839355C16EA8F4DC6605A086DDD22936A0ACEBCA76B5246C06BC846CCA8DEB9E406B923099784F17FFEF95CD35B8DAAED33172D83B31EFB2277EC16A29146FCBCD797412D05D2CECEC79E8521EF5E326FFDF527209190D6C93E21623B0839BEEED3313D0061E298AFEA9EEF6270F73E26E3E513694B", + "4635A376E61E4A6FE9E462337229DCDE81D11F87E38D3A37B2B3487BB9AC85FFC9D45881F10420D2953CEEF13715B2AEA5F3E3CA4F6757BC27CE7AAF07998969DACCB093C2A584C21DE4D6E3D1BFDFEED91550F4D2AC5972B1A581D02C1EFD9E28FDF32B906F1DF0FBF73527DB5CCF326931407A56A64035554CBE3C9E3CBCC2794D9B0022A87A31C322C083B8A923C75DB70485AB2EF4E89929DF369065D3352EEC7F6D3F8607DB77F1E25AE4F4C46FA724793772175D48439AAF13CABC7CD70BE5FDCFAAE1079EFE14AA1F1A89BA953E5918142C245D14B03A5BE58B696F80F07E44065FCB38E63A80559063E0001606C048F1DCF29DD02CABAAEC247B36F132B4A9413D4FDBA79C2068459505B42B3EE719B9E4B785E1405BE2843A30B17F351B98AF012091B91C414F1B8DCBE92FF47C1F979D3FBD4EAC19092A6B7F54CD3ACE6925114659CE8C84B44C5B577B1E95EFA7F5EC57752AA03F10781156FEB3514034BD382470DB6C85AC9E6AC63C45D5AE51CB620665C53F5E15371F1F4888B6EDA8C7F064A44DDD065DB2E57ED7A7BD096E51C7670C9442DDAE096CE0A0BCEB1711E720038A3437AAF4F2E343DE2504AA1C3993F0C2AF6567C36376F0E863F6A42FA5151A3A9C2B4CB72CFB9EE286608AAD9B8F57F7C2162EAA074A1F09DC66119A08FF2C703A1C8DC43C309F4E6B10C5A96BA87AA1635D2293B12365E6BC2FFEB0D15ED8F4360B9B0B790DF575A1904C53B40F15E301F78B38235D8D959C7DF7990459E0C08249F7AB50D23F940A1F511F61D9E404F8EB23DABBF0D4C82432E0E043613B556B96DACA4094BFF69BC989078D7F3D396ABF0ED719E19EAF4C80B985F0AF88174FBE17F8C666C054DDA825896ED0CBF4001F846A7CD74A0AF1487EB22A6692B5D4824A5C3B1F5739CABBF9DEBF53521B375A83C49A81465F6DD7CA562244A137F68CC8C9874EE0EADF84A2087F055669616D4EDF52A05FF271002A1416633045A04B3BEB20119EA05E9EBC2924ED42B74D5086CFA3D52EECEEA0E7D7D49FDEE8BB06BFCF997CE992644D510982E25B1A74F69FB5A9AC28E138", + "E127EB4228395463C415F558464E7A91A84AA92C2F476D8AB05BCA3F5435599E", + ), // modified ciphertext - ("1E147CBDB5081FCC5B41461AE6FB10BAA09DA3BA057C9B2CE0793F05B525DEEB046C42B3AE2A14831014A367BC4BE913A1C3779110ADA4386918287A75F2AC96F6A010352DCA6B5189BCBFEBA5B524FB3A2E203FE07A1B62CA9D18511050DB37044429C1D0B5690B3224A0856C46B78A63A1335B7696B9C792A7B8DB696DB4AB21E83523B9E75FAD29B940834A389798E6042174526E4403B8977C1C7881132C8CB5348BB34B40B0966CCAEFEC19904851947A91D7E25C8D7611FA15C003A0C938100FA697536136CE6C848DE34A71F86BC68EF02D3B460FAB701E41D70050CB9F88A9973D1C29F2719CA955684CE225D033B89F8927E3171672D5654E636B03172347EA62216BBC3DDA0BBF052C31C089EAC65684B07CA71849CF07809BFA48BAE59EF654B3E300932403A953F3CB2CA82CABFC55B3C4B9BA0420988716E4EC3F6D460B099B2923B517A4462F2113559122898C11024406516049B39E4089198CA85D1B26E01303C2336107B75181A4AA4C39456E6BC0E3D85C46A700F48C0B738703E0B76C86081ED01B6A2911B73B4779EC36354B9639B8A5971DD5623C9965F64283EA578558A49CBD5CB6BA71AF90509E69F7BAE5B2C76BC71700D91CE7B32E127BA73A134190290455179ECD15CCDFD13FCC548159E17B1D2C16AC477AB0C721E55696891A86A7B363D465AF65CA130701A91790420F38AA76788CA0918B5E7A5F1D708C1FD7855C8147415A658CEABB1236A89FE2964A0B8E9B80A926A3756CBBB1B594637CEA3118F23256F1513FC2076F10C19F323CCA44B51B393E2C042B44F9581B719609976A07A768DED88FCA270B7E64480EF18628BC6B463BBF2EE9CEBE6811B2604E3C50757CA658003291EBEC89F8403C79F4CB3A1195C342373BD012D215C90C66842DE394E6E65FC2255CCDC239B72884D63A8EAB8C70632225FB62600BB3282BD82C821867A73C2D2D7BB9BCA173BAB812343015CD811A4408BF637282C5F21C8B843584CB1BA56366AFC029726951370C22D1E79195542F98D43E3D1CB4743A16BBA219D8398BD65065F0A2B4C8019A1740121F3C2892743B087010B5F559150C84651A1B6D12AFC9F684D93668503B518A9070CF6CA3BFD7329A44680E7368F4D785300783F09091820A0D851875DE94CE39E9753A88CC4993C01D6582B1E52B53D51BE441AFA099CCA9B4A78469BCE0B01B3892BEA402ACCABAC6C52C867B8A776613648A963C5C64A978C63F62188A740719A5808273A9BD84532914F783C3B0B1C5E54CCBD45FD257C436DA129030A6A7500B39E220FA67115234330EA330801C1C040709EE5968605AA773EA23E038A7389C5EEB64A645898686322532935FA950BAAADA490B249EC7D5C795113EBF2B6916A0772285A3A678470A42A7F677A53002115F54A6A712AB688C2333B6819472525D339B20959D5634C185976059846AF710C2AA9A127F842D02D54C23D7866453953282A2D4597273E3BB1F7C71E94C3707E38552B2CE6172B989948D61A01945D2599E5A99D05544DBD80A75B273AFC4CC906389EF44164ECB87BFDAB29DF42B498343E89A7850379311500C1CC8094B0227947CB17A3008EA163257926461CB83076B6D09EB80D190287D5728F8F7179D5A3E9D6962CF8878BB695C6F2A2A6EC27F63B87C4AE1143D30CC9135680914A53E9A0777B99BD0F7C031CBC886F9991D67C1169883B086C39D174EEF6A1495409EDD91839D66BD1AD5209AB32A97230BF37129E273726AC7BAFE7CCEF37365581BCCD03729E1235C80B475F7B563AD31C81D49B5BCD0AA24593971E93EDD963D31545418DBA162920FCB3BB1C779212B726823540C1167172E28BBB5714292956C6F5149829A61F1D3A6194A3A7D99CC54E12C9D06BACDD011ACB31A3F65AFA7EB182523937A49CAEA6BB4D7A6983B6334A23C4BFB041AC44CBEBAFC5225EC0951D44973090E28299187414C6EFC9C07763B7BAB5D6790B97987CF8DF2958250BAF24814B5E30F4F9A0A99F03EE39B8FA7498EB7ABA840E50DB2C3AB9D869DC7B60CAC6455F4531F3B57804F414CF0E0CAFC3978846C4C16C0CA3698C5B06519E110C6FB7B2D56D741CE13556D022F9E3775FB96291AAC43E0F7374815B029205150122FC0DB12539110D82A7561C94F046918B0C80DC99F4E41D8AF986687A90ABA8E61C37B6C590F7F2F2F17A6802F4EAF957851DA28366FF95FA05B565ECF7B49B862D5A9EEA0B60F7B4C1B358B94DD04925D84D23F87A62080DDFAA4AFE354C71B17ECE01398A09A50828D4E52CD", "8367DE68855C4267F0152267D9A84CB78354D2AE11429EDAAA8732CF0874F3BA113E750C33FB2B5F260F5074EC357737345E82045BB988AE7DCDF13CEE3FA931813DB67AB6ABB51D152277F4991FCFFFFE12FACDFCC3960FA7646EA287A0089FEEF0608AA689C6DC4E2B1B194DE4AF5C1C05A36AE7E265974709D795D3508741980CF57C14501DF3F11C9B21EBC056F86450CF3CB716B8A7B6532A9F3D8856770A455156470BF85E9E34ECA3569147020BE908473AE44DBC89A726A974956B113AA490CB4F3B3997A81121F7700210C01D6CB10DD18C903CE8127418DFBE03BC0AF678556A038827A35A82396F2F35CF68E7F0B4883664A1A098E9578A375B93D39B07B655F06A83FB3645F56AFFFF975574EE395C09752063BC8EDCB075DC71EF8EF3CA17639CA6191775C1576B5C0A2808701CEF8D7C9A6FC73C8D2B0F0FC3923F5014A8442D3F8CD22A43C4C99B6D498DB3F7136B785463E4138981314DB54B40C3496006E3B9AA840EBC1470732A1682B53573D02BDF5B8E1E5BE76E0472AFAD94C839A9577B5FBAEB1D9389815A60A26B2F8580A3A0F5A304FF6C128D7CC445253FF3C42027F66F1590EDA6A91C697721BEC9A6245FC88429C7C8C12780332505EE5A34C6AC48A29675A2894FB3055709339016DC9584C35A44526C4EF2C5FFBAFA22B7BCE03E5BB20256D24B3A5BA1C41B4648B02D841A55C6FCE58A58F7842969D72B3F2141FBDBE341531205AB350AE77E4A6AB01B997C0C9FFAFF46ADE368206952699186068F5567CE494AB302A31C5BED291CC3DDF81053C27F9BF1F215F15ECB2101464079555A5F332FF862CF600C5BF0A195DD91083B86BC6EC1E08506D44CFDAEE6F3D6D6F086A239E9A89FEFA845E57B04EFE62B35B54087908FA81BDCFC049B46A959F5EC7F4022E58831E3B523AF7A01D1EBC1580CD629E6BA2F690D959CAF01055F978B88CF695F2E5441A688D7D876994463C18A2B409560DCD814DB8FF6D23B6D51FD9F6F20AF63987DFB48D5F9C3EAB09C3343FADBCC5D7192A4B17D80DDFCB3E6D2F6531574F11B3F649D04F7E719BF8E1BE75A25", "CD6BFE40563C026B09692C7BD13181619101E6F35773F0B00C686650DCDC4EEC"), + ( + "1E147CBDB5081FCC5B41461AE6FB10BAA09DA3BA057C9B2CE0793F05B525DEEB046C42B3AE2A14831014A367BC4BE913A1C3779110ADA4386918287A75F2AC96F6A010352DCA6B5189BCBFEBA5B524FB3A2E203FE07A1B62CA9D18511050DB37044429C1D0B5690B3224A0856C46B78A63A1335B7696B9C792A7B8DB696DB4AB21E83523B9E75FAD29B940834A389798E6042174526E4403B8977C1C7881132C8CB5348BB34B40B0966CCAEFEC19904851947A91D7E25C8D7611FA15C003A0C938100FA697536136CE6C848DE34A71F86BC68EF02D3B460FAB701E41D70050CB9F88A9973D1C29F2719CA955684CE225D033B89F8927E3171672D5654E636B03172347EA62216BBC3DDA0BBF052C31C089EAC65684B07CA71849CF07809BFA48BAE59EF654B3E300932403A953F3CB2CA82CABFC55B3C4B9BA0420988716E4EC3F6D460B099B2923B517A4462F2113559122898C11024406516049B39E4089198CA85D1B26E01303C2336107B75181A4AA4C39456E6BC0E3D85C46A700F48C0B738703E0B76C86081ED01B6A2911B73B4779EC36354B9639B8A5971DD5623C9965F64283EA578558A49CBD5CB6BA71AF90509E69F7BAE5B2C76BC71700D91CE7B32E127BA73A134190290455179ECD15CCDFD13FCC548159E17B1D2C16AC477AB0C721E55696891A86A7B363D465AF65CA130701A91790420F38AA76788CA0918B5E7A5F1D708C1FD7855C8147415A658CEABB1236A89FE2964A0B8E9B80A926A3756CBBB1B594637CEA3118F23256F1513FC2076F10C19F323CCA44B51B393E2C042B44F9581B719609976A07A768DED88FCA270B7E64480EF18628BC6B463BBF2EE9CEBE6811B2604E3C50757CA658003291EBEC89F8403C79F4CB3A1195C342373BD012D215C90C66842DE394E6E65FC2255CCDC239B72884D63A8EAB8C70632225FB62600BB3282BD82C821867A73C2D2D7BB9BCA173BAB812343015CD811A4408BF637282C5F21C8B843584CB1BA56366AFC029726951370C22D1E79195542F98D43E3D1CB4743A16BBA219D8398BD65065F0A2B4C8019A1740121F3C2892743B087010B5F559150C84651A1B6D12AFC9F684D93668503B518A9070CF6CA3BFD7329A44680E7368F4D785300783F09091820A0D851875DE94CE39E9753A88CC4993C01D6582B1E52B53D51BE441AFA099CCA9B4A78469BCE0B01B3892BEA402ACCABAC6C52C867B8A776613648A963C5C64A978C63F62188A740719A5808273A9BD84532914F783C3B0B1C5E54CCBD45FD257C436DA129030A6A7500B39E220FA67115234330EA330801C1C040709EE5968605AA773EA23E038A7389C5EEB64A645898686322532935FA950BAAADA490B249EC7D5C795113EBF2B6916A0772285A3A678470A42A7F677A53002115F54A6A712AB688C2333B6819472525D339B20959D5634C185976059846AF710C2AA9A127F842D02D54C23D7866453953282A2D4597273E3BB1F7C71E94C3707E38552B2CE6172B989948D61A01945D2599E5A99D05544DBD80A75B273AFC4CC906389EF44164ECB87BFDAB29DF42B498343E89A7850379311500C1CC8094B0227947CB17A3008EA163257926461CB83076B6D09EB80D190287D5728F8F7179D5A3E9D6962CF8878BB695C6F2A2A6EC27F63B87C4AE1143D30CC9135680914A53E9A0777B99BD0F7C031CBC886F9991D67C1169883B086C39D174EEF6A1495409EDD91839D66BD1AD5209AB32A97230BF37129E273726AC7BAFE7CCEF37365581BCCD03729E1235C80B475F7B563AD31C81D49B5BCD0AA24593971E93EDD963D31545418DBA162920FCB3BB1C779212B726823540C1167172E28BBB5714292956C6F5149829A61F1D3A6194A3A7D99CC54E12C9D06BACDD011ACB31A3F65AFA7EB182523937A49CAEA6BB4D7A6983B6334A23C4BFB041AC44CBEBAFC5225EC0951D44973090E28299187414C6EFC9C07763B7BAB5D6790B97987CF8DF2958250BAF24814B5E30F4F9A0A99F03EE39B8FA7498EB7ABA840E50DB2C3AB9D869DC7B60CAC6455F4531F3B57804F414CF0E0CAFC3978846C4C16C0CA3698C5B06519E110C6FB7B2D56D741CE13556D022F9E3775FB96291AAC43E0F7374815B029205150122FC0DB12539110D82A7561C94F046918B0C80DC99F4E41D8AF986687A90ABA8E61C37B6C590F7F2F2F17A6802F4EAF957851DA28366FF95FA05B565ECF7B49B862D5A9EEA0B60F7B4C1B358B94DD04925D84D23F87A62080DDFAA4AFE354C71B17ECE01398A09A50828D4E52CD", + "8367DE68855C4267F0152267D9A84CB78354D2AE11429EDAAA8732CF0874F3BA113E750C33FB2B5F260F5074EC357737345E82045BB988AE7DCDF13CEE3FA931813DB67AB6ABB51D152277F4991FCFFFFE12FACDFCC3960FA7646EA287A0089FEEF0608AA689C6DC4E2B1B194DE4AF5C1C05A36AE7E265974709D795D3508741980CF57C14501DF3F11C9B21EBC056F86450CF3CB716B8A7B6532A9F3D8856770A455156470BF85E9E34ECA3569147020BE908473AE44DBC89A726A974956B113AA490CB4F3B3997A81121F7700210C01D6CB10DD18C903CE8127418DFBE03BC0AF678556A038827A35A82396F2F35CF68E7F0B4883664A1A098E9578A375B93D39B07B655F06A83FB3645F56AFFFF975574EE395C09752063BC8EDCB075DC71EF8EF3CA17639CA6191775C1576B5C0A2808701CEF8D7C9A6FC73C8D2B0F0FC3923F5014A8442D3F8CD22A43C4C99B6D498DB3F7136B785463E4138981314DB54B40C3496006E3B9AA840EBC1470732A1682B53573D02BDF5B8E1E5BE76E0472AFAD94C839A9577B5FBAEB1D9389815A60A26B2F8580A3A0F5A304FF6C128D7CC445253FF3C42027F66F1590EDA6A91C697721BEC9A6245FC88429C7C8C12780332505EE5A34C6AC48A29675A2894FB3055709339016DC9584C35A44526C4EF2C5FFBAFA22B7BCE03E5BB20256D24B3A5BA1C41B4648B02D841A55C6FCE58A58F7842969D72B3F2141FBDBE341531205AB350AE77E4A6AB01B997C0C9FFAFF46ADE368206952699186068F5567CE494AB302A31C5BED291CC3DDF81053C27F9BF1F215F15ECB2101464079555A5F332FF862CF600C5BF0A195DD91083B86BC6EC1E08506D44CFDAEE6F3D6D6F086A239E9A89FEFA845E57B04EFE62B35B54087908FA81BDCFC049B46A959F5EC7F4022E58831E3B523AF7A01D1EBC1580CD629E6BA2F690D959CAF01055F978B88CF695F2E5441A688D7D876994463C18A2B409560DCD814DB8FF6D23B6D51FD9F6F20AF63987DFB48D5F9C3EAB09C3343FADBCC5D7192A4B17D80DDFCB3E6D2F6531574F11B3F649D04F7E719BF8E1BE75A25", + "CD6BFE40563C026B09692C7BD13181619101E6F35773F0B00C686650DCDC4EEC", + ), // modified ciphertext - ("22254A698964FE9234C17549A8296C00E440BC71CE01C9302C4A3D11CC4C86A74846A6C54434382EE92EB985916AF48351125A73900A699BCBF2A152983257B2F28ED16286BC2C1D64247E3FD83FB69988C3752172E4A21C9199BFC05A0B74AE32A0B73E74B0502488DB0C416A0834F4816D682173D32038004DC873038144DAC76C61637E35B3C0F37B8AB5906CF67EAD97C00D57675DD1AA154703DC9B8ED2F8BC57DB05CE47430662553C507804F97856DC4F45422E8B502714CB587A3014637534D7C3742B762230FCBE7668061821A1191A41A5E447DBB560B5267B4EAB4F8ECACB4DD201A2173BA2E55395DBA0993C83A903C328801539F734EBD6232E475135D9AE6774C56B9083FAEBB308D7214F845EE57332D2D06F89C4840782C35471538C8403A6B751298BB1E7274B165C4CAEE50A8ECA2C90DA8A02A25E81228C9281A6E6C82C16F77F3139B6E37CCC35EABC37416FD3F37E5FA1236AB4958E0B7A2F363725911E945778F768CD9F35241180162A841A3B45A4CD2B3A3E013E90B525D7B338C1E25C1D2776D9F6900DD0777C330A4E4B5A90071AFF850A409A361BAA2B0995C41F37AB03DB8AE24312CB6864EE69786C29CDE9E20359571349638C6DC11D574348B037B7B50821BD3CC38607B529400B06F2341B68CAD3E4A62679A5FF6B019B77C64CD15FF830BD5BB841F1FAB141F7609AA9540C2B33D0BA3730B089D7680F06D468C716A8078B36E42A150E775E88BB00FE98811D8A87EC11963F161C76229B42893E81A902F2A755E4266118A7BE4B667C46C56A638A23A3C89A57B1673F92405AD7275E8CB0FFD91B78C2C40EB93DAC278611D08AC11A3278A22D1D555D7B0562226AACD11464D4E67FEBC3A6BFBC41D775AF11383563F963C9634C979B9DB81100052712CA65157C6C308B55062CBAC0BAA7155E263AA6A51B7153C8B6803940871309E246325C99A81241ECF32133767308FBA4B6497729893F2345CA59827F36ACA3BDE2C834949D27535BB20AAD87B91189EA7C67372D521346E32CCC4A417657CC4178C09E5ABC66483B84EBC9C2141C5318F551C5132515707F858460EA953A1F4733A782BC89E3BD44B4B282E10553B4B73366C42464994D435EFF2363D71C50B7D6CC45BA52461ACB59F9677C939D9C7BC7AF992ED03262787BB4CCBCB6A413AF13E18CCA3754264353BAB3CE1A2AC0415508B47B25BA7BBC9EF335827A3099151F03B39996F1CC9FE603CBF9A43FB10726682BE77767A1323AA9A07250C29310A9C4B5AB08A700ABB0E7CF572C81E0A4C28FB76D8B0AC8E3B35D9BC4089930671446548F65BFBB6CA2A7EB9B3CB61643B0A1388804D5C51A707033A6ECB8808B1DCE18B55860BFA1929CB84874E8617A465512691B4F4EB7755B730D3837C8729164C741C14FB7805C936C70FA051BA607B8EB0B0936C3C2701C6B2C7F2542B31C5A53D59CC9F7840311EA3F2E96939557754E665C968926017691C48A548FAA14AB0196D947C415F912E5AC316F81082BE2BD30735289DA87CBB86E71C07162F22952B6406BAC6A528151E74B18163A3D911AAE52057A13F85158C489D3E90BFADCBD48AA4136483DF069C0A6C6C72499B07F21077BD1B4015B50F05B5E2B98AE8FD24B27509379E87C5F308704215DC17707689913A68BBAB76C22A85CA540E8B0267C857AF49BB7242E89D63B697A0332FC588FCA1B987741C05A98A7669DE7B12348472F7EA70BEB68A008C8234B10B18C6A8067607CF451A47430BE3CC01FD718A2C49962E9F90D9C02644A7A2E96731AEB56601D0651638649E4FC299292504743214191312F11B060D56510AA00588B8709AB98741CC5D1126AC0C64B76487716C216BFDBC13A6A4D36D6B55206519618CDB4B2C55CDA81B35558F3D45C3818193B3BC52475521F704DD9947E90280C1FA01C894BBD81EA19B87A15834544CF51232C0A61C5E40684054D84C37602ECBB219C63F7E158137768EDA0650ED266221745FFE5C15F5C3B948915521015E336A724C76182D835BEB22503EA0904C73567FB08E03307880700DE150E0D013D8C1A8E83270035F9249F220043B836C53B6390832C6EDBA52CA131307412248B80FA2162ED54B4EE23853143817F72C6D3A6A1C89AB236085BD9FC9118E6A7A96178D28C13E688E0F9FA82901BA3DF12C3BFD734BB6A5066BADAA40B8375C67CBBE0BBA80A6F16C841CFE33FDE7973C0CBE5E77DE4F995B49155FE393C10008392B890A5D0EBBF3217FB4CD2F002DA3B0CEE11DE3AFFEED891E8DDFCA23A30", "3318D3B70DF3E6D2BBA0972E36E9D5F5FE8902E9D581186C930E091FC038E6E0C13465432B3F99DE5AF86BFC8DCF748AA094C713A72868571C4BFE10D9D3774A7829135C3F5A17CFF577A4EDDB60DEC5B0359CC403C479795B931CFB8B6EFFC323D0B0F4CC503C59225E60A55D4C8AA77DAA69801F26DFE1A5CB003B9206F6D68FF45B1D7E962315E8A9E2078104543289B9D06BA666444C2603F730D327C1AA2099C0356CE81A499FF3B5894363C8FAAE3EAA60A9C0A2BD052877082915E21095E365387E05A6F0DD41B72A4C77D795143156B76F1DA5B779548101686009FBD95EF9DABD470169400393D9B00FBDBD8F37F2DB323A109AF2476604D3CC03F71247ED16CBC6F202E38AA58934A5724E0D0497981C30EE7E340A629884D1808041611FA33548C900EACFA04687F40A7FFD02789DC480CE92B5B8F1F3D6F25A2E5F05F647EC5E718CB7A27EDA0F7B95374238D5D03FEEBA7560C974A5C9C3D04E92D63E3BA2D04A56330ABC5820C6FDDCCF11F21726590984B6E24A95CCBB1C7B577DD12DF391A4900A21232583ABD01630790152ADDD8B50871945D5D36909904D8F669C5788A0C11D9815FB68E6DB7A0D81ADF3BFBF096F926B1C8789280641A2EE9C5F4406E4999C2AED0377DB8FD23FE0D49DF214311988F75E9BF14ECD024615C436908802AB25EAC6D648C94DE53199715D524646A010D7B3AE115DABEE4CEFB2F57917072B40383E00442E4AF27D97048A2749122E0654006CAE385BBD16F8211648AD963837941BB071128EC838C76F80124EE3713773B480E2BCA9459AB4AF3C805706066EC0F69EBF0EA156E6B698B9187001CA3E5A6F55BB25D0FC4A4C14B84F205C4E2A4282F17DC1834604FBB6C7752F878FBC9A5C0ECF496EF782A0844354F5C87331A643A0163AA4153A48AA58B5170A20C9F315421A39C53D91BA0A717AB14589CBC067DFFF9E4FFA8765756CED0D3641AC91A82D2F3C1096E63C0DF97B01F53642D7DC11D9E62B0065AD4BE2207E43CC0E60F78EF0847ED548369AC1730DE5AD31C1E9A3741290CE633C878F8C9F3F7DCF3DE69D0EB98E3F", "DCBE144556BF24ABF89F6DAC5F5DA70DD74B07C60E4E6DBD80509AC80F603783"), + ( + "22254A698964FE9234C17549A8296C00E440BC71CE01C9302C4A3D11CC4C86A74846A6C54434382EE92EB985916AF48351125A73900A699BCBF2A152983257B2F28ED16286BC2C1D64247E3FD83FB69988C3752172E4A21C9199BFC05A0B74AE32A0B73E74B0502488DB0C416A0834F4816D682173D32038004DC873038144DAC76C61637E35B3C0F37B8AB5906CF67EAD97C00D57675DD1AA154703DC9B8ED2F8BC57DB05CE47430662553C507804F97856DC4F45422E8B502714CB587A3014637534D7C3742B762230FCBE7668061821A1191A41A5E447DBB560B5267B4EAB4F8ECACB4DD201A2173BA2E55395DBA0993C83A903C328801539F734EBD6232E475135D9AE6774C56B9083FAEBB308D7214F845EE57332D2D06F89C4840782C35471538C8403A6B751298BB1E7274B165C4CAEE50A8ECA2C90DA8A02A25E81228C9281A6E6C82C16F77F3139B6E37CCC35EABC37416FD3F37E5FA1236AB4958E0B7A2F363725911E945778F768CD9F35241180162A841A3B45A4CD2B3A3E013E90B525D7B338C1E25C1D2776D9F6900DD0777C330A4E4B5A90071AFF850A409A361BAA2B0995C41F37AB03DB8AE24312CB6864EE69786C29CDE9E20359571349638C6DC11D574348B037B7B50821BD3CC38607B529400B06F2341B68CAD3E4A62679A5FF6B019B77C64CD15FF830BD5BB841F1FAB141F7609AA9540C2B33D0BA3730B089D7680F06D468C716A8078B36E42A150E775E88BB00FE98811D8A87EC11963F161C76229B42893E81A902F2A755E4266118A7BE4B667C46C56A638A23A3C89A57B1673F92405AD7275E8CB0FFD91B78C2C40EB93DAC278611D08AC11A3278A22D1D555D7B0562226AACD11464D4E67FEBC3A6BFBC41D775AF11383563F963C9634C979B9DB81100052712CA65157C6C308B55062CBAC0BAA7155E263AA6A51B7153C8B6803940871309E246325C99A81241ECF32133767308FBA4B6497729893F2345CA59827F36ACA3BDE2C834949D27535BB20AAD87B91189EA7C67372D521346E32CCC4A417657CC4178C09E5ABC66483B84EBC9C2141C5318F551C5132515707F858460EA953A1F4733A782BC89E3BD44B4B282E10553B4B73366C42464994D435EFF2363D71C50B7D6CC45BA52461ACB59F9677C939D9C7BC7AF992ED03262787BB4CCBCB6A413AF13E18CCA3754264353BAB3CE1A2AC0415508B47B25BA7BBC9EF335827A3099151F03B39996F1CC9FE603CBF9A43FB10726682BE77767A1323AA9A07250C29310A9C4B5AB08A700ABB0E7CF572C81E0A4C28FB76D8B0AC8E3B35D9BC4089930671446548F65BFBB6CA2A7EB9B3CB61643B0A1388804D5C51A707033A6ECB8808B1DCE18B55860BFA1929CB84874E8617A465512691B4F4EB7755B730D3837C8729164C741C14FB7805C936C70FA051BA607B8EB0B0936C3C2701C6B2C7F2542B31C5A53D59CC9F7840311EA3F2E96939557754E665C968926017691C48A548FAA14AB0196D947C415F912E5AC316F81082BE2BD30735289DA87CBB86E71C07162F22952B6406BAC6A528151E74B18163A3D911AAE52057A13F85158C489D3E90BFADCBD48AA4136483DF069C0A6C6C72499B07F21077BD1B4015B50F05B5E2B98AE8FD24B27509379E87C5F308704215DC17707689913A68BBAB76C22A85CA540E8B0267C857AF49BB7242E89D63B697A0332FC588FCA1B987741C05A98A7669DE7B12348472F7EA70BEB68A008C8234B10B18C6A8067607CF451A47430BE3CC01FD718A2C49962E9F90D9C02644A7A2E96731AEB56601D0651638649E4FC299292504743214191312F11B060D56510AA00588B8709AB98741CC5D1126AC0C64B76487716C216BFDBC13A6A4D36D6B55206519618CDB4B2C55CDA81B35558F3D45C3818193B3BC52475521F704DD9947E90280C1FA01C894BBD81EA19B87A15834544CF51232C0A61C5E40684054D84C37602ECBB219C63F7E158137768EDA0650ED266221745FFE5C15F5C3B948915521015E336A724C76182D835BEB22503EA0904C73567FB08E03307880700DE150E0D013D8C1A8E83270035F9249F220043B836C53B6390832C6EDBA52CA131307412248B80FA2162ED54B4EE23853143817F72C6D3A6A1C89AB236085BD9FC9118E6A7A96178D28C13E688E0F9FA82901BA3DF12C3BFD734BB6A5066BADAA40B8375C67CBBE0BBA80A6F16C841CFE33FDE7973C0CBE5E77DE4F995B49155FE393C10008392B890A5D0EBBF3217FB4CD2F002DA3B0CEE11DE3AFFEED891E8DDFCA23A30", + "3318D3B70DF3E6D2BBA0972E36E9D5F5FE8902E9D581186C930E091FC038E6E0C13465432B3F99DE5AF86BFC8DCF748AA094C713A72868571C4BFE10D9D3774A7829135C3F5A17CFF577A4EDDB60DEC5B0359CC403C479795B931CFB8B6EFFC323D0B0F4CC503C59225E60A55D4C8AA77DAA69801F26DFE1A5CB003B9206F6D68FF45B1D7E962315E8A9E2078104543289B9D06BA666444C2603F730D327C1AA2099C0356CE81A499FF3B5894363C8FAAE3EAA60A9C0A2BD052877082915E21095E365387E05A6F0DD41B72A4C77D795143156B76F1DA5B779548101686009FBD95EF9DABD470169400393D9B00FBDBD8F37F2DB323A109AF2476604D3CC03F71247ED16CBC6F202E38AA58934A5724E0D0497981C30EE7E340A629884D1808041611FA33548C900EACFA04687F40A7FFD02789DC480CE92B5B8F1F3D6F25A2E5F05F647EC5E718CB7A27EDA0F7B95374238D5D03FEEBA7560C974A5C9C3D04E92D63E3BA2D04A56330ABC5820C6FDDCCF11F21726590984B6E24A95CCBB1C7B577DD12DF391A4900A21232583ABD01630790152ADDD8B50871945D5D36909904D8F669C5788A0C11D9815FB68E6DB7A0D81ADF3BFBF096F926B1C8789280641A2EE9C5F4406E4999C2AED0377DB8FD23FE0D49DF214311988F75E9BF14ECD024615C436908802AB25EAC6D648C94DE53199715D524646A010D7B3AE115DABEE4CEFB2F57917072B40383E00442E4AF27D97048A2749122E0654006CAE385BBD16F8211648AD963837941BB071128EC838C76F80124EE3713773B480E2BCA9459AB4AF3C805706066EC0F69EBF0EA156E6B698B9187001CA3E5A6F55BB25D0FC4A4C14B84F205C4E2A4282F17DC1834604FBB6C7752F878FBC9A5C0ECF496EF782A0844354F5C87331A643A0163AA4153A48AA58B5170A20C9F315421A39C53D91BA0A717AB14589CBC067DFFF9E4FFA8765756CED0D3641AC91A82D2F3C1096E63C0DF97B01F53642D7DC11D9E62B0065AD4BE2207E43CC0E60F78EF0847ED548369AC1730DE5AD31C1E9A3741290CE633C878F8C9F3F7DCF3DE69D0EB98E3F", + "DCBE144556BF24ABF89F6DAC5F5DA70DD74B07C60E4E6DBD80509AC80F603783", + ), ]; for (dk, c, k) in cases { let dk: [u8; 1632] = unhex(dk).try_into().unwrap(); @@ -109,7 +199,10 @@ fn kat_mlkem512_rejects_malformed_ek() { ek[1] |= 0x0F; let m = [0u8; 32]; assert!( - matches!(mlkem512_encaps(&ek, &m), Err(Error::InvalidEncapsulationKey)), + matches!( + mlkem512_encaps(&ek, &m), + Err(Error::InvalidEncapsulationKey) + ), "비정규 ek 미거부" ); } @@ -118,11 +211,36 @@ fn kat_mlkem512_rejects_malformed_ek() { fn kat_mlkem768_keygen() { // (d, z, ek, dk) let cases: &[(&str, &str, &str, &str)] = &[ - ("E582B7D75E6C80B05AE392A1FC9F7153B12390FD99930368CC67A768BAEBC8A0", "1CDACB8740C0B87C4A379575F187B367CBFA3B300BF591B109F79816E9CBE8F0", "28C793778741B80B02B4339F2AA4347255B099F17264E1B8CC0A2C7C2A1A79F7997B907FD0496C6E6C8AD7714F5F339D75F11F625591A869BE1175AE47F05FD4313468232BA6957D7807B824F445AC99A0D568AB1AD54DCA8249D1482E61275F52248C77F61A4248753188CD1794CD0A465EC0DC4B025985C461B74E76286E4C37E77405695CC9FD0654374B427A20343AEC0FF1A187768273BFC4905472A1DA387F14559D6CE87313F6A5B6138434539F9A13684055B177E543F8B40F432ABD7CC49989A50A9084C660913F45A8593B17499BC4CF936C2BC1851421CB986808A0EF30AFE97AAB5B8B8EB3F0B3506A95B91563A0E57DB7231044987EF141BDAB3537C316AD16F17805A81F29329879A94E96157E4B7447F7D59603B21BD896CC47B7CD4E232322EB9C5D2215696BCFFCA3A04EFCC4C5D9CC39AC9A6E8700D38C244B0169E7FA1FE81B4B10365E74E6A1F7F756D11ACDC84043F81006D62995376C22535958FEB53F78117EE0F61C4C862640D06DC57A2B8BE62A41A642AF3BC63F6BAC98BBBBFF70570F37B8F8D9572F2735657A6C98F96CAF57A849868720B2640B8BB2732237A1F984C18872D10289CE43C952C9257E06529AEB76AFD127B17596FD25C5216C9CABD9B18EFC50E87BBB04568BB7D5C4E9288C006483AF5912E19108573700BD10CD77224B80659EA75AA74270B33AC4008B738BFEE271E78658C8742FF13C96AD0781A03C7576CA26DD58B52980BA58C0505E446AFA140CDCEA0490DB1F9B18815D4314B2459CACC562441C91F4084E5426C88E632CF7482E79907911D06473260835D7B85E7856A829AEA0381707B939CE86882CC09C4448C6AE94A9C303107C5667EEFB8DF7763CC21189A3C590C40AA51F491503A7935EC08F4FC300CBE607ED8C9100C29FBF45584B13C8D780069337AEC76C36CEB70373E2AB6E7B934B466F53FB32EAF040055496B8540E23A2A277E534468608D5EC0F8D38CEA5BBB806C1BF4F164F6AC826FE733F95461E29DCC11200C0AADA1B8332023EAB329718CE25CC0A09555903F3578BBC863B1752CA94365DA556DF54C3B7E05CBB7115FBC1B6C57A172C31B9906560C8FB54F3C563A2256CC073243B8179B4A28D60E086CF51082EE429272996F0AABE03BA0EAFD3C8E7D954BD0933E2F60ED0C32CEDE7B820A28E48F3CA3C40913CCCAE2337ABFC59843F08C9863325D65A4E9E15C1F46172B118B2B5EB0F1D5158A00134F27B085488C3A0621FE4E5678698250FB74EE5152E3E35A66544A05D279EA99131FBC15165060B90F88EEB7B20892A4DE4CB1683495BD7DA037966B47CC040F1764C5DEB06B5499D4267391CEBBB47F734D8539E39528436A1858182854BF20B1F93279AFB706464C65CCC5AE099B37CC03556C26ABF4C3F8B9BA3A936707211A49A59B268F5284F7970C77612719450377417428C4BA47C9CA115CF95304C4759C5D8859B44985C06A6C924689237BA320D610960D61C53E85431789E67A40113F167FF93429C264F6CABC95448C903437D39A6577BE0CF0012852AA476351A9046A110A1A625A3D74C910B78BCE9CFCA735E4F91B8A4C57DBE489E849446098AACF73070AEE638FCC8896473D3C159D3AFB4B687B40DFBF371A9C2644B605187B71A14BC4C8678FE8247", "3808B98D9A093C7853B0B814D1CA5F392677D3D0A38F81C852F95B9A69B374A24588C0ADD5B510BE567C5A24688EC91ED0F28BB4C86978C09793795C36B94E5DD8498EB4353FF40EA87B17E921B5B4CBA08E5B7BE5A9C8BC69AC5AC3075DD947D04B8695097AC39790A0C8ABE11200C9F136ED7B0C10077E36111C1F139D9BD27142993F5883925C4918413CB8043962A28567397A1C7E003BAC30644055155CE9464E623FC5E2160CD1143FD2C90C03A08D0B333FBB40A308CACD81618674B406809790B2538D30431F064F65EBBA7C41B2A53146A3ABCA66754CAE591534045C1D640D6308472EBCA660A8A7D372470D3869115B8860B577311980B2C66B8352229ABC17B0C31F58E5A5EBDBCA6E3B157D9CA79EE890778A4CF28429C2A82C80DC9C524B0864E385E355BD4E65732D395E464070F61828E57C3E1BE2A50052C4FF1951F5D0C89720987D8941F085871113A91F518FC79B7189056990B2447BF54FC170C932E0A4E0F7A262542F56D1B49EEBC392106D93C77DE9186DFBB824E73B2F7FC796B312875B43414877AFB356214492C19C748DD381BC7A9237BF097CC606A03119A240A5536AC7844F6A79D0E2B821B68D96542450179BCCA231F2DA68C5EB6D118B99A1B66EAA566C78A9009008B0D66155BA4839F8E518C650177DEC170EB09E8BF6A89905320590C642B801CF5B5151C2AF3E7271CB9961C65A5BD17479429A31BE9081F2767D94816C16D1B04772012382B689AB2D3FB9BDC66547BFEB23052600BB369771494D9C914EC93A066ABC9611940B947310DA04197312425D736A1933B785C95DC430791E42CD691C79BA63BE06EC80765C9C07053EAC706697F21720C672B9803DF9935532197B0485BBE42B0DD16561AC0605E9DA73C189C9E29ABA3AECA19C21621B209326418543C44B88843EB3720A2CEC3DB52C59D6507EE5CB03C268B31FC4BF115695EB0B8AD3F11FEDEA1A5D0724A5D5284D0C10E9844D0D32BB5735448421BC5317650BEAC4829B234B787339812F2316ACE8C22C42D6346214934FC0B49ECCBDF8DA19AEE64DE9628C4F7AB3E512A399B0C227B677AC4A69891A6874F6641D2CC95460B751E17E434C924B2947D806856665696E3CC4DB9553B81606C31C3800B48756A073BF685EEA20899C176769E902DB971827D153B9C33516E959C6B469841946C0D921A371A5DE9740C6AB9CA272B5850BA8753CA023A460EBF7BD573C0745F40B90F105EE17C19B6832E019B80F8858BB515F7C709E68F29C1375B22567AFE7B528F4431A94B553DF825CDEB84B4EA9296EAB9AD66271EEC5AEF6F79509A20A182279FDF92F87FB6E7509968D22CA750B5056974841B9654E5716BDC33A2C6A116A4117FA757A1D22710668412B8878E134B70FE32158A2317FBC62F01371296AA42E33C903E0C439F19684B11E2F911F7FB79860C8800F9CA146EABE29DB07237AABB503A9BE2AA7263A0626C162A27537775792B2E7B0FD347929934AD8F521D4159059F611312AB903879490059BE8B38920E2CB4A256B8B35783E909346D13E9888BEB9350369F7C1A8501331110C651621A616B365A026D1CC47DF440C9E650DD0C0BFC8295439114528C793778741B80B02B4339F2AA4347255B099F17264E1B8CC0A2C7C2A1A79F7997B907FD0496C6E6C8AD7714F5F339D75F11F625591A869BE1175AE47F05FD4313468232BA6957D7807B824F445AC99A0D568AB1AD54DCA8249D1482E61275F52248C77F61A4248753188CD1794CD0A465EC0DC4B025985C461B74E76286E4C37E77405695CC9FD0654374B427A20343AEC0FF1A187768273BFC4905472A1DA387F14559D6CE87313F6A5B6138434539F9A13684055B177E543F8B40F432ABD7CC49989A50A9084C660913F45A8593B17499BC4CF936C2BC1851421CB986808A0EF30AFE97AAB5B8B8EB3F0B3506A95B91563A0E57DB7231044987EF141BDAB3537C316AD16F17805A81F29329879A94E96157E4B7447F7D59603B21BD896CC47B7CD4E232322EB9C5D2215696BCFFCA3A04EFCC4C5D9CC39AC9A6E8700D38C244B0169E7FA1FE81B4B10365E74E6A1F7F756D11ACDC84043F81006D62995376C22535958FEB53F78117EE0F61C4C862640D06DC57A2B8BE62A41A642AF3BC63F6BAC98BBBBFF70570F37B8F8D9572F2735657A6C98F96CAF57A849868720B2640B8BB2732237A1F984C18872D10289CE43C952C9257E06529AEB76AFD127B17596FD25C5216C9CABD9B18EFC50E87BBB04568BB7D5C4E9288C006483AF5912E19108573700BD10CD77224B80659EA75AA74270B33AC4008B738BFEE271E78658C8742FF13C96AD0781A03C7576CA26DD58B52980BA58C0505E446AFA140CDCEA0490DB1F9B18815D4314B2459CACC562441C91F4084E5426C88E632CF7482E79907911D06473260835D7B85E7856A829AEA0381707B939CE86882CC09C4448C6AE94A9C303107C5667EEFB8DF7763CC21189A3C590C40AA51F491503A7935EC08F4FC300CBE607ED8C9100C29FBF45584B13C8D780069337AEC76C36CEB70373E2AB6E7B934B466F53FB32EAF040055496B8540E23A2A277E534468608D5EC0F8D38CEA5BBB806C1BF4F164F6AC826FE733F95461E29DCC11200C0AADA1B8332023EAB329718CE25CC0A09555903F3578BBC863B1752CA94365DA556DF54C3B7E05CBB7115FBC1B6C57A172C31B9906560C8FB54F3C563A2256CC073243B8179B4A28D60E086CF51082EE429272996F0AABE03BA0EAFD3C8E7D954BD0933E2F60ED0C32CEDE7B820A28E48F3CA3C40913CCCAE2337ABFC59843F08C9863325D65A4E9E15C1F46172B118B2B5EB0F1D5158A00134F27B085488C3A0621FE4E5678698250FB74EE5152E3E35A66544A05D279EA99131FBC15165060B90F88EEB7B20892A4DE4CB1683495BD7DA037966B47CC040F1764C5DEB06B5499D4267391CEBBB47F734D8539E39528436A1858182854BF20B1F93279AFB706464C65CCC5AE099B37CC03556C26ABF4C3F8B9BA3A936707211A49A59B268F5284F7970C77612719450377417428C4BA47C9CA115CF95304C4759C5D8859B44985C06A6C924689237BA320D610960D61C53E85431789E67A40113F167FF93429C264F6CABC95448C903437D39A6577BE0CF0012852AA476351A9046A110A1A625A3D74C910B78BCE9CFCA735E4F91B8A4C57DBE489E849446098AACF73070AEE638FCC8896473D3C159D3AFB4B687B40DFBF371A9C2644B605187B71A14BC4C8678FE824781E66EF5A7A221619F6A64039CC369843E10DF5C859F6959CC3FD8E5272330FD1CDACB8740C0B87C4A379575F187B367CBFA3B300BF591B109F79816E9CBE8F0"), - ("3E5848DB624613F7AC144457CC1375F006FA8CB953E767DC9E7428D00F5DAD8B", "012DD6C2F0918B9EB6182474EB86D848F65974759D59CE151A396DEEE4CA10D1", "43166BBD4C97F4B57D4DBA6FE40A48498B001A502C28385B74306BBA43C670ACB191FB031C220CA3238AE1CBC961E66898A4C19C0A4BFD076863651FB91A31C75A33DC18B53EC6C56679159783A563C4504CA10D1405C261F8C7FFA87280242EC8BCC3077ABDF371CF55C140D0A131E6529B40788F1943964136000CD7319E05553EA58E5898384822A62B8AB9BD03609F1566F53435C472520D957703090794179EBC73744525A8F002A14270CBD1D4B6E2538353765D49F5651C1C34CC228C4E753C5210BFD00CB378D6A4ECE80168D54A4C62740A0C4A292A42E1621F2C499E4FB94E37912C9CEA7472C65B27F65036E85D5512C38D3582DF290FA819BDF0E37EBA7C2607E6A94A2323A0FB88A6808B2F444E33999F1A10830F715960D563D0A43AAD7AC7F0B4AC4A9A3236286D5249C83DC5C4666354D0EA5FE06530E4081370264566E47ACCBC22380414FF530C394007FE6CA79DB0AC53FB2B06719908D15DDE432F03C84463EC5009D81E9882030360A0FBC34F156391B6E3AC92C795FAA2199BB4A9A33A994A16AEDB6558CE901C4B60295DFA39CDD5B97C9B1E9C3199C825BD64B819D8346B1EAA6A5F5A4BB90A7F32253D7F7BA2778BB796BC707BA53CBA7A36E3682D54C3003F572D2779AD543247C6D78EDB63739E17029594BAC09620651227C8F072C7F51672385F62E06B2C80A4160BC73C8691C5314914F4537266C0D2EC74365B8CEB99C71F076BA772493CB534192A70BD576B1BE0A42D2B87DB45A423309704BA17CBD7CC610095FAB3CA61281AB2949A0D2B548A1110852073A98C4C612BB095D28049201C4F08A6083BBB60A65CBEB81B6D2A40E0D7AB0628B5EE251143812AE9B6524174C420A82D486350954AA1E6442007239FC7ECC969E18507FB55C78CCCA6C18842D745BCD9C425DABA9033B8161374EB63244F2570D228392174545FAA4664A886118A6948451DCDF489A1BAAD5E2B605B4756F3124F80B546FCC61AD30660EC342228ABA6CFCC06E61884F96C7F675711C84451D3902C206B6F1B928A2701C021E42FCB49C28C19414ECB9288C4A51E957B4D742D0E5307D731AE0EEA9D7BC9B48BCB332EFBCC3894AF4986628FF77420B6B8ED75C4F54A063B49265C38CF17B95DE45A64CF30B5EBA5643603B28B2C4948E2A2F1E8C440C54341491B20D378DE5481CDF8310074A69D79767EE1135FD8AAE3934449B406AE447BB33148A08C1C0EE4A615A44C42943CE3D56ED50B77BB7C3D4448C106363472CA0F11F526349C673FC2C8158ACB21BC3BBA9CC39891672FFCB4B3E165E6C4A1D156AD7F7085BBC49AAC5B5AA2682F165560C4132C49E0B12FA4BBF4B40E630B375086B3EFF66B07B0CFBD207D7F1AB67EC580ADA48F36308E1566CF2FF5BD1F84B9161A73014290C384BF9E0B0AA653CE4F8C6D05CB087EF846A1478468E06F81D0B5406B9AC937AFC2795AB4472E0BA964DF4259E849030108D083BA6ADFFC63E4B77126C3BFFE389401E15EC6A88BD0B5B32BD06AEDF80792636F167589C346A6E651009557CA079C593270250D9ACB3A1BB06AF03C65F88D71EA932EB79553D60FD4D34499F23FA3498BBC3A89BE3369661CC2D292221A9B8CF379DF6702808576A37975C9B593EAB54AD4B8936DA5217C6B843AD33D", "0E62A20CC7B3062B2EE82CAAC1005F0E9655B03BAAA905AA59283126149B60969B2720563F1BAF1A93908CF41DB362589E9694C3E6734620AF30FCAF3186B5B42B5376390BEF3B4CD1833BADF19DCCB0334A68B1C6541874659FF14CC49357C2B5EAC09DAAA77CACA604242F85039A89FB456DE488FD06137E7C527E76A18B5CA066AC95A93094B300185B001F44CC2505813534A949F335027B6C12DE7C580451A53C499DE6263117A17B5BA64471A97C0579A4FE3B20F5D24569C85D214155B4E9818D9466184C8E2FB843DF396B2AC18FCA8B61B8387A2B3458E9D0AB9E2B751BE34DD5C70132595835DA55D454C301F13B8BA542EBB6C1E86B2F621312E4B80A54E106A7E5743257CB52603A68B4A9F5D195B1283150E48F9766A1BD248438C92084B451EABA445AFC6BB03805B963537AC8255CBA18D4A9832B2752EBD9B5F7B8AA52F5207B82945C473278DC5D7850C0921423587401D9210053C9C9844460917553A60A099449A7B0D360E00044FD9932140CC1D1D444F14010F8F35869A03728951E28B7C203C5CE3F7AA61205D08A15B181C0445AB27E3F889C0E72B050476CFE29420085A09D203B1C73ADB6B59074893BC3A7533758467D6046E3633044C8A1E9BB3824B72EDC7A8391721E01E4A2A214BB71008B1EFA343B064901831B20698D3958991DD9C22058A0AD4A49657B929AD926EE1B608CB29DB3174B27CB09F7A75CE617A21AB971A28BB6445267EBB838DD3B3BB82759C26A021D0382992A6CAD218ABC44A8D9667A07F5040ADCC1423CAE1363729BE457FD94C025E1B48F071140DA992E1A557A7497907CC729E33E773221F79096ED8BB830373CFBE84F5466A02D1302B1C09215983EC0229B61302F77AB4E698016A600CAB1F9729C320F65EC1537733F3976C4AE75723903BD83437B0A519A53E56B5C49C5C15980DC5341BB45305229536D324F513615A459C5D69019DACA536C42427332279BCB8A74503F76A5AB90EC7476105BA69C518921C62AE043B5E91DC958949D57B0BEF7903CF5CFC5A06C0654A60F48388DA4723B5B3A320184FE6755A881B27C2ABE23A0B63338B1EF778DB47B2CDEE043F0F5390C84BAE4D2B3ADB2B67558B567925DA6E1B33CCB3B87126293BAA2A8B18479D65C4BDA14FFF45E64223EA6E455506164F3F453651439D275862937257F666CDC564FFA255ABBB3B6FC8493FEAB7E76E2A17E849044E62A1F980A57BA5B9397271E2B5D5B5063EDAB2098FC561D10C731AA9D7AC13FCBC793EE659D6994BB00D53323232C6A192DCBE39F6193676C69C256E9050DA78EA442078374A27BE054AA30462374B992C8A32F0440E2DCAA70927D06C97EBF4C04C9557A622203187AB514C0284ED45BDFF560F3B3A15A6A6F0A6B098DF3C9BC083348138710D8B5D97A2EBE2A90DE907658F7B8B67865A4BAA111898BDCB5521F209C5801861D394BEDF51B0302888D39A73BCB07E3E99C2174BD080B1EB4086199B4859FD233D9BA3B34A04CCC67BF59896D98675AFC4CB1656005C57BA8CA5A1B085B686A1C27BE292FD3FC0D7594522C9232DE4B09541C3C1C11A2ABE1830AB3BD80B313369407F66C970FF61586A040F7DA4743166BBD4C97F4B57D4DBA6FE40A48498B001A502C28385B74306BBA43C670ACB191FB031C220CA3238AE1CBC961E66898A4C19C0A4BFD076863651FB91A31C75A33DC18B53EC6C56679159783A563C4504CA10D1405C261F8C7FFA87280242EC8BCC3077ABDF371CF55C140D0A131E6529B40788F1943964136000CD7319E05553EA58E5898384822A62B8AB9BD03609F1566F53435C472520D957703090794179EBC73744525A8F002A14270CBD1D4B6E2538353765D49F5651C1C34CC228C4E753C5210BFD00CB378D6A4ECE80168D54A4C62740A0C4A292A42E1621F2C499E4FB94E37912C9CEA7472C65B27F65036E85D5512C38D3582DF290FA819BDF0E37EBA7C2607E6A94A2323A0FB88A6808B2F444E33999F1A10830F715960D563D0A43AAD7AC7F0B4AC4A9A3236286D5249C83DC5C4666354D0EA5FE06530E4081370264566E47ACCBC22380414FF530C394007FE6CA79DB0AC53FB2B06719908D15DDE432F03C84463EC5009D81E9882030360A0FBC34F156391B6E3AC92C795FAA2199BB4A9A33A994A16AEDB6558CE901C4B60295DFA39CDD5B97C9B1E9C3199C825BD64B819D8346B1EAA6A5F5A4BB90A7F32253D7F7BA2778BB796BC707BA53CBA7A36E3682D54C3003F572D2779AD543247C6D78EDB63739E17029594BAC09620651227C8F072C7F51672385F62E06B2C80A4160BC73C8691C5314914F4537266C0D2EC74365B8CEB99C71F076BA772493CB534192A70BD576B1BE0A42D2B87DB45A423309704BA17CBD7CC610095FAB3CA61281AB2949A0D2B548A1110852073A98C4C612BB095D28049201C4F08A6083BBB60A65CBEB81B6D2A40E0D7AB0628B5EE251143812AE9B6524174C420A82D486350954AA1E6442007239FC7ECC969E18507FB55C78CCCA6C18842D745BCD9C425DABA9033B8161374EB63244F2570D228392174545FAA4664A886118A6948451DCDF489A1BAAD5E2B605B4756F3124F80B546FCC61AD30660EC342228ABA6CFCC06E61884F96C7F675711C84451D3902C206B6F1B928A2701C021E42FCB49C28C19414ECB9288C4A51E957B4D742D0E5307D731AE0EEA9D7BC9B48BCB332EFBCC3894AF4986628FF77420B6B8ED75C4F54A063B49265C38CF17B95DE45A64CF30B5EBA5643603B28B2C4948E2A2F1E8C440C54341491B20D378DE5481CDF8310074A69D79767EE1135FD8AAE3934449B406AE447BB33148A08C1C0EE4A615A44C42943CE3D56ED50B77BB7C3D4448C106363472CA0F11F526349C673FC2C8158ACB21BC3BBA9CC39891672FFCB4B3E165E6C4A1D156AD7F7085BBC49AAC5B5AA2682F165560C4132C49E0B12FA4BBF4B40E630B375086B3EFF66B07B0CFBD207D7F1AB67EC580ADA48F36308E1566CF2FF5BD1F84B9161A73014290C384BF9E0B0AA653CE4F8C6D05CB087EF846A1478468E06F81D0B5406B9AC937AFC2795AB4472E0BA964DF4259E849030108D083BA6ADFFC63E4B77126C3BFFE389401E15EC6A88BD0B5B32BD06AEDF80792636F167589C346A6E651009557CA079C593270250D9ACB3A1BB06AF03C65F88D71EA932EB79553D60FD4D34499F23FA3498BBC3A89BE3369661CC2D292221A9B8CF379DF6702808576A37975C9B593EAB54AD4B8936DA5217C6B843AD33D423290E0CCB74DA11B95294FEF12EC690815FF103075053B89C5021AEFF4CA0C012DD6C2F0918B9EB6182474EB86D848F65974759D59CE151A396DEEE4CA10D1"), - ("882FDEA55C6B497A6F2A1321FC82160C630D9A1F2E35BBE0D3332E3A0CFBC8C9", "3A21F601923C559179A3AA5839A148487379EAAD934BF27F3071E1B1FDC21D1A", "3A7714CDE9B06A8990EAC99A89A12EB606017F841D4BE2100AEB750C3892EF544418216EA437AFFE88059A5A42694089FE7020828160683C0E7EFA4519591A4DAAC30456A5EC2AC39CA0CB3F3920237097E6E5157E1881A09A03AEBA5B9925CC81F20A66D86EC52245E828655032067E8BB813F43BE6DA09FCFA2FC2452EED5CA4DBA87DC306A573911205B53B91187D44214D12CA1147B4174956C2BBF1863D85567B8BA14F24B2B6949F6639288DE34C117391A19B678B3406FF784AB87B66031371BD0073DB89A4ABF597D40213BCD4A93AE834F14A52D527C7486C9DDBF62148036AA55A06ED156A0C0B9366D15B582338BEDB8466317FEC3109139C8CE2C1079A070597C6B712C1B16ACA049F3C8999A95E1DD20163226B96C5724EF6908A9C5DB43637708471E02C31E1B40A226B01DABB057C328717A4BB41F9A32230079A49BEB2E892F9DA40FF947CCA4410497701795C62A113B30AC0A7D143A8CA8C3C0189407F95AB1B05719C1ACCF500676B3596141A409B6C0E952A5F1D456A70492B83C4515DD99B763A24B4C1545ABC8715E61857A89CF6D760D00C7B47D407C1C230C6212215CC805C76979671AF6D3168C73CCA4066233BF66FC1D485E225757783CC0457059CE05B9A2409B7B25415121BB2823B032BB40AD8318A86682A1C9FA6F571A08054939144F1AC2F5D299C97C730F742B75C4521851B8D9FE6AD43121C74AA248B751998A2AD9FB747BCF18AEDB4B4B0E718E979B7F7580C14BA62B42B8997F17E6812BC46AB6799F7519A93BFF43170F2C01B31D96459B7A8956B9DA9B1C2CC7C7E3C849FF326003BA17440539E01498CBA96B0922468DE2345857C3405597AD822004A2B2FBD8723E9F59CFBA30732E3BF68131283B87D05620440A2A7FC3582C20B68D0A8763B5276C12957AFA766C4D0A524800A1D954934FA0CDDD684D85C997BF2C54D8CBCD924B19E731F80B35138752A8E91C0876700F4AC3247BA6DDEBC1657941DFFE96D1803C81B75A6D07CB6CD4C14B8C904020CBB6BDA1D019CC49EC90A12581F944263DFE06DB5E16C94EBA98276B9AC3B13B8A9083AD3190195583EA7A76051A27F2121F46318580C37E50B0429480F7EAC312294CD3C71BDBBB0557C41AFC363A6411805964221D26B0EB7B798CE791EF121BBBAC1BDBB88BD57DCA9114564F4B388B1A2C32912A8BB5C1F21763294498E2B0620F3C085E7C4099D6A3E4FAB7FA12A73E39167BCE99A6F69A293A37EF238CE2ED7A1804A596CA33897F4CB772C4083A55438490D3E1CC1C5E91D9B78A5E67BBE3C7C3B1293CEB0BA7B7E48CB2BCBB30DD70296A17BEF964F265C5A41152964574C0505975DDC10BA9850C6199C83E157F2A74D8FE7496AD2BA6D83586C7624B87C3E00B84B65620DB69643AD9046105B5A6CD8065716C398D0BCE0E1577E3BBE192651692B79DFB747F7621E2C9A87BF2B602105CD893B2BEA50A10D952CC8DBBB1499B9771A3999FB91E0C7438AB641D79A568A375DF1D07E28E2C9C450A72F8C4AA5D71B82A09E1196AA3FCA8E7C684A4DDB635E775FC03604711139F332AE97EA1BA2813B1690BF9CF70F506CBA90641963D5CFAD3698DA30C866C95C66B4846F785CCBF373F4621855FD4815F0920AC4F940CA25DD130DCFA223E9B4", "7A804DFB96B63BA97AF4E35FE6117A82D68509A81C70CB54C68A33280C2640599E918A2074627CC1AC8966A74E08822DC5731E79D82B8DCA0CBBE6C8D1EA6973A970DBB44D5FBA31E14593E9ACBC77F02ACAB7A58D656854F4031DD209A5B33A167943072BBD660B815FD13AAD82A045269A161A86FC5BB72C4CAA704C105C37CF04CC4C8A1643B01C37AF0C8D4D39CA85025665A1B66E907720288BB955A7BBF639123376A3E27D919499BE7A42E1FB09B9AB27029AC076DB2F25147A2BF38F6C5B287BB424BAE821D79875C8C41FB120A1BB957BBB57236684636C758E909120AD01332C60BA5304133CF7837EAC6D31FB7CF160251DA3C4BD1B18B0FA674F1B5ECA1603FCE92928B821A8A68E6A085E98EB2843E722481A0B81E16E2C4BB0F4B0896DBB2C900C8DD7E500A6C84EABB44D4A670D54203E921344C6C1401516AF0168CD1F0821C0FB260044B6F908A68E58170CCB34C4E39919DC747FFA13813240A43631D865ADFE335936034BDA443773B967C8B9636109CCF919A9B6F233F0C3598A7ACC96721CD5489EBDC29549ACC16F0A64C3B0B9C742CD81C504BF6A9898A38D052A36036436655285BC26A249D190E72337D4A0BF191954A5AC76982945205113977A1D6937824C773234CB2EE7716570628BF1048A04B38E5D9C42C3AAAB1581446C3B96CE96674C119A709C0425E6BCE65477252C628C6988DF6316813B6F4D37C9FF339D22A76E3A8A1376498B63AA221FB329EF270614745E94655C6591747DE740586386A6150D66E8557225B38D3173F5816BA12C395C09712A5B289031981C2BC97C51C39499A144C2976305BE1BB08CD6A305212C2136FB8921F337E6D61E8E8ACAA525B8C9C60F3AA66EF9A56DBB247DDD424D67C95DC6B1428AE63CD2A236CCEC911378BAA1E101961864D858682A762113E88F98F25D1ACC9E2E1907536257C3CA97B81091557CA0C691B90DF6B707D55A94413C5382CA4DE2605740156CE738796530781A2E9C629BB1CA0D82B5024A262437243B29E2586F4C8585777EC82898E4942ECD1325D719B21017451B5908691B35722650A81964720ACB595009B99749852035CBD4B1AD51C178E9C97C604FFEF08E7FF4057D274F1E078F46D04298F0CE82A44C8224C31697C25A687DC845B8A55A050D69C6E6A4226759BD0794C5BF0C33CAE843E2FABF738C7957D61206FBACF8B54472C896174195EA8923E3995DE50C9C430C4FD4F81ABD21731DF968E3617575E90A7B430990055068D3390E376FC62490AA97A27F5441A5A149C07344AAB833E19B093FD9BCBC8A05CE1B6FBFFBC7BC424EFC7255178569E2447C59B851E39A2017390B4F230D6DC927BF83BF2F868E104129C5DB003EEC8F9218357A6553EBB41D6CE558CB3B8FF86A3F125A8BED75C675E13A9F343A4B791E7FD87ED4228556A494B5C0CA685AC4AB663527ECCA5E5279F1277AB51186D0EBC50FE5B4B64590F26A87E9049E29735190579DDD194180B60B78579EAD248895C55422A18E59D01E6166BCD19C08D1917280D0265A370D5A34334FC9B29F931C5C68297937198B06791D838492D73C389A9556FA0640E03537F5C744A379892B362B0B1300678F3A7714CDE9B06A8990EAC99A89A12EB606017F841D4BE2100AEB750C3892EF544418216EA437AFFE88059A5A42694089FE7020828160683C0E7EFA4519591A4DAAC30456A5EC2AC39CA0CB3F3920237097E6E5157E1881A09A03AEBA5B9925CC81F20A66D86EC52245E828655032067E8BB813F43BE6DA09FCFA2FC2452EED5CA4DBA87DC306A573911205B53B91187D44214D12CA1147B4174956C2BBF1863D85567B8BA14F24B2B6949F6639288DE34C117391A19B678B3406FF784AB87B66031371BD0073DB89A4ABF597D40213BCD4A93AE834F14A52D527C7486C9DDBF62148036AA55A06ED156A0C0B9366D15B582338BEDB8466317FEC3109139C8CE2C1079A070597C6B712C1B16ACA049F3C8999A95E1DD20163226B96C5724EF6908A9C5DB43637708471E02C31E1B40A226B01DABB057C328717A4BB41F9A32230079A49BEB2E892F9DA40FF947CCA4410497701795C62A113B30AC0A7D143A8CA8C3C0189407F95AB1B05719C1ACCF500676B3596141A409B6C0E952A5F1D456A70492B83C4515DD99B763A24B4C1545ABC8715E61857A89CF6D760D00C7B47D407C1C230C6212215CC805C76979671AF6D3168C73CCA4066233BF66FC1D485E225757783CC0457059CE05B9A2409B7B25415121BB2823B032BB40AD8318A86682A1C9FA6F571A08054939144F1AC2F5D299C97C730F742B75C4521851B8D9FE6AD43121C74AA248B751998A2AD9FB747BCF18AEDB4B4B0E718E979B7F7580C14BA62B42B8997F17E6812BC46AB6799F7519A93BFF43170F2C01B31D96459B7A8956B9DA9B1C2CC7C7E3C849FF326003BA17440539E01498CBA96B0922468DE2345857C3405597AD822004A2B2FBD8723E9F59CFBA30732E3BF68131283B87D05620440A2A7FC3582C20B68D0A8763B5276C12957AFA766C4D0A524800A1D954934FA0CDDD684D85C997BF2C54D8CBCD924B19E731F80B35138752A8E91C0876700F4AC3247BA6DDEBC1657941DFFE96D1803C81B75A6D07CB6CD4C14B8C904020CBB6BDA1D019CC49EC90A12581F944263DFE06DB5E16C94EBA98276B9AC3B13B8A9083AD3190195583EA7A76051A27F2121F46318580C37E50B0429480F7EAC312294CD3C71BDBBB0557C41AFC363A6411805964221D26B0EB7B798CE791EF121BBBAC1BDBB88BD57DCA9114564F4B388B1A2C32912A8BB5C1F21763294498E2B0620F3C085E7C4099D6A3E4FAB7FA12A73E39167BCE99A6F69A293A37EF238CE2ED7A1804A596CA33897F4CB772C4083A55438490D3E1CC1C5E91D9B78A5E67BBE3C7C3B1293CEB0BA7B7E48CB2BCBB30DD70296A17BEF964F265C5A41152964574C0505975DDC10BA9850C6199C83E157F2A74D8FE7496AD2BA6D83586C7624B87C3E00B84B65620DB69643AD9046105B5A6CD8065716C398D0BCE0E1577E3BBE192651692B79DFB747F7621E2C9A87BF2B602105CD893B2BEA50A10D952CC8DBBB1499B9771A3999FB91E0C7438AB641D79A568A375DF1D07E28E2C9C450A72F8C4AA5D71B82A09E1196AA3FCA8E7C684A4DDB635E775FC03604711139F332AE97EA1BA2813B1690BF9CF70F506CBA90641963D5CFAD3698DA30C866C95C66B4846F785CCBF373F4621855FD4815F0920AC4F940CA25DD130DCFA223E9B45F13EE3FD74B1366AFA7F3FFF4B8F645331DB5526FDD7998A4EEB5C27C884DB83A21F601923C559179A3AA5839A148487379EAAD934BF27F3071E1B1FDC21D1A"), - ("AEE013310D04215B77F073A7E22B77BF0221394A08E96BD07E3C99A9AF45F16B", "7DCB9D6A2C3832E5FE9261E29172375A78CBECDA0EF5484C4B3F6940EA20EDD9", "7105C33BA18D19F691968662854A13ECD19E3D93CC15EABB1F4595BC189CB951A957D310D4469CFEB8892021A58D3C023F815D42411C27B0777AF8C25A898C96C2BB519C5830B295EF882ADCECA22D927EBA65A00C815AC8B03B76B4CF56211A32CCC5D9957C52718E6F7240D4E389F0854D160B3FC7D51A9318C743A5748ED06DBC1051AA885E7141655FD378F99C579280027F1242DE118B69E46E2510A5E420B7D8A39E8AB6BBDA342034B1AA3E394DCF02813877A4AC02A76A03BB74804305A21C9261786463440540740A814AB3681E0FA260FFB055D8E7560BA3286E839F44D04AC9C44A6D29BF09127DD0431BF3E379E031C236D44D79C3A48CA7A42064546BC86ACFF82141E6BC17529374DC11CB5AADCD612E7415053EE266494128E87B1091DBA6591CB86C4486587B095F872A70C53DFC3A0D456A0C8865B45E5404A7FC489A8A09641B9A3B149D7261CC896107F000A7A75C6AAF772D3F9B242509318DCB1CECEC4527C92CECA5648441B99D1C84CF472572AB5575ACA40204C434283C1229570012B47829679FC8A63CE1302E132FABB9687F93095E932FA419B22849A4D6AA090FC69ECD029B6F7489B59A43C720C49CC557D8288680F6B0BC6B87D7462AD128022BFA946685A53E2A8C1F6883567B9C854A012475756FFA2A6B2C6040B443F3A425B35A9A53B6BFB029BC08986FEE5B05D8E11CC006544EA96F5400A8879C9E61E310CBF83675070A4ED62BAE9B29D530AE845B3485569ECBA95672EA1A2A713252198DF411CE387888010C2EAA735605D3B9D889A253338276697283443A4739CA62A64883217CAFE5C1916ACFDD689E272CC51AB5C1988C3029B7795B336F8697114DD856A0573D65395A1289B6E285A99016776B97A76E315505190A7BD331E568987BFB53346419097B83962C9D5DB85F956B0B9E0506846416F3D83D4734063B06ACEED16E839957C5A7BEFC2A4346C0051A49CD199A42F8AA2F55687F9F82AA25BA1AB71CA172078F1D2BC75A3B5FF2557167EC3D10E10813D0625D925F1B2253816486F248A2F5D02A06A8343200003F22094D9603AEB35FF9C6512A9C7D21B8C78B0961DD914A95185572730AFB39179E679F45DB4E5DE5B1C97CB85FCB89BCE8CA958654A3293EC875801F8B40A85C9D1D53025A0B28CECB0AE0D0100B73543351949C259956A859D522BD37E96CACC314E52C7B41A76BCC5C88A677AF9EBBBAA6A573B642962EA821E5A516C44B0A7C0B05F68B02EE078F7FC5C7FB02211F7CA52793437F7A9A32DB0F1040A8A01C1779DB589FF550622ACDFEA78CEEF0513A004CB77141F5CC7D163B5038AA5CAD29854BB43109D53EA3873A5F668125C58AA7BAB3C5A4551FF2990A117056536BA4A499548193CE083D8B22303A6AC6F5442F8231CDDF3B62CCA59BD6F402B612316370A3C857311262AC897B290533229AECA42CDAAF33F2943E496FFA371992D8C80AC3C7FEB46C7748910B4875672A8BA4F3CCE4762894A76846A0CCC13149E02CA12FF1776F99B140731E3E78B23799BE84163B8EE399DE987C3CB4253D15AB07F4678BF03766C0ABBFAB39CCBB8D7064C660A66335F926523A161BC4004D746FB4A764F8FF76676AF904F129457550CCC566089B81BBADC284BA712C521BA3EB8AB0C4", "8C50933FD05144630DBD942FCD3C1BF037C176257A332A440C88615D8A613EC3C219D2194F362A7BC3291D3A50C5911C26DA86AF2209A6F7A7DE997892B0149B3ACB5D4C1E2F318DDE95B1D09537571BCE152435ABD7072A60BEDF7A525AB98B28EA6E67E91FB2BC49BBBC90BA476A57DC16B5EC8663C72F2CE06B2AC91B645400EBD6B38662A3AB157190F7C3149C7638F08161185A01A594D2795D8CC6BF40996D776528D018C4603950AE4AB892D45CE3D909F2A962C29C5A99A05FBC4C3DD4C711BB8679D5614CBC039D5D13A9E23C2C5C087B7D6287906A1CAF5105F6EAACB90264824174349C21D1D940882B70D981BB2116906120B143F07E0C41709D0B03D0C21D2047A350CBC0CAB65455389FEE273A28F916A2D69D7A7B2D7A210F597235BF4994DDA3B419704C787779F07B55A2F11D94CA492B147E667BA7E77B6F208CCAA8D6105BD441F9A47E147B76EA46B9D56309E1C224D2593B5B084EFE6ACDC8F5A0D4CCB83BBB0743FC8266D42E065086E5692E4479548C399A62B4ABB246A85E4C1B6AE218D76BA984B020B48981E29156A87341D251B979E7AF8B1687F4F1AAD95C7CCF4BC7A0FCC4C33968878A6912052F16227C92BB7EAD54574E552F7EA05ED474371A3AC690601397536E8280268D1BCB3A8602F21B24FFA64B60A04A7A127E966361CD742B2BE404C14B6597DC4FBE60957724B7E16888A1260DED6268FC743DAE33A50A4532CD9396C17ABE684197907C6743C7883126C77BE58B25B75AF821C1ECC59BB504C2C9D4C45803725AFA3EEDD63A6D391AD047988A987F2EF3624B20BD795C38FC27CB9489487E223F93D228EBE9A40DB61E9F420841E74A20159770302A7A4692649247CFE2A4CF53B00C32376B33625928C4FFF278AC08BC413C1DBCECBF86C7ABE7CA4D7EF074DFF9750708A2A5199DD8598DF295AA2997AE3BC02B4EF5C957B50122E5751B0C2C844979C6E617E50A6E7E0CAC8CE0891F2133EBE59135799FC9FC6FEEE2614633A53EC194E3C9737EF68C5F208E881358080314A31CACA6C78067AACFFB572F4790C7DED53A6C6C451FAAC150203A51D208048486911A3A991627812B1BD74C834B9C08197B37028C716DFACB6BD6AB9973731E0962EBAACB4AE53F8C40CDE680C430B45DF0CB00D60BBFCDB7CAB190A63B027E9F5373CD3097145581BAE7B902D08265C488B363114AF81A866023F4B665B19868FB3A291B1139CA336DFDB6C28A74B231549790548E0C74CE067B594C47880D344A9A833033919FD505868F14108C9671535488E5F52AB38B66C615C2F2D3966DA77118FA72356321E5C55E3B9165E2D374DA05C059B47FA36310018357AC367EB8622BCD1BB2C6410DFEF40AE94A7BD1DBC7E2F01292A4CE4E3BC274AA159BE5644780C1041C0213C6311B79B923934E806586BFC15EB73C7CF0F34F99EB1AB99A5ECBE67547002DFEA019452A7F46FB3F1127A8D3B133992B9A2BAC81B94B86862166BA59B76072385E649EA6577B4FF3624B0426F3F75E493CACE7F515FDA953BA208F5B8A1C22FB2FA84B4A9EE72D9EB059E92569680029B3C90201C82D60272AD2D3C6E1914980836FA1704ABADC7C560A186BDC607105C33BA18D19F691968662854A13ECD19E3D93CC15EABB1F4595BC189CB951A957D310D4469CFEB8892021A58D3C023F815D42411C27B0777AF8C25A898C96C2BB519C5830B295EF882ADCECA22D927EBA65A00C815AC8B03B76B4CF56211A32CCC5D9957C52718E6F7240D4E389F0854D160B3FC7D51A9318C743A5748ED06DBC1051AA885E7141655FD378F99C579280027F1242DE118B69E46E2510A5E420B7D8A39E8AB6BBDA342034B1AA3E394DCF02813877A4AC02A76A03BB74804305A21C9261786463440540740A814AB3681E0FA260FFB055D8E7560BA3286E839F44D04AC9C44A6D29BF09127DD0431BF3E379E031C236D44D79C3A48CA7A42064546BC86ACFF82141E6BC17529374DC11CB5AADCD612E7415053EE266494128E87B1091DBA6591CB86C4486587B095F872A70C53DFC3A0D456A0C8865B45E5404A7FC489A8A09641B9A3B149D7261CC896107F000A7A75C6AAF772D3F9B242509318DCB1CECEC4527C92CECA5648441B99D1C84CF472572AB5575ACA40204C434283C1229570012B47829679FC8A63CE1302E132FABB9687F93095E932FA419B22849A4D6AA090FC69ECD029B6F7489B59A43C720C49CC557D8288680F6B0BC6B87D7462AD128022BFA946685A53E2A8C1F6883567B9C854A012475756FFA2A6B2C6040B443F3A425B35A9A53B6BFB029BC08986FEE5B05D8E11CC006544EA96F5400A8879C9E61E310CBF83675070A4ED62BAE9B29D530AE845B3485569ECBA95672EA1A2A713252198DF411CE387888010C2EAA735605D3B9D889A253338276697283443A4739CA62A64883217CAFE5C1916ACFDD689E272CC51AB5C1988C3029B7795B336F8697114DD856A0573D65395A1289B6E285A99016776B97A76E315505190A7BD331E568987BFB53346419097B83962C9D5DB85F956B0B9E0506846416F3D83D4734063B06ACEED16E839957C5A7BEFC2A4346C0051A49CD199A42F8AA2F55687F9F82AA25BA1AB71CA172078F1D2BC75A3B5FF2557167EC3D10E10813D0625D925F1B2253816486F248A2F5D02A06A8343200003F22094D9603AEB35FF9C6512A9C7D21B8C78B0961DD914A95185572730AFB39179E679F45DB4E5DE5B1C97CB85FCB89BCE8CA958654A3293EC875801F8B40A85C9D1D53025A0B28CECB0AE0D0100B73543351949C259956A859D522BD37E96CACC314E52C7B41A76BCC5C88A677AF9EBBBAA6A573B642962EA821E5A516C44B0A7C0B05F68B02EE078F7FC5C7FB02211F7CA52793437F7A9A32DB0F1040A8A01C1779DB589FF550622ACDFEA78CEEF0513A004CB77141F5CC7D163B5038AA5CAD29854BB43109D53EA3873A5F668125C58AA7BAB3C5A4551FF2990A117056536BA4A499548193CE083D8B22303A6AC6F5442F8231CDDF3B62CCA59BD6F402B612316370A3C857311262AC897B290533229AECA42CDAAF33F2943E496FFA371992D8C80AC3C7FEB46C7748910B4875672A8BA4F3CCE4762894A76846A0CCC13149E02CA12FF1776F99B140731E3E78B23799BE84163B8EE399DE987C3CB4253D15AB07F4678BF03766C0ABBFAB39CCBB8D7064C660A66335F926523A161BC4004D746FB4A764F8FF76676AF904F129457550CCC566089B81BBADC284BA712C521BA3EB8AB0C46E2B36020E42C91F7BE2FCDFDBC37B117984406F46374188D1A2264445D3F44D7DCB9D6A2C3832E5FE9261E29172375A78CBECDA0EF5484C4B3F6940EA20EDD9"), - ("B5DCC09F5402F5DA7695E1D61B4C1CE11380347D4C6BEF40BA11E94E755B1AFC", "AB6EAE982DDA0008CF1BDC59B5C8AC88BA11F169798AA56374ED7A550BC4F552", "52339C216A28924BC691C5C263345BD2600C3080C51B274C119606F08C628AC2C4A49565704BC16D4143EFD78844472C68E56869A599CA3A1B3831B6B170436BECAD6EB880F6C5C787674DE25202DA3C86ACC00B9FA61E0A3B6FD34AA616D95F08F09A12C896EF0C603D639676A658F2F45ACA340BDF05CCB4C72B88438369F974D8EB46732128004B27430A3B73B983FCF4276A25539DD191AE78C897266A8F31A728871CACE5B027D714A85519F57C713A1171205CBD0924CED5D7A7C6FAA0CC502EE59583C5E4848370CA25E55A554822C3C2248DE70983F16E51240D35783061AC2D7883CD90514CF2847A6ED46437427AB5E06AF5561B41AB5189853817AB6062F8BC9509AB9239BEF180319BA98C0F9B4D85D7A258B00AD2F9ADF9BCA3F1D13AAE149092769EEC263733D38CD10B56152A40703B75C0D65ACFE10932827C9B29719C6808F67112206284CF942870AA86DCB549E8023CD47608A98182ACB627DF216FC4423844D746F9993302C88FD796273A7BB18EB31B2F02A9B63CC9731A267B5C544BE82465BA1F41A1171DF44BEF444554CBC2D2AAB729EAAB510C0FDC3A8018760CAA37359A2882E3B286C7582E1EB08558F55207673BD8818E3FD98F59B913349AC15DCACDFCA901F8D63D779774521A0A02E1AB32F77AF11823BA6BCC0E5093B2F7A50099787A719350B64D2860751321AAD6C17089E3408034B708C4390A32C9EC01D094B01A4E40A0F49A5BD649BDF9F0004ADB73611C50C2C927B76C31B211B7381B9B76A521EF876D2CDB280498BC3C4261F544399F1803E2A8950B9B7F056ABFBFCC7BD380A3D4FB14C4894D8D9C2B98DC5E3BBC4057252563C58C1587C7C2F78360785A65C7793E34BAC4F616F148CAD29A6851431C948C6DA3E42B3AB7BDC07A3B0C6A6496963B49AA5F1BE56D28E64D6639CB85A2540771671C0C021098A1A0489CB7C579B33106E2D815FFE6AAB3466742E63C20062857C8B1E770CFE537C1A2E20085D3BEA8FB986C856F3BB3A50CF874034556AA564752579457BB3DDA316A5CF044F2A9B3397BA543B4BF51BA2C640698160AB9D7298753DB22F2B16EE5B433C7D08845BA32F05006AC1C9899371D63A2779DE5803B3307082382EE805E00ADA6B2D0788CBCC3DBB9C003B38AF549B83ED6B119850E4D2B360A0230677A85894A882338781B6BA9AAE3023F5B401460BE97E2ABB95B862C54269161334D5A64557656141C421D497476D81E04ABC47AE1B42025B6B91607DD14277DF333F252AF596606C0777893142CC7BC685C4C931B37A0AFB37A633B2FF7A4B49FE55BD9E55F9FDCB22C45A6DA169A49D44DAEC44567C651FBC368336CAFB82467733A28E94A23E3E1AC6D901AAFC86809683365DA7396678B1FD27AF5B9C52AA7BD02171DB9E80FC0B04E502B723A5C0552947D3FC488181297DCD41D0C42A232034621A1655C36A6973C4D9AD47B60A78F3A680709E83C4D93CDBC96BCCFB06128E9358EB98B41119FE7D29C7F315C92A17B34D666AC5AB49DD65B6021653C558DBE0C79A65BBA23D683687325D7A004AC53B20FB6601D2606AC6872DDAC03CA86BC3DB47F19A828B8A35CE7293E3E1A3B269B4488E1A30B9D15152B039DF36221FB7184EA150EAC2E80F0C5255DFC4C9A542B9953E627", "952119B40B1710433F4321C2CA494BD4F40218233943BC1147552D2DC91B026717215450F96983CF2A58E405AB2A241A58702929FC7B0905A01DC772ABC0C2DCACC02A12CE431099A2725B30A1A731DB62B46A5187040913FB1AE54096D49728F1511747386BA2F4C059F9146FB50E7DF06468676E5025B34A9765737C23719209A7860C982AA634B358E998386B2307DC994F1A4A2F1D182A8EF23429952D663B1737B2B025179FFF67A18357B36433590B8012EEC9B7FA1128B56B2A926B2D9B86A60CF35FD1C44DB8C3062E4067BD815090224DA631A00FC46186230FE52CAB341270390C00E4952557046137957C428CBAC6C25E9EBB3A5DFCCE9C7BAB51A61541E59998760F853CAC6D1478422B38B89557247388A414CE19386F01DC1C19489A29AA1123C04AFB8143E1542B45350E7A853302441D1BB836C6C97F4AA08138338A8A2238C7682C3D1C6EBE63A1E74390E2A47442C3718E61CB869B35E8FAA1D80A027AB585AAC62219A602941CC7EE60B22E97A1851225AF77082C3039E0B84035ECA9C7D2432253B80C0BCAD6E20BEC482A18500BDF75C6820A6C2F2C945A13BD2A0CB5188300D5CBCCB7B3846AF78647413DE8047CF0F9954A8804F889165374A9F5341D93AC6DF3995EF2396850AC793DE83B11A72EBFA673DFF04B868199BDC60FD44029BED01428957D505C685CFC6DFE04AF468735620B96BCA9B1B6298007EC026C54C0CF6476F2B995CC35992C791E80146E957893B744C409564D1025CE8B957FED9B95EDF00CC2B36D0F891EC7EBC545B4889BE31F5052BDE7360B0C69BD280255F5E5C66A655FC857AB5B6230000A2B2A587313D582350A35A0DA4F28F3B738989C22E25490C08456556A5A47BDE0651C9468AB297480CC3056B5AA2326F927BC19B406E41D4663CFDD00CF39A2154FC9439908020CC724C6D67E7CF77248A39C05004FF9F7A8D1569AD8B3866675AC6DF8757CE8C492460227D3A6A7D089D850BCEB770EE8B25276336C488B648CD7284AA54668E074E41A27C6E2972F922705108DF582CC713C3B981110E522A9F3873B3F345D1AB01705535A151BC92A1A0C6D126810D88B52EAA83597CC5EA7286CA243ED1542929CCFC835CC7F279A70119D3C8933015C4DD78A4C5D1184CD86AF06B6806F578AC5C4949F468AAFFC0E74FB989F760EA3525D933A0B939400D62B5681968F0012885B042481724D0DCC1A994336A37B1CB866926D331F705775B0439819393C790C78808385A0E86D92F100D95A5E5C0325F8A42E7243266D477C8FA60AF2D1C0DA05B697B945C2C1BAE9D73ACADA3C73344A7E08793D1186031449B291323A8568EBC450F82B71B7882A76991BCCB55FB70C83766713704094F4F54C0765C8E85966AA95AE0CB02360903BF5D86D2B33347D93C080A575894A2A67F32FD5A279E4B269E1896E788807BF2BACB2B3402F88B3D3193B7BB005D36752D1F47DFDC5804FD2751AA5324BCB360CE87A57B05E25467D8C76A42E574757783DF6AC54A366CFF62B52EA836C833790DA23483FE05A7B2B46DF986BF920C755064358445C7A003B0A4016A8D9597FB60020D348DFE806684B6CD9E8582CB1380DB26B99609452339C216A28924BC691C5C263345BD2600C3080C51B274C119606F08C628AC2C4A49565704BC16D4143EFD78844472C68E56869A599CA3A1B3831B6B170436BECAD6EB880F6C5C787674DE25202DA3C86ACC00B9FA61E0A3B6FD34AA616D95F08F09A12C896EF0C603D639676A658F2F45ACA340BDF05CCB4C72B88438369F974D8EB46732128004B27430A3B73B983FCF4276A25539DD191AE78C897266A8F31A728871CACE5B027D714A85519F57C713A1171205CBD0924CED5D7A7C6FAA0CC502EE59583C5E4848370CA25E55A554822C3C2248DE70983F16E51240D35783061AC2D7883CD90514CF2847A6ED46437427AB5E06AF5561B41AB5189853817AB6062F8BC9509AB9239BEF180319BA98C0F9B4D85D7A258B00AD2F9ADF9BCA3F1D13AAE149092769EEC263733D38CD10B56152A40703B75C0D65ACFE10932827C9B29719C6808F67112206284CF942870AA86DCB549E8023CD47608A98182ACB627DF216FC4423844D746F9993302C88FD796273A7BB18EB31B2F02A9B63CC9731A267B5C544BE82465BA1F41A1171DF44BEF444554CBC2D2AAB729EAAB510C0FDC3A8018760CAA37359A2882E3B286C7582E1EB08558F55207673BD8818E3FD98F59B913349AC15DCACDFCA901F8D63D779774521A0A02E1AB32F77AF11823BA6BCC0E5093B2F7A50099787A719350B64D2860751321AAD6C17089E3408034B708C4390A32C9EC01D094B01A4E40A0F49A5BD649BDF9F0004ADB73611C50C2C927B76C31B211B7381B9B76A521EF876D2CDB280498BC3C4261F544399F1803E2A8950B9B7F056ABFBFCC7BD380A3D4FB14C4894D8D9C2B98DC5E3BBC4057252563C58C1587C7C2F78360785A65C7793E34BAC4F616F148CAD29A6851431C948C6DA3E42B3AB7BDC07A3B0C6A6496963B49AA5F1BE56D28E64D6639CB85A2540771671C0C021098A1A0489CB7C579B33106E2D815FFE6AAB3466742E63C20062857C8B1E770CFE537C1A2E20085D3BEA8FB986C856F3BB3A50CF874034556AA564752579457BB3DDA316A5CF044F2A9B3397BA543B4BF51BA2C640698160AB9D7298753DB22F2B16EE5B433C7D08845BA32F05006AC1C9899371D63A2779DE5803B3307082382EE805E00ADA6B2D0788CBCC3DBB9C003B38AF549B83ED6B119850E4D2B360A0230677A85894A882338781B6BA9AAE3023F5B401460BE97E2ABB95B862C54269161334D5A64557656141C421D497476D81E04ABC47AE1B42025B6B91607DD14277DF333F252AF596606C0777893142CC7BC685C4C931B37A0AFB37A633B2FF7A4B49FE55BD9E55F9FDCB22C45A6DA169A49D44DAEC44567C651FBC368336CAFB82467733A28E94A23E3E1AC6D901AAFC86809683365DA7396678B1FD27AF5B9C52AA7BD02171DB9E80FC0B04E502B723A5C0552947D3FC488181297DCD41D0C42A232034621A1655C36A6973C4D9AD47B60A78F3A680709E83C4D93CDBC96BCCFB06128E9358EB98B41119FE7D29C7F315C92A17B34D666AC5AB49DD65B6021653C558DBE0C79A65BBA23D683687325D7A004AC53B20FB6601D2606AC6872DDAC03CA86BC3DB47F19A828B8A35CE7293E3E1A3B269B4488E1A30B9D15152B039DF36221FB7184EA150EAC2E80F0C5255DFC4C9A542B9953E6272210BAEE60864394042F2E190EDAC61E3C72AC42214318C84151D6E12D56CAB2AB6EAE982DDA0008CF1BDC59B5C8AC88BA11F169798AA56374ED7A550BC4F552"), + ( + "E582B7D75E6C80B05AE392A1FC9F7153B12390FD99930368CC67A768BAEBC8A0", + "1CDACB8740C0B87C4A379575F187B367CBFA3B300BF591B109F79816E9CBE8F0", + "28C793778741B80B02B4339F2AA4347255B099F17264E1B8CC0A2C7C2A1A79F7997B907FD0496C6E6C8AD7714F5F339D75F11F625591A869BE1175AE47F05FD4313468232BA6957D7807B824F445AC99A0D568AB1AD54DCA8249D1482E61275F52248C77F61A4248753188CD1794CD0A465EC0DC4B025985C461B74E76286E4C37E77405695CC9FD0654374B427A20343AEC0FF1A187768273BFC4905472A1DA387F14559D6CE87313F6A5B6138434539F9A13684055B177E543F8B40F432ABD7CC49989A50A9084C660913F45A8593B17499BC4CF936C2BC1851421CB986808A0EF30AFE97AAB5B8B8EB3F0B3506A95B91563A0E57DB7231044987EF141BDAB3537C316AD16F17805A81F29329879A94E96157E4B7447F7D59603B21BD896CC47B7CD4E232322EB9C5D2215696BCFFCA3A04EFCC4C5D9CC39AC9A6E8700D38C244B0169E7FA1FE81B4B10365E74E6A1F7F756D11ACDC84043F81006D62995376C22535958FEB53F78117EE0F61C4C862640D06DC57A2B8BE62A41A642AF3BC63F6BAC98BBBBFF70570F37B8F8D9572F2735657A6C98F96CAF57A849868720B2640B8BB2732237A1F984C18872D10289CE43C952C9257E06529AEB76AFD127B17596FD25C5216C9CABD9B18EFC50E87BBB04568BB7D5C4E9288C006483AF5912E19108573700BD10CD77224B80659EA75AA74270B33AC4008B738BFEE271E78658C8742FF13C96AD0781A03C7576CA26DD58B52980BA58C0505E446AFA140CDCEA0490DB1F9B18815D4314B2459CACC562441C91F4084E5426C88E632CF7482E79907911D06473260835D7B85E7856A829AEA0381707B939CE86882CC09C4448C6AE94A9C303107C5667EEFB8DF7763CC21189A3C590C40AA51F491503A7935EC08F4FC300CBE607ED8C9100C29FBF45584B13C8D780069337AEC76C36CEB70373E2AB6E7B934B466F53FB32EAF040055496B8540E23A2A277E534468608D5EC0F8D38CEA5BBB806C1BF4F164F6AC826FE733F95461E29DCC11200C0AADA1B8332023EAB329718CE25CC0A09555903F3578BBC863B1752CA94365DA556DF54C3B7E05CBB7115FBC1B6C57A172C31B9906560C8FB54F3C563A2256CC073243B8179B4A28D60E086CF51082EE429272996F0AABE03BA0EAFD3C8E7D954BD0933E2F60ED0C32CEDE7B820A28E48F3CA3C40913CCCAE2337ABFC59843F08C9863325D65A4E9E15C1F46172B118B2B5EB0F1D5158A00134F27B085488C3A0621FE4E5678698250FB74EE5152E3E35A66544A05D279EA99131FBC15165060B90F88EEB7B20892A4DE4CB1683495BD7DA037966B47CC040F1764C5DEB06B5499D4267391CEBBB47F734D8539E39528436A1858182854BF20B1F93279AFB706464C65CCC5AE099B37CC03556C26ABF4C3F8B9BA3A936707211A49A59B268F5284F7970C77612719450377417428C4BA47C9CA115CF95304C4759C5D8859B44985C06A6C924689237BA320D610960D61C53E85431789E67A40113F167FF93429C264F6CABC95448C903437D39A6577BE0CF0012852AA476351A9046A110A1A625A3D74C910B78BCE9CFCA735E4F91B8A4C57DBE489E849446098AACF73070AEE638FCC8896473D3C159D3AFB4B687B40DFBF371A9C2644B605187B71A14BC4C8678FE8247", + "3808B98D9A093C7853B0B814D1CA5F392677D3D0A38F81C852F95B9A69B374A24588C0ADD5B510BE567C5A24688EC91ED0F28BB4C86978C09793795C36B94E5DD8498EB4353FF40EA87B17E921B5B4CBA08E5B7BE5A9C8BC69AC5AC3075DD947D04B8695097AC39790A0C8ABE11200C9F136ED7B0C10077E36111C1F139D9BD27142993F5883925C4918413CB8043962A28567397A1C7E003BAC30644055155CE9464E623FC5E2160CD1143FD2C90C03A08D0B333FBB40A308CACD81618674B406809790B2538D30431F064F65EBBA7C41B2A53146A3ABCA66754CAE591534045C1D640D6308472EBCA660A8A7D372470D3869115B8860B577311980B2C66B8352229ABC17B0C31F58E5A5EBDBCA6E3B157D9CA79EE890778A4CF28429C2A82C80DC9C524B0864E385E355BD4E65732D395E464070F61828E57C3E1BE2A50052C4FF1951F5D0C89720987D8941F085871113A91F518FC79B7189056990B2447BF54FC170C932E0A4E0F7A262542F56D1B49EEBC392106D93C77DE9186DFBB824E73B2F7FC796B312875B43414877AFB356214492C19C748DD381BC7A9237BF097CC606A03119A240A5536AC7844F6A79D0E2B821B68D96542450179BCCA231F2DA68C5EB6D118B99A1B66EAA566C78A9009008B0D66155BA4839F8E518C650177DEC170EB09E8BF6A89905320590C642B801CF5B5151C2AF3E7271CB9961C65A5BD17479429A31BE9081F2767D94816C16D1B04772012382B689AB2D3FB9BDC66547BFEB23052600BB369771494D9C914EC93A066ABC9611940B947310DA04197312425D736A1933B785C95DC430791E42CD691C79BA63BE06EC80765C9C07053EAC706697F21720C672B9803DF9935532197B0485BBE42B0DD16561AC0605E9DA73C189C9E29ABA3AECA19C21621B209326418543C44B88843EB3720A2CEC3DB52C59D6507EE5CB03C268B31FC4BF115695EB0B8AD3F11FEDEA1A5D0724A5D5284D0C10E9844D0D32BB5735448421BC5317650BEAC4829B234B787339812F2316ACE8C22C42D6346214934FC0B49ECCBDF8DA19AEE64DE9628C4F7AB3E512A399B0C227B677AC4A69891A6874F6641D2CC95460B751E17E434C924B2947D806856665696E3CC4DB9553B81606C31C3800B48756A073BF685EEA20899C176769E902DB971827D153B9C33516E959C6B469841946C0D921A371A5DE9740C6AB9CA272B5850BA8753CA023A460EBF7BD573C0745F40B90F105EE17C19B6832E019B80F8858BB515F7C709E68F29C1375B22567AFE7B528F4431A94B553DF825CDEB84B4EA9296EAB9AD66271EEC5AEF6F79509A20A182279FDF92F87FB6E7509968D22CA750B5056974841B9654E5716BDC33A2C6A116A4117FA757A1D22710668412B8878E134B70FE32158A2317FBC62F01371296AA42E33C903E0C439F19684B11E2F911F7FB79860C8800F9CA146EABE29DB07237AABB503A9BE2AA7263A0626C162A27537775792B2E7B0FD347929934AD8F521D4159059F611312AB903879490059BE8B38920E2CB4A256B8B35783E909346D13E9888BEB9350369F7C1A8501331110C651621A616B365A026D1CC47DF440C9E650DD0C0BFC8295439114528C793778741B80B02B4339F2AA4347255B099F17264E1B8CC0A2C7C2A1A79F7997B907FD0496C6E6C8AD7714F5F339D75F11F625591A869BE1175AE47F05FD4313468232BA6957D7807B824F445AC99A0D568AB1AD54DCA8249D1482E61275F52248C77F61A4248753188CD1794CD0A465EC0DC4B025985C461B74E76286E4C37E77405695CC9FD0654374B427A20343AEC0FF1A187768273BFC4905472A1DA387F14559D6CE87313F6A5B6138434539F9A13684055B177E543F8B40F432ABD7CC49989A50A9084C660913F45A8593B17499BC4CF936C2BC1851421CB986808A0EF30AFE97AAB5B8B8EB3F0B3506A95B91563A0E57DB7231044987EF141BDAB3537C316AD16F17805A81F29329879A94E96157E4B7447F7D59603B21BD896CC47B7CD4E232322EB9C5D2215696BCFFCA3A04EFCC4C5D9CC39AC9A6E8700D38C244B0169E7FA1FE81B4B10365E74E6A1F7F756D11ACDC84043F81006D62995376C22535958FEB53F78117EE0F61C4C862640D06DC57A2B8BE62A41A642AF3BC63F6BAC98BBBBFF70570F37B8F8D9572F2735657A6C98F96CAF57A849868720B2640B8BB2732237A1F984C18872D10289CE43C952C9257E06529AEB76AFD127B17596FD25C5216C9CABD9B18EFC50E87BBB04568BB7D5C4E9288C006483AF5912E19108573700BD10CD77224B80659EA75AA74270B33AC4008B738BFEE271E78658C8742FF13C96AD0781A03C7576CA26DD58B52980BA58C0505E446AFA140CDCEA0490DB1F9B18815D4314B2459CACC562441C91F4084E5426C88E632CF7482E79907911D06473260835D7B85E7856A829AEA0381707B939CE86882CC09C4448C6AE94A9C303107C5667EEFB8DF7763CC21189A3C590C40AA51F491503A7935EC08F4FC300CBE607ED8C9100C29FBF45584B13C8D780069337AEC76C36CEB70373E2AB6E7B934B466F53FB32EAF040055496B8540E23A2A277E534468608D5EC0F8D38CEA5BBB806C1BF4F164F6AC826FE733F95461E29DCC11200C0AADA1B8332023EAB329718CE25CC0A09555903F3578BBC863B1752CA94365DA556DF54C3B7E05CBB7115FBC1B6C57A172C31B9906560C8FB54F3C563A2256CC073243B8179B4A28D60E086CF51082EE429272996F0AABE03BA0EAFD3C8E7D954BD0933E2F60ED0C32CEDE7B820A28E48F3CA3C40913CCCAE2337ABFC59843F08C9863325D65A4E9E15C1F46172B118B2B5EB0F1D5158A00134F27B085488C3A0621FE4E5678698250FB74EE5152E3E35A66544A05D279EA99131FBC15165060B90F88EEB7B20892A4DE4CB1683495BD7DA037966B47CC040F1764C5DEB06B5499D4267391CEBBB47F734D8539E39528436A1858182854BF20B1F93279AFB706464C65CCC5AE099B37CC03556C26ABF4C3F8B9BA3A936707211A49A59B268F5284F7970C77612719450377417428C4BA47C9CA115CF95304C4759C5D8859B44985C06A6C924689237BA320D610960D61C53E85431789E67A40113F167FF93429C264F6CABC95448C903437D39A6577BE0CF0012852AA476351A9046A110A1A625A3D74C910B78BCE9CFCA735E4F91B8A4C57DBE489E849446098AACF73070AEE638FCC8896473D3C159D3AFB4B687B40DFBF371A9C2644B605187B71A14BC4C8678FE824781E66EF5A7A221619F6A64039CC369843E10DF5C859F6959CC3FD8E5272330FD1CDACB8740C0B87C4A379575F187B367CBFA3B300BF591B109F79816E9CBE8F0", + ), + ( + "3E5848DB624613F7AC144457CC1375F006FA8CB953E767DC9E7428D00F5DAD8B", + "012DD6C2F0918B9EB6182474EB86D848F65974759D59CE151A396DEEE4CA10D1", + "43166BBD4C97F4B57D4DBA6FE40A48498B001A502C28385B74306BBA43C670ACB191FB031C220CA3238AE1CBC961E66898A4C19C0A4BFD076863651FB91A31C75A33DC18B53EC6C56679159783A563C4504CA10D1405C261F8C7FFA87280242EC8BCC3077ABDF371CF55C140D0A131E6529B40788F1943964136000CD7319E05553EA58E5898384822A62B8AB9BD03609F1566F53435C472520D957703090794179EBC73744525A8F002A14270CBD1D4B6E2538353765D49F5651C1C34CC228C4E753C5210BFD00CB378D6A4ECE80168D54A4C62740A0C4A292A42E1621F2C499E4FB94E37912C9CEA7472C65B27F65036E85D5512C38D3582DF290FA819BDF0E37EBA7C2607E6A94A2323A0FB88A6808B2F444E33999F1A10830F715960D563D0A43AAD7AC7F0B4AC4A9A3236286D5249C83DC5C4666354D0EA5FE06530E4081370264566E47ACCBC22380414FF530C394007FE6CA79DB0AC53FB2B06719908D15DDE432F03C84463EC5009D81E9882030360A0FBC34F156391B6E3AC92C795FAA2199BB4A9A33A994A16AEDB6558CE901C4B60295DFA39CDD5B97C9B1E9C3199C825BD64B819D8346B1EAA6A5F5A4BB90A7F32253D7F7BA2778BB796BC707BA53CBA7A36E3682D54C3003F572D2779AD543247C6D78EDB63739E17029594BAC09620651227C8F072C7F51672385F62E06B2C80A4160BC73C8691C5314914F4537266C0D2EC74365B8CEB99C71F076BA772493CB534192A70BD576B1BE0A42D2B87DB45A423309704BA17CBD7CC610095FAB3CA61281AB2949A0D2B548A1110852073A98C4C612BB095D28049201C4F08A6083BBB60A65CBEB81B6D2A40E0D7AB0628B5EE251143812AE9B6524174C420A82D486350954AA1E6442007239FC7ECC969E18507FB55C78CCCA6C18842D745BCD9C425DABA9033B8161374EB63244F2570D228392174545FAA4664A886118A6948451DCDF489A1BAAD5E2B605B4756F3124F80B546FCC61AD30660EC342228ABA6CFCC06E61884F96C7F675711C84451D3902C206B6F1B928A2701C021E42FCB49C28C19414ECB9288C4A51E957B4D742D0E5307D731AE0EEA9D7BC9B48BCB332EFBCC3894AF4986628FF77420B6B8ED75C4F54A063B49265C38CF17B95DE45A64CF30B5EBA5643603B28B2C4948E2A2F1E8C440C54341491B20D378DE5481CDF8310074A69D79767EE1135FD8AAE3934449B406AE447BB33148A08C1C0EE4A615A44C42943CE3D56ED50B77BB7C3D4448C106363472CA0F11F526349C673FC2C8158ACB21BC3BBA9CC39891672FFCB4B3E165E6C4A1D156AD7F7085BBC49AAC5B5AA2682F165560C4132C49E0B12FA4BBF4B40E630B375086B3EFF66B07B0CFBD207D7F1AB67EC580ADA48F36308E1566CF2FF5BD1F84B9161A73014290C384BF9E0B0AA653CE4F8C6D05CB087EF846A1478468E06F81D0B5406B9AC937AFC2795AB4472E0BA964DF4259E849030108D083BA6ADFFC63E4B77126C3BFFE389401E15EC6A88BD0B5B32BD06AEDF80792636F167589C346A6E651009557CA079C593270250D9ACB3A1BB06AF03C65F88D71EA932EB79553D60FD4D34499F23FA3498BBC3A89BE3369661CC2D292221A9B8CF379DF6702808576A37975C9B593EAB54AD4B8936DA5217C6B843AD33D", + "0E62A20CC7B3062B2EE82CAAC1005F0E9655B03BAAA905AA59283126149B60969B2720563F1BAF1A93908CF41DB362589E9694C3E6734620AF30FCAF3186B5B42B5376390BEF3B4CD1833BADF19DCCB0334A68B1C6541874659FF14CC49357C2B5EAC09DAAA77CACA604242F85039A89FB456DE488FD06137E7C527E76A18B5CA066AC95A93094B300185B001F44CC2505813534A949F335027B6C12DE7C580451A53C499DE6263117A17B5BA64471A97C0579A4FE3B20F5D24569C85D214155B4E9818D9466184C8E2FB843DF396B2AC18FCA8B61B8387A2B3458E9D0AB9E2B751BE34DD5C70132595835DA55D454C301F13B8BA542EBB6C1E86B2F621312E4B80A54E106A7E5743257CB52603A68B4A9F5D195B1283150E48F9766A1BD248438C92084B451EABA445AFC6BB03805B963537AC8255CBA18D4A9832B2752EBD9B5F7B8AA52F5207B82945C473278DC5D7850C0921423587401D9210053C9C9844460917553A60A099449A7B0D360E00044FD9932140CC1D1D444F14010F8F35869A03728951E28B7C203C5CE3F7AA61205D08A15B181C0445AB27E3F889C0E72B050476CFE29420085A09D203B1C73ADB6B59074893BC3A7533758467D6046E3633044C8A1E9BB3824B72EDC7A8391721E01E4A2A214BB71008B1EFA343B064901831B20698D3958991DD9C22058A0AD4A49657B929AD926EE1B608CB29DB3174B27CB09F7A75CE617A21AB971A28BB6445267EBB838DD3B3BB82759C26A021D0382992A6CAD218ABC44A8D9667A07F5040ADCC1423CAE1363729BE457FD94C025E1B48F071140DA992E1A557A7497907CC729E33E773221F79096ED8BB830373CFBE84F5466A02D1302B1C09215983EC0229B61302F77AB4E698016A600CAB1F9729C320F65EC1537733F3976C4AE75723903BD83437B0A519A53E56B5C49C5C15980DC5341BB45305229536D324F513615A459C5D69019DACA536C42427332279BCB8A74503F76A5AB90EC7476105BA69C518921C62AE043B5E91DC958949D57B0BEF7903CF5CFC5A06C0654A60F48388DA4723B5B3A320184FE6755A881B27C2ABE23A0B63338B1EF778DB47B2CDEE043F0F5390C84BAE4D2B3ADB2B67558B567925DA6E1B33CCB3B87126293BAA2A8B18479D65C4BDA14FFF45E64223EA6E455506164F3F453651439D275862937257F666CDC564FFA255ABBB3B6FC8493FEAB7E76E2A17E849044E62A1F980A57BA5B9397271E2B5D5B5063EDAB2098FC561D10C731AA9D7AC13FCBC793EE659D6994BB00D53323232C6A192DCBE39F6193676C69C256E9050DA78EA442078374A27BE054AA30462374B992C8A32F0440E2DCAA70927D06C97EBF4C04C9557A622203187AB514C0284ED45BDFF560F3B3A15A6A6F0A6B098DF3C9BC083348138710D8B5D97A2EBE2A90DE907658F7B8B67865A4BAA111898BDCB5521F209C5801861D394BEDF51B0302888D39A73BCB07E3E99C2174BD080B1EB4086199B4859FD233D9BA3B34A04CCC67BF59896D98675AFC4CB1656005C57BA8CA5A1B085B686A1C27BE292FD3FC0D7594522C9232DE4B09541C3C1C11A2ABE1830AB3BD80B313369407F66C970FF61586A040F7DA4743166BBD4C97F4B57D4DBA6FE40A48498B001A502C28385B74306BBA43C670ACB191FB031C220CA3238AE1CBC961E66898A4C19C0A4BFD076863651FB91A31C75A33DC18B53EC6C56679159783A563C4504CA10D1405C261F8C7FFA87280242EC8BCC3077ABDF371CF55C140D0A131E6529B40788F1943964136000CD7319E05553EA58E5898384822A62B8AB9BD03609F1566F53435C472520D957703090794179EBC73744525A8F002A14270CBD1D4B6E2538353765D49F5651C1C34CC228C4E753C5210BFD00CB378D6A4ECE80168D54A4C62740A0C4A292A42E1621F2C499E4FB94E37912C9CEA7472C65B27F65036E85D5512C38D3582DF290FA819BDF0E37EBA7C2607E6A94A2323A0FB88A6808B2F444E33999F1A10830F715960D563D0A43AAD7AC7F0B4AC4A9A3236286D5249C83DC5C4666354D0EA5FE06530E4081370264566E47ACCBC22380414FF530C394007FE6CA79DB0AC53FB2B06719908D15DDE432F03C84463EC5009D81E9882030360A0FBC34F156391B6E3AC92C795FAA2199BB4A9A33A994A16AEDB6558CE901C4B60295DFA39CDD5B97C9B1E9C3199C825BD64B819D8346B1EAA6A5F5A4BB90A7F32253D7F7BA2778BB796BC707BA53CBA7A36E3682D54C3003F572D2779AD543247C6D78EDB63739E17029594BAC09620651227C8F072C7F51672385F62E06B2C80A4160BC73C8691C5314914F4537266C0D2EC74365B8CEB99C71F076BA772493CB534192A70BD576B1BE0A42D2B87DB45A423309704BA17CBD7CC610095FAB3CA61281AB2949A0D2B548A1110852073A98C4C612BB095D28049201C4F08A6083BBB60A65CBEB81B6D2A40E0D7AB0628B5EE251143812AE9B6524174C420A82D486350954AA1E6442007239FC7ECC969E18507FB55C78CCCA6C18842D745BCD9C425DABA9033B8161374EB63244F2570D228392174545FAA4664A886118A6948451DCDF489A1BAAD5E2B605B4756F3124F80B546FCC61AD30660EC342228ABA6CFCC06E61884F96C7F675711C84451D3902C206B6F1B928A2701C021E42FCB49C28C19414ECB9288C4A51E957B4D742D0E5307D731AE0EEA9D7BC9B48BCB332EFBCC3894AF4986628FF77420B6B8ED75C4F54A063B49265C38CF17B95DE45A64CF30B5EBA5643603B28B2C4948E2A2F1E8C440C54341491B20D378DE5481CDF8310074A69D79767EE1135FD8AAE3934449B406AE447BB33148A08C1C0EE4A615A44C42943CE3D56ED50B77BB7C3D4448C106363472CA0F11F526349C673FC2C8158ACB21BC3BBA9CC39891672FFCB4B3E165E6C4A1D156AD7F7085BBC49AAC5B5AA2682F165560C4132C49E0B12FA4BBF4B40E630B375086B3EFF66B07B0CFBD207D7F1AB67EC580ADA48F36308E1566CF2FF5BD1F84B9161A73014290C384BF9E0B0AA653CE4F8C6D05CB087EF846A1478468E06F81D0B5406B9AC937AFC2795AB4472E0BA964DF4259E849030108D083BA6ADFFC63E4B77126C3BFFE389401E15EC6A88BD0B5B32BD06AEDF80792636F167589C346A6E651009557CA079C593270250D9ACB3A1BB06AF03C65F88D71EA932EB79553D60FD4D34499F23FA3498BBC3A89BE3369661CC2D292221A9B8CF379DF6702808576A37975C9B593EAB54AD4B8936DA5217C6B843AD33D423290E0CCB74DA11B95294FEF12EC690815FF103075053B89C5021AEFF4CA0C012DD6C2F0918B9EB6182474EB86D848F65974759D59CE151A396DEEE4CA10D1", + ), + ( + "882FDEA55C6B497A6F2A1321FC82160C630D9A1F2E35BBE0D3332E3A0CFBC8C9", + "3A21F601923C559179A3AA5839A148487379EAAD934BF27F3071E1B1FDC21D1A", + "3A7714CDE9B06A8990EAC99A89A12EB606017F841D4BE2100AEB750C3892EF544418216EA437AFFE88059A5A42694089FE7020828160683C0E7EFA4519591A4DAAC30456A5EC2AC39CA0CB3F3920237097E6E5157E1881A09A03AEBA5B9925CC81F20A66D86EC52245E828655032067E8BB813F43BE6DA09FCFA2FC2452EED5CA4DBA87DC306A573911205B53B91187D44214D12CA1147B4174956C2BBF1863D85567B8BA14F24B2B6949F6639288DE34C117391A19B678B3406FF784AB87B66031371BD0073DB89A4ABF597D40213BCD4A93AE834F14A52D527C7486C9DDBF62148036AA55A06ED156A0C0B9366D15B582338BEDB8466317FEC3109139C8CE2C1079A070597C6B712C1B16ACA049F3C8999A95E1DD20163226B96C5724EF6908A9C5DB43637708471E02C31E1B40A226B01DABB057C328717A4BB41F9A32230079A49BEB2E892F9DA40FF947CCA4410497701795C62A113B30AC0A7D143A8CA8C3C0189407F95AB1B05719C1ACCF500676B3596141A409B6C0E952A5F1D456A70492B83C4515DD99B763A24B4C1545ABC8715E61857A89CF6D760D00C7B47D407C1C230C6212215CC805C76979671AF6D3168C73CCA4066233BF66FC1D485E225757783CC0457059CE05B9A2409B7B25415121BB2823B032BB40AD8318A86682A1C9FA6F571A08054939144F1AC2F5D299C97C730F742B75C4521851B8D9FE6AD43121C74AA248B751998A2AD9FB747BCF18AEDB4B4B0E718E979B7F7580C14BA62B42B8997F17E6812BC46AB6799F7519A93BFF43170F2C01B31D96459B7A8956B9DA9B1C2CC7C7E3C849FF326003BA17440539E01498CBA96B0922468DE2345857C3405597AD822004A2B2FBD8723E9F59CFBA30732E3BF68131283B87D05620440A2A7FC3582C20B68D0A8763B5276C12957AFA766C4D0A524800A1D954934FA0CDDD684D85C997BF2C54D8CBCD924B19E731F80B35138752A8E91C0876700F4AC3247BA6DDEBC1657941DFFE96D1803C81B75A6D07CB6CD4C14B8C904020CBB6BDA1D019CC49EC90A12581F944263DFE06DB5E16C94EBA98276B9AC3B13B8A9083AD3190195583EA7A76051A27F2121F46318580C37E50B0429480F7EAC312294CD3C71BDBBB0557C41AFC363A6411805964221D26B0EB7B798CE791EF121BBBAC1BDBB88BD57DCA9114564F4B388B1A2C32912A8BB5C1F21763294498E2B0620F3C085E7C4099D6A3E4FAB7FA12A73E39167BCE99A6F69A293A37EF238CE2ED7A1804A596CA33897F4CB772C4083A55438490D3E1CC1C5E91D9B78A5E67BBE3C7C3B1293CEB0BA7B7E48CB2BCBB30DD70296A17BEF964F265C5A41152964574C0505975DDC10BA9850C6199C83E157F2A74D8FE7496AD2BA6D83586C7624B87C3E00B84B65620DB69643AD9046105B5A6CD8065716C398D0BCE0E1577E3BBE192651692B79DFB747F7621E2C9A87BF2B602105CD893B2BEA50A10D952CC8DBBB1499B9771A3999FB91E0C7438AB641D79A568A375DF1D07E28E2C9C450A72F8C4AA5D71B82A09E1196AA3FCA8E7C684A4DDB635E775FC03604711139F332AE97EA1BA2813B1690BF9CF70F506CBA90641963D5CFAD3698DA30C866C95C66B4846F785CCBF373F4621855FD4815F0920AC4F940CA25DD130DCFA223E9B4", + "7A804DFB96B63BA97AF4E35FE6117A82D68509A81C70CB54C68A33280C2640599E918A2074627CC1AC8966A74E08822DC5731E79D82B8DCA0CBBE6C8D1EA6973A970DBB44D5FBA31E14593E9ACBC77F02ACAB7A58D656854F4031DD209A5B33A167943072BBD660B815FD13AAD82A045269A161A86FC5BB72C4CAA704C105C37CF04CC4C8A1643B01C37AF0C8D4D39CA85025665A1B66E907720288BB955A7BBF639123376A3E27D919499BE7A42E1FB09B9AB27029AC076DB2F25147A2BF38F6C5B287BB424BAE821D79875C8C41FB120A1BB957BBB57236684636C758E909120AD01332C60BA5304133CF7837EAC6D31FB7CF160251DA3C4BD1B18B0FA674F1B5ECA1603FCE92928B821A8A68E6A085E98EB2843E722481A0B81E16E2C4BB0F4B0896DBB2C900C8DD7E500A6C84EABB44D4A670D54203E921344C6C1401516AF0168CD1F0821C0FB260044B6F908A68E58170CCB34C4E39919DC747FFA13813240A43631D865ADFE335936034BDA443773B967C8B9636109CCF919A9B6F233F0C3598A7ACC96721CD5489EBDC29549ACC16F0A64C3B0B9C742CD81C504BF6A9898A38D052A36036436655285BC26A249D190E72337D4A0BF191954A5AC76982945205113977A1D6937824C773234CB2EE7716570628BF1048A04B38E5D9C42C3AAAB1581446C3B96CE96674C119A709C0425E6BCE65477252C628C6988DF6316813B6F4D37C9FF339D22A76E3A8A1376498B63AA221FB329EF270614745E94655C6591747DE740586386A6150D66E8557225B38D3173F5816BA12C395C09712A5B289031981C2BC97C51C39499A144C2976305BE1BB08CD6A305212C2136FB8921F337E6D61E8E8ACAA525B8C9C60F3AA66EF9A56DBB247DDD424D67C95DC6B1428AE63CD2A236CCEC911378BAA1E101961864D858682A762113E88F98F25D1ACC9E2E1907536257C3CA97B81091557CA0C691B90DF6B707D55A94413C5382CA4DE2605740156CE738796530781A2E9C629BB1CA0D82B5024A262437243B29E2586F4C8585777EC82898E4942ECD1325D719B21017451B5908691B35722650A81964720ACB595009B99749852035CBD4B1AD51C178E9C97C604FFEF08E7FF4057D274F1E078F46D04298F0CE82A44C8224C31697C25A687DC845B8A55A050D69C6E6A4226759BD0794C5BF0C33CAE843E2FABF738C7957D61206FBACF8B54472C896174195EA8923E3995DE50C9C430C4FD4F81ABD21731DF968E3617575E90A7B430990055068D3390E376FC62490AA97A27F5441A5A149C07344AAB833E19B093FD9BCBC8A05CE1B6FBFFBC7BC424EFC7255178569E2447C59B851E39A2017390B4F230D6DC927BF83BF2F868E104129C5DB003EEC8F9218357A6553EBB41D6CE558CB3B8FF86A3F125A8BED75C675E13A9F343A4B791E7FD87ED4228556A494B5C0CA685AC4AB663527ECCA5E5279F1277AB51186D0EBC50FE5B4B64590F26A87E9049E29735190579DDD194180B60B78579EAD248895C55422A18E59D01E6166BCD19C08D1917280D0265A370D5A34334FC9B29F931C5C68297937198B06791D838492D73C389A9556FA0640E03537F5C744A379892B362B0B1300678F3A7714CDE9B06A8990EAC99A89A12EB606017F841D4BE2100AEB750C3892EF544418216EA437AFFE88059A5A42694089FE7020828160683C0E7EFA4519591A4DAAC30456A5EC2AC39CA0CB3F3920237097E6E5157E1881A09A03AEBA5B9925CC81F20A66D86EC52245E828655032067E8BB813F43BE6DA09FCFA2FC2452EED5CA4DBA87DC306A573911205B53B91187D44214D12CA1147B4174956C2BBF1863D85567B8BA14F24B2B6949F6639288DE34C117391A19B678B3406FF784AB87B66031371BD0073DB89A4ABF597D40213BCD4A93AE834F14A52D527C7486C9DDBF62148036AA55A06ED156A0C0B9366D15B582338BEDB8466317FEC3109139C8CE2C1079A070597C6B712C1B16ACA049F3C8999A95E1DD20163226B96C5724EF6908A9C5DB43637708471E02C31E1B40A226B01DABB057C328717A4BB41F9A32230079A49BEB2E892F9DA40FF947CCA4410497701795C62A113B30AC0A7D143A8CA8C3C0189407F95AB1B05719C1ACCF500676B3596141A409B6C0E952A5F1D456A70492B83C4515DD99B763A24B4C1545ABC8715E61857A89CF6D760D00C7B47D407C1C230C6212215CC805C76979671AF6D3168C73CCA4066233BF66FC1D485E225757783CC0457059CE05B9A2409B7B25415121BB2823B032BB40AD8318A86682A1C9FA6F571A08054939144F1AC2F5D299C97C730F742B75C4521851B8D9FE6AD43121C74AA248B751998A2AD9FB747BCF18AEDB4B4B0E718E979B7F7580C14BA62B42B8997F17E6812BC46AB6799F7519A93BFF43170F2C01B31D96459B7A8956B9DA9B1C2CC7C7E3C849FF326003BA17440539E01498CBA96B0922468DE2345857C3405597AD822004A2B2FBD8723E9F59CFBA30732E3BF68131283B87D05620440A2A7FC3582C20B68D0A8763B5276C12957AFA766C4D0A524800A1D954934FA0CDDD684D85C997BF2C54D8CBCD924B19E731F80B35138752A8E91C0876700F4AC3247BA6DDEBC1657941DFFE96D1803C81B75A6D07CB6CD4C14B8C904020CBB6BDA1D019CC49EC90A12581F944263DFE06DB5E16C94EBA98276B9AC3B13B8A9083AD3190195583EA7A76051A27F2121F46318580C37E50B0429480F7EAC312294CD3C71BDBBB0557C41AFC363A6411805964221D26B0EB7B798CE791EF121BBBAC1BDBB88BD57DCA9114564F4B388B1A2C32912A8BB5C1F21763294498E2B0620F3C085E7C4099D6A3E4FAB7FA12A73E39167BCE99A6F69A293A37EF238CE2ED7A1804A596CA33897F4CB772C4083A55438490D3E1CC1C5E91D9B78A5E67BBE3C7C3B1293CEB0BA7B7E48CB2BCBB30DD70296A17BEF964F265C5A41152964574C0505975DDC10BA9850C6199C83E157F2A74D8FE7496AD2BA6D83586C7624B87C3E00B84B65620DB69643AD9046105B5A6CD8065716C398D0BCE0E1577E3BBE192651692B79DFB747F7621E2C9A87BF2B602105CD893B2BEA50A10D952CC8DBBB1499B9771A3999FB91E0C7438AB641D79A568A375DF1D07E28E2C9C450A72F8C4AA5D71B82A09E1196AA3FCA8E7C684A4DDB635E775FC03604711139F332AE97EA1BA2813B1690BF9CF70F506CBA90641963D5CFAD3698DA30C866C95C66B4846F785CCBF373F4621855FD4815F0920AC4F940CA25DD130DCFA223E9B45F13EE3FD74B1366AFA7F3FFF4B8F645331DB5526FDD7998A4EEB5C27C884DB83A21F601923C559179A3AA5839A148487379EAAD934BF27F3071E1B1FDC21D1A", + ), + ( + "AEE013310D04215B77F073A7E22B77BF0221394A08E96BD07E3C99A9AF45F16B", + "7DCB9D6A2C3832E5FE9261E29172375A78CBECDA0EF5484C4B3F6940EA20EDD9", + "7105C33BA18D19F691968662854A13ECD19E3D93CC15EABB1F4595BC189CB951A957D310D4469CFEB8892021A58D3C023F815D42411C27B0777AF8C25A898C96C2BB519C5830B295EF882ADCECA22D927EBA65A00C815AC8B03B76B4CF56211A32CCC5D9957C52718E6F7240D4E389F0854D160B3FC7D51A9318C743A5748ED06DBC1051AA885E7141655FD378F99C579280027F1242DE118B69E46E2510A5E420B7D8A39E8AB6BBDA342034B1AA3E394DCF02813877A4AC02A76A03BB74804305A21C9261786463440540740A814AB3681E0FA260FFB055D8E7560BA3286E839F44D04AC9C44A6D29BF09127DD0431BF3E379E031C236D44D79C3A48CA7A42064546BC86ACFF82141E6BC17529374DC11CB5AADCD612E7415053EE266494128E87B1091DBA6591CB86C4486587B095F872A70C53DFC3A0D456A0C8865B45E5404A7FC489A8A09641B9A3B149D7261CC896107F000A7A75C6AAF772D3F9B242509318DCB1CECEC4527C92CECA5648441B99D1C84CF472572AB5575ACA40204C434283C1229570012B47829679FC8A63CE1302E132FABB9687F93095E932FA419B22849A4D6AA090FC69ECD029B6F7489B59A43C720C49CC557D8288680F6B0BC6B87D7462AD128022BFA946685A53E2A8C1F6883567B9C854A012475756FFA2A6B2C6040B443F3A425B35A9A53B6BFB029BC08986FEE5B05D8E11CC006544EA96F5400A8879C9E61E310CBF83675070A4ED62BAE9B29D530AE845B3485569ECBA95672EA1A2A713252198DF411CE387888010C2EAA735605D3B9D889A253338276697283443A4739CA62A64883217CAFE5C1916ACFDD689E272CC51AB5C1988C3029B7795B336F8697114DD856A0573D65395A1289B6E285A99016776B97A76E315505190A7BD331E568987BFB53346419097B83962C9D5DB85F956B0B9E0506846416F3D83D4734063B06ACEED16E839957C5A7BEFC2A4346C0051A49CD199A42F8AA2F55687F9F82AA25BA1AB71CA172078F1D2BC75A3B5FF2557167EC3D10E10813D0625D925F1B2253816486F248A2F5D02A06A8343200003F22094D9603AEB35FF9C6512A9C7D21B8C78B0961DD914A95185572730AFB39179E679F45DB4E5DE5B1C97CB85FCB89BCE8CA958654A3293EC875801F8B40A85C9D1D53025A0B28CECB0AE0D0100B73543351949C259956A859D522BD37E96CACC314E52C7B41A76BCC5C88A677AF9EBBBAA6A573B642962EA821E5A516C44B0A7C0B05F68B02EE078F7FC5C7FB02211F7CA52793437F7A9A32DB0F1040A8A01C1779DB589FF550622ACDFEA78CEEF0513A004CB77141F5CC7D163B5038AA5CAD29854BB43109D53EA3873A5F668125C58AA7BAB3C5A4551FF2990A117056536BA4A499548193CE083D8B22303A6AC6F5442F8231CDDF3B62CCA59BD6F402B612316370A3C857311262AC897B290533229AECA42CDAAF33F2943E496FFA371992D8C80AC3C7FEB46C7748910B4875672A8BA4F3CCE4762894A76846A0CCC13149E02CA12FF1776F99B140731E3E78B23799BE84163B8EE399DE987C3CB4253D15AB07F4678BF03766C0ABBFAB39CCBB8D7064C660A66335F926523A161BC4004D746FB4A764F8FF76676AF904F129457550CCC566089B81BBADC284BA712C521BA3EB8AB0C4", + "8C50933FD05144630DBD942FCD3C1BF037C176257A332A440C88615D8A613EC3C219D2194F362A7BC3291D3A50C5911C26DA86AF2209A6F7A7DE997892B0149B3ACB5D4C1E2F318DDE95B1D09537571BCE152435ABD7072A60BEDF7A525AB98B28EA6E67E91FB2BC49BBBC90BA476A57DC16B5EC8663C72F2CE06B2AC91B645400EBD6B38662A3AB157190F7C3149C7638F08161185A01A594D2795D8CC6BF40996D776528D018C4603950AE4AB892D45CE3D909F2A962C29C5A99A05FBC4C3DD4C711BB8679D5614CBC039D5D13A9E23C2C5C087B7D6287906A1CAF5105F6EAACB90264824174349C21D1D940882B70D981BB2116906120B143F07E0C41709D0B03D0C21D2047A350CBC0CAB65455389FEE273A28F916A2D69D7A7B2D7A210F597235BF4994DDA3B419704C787779F07B55A2F11D94CA492B147E667BA7E77B6F208CCAA8D6105BD441F9A47E147B76EA46B9D56309E1C224D2593B5B084EFE6ACDC8F5A0D4CCB83BBB0743FC8266D42E065086E5692E4479548C399A62B4ABB246A85E4C1B6AE218D76BA984B020B48981E29156A87341D251B979E7AF8B1687F4F1AAD95C7CCF4BC7A0FCC4C33968878A6912052F16227C92BB7EAD54574E552F7EA05ED474371A3AC690601397536E8280268D1BCB3A8602F21B24FFA64B60A04A7A127E966361CD742B2BE404C14B6597DC4FBE60957724B7E16888A1260DED6268FC743DAE33A50A4532CD9396C17ABE684197907C6743C7883126C77BE58B25B75AF821C1ECC59BB504C2C9D4C45803725AFA3EEDD63A6D391AD047988A987F2EF3624B20BD795C38FC27CB9489487E223F93D228EBE9A40DB61E9F420841E74A20159770302A7A4692649247CFE2A4CF53B00C32376B33625928C4FFF278AC08BC413C1DBCECBF86C7ABE7CA4D7EF074DFF9750708A2A5199DD8598DF295AA2997AE3BC02B4EF5C957B50122E5751B0C2C844979C6E617E50A6E7E0CAC8CE0891F2133EBE59135799FC9FC6FEEE2614633A53EC194E3C9737EF68C5F208E881358080314A31CACA6C78067AACFFB572F4790C7DED53A6C6C451FAAC150203A51D208048486911A3A991627812B1BD74C834B9C08197B37028C716DFACB6BD6AB9973731E0962EBAACB4AE53F8C40CDE680C430B45DF0CB00D60BBFCDB7CAB190A63B027E9F5373CD3097145581BAE7B902D08265C488B363114AF81A866023F4B665B19868FB3A291B1139CA336DFDB6C28A74B231549790548E0C74CE067B594C47880D344A9A833033919FD505868F14108C9671535488E5F52AB38B66C615C2F2D3966DA77118FA72356321E5C55E3B9165E2D374DA05C059B47FA36310018357AC367EB8622BCD1BB2C6410DFEF40AE94A7BD1DBC7E2F01292A4CE4E3BC274AA159BE5644780C1041C0213C6311B79B923934E806586BFC15EB73C7CF0F34F99EB1AB99A5ECBE67547002DFEA019452A7F46FB3F1127A8D3B133992B9A2BAC81B94B86862166BA59B76072385E649EA6577B4FF3624B0426F3F75E493CACE7F515FDA953BA208F5B8A1C22FB2FA84B4A9EE72D9EB059E92569680029B3C90201C82D60272AD2D3C6E1914980836FA1704ABADC7C560A186BDC607105C33BA18D19F691968662854A13ECD19E3D93CC15EABB1F4595BC189CB951A957D310D4469CFEB8892021A58D3C023F815D42411C27B0777AF8C25A898C96C2BB519C5830B295EF882ADCECA22D927EBA65A00C815AC8B03B76B4CF56211A32CCC5D9957C52718E6F7240D4E389F0854D160B3FC7D51A9318C743A5748ED06DBC1051AA885E7141655FD378F99C579280027F1242DE118B69E46E2510A5E420B7D8A39E8AB6BBDA342034B1AA3E394DCF02813877A4AC02A76A03BB74804305A21C9261786463440540740A814AB3681E0FA260FFB055D8E7560BA3286E839F44D04AC9C44A6D29BF09127DD0431BF3E379E031C236D44D79C3A48CA7A42064546BC86ACFF82141E6BC17529374DC11CB5AADCD612E7415053EE266494128E87B1091DBA6591CB86C4486587B095F872A70C53DFC3A0D456A0C8865B45E5404A7FC489A8A09641B9A3B149D7261CC896107F000A7A75C6AAF772D3F9B242509318DCB1CECEC4527C92CECA5648441B99D1C84CF472572AB5575ACA40204C434283C1229570012B47829679FC8A63CE1302E132FABB9687F93095E932FA419B22849A4D6AA090FC69ECD029B6F7489B59A43C720C49CC557D8288680F6B0BC6B87D7462AD128022BFA946685A53E2A8C1F6883567B9C854A012475756FFA2A6B2C6040B443F3A425B35A9A53B6BFB029BC08986FEE5B05D8E11CC006544EA96F5400A8879C9E61E310CBF83675070A4ED62BAE9B29D530AE845B3485569ECBA95672EA1A2A713252198DF411CE387888010C2EAA735605D3B9D889A253338276697283443A4739CA62A64883217CAFE5C1916ACFDD689E272CC51AB5C1988C3029B7795B336F8697114DD856A0573D65395A1289B6E285A99016776B97A76E315505190A7BD331E568987BFB53346419097B83962C9D5DB85F956B0B9E0506846416F3D83D4734063B06ACEED16E839957C5A7BEFC2A4346C0051A49CD199A42F8AA2F55687F9F82AA25BA1AB71CA172078F1D2BC75A3B5FF2557167EC3D10E10813D0625D925F1B2253816486F248A2F5D02A06A8343200003F22094D9603AEB35FF9C6512A9C7D21B8C78B0961DD914A95185572730AFB39179E679F45DB4E5DE5B1C97CB85FCB89BCE8CA958654A3293EC875801F8B40A85C9D1D53025A0B28CECB0AE0D0100B73543351949C259956A859D522BD37E96CACC314E52C7B41A76BCC5C88A677AF9EBBBAA6A573B642962EA821E5A516C44B0A7C0B05F68B02EE078F7FC5C7FB02211F7CA52793437F7A9A32DB0F1040A8A01C1779DB589FF550622ACDFEA78CEEF0513A004CB77141F5CC7D163B5038AA5CAD29854BB43109D53EA3873A5F668125C58AA7BAB3C5A4551FF2990A117056536BA4A499548193CE083D8B22303A6AC6F5442F8231CDDF3B62CCA59BD6F402B612316370A3C857311262AC897B290533229AECA42CDAAF33F2943E496FFA371992D8C80AC3C7FEB46C7748910B4875672A8BA4F3CCE4762894A76846A0CCC13149E02CA12FF1776F99B140731E3E78B23799BE84163B8EE399DE987C3CB4253D15AB07F4678BF03766C0ABBFAB39CCBB8D7064C660A66335F926523A161BC4004D746FB4A764F8FF76676AF904F129457550CCC566089B81BBADC284BA712C521BA3EB8AB0C46E2B36020E42C91F7BE2FCDFDBC37B117984406F46374188D1A2264445D3F44D7DCB9D6A2C3832E5FE9261E29172375A78CBECDA0EF5484C4B3F6940EA20EDD9", + ), + ( + "B5DCC09F5402F5DA7695E1D61B4C1CE11380347D4C6BEF40BA11E94E755B1AFC", + "AB6EAE982DDA0008CF1BDC59B5C8AC88BA11F169798AA56374ED7A550BC4F552", + "52339C216A28924BC691C5C263345BD2600C3080C51B274C119606F08C628AC2C4A49565704BC16D4143EFD78844472C68E56869A599CA3A1B3831B6B170436BECAD6EB880F6C5C787674DE25202DA3C86ACC00B9FA61E0A3B6FD34AA616D95F08F09A12C896EF0C603D639676A658F2F45ACA340BDF05CCB4C72B88438369F974D8EB46732128004B27430A3B73B983FCF4276A25539DD191AE78C897266A8F31A728871CACE5B027D714A85519F57C713A1171205CBD0924CED5D7A7C6FAA0CC502EE59583C5E4848370CA25E55A554822C3C2248DE70983F16E51240D35783061AC2D7883CD90514CF2847A6ED46437427AB5E06AF5561B41AB5189853817AB6062F8BC9509AB9239BEF180319BA98C0F9B4D85D7A258B00AD2F9ADF9BCA3F1D13AAE149092769EEC263733D38CD10B56152A40703B75C0D65ACFE10932827C9B29719C6808F67112206284CF942870AA86DCB549E8023CD47608A98182ACB627DF216FC4423844D746F9993302C88FD796273A7BB18EB31B2F02A9B63CC9731A267B5C544BE82465BA1F41A1171DF44BEF444554CBC2D2AAB729EAAB510C0FDC3A8018760CAA37359A2882E3B286C7582E1EB08558F55207673BD8818E3FD98F59B913349AC15DCACDFCA901F8D63D779774521A0A02E1AB32F77AF11823BA6BCC0E5093B2F7A50099787A719350B64D2860751321AAD6C17089E3408034B708C4390A32C9EC01D094B01A4E40A0F49A5BD649BDF9F0004ADB73611C50C2C927B76C31B211B7381B9B76A521EF876D2CDB280498BC3C4261F544399F1803E2A8950B9B7F056ABFBFCC7BD380A3D4FB14C4894D8D9C2B98DC5E3BBC4057252563C58C1587C7C2F78360785A65C7793E34BAC4F616F148CAD29A6851431C948C6DA3E42B3AB7BDC07A3B0C6A6496963B49AA5F1BE56D28E64D6639CB85A2540771671C0C021098A1A0489CB7C579B33106E2D815FFE6AAB3466742E63C20062857C8B1E770CFE537C1A2E20085D3BEA8FB986C856F3BB3A50CF874034556AA564752579457BB3DDA316A5CF044F2A9B3397BA543B4BF51BA2C640698160AB9D7298753DB22F2B16EE5B433C7D08845BA32F05006AC1C9899371D63A2779DE5803B3307082382EE805E00ADA6B2D0788CBCC3DBB9C003B38AF549B83ED6B119850E4D2B360A0230677A85894A882338781B6BA9AAE3023F5B401460BE97E2ABB95B862C54269161334D5A64557656141C421D497476D81E04ABC47AE1B42025B6B91607DD14277DF333F252AF596606C0777893142CC7BC685C4C931B37A0AFB37A633B2FF7A4B49FE55BD9E55F9FDCB22C45A6DA169A49D44DAEC44567C651FBC368336CAFB82467733A28E94A23E3E1AC6D901AAFC86809683365DA7396678B1FD27AF5B9C52AA7BD02171DB9E80FC0B04E502B723A5C0552947D3FC488181297DCD41D0C42A232034621A1655C36A6973C4D9AD47B60A78F3A680709E83C4D93CDBC96BCCFB06128E9358EB98B41119FE7D29C7F315C92A17B34D666AC5AB49DD65B6021653C558DBE0C79A65BBA23D683687325D7A004AC53B20FB6601D2606AC6872DDAC03CA86BC3DB47F19A828B8A35CE7293E3E1A3B269B4488E1A30B9D15152B039DF36221FB7184EA150EAC2E80F0C5255DFC4C9A542B9953E627", + "952119B40B1710433F4321C2CA494BD4F40218233943BC1147552D2DC91B026717215450F96983CF2A58E405AB2A241A58702929FC7B0905A01DC772ABC0C2DCACC02A12CE431099A2725B30A1A731DB62B46A5187040913FB1AE54096D49728F1511747386BA2F4C059F9146FB50E7DF06468676E5025B34A9765737C23719209A7860C982AA634B358E998386B2307DC994F1A4A2F1D182A8EF23429952D663B1737B2B025179FFF67A18357B36433590B8012EEC9B7FA1128B56B2A926B2D9B86A60CF35FD1C44DB8C3062E4067BD815090224DA631A00FC46186230FE52CAB341270390C00E4952557046137957C428CBAC6C25E9EBB3A5DFCCE9C7BAB51A61541E59998760F853CAC6D1478422B38B89557247388A414CE19386F01DC1C19489A29AA1123C04AFB8143E1542B45350E7A853302441D1BB836C6C97F4AA08138338A8A2238C7682C3D1C6EBE63A1E74390E2A47442C3718E61CB869B35E8FAA1D80A027AB585AAC62219A602941CC7EE60B22E97A1851225AF77082C3039E0B84035ECA9C7D2432253B80C0BCAD6E20BEC482A18500BDF75C6820A6C2F2C945A13BD2A0CB5188300D5CBCCB7B3846AF78647413DE8047CF0F9954A8804F889165374A9F5341D93AC6DF3995EF2396850AC793DE83B11A72EBFA673DFF04B868199BDC60FD44029BED01428957D505C685CFC6DFE04AF468735620B96BCA9B1B6298007EC026C54C0CF6476F2B995CC35992C791E80146E957893B744C409564D1025CE8B957FED9B95EDF00CC2B36D0F891EC7EBC545B4889BE31F5052BDE7360B0C69BD280255F5E5C66A655FC857AB5B6230000A2B2A587313D582350A35A0DA4F28F3B738989C22E25490C08456556A5A47BDE0651C9468AB297480CC3056B5AA2326F927BC19B406E41D4663CFDD00CF39A2154FC9439908020CC724C6D67E7CF77248A39C05004FF9F7A8D1569AD8B3866675AC6DF8757CE8C492460227D3A6A7D089D850BCEB770EE8B25276336C488B648CD7284AA54668E074E41A27C6E2972F922705108DF582CC713C3B981110E522A9F3873B3F345D1AB01705535A151BC92A1A0C6D126810D88B52EAA83597CC5EA7286CA243ED1542929CCFC835CC7F279A70119D3C8933015C4DD78A4C5D1184CD86AF06B6806F578AC5C4949F468AAFFC0E74FB989F760EA3525D933A0B939400D62B5681968F0012885B042481724D0DCC1A994336A37B1CB866926D331F705775B0439819393C790C78808385A0E86D92F100D95A5E5C0325F8A42E7243266D477C8FA60AF2D1C0DA05B697B945C2C1BAE9D73ACADA3C73344A7E08793D1186031449B291323A8568EBC450F82B71B7882A76991BCCB55FB70C83766713704094F4F54C0765C8E85966AA95AE0CB02360903BF5D86D2B33347D93C080A575894A2A67F32FD5A279E4B269E1896E788807BF2BACB2B3402F88B3D3193B7BB005D36752D1F47DFDC5804FD2751AA5324BCB360CE87A57B05E25467D8C76A42E574757783DF6AC54A366CFF62B52EA836C833790DA23483FE05A7B2B46DF986BF920C755064358445C7A003B0A4016A8D9597FB60020D348DFE806684B6CD9E8582CB1380DB26B99609452339C216A28924BC691C5C263345BD2600C3080C51B274C119606F08C628AC2C4A49565704BC16D4143EFD78844472C68E56869A599CA3A1B3831B6B170436BECAD6EB880F6C5C787674DE25202DA3C86ACC00B9FA61E0A3B6FD34AA616D95F08F09A12C896EF0C603D639676A658F2F45ACA340BDF05CCB4C72B88438369F974D8EB46732128004B27430A3B73B983FCF4276A25539DD191AE78C897266A8F31A728871CACE5B027D714A85519F57C713A1171205CBD0924CED5D7A7C6FAA0CC502EE59583C5E4848370CA25E55A554822C3C2248DE70983F16E51240D35783061AC2D7883CD90514CF2847A6ED46437427AB5E06AF5561B41AB5189853817AB6062F8BC9509AB9239BEF180319BA98C0F9B4D85D7A258B00AD2F9ADF9BCA3F1D13AAE149092769EEC263733D38CD10B56152A40703B75C0D65ACFE10932827C9B29719C6808F67112206284CF942870AA86DCB549E8023CD47608A98182ACB627DF216FC4423844D746F9993302C88FD796273A7BB18EB31B2F02A9B63CC9731A267B5C544BE82465BA1F41A1171DF44BEF444554CBC2D2AAB729EAAB510C0FDC3A8018760CAA37359A2882E3B286C7582E1EB08558F55207673BD8818E3FD98F59B913349AC15DCACDFCA901F8D63D779774521A0A02E1AB32F77AF11823BA6BCC0E5093B2F7A50099787A719350B64D2860751321AAD6C17089E3408034B708C4390A32C9EC01D094B01A4E40A0F49A5BD649BDF9F0004ADB73611C50C2C927B76C31B211B7381B9B76A521EF876D2CDB280498BC3C4261F544399F1803E2A8950B9B7F056ABFBFCC7BD380A3D4FB14C4894D8D9C2B98DC5E3BBC4057252563C58C1587C7C2F78360785A65C7793E34BAC4F616F148CAD29A6851431C948C6DA3E42B3AB7BDC07A3B0C6A6496963B49AA5F1BE56D28E64D6639CB85A2540771671C0C021098A1A0489CB7C579B33106E2D815FFE6AAB3466742E63C20062857C8B1E770CFE537C1A2E20085D3BEA8FB986C856F3BB3A50CF874034556AA564752579457BB3DDA316A5CF044F2A9B3397BA543B4BF51BA2C640698160AB9D7298753DB22F2B16EE5B433C7D08845BA32F05006AC1C9899371D63A2779DE5803B3307082382EE805E00ADA6B2D0788CBCC3DBB9C003B38AF549B83ED6B119850E4D2B360A0230677A85894A882338781B6BA9AAE3023F5B401460BE97E2ABB95B862C54269161334D5A64557656141C421D497476D81E04ABC47AE1B42025B6B91607DD14277DF333F252AF596606C0777893142CC7BC685C4C931B37A0AFB37A633B2FF7A4B49FE55BD9E55F9FDCB22C45A6DA169A49D44DAEC44567C651FBC368336CAFB82467733A28E94A23E3E1AC6D901AAFC86809683365DA7396678B1FD27AF5B9C52AA7BD02171DB9E80FC0B04E502B723A5C0552947D3FC488181297DCD41D0C42A232034621A1655C36A6973C4D9AD47B60A78F3A680709E83C4D93CDBC96BCCFB06128E9358EB98B41119FE7D29C7F315C92A17B34D666AC5AB49DD65B6021653C558DBE0C79A65BBA23D683687325D7A004AC53B20FB6601D2606AC6872DDAC03CA86BC3DB47F19A828B8A35CE7293E3E1A3B269B4488E1A30B9D15152B039DF36221FB7184EA150EAC2E80F0C5255DFC4C9A542B9953E6272210BAEE60864394042F2E190EDAC61E3C72AC42214318C84151D6E12D56CAB2AB6EAE982DDA0008CF1BDC59B5C8AC88BA11F169798AA56374ED7A550BC4F552", + ), ]; for (d, z, ek, dk) in cases { let d: [u8; 32] = unhex(d).try_into().unwrap(); @@ -137,11 +255,36 @@ fn kat_mlkem768_keygen() { fn kat_mlkem768_encaps() { // (ek, m, c, k) let cases: &[(&str, &str, &str, &str)] = &[ - ("B649B9AD5A59AA45640B03ACE153499BC1244465735DCA6E5ED0C7116070287758E7A31EE53BA171E7C8964B3615075286A4AF1EA12479AB0218608692A2606A024D12FCAE691C8114828F3547C9D0344AF9920D952BA6BCE6AAE6A47360DA1588697F91AB5475C5588AD6328389A34BA50E41514343C534AD7947C5AA4220C73D335BB24F6676CC2549FD40759CD4B54549B04D8932921B183ECB634B579A54742DD6734C7225741BA32AC196AA68FAAF3D1425D4A44CC563AAF8816A8258BF745842F1CA7D8EDA9A7A6CCD72966ABAB9061EE21EF3D2B1155133F4B8099B653BA8B5224360CF00295F2B3887D1B12D601B18BD407B80D167AEFA0D3F6A906FC2CD08A663B7766815A26C6E2BC83318AC99B5A56D338EC347ADBD9A57EC53359CE898FB637B32FC4A6FC216BFA30EEC501681751BEE46C5C02317C3B3B98F24AC67ACC53941CD20035FE2A59890E9AB7CF063FE07A62703643E0580D99C152343C5BDD8CB9F9C1FD0C194EE7281913A7D1F0473722C024DF76568A731D309CD5FA87FB3A0C771AA42EFD160AF89752C1C3EEAC74A934B163AF92D4EE74C709A31E901045FE6202DE9622B552ACD807829F46AD9C47087E2856F294B97546103568292A4B7462895F161891AF4A66D537E79087F87F63E4E5A7D767A5D6A4A52267C8CE41413EF6C3DC4B1C64EE5AD75D9542099361EF81246A64AD997885FE0631D02919AB6B967B8C441D73B67D52B5FA64AC7789D30E659D776334DA3A65A3B4081014455BC858637B23A991FE8EC315C687C36D81553C79F159C2B4B285604C0541AB62749CBA6C29472B5DC6AB61B2BE2E6A57A1942E729C1E95BA95C8100D4554FCEDC0D73AC8023F736A94AC757B7B5108807A5EABA507B6F22E627EF325C0EF3B28123BE7882840B7A8EFCBA7E0D82434C330B37B7C7F546B123D460A0D0C58893A7E4664F49ACC9150A5DFBB71FBEF44374A987E3192BE4A50FC1F1160A0488844864532689E9F29D55366969E014B19869251977C34049437BB41B334C2DE7A2EB63CC3FF21B042AA0E6839469E4BFA226CDBF8331CD1640E04B4CF2A89BFFC20283DC2D90706604C1021153417B26C650B483856463DF2C2C064AB4A9F316C5BA02109B1023370DDED31AB1DA2EB837BD8CCC52106712EB91A019119BD60951B3662F3F6291ECB76561B253DC4A1CB8E41B3A16B2EC87A252C4B747448823902845527B31C15A3EF18E174B644F548FAF30B3DA5610EDCCB73E3A8714BBBDD668C14A9472718B34EFC545FFF2783F033F13FC1665BC324CA244F1E91851D8CE2DF2B388EA24B2CB8EAB400F5A8AC1D01442F765688393CE21C4C63113BA49480B247C3FB4D49DF82B1F493430BFA78F6D948DA4E927BDD9BD2D18A7F230046853BD8BE51CD59178D0295509213B7E1B0798584DCE835B48312F0257A185D9360E0A702AD8BB0A53C119336889974B8E52B636328556CA1A9EEC413F5259C66503C90206A7857925C727815C94FD545F0112C6A7E89C2EF54AE897A4B0792F98F5710CA174288658F5C8596C7807008369831135E1D50D5AC77F6AE9641DE0622BCA6A8E746700818C4A22A9AD30C9BC660117F3462617BAF392280DE09F5695B3CDDA5E931C5B521BDAA455C3D0F0F7375153A754ED9620DA68DD", "7D5201502FAD05B1463BC2212D6AEC1C8503204C491F12D9366AE750144B7831", "04F4A18C69708A17F561778B2AC10D94380ABEA4A20835939C9015D78DAC41A5012CED1BED948AED6C79193F8B2FC6DEABD3B092EC33AE2F54778F1C54CE762A69521764E20C05BC2EF96992F463CA95D09DD588AF622C297BBD8805113E985388FC9E16FDA06B5EED42DA629D514F86ED84ACFF0A09418E720201B794B49D072DF15E7B7D6EC6D82379A212C71C7603A1C9BBE57FB1CB9A431DE1980ECADA0A4FBF5CACE9AD0CEEDBFDC40761839D9CC1C8590EB6335179075892A8015E04ECADAD37FDCD4644EC2284CF4CBB4620FBAB6055A163E3733E3A7747044B766EBC356436B33E28FA4E67B083592B05811361445C719F6AE8ADD4EF8CE145E3933CEE75D19E98BB964D58044B6DE2B46107F80C3D4690114CC84FB0D3B3D4C3AF671EA7B833746B54FCE5CC761CA4FD20CD163AFA849E5797619C31144A74140ABE1C7540D1A3C557A9F23AF6E6E3523667FFD13B92444CD3BE01B1581CA0CF7A536CE4C073DC17DE955BA22E469BC1C0EC213B3B7CEDDFC47567A7ECFC2A58A6C2A3C2185563277866F8979BBB86AF844349C6021EB9926ACFE0188FD0F809E056A8E0A8AAA2A4208562E775EF60C56CADD6E26A9E52D60187BF6ED0565616020E0C2BFD79D961B1069FF261B2ABF40C9EE2A2C442877F4EDB8D9AD717CB434FED67EF2EACD629DA1CE78023548853EEAF7D998923DB7CEB0174E67875E787F398435DA84C26B478FF6BF785C4714BC6F8E91804E10CC699E1BE342C952D57D3C84654D603709F4F6BB596E022E2E6149C81025226B9925045FF365D83991F7D4C8693544CA7BA6DA60F8E4F6723C9F14AC48882556336ED88C20163544C55AB4238E510AA910B04F445252D507AF02AD24E7467920C81F2D31A71A7241BE2726BB9F8B20BF2100633F616A1233801EB37597DDBE2DEF36EF0727515E7DA178DA7760A41EDF9FFE98FBAA3495A35025F2BD100B3D63E940BA7D997104AC67F653D0A24A2BA2C8A355AF1EE048CB116B1A492577CC7CF61226FBBBABD9CBB043839585F2E00AE673EE6BECAAF5DA7919921C90C74D5B8B173B8A1A650F379B3B5E5F1D04538B936FC2CD0D4F8B9DF9F5052ECD9E66602815B4F96586D038D5BD5A3E44BDE1EF9FF9CFCB6B9AECE3129EF1F026BEFD299A7A8AD324149B156BC5AB868099DF52A2056103432879B495B0655FC1FE8073B502F3F40D403548B1629118CE0EDD41558E4215E8E241A45637A3434BF070F17DAC885ED656F80783A4C47000464FE78B9DB0DBB55895E271D3376BF0C50CEC9A403A8729982DC5B9172B5E80A0EF03FA2A24873188F8022A6F9DA8CA4F2E24AA7E29987B1060ECFE0B08E039EE1F7FB55A0CD35A73B6C25DC26E469BBC2D034265DB5F74E644842BB99199F83947C97BF87532B37A8D40A06F8BC5508EFB117D11DFB07325D9482CDCE60AA34529546D4C8D8F98E3F5B34B5C757075FEE9C3443E0A1109253F5F0A905C571E5343B277E0636A5A46AB36BECF5672E93B712B9BC8E3CD3656CAD1B29C16E", "11B62291B1A9D307C8240D70BE0B45436DB445793173F6E79FCD2B273D7F3B01"), - ("92C65B345762C2B52CEDBA2FA1482CAE931186922095B6CC2495A49A78B77A01B20C8904560696FEB829F0430C2351037D9338584C40637275B62B575F5809673639508CABD4210B676A8A9CD77FCE134FD593C4CCA8460D52CE326067B3C38A8C6C64BCEC23B5746A2965AE8E4622EC5569E9861EFA52A13A21C39683A29DE596BB40304493575D28CB2EB6109E6C1E66A3BABB733E7ED6B01F841A4D895AE084B34B100F0197CA53F774E9DB0B2F8C9F4BCC1921F26BD16544893B66846B3304C71B74F40BC998332A04BA33CBC6B291AA967B922660C855D124A5376CC8047EEE3542057B27B658A215CC990B3177EADC3D214078960BB9B28C132866BA167C07F5E876ABCCA36422B8FC0B781C59CA5D1C7DFE777503D7A6F9084E62519A5F59448C4123FC1096BFEC3D58E81D7E0BC675F106BC0514E86992E3490A0525317EE4C784F96DFAAB8A430A9CDF246B92A36785F6094E2509FCD2A7A4022638B37177468549A04869787E946401DC5CA4DE968796908EFA8B67B9D45BB8A1A00E921E9CBA86C14A96E79C808DD65F04184781C46AE6FC7B1AA159EB55563B72184D12AB81590C86FC914E58C9AE186D63E780ED560A1866AB84591F0557867286350C3BCD4AFAC26799B68EBC60122C85CC9A26A9059FBA3101A0E8263ABB12FA93C5B0B63DF31B563C3086C8BB5C91989B96C3C933B82F529A8E4A02713F4B08F12ACCEBD343CAE17FDB2902C58A595CF2695610BE6208839B719F69257AD42805E892389EF9C76613B1074830B87217C6866E9C943E7CABAC44C88D5520CDFD3A594CA3374B603D2B1173F8D114D742047819C54FD411FFA93B08C482D6933D3EEA6D861C258BD0B4059A6B5FC338FBB003813744896C61C0D825E69B4FB7D56CE2563F6A120ED13CB1CC0BA776415EBF17BF697A95E04B5EEB8A316DF170F59770E4FB20A7221891F872AFD2424794B60F033FC6BA022101B162D6034BF01ADA53B4A7B1268238AE0E65C0D166022126138C3093126C87404A0FC6E20D901C33DC36110B64095F0A7109B2CC08190006E712D6F8B9F6D6AEA3437E7ECC7D2B303FD137A357EAB63CD1B87B0A73E36950E33AA75C3714F98990B62AA41DBB992DCB406D8552AFC687FE220C48455814941D41C675A0D9C3C88068537BBAA4A208555A2319F365D7D8CC87E42A1F488564F84727898963380338F30C4E783DEB161026B04CE1856EEB04CAB63158BD037A6C124522E91FD1F233A5A008EF39ADDD92C15D545D07F532F762A42C4C528CC59F650B305BB1042DA369FD70A226145CDEB389DA185972636F04974E704CA9D28CC240F3B1BFD03157D88018D41B352788A14C9CE9D33F20416B0E7A7A1055CC344397974BBE611A788585AA4126BE3BB461D7403B85F60599796F8AA928A28235F3305DA4315CB50441BA596DCDE15BD09309EC11823D377F1B0A4C8514496240BD7CC04B5A919EB4C0AB9EC1B9EF79C849450D6FD5B6EE188BBEA35C52289C81701542991CB2529C4448269576B3456A0AAB95227663388FEACC63B346A84742D8257428912B35E5A98A98786F04B0B8D72EE901B1F86C0F7135B6C2865174CABA64D76070191D6EA43194A7BD87C395741E7C40A35A57A87C97C2FACAFD6BC804FEB5D5C57EC896AF08B81CCE", "BD8D55A47DF20F6B636E06B0ABA5F258B6E7712074B6FFFCE9153157D8B849B8", "9DF0613102915C62D693A1FA216A588267D923E3B261BC37CC0D6910D3A87C525FEF2CDD80D5A2CDD55D2A30348AEAB46881C29803EAB9F8B53C85D8768FB390FBAA94236181467BE4B0DA9F51568E027AF0BA33E4F290A80631DBE2B102FE4D64B1E87AA5CA7EAEEE0F414D685C06C0754353D94AA7CB8FBE3ADF07E5042CB2E49EB24BF2B1F22B69E97C4D455B8A26F4718E5ABBEEE68B09FFB784FEF4D7AF2E62E835DE09548702B22CD041CF216D19380086196666AA9362D450C9A0DA6ADC97F09A2202157EF5D113CEAB30D261D852A46C531AEFE8F95D05AA6CF8CF87D0FEB374EA17394D7DD1D37D1D6B916B6DF2BA31072F28E8539AB9CD48DB64A14C5E1CE9F5B067C517A8B38C7D44B4DA8CA9429639E0076969F9F24979A1D2B83192E48F3B143DAD0E528ACF1B9AF2386626B51C7936F8F174964BC0913FAB829ADD7AA5B63D47A73B01BA06381FA61710897730516A522EF1DA17A6E21FCF7F6C8135DA057899BB1046C447A91A3F34D5844B7FBD9699C016F10B00BF71B052B88C432F98CA943AFF767A45F18D1476AD989C7F98393364359B8089B4B2949F76E8C93659930F837D1DD3E68C12564F58347AD5D0542A46B974816CC0CADF85B235BC786ED41F4AF242E1C21017144286DE483B287910E242B5991BA1F936C6B51B0673E77D436C77689C0970608A5C7E58AF1AB6AEAD4F93E3FDDA58527BB3C2535E20FB4FA5094639302B8D178525891D7EC052C7D1CEEFB91A40146234FB77633C2D94F463D1956ACD2CB1C77492992E83FABC9B1EB6728367CC64EE96D45475A5209C88EE6F13E0AACE0FCBE503B5948E80859F4520BA238BA8951A992A2CD1100AB53D24C6E702B8808826E206DF141BFBEF0791AFBD7DAF48A2C49A4C3FACF2C125771D1DBFEA06E95D0EB50790994A5E0B51267EE7245D5A6F4F5A140CA7DD6DD44B90B1D1C1B37FBA776089B24D5C466C45A0E10710BB82EE6376F9AE7FE77F51331FCF82F8D442BAE0344EEE2AFF62D4D662A4B00F3492ECD515421A8F49A9504F08D83B09AB45523DE37A9913B48A619ED314C21D5A2BCD7F09E584E7425422D21E72459EC09A6C349D5642C6812845B0D92E473A45FF1155F941E8A544EBC1025BDB671E7C243EB25B76BBFB87118470C12D0C65FA114F145BD7FB5CE2A524F636B2FD3C8B2B7B19A1C2A0E497461623E777052BB8E2367D702EF15448D89B28E07C7B636D51E94A4F714781179275E109167C162572328A2654A8C6D1541B45B291D13FB781F7B3FB3D43DC31CCCBFC1037DCA1D8C0DD3856B3FEC7A38484123E5D93584F4B0C116BCB173B1E2F060E04AB26E4A5F19561A51C7898A68A7BF4137A261279885D81E13EF6937EBF345AF5D301AC1422A4810A883D008C7AEFD7BF0447A0E987818AA4C17B3F57B5F93E22BF2B5A309DA5B8AF0F9BF841E099CDC097720DFD5928B5F2E77F525D66896530FBCA3264B795FE2D1A3FA2593A83D23C760F80C1A4952C78E1B1DAFB3CF5E39A0DBA17699C34363CF7", "D281A3833118CBBB15EF35F846F9358576F030E49527BACDA4FADA9FA2733D41"), - ("69043B0269B4D596BEB41353D51C29DD687E6FF788989107ECB3C99F931AADA6AF8018A6F8A138510960B7419D82014BA6E58BFCCB2E386A278623C9891B4A7BC5B062E8CD40482ACEA10123C85A9E188C70E00FCCBA9DFD485E122C9B4E0A1537A5421AC134E887125AD9B049BB3484227FA0173C9E4B54CE946780ECAD4ED4055AB23EA1CCBF80D11CE0AC85C053B654E629C69B9A935B6AAB2303B47259FB237EEFBA8BDB9694AA521F068A61A1591ECB09439D13908882CC82E42A45BC558F1C5DB7C46262C5515378082BF68E624055029A818FB8B4CC9AB6E41CC8C0F3C66B0713B7674037C8B781214D5C89A4E8271159E08DE2DC0A367817C4E7C8DA3CB69D573F7964B4D7F04602E7382DD12D674BB4CB65209B35574E651FA0B24A0ABB3C64B899D5DB9C663B1D9D282D70D35A38F50FAC111CFA399592DCA8D8F8B7B1D1BE85218E3C07395DC162A85AAB9D267E12C191558849F9188E56357CD8374B8ED20DABC79EAE8512FCC005E15A4262C06D6DFC216B753E4626A3C59538C58B06ED98897677508CF798FE3A94AF19A583263CFCC24302CCBF917ABDE0003C20A39D8F42CACE486200DD7B1F346576690305687FFD877BD6A089C5029F597B278F173A3B41B1DBE63FA7B405ACD0856772A1C902B7E35683FDF64C6CE190677B531C077564D22082C82CB46B77FA33B276E841367600290B7E8CB32B8D1A1DBB450111EB8FD8916C4286A56F7214D2B0B3A89377509C817D4632CC085326301BD13203BFA889D006B7402C866035B01728A2D57C7A47F60512F25E63D4A9E3739A4061CCCBA9250EA8A53C4B3AE52AAF2175C7AAF365C13423FF586A0E3ACE217B08330625A9B1A2402CC0D8B714D380A2BC4B0E5C80AF03FC94CD7030E94719F62A5045DACE70030D3E1598F59AC08B6696D2932E5FF00CB8964916DCC81BE7A0656B8E30837A030672294A298BF58B2AC79B9C56738CB6A63D3117C0CC60E68C6AFAB0765F483EA3B30128970EC172139CABBAE9F5A1C57B0DEA44AB0E40821B18A35CC41374449A364BCB478736BB9A32ADAC543DE71085DAC2FFC6A5807B2D80B2495B3A1122318144E89DE37A893CC24F1204CC22778C86EA58440165487C8A7CD660C11C1CB71BAE86D51BD8BC42C4B811891B59E7F8C4375830155B6F8F558875DA21EF831365440369EC1FAA2294E553CE79655F35EC11058609D21CA8D034C4E4365395E07AD6436BDB4C879A38BC8AC49712B642AF4263B5999A698C062C2A11AF7B1FB447AB0F6810819A4642289AA9871ADEDC425136C52FD8B45A64440D3A57E3439A5D424B7A802E62802A89B9AAAFC027C2C11BCE84B472F084CFD50FE04388D8D559AC6B89A0860EA70198D4360BC7AC175B03CAE9B79D17C09F19914468849F85864A2C2518E6108FE3981B20476A60C10ADF654F616710FAE03B03D8B4DB169B06DBAC17AA033A270CFE3BBD84353B28AB4249A3CDDB81CEE5EC003E08CF392435949524057C4DDFDB67B5E9788835C749C0AE7238CDFB06BD78C51FE3A8B6CDBA141511464AA3A272C1785A0CC2CF169A1FC1830A88BB052B14FD1B51E1320CF1290096B30C2CAC2AF4820CC1D6CB0F117BE6086D4CDA01DCD595F2E5F779198FBB43B083CAF0DEBC36BC734BC4316829F3E2BED7", "035B33594F2029ECB3845CB7BC2059B8A77A660BF37DCD55C97AFE708000DA40", "476107D63A7498AE87E1557724AB397270FA8938B0BAB7E7D78C1ECE337A6D087EF862B7E1A104790F2B5120FD9FD71DF27165680FEFB1A2EB24351EA1327AE9D582F8D120C8DACDBD4DF49C2940EBD40FBA5177F8854055883CB9CFBDF18C1E17C56083FE4166112CD6486BE91A5DD889820F5F677CA850AC45CF1B627A0C6C23547CE85C10E80EC5DE319D483E78305DEB1DD115E38453EEDD78FC5B5018B9042712F52A14F6348959C7B4EF961F6F6789ADA6E7BDE59060636DCD9D590B71490B23083FDD68BADA5F50CBDA437691CF882D3151311274B377FEEACBC47447AF40C1BA96487D72357B22ABAEADD9F4C32F74FD4E82C862CFEA8B5D76B511A56AE68895F43E743C87E7BCDCE38ECC68B12C366B2FD47E5A3630F45ABB9994DB880009DC2292803374446C7B09C45A9259EA13A702844FBFBAB943B3BB114DF7A4D6F8D9F07001EB0EDCD80B8F52FDF60B01729AB12287E01CBFAB3E43B7A69B98BBD0F9D21DF4FB963D6D3C81EED366D47BD761A3239953378761EC3A2CB01A8856B4C57E74082819AEA218F2D7D5FF96E3A80E3179A86716F7174EE6EBC686082A281382A237298BC0013FA685B7B48BC975EE530575BC89090E1858FBD0E1F129392B3038A2C4599AF3A80F09BC8B903F8A7AB217265E240F1314F10FD355E4781EB1EB4DE3B81DC0ABFA62CD008E189864D6CF8B0A92B629B8821DD0812E51B6CBEAC913943953CF6CD2B59E94760395616DF7961BB3D3FAE4A36C517694140F029275D22AA4F72C945B6B001CC71FC77C3A2EDA5CF2934D00C935A33E209018302AA91D5B12905403D358DCD6B57D47F544F6807F1BCAE018934EC7B2C093B202E401D7D8D87B2B42A1D18625FD2D6B9FB9AF92586FAD87006D5CAD37B30985FDF812E489E215815E40A8BC87FDEFA6A11935550765AD39D07122444E63134F6C07D0CDDD6AC854048D907F43A272F88A5C5D93619FB03F0A95D011FE0CEA4A73F2F4AF30B2565DFE5AB0C48FE551587F52C37E9C296556E23CA78EC73B394ED7831A6692A709021ED1CAD78F1556F1D9FFDC18A1D2625D9C9CBCA10896045CEC29A2F1DC6AF2E80690A9EDA18FED5EA3558C0E91A6DC97CEB82FA56C90AADE516A170009F471092630611183221F1B3A770BAED65EC50439B1B447F514DB2042FB6FD7FE2B8FC6C272DC8EE0E00C7A1BBD6AD035295D5F0AAE2F4A09C3F75A0442E48EE826C3611FFF33F1429F9FCDF4F61367F3E95328E8A108A83C8B47420172E8AF8B7D14A5606349C66C9E0D61F9F49D82C334CA43269D07B9E73D8612D158F038D8F824E1C4E51FD5D894E12452C5F940AFE91D90F65591AF8DD161D2E40DEE1FE5DD32E01972D6532E31859127D12084F32077C613AAF1D71060ACA8ADDFF1E56819EB001CCE222F0A2966F9A836135010985353029FB3A638035DCB9567984ED7ADBE7938CC72BFFBC4682230D566804ED1DB47A94C6095848AE219AD2E14600315B333317A7A269906CD5AC4B69FAED011EEA9B2B525A0FDCB", "61338EC538AE04644A7A2FBFCB49C968F68CB1B634DCA5626A9BCA03C4E30F89"), - ("32831A9AD55DDF56618550063F779D5FC0519D1369E50298BFC0413EA042B83891593A5F388700324C1481AA6EBD970264D66A9B244044955BBA2B41BC168C03D28354BA904E68ABCB911F60F189D1C77EE0845E243A9BC87590EAA53BFB783A1DB202BDB663F9787DA7939CE404295DC5710B66C4941B06B18569E4471FD265CA1A50B82EACCC55D4B16870BC949AA9000ABD627B07B19BC039B2C28707081D9B4BCD4C0B398631E835697B0C65B05068D6977C4EA011FF570C983A169A15397B45146FCA5E73D011D4D959BC8A074379C26D182E05A30764D3A7C5094482584D320A0CA885AB0AC41DCB430C01BC94EDF4186788A23E4434602C928B24490B3957D61C48B469C50829B81ED5031209A2A0533A41D878A3CC0690C2204ED30F1C90C2F3F36A78010EFF3C4CD4D76B950B13FDE25A4273313706BEF6165B0DD8B669901D0EDC0540712DB9E9AD34222077564FDE9AB58C225063187E2A472B7EA07713369E17276E6B27552BE0BA65F06B575538B69780B9F2A2D528AD6B94AF9A35AF05D8053D40A129F13CE1EC80C39A51E3DBA1A59C5B803449A3DB0351D3824D30336EE19FB5733B1CC5668E290591DB86729683DA56A223744540B7C50FB3983CF51F5B7012114C050D962A26052F0BF411104566366438FFC8833F36BC26D4ADB2438024755B3015207446286E7B638B8169D83A222C953B5832661F1BA6BDF34B95D383B3930F1997AF29C7A9D28080BB390370AB84596B47551745074567803A497E834D1D8B6136F1B2BCD57D5D9A203A09845B3B3DDD76A097F744F3A91D63451D578BB186BBA6B159BA3042767FC3B26CF211FC0539B29520CC748AB99019FFE4A1BD1886EA00A30C319B0D71C2F967669AEC4AF0DBABBCD23ADB96AB5A916291092CBFE2B36D9616EB25CCE4C676BA65A38AC29EEAA57C1D988A442BC4B0F287368247EC522A1D29BA49F798439C525EAA53B23872430487A1635E226095EC5B3C47F6611ACBA06EA8791CF437AFFA7BB51898817B7E6CEA61BD675C3068356203426C42712DCB9AB3E53F4CB6CF74EC1EA24A24E54B046347A03878BE4228559FE49164E857D7866F084A44502334DF0316DED139B8C45A279C6F748575E2ABB4B0A9AE34DB033046755C1A3EA8A493EF0560B14623C3C92BD03BA930618650486FA7920BB59CBCCFBAC846184B5050818E90902FD101CDCC9C37722E723733A7B348A99C6F1917412F57222490526FCC825BA2A7408A387B31B4215BA803099EF4524BFB141434BA5E069B973105A84AF4C12BA09FB462380F2688B12222DFD77575366F66D042C4E54C70283B70503B24253D2D975784795798B7887C025238E1B0C27C620C323E527B330315CEC7A7CB8818BEC429342CB5C6EC93802C5855359B2C1FD48A0E761B8608A5E8EB55A667C009CCAB28E2CC23B75C97D7BE655B87EB0A8FE5B420591691EA4754D49608414CCC796431E8A59A39C76125A393BE461D7CA2B56DC46E244A8A21205DFDF037219CC26E4234FFE496C92965727A6FC9D660413369AB27797C4B1F88D2578B54721F64CFB574591B40CEEAA5A671CB7ED2AA25A5D733A9472CCB135C8C21ADF968B97C159CCDF78A91567B4CFB7FEB732B6D2CE28924865D4DAE0F034628D662D6CDDAEDD117862D", "BF274241021405CD6CFA844F1C0A24F6EA3A5E6E15040902653E0A17EAAE52DF", "B1F97544D04EDD67B7E1B8FC038F2F36334C9496F5F2A775C098671AF26FE27D79D1FD645EA7968E6FC353BF4FC5944D7D836591342F62C5663403EABF1509C65633D99F3F58B317C3C67500D8695337CB83FF2795863141F5E0393239CBD37C82A6187077EF3AAAC3E81379CEFF229B60128ADBE4C282B8AAFB1447E8B46463CB4CBB081BC4F99EA872A77526E1CF0F7BF492FEE41CEC46446A2D9D7E6179BEE633D6494ACA09D14E7960FEAEF309C907DA4B275503CA8C3DC2E727F071AC5C2A089891E798AEAC3B8E984FE1C03378C63FA1F37A75C7595E5CFA2BEB45F110E3B355AB6644237B86A7F061CAED6DE08647FC0BF70774CD470CAACC5F44E824ED6EE584EFFB94DE9B9A555391006B4766FA64CE93D65CB244A681CEFAB982935110DCC3768E11DD59C955A4E2F45930CAE5034F5D5A5D5DD37C43509E0E5FBBB616953D5309992A9F0EACD1D4C570E5EE8324531EF17500675325112F3ADAC7C901396590CAA0B35F6BE0E9FF008F57995EE7727064D2DF1E1D450E6134781480EB526059D2C37767C7696E484C4208ED3AC4880B34447DE2B577D20A72E0F59CCC29A99FC139B3978DA97F924F1FC6040598417A70DFB8FB528E62669A363506E5CDD037950286AAC1635EC04627026AE97D70759CE62F9811C1663EE39E5FB148AEF6074A1FBD2193ECD4D06421BD0B7CCA0C024BEB1F8441F583E21D638F106B8DEE2E85268B366807E3FC0A23B530C881DD54FEC2A5429D89F9B702A4B248ABE596F021EC8AF984F2F074C994D5C6947527DE0863F51A972303A82D59A88E0FE79A2F284EF3F40DDF55479762A66050FA4A4A40BD51F1526C7BEC652DB46E4B0D6921EF52442C3654F83005CDF394E1E55F8F1EEDB47371C3C4B2F807586475BEE0BFB95E68CD760ECE689CAAFCDB3441536D8A6AA3C7EA80B6DFCFA2D84AFBF187246F32E808F7171CDFC52284B81857C9191B16E01F8C1706CD4C7C647A08D71E732E55FF645AA013DDA9E67E7EB9830ECB5DC244B4850B7329DFDB2B8D97609DC751D9262512E167CFDA29590B7403DD94D3FB49E08A9EED4586E9EBDF5365971375AEF61DF87B10DFA64AC35BF356A1E1C990EA12453378ACE2ACD7DEB1DD2685426557D1F41A022F1F8BD64FE84939315AC081132331C878D3D0DAE2D00B1B3B751822959D473942553742E61F8C618F8AB5067D8A49C836C4EA75356111FE3D1A781E8CE83676557F546B906EA4C40E7908B4473B2620D59C06D23BFF494315F8DC3DC41BC65DDB141CA2EDA86F843358AE92C44197B6F7FD57C7C936B19FC1D96BB12878CDBE9D105BF53595649B84BE94A3B792F1474372EC57A2FBDF36912E61DC1869AC70776EFAC0A00E275C2D4B52345C11CD9A9D4604DBEFAE168513966473A7DFFBCA0479211FA5F5A8B28D0F6140E4353807C6AE43A809AABB62F4896BC0B9E33339C3A1F1A23E65D28030D9107022FE870FAF0C39BE4288C72AD67008DE94C2EC7F502FFF33B9255BA3574DD61863464E44AAD5BBD2", "CCD2596065F728BFF32792DF57BF133F8F50BC8DAB822640CCB6976BFE693E0F"), - ("DBD23E1744CAFFB0984F97A697E7760ACCB421D07EB7E25BB1C91B3E8473E2727600855A038419765C8E86A025794290CE095717D5BEDDB6364A8BCEC06617F66638C7658651067169E3CF69607977E6378039212EDBCFC7926D7C9B50C960758C85A21C743AF6DB5351198636C0072012913B641B6E53AB0594522E5185D0060B595A6F3C628BBF7614C1D88A1D7788C6E994A0C29FEB14A4A0121E155B976035A0A1A121E4D41C1FDAC830837E9071187622C3A128650D91831AD1CB6B660C2A5669CE38C99BC1C546D706F8DB82058C7A5C666F54F5BEC8B1142818686947B511F2132B72815E296BEA04546C9A1EE9251768281436114E4980095D8A25EF048DB69A661CAB717CC6BD3F436A77A19FC1D146CC77AC5FCA3C3EE32D711143D6E8642FF6247D5BCB2BBA511C57415F2C43DA50BFBFB6116E970B2CF589BF2805E42A45D4B60B1B9344E7E3CA77E88B0BB95DAAF96F388C2E410258C61265E2C636684586C40332D318781EA27A5438B9B336AED64B1785DAA44B26092B2A53316387B007BE08019FBDA577687BCA204BB29D63CFEF1255F55A713A120025E94B780137DF5B1148861CE3B86853E61EDC279CE59373DEA5C96B45A404342821228EF51166FF639184024A65F2BBC50C629EF69EE9E8C4149C478683051DE7A9E478908310463BFB72ADC44F91941C1A0C3C945C0A60D08E0E4714A4BC454CF7AE1A888A835CC89D540B83771EF8214A32E0A2B97B8CD971C8FD2188BF311A9CAB1A36142AB73436B68AA22F5A5B7E55138835512FB51319C94CF094893522921615343102C0D1F4C9A5F52AC913760177C61BA467669ABE22136C25EC352EC8025AA6522B47BC4B4326A3279144E53D0B2012ABE8C298CB7A6C9B4AB17B069DCC3A086CC60886CFD05286D33373C3D8A50B302A5561033971CC31D9CE56254BA3C32146F815FFA0C2A5005E78B13A04FC99A0176CD132C750FC7FECF67857A047E174850723B33B1A2984C559C2D8A0E60C55B2D427EF67C0E2396B3B854C86CB09FDBB78FC234FE84C89592520D66B3AB2D1291D5433380576AB74AD74170D805B10CCCC0114F714ECA9B63300C445D82B94051D3CE075F2FBB8F7351EF454B08998664ED14C834789984C939473906582106DDB2BAC7BB2696531FCE5CD580B9DF56B82044029B2895F0F41936B91916455019749886F03886C8A9E2DA147EF5073BD96BFAECB92729A71F2F18A2EF694CAF5674F6736E0F94CFA0B2B60695D64D75A85B14DE6DC7782A351BFF2CC0EC47BEC3374A89C1BF5DCBBDFF782FC0AC98387AB61E7387DC2BCDD92816C13C011E8263AD052A0136FCA88C58AC419F16B12E381A8873044B83C2FEC68BFF82B8499C47621B3C0E6969ACB17658D36772422880AA3485F621933E407381C712FEA2280297507194625EAC3F30014A4A516D6BB8593379BA62733F43203AC23985C17925D428EB84AC3AACB7B80B19B3EE84600078712F72E9D0043FDBCAA949B4AEEF5A8B6C5096823049434ACA7510A1BF9A50ED0C9EBA64A9D53C88079C87174929DF89AB75732F0AB3C138148EB8C5FF92C88E17678B701344A3021428A842C828C28A6C5C15C852D3C4368DC0D6AD8545D45F0A8BC16BBBB45291B092D25F44016462DC96C9593D007794BEDE1", "101F8466E34BA303AB8032030B9ADDAA6DF2F1A7D212BF807426770511350948", "7839E8067401971FC39BFB619EF8935446FB735368689CAB95605771EFC5D30402C6DF666675C0D8CB8178013A70E00EFE321E46DBB4A2BD09056A0219CC8DC1526D6D322750C4AE257AB875C5E1109559B4ACFA5D59E48500C2FD5CA714B08E1967EFDF90C9944F02E27AF389C79CE0C8EAA166CB7A1846A1DB0479EB4CBEC2BC9ACB77CECD42C05540A5290B7DE2E3DA121665CA195767258F76517EDF5D3E30E0D7229A91B2AD47645CD31D50CA1543192A0782F56E4378E6EF29C5B12F73DB0B8A9C2DF495206D8D28C8D0964789480E20EF6E70BC959EC6DA2E7B958AD1EC402090AD8595E8A6F642FAD9C6EE4B9C59F6A4E4EEFA0F205C5B519729D9AEE915C0E3803ECAFBBD031910F3FD1644BFD54ED10B230561259305C7FFF2B7DA35FA6BB19424DECCF2DA47CCB947FA6ECB10CA573F8B3271EA0B635E5FD25C5BF57452285141D28B02885FCBA1F949F8ABFECFD548FFE12690BA37EA43EA7209960F6F25496076A74D195256B5BBA7E62DBC51AAFD5928421B21102F41A9422181D7924731AA385B2DC3741D4BFE6DE4CA5A91057E1F0369742270ABAAE4BA6E5AE28CE200FEFDD06ADC2B2238B2D0973F733B8FA10C62DF6F0429BDA45D83485B528AA5B2C16464B896CEC7DFAD1534B31185FBA569A6D86EB309467A32A496AB93B84A62F781CCE7FBB4A44A1EC06B7F241D42BAD37222619A12F993097B027291D332743E6B4B6CFC940967BD0BC394C3CE2CEF5165706A65E143DDD97B87F1682DEBDFE87E8E73FE684AD203822A9AE02224640CD5801590E24785E43EAFA9C144939EEAE143AF90B2A8720613D16C3103061E14C0D15CC85A45223B9BD013C7D3E0D1902A2CDE44E09F26110421E27AB5654989C2AF4C4980EC3C2A7D9D2DD5B90C90A32DA27D9FBB3EA58F17D321F935BBCC18A4F058513B019FE0279C5F0904204B83E0079AD17B99C37AD4202648F44AA9A64F9F9E57D90191BA861D7089058DBD5FF0CA9C4710E0498DA97422881AAEFA6C32FB20A84D5EADABAEE18BFD1A9D5307513938C2173BFD7C9BC057541B85C313423E0128F49F46EA1D48B0B61EC4793193E18814DC0BFCAE063E61919BBCB4E87E1AFFEE4D07AC2EA68ADF9D05FA7B11257C2B84227B20284B840178C14FB7285E8D54B60C2659545AF4BE8BA71A0866961D2DC36E580DCFDA824846AC2AE540CD99C9B351C4C6387A9C11515E0C91408846464400220876642059E9EE7620095FC440A6AD7B1E8E97CCDBE5BA102913F392477C0C0A2BE410BACC9B5252FCC0E17A18A363A05BD523131BB952E464A7595E8257D2472E92C9198BE488834076596FD96839A3E14991377A4745B3FA4ECBCC4092C2EB9710D2C2CF34581B4A680DAE5929D0967D4F1EC62EC578B324D41EA3E9078B63DC0E128D8779B8F4091593FC31E47D16AB4FAFF6AFEE850CAAD9F48E2A5DD9C8B78AF04A2E56D328A54B9FA769369802F56D5765368C435800B09A52C1483E3A44DBDFA22965E5BA1AFFBB4C2DB139497B43A1CC", "966CF8E6E799440C243BEDB9AA9305BC6261A48A4A046B37B80EE93BAA24FE4C"), + ( + "B649B9AD5A59AA45640B03ACE153499BC1244465735DCA6E5ED0C7116070287758E7A31EE53BA171E7C8964B3615075286A4AF1EA12479AB0218608692A2606A024D12FCAE691C8114828F3547C9D0344AF9920D952BA6BCE6AAE6A47360DA1588697F91AB5475C5588AD6328389A34BA50E41514343C534AD7947C5AA4220C73D335BB24F6676CC2549FD40759CD4B54549B04D8932921B183ECB634B579A54742DD6734C7225741BA32AC196AA68FAAF3D1425D4A44CC563AAF8816A8258BF745842F1CA7D8EDA9A7A6CCD72966ABAB9061EE21EF3D2B1155133F4B8099B653BA8B5224360CF00295F2B3887D1B12D601B18BD407B80D167AEFA0D3F6A906FC2CD08A663B7766815A26C6E2BC83318AC99B5A56D338EC347ADBD9A57EC53359CE898FB637B32FC4A6FC216BFA30EEC501681751BEE46C5C02317C3B3B98F24AC67ACC53941CD20035FE2A59890E9AB7CF063FE07A62703643E0580D99C152343C5BDD8CB9F9C1FD0C194EE7281913A7D1F0473722C024DF76568A731D309CD5FA87FB3A0C771AA42EFD160AF89752C1C3EEAC74A934B163AF92D4EE74C709A31E901045FE6202DE9622B552ACD807829F46AD9C47087E2856F294B97546103568292A4B7462895F161891AF4A66D537E79087F87F63E4E5A7D767A5D6A4A52267C8CE41413EF6C3DC4B1C64EE5AD75D9542099361EF81246A64AD997885FE0631D02919AB6B967B8C441D73B67D52B5FA64AC7789D30E659D776334DA3A65A3B4081014455BC858637B23A991FE8EC315C687C36D81553C79F159C2B4B285604C0541AB62749CBA6C29472B5DC6AB61B2BE2E6A57A1942E729C1E95BA95C8100D4554FCEDC0D73AC8023F736A94AC757B7B5108807A5EABA507B6F22E627EF325C0EF3B28123BE7882840B7A8EFCBA7E0D82434C330B37B7C7F546B123D460A0D0C58893A7E4664F49ACC9150A5DFBB71FBEF44374A987E3192BE4A50FC1F1160A0488844864532689E9F29D55366969E014B19869251977C34049437BB41B334C2DE7A2EB63CC3FF21B042AA0E6839469E4BFA226CDBF8331CD1640E04B4CF2A89BFFC20283DC2D90706604C1021153417B26C650B483856463DF2C2C064AB4A9F316C5BA02109B1023370DDED31AB1DA2EB837BD8CCC52106712EB91A019119BD60951B3662F3F6291ECB76561B253DC4A1CB8E41B3A16B2EC87A252C4B747448823902845527B31C15A3EF18E174B644F548FAF30B3DA5610EDCCB73E3A8714BBBDD668C14A9472718B34EFC545FFF2783F033F13FC1665BC324CA244F1E91851D8CE2DF2B388EA24B2CB8EAB400F5A8AC1D01442F765688393CE21C4C63113BA49480B247C3FB4D49DF82B1F493430BFA78F6D948DA4E927BDD9BD2D18A7F230046853BD8BE51CD59178D0295509213B7E1B0798584DCE835B48312F0257A185D9360E0A702AD8BB0A53C119336889974B8E52B636328556CA1A9EEC413F5259C66503C90206A7857925C727815C94FD545F0112C6A7E89C2EF54AE897A4B0792F98F5710CA174288658F5C8596C7807008369831135E1D50D5AC77F6AE9641DE0622BCA6A8E746700818C4A22A9AD30C9BC660117F3462617BAF392280DE09F5695B3CDDA5E931C5B521BDAA455C3D0F0F7375153A754ED9620DA68DD", + "7D5201502FAD05B1463BC2212D6AEC1C8503204C491F12D9366AE750144B7831", + "04F4A18C69708A17F561778B2AC10D94380ABEA4A20835939C9015D78DAC41A5012CED1BED948AED6C79193F8B2FC6DEABD3B092EC33AE2F54778F1C54CE762A69521764E20C05BC2EF96992F463CA95D09DD588AF622C297BBD8805113E985388FC9E16FDA06B5EED42DA629D514F86ED84ACFF0A09418E720201B794B49D072DF15E7B7D6EC6D82379A212C71C7603A1C9BBE57FB1CB9A431DE1980ECADA0A4FBF5CACE9AD0CEEDBFDC40761839D9CC1C8590EB6335179075892A8015E04ECADAD37FDCD4644EC2284CF4CBB4620FBAB6055A163E3733E3A7747044B766EBC356436B33E28FA4E67B083592B05811361445C719F6AE8ADD4EF8CE145E3933CEE75D19E98BB964D58044B6DE2B46107F80C3D4690114CC84FB0D3B3D4C3AF671EA7B833746B54FCE5CC761CA4FD20CD163AFA849E5797619C31144A74140ABE1C7540D1A3C557A9F23AF6E6E3523667FFD13B92444CD3BE01B1581CA0CF7A536CE4C073DC17DE955BA22E469BC1C0EC213B3B7CEDDFC47567A7ECFC2A58A6C2A3C2185563277866F8979BBB86AF844349C6021EB9926ACFE0188FD0F809E056A8E0A8AAA2A4208562E775EF60C56CADD6E26A9E52D60187BF6ED0565616020E0C2BFD79D961B1069FF261B2ABF40C9EE2A2C442877F4EDB8D9AD717CB434FED67EF2EACD629DA1CE78023548853EEAF7D998923DB7CEB0174E67875E787F398435DA84C26B478FF6BF785C4714BC6F8E91804E10CC699E1BE342C952D57D3C84654D603709F4F6BB596E022E2E6149C81025226B9925045FF365D83991F7D4C8693544CA7BA6DA60F8E4F6723C9F14AC48882556336ED88C20163544C55AB4238E510AA910B04F445252D507AF02AD24E7467920C81F2D31A71A7241BE2726BB9F8B20BF2100633F616A1233801EB37597DDBE2DEF36EF0727515E7DA178DA7760A41EDF9FFE98FBAA3495A35025F2BD100B3D63E940BA7D997104AC67F653D0A24A2BA2C8A355AF1EE048CB116B1A492577CC7CF61226FBBBABD9CBB043839585F2E00AE673EE6BECAAF5DA7919921C90C74D5B8B173B8A1A650F379B3B5E5F1D04538B936FC2CD0D4F8B9DF9F5052ECD9E66602815B4F96586D038D5BD5A3E44BDE1EF9FF9CFCB6B9AECE3129EF1F026BEFD299A7A8AD324149B156BC5AB868099DF52A2056103432879B495B0655FC1FE8073B502F3F40D403548B1629118CE0EDD41558E4215E8E241A45637A3434BF070F17DAC885ED656F80783A4C47000464FE78B9DB0DBB55895E271D3376BF0C50CEC9A403A8729982DC5B9172B5E80A0EF03FA2A24873188F8022A6F9DA8CA4F2E24AA7E29987B1060ECFE0B08E039EE1F7FB55A0CD35A73B6C25DC26E469BBC2D034265DB5F74E644842BB99199F83947C97BF87532B37A8D40A06F8BC5508EFB117D11DFB07325D9482CDCE60AA34529546D4C8D8F98E3F5B34B5C757075FEE9C3443E0A1109253F5F0A905C571E5343B277E0636A5A46AB36BECF5672E93B712B9BC8E3CD3656CAD1B29C16E", + "11B62291B1A9D307C8240D70BE0B45436DB445793173F6E79FCD2B273D7F3B01", + ), + ( + "92C65B345762C2B52CEDBA2FA1482CAE931186922095B6CC2495A49A78B77A01B20C8904560696FEB829F0430C2351037D9338584C40637275B62B575F5809673639508CABD4210B676A8A9CD77FCE134FD593C4CCA8460D52CE326067B3C38A8C6C64BCEC23B5746A2965AE8E4622EC5569E9861EFA52A13A21C39683A29DE596BB40304493575D28CB2EB6109E6C1E66A3BABB733E7ED6B01F841A4D895AE084B34B100F0197CA53F774E9DB0B2F8C9F4BCC1921F26BD16544893B66846B3304C71B74F40BC998332A04BA33CBC6B291AA967B922660C855D124A5376CC8047EEE3542057B27B658A215CC990B3177EADC3D214078960BB9B28C132866BA167C07F5E876ABCCA36422B8FC0B781C59CA5D1C7DFE777503D7A6F9084E62519A5F59448C4123FC1096BFEC3D58E81D7E0BC675F106BC0514E86992E3490A0525317EE4C784F96DFAAB8A430A9CDF246B92A36785F6094E2509FCD2A7A4022638B37177468549A04869787E946401DC5CA4DE968796908EFA8B67B9D45BB8A1A00E921E9CBA86C14A96E79C808DD65F04184781C46AE6FC7B1AA159EB55563B72184D12AB81590C86FC914E58C9AE186D63E780ED560A1866AB84591F0557867286350C3BCD4AFAC26799B68EBC60122C85CC9A26A9059FBA3101A0E8263ABB12FA93C5B0B63DF31B563C3086C8BB5C91989B96C3C933B82F529A8E4A02713F4B08F12ACCEBD343CAE17FDB2902C58A595CF2695610BE6208839B719F69257AD42805E892389EF9C76613B1074830B87217C6866E9C943E7CABAC44C88D5520CDFD3A594CA3374B603D2B1173F8D114D742047819C54FD411FFA93B08C482D6933D3EEA6D861C258BD0B4059A6B5FC338FBB003813744896C61C0D825E69B4FB7D56CE2563F6A120ED13CB1CC0BA776415EBF17BF697A95E04B5EEB8A316DF170F59770E4FB20A7221891F872AFD2424794B60F033FC6BA022101B162D6034BF01ADA53B4A7B1268238AE0E65C0D166022126138C3093126C87404A0FC6E20D901C33DC36110B64095F0A7109B2CC08190006E712D6F8B9F6D6AEA3437E7ECC7D2B303FD137A357EAB63CD1B87B0A73E36950E33AA75C3714F98990B62AA41DBB992DCB406D8552AFC687FE220C48455814941D41C675A0D9C3C88068537BBAA4A208555A2319F365D7D8CC87E42A1F488564F84727898963380338F30C4E783DEB161026B04CE1856EEB04CAB63158BD037A6C124522E91FD1F233A5A008EF39ADDD92C15D545D07F532F762A42C4C528CC59F650B305BB1042DA369FD70A226145CDEB389DA185972636F04974E704CA9D28CC240F3B1BFD03157D88018D41B352788A14C9CE9D33F20416B0E7A7A1055CC344397974BBE611A788585AA4126BE3BB461D7403B85F60599796F8AA928A28235F3305DA4315CB50441BA596DCDE15BD09309EC11823D377F1B0A4C8514496240BD7CC04B5A919EB4C0AB9EC1B9EF79C849450D6FD5B6EE188BBEA35C52289C81701542991CB2529C4448269576B3456A0AAB95227663388FEACC63B346A84742D8257428912B35E5A98A98786F04B0B8D72EE901B1F86C0F7135B6C2865174CABA64D76070191D6EA43194A7BD87C395741E7C40A35A57A87C97C2FACAFD6BC804FEB5D5C57EC896AF08B81CCE", + "BD8D55A47DF20F6B636E06B0ABA5F258B6E7712074B6FFFCE9153157D8B849B8", + "9DF0613102915C62D693A1FA216A588267D923E3B261BC37CC0D6910D3A87C525FEF2CDD80D5A2CDD55D2A30348AEAB46881C29803EAB9F8B53C85D8768FB390FBAA94236181467BE4B0DA9F51568E027AF0BA33E4F290A80631DBE2B102FE4D64B1E87AA5CA7EAEEE0F414D685C06C0754353D94AA7CB8FBE3ADF07E5042CB2E49EB24BF2B1F22B69E97C4D455B8A26F4718E5ABBEEE68B09FFB784FEF4D7AF2E62E835DE09548702B22CD041CF216D19380086196666AA9362D450C9A0DA6ADC97F09A2202157EF5D113CEAB30D261D852A46C531AEFE8F95D05AA6CF8CF87D0FEB374EA17394D7DD1D37D1D6B916B6DF2BA31072F28E8539AB9CD48DB64A14C5E1CE9F5B067C517A8B38C7D44B4DA8CA9429639E0076969F9F24979A1D2B83192E48F3B143DAD0E528ACF1B9AF2386626B51C7936F8F174964BC0913FAB829ADD7AA5B63D47A73B01BA06381FA61710897730516A522EF1DA17A6E21FCF7F6C8135DA057899BB1046C447A91A3F34D5844B7FBD9699C016F10B00BF71B052B88C432F98CA943AFF767A45F18D1476AD989C7F98393364359B8089B4B2949F76E8C93659930F837D1DD3E68C12564F58347AD5D0542A46B974816CC0CADF85B235BC786ED41F4AF242E1C21017144286DE483B287910E242B5991BA1F936C6B51B0673E77D436C77689C0970608A5C7E58AF1AB6AEAD4F93E3FDDA58527BB3C2535E20FB4FA5094639302B8D178525891D7EC052C7D1CEEFB91A40146234FB77633C2D94F463D1956ACD2CB1C77492992E83FABC9B1EB6728367CC64EE96D45475A5209C88EE6F13E0AACE0FCBE503B5948E80859F4520BA238BA8951A992A2CD1100AB53D24C6E702B8808826E206DF141BFBEF0791AFBD7DAF48A2C49A4C3FACF2C125771D1DBFEA06E95D0EB50790994A5E0B51267EE7245D5A6F4F5A140CA7DD6DD44B90B1D1C1B37FBA776089B24D5C466C45A0E10710BB82EE6376F9AE7FE77F51331FCF82F8D442BAE0344EEE2AFF62D4D662A4B00F3492ECD515421A8F49A9504F08D83B09AB45523DE37A9913B48A619ED314C21D5A2BCD7F09E584E7425422D21E72459EC09A6C349D5642C6812845B0D92E473A45FF1155F941E8A544EBC1025BDB671E7C243EB25B76BBFB87118470C12D0C65FA114F145BD7FB5CE2A524F636B2FD3C8B2B7B19A1C2A0E497461623E777052BB8E2367D702EF15448D89B28E07C7B636D51E94A4F714781179275E109167C162572328A2654A8C6D1541B45B291D13FB781F7B3FB3D43DC31CCCBFC1037DCA1D8C0DD3856B3FEC7A38484123E5D93584F4B0C116BCB173B1E2F060E04AB26E4A5F19561A51C7898A68A7BF4137A261279885D81E13EF6937EBF345AF5D301AC1422A4810A883D008C7AEFD7BF0447A0E987818AA4C17B3F57B5F93E22BF2B5A309DA5B8AF0F9BF841E099CDC097720DFD5928B5F2E77F525D66896530FBCA3264B795FE2D1A3FA2593A83D23C760F80C1A4952C78E1B1DAFB3CF5E39A0DBA17699C34363CF7", + "D281A3833118CBBB15EF35F846F9358576F030E49527BACDA4FADA9FA2733D41", + ), + ( + "69043B0269B4D596BEB41353D51C29DD687E6FF788989107ECB3C99F931AADA6AF8018A6F8A138510960B7419D82014BA6E58BFCCB2E386A278623C9891B4A7BC5B062E8CD40482ACEA10123C85A9E188C70E00FCCBA9DFD485E122C9B4E0A1537A5421AC134E887125AD9B049BB3484227FA0173C9E4B54CE946780ECAD4ED4055AB23EA1CCBF80D11CE0AC85C053B654E629C69B9A935B6AAB2303B47259FB237EEFBA8BDB9694AA521F068A61A1591ECB09439D13908882CC82E42A45BC558F1C5DB7C46262C5515378082BF68E624055029A818FB8B4CC9AB6E41CC8C0F3C66B0713B7674037C8B781214D5C89A4E8271159E08DE2DC0A367817C4E7C8DA3CB69D573F7964B4D7F04602E7382DD12D674BB4CB65209B35574E651FA0B24A0ABB3C64B899D5DB9C663B1D9D282D70D35A38F50FAC111CFA399592DCA8D8F8B7B1D1BE85218E3C07395DC162A85AAB9D267E12C191558849F9188E56357CD8374B8ED20DABC79EAE8512FCC005E15A4262C06D6DFC216B753E4626A3C59538C58B06ED98897677508CF798FE3A94AF19A583263CFCC24302CCBF917ABDE0003C20A39D8F42CACE486200DD7B1F346576690305687FFD877BD6A089C5029F597B278F173A3B41B1DBE63FA7B405ACD0856772A1C902B7E35683FDF64C6CE190677B531C077564D22082C82CB46B77FA33B276E841367600290B7E8CB32B8D1A1DBB450111EB8FD8916C4286A56F7214D2B0B3A89377509C817D4632CC085326301BD13203BFA889D006B7402C866035B01728A2D57C7A47F60512F25E63D4A9E3739A4061CCCBA9250EA8A53C4B3AE52AAF2175C7AAF365C13423FF586A0E3ACE217B08330625A9B1A2402CC0D8B714D380A2BC4B0E5C80AF03FC94CD7030E94719F62A5045DACE70030D3E1598F59AC08B6696D2932E5FF00CB8964916DCC81BE7A0656B8E30837A030672294A298BF58B2AC79B9C56738CB6A63D3117C0CC60E68C6AFAB0765F483EA3B30128970EC172139CABBAE9F5A1C57B0DEA44AB0E40821B18A35CC41374449A364BCB478736BB9A32ADAC543DE71085DAC2FFC6A5807B2D80B2495B3A1122318144E89DE37A893CC24F1204CC22778C86EA58440165487C8A7CD660C11C1CB71BAE86D51BD8BC42C4B811891B59E7F8C4375830155B6F8F558875DA21EF831365440369EC1FAA2294E553CE79655F35EC11058609D21CA8D034C4E4365395E07AD6436BDB4C879A38BC8AC49712B642AF4263B5999A698C062C2A11AF7B1FB447AB0F6810819A4642289AA9871ADEDC425136C52FD8B45A64440D3A57E3439A5D424B7A802E62802A89B9AAAFC027C2C11BCE84B472F084CFD50FE04388D8D559AC6B89A0860EA70198D4360BC7AC175B03CAE9B79D17C09F19914468849F85864A2C2518E6108FE3981B20476A60C10ADF654F616710FAE03B03D8B4DB169B06DBAC17AA033A270CFE3BBD84353B28AB4249A3CDDB81CEE5EC003E08CF392435949524057C4DDFDB67B5E9788835C749C0AE7238CDFB06BD78C51FE3A8B6CDBA141511464AA3A272C1785A0CC2CF169A1FC1830A88BB052B14FD1B51E1320CF1290096B30C2CAC2AF4820CC1D6CB0F117BE6086D4CDA01DCD595F2E5F779198FBB43B083CAF0DEBC36BC734BC4316829F3E2BED7", + "035B33594F2029ECB3845CB7BC2059B8A77A660BF37DCD55C97AFE708000DA40", + "476107D63A7498AE87E1557724AB397270FA8938B0BAB7E7D78C1ECE337A6D087EF862B7E1A104790F2B5120FD9FD71DF27165680FEFB1A2EB24351EA1327AE9D582F8D120C8DACDBD4DF49C2940EBD40FBA5177F8854055883CB9CFBDF18C1E17C56083FE4166112CD6486BE91A5DD889820F5F677CA850AC45CF1B627A0C6C23547CE85C10E80EC5DE319D483E78305DEB1DD115E38453EEDD78FC5B5018B9042712F52A14F6348959C7B4EF961F6F6789ADA6E7BDE59060636DCD9D590B71490B23083FDD68BADA5F50CBDA437691CF882D3151311274B377FEEACBC47447AF40C1BA96487D72357B22ABAEADD9F4C32F74FD4E82C862CFEA8B5D76B511A56AE68895F43E743C87E7BCDCE38ECC68B12C366B2FD47E5A3630F45ABB9994DB880009DC2292803374446C7B09C45A9259EA13A702844FBFBAB943B3BB114DF7A4D6F8D9F07001EB0EDCD80B8F52FDF60B01729AB12287E01CBFAB3E43B7A69B98BBD0F9D21DF4FB963D6D3C81EED366D47BD761A3239953378761EC3A2CB01A8856B4C57E74082819AEA218F2D7D5FF96E3A80E3179A86716F7174EE6EBC686082A281382A237298BC0013FA685B7B48BC975EE530575BC89090E1858FBD0E1F129392B3038A2C4599AF3A80F09BC8B903F8A7AB217265E240F1314F10FD355E4781EB1EB4DE3B81DC0ABFA62CD008E189864D6CF8B0A92B629B8821DD0812E51B6CBEAC913943953CF6CD2B59E94760395616DF7961BB3D3FAE4A36C517694140F029275D22AA4F72C945B6B001CC71FC77C3A2EDA5CF2934D00C935A33E209018302AA91D5B12905403D358DCD6B57D47F544F6807F1BCAE018934EC7B2C093B202E401D7D8D87B2B42A1D18625FD2D6B9FB9AF92586FAD87006D5CAD37B30985FDF812E489E215815E40A8BC87FDEFA6A11935550765AD39D07122444E63134F6C07D0CDDD6AC854048D907F43A272F88A5C5D93619FB03F0A95D011FE0CEA4A73F2F4AF30B2565DFE5AB0C48FE551587F52C37E9C296556E23CA78EC73B394ED7831A6692A709021ED1CAD78F1556F1D9FFDC18A1D2625D9C9CBCA10896045CEC29A2F1DC6AF2E80690A9EDA18FED5EA3558C0E91A6DC97CEB82FA56C90AADE516A170009F471092630611183221F1B3A770BAED65EC50439B1B447F514DB2042FB6FD7FE2B8FC6C272DC8EE0E00C7A1BBD6AD035295D5F0AAE2F4A09C3F75A0442E48EE826C3611FFF33F1429F9FCDF4F61367F3E95328E8A108A83C8B47420172E8AF8B7D14A5606349C66C9E0D61F9F49D82C334CA43269D07B9E73D8612D158F038D8F824E1C4E51FD5D894E12452C5F940AFE91D90F65591AF8DD161D2E40DEE1FE5DD32E01972D6532E31859127D12084F32077C613AAF1D71060ACA8ADDFF1E56819EB001CCE222F0A2966F9A836135010985353029FB3A638035DCB9567984ED7ADBE7938CC72BFFBC4682230D566804ED1DB47A94C6095848AE219AD2E14600315B333317A7A269906CD5AC4B69FAED011EEA9B2B525A0FDCB", + "61338EC538AE04644A7A2FBFCB49C968F68CB1B634DCA5626A9BCA03C4E30F89", + ), + ( + "32831A9AD55DDF56618550063F779D5FC0519D1369E50298BFC0413EA042B83891593A5F388700324C1481AA6EBD970264D66A9B244044955BBA2B41BC168C03D28354BA904E68ABCB911F60F189D1C77EE0845E243A9BC87590EAA53BFB783A1DB202BDB663F9787DA7939CE404295DC5710B66C4941B06B18569E4471FD265CA1A50B82EACCC55D4B16870BC949AA9000ABD627B07B19BC039B2C28707081D9B4BCD4C0B398631E835697B0C65B05068D6977C4EA011FF570C983A169A15397B45146FCA5E73D011D4D959BC8A074379C26D182E05A30764D3A7C5094482584D320A0CA885AB0AC41DCB430C01BC94EDF4186788A23E4434602C928B24490B3957D61C48B469C50829B81ED5031209A2A0533A41D878A3CC0690C2204ED30F1C90C2F3F36A78010EFF3C4CD4D76B950B13FDE25A4273313706BEF6165B0DD8B669901D0EDC0540712DB9E9AD34222077564FDE9AB58C225063187E2A472B7EA07713369E17276E6B27552BE0BA65F06B575538B69780B9F2A2D528AD6B94AF9A35AF05D8053D40A129F13CE1EC80C39A51E3DBA1A59C5B803449A3DB0351D3824D30336EE19FB5733B1CC5668E290591DB86729683DA56A223744540B7C50FB3983CF51F5B7012114C050D962A26052F0BF411104566366438FFC8833F36BC26D4ADB2438024755B3015207446286E7B638B8169D83A222C953B5832661F1BA6BDF34B95D383B3930F1997AF29C7A9D28080BB390370AB84596B47551745074567803A497E834D1D8B6136F1B2BCD57D5D9A203A09845B3B3DDD76A097F744F3A91D63451D578BB186BBA6B159BA3042767FC3B26CF211FC0539B29520CC748AB99019FFE4A1BD1886EA00A30C319B0D71C2F967669AEC4AF0DBABBCD23ADB96AB5A916291092CBFE2B36D9616EB25CCE4C676BA65A38AC29EEAA57C1D988A442BC4B0F287368247EC522A1D29BA49F798439C525EAA53B23872430487A1635E226095EC5B3C47F6611ACBA06EA8791CF437AFFA7BB51898817B7E6CEA61BD675C3068356203426C42712DCB9AB3E53F4CB6CF74EC1EA24A24E54B046347A03878BE4228559FE49164E857D7866F084A44502334DF0316DED139B8C45A279C6F748575E2ABB4B0A9AE34DB033046755C1A3EA8A493EF0560B14623C3C92BD03BA930618650486FA7920BB59CBCCFBAC846184B5050818E90902FD101CDCC9C37722E723733A7B348A99C6F1917412F57222490526FCC825BA2A7408A387B31B4215BA803099EF4524BFB141434BA5E069B973105A84AF4C12BA09FB462380F2688B12222DFD77575366F66D042C4E54C70283B70503B24253D2D975784795798B7887C025238E1B0C27C620C323E527B330315CEC7A7CB8818BEC429342CB5C6EC93802C5855359B2C1FD48A0E761B8608A5E8EB55A667C009CCAB28E2CC23B75C97D7BE655B87EB0A8FE5B420591691EA4754D49608414CCC796431E8A59A39C76125A393BE461D7CA2B56DC46E244A8A21205DFDF037219CC26E4234FFE496C92965727A6FC9D660413369AB27797C4B1F88D2578B54721F64CFB574591B40CEEAA5A671CB7ED2AA25A5D733A9472CCB135C8C21ADF968B97C159CCDF78A91567B4CFB7FEB732B6D2CE28924865D4DAE0F034628D662D6CDDAEDD117862D", + "BF274241021405CD6CFA844F1C0A24F6EA3A5E6E15040902653E0A17EAAE52DF", + "B1F97544D04EDD67B7E1B8FC038F2F36334C9496F5F2A775C098671AF26FE27D79D1FD645EA7968E6FC353BF4FC5944D7D836591342F62C5663403EABF1509C65633D99F3F58B317C3C67500D8695337CB83FF2795863141F5E0393239CBD37C82A6187077EF3AAAC3E81379CEFF229B60128ADBE4C282B8AAFB1447E8B46463CB4CBB081BC4F99EA872A77526E1CF0F7BF492FEE41CEC46446A2D9D7E6179BEE633D6494ACA09D14E7960FEAEF309C907DA4B275503CA8C3DC2E727F071AC5C2A089891E798AEAC3B8E984FE1C03378C63FA1F37A75C7595E5CFA2BEB45F110E3B355AB6644237B86A7F061CAED6DE08647FC0BF70774CD470CAACC5F44E824ED6EE584EFFB94DE9B9A555391006B4766FA64CE93D65CB244A681CEFAB982935110DCC3768E11DD59C955A4E2F45930CAE5034F5D5A5D5DD37C43509E0E5FBBB616953D5309992A9F0EACD1D4C570E5EE8324531EF17500675325112F3ADAC7C901396590CAA0B35F6BE0E9FF008F57995EE7727064D2DF1E1D450E6134781480EB526059D2C37767C7696E484C4208ED3AC4880B34447DE2B577D20A72E0F59CCC29A99FC139B3978DA97F924F1FC6040598417A70DFB8FB528E62669A363506E5CDD037950286AAC1635EC04627026AE97D70759CE62F9811C1663EE39E5FB148AEF6074A1FBD2193ECD4D06421BD0B7CCA0C024BEB1F8441F583E21D638F106B8DEE2E85268B366807E3FC0A23B530C881DD54FEC2A5429D89F9B702A4B248ABE596F021EC8AF984F2F074C994D5C6947527DE0863F51A972303A82D59A88E0FE79A2F284EF3F40DDF55479762A66050FA4A4A40BD51F1526C7BEC652DB46E4B0D6921EF52442C3654F83005CDF394E1E55F8F1EEDB47371C3C4B2F807586475BEE0BFB95E68CD760ECE689CAAFCDB3441536D8A6AA3C7EA80B6DFCFA2D84AFBF187246F32E808F7171CDFC52284B81857C9191B16E01F8C1706CD4C7C647A08D71E732E55FF645AA013DDA9E67E7EB9830ECB5DC244B4850B7329DFDB2B8D97609DC751D9262512E167CFDA29590B7403DD94D3FB49E08A9EED4586E9EBDF5365971375AEF61DF87B10DFA64AC35BF356A1E1C990EA12453378ACE2ACD7DEB1DD2685426557D1F41A022F1F8BD64FE84939315AC081132331C878D3D0DAE2D00B1B3B751822959D473942553742E61F8C618F8AB5067D8A49C836C4EA75356111FE3D1A781E8CE83676557F546B906EA4C40E7908B4473B2620D59C06D23BFF494315F8DC3DC41BC65DDB141CA2EDA86F843358AE92C44197B6F7FD57C7C936B19FC1D96BB12878CDBE9D105BF53595649B84BE94A3B792F1474372EC57A2FBDF36912E61DC1869AC70776EFAC0A00E275C2D4B52345C11CD9A9D4604DBEFAE168513966473A7DFFBCA0479211FA5F5A8B28D0F6140E4353807C6AE43A809AABB62F4896BC0B9E33339C3A1F1A23E65D28030D9107022FE870FAF0C39BE4288C72AD67008DE94C2EC7F502FFF33B9255BA3574DD61863464E44AAD5BBD2", + "CCD2596065F728BFF32792DF57BF133F8F50BC8DAB822640CCB6976BFE693E0F", + ), + ( + "DBD23E1744CAFFB0984F97A697E7760ACCB421D07EB7E25BB1C91B3E8473E2727600855A038419765C8E86A025794290CE095717D5BEDDB6364A8BCEC06617F66638C7658651067169E3CF69607977E6378039212EDBCFC7926D7C9B50C960758C85A21C743AF6DB5351198636C0072012913B641B6E53AB0594522E5185D0060B595A6F3C628BBF7614C1D88A1D7788C6E994A0C29FEB14A4A0121E155B976035A0A1A121E4D41C1FDAC830837E9071187622C3A128650D91831AD1CB6B660C2A5669CE38C99BC1C546D706F8DB82058C7A5C666F54F5BEC8B1142818686947B511F2132B72815E296BEA04546C9A1EE9251768281436114E4980095D8A25EF048DB69A661CAB717CC6BD3F436A77A19FC1D146CC77AC5FCA3C3EE32D711143D6E8642FF6247D5BCB2BBA511C57415F2C43DA50BFBFB6116E970B2CF589BF2805E42A45D4B60B1B9344E7E3CA77E88B0BB95DAAF96F388C2E410258C61265E2C636684586C40332D318781EA27A5438B9B336AED64B1785DAA44B26092B2A53316387B007BE08019FBDA577687BCA204BB29D63CFEF1255F55A713A120025E94B780137DF5B1148861CE3B86853E61EDC279CE59373DEA5C96B45A404342821228EF51166FF639184024A65F2BBC50C629EF69EE9E8C4149C478683051DE7A9E478908310463BFB72ADC44F91941C1A0C3C945C0A60D08E0E4714A4BC454CF7AE1A888A835CC89D540B83771EF8214A32E0A2B97B8CD971C8FD2188BF311A9CAB1A36142AB73436B68AA22F5A5B7E55138835512FB51319C94CF094893522921615343102C0D1F4C9A5F52AC913760177C61BA467669ABE22136C25EC352EC8025AA6522B47BC4B4326A3279144E53D0B2012ABE8C298CB7A6C9B4AB17B069DCC3A086CC60886CFD05286D33373C3D8A50B302A5561033971CC31D9CE56254BA3C32146F815FFA0C2A5005E78B13A04FC99A0176CD132C750FC7FECF67857A047E174850723B33B1A2984C559C2D8A0E60C55B2D427EF67C0E2396B3B854C86CB09FDBB78FC234FE84C89592520D66B3AB2D1291D5433380576AB74AD74170D805B10CCCC0114F714ECA9B63300C445D82B94051D3CE075F2FBB8F7351EF454B08998664ED14C834789984C939473906582106DDB2BAC7BB2696531FCE5CD580B9DF56B82044029B2895F0F41936B91916455019749886F03886C8A9E2DA147EF5073BD96BFAECB92729A71F2F18A2EF694CAF5674F6736E0F94CFA0B2B60695D64D75A85B14DE6DC7782A351BFF2CC0EC47BEC3374A89C1BF5DCBBDFF782FC0AC98387AB61E7387DC2BCDD92816C13C011E8263AD052A0136FCA88C58AC419F16B12E381A8873044B83C2FEC68BFF82B8499C47621B3C0E6969ACB17658D36772422880AA3485F621933E407381C712FEA2280297507194625EAC3F30014A4A516D6BB8593379BA62733F43203AC23985C17925D428EB84AC3AACB7B80B19B3EE84600078712F72E9D0043FDBCAA949B4AEEF5A8B6C5096823049434ACA7510A1BF9A50ED0C9EBA64A9D53C88079C87174929DF89AB75732F0AB3C138148EB8C5FF92C88E17678B701344A3021428A842C828C28A6C5C15C852D3C4368DC0D6AD8545D45F0A8BC16BBBB45291B092D25F44016462DC96C9593D007794BEDE1", + "101F8466E34BA303AB8032030B9ADDAA6DF2F1A7D212BF807426770511350948", + "7839E8067401971FC39BFB619EF8935446FB735368689CAB95605771EFC5D30402C6DF666675C0D8CB8178013A70E00EFE321E46DBB4A2BD09056A0219CC8DC1526D6D322750C4AE257AB875C5E1109559B4ACFA5D59E48500C2FD5CA714B08E1967EFDF90C9944F02E27AF389C79CE0C8EAA166CB7A1846A1DB0479EB4CBEC2BC9ACB77CECD42C05540A5290B7DE2E3DA121665CA195767258F76517EDF5D3E30E0D7229A91B2AD47645CD31D50CA1543192A0782F56E4378E6EF29C5B12F73DB0B8A9C2DF495206D8D28C8D0964789480E20EF6E70BC959EC6DA2E7B958AD1EC402090AD8595E8A6F642FAD9C6EE4B9C59F6A4E4EEFA0F205C5B519729D9AEE915C0E3803ECAFBBD031910F3FD1644BFD54ED10B230561259305C7FFF2B7DA35FA6BB19424DECCF2DA47CCB947FA6ECB10CA573F8B3271EA0B635E5FD25C5BF57452285141D28B02885FCBA1F949F8ABFECFD548FFE12690BA37EA43EA7209960F6F25496076A74D195256B5BBA7E62DBC51AAFD5928421B21102F41A9422181D7924731AA385B2DC3741D4BFE6DE4CA5A91057E1F0369742270ABAAE4BA6E5AE28CE200FEFDD06ADC2B2238B2D0973F733B8FA10C62DF6F0429BDA45D83485B528AA5B2C16464B896CEC7DFAD1534B31185FBA569A6D86EB309467A32A496AB93B84A62F781CCE7FBB4A44A1EC06B7F241D42BAD37222619A12F993097B027291D332743E6B4B6CFC940967BD0BC394C3CE2CEF5165706A65E143DDD97B87F1682DEBDFE87E8E73FE684AD203822A9AE02224640CD5801590E24785E43EAFA9C144939EEAE143AF90B2A8720613D16C3103061E14C0D15CC85A45223B9BD013C7D3E0D1902A2CDE44E09F26110421E27AB5654989C2AF4C4980EC3C2A7D9D2DD5B90C90A32DA27D9FBB3EA58F17D321F935BBCC18A4F058513B019FE0279C5F0904204B83E0079AD17B99C37AD4202648F44AA9A64F9F9E57D90191BA861D7089058DBD5FF0CA9C4710E0498DA97422881AAEFA6C32FB20A84D5EADABAEE18BFD1A9D5307513938C2173BFD7C9BC057541B85C313423E0128F49F46EA1D48B0B61EC4793193E18814DC0BFCAE063E61919BBCB4E87E1AFFEE4D07AC2EA68ADF9D05FA7B11257C2B84227B20284B840178C14FB7285E8D54B60C2659545AF4BE8BA71A0866961D2DC36E580DCFDA824846AC2AE540CD99C9B351C4C6387A9C11515E0C91408846464400220876642059E9EE7620095FC440A6AD7B1E8E97CCDBE5BA102913F392477C0C0A2BE410BACC9B5252FCC0E17A18A363A05BD523131BB952E464A7595E8257D2472E92C9198BE488834076596FD96839A3E14991377A4745B3FA4ECBCC4092C2EB9710D2C2CF34581B4A680DAE5929D0967D4F1EC62EC578B324D41EA3E9078B63DC0E128D8779B8F4091593FC31E47D16AB4FAFF6AFEE850CAAD9F48E2A5DD9C8B78AF04A2E56D328A54B9FA769369802F56D5765368C435800B09A52C1483E3A44DBDFA22965E5BA1AFFBB4C2DB139497B43A1CC", + "966CF8E6E799440C243BEDB9AA9305BC6261A48A4A046B37B80EE93BAA24FE4C", + ), ]; for (ek, m, c, k) in cases { let ek: [u8; 1184] = unhex(ek).try_into().unwrap(); @@ -157,25 +300,65 @@ fn kat_mlkem768_decaps() { // (dk, c, k) — 정상 복호화 및 변조 암호문(암묵적 거부) 혼합 let cases: &[(&str, &str, &str)] = &[ // valid decapsulation - ("CD4178D37142A91C7C597854DF1B00E6147DF88C58769B10B2A0B9BBD875FBE05BA228847D94C01ED4C57860339D6735B0F73788C495688395BCD51E4A36BCC15C1F3311162F8525557CBEFA176D0FD8BCFFF79843085B78C0ADE09104EF77179927C37E739AAF7166CF6539C35097C0C3769051186572735B600C3EB07A412932650CA28D469D971B635D789C7FA608CBF819B7B7707291B9D22C25CBB55B0D875EA903CE778C81A2E96BF6E96BAA1A484E69524C644FC3D8C564140E32D4AE0BE1B1CC875CB4EC7764C498E5F01C04C9A41CF26BBDBC08C8E65BBDA78A1C4C0F362AAC60E16F5D225454C2103A99758B0A93864C5F32179808D2A3855AC837424E00F0A0A3055C07F5AB5BD04BC6950603960C29AA2B18C60C154A1022C32EF59BB718E94ADEF990C045C27E3943C115C1D12A96EBA503A01098E33673C98B0421A7C71189530D08B4933912C37C1DCFFB0A14A93A3EA6603A6035CC194C58532D70E2B1B7B0CA5254BD7E010D7409CA58973C7F2062EAC9BA196429D7DA3DD512C1D81182E17B7CEC7C630659602C117F74008070E967F5F2293E9643CF70A1F5A38069D0359D76A84C6846BA1A1BB91B5279864B9671C3896BB2D43278DE11B1A89B8F95F96A9EE815A6E7B063989A13D32080CA55D6C0421E6893B77632DCF96D09F11AC5A740029273CBEB42C7C96FD36C1EE5589F61D70B57E93B8405BDFAA460FA09BE4A345B1D9939466806B9E462929240051B57DD614626A813BAE8248A66C3C0BBA804C52B345924A76CCEED5468EC066DFD5A1770656F3A3B956531A6D4166CB36480235A4E62CC7A9DBA2B45F464EB39002BA71A1211963A86B16880C83B3BBBEFA5A7BC4B31B2A69C09D9675A4B300A829F6A438649E26B82A7CDA8B80CB65986E47BAA2F544CFDE7B46CD17FCF1BAAE568BB28C8235C591AE717BBC05C0CF86344E4627542639FABB11BB5F6C66BBAB92F981CF6BB217942B95F635E7EE75E314CC97F050276801B32BB86CA586E79C40638DB283C3948DA4C7195C4421A1B3BC35886678C33EB0A5A52BA5C78B138D51CAC85704BC0140CA5B830117225CF4B0E363B8155EB4B83C7073BB20AF2798376BA442D309577C62705C7A9172527FAEA9CE21B3863F32DAE7A2D4B2314A376C14917563AE41DE067007567A9E22BB30B834AFEF2B6B07846AC30B54104B8083248CD23AFCCF794C1F59C1C62803B28873013296F5048F0637DFF072529443FFECB57E12A7F0CE52CC8A85D258372DCBB5C0612735D1A6BA7E0AE02215547B28F5E77488B3A241EABB2F3F19F89974B2D822604E74495A2091C363E9E88A7B80769D4492F6B7A7A3BB27DA60A1BA326882B7732C9118E545B6C4B79787225BA598378BAD7B3CAE90EDC06CD0EAC43A2C559CD554C5344A2751952F4B4BD2BB694016B41F65494A1C750A9D9C338A25F5AE26BFE851540E6B84202114ADB75986193737A5F28C2BC2658CD6C960FB0C069C910A8F259593F2770984C87178C29530B6E6424B70BEA514F8568255B62CA6CC39340515EE1968AA800DB4B19AA7676984B798ED5B134116CEF52B5634C7997F33C58E18D1A64499B6450F247A3555BA2A4D44782E02EC6397C69764FD9C08F87E6B802A01BD3E77E0F544CC39944D86A5225878B435B260BD6236F2A68EE1440AB7C06F93991C7FA6AAFF20A1D862D7C73686F071CC7E6624A5B5568E10CB3C23E6FCCCA3FBC63C445472A235758129FD8067B839588A7221C250791243A8C020CCCBD29425201A53BC84621412CDD0BA72AC75D2DA4A13F26A59A7629BC372E78D54776F2C781B99B9110B4833C9E69136A5098BC7FD3A4FA9C3280557EAEEB89D0B15AF4016452E42EB0D608D5850BB7491D8DB6C703BB8D00251D69950DF0E99553160122D4C5FBAB7BE3727F74DA52BA2A240CB5140B9704A7742D17113022B9260A15CC6A9CCBF23349AD94B57B04A29D3859BADCBF979515B433C86008BB43E010A2C7AFF8EC7BFE508B7565B6B3898459599FDB830B7461C1C1C52B145B2829E59F09B0CA28863C70CA1D8A7AADAF6B0FDFAA45096268505A70AF267DA7771555D66802405F72D190189411C5A0BBF7A7185F857EDD5A4AEFC948B6B6997C3BB9FFAC18153785FF752481839CB39027709A8306538687F2501118617E5C8A67E41A2C856DBB274BE4FB17EA47097F81693ED7389B21C8EEBC200F601DD2D78E89EBB919D9387768311E04C9894B08E73C4122B41D334A56E930084645C000E21C7738765BD02FA9F6A7A3188B15739F50AB5EF1713879D94B19964FD272A949A275E289A7C0BA9EB52558926146A280B1036501DC76400993BA71698AFC4318E38847C3F77134D7300F211862F6078BF3685928C5A8A809A945BE058B4A2E000E25E88EB7907BB7977993D9C0183248FDBB8106EA3ADE37B4B7B8A2E139C2AA19088C25227FC5BDD59826F6FA15462301EECA0F4B76BC8992891D066B41700701B10D37899898AC9E69F7257F9330BB0770E0621417AC392D7685C7E47497D096DD0B26161B8A41CA9D9D67664C8120DE5C4ED303C5926126B58979C3F7300EA8533AC0CBF32178A0531CA634634B46B097B367FB2612EA461AC8E04DCDA2C5EB9543457969F9481457188A56E115DD965A810C3CF3217CFBBC51CC316D7799107B20256EE08DDFA87CF318A92D489C030AC57C25344182A5C8B770F0E4B8C03838050CC56A0B91FE84C46034C8DAD78F74161D0E93A9B9C4CFBA5384D023981AB6734977B1E0F900443B4B62F6BDF3227570ACB36B0B2E5D69CDFD9B7A05F45300E5A8918290CB0968FF1363C6048FDCB53F63207BC841BC3C900074E48AB4C73C38662F66C9996B3A926DE61A0B784E2A392E93DB87EA841A190A6002C9B4BF237003E79961C07F156B606AC861ECE75F44AC60717871FB3B0CC2D9C987AA768EAC3497B9893EB809EA4A8B24B144C2BB014A8A7D51D94FA5A05BEF7C47459260D2CCB035806F286793343ACBCFD207FD086B00CD517979ABBA9159C889BC6191290BEB2AA793BA19DA9DDD6275B430182EAC87C5B27115029A696CAE0AFA5C38AA506F486FF52B6A9EAC1414516F7FC34255633B1ABB7252218A67C213966BA6FB237A0A43B59E45C0A34902494359B19304D9F04CD432715A17C70432680DE20FBC1BCBDDDC3C24037573D9A22FDB34F987C610A9799861369288405A138CD4850B14393A6B6344FCC38CBF07875F37A3CA04DA17CF188E017E8CEE406DB439078D7B0E5170AA973C154D3EF4A59628ED3739622864BD402C715EB878247BCAF1F1070AA50599421D778CCE8FFE07BDEE87DD36086FB6FCD1EDB33AA50EA330F9ABD98F1D6117191BB3CAACB0E3F2C139", "997F33A26049E8467F96E36B0F30A68BA7B99CB86A65DEC373DC53388FFCD140BDDB7F966064C3F95766EBB90482089A136EBBC5727B8CFEA68F1A5119817735EB71EEE31444FDB38F70B67B9560D8EF6D807E3339B1C824047233AD57D8D4ADCF2097B017487BFABC85F6D23C6449CD2D0A8DAB7AEE9BFFFC5A2A1FCFECB289D1940A11BF1573945971D2E80D117B00EA95AA06BE7E7A91DEC35B291F971E790B4BFF8A07D6099E40232746E550470A64205DE84911C1E8EB88DCEB615B43792D040031C38D1BFE55108B18E64706FD7E26384E31970655E033C26D7C27595DCDB469A95F5A19727B38D150CD8538B5CCC14936E49A0D92692273C3CA9EBFB2AC760A77B527E3E2B537D50900819F5D1274D4F4952F1C776F0BA4E01DF91DE2D51AFD4E2FE9B98208B60313961140188EC6F9AB0E8B346CB3306A2BFF3734958163A66663B93B7F5A0E3863C37B3D2803B7C29EA6EC49DACFE6DDC589EA41C4D0543C8A8E05092B0EB2956D974042B0CB522DA76F8F611BF9FC956917922C010D7DDB50563ABC7479F05E7D80B583A3335AFBE569F6F3D5683EB131C6B3F2A0B5284EBE03EEA6CC7C3435EE7B47B1680122A562428C936DDD90AAD907989A4BD26730F444900A48889582EAF39897FE19D3CAC3B08D28B0E663B7CF2C30C6CFD6B73DC227E6267503CF8EC2DEF07AF399FA171E31248F23B8DDF33BA95C463DF012E20170204ED8C3A6BDFAFBE82A398FA5EE2E61FAC25A0107614D7545421A4EA899F0DBD2A1C34517004764C15ECB0A8BF41DAD12B5B555ED4A29D4240D6060EA3F9CBC08991F140A7CCD886B2B6A4EE6A0F87AFAA9169AAD9F354851E7513EF9C85C109D617563F42057866B9CBCFBA2733AB06FBDCA8D4350F8AFAC77BC28758CE2435196A281F13096235077AE75AC0ECB09DD123469B645796A8153C158934BEC4C7B40174A002B6993FDBC39F187E69E465CF773515BEADDA077B13271D24D8C2E05F02FC8DDA19F7AB0AD5D8DEB5F4AA124E3EF5288E4C8C46027AB067B94E130C41460B68BE068C6BE2C62A3772E23CD13CDFA36D01B21B62B166C0955C728C234970731D73C1BBC1967178EE146803E81772549377872CC46EBE017ECF40955DFE47A98F6A2B85E51A9E8F9070D844E24C3F4BBFC11392C87DACF1F963CA1C91FEA33ABFC662FDB709B7ADAF81AC17740BBB2699A40D1D162FF55F277B7A9A3BE7E8081A203F4C08284C8637994B0692B66CCA611177BC2FB9850A367DAD0B7ABAB85C88880947BAD9C02D54EA60EC0D9C7C36CE1A62C3CC7FA15BF22CE2B292886485E496AF718AFF7A8C64915D9267D3AB89D47317FAD00DCB2BFB4953CF90A3CB849D87EB4FF67F1850684E2FD2AD26BB6681C50CE8B9817CFE1C1E4F30E07ADC3022020361B1BAD3C98A49FE3D0D75A2A7DAB6E3E665A3E801F6F124A00818DFABE19A22CC684C5BDFB94AF3823294367F95C9CE3D8ACCC91FAEBDDD096ADC90543744BA5FC61BF166B8E9AC1DC03FDA0CE29755B17DDEF3C", "96980F7C1B160A45A8F56FB38D38D7FAEC7844DDF617FA47522CA2998605A71C"), + ( + "CD4178D37142A91C7C597854DF1B00E6147DF88C58769B10B2A0B9BBD875FBE05BA228847D94C01ED4C57860339D6735B0F73788C495688395BCD51E4A36BCC15C1F3311162F8525557CBEFA176D0FD8BCFFF79843085B78C0ADE09104EF77179927C37E739AAF7166CF6539C35097C0C3769051186572735B600C3EB07A412932650CA28D469D971B635D789C7FA608CBF819B7B7707291B9D22C25CBB55B0D875EA903CE778C81A2E96BF6E96BAA1A484E69524C644FC3D8C564140E32D4AE0BE1B1CC875CB4EC7764C498E5F01C04C9A41CF26BBDBC08C8E65BBDA78A1C4C0F362AAC60E16F5D225454C2103A99758B0A93864C5F32179808D2A3855AC837424E00F0A0A3055C07F5AB5BD04BC6950603960C29AA2B18C60C154A1022C32EF59BB718E94ADEF990C045C27E3943C115C1D12A96EBA503A01098E33673C98B0421A7C71189530D08B4933912C37C1DCFFB0A14A93A3EA6603A6035CC194C58532D70E2B1B7B0CA5254BD7E010D7409CA58973C7F2062EAC9BA196429D7DA3DD512C1D81182E17B7CEC7C630659602C117F74008070E967F5F2293E9643CF70A1F5A38069D0359D76A84C6846BA1A1BB91B5279864B9671C3896BB2D43278DE11B1A89B8F95F96A9EE815A6E7B063989A13D32080CA55D6C0421E6893B77632DCF96D09F11AC5A740029273CBEB42C7C96FD36C1EE5589F61D70B57E93B8405BDFAA460FA09BE4A345B1D9939466806B9E462929240051B57DD614626A813BAE8248A66C3C0BBA804C52B345924A76CCEED5468EC066DFD5A1770656F3A3B956531A6D4166CB36480235A4E62CC7A9DBA2B45F464EB39002BA71A1211963A86B16880C83B3BBBEFA5A7BC4B31B2A69C09D9675A4B300A829F6A438649E26B82A7CDA8B80CB65986E47BAA2F544CFDE7B46CD17FCF1BAAE568BB28C8235C591AE717BBC05C0CF86344E4627542639FABB11BB5F6C66BBAB92F981CF6BB217942B95F635E7EE75E314CC97F050276801B32BB86CA586E79C40638DB283C3948DA4C7195C4421A1B3BC35886678C33EB0A5A52BA5C78B138D51CAC85704BC0140CA5B830117225CF4B0E363B8155EB4B83C7073BB20AF2798376BA442D309577C62705C7A9172527FAEA9CE21B3863F32DAE7A2D4B2314A376C14917563AE41DE067007567A9E22BB30B834AFEF2B6B07846AC30B54104B8083248CD23AFCCF794C1F59C1C62803B28873013296F5048F0637DFF072529443FFECB57E12A7F0CE52CC8A85D258372DCBB5C0612735D1A6BA7E0AE02215547B28F5E77488B3A241EABB2F3F19F89974B2D822604E74495A2091C363E9E88A7B80769D4492F6B7A7A3BB27DA60A1BA326882B7732C9118E545B6C4B79787225BA598378BAD7B3CAE90EDC06CD0EAC43A2C559CD554C5344A2751952F4B4BD2BB694016B41F65494A1C750A9D9C338A25F5AE26BFE851540E6B84202114ADB75986193737A5F28C2BC2658CD6C960FB0C069C910A8F259593F2770984C87178C29530B6E6424B70BEA514F8568255B62CA6CC39340515EE1968AA800DB4B19AA7676984B798ED5B134116CEF52B5634C7997F33C58E18D1A64499B6450F247A3555BA2A4D44782E02EC6397C69764FD9C08F87E6B802A01BD3E77E0F544CC39944D86A5225878B435B260BD6236F2A68EE1440AB7C06F93991C7FA6AAFF20A1D862D7C73686F071CC7E6624A5B5568E10CB3C23E6FCCCA3FBC63C445472A235758129FD8067B839588A7221C250791243A8C020CCCBD29425201A53BC84621412CDD0BA72AC75D2DA4A13F26A59A7629BC372E78D54776F2C781B99B9110B4833C9E69136A5098BC7FD3A4FA9C3280557EAEEB89D0B15AF4016452E42EB0D608D5850BB7491D8DB6C703BB8D00251D69950DF0E99553160122D4C5FBAB7BE3727F74DA52BA2A240CB5140B9704A7742D17113022B9260A15CC6A9CCBF23349AD94B57B04A29D3859BADCBF979515B433C86008BB43E010A2C7AFF8EC7BFE508B7565B6B3898459599FDB830B7461C1C1C52B145B2829E59F09B0CA28863C70CA1D8A7AADAF6B0FDFAA45096268505A70AF267DA7771555D66802405F72D190189411C5A0BBF7A7185F857EDD5A4AEFC948B6B6997C3BB9FFAC18153785FF752481839CB39027709A8306538687F2501118617E5C8A67E41A2C856DBB274BE4FB17EA47097F81693ED7389B21C8EEBC200F601DD2D78E89EBB919D9387768311E04C9894B08E73C4122B41D334A56E930084645C000E21C7738765BD02FA9F6A7A3188B15739F50AB5EF1713879D94B19964FD272A949A275E289A7C0BA9EB52558926146A280B1036501DC76400993BA71698AFC4318E38847C3F77134D7300F211862F6078BF3685928C5A8A809A945BE058B4A2E000E25E88EB7907BB7977993D9C0183248FDBB8106EA3ADE37B4B7B8A2E139C2AA19088C25227FC5BDD59826F6FA15462301EECA0F4B76BC8992891D066B41700701B10D37899898AC9E69F7257F9330BB0770E0621417AC392D7685C7E47497D096DD0B26161B8A41CA9D9D67664C8120DE5C4ED303C5926126B58979C3F7300EA8533AC0CBF32178A0531CA634634B46B097B367FB2612EA461AC8E04DCDA2C5EB9543457969F9481457188A56E115DD965A810C3CF3217CFBBC51CC316D7799107B20256EE08DDFA87CF318A92D489C030AC57C25344182A5C8B770F0E4B8C03838050CC56A0B91FE84C46034C8DAD78F74161D0E93A9B9C4CFBA5384D023981AB6734977B1E0F900443B4B62F6BDF3227570ACB36B0B2E5D69CDFD9B7A05F45300E5A8918290CB0968FF1363C6048FDCB53F63207BC841BC3C900074E48AB4C73C38662F66C9996B3A926DE61A0B784E2A392E93DB87EA841A190A6002C9B4BF237003E79961C07F156B606AC861ECE75F44AC60717871FB3B0CC2D9C987AA768EAC3497B9893EB809EA4A8B24B144C2BB014A8A7D51D94FA5A05BEF7C47459260D2CCB035806F286793343ACBCFD207FD086B00CD517979ABBA9159C889BC6191290BEB2AA793BA19DA9DDD6275B430182EAC87C5B27115029A696CAE0AFA5C38AA506F486FF52B6A9EAC1414516F7FC34255633B1ABB7252218A67C213966BA6FB237A0A43B59E45C0A34902494359B19304D9F04CD432715A17C70432680DE20FBC1BCBDDDC3C24037573D9A22FDB34F987C610A9799861369288405A138CD4850B14393A6B6344FCC38CBF07875F37A3CA04DA17CF188E017E8CEE406DB439078D7B0E5170AA973C154D3EF4A59628ED3739622864BD402C715EB878247BCAF1F1070AA50599421D778CCE8FFE07BDEE87DD36086FB6FCD1EDB33AA50EA330F9ABD98F1D6117191BB3CAACB0E3F2C139", + "997F33A26049E8467F96E36B0F30A68BA7B99CB86A65DEC373DC53388FFCD140BDDB7F966064C3F95766EBB90482089A136EBBC5727B8CFEA68F1A5119817735EB71EEE31444FDB38F70B67B9560D8EF6D807E3339B1C824047233AD57D8D4ADCF2097B017487BFABC85F6D23C6449CD2D0A8DAB7AEE9BFFFC5A2A1FCFECB289D1940A11BF1573945971D2E80D117B00EA95AA06BE7E7A91DEC35B291F971E790B4BFF8A07D6099E40232746E550470A64205DE84911C1E8EB88DCEB615B43792D040031C38D1BFE55108B18E64706FD7E26384E31970655E033C26D7C27595DCDB469A95F5A19727B38D150CD8538B5CCC14936E49A0D92692273C3CA9EBFB2AC760A77B527E3E2B537D50900819F5D1274D4F4952F1C776F0BA4E01DF91DE2D51AFD4E2FE9B98208B60313961140188EC6F9AB0E8B346CB3306A2BFF3734958163A66663B93B7F5A0E3863C37B3D2803B7C29EA6EC49DACFE6DDC589EA41C4D0543C8A8E05092B0EB2956D974042B0CB522DA76F8F611BF9FC956917922C010D7DDB50563ABC7479F05E7D80B583A3335AFBE569F6F3D5683EB131C6B3F2A0B5284EBE03EEA6CC7C3435EE7B47B1680122A562428C936DDD90AAD907989A4BD26730F444900A48889582EAF39897FE19D3CAC3B08D28B0E663B7CF2C30C6CFD6B73DC227E6267503CF8EC2DEF07AF399FA171E31248F23B8DDF33BA95C463DF012E20170204ED8C3A6BDFAFBE82A398FA5EE2E61FAC25A0107614D7545421A4EA899F0DBD2A1C34517004764C15ECB0A8BF41DAD12B5B555ED4A29D4240D6060EA3F9CBC08991F140A7CCD886B2B6A4EE6A0F87AFAA9169AAD9F354851E7513EF9C85C109D617563F42057866B9CBCFBA2733AB06FBDCA8D4350F8AFAC77BC28758CE2435196A281F13096235077AE75AC0ECB09DD123469B645796A8153C158934BEC4C7B40174A002B6993FDBC39F187E69E465CF773515BEADDA077B13271D24D8C2E05F02FC8DDA19F7AB0AD5D8DEB5F4AA124E3EF5288E4C8C46027AB067B94E130C41460B68BE068C6BE2C62A3772E23CD13CDFA36D01B21B62B166C0955C728C234970731D73C1BBC1967178EE146803E81772549377872CC46EBE017ECF40955DFE47A98F6A2B85E51A9E8F9070D844E24C3F4BBFC11392C87DACF1F963CA1C91FEA33ABFC662FDB709B7ADAF81AC17740BBB2699A40D1D162FF55F277B7A9A3BE7E8081A203F4C08284C8637994B0692B66CCA611177BC2FB9850A367DAD0B7ABAB85C88880947BAD9C02D54EA60EC0D9C7C36CE1A62C3CC7FA15BF22CE2B292886485E496AF718AFF7A8C64915D9267D3AB89D47317FAD00DCB2BFB4953CF90A3CB849D87EB4FF67F1850684E2FD2AD26BB6681C50CE8B9817CFE1C1E4F30E07ADC3022020361B1BAD3C98A49FE3D0D75A2A7DAB6E3E665A3E801F6F124A00818DFABE19A22CC684C5BDFB94AF3823294367F95C9CE3D8ACCC91FAEBDDD096ADC90543744BA5FC61BF166B8E9AC1DC03FDA0CE29755B17DDEF3C", + "96980F7C1B160A45A8F56FB38D38D7FAEC7844DDF617FA47522CA2998605A71C", + ), // valid decapsulation - ("CC918926213945851DCC669396F5CE20845435541567C3A8D7F70110DB0511607493814CA66C51287142E5178C60B23C2018374E273765618F1F413933286D457A7C2F35129BBB78C25473D845CF4996809B2216EC3A0366E97BB0214AAEDC263673B3EC0A0805335CEFFB1A99D221BB471B6647664665C6F11A19C1A5AE7E0344D2D3A61C054B7A401AC17652650B2DC036073766738BD67D033B21ED777A184720BBFC3A174A23A8C0A392E428C448654FDC5730488E29EC4955015B5F2262B048888B977EE2B126BF9CB83175AB22220194055085E90D72D1BC1B5183E2C620AF2B1E6CBC57F03545F7F43B09E9C6A73797EF861BEF6AAE0D20A8CB2459DC59AEF2FA7EFF9580B427795B3AC024227EDB0253F970BF011433BF9623D646BD3C67265523477B847660711BAC503AAB5C4A82A5A2237BC1E2A8C229289996088501E03CE1D052A820C31D641B85B8AE93D333AEB18A6BA5205BB38463BA52D1FC12D8D45E4F780B905B78414294B992517AA0358706B534D6BC6A830D949176CF9499DAF8CB4EA4A52B2A233516416E125815D4B47D9B4A1712319471C81F5926BC274FD746B82100912C4581AAE41962257909D683533369D6D69D32311706535B4A28B29E930908A1B41043A6BF0A8E1D2AC7E0F721C70C53B64B74D64A193692A0CB7258AF7C47D7221E9DC87FA9BB9CDF943CCD235A663C0340C17418D5650405CDECF77AE673A2DE0C159B4873879237CB2422B1625DF773835D30BE55BC5D660805B51C0D50909A34F778DE9267A2002253E00FAD1819781CBEEEDABCBDF0869F4409A3763DC82727C75707F0C578634289D4945E78CA0E6962ABABEC7FC5C56DA2998573C8A87DBC48F35B27EC45245ED5A7107C788B16347758320A556D90B208F5AB7B19F813CF93B5A4791C25A054D9881535E6435EBBCDD70A4466E6A34FD8841A8827FEE68BA67C309C70A66A787F1D205739C16FBA556F7DA053CC201EE45389FF59C19BC45012716E1AE7B460D12119876F2C204DC168941A7C13328B5EA0997B24C1C5170C40442640A1D6C3AD69A46B8397A762BEEAE257273C618D617C2A996E85B444D141C2CB8275DD5B788427CCA4AAAB99D48899230849F833E0D1B6211141F29025372A926C013D012AC4AA2077002C171C9A3C55F09904DA4CEE84220CA2330B7420D45C2D2EA46F4C6C9132AA48BE4685A7F80865B02375659530977A42A661B96A2969C71936DB17D574C3FAC6B5303779282622CAC311282ACFD854380B65C7D1691D2F17B53BB800AA60C83D641941BB7569A22263567D1262C27C6092E90A64C8BCBFBBF09D02D93A8EF157E8A447DEFC67F3C0BD3575973F90CE49475BFEB91622B13CA6AB94741C35E13B5837D2CA5FF5164EC33ED52B8A5DEB398598010EF22410A2B75E57747F1A12F6121FB12B84A43426939071C3A91427B01BADB2A27D41CA6FE882E986086F972F4B5B2D9BC65B82DA71D5C932AF53838567621D9B2B3A8187170C4D16E66BBCC384EA8B5724BA232AE7AC16C397CA7563DB43879C566056B5ADA202A92CEC5A00059E73F2AE948ACF32B6674ED4778BEB992F10A4EB496DA38C02FD9194A1C41EB6DB06618A0BA2918394B9CBF4A87654A5A63BC8BDDAE5A45B9C9B61049CA50924A7D6380A8507A1DA4BC09B3C6AE61185770297E79C1A958689669C03E300F6D5A80A2786DF1C3950C808827AC810FA962D6B57FCF14F3CD95E8D3CB6C069B32ED6012B708D12A823C0D5472D14BC1EE3181C05BF26067BCA30C3DBB96F6520752A03AA086CC21E11BB506A8E3365B041373FA0E3A3E7D71A4BF717619A7FC5E44A54B95FB362CF83B665C8C8217C7547D6321BBE68BEEF52CB1113AAA4B42532032FCF19C119D14EEA89618D41133F953CE0D6ACC5D0C9A8C55923673AEBD633DC4243E3E55C50285295431D015A303ED7B1A13853175A6D644628F8A0C9C7E8CF958920CF572C00633CA4D5C5C7E385919A37B1DB4588FC8F12017DC76C2124BC5CB481BD5A40200901B0A39C7D27AB3C7A8AA4A7E64DEF45896BF6BE77B3BC65622C6C711C604B01A6E5AB251383DA256547FB6DC532BCF014731C51C20C57B12744B64216090438CE34C50FD9CA64048461B3022B1CDABF57EA787176AC6772CCE2DA376C8773E646A2EA80667476CC4C737E8AECC6B56963556061A93C929FC2040A110A6AA9880D306D4985524B3A202C687AF8AA5DC879B1290023FE262E8F887CCC9554FDCA5724896CF1A8593AC68C6DEB5B5CE76C3A11C886C47F69286A9DB76ABF1901D4A88EF5E38C3AE24B329032B3E6773CE7A6E40010AF08002E85AC307316BCD42D26447233010537A036EFA82A7A3889CDC165F10B79CBD18E096CB47CC41F68210CD3B8700474A641362DFF77073DF20DDFE3B33F1993191148D39C9DCEE1668E9A4097B03B36A95F652415A6B4A1856A6E6CC31EF6F62FA8901B8E1048F3C93870950DB1E609660157DA76553B019C7BD65712C88D4BDCC6D75B1839BB30C774AEBEA6793CBB7D887997A56442EA0A7BC024161745733EBC2B99B22D6FC8258F2BB74D618D544408748354688285A48800E8075D38B43F241BC67A24CA429464D041009A791FDF954050E34064705EB692366B1CA1A63C1493382D917A9E182C7A7965515580BBECA643E5E5B7FB544E95D4A0948475AD399E7487C31149C7DD7A69B66C1C1ECCC1940C22D2F538C59133C7D1B55854A8D43261394460C950806D964564D69065A0C710D95D1736567AC21C3EEB67815B53BC0A89BF7A398F9BC6C4DB35FBD9BF69329E0CC6C59173B8F9F85D14E5C2CE10214A310100D58EDBA716B8981456DA45CC521C4A67B1D6B50527691973CAC8E1D2106F69B8C6906CBDA9C368A22FA3839ED6E81E413B359A773C5D056526CB1B6AA84EB7A2B5E6B40EB1456B93987896B294D11279CAB4870FD756745C74765785249112046C9D57EB073F263DDE66A09F59B75413C7F9695A7959CCCD2CA6E30AC3E682604D9A4957C71F6DE4284753342D2CA41119300045B0B7E7554E5A6AFF486CBFD93B7F91B8F2025812385D724056A9CB5B6DF89FB044176E57CFF6D267F8B22C96A34C3D799434C37FC613731BA96427501C7938B9EE780CE5723421B44D6877AFBA9920FFB6C1E498C4E7714BE6BA3CDF337386B2753FE2310873211106C9A3E43068326707345B531C8B72E16AB0D310C25A9E9199304E9730CF3CA91A1938A4D1D388F4D37181A32BEB9BFF80D3411D55882A9AD6FEC1CBC387F5690AE7FAF14DA2828D154418C7C3AD627A49B6D549254A0A113036B9D4345EE6FE4B7FBA212AE9FCCF326870E5E7936274F3D3BEE4D0FF1BD5A0745BA5997667186A3B24", "71DC9BF164275E5CDF117D9D5CC03FED2267B3F379B5878E9E612BD0C77DE6B2346B51B36F1CD6A836A97D6B0DFB853F6E3EE730901D0A6CAC1C7AF186CA9908A535E04A8729BFDFD36CAC030D47A4CB9766819B52D6A76ED0B9E2A550460DFAFA604C5453A811618FCB22D504C062912D59BCDFA52989A8819E05A38E6E500F700911B8C73FC3DF8B0922F6305C18EF97243299643E9628A933DD160D6501B80ED2C05545BFD6FF956A8CA9579C27C09C0CABD49BB53D1E7307B3989F36DB9D5625D09AF82EBEA7B0381C071E7FCEA5E5DC8248440C684A2DEEDB8C824E00E909A7DAB7C7B269E72B65813518C117438F5288E0721B355457E58EBA6FFBA8B2F9E6A3EB19A5CC5816BDA4CAB08D6C1DE81F90D6E7D4F70FE4D91B834247D7E4AAEAE653D9F887B26BEF6D6D446618A7BEDB04F5094B9B5AD1C5C892313EBDE87EBB310E38A1075D6217160CDF51301262EC7CA82D359121A4D248341F285CAE06C0DFD27FEFFA212AD8415AD90DE416D0F3AEB743E62A708B5DAF62F0A53A6F9B3F965FA727D08A1EA3493526761FDA0703C1D46E5CA6F91D383EF6514B6777C08DD8B1201635F23233C49A910E9B9933207392787AEB94899E3A01B1E65D84095ADF3B011AAEA64868160B4925B137A3A47BF1A68916C1CA656AF87906A303CA1720EEA5F42DDC9FFBC3A3C56F86159B93240A6E1D1C24EECE30DD8BAFE45280830BE1A1B42A15605E4A207D5D694798D60628CED3E22E8C8E8E0310177E0077699F2CCDF152CF91EFE72568C43C8D8956CE4570A3B02B7681D04AD9EF701462C4FE95F0BA943B08F4EE43FBE132679580F8DF82C3615751AA6CDDF345B11D14FEF2ECBD16B631F84D6DE75189202E84576725DD02DAC6DE8A1CB517932CE40D9AC34B469367B2A534E5435861F8CD11B7F8AF15CEDD67C9DCDE2476BB1035021D6149392C60550EE09667A1C2933350688ED91B3762E2B9AE3B25D21062058B5D45AB31F7B07753760B9B1C92F21FB02C1481A3AB0AE39F01D60713E7BD67E2B775624458E06FBB69614861DAD78C9A918DBB3146E8C5484FB75C50147D4886766EC62709DADE00BCFBA2E21E26F1449C2753EDF7F3506FFA0B43754D534860BF4907EB68C8B9DA23F5935199D5D4CF4C1D8DF25B59182EDEC27D0FAD70BD6638E4C3CB3C3AF2E8A900887629ED9132A12EE6B693BBB40BFE9A44991147B2922509AFFC8179F2241A895719BD3916587E5642AF812552D3D5513982651A2DA7EB7E88DCB1D30C2EBC0662EC9C9E8BD8CE26C6C846BE802D11C45AEADEC877211AE09B5D663D4851184B44C6B79864AB29C1B6EAD0E82DE4C82FF7BBAED21E89C72C8D718995C19EF305263BFAD3EDA5DE77EB188110E108DCC2D5F6AEE163440F9EDA7405C3793EB4CA3B09D0377EC70ADE3E247AB6130B33DA1FA3B2BF0A3BF7A878A3C633212A09235A93D291F8595E22D9B1124FC5BA306B937183BF8CD5F997113B876B769DD9A78B19F86604549D6EAD96DCA1B877C292EA6B25C04E", "A73828B556426C787F66D75BE272B3C942349344A5EAAF62F2AC8B46FD110A70"), + ( + "CC918926213945851DCC669396F5CE20845435541567C3A8D7F70110DB0511607493814CA66C51287142E5178C60B23C2018374E273765618F1F413933286D457A7C2F35129BBB78C25473D845CF4996809B2216EC3A0366E97BB0214AAEDC263673B3EC0A0805335CEFFB1A99D221BB471B6647664665C6F11A19C1A5AE7E0344D2D3A61C054B7A401AC17652650B2DC036073766738BD67D033B21ED777A184720BBFC3A174A23A8C0A392E428C448654FDC5730488E29EC4955015B5F2262B048888B977EE2B126BF9CB83175AB22220194055085E90D72D1BC1B5183E2C620AF2B1E6CBC57F03545F7F43B09E9C6A73797EF861BEF6AAE0D20A8CB2459DC59AEF2FA7EFF9580B427795B3AC024227EDB0253F970BF011433BF9623D646BD3C67265523477B847660711BAC503AAB5C4A82A5A2237BC1E2A8C229289996088501E03CE1D052A820C31D641B85B8AE93D333AEB18A6BA5205BB38463BA52D1FC12D8D45E4F780B905B78414294B992517AA0358706B534D6BC6A830D949176CF9499DAF8CB4EA4A52B2A233516416E125815D4B47D9B4A1712319471C81F5926BC274FD746B82100912C4581AAE41962257909D683533369D6D69D32311706535B4A28B29E930908A1B41043A6BF0A8E1D2AC7E0F721C70C53B64B74D64A193692A0CB7258AF7C47D7221E9DC87FA9BB9CDF943CCD235A663C0340C17418D5650405CDECF77AE673A2DE0C159B4873879237CB2422B1625DF773835D30BE55BC5D660805B51C0D50909A34F778DE9267A2002253E00FAD1819781CBEEEDABCBDF0869F4409A3763DC82727C75707F0C578634289D4945E78CA0E6962ABABEC7FC5C56DA2998573C8A87DBC48F35B27EC45245ED5A7107C788B16347758320A556D90B208F5AB7B19F813CF93B5A4791C25A054D9881535E6435EBBCDD70A4466E6A34FD8841A8827FEE68BA67C309C70A66A787F1D205739C16FBA556F7DA053CC201EE45389FF59C19BC45012716E1AE7B460D12119876F2C204DC168941A7C13328B5EA0997B24C1C5170C40442640A1D6C3AD69A46B8397A762BEEAE257273C618D617C2A996E85B444D141C2CB8275DD5B788427CCA4AAAB99D48899230849F833E0D1B6211141F29025372A926C013D012AC4AA2077002C171C9A3C55F09904DA4CEE84220CA2330B7420D45C2D2EA46F4C6C9132AA48BE4685A7F80865B02375659530977A42A661B96A2969C71936DB17D574C3FAC6B5303779282622CAC311282ACFD854380B65C7D1691D2F17B53BB800AA60C83D641941BB7569A22263567D1262C27C6092E90A64C8BCBFBBF09D02D93A8EF157E8A447DEFC67F3C0BD3575973F90CE49475BFEB91622B13CA6AB94741C35E13B5837D2CA5FF5164EC33ED52B8A5DEB398598010EF22410A2B75E57747F1A12F6121FB12B84A43426939071C3A91427B01BADB2A27D41CA6FE882E986086F972F4B5B2D9BC65B82DA71D5C932AF53838567621D9B2B3A8187170C4D16E66BBCC384EA8B5724BA232AE7AC16C397CA7563DB43879C566056B5ADA202A92CEC5A00059E73F2AE948ACF32B6674ED4778BEB992F10A4EB496DA38C02FD9194A1C41EB6DB06618A0BA2918394B9CBF4A87654A5A63BC8BDDAE5A45B9C9B61049CA50924A7D6380A8507A1DA4BC09B3C6AE61185770297E79C1A958689669C03E300F6D5A80A2786DF1C3950C808827AC810FA962D6B57FCF14F3CD95E8D3CB6C069B32ED6012B708D12A823C0D5472D14BC1EE3181C05BF26067BCA30C3DBB96F6520752A03AA086CC21E11BB506A8E3365B041373FA0E3A3E7D71A4BF717619A7FC5E44A54B95FB362CF83B665C8C8217C7547D6321BBE68BEEF52CB1113AAA4B42532032FCF19C119D14EEA89618D41133F953CE0D6ACC5D0C9A8C55923673AEBD633DC4243E3E55C50285295431D015A303ED7B1A13853175A6D644628F8A0C9C7E8CF958920CF572C00633CA4D5C5C7E385919A37B1DB4588FC8F12017DC76C2124BC5CB481BD5A40200901B0A39C7D27AB3C7A8AA4A7E64DEF45896BF6BE77B3BC65622C6C711C604B01A6E5AB251383DA256547FB6DC532BCF014731C51C20C57B12744B64216090438CE34C50FD9CA64048461B3022B1CDABF57EA787176AC6772CCE2DA376C8773E646A2EA80667476CC4C737E8AECC6B56963556061A93C929FC2040A110A6AA9880D306D4985524B3A202C687AF8AA5DC879B1290023FE262E8F887CCC9554FDCA5724896CF1A8593AC68C6DEB5B5CE76C3A11C886C47F69286A9DB76ABF1901D4A88EF5E38C3AE24B329032B3E6773CE7A6E40010AF08002E85AC307316BCD42D26447233010537A036EFA82A7A3889CDC165F10B79CBD18E096CB47CC41F68210CD3B8700474A641362DFF77073DF20DDFE3B33F1993191148D39C9DCEE1668E9A4097B03B36A95F652415A6B4A1856A6E6CC31EF6F62FA8901B8E1048F3C93870950DB1E609660157DA76553B019C7BD65712C88D4BDCC6D75B1839BB30C774AEBEA6793CBB7D887997A56442EA0A7BC024161745733EBC2B99B22D6FC8258F2BB74D618D544408748354688285A48800E8075D38B43F241BC67A24CA429464D041009A791FDF954050E34064705EB692366B1CA1A63C1493382D917A9E182C7A7965515580BBECA643E5E5B7FB544E95D4A0948475AD399E7487C31149C7DD7A69B66C1C1ECCC1940C22D2F538C59133C7D1B55854A8D43261394460C950806D964564D69065A0C710D95D1736567AC21C3EEB67815B53BC0A89BF7A398F9BC6C4DB35FBD9BF69329E0CC6C59173B8F9F85D14E5C2CE10214A310100D58EDBA716B8981456DA45CC521C4A67B1D6B50527691973CAC8E1D2106F69B8C6906CBDA9C368A22FA3839ED6E81E413B359A773C5D056526CB1B6AA84EB7A2B5E6B40EB1456B93987896B294D11279CAB4870FD756745C74765785249112046C9D57EB073F263DDE66A09F59B75413C7F9695A7959CCCD2CA6E30AC3E682604D9A4957C71F6DE4284753342D2CA41119300045B0B7E7554E5A6AFF486CBFD93B7F91B8F2025812385D724056A9CB5B6DF89FB044176E57CFF6D267F8B22C96A34C3D799434C37FC613731BA96427501C7938B9EE780CE5723421B44D6877AFBA9920FFB6C1E498C4E7714BE6BA3CDF337386B2753FE2310873211106C9A3E43068326707345B531C8B72E16AB0D310C25A9E9199304E9730CF3CA91A1938A4D1D388F4D37181A32BEB9BFF80D3411D55882A9AD6FEC1CBC387F5690AE7FAF14DA2828D154418C7C3AD627A49B6D549254A0A113036B9D4345EE6FE4B7FBA212AE9FCCF326870E5E7936274F3D3BEE4D0FF1BD5A0745BA5997667186A3B24", + "71DC9BF164275E5CDF117D9D5CC03FED2267B3F379B5878E9E612BD0C77DE6B2346B51B36F1CD6A836A97D6B0DFB853F6E3EE730901D0A6CAC1C7AF186CA9908A535E04A8729BFDFD36CAC030D47A4CB9766819B52D6A76ED0B9E2A550460DFAFA604C5453A811618FCB22D504C062912D59BCDFA52989A8819E05A38E6E500F700911B8C73FC3DF8B0922F6305C18EF97243299643E9628A933DD160D6501B80ED2C05545BFD6FF956A8CA9579C27C09C0CABD49BB53D1E7307B3989F36DB9D5625D09AF82EBEA7B0381C071E7FCEA5E5DC8248440C684A2DEEDB8C824E00E909A7DAB7C7B269E72B65813518C117438F5288E0721B355457E58EBA6FFBA8B2F9E6A3EB19A5CC5816BDA4CAB08D6C1DE81F90D6E7D4F70FE4D91B834247D7E4AAEAE653D9F887B26BEF6D6D446618A7BEDB04F5094B9B5AD1C5C892313EBDE87EBB310E38A1075D6217160CDF51301262EC7CA82D359121A4D248341F285CAE06C0DFD27FEFFA212AD8415AD90DE416D0F3AEB743E62A708B5DAF62F0A53A6F9B3F965FA727D08A1EA3493526761FDA0703C1D46E5CA6F91D383EF6514B6777C08DD8B1201635F23233C49A910E9B9933207392787AEB94899E3A01B1E65D84095ADF3B011AAEA64868160B4925B137A3A47BF1A68916C1CA656AF87906A303CA1720EEA5F42DDC9FFBC3A3C56F86159B93240A6E1D1C24EECE30DD8BAFE45280830BE1A1B42A15605E4A207D5D694798D60628CED3E22E8C8E8E0310177E0077699F2CCDF152CF91EFE72568C43C8D8956CE4570A3B02B7681D04AD9EF701462C4FE95F0BA943B08F4EE43FBE132679580F8DF82C3615751AA6CDDF345B11D14FEF2ECBD16B631F84D6DE75189202E84576725DD02DAC6DE8A1CB517932CE40D9AC34B469367B2A534E5435861F8CD11B7F8AF15CEDD67C9DCDE2476BB1035021D6149392C60550EE09667A1C2933350688ED91B3762E2B9AE3B25D21062058B5D45AB31F7B07753760B9B1C92F21FB02C1481A3AB0AE39F01D60713E7BD67E2B775624458E06FBB69614861DAD78C9A918DBB3146E8C5484FB75C50147D4886766EC62709DADE00BCFBA2E21E26F1449C2753EDF7F3506FFA0B43754D534860BF4907EB68C8B9DA23F5935199D5D4CF4C1D8DF25B59182EDEC27D0FAD70BD6638E4C3CB3C3AF2E8A900887629ED9132A12EE6B693BBB40BFE9A44991147B2922509AFFC8179F2241A895719BD3916587E5642AF812552D3D5513982651A2DA7EB7E88DCB1D30C2EBC0662EC9C9E8BD8CE26C6C846BE802D11C45AEADEC877211AE09B5D663D4851184B44C6B79864AB29C1B6EAD0E82DE4C82FF7BBAED21E89C72C8D718995C19EF305263BFAD3EDA5DE77EB188110E108DCC2D5F6AEE163440F9EDA7405C3793EB4CA3B09D0377EC70ADE3E247AB6130B33DA1FA3B2BF0A3BF7A878A3C633212A09235A93D291F8595E22D9B1124FC5BA306B937183BF8CD5F997113B876B769DD9A78B19F86604549D6EAD96DCA1B877C292EA6B25C04E", + "A73828B556426C787F66D75BE272B3C942349344A5EAAF62F2AC8B46FD110A70", + ), // valid decapsulation - ("C3DA5C2B2C4E8CE093851782A8E918733731BCCB90B2C39234F164766AA401D326F5E340A121918E5691FB119732BB4AB74CC29116A7A775B7B5A51FCB16A16C45AB7A368DDB2B5C736B29B2630B7DC25F70CA85CB0A0689FAC6A49A29AB292DA052A98FF56C02174E7DA4A2EF2C7E15583358973B5F123F01580A8E0CC2A27267F8DC5679210C3EA6AA5172CA46AB3B757C75684054D35976AA902A1CCCCA53727B96EB8D5670B911F7A7EC8159E60562830BC7FBA0901EF3308711A2FC274A6DE59D180427D8D33C8D24BA647378D2F45229827249BB93FAE5A0CAE54575D61FE74A475BE9858790BCCC7136ADFA0D0640B194B7658DF2A49450130B6B0C2B2B89AD45C2FEE71D360887289404E0FC8FFE51BA13B73268C8C7B72A0330410D38999602594EC1959DD0C42AF3542444120FE4DB4FFA553E8853022841004AAA3F978B6E2C1A4314A5B833C84F41277E9B920FBA7C94C56489801A3AAA511195C44ACC9806A3994ACFE912F016AC0F223C9456CAA157842974ADDDA30B03003A23C3C4D22B26758C361E67B4CDC285B0F9C214F949F44725F4B25D3F1255A32461B9BC7E531910F922C463960CF1F177F4055A2F180ECF9C956700A4C6405B25C081B5D009F1BB62F613388E60650855971A01A39E2A0E3A9C5B95B4322C5B4FC5F329CB64803EF6A55F0178D561729D29522DACC07085C55CC6437007479ED75C452A979DA06AAC3A5F19118431F5AC88BA849BE7AF25E262DBF93C7281694EA8BBCA2721B16A54A42C0851E046AA226456044C51EA0037A88459108AE6A3702026B29CB5C92DA2B3D627AB948738B5841D7AAA18AB74A4ECB00F64ECAA8D80AA6BBBBEACEA2868E264374A1AD23A5543DC1D55687460765469E5C01D12CA94CC86738A896E309AEA759AA74A8461063717522856520E1DCAAB3BF40AD70471C1788FD45A9C24048B5E8C3BAFB1BE9D905F25641100F5627A14C773971BFB402CB8A3B1C141621AC588B832473B0A0A6D937DB13A5BF40B8D3CD71566E58859CA1EAF792CC9368DB1460EAE61A92A32309E406D919878EBD088F6551C994ABAE06973FFA6B901040643C221CB378EE829564E42CC78E67B1E641F8BF30CB5C39D2EC340C3F0626CBBB3D8A722FD5C93349670CAE684E247CF3A24A68805217F91AA3698B9D18369D6F5B620C39739C911239388E0543DF2F137566897C26C08A2F58BF80B68D96BCC8CD28F59F1391997C9AD58285710A96461194F429920FC3837A581A0D38D7B717696C052D6C81E36E70828566B4487192846229B7CCA973A25DD548F27D610F921B2EA83A1050B3CF1D2785FA99004898026B2C8C443CC219199A9B86E2B3C2F8BEB5BC008D04CA40339472E4D11255B61B2C08C3B73B13C3AE39FE8FC5913A974A882C2C74157128CA1D4E8B86289A7266CC668DAB96F6AA5E43C09E8A262B721A2C02042965092CB3A70402449DEB40DAC61A0B0E3A1C65A7BAA5032ABEB165F5628A4C38041349519278272E5144B70913ADC1D3F93B42BA09A0C831EBC7369CEEBAA61F7A2E8208B19AB0F66A130165B2C2BAAA9819C860DD7A7BD829803E483A56C75D82CA96C277FACC4473201C40C080741B61BBE70C944B124C78A91F86A1DB6E9C18DF7AFD642652BE118061AB02B055DA9950B32732180E85AB3B56A79EA2073229AAEAAA79C1518E9F76570B949D04C9771220D8FBC1957A6A2C07855F6002D7EF48B098C627AA2A9C099199D2293AE687066E39CEA9C3FE321BEC52044DE7C0FD6998B4F7C9E8D4159E810679EDAA9F7B32DF0C161B5938229818FC93403CF48594FE5CD5DAA67CE412A9D7C5590998BD296AE9B77A536D023DC63B978D754D624A703435272FA2E18291E9BF45FD824137B4B98460891AEECBF4B97102F547D2BD43A00A57AFD60C3AE7A87083A1F1DAC601C41BE0212B98A4C999DC64FB51C2DA53A0B95C47DB9C9B7815C560226B373A78899A19A2C757CBF02A52025BDCF086CA3A2327FE66E9177864363C79B326501274BC3938F440ACCE63663B8F0704F960FE81C3B31020DE1102724E98150489AFD105111F15C2088BB66B1A05AC67EAC29BCB2555BD7B8763CDB45D3F0CEF417A5A24A62950A9050313EC6E6048E6A52D5ACB4B7041F2168A847FC696092210C848A1F0390A405B60FC83FF86931E6ECCD16199D31457951A249CB0C0D2C66507B7025EE8859D9CB23E733A07ACA1038F0A822717AE95C38128BBA019437F9A63BD6D7729EB4C873095CF1C3C1BD6A91B471B8C1510ACAA7A6F52A9899C180AA229AA9634CDDBA1DFF187D6FE61B8B8B8FA7D38D453471599B6FE4F9429683C0BDA6329F080C9053531E47A0B7B147D08B5C8BEABEE61530B8F88154AB5ABB09377D54A3595C9837888F19122F47B9C959070D8635261AA9099119411B1C54E6E527A3DA336054C4F14904F4280ED8E83D5994BBCD67BC67542348F80DFE300A75DC9926A6786B73245867AC95CA196B44B8F1277B57F5A09BB084DFDA3B7ABC3077289D8EB92A56B95A7A5842E14C425B74ACFBB9B4B49B5BF1908B963915584202E974641C8A7D5A8205677758B3814A0206C990CA1C927A0CAAA10DD0D1753A50B4E8CA01CB0A2291BCBFD4E4B51CEA83BF6B5D3EEBBB5974B689983BB3FC38688749FBC94352E4454CF0A8AE46A237E8336618117A7031BBF1AFBD414A736024636807DEE5C564E80213036630802906EAAE35210EE643ADC6750CB903274395546848C156D29E54FC986AC36E5AF218C32CA2DA3B2BC30B5E322B9128B0955783719310104F263A4BA88B835136665A3E76AABF513B15DE159225F8947664CBAEA9C83111B347AA1403E418EE30040918B9A44CB1755CAB81276A101A2E72EA8663F34CB4B318E5E183DE629DD4F4C3C54AA5E84C64441755A82A3B3949CE217A4F10C89F6AB925A37990FCE681154AAC701527318AC35C7912E001C8DBD2CB3BCCAC727B51AB6A7FE3B623D3811105792248DA05E536C90DA29CEAA9BA30D53795DB47641A9DAF273D52E8987EC84F43FA61858B437A4B0BE5822F456C2767F986F6E6850F177C3DB1A257B4AA24D5296C800AA2AB998899849C3C564966466B6CC38EFBBFB87AA3CADC0154FCAD6DE831765932A705C3CC21B51DEA5268343CAE394177768AC7D4914D6969B185200753415CC28FD1B5C22E3547A12348502C1C5D64335C604D7638722C3B32A394AE1FA1CB69D785A8B76CB039CD646143F71560AA15060C02349CAEE26D854F8C90C5861784C90D410E9F98FBE7D73B373402E60A4C12F1C651AEA71B0E5E947C207DBC41C486DA3ADC460D3319542D5785B039B6C7D3B40CF2CA8C41B404E781BAF86F3FC47C34A02D47924151", "4BCC0DE7F66858B01BC0EDCA0988AD438FF7722DF56DF292D2717AAD9DEF54047EDCCBDF8814FE6A99FC10D85BC96622625E06561925D259D6B26F46260914BA55EFDE4D16DCDBEA3EEEC6B241409D111411383CFE03F113920742C8AE031D5496F671087A08B2A81BC88694680C43C7B294134E657DF7CBE70EAE81EC1E5239EBF1A33108ABBDA851B5396414B63FB6ADCEC6E23206CA8AA938A7657613C19A6FADE8ACE9245065072CA936682E636F61225098BC66E290A19956FEF98E429F2715FFDF546DBEA0F0AB94734E32FBBF9829326A9FF4A3AD0BAC8AE0666C3726EC9701FB3ABDF90FA73C571D550F1273573B904964903940BF9DBCBCA39A8B85C022FA71C1AB87120A4B826A2E11DDF86F4529549B1FB8493D21E6C8EDF8A4AF8D85DE45C7F2D49EF0A29F3FEE950ACDBBB083416C36178110ADE27FDC2AD14D2A7CA71AD7336F0E18E3C6AB2C92CC1C9500745DB989D6FA2999BFAB6228F4AF743F745F403D81A31A61E2FCE1C58C599390108EA416F66B14285E266827E86E8289AE756F8AFE684C9811EAB0EFCB13494872F15B5D72BB8CB80FF7F111B9DF3AB66D0C7CE40F1F688BBACE0899AC2EC85651AD2805AD65A7A451BE2CD9E47CA30646199AF03E0C29B10AE0B7015F0490739B27528198D78DDD537ACB4D110822D2D405917A2D6BCABAFCB1378ED1CE994B2E7A16280453728AE3525D2688F342951505C00B20C1A304CBD5DB81E0A70D9BD575B3D8E6FD4DE2774F5EF97EC7D00F7A6569459ED6E07ACDFD5860FB007E1A39D7501D35FE4BDC3048BE978845AC68FB239A249E8F4EADFB8CB016EEE0CD781CC53BFF488969BC3A483F5C0C919F726A75D546D74BA46A2A14780267D7D760446C393A792A33DD47164A0D7538C39EBE1AC9779C9195E6AE039C877A7197B70FB7DDD1E1247F26E4B45D0F889768BDA43C30165437467171AAB17B1032AAD853630D1EB25896F3FFC95AC45C679736A0075AE7504ECB345953AC81B467DB17E8CE168EB2AA161817DFEA2896E7D027FD69CAA76DDC6C5C0BAE04957F28AC505BB2F49944B1E2AC1C78B0BD5C996D294F23EDC62C0560209E589162138D4DC69024FC0FF925B6904BE1B4B7E3F4BEAAE65C5B7351091C32807A93DF9E4C5C783DB5DC99C8A6DC92C391A4FD692B23C28550862EBD3990BB3F82280F3DFB361F5B3A46A2CA2ED3206989A57ED172FDF3A53011F5F9B79CBCA3B3D3B7E6FBAE5BAF84815ED0AFD6695B9FB89FDB7278EFBE2C368F2130FD48A23A39F43358DA2829D17AA7E611CC29AFE1C319A683F557390C63B102A38F09F8C547C20EAAFB6F8583CAB0960C4C35474E0B48B1106E2D8B1010218FFB0F9B53FC7E91F0D762617C5DFC72CC7D2F8EFC5BA4C6150891CD51C56A699CFE8C3E5A1783F01D43DCB80381C18973AD9CE8E9823C32F73F93EAC679D8BCBB0B137A5274517E3432DEE29282A54EF02D73B11CB5FEBC6CB25ADA82E010320E43E6CFA15D924B1B0F4217C4A779BB1FFE8FE31E28CD40283C", "D284C566FB6528F5F51881592944792309AC950031514025B7DBF0A87F3FB7D6"), + ( + "C3DA5C2B2C4E8CE093851782A8E918733731BCCB90B2C39234F164766AA401D326F5E340A121918E5691FB119732BB4AB74CC29116A7A775B7B5A51FCB16A16C45AB7A368DDB2B5C736B29B2630B7DC25F70CA85CB0A0689FAC6A49A29AB292DA052A98FF56C02174E7DA4A2EF2C7E15583358973B5F123F01580A8E0CC2A27267F8DC5679210C3EA6AA5172CA46AB3B757C75684054D35976AA902A1CCCCA53727B96EB8D5670B911F7A7EC8159E60562830BC7FBA0901EF3308711A2FC274A6DE59D180427D8D33C8D24BA647378D2F45229827249BB93FAE5A0CAE54575D61FE74A475BE9858790BCCC7136ADFA0D0640B194B7658DF2A49450130B6B0C2B2B89AD45C2FEE71D360887289404E0FC8FFE51BA13B73268C8C7B72A0330410D38999602594EC1959DD0C42AF3542444120FE4DB4FFA553E8853022841004AAA3F978B6E2C1A4314A5B833C84F41277E9B920FBA7C94C56489801A3AAA511195C44ACC9806A3994ACFE912F016AC0F223C9456CAA157842974ADDDA30B03003A23C3C4D22B26758C361E67B4CDC285B0F9C214F949F44725F4B25D3F1255A32461B9BC7E531910F922C463960CF1F177F4055A2F180ECF9C956700A4C6405B25C081B5D009F1BB62F613388E60650855971A01A39E2A0E3A9C5B95B4322C5B4FC5F329CB64803EF6A55F0178D561729D29522DACC07085C55CC6437007479ED75C452A979DA06AAC3A5F19118431F5AC88BA849BE7AF25E262DBF93C7281694EA8BBCA2721B16A54A42C0851E046AA226456044C51EA0037A88459108AE6A3702026B29CB5C92DA2B3D627AB948738B5841D7AAA18AB74A4ECB00F64ECAA8D80AA6BBBBEACEA2868E264374A1AD23A5543DC1D55687460765469E5C01D12CA94CC86738A896E309AEA759AA74A8461063717522856520E1DCAAB3BF40AD70471C1788FD45A9C24048B5E8C3BAFB1BE9D905F25641100F5627A14C773971BFB402CB8A3B1C141621AC588B832473B0A0A6D937DB13A5BF40B8D3CD71566E58859CA1EAF792CC9368DB1460EAE61A92A32309E406D919878EBD088F6551C994ABAE06973FFA6B901040643C221CB378EE829564E42CC78E67B1E641F8BF30CB5C39D2EC340C3F0626CBBB3D8A722FD5C93349670CAE684E247CF3A24A68805217F91AA3698B9D18369D6F5B620C39739C911239388E0543DF2F137566897C26C08A2F58BF80B68D96BCC8CD28F59F1391997C9AD58285710A96461194F429920FC3837A581A0D38D7B717696C052D6C81E36E70828566B4487192846229B7CCA973A25DD548F27D610F921B2EA83A1050B3CF1D2785FA99004898026B2C8C443CC219199A9B86E2B3C2F8BEB5BC008D04CA40339472E4D11255B61B2C08C3B73B13C3AE39FE8FC5913A974A882C2C74157128CA1D4E8B86289A7266CC668DAB96F6AA5E43C09E8A262B721A2C02042965092CB3A70402449DEB40DAC61A0B0E3A1C65A7BAA5032ABEB165F5628A4C38041349519278272E5144B70913ADC1D3F93B42BA09A0C831EBC7369CEEBAA61F7A2E8208B19AB0F66A130165B2C2BAAA9819C860DD7A7BD829803E483A56C75D82CA96C277FACC4473201C40C080741B61BBE70C944B124C78A91F86A1DB6E9C18DF7AFD642652BE118061AB02B055DA9950B32732180E85AB3B56A79EA2073229AAEAAA79C1518E9F76570B949D04C9771220D8FBC1957A6A2C07855F6002D7EF48B098C627AA2A9C099199D2293AE687066E39CEA9C3FE321BEC52044DE7C0FD6998B4F7C9E8D4159E810679EDAA9F7B32DF0C161B5938229818FC93403CF48594FE5CD5DAA67CE412A9D7C5590998BD296AE9B77A536D023DC63B978D754D624A703435272FA2E18291E9BF45FD824137B4B98460891AEECBF4B97102F547D2BD43A00A57AFD60C3AE7A87083A1F1DAC601C41BE0212B98A4C999DC64FB51C2DA53A0B95C47DB9C9B7815C560226B373A78899A19A2C757CBF02A52025BDCF086CA3A2327FE66E9177864363C79B326501274BC3938F440ACCE63663B8F0704F960FE81C3B31020DE1102724E98150489AFD105111F15C2088BB66B1A05AC67EAC29BCB2555BD7B8763CDB45D3F0CEF417A5A24A62950A9050313EC6E6048E6A52D5ACB4B7041F2168A847FC696092210C848A1F0390A405B60FC83FF86931E6ECCD16199D31457951A249CB0C0D2C66507B7025EE8859D9CB23E733A07ACA1038F0A822717AE95C38128BBA019437F9A63BD6D7729EB4C873095CF1C3C1BD6A91B471B8C1510ACAA7A6F52A9899C180AA229AA9634CDDBA1DFF187D6FE61B8B8B8FA7D38D453471599B6FE4F9429683C0BDA6329F080C9053531E47A0B7B147D08B5C8BEABEE61530B8F88154AB5ABB09377D54A3595C9837888F19122F47B9C959070D8635261AA9099119411B1C54E6E527A3DA336054C4F14904F4280ED8E83D5994BBCD67BC67542348F80DFE300A75DC9926A6786B73245867AC95CA196B44B8F1277B57F5A09BB084DFDA3B7ABC3077289D8EB92A56B95A7A5842E14C425B74ACFBB9B4B49B5BF1908B963915584202E974641C8A7D5A8205677758B3814A0206C990CA1C927A0CAAA10DD0D1753A50B4E8CA01CB0A2291BCBFD4E4B51CEA83BF6B5D3EEBBB5974B689983BB3FC38688749FBC94352E4454CF0A8AE46A237E8336618117A7031BBF1AFBD414A736024636807DEE5C564E80213036630802906EAAE35210EE643ADC6750CB903274395546848C156D29E54FC986AC36E5AF218C32CA2DA3B2BC30B5E322B9128B0955783719310104F263A4BA88B835136665A3E76AABF513B15DE159225F8947664CBAEA9C83111B347AA1403E418EE30040918B9A44CB1755CAB81276A101A2E72EA8663F34CB4B318E5E183DE629DD4F4C3C54AA5E84C64441755A82A3B3949CE217A4F10C89F6AB925A37990FCE681154AAC701527318AC35C7912E001C8DBD2CB3BCCAC727B51AB6A7FE3B623D3811105792248DA05E536C90DA29CEAA9BA30D53795DB47641A9DAF273D52E8987EC84F43FA61858B437A4B0BE5822F456C2767F986F6E6850F177C3DB1A257B4AA24D5296C800AA2AB998899849C3C564966466B6CC38EFBBFB87AA3CADC0154FCAD6DE831765932A705C3CC21B51DEA5268343CAE394177768AC7D4914D6969B185200753415CC28FD1B5C22E3547A12348502C1C5D64335C604D7638722C3B32A394AE1FA1CB69D785A8B76CB039CD646143F71560AA15060C02349CAEE26D854F8C90C5861784C90D410E9F98FBE7D73B373402E60A4C12F1C651AEA71B0E5E947C207DBC41C486DA3ADC460D3319542D5785B039B6C7D3B40CF2CA8C41B404E781BAF86F3FC47C34A02D47924151", + "4BCC0DE7F66858B01BC0EDCA0988AD438FF7722DF56DF292D2717AAD9DEF54047EDCCBDF8814FE6A99FC10D85BC96622625E06561925D259D6B26F46260914BA55EFDE4D16DCDBEA3EEEC6B241409D111411383CFE03F113920742C8AE031D5496F671087A08B2A81BC88694680C43C7B294134E657DF7CBE70EAE81EC1E5239EBF1A33108ABBDA851B5396414B63FB6ADCEC6E23206CA8AA938A7657613C19A6FADE8ACE9245065072CA936682E636F61225098BC66E290A19956FEF98E429F2715FFDF546DBEA0F0AB94734E32FBBF9829326A9FF4A3AD0BAC8AE0666C3726EC9701FB3ABDF90FA73C571D550F1273573B904964903940BF9DBCBCA39A8B85C022FA71C1AB87120A4B826A2E11DDF86F4529549B1FB8493D21E6C8EDF8A4AF8D85DE45C7F2D49EF0A29F3FEE950ACDBBB083416C36178110ADE27FDC2AD14D2A7CA71AD7336F0E18E3C6AB2C92CC1C9500745DB989D6FA2999BFAB6228F4AF743F745F403D81A31A61E2FCE1C58C599390108EA416F66B14285E266827E86E8289AE756F8AFE684C9811EAB0EFCB13494872F15B5D72BB8CB80FF7F111B9DF3AB66D0C7CE40F1F688BBACE0899AC2EC85651AD2805AD65A7A451BE2CD9E47CA30646199AF03E0C29B10AE0B7015F0490739B27528198D78DDD537ACB4D110822D2D405917A2D6BCABAFCB1378ED1CE994B2E7A16280453728AE3525D2688F342951505C00B20C1A304CBD5DB81E0A70D9BD575B3D8E6FD4DE2774F5EF97EC7D00F7A6569459ED6E07ACDFD5860FB007E1A39D7501D35FE4BDC3048BE978845AC68FB239A249E8F4EADFB8CB016EEE0CD781CC53BFF488969BC3A483F5C0C919F726A75D546D74BA46A2A14780267D7D760446C393A792A33DD47164A0D7538C39EBE1AC9779C9195E6AE039C877A7197B70FB7DDD1E1247F26E4B45D0F889768BDA43C30165437467171AAB17B1032AAD853630D1EB25896F3FFC95AC45C679736A0075AE7504ECB345953AC81B467DB17E8CE168EB2AA161817DFEA2896E7D027FD69CAA76DDC6C5C0BAE04957F28AC505BB2F49944B1E2AC1C78B0BD5C996D294F23EDC62C0560209E589162138D4DC69024FC0FF925B6904BE1B4B7E3F4BEAAE65C5B7351091C32807A93DF9E4C5C783DB5DC99C8A6DC92C391A4FD692B23C28550862EBD3990BB3F82280F3DFB361F5B3A46A2CA2ED3206989A57ED172FDF3A53011F5F9B79CBCA3B3D3B7E6FBAE5BAF84815ED0AFD6695B9FB89FDB7278EFBE2C368F2130FD48A23A39F43358DA2829D17AA7E611CC29AFE1C319A683F557390C63B102A38F09F8C547C20EAAFB6F8583CAB0960C4C35474E0B48B1106E2D8B1010218FFB0F9B53FC7E91F0D762617C5DFC72CC7D2F8EFC5BA4C6150891CD51C56A699CFE8C3E5A1783F01D43DCB80381C18973AD9CE8E9823C32F73F93EAC679D8BCBB0B137A5274517E3432DEE29282A54EF02D73B11CB5FEBC6CB25ADA82E010320E43E6CFA15D924B1B0F4217C4A779BB1FFE8FE31E28CD40283C", + "D284C566FB6528F5F51881592944792309AC950031514025B7DBF0A87F3FB7D6", + ), // valid decapsulation - ("5D4A76278400850726C8EACEB001719719C1B8CA4CD23A4FFDF67CE738CE6BE11FD9B9C5867463EDD33AF8644021E2088FB9B23EF1915930C24DE1B5858334556AC154F2468AEC109A5212EEB4145C3A6A4B9C517FE97433247FB652B34F3176778913CDD8BD2C983CB8F723654A8CF65943AA57AAC17935EE5B1305E4B70C711736C27A10823739C57C52EA8135BAA8D0C5BA35661CAD2AB6E17CC1ED38C701C072BC78634A7BBF10389DF6B4A7B41787D833C4F5043635A6A5DA47388BD1126C310A43F80F0B678D7BD083DAE9028179BF23D4C4D548B5BE042E0523988A11A903102F3FC517ADF06AE7E6B585108CFFEB8229E969DCB5C6B521755BE4AEEA922702D188C3554FDAA6AC56317C4F55C945C7290168B09F9C637E286848976A93A51EDBE98184C78FF7035EFED6172A734F23915BFBC9509742687E158E1E91944CE22415F903899BB29D8A7AA34A4871D28FDF4B753547B55D6B89502A66EDF00FE3A370DFE66DD838094A14C39435A3EA21BA93474EE8C356F67471A0F130EF85B00F30A71BF488A139A6832A0A9BE2118296820658C711F53CD596C41753CD0741506EE47AC2BA204DE3732D689C7402CA5E91793C21717804700181B23BEB136B276C96E2103C8723B9F6A60C6B4CA7CA91A2B07F1A4BBAA6742F33836D61A2AE11CA3C0C203F8A331D5122B4128266718C44B26773B5680D1FF4BF3E5B2B61C93996290423B35999F89C393A5319864BAF36C3FB861C733B8055F184A9C43391184768D4AF3C19CAD7A69FE45C16353A2A541B8E0576CDC4B4500F59899A19CFF37436D9E21E076A388B1A77C89B9D7956CCC8B7BC99409FE7B1C2A40712FECC9141C616E2E22E7817756B59870DFB187A1952A6E1C6F2CA1FF79888401667C1FCB145E5361E8276E10BAA62326F9AD1342A60BE391C293AFC035C1998136C3210F2A6DD591A37917376230063B0C43610A6D048BE4E95908C707B0FEA370855495F2C30231754E3AA97C5A908E088750CB50EE3A4A1CC84A09B81789511CE08133932C7659EF1C445885B135B569D2379FEABBA8CDA47AF083BA605397C014AFF278D936525D964637AC9C11B33808D1009A962AC8A0632CE4C0550F5538B8C99BBF5C51D7A803380724AE14F8F1B4A40989E5B220E95B78EDE458779D263FB8993E4734C6282C523AB3341A0BF2365009FE01F0B7C686BF3C1BAEC30C8B68D1A28B06230C971E59BF98B53B800430138848C39CAE288AD39CB37D545BA5B000191855B8B593717A0986FA42A5D56337607385B657643B519ECAB7DBB20031BD9911CE5716B75A26575C009E76AE138CE4E57113A35390B22273BDB50F0E60D6FAA0A0699A12C1311289520938834D796B08D36B40A270489494636649F65AC8B282A1ACD2B298688395060903F86C5EC501A87165583C12F8F8A7636B25AE6A3661191A61E78417B9A1A0BF877197137CB5643E85277AD09D03192CEFF87441F999AF8983F271201F1AB6026837DFB936001A4600D564CEE3B4315622281721416588C666CA88A286110583CE40064D703050858A3C22B09A2E80C36D81FED324D162CC2EFE9796D65069F38416D3680D774B32204090FA719F24C1EDBE1B6B62C465F45A654E06AECC54D8FB32C6C539DFC02230E336DF7748B440900B3A83EE6826E05A8A29E4BBFCFA0CFA4ECA1B8139B2E53405DFA1BC7BA2DA9D5B56C3B5285029CB9D50A905808E1351A6E57CCE037C979D7531F4ABC0DBB76C943B78C473C08135AADC519EFF19140D3272916C4183C2E2EF25BD6C414B9411688B813EE947A48DA72286A8855F8BAB64718B73401F88153B4405583400C91D90171C1C3228258D4DAA1B17B02A1089AE543021A53AFC877AF5BAA3E6E1902B4B98CBABB6776837DC140C42433CB33D383B842A45CC4907E43500BD956D3C0130089AEC3C1CF7E863ACFE4960D790A8D9CB4AE0076A92169FFA1CCDA7A333BCCBCF2948C3134623D54A83D7638C6695C6E9296849904F1744E125B8F3B1943C23318B3A7BDB4D44131A3AA796A24F4833B975C00EDAA085281336BA75C90D21FA7291F12E38AA2D5834B1003CCD6000CF2400AA85CD3974D0BC8C3A7CA19AD0139B5B08DB9224B12461F1C0BC96E3101A516BB5C9682257986F3A98AD2786302B054F12719BA1845CBCABBB1A50BB748BBC020BBFD293A14F8B01EEB0917A7BD6B6682A4389EA62B2A39D8346C76ABCF07CB2BEA4D7E4192B5BC2D9BD9B83F374512B76895C5038373C7A6E27543930C88AB4213C13C21F207D8625784C576B5887508A106FAE6B70DDBC1BC3BABFEB7273E8C3BE893CB116342C308CE3DD1B97920498938887BFA07E12C5B7A69B8E6F7A263E8182E884AD0E99C084431DBF6CAF9A44D13FC00BA58C109DCCE67349F337A4B0F3C6ED4783AFD19B91FB27490A896816474EDC41E6411B5D2AA1388C07556A3065FBBB4457301EE7632A6479AFEB03FA7DA46715443C6523235A25D62FB755063C016B4CCC50AB2E043C8058108B77A58A8E4AB9A7B3E2764C48A3729D295BF1ADB2248DC2EE33907A21693B0CAC248C2CE71D833EC80C97AB2CAD915442AD441BDDCB2EDF5B47D461B6DD83147D42ED86848375278A92B55EC885FDC2881ADFC885FE78B4910CAEF778E147532F9C74F141BAAEB3C2A0C80A870B36213E5A089127010A889EC8CCACAB005B0B2A508078E57A94A57A03ABAD12FE6D8CFFCC95AFCC6AB7F868E545271B840092FDCA5C54BBCB3A8A239D83C63A4365773B6718494D99C4EFD6C4DAEE04B7033C28E7C97F3965960889241E6B255654E1ABC3BC7D88974113891A221399B44CA0A5E3F49C791F490AF0C60785B0B62E10AD1667E8A6B5B5D812AC35159064B6F0B300A06744B0266BD1D589450F997C3DC703A73108CD01E7E79342C525F715C59E479190F53B4BBE6AD6D367027DB6FE5F615E84B2B0CF5889F9B2193D38F28C74DBA699BE15781FEE9686684638A843656C659E1FCC9F1FB0BCCCA1CF36B021C67B855A95B16C8B4D464148D5BB1422A169E68C4FFB186F7E308219255E5309F60B00DF321A3A39711BC019A5CFB0170C476094A6D12C59897769EE4BAB22CBC882B846E8057AA4F63336141B2DCE100A14029B153599558163CB5079D5853F5552321529CA11194D3934EDB98A466367460558A1C662DE04C9B794AA51879AB77019250F39C988A86A59922763C936B901D57150150C3B2D976A26B42CB0AA43BB3FCD71387FF3E755CC13628E14CA83359D65EFF03DCC9A822968982BC76FF866D65B941AC37B2B2806768A1034592FE684BD1B77BD666942077FFD67F0FC9F5DD2413D8C735CBB9603DD69ADA92C59E27072231318C9D798D80997ABA", "A16694D2B041768250DF8BD9BB6918AA9C9049C8C54963FA534A7E867551AA39B4EAD86B8405917145ADF1E9DEBD6DA0A749535D11916F652FCAD1A8F11A921F2FDC4E52267B85A4AD7FC39A977C119CECD48698C96C48848C6D45B023A208BB9277DF8A66D3C0785461E99D2814437B6B470413C0BA148B4A236D1E25054BCDA5C2C8F97076C7D2360FD4E976D84D5081750523E081E4B236B041EF35770EFF5969DD7778654C93B26E83E91292FB2C9DDA23B7D5FB1365363EF7B0D25683F3A606751602853BA9F88DDE18432865AA27E9B5F1170E681B2B86A9F89CDDDFCC2A5D1E05C6E23ED81A794DD0A4E699EAEADF43FC5A1E159D3DF744E28535A395EB521172542EC68E2C7D7B6672AF67356BD61AE2406F49E7360FBC5791E9691F510E034BB79E2FF5081E8052015067EF13E65602FDAFE6995A52881FA5C76659EA35EC5B4A5ED8801166D983E4B74692919ED8CE032471DD0F6DF5953AB47555765124E696BE148467F25700A08B1BAFCC907020AA108FA15CEEE6D71EE596293353A0F26FD387861A98F6CB8AF0A79F1172247114BEB3D2313B57EBEF1156E478BC109ADE1D374B92619DC2DE94CEFBA62F332E5FFCD3E744139F691334A09092FA78438006DFD06C63CA823CB837AF7F1F650D08CE81B46B0BD1B2517D51DB8390BFAD88D3491936F3C9008A8F0627AB432403D51E1AB27D541405A3639366D0305349C76F9C46A5AEE92784D7620B7147721481032E8A5EFF79C873860A4AC7E9D6D3851A105DD93A316C84AC048F4E8D0B31B87DCFB3C6A4E8F41A0303C62C44BB1E8D718FEABDACA9A665B822BB9F60B9B9FB44F5964B41A5EDFB2B7A4334EDF2C19A5D909043FC2D2437298A35AEC686298FDAAE84C5D97BC97E3CD8D9F950DA9AF8731FC3FEF784181E1EF745335B5B3A672397479BD5B07D30735457C39FBA80B65461448581610AB4C9EC084C6795B7E3DFCE83D229C166264ADED2BF4544EC62E9CF4605E5C9BA7C766FE6F5319520E1A2D0039ACEE637882A5FF545CDAF7C9EDDE23D8A22D5D84B70C822C71D1D4BC39EB17E623A964D895709AD62ADAB86AD477424DD04A199B473497A4949ECBC285A6066CEA0E49336FCD2FE6DBA478F08139766B3359213A0573E54E4E3178ECEC39465C01537250661CD68A235E1B622EC028B5BD1F016BAB8C9FB6BDBF2CBE4749F3B3A3F5D035611AFAF136A8B03ACFEF99D7B6EA1353EECF5DA28EC02107D1F9093B78ADA69A7866AF4322FEBA92CF3941402EFDE68443DFDC14D9C94A4FC8118F53740C3C239CD017E1BEB6D41593D27484D692527D59CB10FBC5593398B587563AE36639FE411527A7F6774D843323911911EAFA854AF383CFC66829F7D255079FA11E88ED8F1EEF5D658566BB123621C13044ACBEFC9BC722B99123011576B748A4F0E54CA39F5A96136B3F98265FCD394FC8C743C82F3EBC3CCDB1A0591525F1581688BA93CE2CCFF5AC9B0E109BA774D9035FE744D28397B9CC8446DDABD91A43FD73EA0C8CBF9", "D37D67DE69A313B472D9CDDE6F9540A5DCC04FAE34F3FFB523A29B979453F62E"), + ( + "5D4A76278400850726C8EACEB001719719C1B8CA4CD23A4FFDF67CE738CE6BE11FD9B9C5867463EDD33AF8644021E2088FB9B23EF1915930C24DE1B5858334556AC154F2468AEC109A5212EEB4145C3A6A4B9C517FE97433247FB652B34F3176778913CDD8BD2C983CB8F723654A8CF65943AA57AAC17935EE5B1305E4B70C711736C27A10823739C57C52EA8135BAA8D0C5BA35661CAD2AB6E17CC1ED38C701C072BC78634A7BBF10389DF6B4A7B41787D833C4F5043635A6A5DA47388BD1126C310A43F80F0B678D7BD083DAE9028179BF23D4C4D548B5BE042E0523988A11A903102F3FC517ADF06AE7E6B585108CFFEB8229E969DCB5C6B521755BE4AEEA922702D188C3554FDAA6AC56317C4F55C945C7290168B09F9C637E286848976A93A51EDBE98184C78FF7035EFED6172A734F23915BFBC9509742687E158E1E91944CE22415F903899BB29D8A7AA34A4871D28FDF4B753547B55D6B89502A66EDF00FE3A370DFE66DD838094A14C39435A3EA21BA93474EE8C356F67471A0F130EF85B00F30A71BF488A139A6832A0A9BE2118296820658C711F53CD596C41753CD0741506EE47AC2BA204DE3732D689C7402CA5E91793C21717804700181B23BEB136B276C96E2103C8723B9F6A60C6B4CA7CA91A2B07F1A4BBAA6742F33836D61A2AE11CA3C0C203F8A331D5122B4128266718C44B26773B5680D1FF4BF3E5B2B61C93996290423B35999F89C393A5319864BAF36C3FB861C733B8055F184A9C43391184768D4AF3C19CAD7A69FE45C16353A2A541B8E0576CDC4B4500F59899A19CFF37436D9E21E076A388B1A77C89B9D7956CCC8B7BC99409FE7B1C2A40712FECC9141C616E2E22E7817756B59870DFB187A1952A6E1C6F2CA1FF79888401667C1FCB145E5361E8276E10BAA62326F9AD1342A60BE391C293AFC035C1998136C3210F2A6DD591A37917376230063B0C43610A6D048BE4E95908C707B0FEA370855495F2C30231754E3AA97C5A908E088750CB50EE3A4A1CC84A09B81789511CE08133932C7659EF1C445885B135B569D2379FEABBA8CDA47AF083BA605397C014AFF278D936525D964637AC9C11B33808D1009A962AC8A0632CE4C0550F5538B8C99BBF5C51D7A803380724AE14F8F1B4A40989E5B220E95B78EDE458779D263FB8993E4734C6282C523AB3341A0BF2365009FE01F0B7C686BF3C1BAEC30C8B68D1A28B06230C971E59BF98B53B800430138848C39CAE288AD39CB37D545BA5B000191855B8B593717A0986FA42A5D56337607385B657643B519ECAB7DBB20031BD9911CE5716B75A26575C009E76AE138CE4E57113A35390B22273BDB50F0E60D6FAA0A0699A12C1311289520938834D796B08D36B40A270489494636649F65AC8B282A1ACD2B298688395060903F86C5EC501A87165583C12F8F8A7636B25AE6A3661191A61E78417B9A1A0BF877197137CB5643E85277AD09D03192CEFF87441F999AF8983F271201F1AB6026837DFB936001A4600D564CEE3B4315622281721416588C666CA88A286110583CE40064D703050858A3C22B09A2E80C36D81FED324D162CC2EFE9796D65069F38416D3680D774B32204090FA719F24C1EDBE1B6B62C465F45A654E06AECC54D8FB32C6C539DFC02230E336DF7748B440900B3A83EE6826E05A8A29E4BBFCFA0CFA4ECA1B8139B2E53405DFA1BC7BA2DA9D5B56C3B5285029CB9D50A905808E1351A6E57CCE037C979D7531F4ABC0DBB76C943B78C473C08135AADC519EFF19140D3272916C4183C2E2EF25BD6C414B9411688B813EE947A48DA72286A8855F8BAB64718B73401F88153B4405583400C91D90171C1C3228258D4DAA1B17B02A1089AE543021A53AFC877AF5BAA3E6E1902B4B98CBABB6776837DC140C42433CB33D383B842A45CC4907E43500BD956D3C0130089AEC3C1CF7E863ACFE4960D790A8D9CB4AE0076A92169FFA1CCDA7A333BCCBCF2948C3134623D54A83D7638C6695C6E9296849904F1744E125B8F3B1943C23318B3A7BDB4D44131A3AA796A24F4833B975C00EDAA085281336BA75C90D21FA7291F12E38AA2D5834B1003CCD6000CF2400AA85CD3974D0BC8C3A7CA19AD0139B5B08DB9224B12461F1C0BC96E3101A516BB5C9682257986F3A98AD2786302B054F12719BA1845CBCABBB1A50BB748BBC020BBFD293A14F8B01EEB0917A7BD6B6682A4389EA62B2A39D8346C76ABCF07CB2BEA4D7E4192B5BC2D9BD9B83F374512B76895C5038373C7A6E27543930C88AB4213C13C21F207D8625784C576B5887508A106FAE6B70DDBC1BC3BABFEB7273E8C3BE893CB116342C308CE3DD1B97920498938887BFA07E12C5B7A69B8E6F7A263E8182E884AD0E99C084431DBF6CAF9A44D13FC00BA58C109DCCE67349F337A4B0F3C6ED4783AFD19B91FB27490A896816474EDC41E6411B5D2AA1388C07556A3065FBBB4457301EE7632A6479AFEB03FA7DA46715443C6523235A25D62FB755063C016B4CCC50AB2E043C8058108B77A58A8E4AB9A7B3E2764C48A3729D295BF1ADB2248DC2EE33907A21693B0CAC248C2CE71D833EC80C97AB2CAD915442AD441BDDCB2EDF5B47D461B6DD83147D42ED86848375278A92B55EC885FDC2881ADFC885FE78B4910CAEF778E147532F9C74F141BAAEB3C2A0C80A870B36213E5A089127010A889EC8CCACAB005B0B2A508078E57A94A57A03ABAD12FE6D8CFFCC95AFCC6AB7F868E545271B840092FDCA5C54BBCB3A8A239D83C63A4365773B6718494D99C4EFD6C4DAEE04B7033C28E7C97F3965960889241E6B255654E1ABC3BC7D88974113891A221399B44CA0A5E3F49C791F490AF0C60785B0B62E10AD1667E8A6B5B5D812AC35159064B6F0B300A06744B0266BD1D589450F997C3DC703A73108CD01E7E79342C525F715C59E479190F53B4BBE6AD6D367027DB6FE5F615E84B2B0CF5889F9B2193D38F28C74DBA699BE15781FEE9686684638A843656C659E1FCC9F1FB0BCCCA1CF36B021C67B855A95B16C8B4D464148D5BB1422A169E68C4FFB186F7E308219255E5309F60B00DF321A3A39711BC019A5CFB0170C476094A6D12C59897769EE4BAB22CBC882B846E8057AA4F63336141B2DCE100A14029B153599558163CB5079D5853F5552321529CA11194D3934EDB98A466367460558A1C662DE04C9B794AA51879AB77019250F39C988A86A59922763C936B901D57150150C3B2D976A26B42CB0AA43BB3FCD71387FF3E755CC13628E14CA83359D65EFF03DCC9A822968982BC76FF866D65B941AC37B2B2806768A1034592FE684BD1B77BD666942077FFD67F0FC9F5DD2413D8C735CBB9603DD69ADA92C59E27072231318C9D798D80997ABA", + "A16694D2B041768250DF8BD9BB6918AA9C9049C8C54963FA534A7E867551AA39B4EAD86B8405917145ADF1E9DEBD6DA0A749535D11916F652FCAD1A8F11A921F2FDC4E52267B85A4AD7FC39A977C119CECD48698C96C48848C6D45B023A208BB9277DF8A66D3C0785461E99D2814437B6B470413C0BA148B4A236D1E25054BCDA5C2C8F97076C7D2360FD4E976D84D5081750523E081E4B236B041EF35770EFF5969DD7778654C93B26E83E91292FB2C9DDA23B7D5FB1365363EF7B0D25683F3A606751602853BA9F88DDE18432865AA27E9B5F1170E681B2B86A9F89CDDDFCC2A5D1E05C6E23ED81A794DD0A4E699EAEADF43FC5A1E159D3DF744E28535A395EB521172542EC68E2C7D7B6672AF67356BD61AE2406F49E7360FBC5791E9691F510E034BB79E2FF5081E8052015067EF13E65602FDAFE6995A52881FA5C76659EA35EC5B4A5ED8801166D983E4B74692919ED8CE032471DD0F6DF5953AB47555765124E696BE148467F25700A08B1BAFCC907020AA108FA15CEEE6D71EE596293353A0F26FD387861A98F6CB8AF0A79F1172247114BEB3D2313B57EBEF1156E478BC109ADE1D374B92619DC2DE94CEFBA62F332E5FFCD3E744139F691334A09092FA78438006DFD06C63CA823CB837AF7F1F650D08CE81B46B0BD1B2517D51DB8390BFAD88D3491936F3C9008A8F0627AB432403D51E1AB27D541405A3639366D0305349C76F9C46A5AEE92784D7620B7147721481032E8A5EFF79C873860A4AC7E9D6D3851A105DD93A316C84AC048F4E8D0B31B87DCFB3C6A4E8F41A0303C62C44BB1E8D718FEABDACA9A665B822BB9F60B9B9FB44F5964B41A5EDFB2B7A4334EDF2C19A5D909043FC2D2437298A35AEC686298FDAAE84C5D97BC97E3CD8D9F950DA9AF8731FC3FEF784181E1EF745335B5B3A672397479BD5B07D30735457C39FBA80B65461448581610AB4C9EC084C6795B7E3DFCE83D229C166264ADED2BF4544EC62E9CF4605E5C9BA7C766FE6F5319520E1A2D0039ACEE637882A5FF545CDAF7C9EDDE23D8A22D5D84B70C822C71D1D4BC39EB17E623A964D895709AD62ADAB86AD477424DD04A199B473497A4949ECBC285A6066CEA0E49336FCD2FE6DBA478F08139766B3359213A0573E54E4E3178ECEC39465C01537250661CD68A235E1B622EC028B5BD1F016BAB8C9FB6BDBF2CBE4749F3B3A3F5D035611AFAF136A8B03ACFEF99D7B6EA1353EECF5DA28EC02107D1F9093B78ADA69A7866AF4322FEBA92CF3941402EFDE68443DFDC14D9C94A4FC8118F53740C3C239CD017E1BEB6D41593D27484D692527D59CB10FBC5593398B587563AE36639FE411527A7F6774D843323911911EAFA854AF383CFC66829F7D255079FA11E88ED8F1EEF5D658566BB123621C13044ACBEFC9BC722B99123011576B748A4F0E54CA39F5A96136B3F98265FCD394FC8C743C82F3EBC3CCDB1A0591525F1581688BA93CE2CCFF5AC9B0E109BA774D9035FE744D28397B9CC8446DDABD91A43FD73EA0C8CBF9", + "D37D67DE69A313B472D9CDDE6F9540A5DCC04FAE34F3FFB523A29B979453F62E", + ), // valid decapsulation - ("EBD9BF0F0C7E35A28EBD907F0A96AF908724E84BBF71A71ED30C0B7EB9AC5B936FF7B02FFEF23CEBD73278B4B1738C70558C0A69058044582C046001008130F8F1017FF9150F5662BDC479AFE43BC8051C657BB7897907D0928611A266E4871801D7C11579348212BC385A7A6D7673CE752D2779B150994064C19B97436C9C15371F468F0831B8A51B7100D46D0244487F92302189761432B865A846FAAC8DA3C9222C5641B93C44E80375F3333EF797197CF890DB44123589B394BC70241C73027B7A417565FD703B8346388C47BD411C257B6976301B7DC20C89752503A00A4153E1112A6031B3204936CA1A70D491B9025334FA9E38223CD2E15259E48E31483344A965052605BCF1ADA0C8CA18E1300D672894B670D5C004025289F7F163BCD533A349C4AAA62854E63906B8638FCA1E755C82BFD85E79A982875B40C4D88B4BA79177922A8CA26FA5D8237AD67DC19A753406BEEDA1812B7792BCA66050B9C532124AF2B2315DF3578A080956FC5F60D854A216CB70348D0B574C7DB83315C7092C02426E67B53175CC671510DEF090C14BA47BF12E1C2548047B43C555036E9ACF6F7440D18750161B7DCE6C942E6B4855080DC0800B4F0BA913AA2E32A47080D5BD04D6CD306A26E68CBC42E864B96090C15185D7821C795A502399435D153883ECC30E3BAF0DDACA3F910301A624E3684A43848E64C47CCF4A7F33170B78735DD6E587D29B943126814D7A965E541EAC78ABD1D9196F651E851A6DE9B638DCB47CF0F6502D71AC4CD4042113B9503690449358C73B5CCF9035FD4A15C4E55269E04A55626F87D60098C9805FA6300797771C21AAA4B4B7FC411573095EF6D08550D9251AE43B0E4359BE7778F595823BF6688561496596788617B019A09C5B71B1EF19231F255C1BF72AADA38D8106B8D902BD70830A331A1E5D581477949DEC3315875292EF7222E40C708C805C8D7A7F531496A98A53A9216256D4CC59B4454470A48A9993F7BA6AE88C3A2669A064E50F4C5690C8810C926C833AEA026812C59D48AD74EB9CBB5367E6040F90E872E976274E69A23A72AD1BEAABFFB4A8D6A4C5008DBC207B4B2D56BAF399BF1EA4364D2179744B926F76137E4541B95362EC5AA3DDDB642980A3A3728E7991558BB15B3EA77256AC4D6DE570A0370C009C8DE9917F8AE663CB58A8C1F0AB339835D6C5A5D328C6F9F7A4A07A784AFAA7A371902661820C840BEB150BD180BEF2E1CEBCB986B7A0C81A44B7C3FA527E648D4152BFD658AEABEAC95CFC9310C580C88A85B0012609250D66B22E376A43F63A12711B275B97C08733A86D079CD2455E9377777BC4435AF8CCA2D531C00B0A6EE92E3194972FE08D0A27364EE068D9E7CA49B6491C2C6967EABD16B0770294A5DF8BA0AEAA7A796A1E34DAA4C028CBBE01650D9A6F5C26A524E76B76F0BDE8CA79B811992E76B696C038C1821E77324DD312054974AD87AA5BFBA6557314524D3274FDCAA95639269F13A24437ABA0419959C1AFD6604DC39137A59865A7E044FCDA114D38BE9D7231415625629397612BCA26774A05BC5C72A258CA2BC579BB0F2ACB700787B4CD8A318E31A48B83618C79A4ACB1C852F350265354B51B9755473597205D52441485326CFA6601554214150A1D2C20902E793376D97C9710255AC45DA5147897FB5C25B5977FFB75107B8072C630AE5B4488316A4734540A462B822B635199ABF7780F1E42BABB2705D7491B175291309C059EC10523B32950A67ABBEB7F37A79CEBF802969532D31654196082890518DB43766B352F9C83BBF74C2BE1C205A5D15D9F8128150356FED68CC5B83C67758D87B6046CF44C13962CA1FB477A856A35B789CEE9757E01ABDA2B15399C4F9AD80711175122309558514D087A576998412A7553D5FB831234078FE3BB83AC0F554162002A11B83998110341910B3013A07FF01616B1F4453A59A0BCEC7948467940512C2DCCBF056749892BC046E33E61687EBA021E64F09B58A056EC170F4D9B7956AC4BBFE99A6A4C56E557C4349C87E557912F879CF11942F268A956A636E8B61FD12058F8D41A868A35083555F89B965691131827C0F3CA2A6999336B823971D237D881A8B1415C2B2396383A2FC036CBF70B18EDB71B3E6C60B1D5AA1C490C99788D9E4B2BFF5857F1615BE961569FA38D71958BFAA9958DF13963543C32F9A53E31ADC433B8509469AA8C8C429047E74C2BD2094836203288B34CED08BA917BA06A14558CC54F62E06121F68B40502417469B93BABAAC39C19C439872975DC155689A41947D526A7B30A1346C3B91F0A272A8B34CBBC127CC4945F5440DC1C97E38276CC754769CA796608AFED1AB483CACC7759495B8739D109ACBA36EDD03696C85C7D219496F84422F3683FE639D3924B93543335F7563C8AA1196932CAD2BC04ED6796DA35CE0AA658B35167492C09428108650194B77C880761C8B94A6CA368287955264723280951CF85AC7E188921AD870AA36A82D62BCF9B78506C065C97C999CB4501A152330A532F2E67B3A89B15D06461DD6CC6E83865CF01AB13B4D890A3D9F573002352BF432A33A035ADE976AF6855A57193CEA9514AA08601AA07CD8560476C2C8E1D8622B36891F8714107C9970B310CCD4078C7090EC90A8035466E1443F59A044747CAD9E27A842893A2AD72F684CB2C7410E929112AAB470A5C86039C4A0FD193BF8E42521A6267557B6F1B7717401BA5AB79436D0508999697EE4590FB569926C15B13C83AB9755E389446290B808054924103B86D4C69DD2AD53285EB9C50BE417815F3B1C0C71A735985FC443796CF5433102A60AC9326F8A1E5F65B4526B225391347BA78D25A010B080C63DDC644FB629EDE699B8E123ACEC10C391A449B886E6997E50382F46C9BA437A36314C39F2B19A345BC9E505B91F9A844A8C6A3AE9C930C09E050202B6447D4B9A3277553055946D259976553468F9CA6235316FEB9C3CE5814B6C48CCBE26A81244225D63C44592ADE0C199EDC18CB3A63323B31389489C4476CDEA081F1A8383CB304067D88DE9083AEBB7822480C5CA1558B250AAE1C6CBAE2482A64834E9F0C47C33AA7E1C39C90264624AA24113A758702C59820011286FB6F0B0C42461A1D070B1C75502EC3A6EE92D1CACA070118D76CC34E3570A3E617B9E4376256104B06A61911856108C1A6AF28EA7A7313A65BD91B1897293275BD93E20B1549A03C50C48A25B0712EA0A9BFFCB6A25855060833032193872D9FD9EF1EC489A46DC5E2F0A66697CDCA86E60EE06B16D0414E821CAED22B0427C650EB0D8349971A6A9B31D59C1512CFDCAB613481B239FD0E65FBFCFFDEA427BB8F9BAEA92394732DCAE11A68957B0A71A11", "DC44FF01668B17FDF5E759C911C44A60F7D4124BEF02F6CAC9D3055207AB607A549FB911A8F8A9034EE3A026AED6DBFE8CABFBA24F9898B415A2FF15FA6A54412F335EF02F27681584584AE9D6AC58264566D750E28B23C0048882CE74F2833F159590CE14F32CCC564CA81C0FC3829ACFD1B22ED3017C3D0C66C4309C2C89F45BAC2E29DE5E351C27D4B16FE3A4BB32791C244ED80AA5903D02097285C5D89C03B998D5DB1721160F25DA76F50DF5907E4C20880575DCD5825ED893F6311204A3049E7B9C9D866719A282545CDA8C7640C26F395E0BBEB26EAECEA1D80BB3C95BEF62C8E3CD05358095C42CFAC2D5DD74C741CDE684752683493AD302E6E3F16890D0ABE0839F9D107599C1789E30AE52AC1EC9E89E077CDB982CAD9280DE02C002740872D56495252FC9B80892BE3BF443597FFB32662BCDE201F704C56B2361E33FD474BCDC438D7D6E80656D19EE068427944C5A8E3F5A42156E80B004B57B5C319C5D6D815A543CE4522BF86D193E42F50429BDE0E55AD5A2914C809825DC4CA1EBD8F295167D7EC8A8652B3331EDD979785CA7053FAF8389B9D6188B62D569C547BEE798AE3AE89FFB0C06238774DBC2E97A2602780C5D7A33C099A91A4DF6FD19D15291F10D7D01675EE249C7A1C991DC352C900705DCCF0AEF2288A9992128525D191AD1D450C7832C913E45E1D2EFE49540E0813AC4D1D75D14631E2A211D3A2D11F0493C6DC1003D02690B583605BC2161AC62D2879AABE900008D287C256465CE3A7AA40A155383F006C786CE4D84B22092D3CD31003F6A7AFA023133B680CCA51B12EF4F49DC2160A6FFBC80F56FE7A5BE0AF9BC0DEB2B17C9F56A4DF1BB544F51F0718D93AEFAAA7D734FCDC8202529C2598B0F0125F1DE7F027DD65B7628A47DDB7619EFEBC1556536E175E956BDCD60559F539F1E9DEECE53F54B2313403E0821C1B61FAA2B2A57F52101A9858911BC4D885BF525048039CF64BD93898D4425DD35B0105BB86635867E2D6F7F219EC5A704839EFE5A477CEF515B8C5F426AB710D1E337C7834B878A7546112E30B8B44839DD7CE9858D350225D36FBC7D1122EE64F5691A754ED3CAD7B9CEC2EF31164D4CD4101097F2787AA6E5C61CE4CA22CE0A969F7641F70903258033897D2407629368DA9825C20CD2A8A6069F0A679DE9F4B8EA9B30DAA1A9EA0EDDCAF7D3BB1FA13FD67429CDF62B140A8106A3B969E96E3120C7B7329FD4D650889AFBD3B89DF961993D63CCD8993073BCD82BF3ED1382989FE43A5AE097744CAB819E3F2DC4450A8961A8491657665B6CA9230B109F6DDA3AACA4C18ADBF032EF3A6F2C7B693062EDEA61040AC3A8B86CC0A76D54F5F42D48378FB4692B2217B33A654CAA983BEFBE870B894494CD496FEF77E1E4796418F65C3CADB970E7BE08F504AF8B915DE8A90398F3A3FC8B184C46EF8C7011B42FDC152FC53DFA98150299171741455328E729F288F41723D4F8B06B07A5F247B3F200AA604B1F672DBEFB5C8D444C6314B2949E6FC403", "22B94A413BE8118640CE786A2107C2AED1461082A1FEBEF610C6C1DA597171AF"), + ( + "EBD9BF0F0C7E35A28EBD907F0A96AF908724E84BBF71A71ED30C0B7EB9AC5B936FF7B02FFEF23CEBD73278B4B1738C70558C0A69058044582C046001008130F8F1017FF9150F5662BDC479AFE43BC8051C657BB7897907D0928611A266E4871801D7C11579348212BC385A7A6D7673CE752D2779B150994064C19B97436C9C15371F468F0831B8A51B7100D46D0244487F92302189761432B865A846FAAC8DA3C9222C5641B93C44E80375F3333EF797197CF890DB44123589B394BC70241C73027B7A417565FD703B8346388C47BD411C257B6976301B7DC20C89752503A00A4153E1112A6031B3204936CA1A70D491B9025334FA9E38223CD2E15259E48E31483344A965052605BCF1ADA0C8CA18E1300D672894B670D5C004025289F7F163BCD533A349C4AAA62854E63906B8638FCA1E755C82BFD85E79A982875B40C4D88B4BA79177922A8CA26FA5D8237AD67DC19A753406BEEDA1812B7792BCA66050B9C532124AF2B2315DF3578A080956FC5F60D854A216CB70348D0B574C7DB83315C7092C02426E67B53175CC671510DEF090C14BA47BF12E1C2548047B43C555036E9ACF6F7440D18750161B7DCE6C942E6B4855080DC0800B4F0BA913AA2E32A47080D5BD04D6CD306A26E68CBC42E864B96090C15185D7821C795A502399435D153883ECC30E3BAF0DDACA3F910301A624E3684A43848E64C47CCF4A7F33170B78735DD6E587D29B943126814D7A965E541EAC78ABD1D9196F651E851A6DE9B638DCB47CF0F6502D71AC4CD4042113B9503690449358C73B5CCF9035FD4A15C4E55269E04A55626F87D60098C9805FA6300797771C21AAA4B4B7FC411573095EF6D08550D9251AE43B0E4359BE7778F595823BF6688561496596788617B019A09C5B71B1EF19231F255C1BF72AADA38D8106B8D902BD70830A331A1E5D581477949DEC3315875292EF7222E40C708C805C8D7A7F531496A98A53A9216256D4CC59B4454470A48A9993F7BA6AE88C3A2669A064E50F4C5690C8810C926C833AEA026812C59D48AD74EB9CBB5367E6040F90E872E976274E69A23A72AD1BEAABFFB4A8D6A4C5008DBC207B4B2D56BAF399BF1EA4364D2179744B926F76137E4541B95362EC5AA3DDDB642980A3A3728E7991558BB15B3EA77256AC4D6DE570A0370C009C8DE9917F8AE663CB58A8C1F0AB339835D6C5A5D328C6F9F7A4A07A784AFAA7A371902661820C840BEB150BD180BEF2E1CEBCB986B7A0C81A44B7C3FA527E648D4152BFD658AEABEAC95CFC9310C580C88A85B0012609250D66B22E376A43F63A12711B275B97C08733A86D079CD2455E9377777BC4435AF8CCA2D531C00B0A6EE92E3194972FE08D0A27364EE068D9E7CA49B6491C2C6967EABD16B0770294A5DF8BA0AEAA7A796A1E34DAA4C028CBBE01650D9A6F5C26A524E76B76F0BDE8CA79B811992E76B696C038C1821E77324DD312054974AD87AA5BFBA6557314524D3274FDCAA95639269F13A24437ABA0419959C1AFD6604DC39137A59865A7E044FCDA114D38BE9D7231415625629397612BCA26774A05BC5C72A258CA2BC579BB0F2ACB700787B4CD8A318E31A48B83618C79A4ACB1C852F350265354B51B9755473597205D52441485326CFA6601554214150A1D2C20902E793376D97C9710255AC45DA5147897FB5C25B5977FFB75107B8072C630AE5B4488316A4734540A462B822B635199ABF7780F1E42BABB2705D7491B175291309C059EC10523B32950A67ABBEB7F37A79CEBF802969532D31654196082890518DB43766B352F9C83BBF74C2BE1C205A5D15D9F8128150356FED68CC5B83C67758D87B6046CF44C13962CA1FB477A856A35B789CEE9757E01ABDA2B15399C4F9AD80711175122309558514D087A576998412A7553D5FB831234078FE3BB83AC0F554162002A11B83998110341910B3013A07FF01616B1F4453A59A0BCEC7948467940512C2DCCBF056749892BC046E33E61687EBA021E64F09B58A056EC170F4D9B7956AC4BBFE99A6A4C56E557C4349C87E557912F879CF11942F268A956A636E8B61FD12058F8D41A868A35083555F89B965691131827C0F3CA2A6999336B823971D237D881A8B1415C2B2396383A2FC036CBF70B18EDB71B3E6C60B1D5AA1C490C99788D9E4B2BFF5857F1615BE961569FA38D71958BFAA9958DF13963543C32F9A53E31ADC433B8509469AA8C8C429047E74C2BD2094836203288B34CED08BA917BA06A14558CC54F62E06121F68B40502417469B93BABAAC39C19C439872975DC155689A41947D526A7B30A1346C3B91F0A272A8B34CBBC127CC4945F5440DC1C97E38276CC754769CA796608AFED1AB483CACC7759495B8739D109ACBA36EDD03696C85C7D219496F84422F3683FE639D3924B93543335F7563C8AA1196932CAD2BC04ED6796DA35CE0AA658B35167492C09428108650194B77C880761C8B94A6CA368287955264723280951CF85AC7E188921AD870AA36A82D62BCF9B78506C065C97C999CB4501A152330A532F2E67B3A89B15D06461DD6CC6E83865CF01AB13B4D890A3D9F573002352BF432A33A035ADE976AF6855A57193CEA9514AA08601AA07CD8560476C2C8E1D8622B36891F8714107C9970B310CCD4078C7090EC90A8035466E1443F59A044747CAD9E27A842893A2AD72F684CB2C7410E929112AAB470A5C86039C4A0FD193BF8E42521A6267557B6F1B7717401BA5AB79436D0508999697EE4590FB569926C15B13C83AB9755E389446290B808054924103B86D4C69DD2AD53285EB9C50BE417815F3B1C0C71A735985FC443796CF5433102A60AC9326F8A1E5F65B4526B225391347BA78D25A010B080C63DDC644FB629EDE699B8E123ACEC10C391A449B886E6997E50382F46C9BA437A36314C39F2B19A345BC9E505B91F9A844A8C6A3AE9C930C09E050202B6447D4B9A3277553055946D259976553468F9CA6235316FEB9C3CE5814B6C48CCBE26A81244225D63C44592ADE0C199EDC18CB3A63323B31389489C4476CDEA081F1A8383CB304067D88DE9083AEBB7822480C5CA1558B250AAE1C6CBAE2482A64834E9F0C47C33AA7E1C39C90264624AA24113A758702C59820011286FB6F0B0C42461A1D070B1C75502EC3A6EE92D1CACA070118D76CC34E3570A3E617B9E4376256104B06A61911856108C1A6AF28EA7A7313A65BD91B1897293275BD93E20B1549A03C50C48A25B0712EA0A9BFFCB6A25855060833032193872D9FD9EF1EC489A46DC5E2F0A66697CDCA86E60EE06B16D0414E821CAED22B0427C650EB0D8349971A6A9B31D59C1512CFDCAB613481B239FD0E65FBFCFFDEA427BB8F9BAEA92394732DCAE11A68957B0A71A11", + "DC44FF01668B17FDF5E759C911C44A60F7D4124BEF02F6CAC9D3055207AB607A549FB911A8F8A9034EE3A026AED6DBFE8CABFBA24F9898B415A2FF15FA6A54412F335EF02F27681584584AE9D6AC58264566D750E28B23C0048882CE74F2833F159590CE14F32CCC564CA81C0FC3829ACFD1B22ED3017C3D0C66C4309C2C89F45BAC2E29DE5E351C27D4B16FE3A4BB32791C244ED80AA5903D02097285C5D89C03B998D5DB1721160F25DA76F50DF5907E4C20880575DCD5825ED893F6311204A3049E7B9C9D866719A282545CDA8C7640C26F395E0BBEB26EAECEA1D80BB3C95BEF62C8E3CD05358095C42CFAC2D5DD74C741CDE684752683493AD302E6E3F16890D0ABE0839F9D107599C1789E30AE52AC1EC9E89E077CDB982CAD9280DE02C002740872D56495252FC9B80892BE3BF443597FFB32662BCDE201F704C56B2361E33FD474BCDC438D7D6E80656D19EE068427944C5A8E3F5A42156E80B004B57B5C319C5D6D815A543CE4522BF86D193E42F50429BDE0E55AD5A2914C809825DC4CA1EBD8F295167D7EC8A8652B3331EDD979785CA7053FAF8389B9D6188B62D569C547BEE798AE3AE89FFB0C06238774DBC2E97A2602780C5D7A33C099A91A4DF6FD19D15291F10D7D01675EE249C7A1C991DC352C900705DCCF0AEF2288A9992128525D191AD1D450C7832C913E45E1D2EFE49540E0813AC4D1D75D14631E2A211D3A2D11F0493C6DC1003D02690B583605BC2161AC62D2879AABE900008D287C256465CE3A7AA40A155383F006C786CE4D84B22092D3CD31003F6A7AFA023133B680CCA51B12EF4F49DC2160A6FFBC80F56FE7A5BE0AF9BC0DEB2B17C9F56A4DF1BB544F51F0718D93AEFAAA7D734FCDC8202529C2598B0F0125F1DE7F027DD65B7628A47DDB7619EFEBC1556536E175E956BDCD60559F539F1E9DEECE53F54B2313403E0821C1B61FAA2B2A57F52101A9858911BC4D885BF525048039CF64BD93898D4425DD35B0105BB86635867E2D6F7F219EC5A704839EFE5A477CEF515B8C5F426AB710D1E337C7834B878A7546112E30B8B44839DD7CE9858D350225D36FBC7D1122EE64F5691A754ED3CAD7B9CEC2EF31164D4CD4101097F2787AA6E5C61CE4CA22CE0A969F7641F70903258033897D2407629368DA9825C20CD2A8A6069F0A679DE9F4B8EA9B30DAA1A9EA0EDDCAF7D3BB1FA13FD67429CDF62B140A8106A3B969E96E3120C7B7329FD4D650889AFBD3B89DF961993D63CCD8993073BCD82BF3ED1382989FE43A5AE097744CAB819E3F2DC4450A8961A8491657665B6CA9230B109F6DDA3AACA4C18ADBF032EF3A6F2C7B693062EDEA61040AC3A8B86CC0A76D54F5F42D48378FB4692B2217B33A654CAA983BEFBE870B894494CD496FEF77E1E4796418F65C3CADB970E7BE08F504AF8B915DE8A90398F3A3FC8B184C46EF8C7011B42FDC152FC53DFA98150299171741455328E729F288F41723D4F8B06B07A5F247B3F200AA604B1F672DBEFB5C8D444C6314B2949E6FC403", + "22B94A413BE8118640CE786A2107C2AED1461082A1FEBEF610C6C1DA597171AF", + ), // modified ciphertext - ("BC4775A9A1393E32C28976CDCFDA0E25F9192FD11BA2175A7B9608B591C63501194FE6606AF76B9F1B22CD2B4D6F86A20FE4039DF0812D78889CA25A31B130A3C9092407448C0318600612CBEB81DDF8A3B760309A5A4C928B0F72F9C8CF530675D46A683261C60108820A9B48751DCBC62526196B02E23892E93A784796B87A99E78C1C348107B68A56EB3874EC63258EAA2B61D70DA2491C0B1C667BF55DD5A373EFD61FBA302281E588F7180B620500D90148E6805BE7E17B05608B2B716017EC87235BA27B0A8BA5F40EE18B0015F9AF1BF3BCD729BD79DC231F603273669A79F5AD09E2A146F2B8782A64FAD55F0E8B6894CBA795F59E9D2211CACA8D39EA6E30DA6C5AD475B2410AA6C73CE0E622A09008ACE68F1E80906B1426B54B723F4B5EC5A9CB831756FC7580B82A149E31485BC84F05B8C37A61813DBC445E56C081BCCBA33147C3A7152378A6AE6A8639029E1D451811E9B5D11C9E80C9026326B9E13066AFBC734FAA8F4F45452B172085A48D9C445C66D6B95B12321D286D4B0B4DF12678E9E973A1FC2BB4BBB579B648766B5E7170424825199AE1A0ADD60C4DA2466302B06A9C4E15B1C423A461F50802E1559D36CC5808D6B2C37A5F5B797B6D24A7D659C7D0A99FD4F4A9B05700AFB90CC830355A5BB73AAB91DBA6CCC5217911725FC5E8CE41D012C3F640093C00649A3373E4CDDA657607833DA75CAB69388D0E40AEE4E8ABBC5750B2701B77BC7DBEA983B42AB0714A8ACD6225A054672CA32938FC967E26282414B06C809827E1C4C3098396BB9DD3DA763E3B9D35DCAD3C4CB20D64C27E15A3203980F754739C091C28E11D0D4CAF6976A81D087B99E84B73A28C52179EA40A8C12C8634E5B8F6DA6CDD8C89A8EA08FB7664BCB9491C90B767138BCFF701EC9CBC4AA91A45B8A13CCC63B6903C8D5E2AEEFB4883FE7A4D5E55E2B2B67A483132A18546EA5986B27C9A34A4404254C2A37744B6914D1194D9AFA4CAF6BB45D3BCA763C88F3E67776A3796E2A9E7D797EC4195F89118C98457B8D94C6ABDB770BE3AB67CB7C0AF38D84BB6EB96C6989221680120074FC3E6126539C93B17411C3000D725354C6A8E582EB76C399D1BE6582B10737BFF083177958130A603DF9B37FF8356C06B1718F4205EB3C6E5DC56F8016900C9A8FCE3092803ACE13EC14B9715A2FB96F2942C95CD4CD6D1A68F16B495826BCF8085620A98B4C10A86D072E68220579AB2783442317581D1FF5CC734164BD08B788514BF171A3446229F949038E814DE557A76359493D193A67B57A5165621A3090BF50C807F68A0F570435A712DB001C8F3078A3041AF3EA64D6901DA5F731D9E3732D34152DD3C4AAEC3B6BA2755CC70C79A906E69860B4D15A5BD62A8D98B41BA39EDDD65618F2327347BA5EA977750A48A85BA249C2B9D080B114A56E6EEC4E9ACB6EF9CAA51B2134B6752847471E23B82385922EFAD11DAEDC882350B3C8E67BD3A8BD7BA052FAE55B91798B89851B1E1B3AEE041EFB66729C508E9CA9C550807EE26619B6C251171C9552D787B57A38491B053C7C72A528BB79C8377ED0B596C57238435ABD1B7458B3B9AA1B70485271E18731E8A3A67ED034DE1B1459E2C251E55C2FA5A0C3842E2BF65C0E6BA46215C0C4F94D6AF8B455AB329C92BF330104993C550BC9149BE0B705407B4D7459E12AA93B337E630597E8D7483FB403E8EC7E3F0AAF19CCCB04C6C29A4A0DB8A9C32AC8729B62CFA90855704383C49C9BD7B0020899A8CA0266E5875EEC2C826A530AF7951ECF280BC33B6158674108D3287D087B656858D22A4063977E4CF04890C18B9AB09E302562E6BAA32F6C07CED990372771D04A13A9F90C8D87B9339C8AA8F47170E5AEB6FBCA56B448DA78122894732F62B5C2E88D53077D91D3C933C741A4C704FA91651CBC81628A3BB2356EC6DA2444F83B2C301AD6CBAE21E0583CB24F05B3BC746805536C3E1F65C1FC0044F2F36230F21232C84A7F2CA325B85AC0C470EA02A6046A4CA22355A4F56715589890C6366DE1972C081E3A7362E380AA4AF3913FD284D8A33AB5CA04886BC7E52B03A6E153C1BB39DE929A5A0339F921BE4BAA63614500EB8497EFA27C69E661BA500D3AC661A135304E425E87B371A3E20D3567929903C8363B8C4F33C4CF1975B99980D60638CAF44800868F508B8B2756AF4572554E49C4C655065CD576E28310E665104AE51109B7B7404018A4A66F49743A1BC182AD32CB6C029860C45FA2292D408934608205124082C4998848517DD8B418A33935AEE37C8A551CDFD1B32F1AA901AC1C616343A93C542BC6CBB147025069491AC14C9BEBC156947D9328A373D2843AB877551A862A8A7A554249668C08CC388333F33BFCA60B4E0ABCEE33260FF3B3D8347D1DCC231A247F88D3C2204B9AA4F429B58015F358727409C53247048B887F213593B37461C7D4A11702B84083400235C54BB79A70E8306CB01B6183649C5684E6E8493CC0AFBA6A90AA9C44D13BC56E71B32B3B7FB965176A96AFC350A39E27BC083064167C5B4D652447815DBB4AC68A7BBF355972F4EA4D0F2099955C40DDBC6AB6C832BAF1C41351CD88E93318CB5926F20ACEFC688DC48247B9027E7800E772CFFCE51EB50C79EE39CAC8784377181BC6A373675C133200484D54230F610799D04A79DA6EC5E82D4EA75A26015E7725392DA31EC1E644D151BD4FC5C808F76DA16A2EB0E7B279227F3000C2A6F3BDCEBC75622B2C684C800848AC55490EDC119756165C5A2560A385781AE34EBEEC3C7718725433B0911AAFC09AB641E5BE03C6C6B5D42485E83D98E79689AC4A7E4A0EA95652583B6DB8465E7A2064528AB62AC555E249CCB5F028682981E3D70EFB64A556462814B094FD5C952E1541BC1CB735AB273B220B0E5A4661E78E02F913BB0875287535BAD77FB82786CF564ED0CB53BBD42249664B1D20122A1851B482B64705165D17804AC43E647728CFE9C87B4A91CCB54700DBB88AF79150C831BC4255EB06763448BA80F0AFD0DA840FE643C59035327A401AF141E2981B0D630A52D633A690B9CC31197AEB78CE5983BFD3044DF438FB983F8FC28C9925C435A5BA796CB776DACCD52995CCC281D007B0D0F1A697E766B4A09327E656C9825CC4D983A0809DC7E000049ABF49ECC337C72255835806CA55E91767D6F860B638030A0A9671FCB7CA8647FAA932A7814127E55268C90E41045A7CF577CE1C5C3D3792B183A5052581419F93CB0B1927C168DED5089B2D0991B923DDD0C6084681A60137CAA6B28ACBB363711316DCE101CCD5C056296502C26BE239B505A01DB7291EFE1F52377053382045516057442B00AA7FB31C1A0AE0718E911821F7044512A8", "38E6D9F5E24CC216630E8B9FCC01FD6E724DD31B55C22C76BC3F4F3A151F6369E313ACEFECC22B11BDEC684C3E7D6DCB7656586CD41C553BD3FB730D17768F89AB9F51A2B18CA3DCED1EA271148F23BB02948F6C38343D33D1DE3E2DDBE507DF7A96D2C6123B9C75E4F96FEA0E8F3A048AB0870ED63E94E5A45DB461D7EABC338F66F0AA5D939B1F1DA50E1172FE01682A29563B2D25B06DA0910F782DA04B7C69BB5796897F48C338044D702B1819C04327B21EB3CE9C2A5533E20DFF5B18EB1310A05CB46CCCDFBDDC0F2E9C348A03F02E01A7425A2A75D97427E570D4C20CE3EC0C27C6228C5076119211E6599A6AA300AD952FE05AC0059A7CDB80A20D6FD91D8D6C1EA05D56098E387E1CABDAD4968AB37D6DC4D26E0CFA953B7EEB6044AA2C52A73F700090D970EC5461E1B4563CDD94D8E79E730AE41A9A1CE7A43E49735450DF09E3111DC2C35560C90F8C98CF3D5235A17679CC7DEDC0BC71933B9F4623436294D06F2F072B31EC9C9B803E8D8D6A7E6D1FC4556B704AEC6A7C82262A1F2A31EAB9DE97416349B7CD7DC34333B8E40E928F94C4DD19EB762CE02A6BEF2D503DB5FC57894E7CF236821274A79262A05527F079D4D0E513FD9A786D826B667C0656681F13EB2599590AD9854C9E5378D8DEFC5D688B861BF5BE6847D475D435F8AC78E3F5A3ED162C6D7534A8FAAF8366C12B095CC1C4724D12DA31B32F229C3CCB328F28E1AC46C48BAE3C9608FEEE472430AA40F9906AEBFEB7650788582B60404586805CE870FBCA22CAD8FF251E6A61384AE1092282D87EEFEFEAFF9DCA2E4B6B1FE7920E860DE8B2232DDA16933323462B1433EC7C2E236B1EB15C0A79DCB264537C42B6818C8D2BFF3AFDA6DEEAD3FD3493AFC799D5A74F2718C15D959819B6A5E8BE3E580EDCEA1A8F96683C7EE318E704F312F9EFC26CE0FDAAA6A389930514A1C97AC0D8DCE8516EC8BCC15E5C6436AB49B819D004CCAA90398AD1C13BB5C2FE9A537A9F6C29692DE939F3C9E87F6732BC85456F3E596D86988B69A4CC813AADF1EC0A2E7D10F60822CC415276A204CD2C1844DDBDFAC8D910C5384A5ED2C8D9B8160A5B74BB3EFCFE7484B4742CBCE861800993D68EB9431F40D495428F9EABE3CC530AB7A9410FCABCD1C9CFAE431E652AF9EE19EAE0F5433DEF0CBA53936E3369BF479A5287B5FD271D0B9F0C294B759DC59F8B0B470D9BF98EB496EE1149ABAC53CA0665D64C16E0069B654EC376C66C456ADC29BF1F6DCAE4AFC020B9783D3F85C48C479E583C477E2218061B9B82EFD0A6A9C9440014B22F9458719191E4BAA1B90C30789974EFCA17C8DD78CB09EDDEE078BFCF5B44B21C7559B9B29A06532D416CF83D3C205E06F0402F89A88E24135948E3A8BDB9DC2E9CD99919CD69B41A51A6211BF2589FD457BE77CA4BCA6C97DA0DCAA1F4DDB886315137212E93877FC8C23E4260A7DFE9679458876026A6044663C42231A1AB5D1BA9DD7BB05E236B80AD48CB9FAE69DA66DDDBBA337090849F4B6916E2", "9652336BB52A7AD8F781E6D8C00E798FEFA7071211D39FC9987779727FD9270C"), + ( + "BC4775A9A1393E32C28976CDCFDA0E25F9192FD11BA2175A7B9608B591C63501194FE6606AF76B9F1B22CD2B4D6F86A20FE4039DF0812D78889CA25A31B130A3C9092407448C0318600612CBEB81DDF8A3B760309A5A4C928B0F72F9C8CF530675D46A683261C60108820A9B48751DCBC62526196B02E23892E93A784796B87A99E78C1C348107B68A56EB3874EC63258EAA2B61D70DA2491C0B1C667BF55DD5A373EFD61FBA302281E588F7180B620500D90148E6805BE7E17B05608B2B716017EC87235BA27B0A8BA5F40EE18B0015F9AF1BF3BCD729BD79DC231F603273669A79F5AD09E2A146F2B8782A64FAD55F0E8B6894CBA795F59E9D2211CACA8D39EA6E30DA6C5AD475B2410AA6C73CE0E622A09008ACE68F1E80906B1426B54B723F4B5EC5A9CB831756FC7580B82A149E31485BC84F05B8C37A61813DBC445E56C081BCCBA33147C3A7152378A6AE6A8639029E1D451811E9B5D11C9E80C9026326B9E13066AFBC734FAA8F4F45452B172085A48D9C445C66D6B95B12321D286D4B0B4DF12678E9E973A1FC2BB4BBB579B648766B5E7170424825199AE1A0ADD60C4DA2466302B06A9C4E15B1C423A461F50802E1559D36CC5808D6B2C37A5F5B797B6D24A7D659C7D0A99FD4F4A9B05700AFB90CC830355A5BB73AAB91DBA6CCC5217911725FC5E8CE41D012C3F640093C00649A3373E4CDDA657607833DA75CAB69388D0E40AEE4E8ABBC5750B2701B77BC7DBEA983B42AB0714A8ACD6225A054672CA32938FC967E26282414B06C809827E1C4C3098396BB9DD3DA763E3B9D35DCAD3C4CB20D64C27E15A3203980F754739C091C28E11D0D4CAF6976A81D087B99E84B73A28C52179EA40A8C12C8634E5B8F6DA6CDD8C89A8EA08FB7664BCB9491C90B767138BCFF701EC9CBC4AA91A45B8A13CCC63B6903C8D5E2AEEFB4883FE7A4D5E55E2B2B67A483132A18546EA5986B27C9A34A4404254C2A37744B6914D1194D9AFA4CAF6BB45D3BCA763C88F3E67776A3796E2A9E7D797EC4195F89118C98457B8D94C6ABDB770BE3AB67CB7C0AF38D84BB6EB96C6989221680120074FC3E6126539C93B17411C3000D725354C6A8E582EB76C399D1BE6582B10737BFF083177958130A603DF9B37FF8356C06B1718F4205EB3C6E5DC56F8016900C9A8FCE3092803ACE13EC14B9715A2FB96F2942C95CD4CD6D1A68F16B495826BCF8085620A98B4C10A86D072E68220579AB2783442317581D1FF5CC734164BD08B788514BF171A3446229F949038E814DE557A76359493D193A67B57A5165621A3090BF50C807F68A0F570435A712DB001C8F3078A3041AF3EA64D6901DA5F731D9E3732D34152DD3C4AAEC3B6BA2755CC70C79A906E69860B4D15A5BD62A8D98B41BA39EDDD65618F2327347BA5EA977750A48A85BA249C2B9D080B114A56E6EEC4E9ACB6EF9CAA51B2134B6752847471E23B82385922EFAD11DAEDC882350B3C8E67BD3A8BD7BA052FAE55B91798B89851B1E1B3AEE041EFB66729C508E9CA9C550807EE26619B6C251171C9552D787B57A38491B053C7C72A528BB79C8377ED0B596C57238435ABD1B7458B3B9AA1B70485271E18731E8A3A67ED034DE1B1459E2C251E55C2FA5A0C3842E2BF65C0E6BA46215C0C4F94D6AF8B455AB329C92BF330104993C550BC9149BE0B705407B4D7459E12AA93B337E630597E8D7483FB403E8EC7E3F0AAF19CCCB04C6C29A4A0DB8A9C32AC8729B62CFA90855704383C49C9BD7B0020899A8CA0266E5875EEC2C826A530AF7951ECF280BC33B6158674108D3287D087B656858D22A4063977E4CF04890C18B9AB09E302562E6BAA32F6C07CED990372771D04A13A9F90C8D87B9339C8AA8F47170E5AEB6FBCA56B448DA78122894732F62B5C2E88D53077D91D3C933C741A4C704FA91651CBC81628A3BB2356EC6DA2444F83B2C301AD6CBAE21E0583CB24F05B3BC746805536C3E1F65C1FC0044F2F36230F21232C84A7F2CA325B85AC0C470EA02A6046A4CA22355A4F56715589890C6366DE1972C081E3A7362E380AA4AF3913FD284D8A33AB5CA04886BC7E52B03A6E153C1BB39DE929A5A0339F921BE4BAA63614500EB8497EFA27C69E661BA500D3AC661A135304E425E87B371A3E20D3567929903C8363B8C4F33C4CF1975B99980D60638CAF44800868F508B8B2756AF4572554E49C4C655065CD576E28310E665104AE51109B7B7404018A4A66F49743A1BC182AD32CB6C029860C45FA2292D408934608205124082C4998848517DD8B418A33935AEE37C8A551CDFD1B32F1AA901AC1C616343A93C542BC6CBB147025069491AC14C9BEBC156947D9328A373D2843AB877551A862A8A7A554249668C08CC388333F33BFCA60B4E0ABCEE33260FF3B3D8347D1DCC231A247F88D3C2204B9AA4F429B58015F358727409C53247048B887F213593B37461C7D4A11702B84083400235C54BB79A70E8306CB01B6183649C5684E6E8493CC0AFBA6A90AA9C44D13BC56E71B32B3B7FB965176A96AFC350A39E27BC083064167C5B4D652447815DBB4AC68A7BBF355972F4EA4D0F2099955C40DDBC6AB6C832BAF1C41351CD88E93318CB5926F20ACEFC688DC48247B9027E7800E772CFFCE51EB50C79EE39CAC8784377181BC6A373675C133200484D54230F610799D04A79DA6EC5E82D4EA75A26015E7725392DA31EC1E644D151BD4FC5C808F76DA16A2EB0E7B279227F3000C2A6F3BDCEBC75622B2C684C800848AC55490EDC119756165C5A2560A385781AE34EBEEC3C7718725433B0911AAFC09AB641E5BE03C6C6B5D42485E83D98E79689AC4A7E4A0EA95652583B6DB8465E7A2064528AB62AC555E249CCB5F028682981E3D70EFB64A556462814B094FD5C952E1541BC1CB735AB273B220B0E5A4661E78E02F913BB0875287535BAD77FB82786CF564ED0CB53BBD42249664B1D20122A1851B482B64705165D17804AC43E647728CFE9C87B4A91CCB54700DBB88AF79150C831BC4255EB06763448BA80F0AFD0DA840FE643C59035327A401AF141E2981B0D630A52D633A690B9CC31197AEB78CE5983BFD3044DF438FB983F8FC28C9925C435A5BA796CB776DACCD52995CCC281D007B0D0F1A697E766B4A09327E656C9825CC4D983A0809DC7E000049ABF49ECC337C72255835806CA55E91767D6F860B638030A0A9671FCB7CA8647FAA932A7814127E55268C90E41045A7CF577CE1C5C3D3792B183A5052581419F93CB0B1927C168DED5089B2D0991B923DDD0C6084681A60137CAA6B28ACBB363711316DCE101CCD5C056296502C26BE239B505A01DB7291EFE1F52377053382045516057442B00AA7FB31C1A0AE0718E911821F7044512A8", + "38E6D9F5E24CC216630E8B9FCC01FD6E724DD31B55C22C76BC3F4F3A151F6369E313ACEFECC22B11BDEC684C3E7D6DCB7656586CD41C553BD3FB730D17768F89AB9F51A2B18CA3DCED1EA271148F23BB02948F6C38343D33D1DE3E2DDBE507DF7A96D2C6123B9C75E4F96FEA0E8F3A048AB0870ED63E94E5A45DB461D7EABC338F66F0AA5D939B1F1DA50E1172FE01682A29563B2D25B06DA0910F782DA04B7C69BB5796897F48C338044D702B1819C04327B21EB3CE9C2A5533E20DFF5B18EB1310A05CB46CCCDFBDDC0F2E9C348A03F02E01A7425A2A75D97427E570D4C20CE3EC0C27C6228C5076119211E6599A6AA300AD952FE05AC0059A7CDB80A20D6FD91D8D6C1EA05D56098E387E1CABDAD4968AB37D6DC4D26E0CFA953B7EEB6044AA2C52A73F700090D970EC5461E1B4563CDD94D8E79E730AE41A9A1CE7A43E49735450DF09E3111DC2C35560C90F8C98CF3D5235A17679CC7DEDC0BC71933B9F4623436294D06F2F072B31EC9C9B803E8D8D6A7E6D1FC4556B704AEC6A7C82262A1F2A31EAB9DE97416349B7CD7DC34333B8E40E928F94C4DD19EB762CE02A6BEF2D503DB5FC57894E7CF236821274A79262A05527F079D4D0E513FD9A786D826B667C0656681F13EB2599590AD9854C9E5378D8DEFC5D688B861BF5BE6847D475D435F8AC78E3F5A3ED162C6D7534A8FAAF8366C12B095CC1C4724D12DA31B32F229C3CCB328F28E1AC46C48BAE3C9608FEEE472430AA40F9906AEBFEB7650788582B60404586805CE870FBCA22CAD8FF251E6A61384AE1092282D87EEFEFEAFF9DCA2E4B6B1FE7920E860DE8B2232DDA16933323462B1433EC7C2E236B1EB15C0A79DCB264537C42B6818C8D2BFF3AFDA6DEEAD3FD3493AFC799D5A74F2718C15D959819B6A5E8BE3E580EDCEA1A8F96683C7EE318E704F312F9EFC26CE0FDAAA6A389930514A1C97AC0D8DCE8516EC8BCC15E5C6436AB49B819D004CCAA90398AD1C13BB5C2FE9A537A9F6C29692DE939F3C9E87F6732BC85456F3E596D86988B69A4CC813AADF1EC0A2E7D10F60822CC415276A204CD2C1844DDBDFAC8D910C5384A5ED2C8D9B8160A5B74BB3EFCFE7484B4742CBCE861800993D68EB9431F40D495428F9EABE3CC530AB7A9410FCABCD1C9CFAE431E652AF9EE19EAE0F5433DEF0CBA53936E3369BF479A5287B5FD271D0B9F0C294B759DC59F8B0B470D9BF98EB496EE1149ABAC53CA0665D64C16E0069B654EC376C66C456ADC29BF1F6DCAE4AFC020B9783D3F85C48C479E583C477E2218061B9B82EFD0A6A9C9440014B22F9458719191E4BAA1B90C30789974EFCA17C8DD78CB09EDDEE078BFCF5B44B21C7559B9B29A06532D416CF83D3C205E06F0402F89A88E24135948E3A8BDB9DC2E9CD99919CD69B41A51A6211BF2589FD457BE77CA4BCA6C97DA0DCAA1F4DDB886315137212E93877FC8C23E4260A7DFE9679458876026A6044663C42231A1AB5D1BA9DD7BB05E236B80AD48CB9FAE69DA66DDDBBA337090849F4B6916E2", + "9652336BB52A7AD8F781E6D8C00E798FEFA7071211D39FC9987779727FD9270C", + ), // modified ciphertext - ("2D7A0D99D1B432C255FDC09FE0528D7B83268518626A73443C55CA0F9C39999B4B54544D561C29AA00211857C6EE321E9C0BA69FB8C9FD486EDFE40B1FF9412DFB498FA81FF90420DFEA08F76310A42C2C31E204523CC555545959BAC9BDD64A3121C9835222070AC9A509A5B0FB768AB617402C74B15985D8CBC971B906D9C373886585E4DA9893E88ECEB8B0DB302B9784952BEB1512A1A7AE6850B6FAC8CEDB764D736DC744289600C39E517B44F15F9BB6AD0DBA8EE6237AE63578D8D89BBADB63BFD302AED91408500AA2963B53DA00CF97AA4F578B039AAF72150C9FA214270B5311F341C79BA93DD538E13C693D19CA20C5927C615E5111C78E444D4C024B3B6C8D251C0107217374E9A9CB73917AD94463496884E856B0B74BF3013FDB9848C793764E5003B6FAA116BC173BB63EBDA38DA5F26BC7D899CE8B8CD6D4571F5028132680A81B426933ACE202643F816966552560C600EBF1866A0541424C3BC7DCB00A6C842A2BB6D39770182520CBFABA18CB47ADA4B396518E8E95849A008B789AB4FFFB5B8C7A438AD1AF865ACEB57B5ECE9727329A7737A8502ED145D5F1ABBC5A608161025334BC084205D7219BBFC94E04B2B26509205EF1AACB015D5000BB200983E589654A4417DB107C9A39BAA706AA097A4E55B9A9F2E560402C1545D5B036F01EB3C96719EA0068649D3196A05CE63B0D293C887B97AB9723BADA859C112DDF99C91C090AC1B67F0F301F35919D2B501C4147876A2045025980D30C424B803B9C20364F408A0D4838F35B0AAD04814168B47E381F3AF82644C0B123603A3161CE2A01A157BC262B665FF2E8C7DC48748AE94CCAC1BB0283A9C637A077F19EEA818686A62479B82A63F4C4EB522FE078B9A4A6B10A612831849C51D0C0437C18A151B396A8589899AF69D02AB733CFF1056E664C32A84906C248BFCE69288985BB7DB1BF48255647EA2C3C3AB72FBB0109E75A22F82903CC1FEED348F250CC3178228335933EF2A2CB1284F4E8777F71ABE3E25514EA912A47A888763985A2A1BC962AD330CB39E29D4C620FCA9964828654CB27A4F7950A57EA255BE54AAF22105B629DFCD0076FB21D5FF956805011A1889BE577CF4847C32F6BBF8FD9C3778656E4A0A3A7043C867844B1B221F0925933E698C3B67C7D18C79C2A521613AF07C4A3A37C5E5A260540781DCF4C373D1661EC753A46878A2C61A2F5971182A87786C625AFA66212D81253310C1F398D2546059AC9935F9A933DB6AAA4B402D7A3B9B6388088FA801147AF5E61594AB61465A5CD7B5370A1B019621C8C244481CD452741CB359F196639F1A40D77344AC18B0470648B407FB801205D5A2195232E72115C41237D4995492B77A64A14748F2315306C2DF4787EF9C60A2F0C1DB4FA7E0F3212B6223063691CB96477EAA04019F39DEA2133665CB6D63183A507C2DDD92ED33A3CF095CC7B5A37F144844DDC8BC07A8A8C69388F733CEB659EE3293F245926E6822EA522CC8EBA2788BBAFC962960FFA320A62C45F6B992C009FB2C2CDCFD3758431CC675A7BD0D985D93789E9A74977B434B305BB871A58C007313607CD40E4B1ABD7B2C3CBB512667876215A08F413F0259A9B0360794B6984157D41863C35599B8B13694445AEF8706D43B097E8473EB9AB71551038F306152494CFF6CC771E7437EF013DE1F27071C6775960BDF3E5C189D10605232ABE24071BCA16ED32154E00233C83C78FBB9B1B0C31D53106BFBC50A5E091DAF6436DC320EC8028486223C2EB705334982DA6CD2999B5FCBB9C26E98A20DA272D6B108AD378EC973504EBA7546C3638766F4AE25CA228B81E9B0EF8E35C8BF774408967D8772F6A73BF57F51F2D7C05D0C8C010735C7E0C14EDEB0ADC0A137E94313F8750408A5E6495C9FE84B73064137292891CDC61FE081DF98A406871680E0A5E09519A0C77C5C857BF2A7B8E6C936B08673B213917E28475A934885DD982D5C366DEA94C7792AAD7C316848561503BA7E7EC37D8E31C742B1BD7A5C64FC0791DC669EF1175E29CA91C308DAD0560B3C071F1D16005554FF5DA3E43C17DA89C30C2EC3ED24256706716FF43842160771094A8B31323CF569ADCD98A2F41C2C3F363AB926516078496404429AA5FEDE024FF22489C234AFB422A4841911D9551882519980B379F61099F770FD7230F9F68875163B26522CD134BB9C50225E0C7277BC8A3FD4486D756486FCA6BC5A9A67E35C0BDA8843D0334586CCE210B757F3322EEB28CE6E88BE3075173319FA039C518546D698611CA35977B9C00DD96291AD9CBC85B71C3CB5FA867ADE4507E6AB8C238B3B79C990376A304EF546BBE88A5E5F74FC4465F4C581B92AC48187C084332B2B7614BDD6263F98A33EBC284B0E7CCB6A4CDEED767E45B93B6D1986122264F3CCEF5DCA5359084097313E2C8AE66ECC75E806978F293A1814A6300516AEB25D46026B5F0851403009B0A1FC49C316BBC97371A3475494B779A70C68847CC603F0A476F0C27314C1B8133D03BCC32520C36C0C4B243DB81B684F206FB577BEBD010967012288C605F28231A40B3D2292470D875A340A764923FF6B495C191BBFDC55591B41407137D69978BB4E8BBC1E1173F0161D35ACE60E3408F022C64574FB23368AC333DBA313F414BA99E639793F4B55A775C9AD4998D01B64D9007E42BCEAD0B5E2F3C5B1CC2C9F095382DEB149DFB815C8A253599974C218B90542D586B8D6F051115E88CCA5959A6A41BE0AC0B1CE7636918C5EB63B9BED99D3BF3B9AB1B7358A50B27437B07E01AE62121737B2632632BE27B9C36113D25866B25571DA59BB2DA92C5174814C8A3BA21E36367D630D26156EA935F14C80269E0B90E647DD6E606668CB3042BCFCC169378533E6B79B118D4ADDD9C251DF9C59105A4DC1C4EB3493647FA2902B5389F78B44C2855DAF879262894AE480594F9B58D57802AB99C6DB448A4237C3AC4B859EA299B5514C65615A2F0206A0049524B37EF7182EFB45CE987AC2AA74BE633BC7125C7FB1A0F602802B8B10F3CAB31FA3852737A871ED006840C81F2769B4238B1E2F3A17BFA7BCCD3BB5797BE0FB2337E0343A3479F1EF822905AA1376A68C873805100C70717B37AC28CE4F331C9347F40437776CB5781F833CD236FF2B376C6A0BB20871AD0753395ABA5F4DC755BAABFA09157644724345A14AB4384F14B6207791C82762ED94B472F61C0677B03898371B58CA765F6C1D6DE3B96F38FAFD5E7D0656D1D4F39DDB71676E1E50C745C4E7CE69ED878D9FF0468C5763197DA7CC32A4DC98E1ED5F428EE03B2CDE7806072E4FA38D66BFD23304F321D7B52E41787760D1BFAFCF447640CC723F45286037E08E0715CA4C6ED", "B5B33099285C4CCE37BEB8801A555F1E9179E821606D66336F078C3B43E598DCC61B57DD999DDD0DEC1A39AA894FEE755BF6017D4F94FD9CF22EDE99F0D353F2654904F3E62A55DD861C1DF43D7B6DB3BDD407FC773F6900FD743E440C2CA9B524AF9CE60B846F02700DBDBFF335B06B80085CE1D5643B8A5F9FF24EB2669074AD63E855B102D25E6F97B7485135D2E20E28C0B4E8F8353FEBA7B67F84ABBF4C67366066F047D6625DE7A1BF72F09109F1D57EC206002482D180B0B7E5E388B754C980D9E02D7D46028F214FB5E3E37E5FB74A4F4E81FBC7B14C178EB185358EA087AE471E6FF3ABAF67C9D47F3F575462A9D7F8A9FBE6C32A88D4791D032CAC99BA2AC6021788920F8421767D854D3A8E116401EB14AE9E6A90B773C8C5FB4A3FE77C200813906B0FA472AB834E9DF4B68CDB4FD2A72FD9023424D4FE5C205FA3F5CCBEF5C951C9937AC2220930A3A6EA4B0B8A4416C07D3837C2EB77EEBE8378A186D82089A41FC1BAB0A1D0CCB02EFC23C99008C4BE42ECEEC7348B8F1C32879891223809737E752DA8EF1968C9DCD28FD0ADE1845818AD3B667AB6DA9B06D699516D32FE4D9FE5C94C776C444F8925F3A4CF5F8C1FD86C418481E0457AB02A86335054326FA49F2DE347E2E8AFF25AD7E2FCDFC562A61F7A6EEB5EE9F05F1E5147A152CE06A4F2023D85FA44C7CB5FC4A75374EA5DA42F67A9063C88B5CAB26FC79E5EB5EBBE633567FC61BEB54BBD6EEAAB698F1FB187F3493F25D5243D063AD5B69E42B96F7D754F441B86E7B707E522A43B4BE5EF38AB5DA38F5A851C8BA4238DC0C339310DCA9B5051684AEC5984A65E17D4C49A823995A05CCA199587F75F913A572A3F1FD4B743AB7BF8470B52224DB110F2563396B798B21073292614BF12CBC6DA258265F9488EDA8DF5DCBCA1EC342CA896C6345BA301BC93EDF2FDAB077A06518AC8951DA6C53616A2B991F2ED16CD5D9330B13FEE5425C4B7FF82B6454E38076573DE419977EE02CBB723203B27437E9971833E088EE58935DE7D838099DDA6BFF5AF2F0FB9307A1252621615341D15B0925377CE817A0F6C3238B76AD589C0032A5ABBE9D226D3D298113AFF3B844149227A5F038547F791AB7D4E32F10FE12AB82C622D2C16A1868D804AFEC8600576E9D571A600049BDB140551EA54B73387A48D1E6A5A643D97E32B405789AA14CCA120057EE2ACACD019402DBA9CBF194D7EFE76F4C166134F59ABAACC392EEB6DFB744366EE4C450C327A26B9105AA314B682DBD7C585E595079EF20A48ED3E307522DCC09D5629B182F7D8ED82F8E3F6D3CAE6601D25A657B488017B91FACB3C1597FAC4B97C614CF7EB386152405B9DDBAA7FBA83A5AB5ABA6D8EA531EC26FA157B931C7DA46CC37636FBD7497585DEE830A2476C758D35F8373B03222EE3702228FC231B7B963889BA66F0050B77A0C60A871C6AD084DD05C67FE8AED38A7EB55E060F49FC89C3809FCE97B2A012BC8829C861A3D88DCC3A8D750FB585C5256845CA1F8FB6378F", "99F8AD0CA949479280ACCC3F08B036AEB926E4A74C5188B267A4A7C7D42B0CF1"), + ( + "2D7A0D99D1B432C255FDC09FE0528D7B83268518626A73443C55CA0F9C39999B4B54544D561C29AA00211857C6EE321E9C0BA69FB8C9FD486EDFE40B1FF9412DFB498FA81FF90420DFEA08F76310A42C2C31E204523CC555545959BAC9BDD64A3121C9835222070AC9A509A5B0FB768AB617402C74B15985D8CBC971B906D9C373886585E4DA9893E88ECEB8B0DB302B9784952BEB1512A1A7AE6850B6FAC8CEDB764D736DC744289600C39E517B44F15F9BB6AD0DBA8EE6237AE63578D8D89BBADB63BFD302AED91408500AA2963B53DA00CF97AA4F578B039AAF72150C9FA214270B5311F341C79BA93DD538E13C693D19CA20C5927C615E5111C78E444D4C024B3B6C8D251C0107217374E9A9CB73917AD94463496884E856B0B74BF3013FDB9848C793764E5003B6FAA116BC173BB63EBDA38DA5F26BC7D899CE8B8CD6D4571F5028132680A81B426933ACE202643F816966552560C600EBF1866A0541424C3BC7DCB00A6C842A2BB6D39770182520CBFABA18CB47ADA4B396518E8E95849A008B789AB4FFFB5B8C7A438AD1AF865ACEB57B5ECE9727329A7737A8502ED145D5F1ABBC5A608161025334BC084205D7219BBFC94E04B2B26509205EF1AACB015D5000BB200983E589654A4417DB107C9A39BAA706AA097A4E55B9A9F2E560402C1545D5B036F01EB3C96719EA0068649D3196A05CE63B0D293C887B97AB9723BADA859C112DDF99C91C090AC1B67F0F301F35919D2B501C4147876A2045025980D30C424B803B9C20364F408A0D4838F35B0AAD04814168B47E381F3AF82644C0B123603A3161CE2A01A157BC262B665FF2E8C7DC48748AE94CCAC1BB0283A9C637A077F19EEA818686A62479B82A63F4C4EB522FE078B9A4A6B10A612831849C51D0C0437C18A151B396A8589899AF69D02AB733CFF1056E664C32A84906C248BFCE69288985BB7DB1BF48255647EA2C3C3AB72FBB0109E75A22F82903CC1FEED348F250CC3178228335933EF2A2CB1284F4E8777F71ABE3E25514EA912A47A888763985A2A1BC962AD330CB39E29D4C620FCA9964828654CB27A4F7950A57EA255BE54AAF22105B629DFCD0076FB21D5FF956805011A1889BE577CF4847C32F6BBF8FD9C3778656E4A0A3A7043C867844B1B221F0925933E698C3B67C7D18C79C2A521613AF07C4A3A37C5E5A260540781DCF4C373D1661EC753A46878A2C61A2F5971182A87786C625AFA66212D81253310C1F398D2546059AC9935F9A933DB6AAA4B402D7A3B9B6388088FA801147AF5E61594AB61465A5CD7B5370A1B019621C8C244481CD452741CB359F196639F1A40D77344AC18B0470648B407FB801205D5A2195232E72115C41237D4995492B77A64A14748F2315306C2DF4787EF9C60A2F0C1DB4FA7E0F3212B6223063691CB96477EAA04019F39DEA2133665CB6D63183A507C2DDD92ED33A3CF095CC7B5A37F144844DDC8BC07A8A8C69388F733CEB659EE3293F245926E6822EA522CC8EBA2788BBAFC962960FFA320A62C45F6B992C009FB2C2CDCFD3758431CC675A7BD0D985D93789E9A74977B434B305BB871A58C007313607CD40E4B1ABD7B2C3CBB512667876215A08F413F0259A9B0360794B6984157D41863C35599B8B13694445AEF8706D43B097E8473EB9AB71551038F306152494CFF6CC771E7437EF013DE1F27071C6775960BDF3E5C189D10605232ABE24071BCA16ED32154E00233C83C78FBB9B1B0C31D53106BFBC50A5E091DAF6436DC320EC8028486223C2EB705334982DA6CD2999B5FCBB9C26E98A20DA272D6B108AD378EC973504EBA7546C3638766F4AE25CA228B81E9B0EF8E35C8BF774408967D8772F6A73BF57F51F2D7C05D0C8C010735C7E0C14EDEB0ADC0A137E94313F8750408A5E6495C9FE84B73064137292891CDC61FE081DF98A406871680E0A5E09519A0C77C5C857BF2A7B8E6C936B08673B213917E28475A934885DD982D5C366DEA94C7792AAD7C316848561503BA7E7EC37D8E31C742B1BD7A5C64FC0791DC669EF1175E29CA91C308DAD0560B3C071F1D16005554FF5DA3E43C17DA89C30C2EC3ED24256706716FF43842160771094A8B31323CF569ADCD98A2F41C2C3F363AB926516078496404429AA5FEDE024FF22489C234AFB422A4841911D9551882519980B379F61099F770FD7230F9F68875163B26522CD134BB9C50225E0C7277BC8A3FD4486D756486FCA6BC5A9A67E35C0BDA8843D0334586CCE210B757F3322EEB28CE6E88BE3075173319FA039C518546D698611CA35977B9C00DD96291AD9CBC85B71C3CB5FA867ADE4507E6AB8C238B3B79C990376A304EF546BBE88A5E5F74FC4465F4C581B92AC48187C084332B2B7614BDD6263F98A33EBC284B0E7CCB6A4CDEED767E45B93B6D1986122264F3CCEF5DCA5359084097313E2C8AE66ECC75E806978F293A1814A6300516AEB25D46026B5F0851403009B0A1FC49C316BBC97371A3475494B779A70C68847CC603F0A476F0C27314C1B8133D03BCC32520C36C0C4B243DB81B684F206FB577BEBD010967012288C605F28231A40B3D2292470D875A340A764923FF6B495C191BBFDC55591B41407137D69978BB4E8BBC1E1173F0161D35ACE60E3408F022C64574FB23368AC333DBA313F414BA99E639793F4B55A775C9AD4998D01B64D9007E42BCEAD0B5E2F3C5B1CC2C9F095382DEB149DFB815C8A253599974C218B90542D586B8D6F051115E88CCA5959A6A41BE0AC0B1CE7636918C5EB63B9BED99D3BF3B9AB1B7358A50B27437B07E01AE62121737B2632632BE27B9C36113D25866B25571DA59BB2DA92C5174814C8A3BA21E36367D630D26156EA935F14C80269E0B90E647DD6E606668CB3042BCFCC169378533E6B79B118D4ADDD9C251DF9C59105A4DC1C4EB3493647FA2902B5389F78B44C2855DAF879262894AE480594F9B58D57802AB99C6DB448A4237C3AC4B859EA299B5514C65615A2F0206A0049524B37EF7182EFB45CE987AC2AA74BE633BC7125C7FB1A0F602802B8B10F3CAB31FA3852737A871ED006840C81F2769B4238B1E2F3A17BFA7BCCD3BB5797BE0FB2337E0343A3479F1EF822905AA1376A68C873805100C70717B37AC28CE4F331C9347F40437776CB5781F833CD236FF2B376C6A0BB20871AD0753395ABA5F4DC755BAABFA09157644724345A14AB4384F14B6207791C82762ED94B472F61C0677B03898371B58CA765F6C1D6DE3B96F38FAFD5E7D0656D1D4F39DDB71676E1E50C745C4E7CE69ED878D9FF0468C5763197DA7CC32A4DC98E1ED5F428EE03B2CDE7806072E4FA38D66BFD23304F321D7B52E41787760D1BFAFCF447640CC723F45286037E08E0715CA4C6ED", + "B5B33099285C4CCE37BEB8801A555F1E9179E821606D66336F078C3B43E598DCC61B57DD999DDD0DEC1A39AA894FEE755BF6017D4F94FD9CF22EDE99F0D353F2654904F3E62A55DD861C1DF43D7B6DB3BDD407FC773F6900FD743E440C2CA9B524AF9CE60B846F02700DBDBFF335B06B80085CE1D5643B8A5F9FF24EB2669074AD63E855B102D25E6F97B7485135D2E20E28C0B4E8F8353FEBA7B67F84ABBF4C67366066F047D6625DE7A1BF72F09109F1D57EC206002482D180B0B7E5E388B754C980D9E02D7D46028F214FB5E3E37E5FB74A4F4E81FBC7B14C178EB185358EA087AE471E6FF3ABAF67C9D47F3F575462A9D7F8A9FBE6C32A88D4791D032CAC99BA2AC6021788920F8421767D854D3A8E116401EB14AE9E6A90B773C8C5FB4A3FE77C200813906B0FA472AB834E9DF4B68CDB4FD2A72FD9023424D4FE5C205FA3F5CCBEF5C951C9937AC2220930A3A6EA4B0B8A4416C07D3837C2EB77EEBE8378A186D82089A41FC1BAB0A1D0CCB02EFC23C99008C4BE42ECEEC7348B8F1C32879891223809737E752DA8EF1968C9DCD28FD0ADE1845818AD3B667AB6DA9B06D699516D32FE4D9FE5C94C776C444F8925F3A4CF5F8C1FD86C418481E0457AB02A86335054326FA49F2DE347E2E8AFF25AD7E2FCDFC562A61F7A6EEB5EE9F05F1E5147A152CE06A4F2023D85FA44C7CB5FC4A75374EA5DA42F67A9063C88B5CAB26FC79E5EB5EBBE633567FC61BEB54BBD6EEAAB698F1FB187F3493F25D5243D063AD5B69E42B96F7D754F441B86E7B707E522A43B4BE5EF38AB5DA38F5A851C8BA4238DC0C339310DCA9B5051684AEC5984A65E17D4C49A823995A05CCA199587F75F913A572A3F1FD4B743AB7BF8470B52224DB110F2563396B798B21073292614BF12CBC6DA258265F9488EDA8DF5DCBCA1EC342CA896C6345BA301BC93EDF2FDAB077A06518AC8951DA6C53616A2B991F2ED16CD5D9330B13FEE5425C4B7FF82B6454E38076573DE419977EE02CBB723203B27437E9971833E088EE58935DE7D838099DDA6BFF5AF2F0FB9307A1252621615341D15B0925377CE817A0F6C3238B76AD589C0032A5ABBE9D226D3D298113AFF3B844149227A5F038547F791AB7D4E32F10FE12AB82C622D2C16A1868D804AFEC8600576E9D571A600049BDB140551EA54B73387A48D1E6A5A643D97E32B405789AA14CCA120057EE2ACACD019402DBA9CBF194D7EFE76F4C166134F59ABAACC392EEB6DFB744366EE4C450C327A26B9105AA314B682DBD7C585E595079EF20A48ED3E307522DCC09D5629B182F7D8ED82F8E3F6D3CAE6601D25A657B488017B91FACB3C1597FAC4B97C614CF7EB386152405B9DDBAA7FBA83A5AB5ABA6D8EA531EC26FA157B931C7DA46CC37636FBD7497585DEE830A2476C758D35F8373B03222EE3702228FC231B7B963889BA66F0050B77A0C60A871C6AD084DD05C67FE8AED38A7EB55E060F49FC89C3809FCE97B2A012BC8829C861A3D88DCC3A8D750FB585C5256845CA1F8FB6378F", + "99F8AD0CA949479280ACCC3F08B036AEB926E4A74C5188B267A4A7C7D42B0CF1", + ), // modified ciphertext - ("F0E83DD90A2966E3C6CA6625FE8392A575B501B760218C3889A4B6285580843085E01732D109B273064361561DA38832EFE42DF982BEAA16191681538D218D11234891C43398F3351E24599F028AF1A507ABA0891A606CC3F987BA0B5F92B0951E322A32F80109365F12A35C8DF2699598B5DC36B5F4191E970589124567BA56A531FA71D37773BBDB89F0F054F5C35B9D62C2C9641962EAAB57AB4FFC91A4C3F715E8DB1FE969B7B2101952D0BA27D303C1AB1A91B12D435C3B0C201A2FE89AA600A260F9BAA0415B45423D08597F2D20316950C6DA11C4E5837AA3D94DA141CBB4959DD5D11C925915B15255151B88A3158318B0CEDEBA0765B17FCB60BCDA4C17BC904FED129058265822BC733252A62796CFB290CFFD55080726678B3675A7EB8E23DC34CAE425F9109832D05DFA68C1EDC13B2EB69F23B23BFF92266CB4B0AB300836747D69D03D45F7CEF1C10F14EC8C927A393600228F4BC2CEE75232D68D4E85084FBA3B3716B110256B22C7318BF3889F015C7D6069958A6E4089A75898AF00F46B9DE6565B241B5E9370DBD4823C3841CC743E9DC1CBCEB71FF20005EA8A23B4BB15566C51E7E67D4407729BE416D013C03F792F26000B71C9849A467361716A00F1628E0B3F85B700C4D53452B09CD4A623AF0BC4A5B192B3B469FCCB3FF68A87A325A25F4B221658CBBDFAA26F9642006A4B25176B278C59EB365159656E32E99D990486661AAF5215A8179400179608B4589D31C3877A25387B645CAA667B1843B2C0F482D158AD6577C5B2E7816311046AC518FC8B8ECD866039078AEEF966120B32608B8D0FD9C058F9926679388B829EE42C17CD8483274064D89304630607437A123AF4BCE25BBEE57325060880818B2C15157D55E8B989565D8D64C80FF0BE18904D842134EDE6CA72A31D50AC6CF205345BB93EB5C806F6F216F8071701B1674A179BE2D9342929706D5944F7FC0E3BE6BF0679A1AADC4F5261A971C61DDEF2B17A31B236EC4649E9A113754F7329BE27151698AA544994BC0B016C7AC65737C24CE815330BFC6D715C2921609923D9B7B6B53DA483C409942246F294E204C918B6353BEC721683236C49B70D66AE1CFC37BE451A7B190DF7579E40515B0043826C7926FA9B5A35676D6F294405B41E5237CE09928C6D70C844C17716C33DA2C4B6C92A219307371626855858761A133370C616024C837A4A26ED6B01A5514EC1AA0BA5A0B8C7986D7C168E8C6160D18229A6A727EB7ABC118CC64C31A605351B972635A6A906393A9282471BF5E3C67B6C8138E93BE276170D844D02E234034352D3086E6AEC30E306B1E3C3A55D2935D02B05D065567ACBB0EE6C7E12A30C22B443DF187310E9C6529C4C5CFC48BBF0358748863A79B3860319C0D9321DA780E5E8AFCCE027DD8777A3A06465B95DA642BE729B12FF63C75DCA6AD253066E9C081808B57E6B1DAA15A3CF37B4FD2B13B684A7A7566F8123A397383905F58C541420AA2833D96233EFE35198CBAA577C693A0963FA2475A73348F15262B9B08B43497613A608AD22B661B3159539355265BC7CD903CBBB841424230E164AAFB538F43582C345179D168BC955B36F93BBB2E9559F0780CB7BBFB6C920B8806ED54252237B3905C07A6EA75797D48DB6310CD6A9448EAB4AA8FC60F78C679F4630AFF35DBBBA1FFF5612DC220BE92C55BC803F75B6B0656080628186ED022229792736CB47F29A4C2E9A4EC3A5583E0B0E629B3BAF0AC2E5F7C0E1529A49B231B006902506C80D7B29B87313F51AB9C7E5C97FC98C3F8A404D559D21DA3AF0E01D88C356284738AD17365EBC997D3B1C515452C3884F27234FB9C351DB1118C6396917F30344BC15D399CCE60C9B8A109D8DD86CB2781673C21F083C1339B4730EB92FA2935AF78C60197233ED955E4190CCB354732D039CB3FBBB0DEBC5BF046A91D7BA67622BEE9A3AB5E3B1CDD64C3C3BC305506A500A51B6B9B249489094677D1A021397610A4171143AD6CCB6769354D5C0ACE964CF552BB4BBCFE4F86AECA44138010A9D0074C9853564E74D46EB88C9FCCAFA24BE5431C0914C377BD4A7A2FA0A44561454925106265283602FBF319AC4284FAD3321DC99C5EC603E388BB60B426733A6C6DEE3463F5071107B061CC01DD3D673C3529495805522BA51EA0B9C4B1271417A06505230579B7BBFA45B1CB6BA127031FBE7BB98A032E6EBBCC1B4C4E19107DF6878D4F35B1D221A600B454230857F42718E2914172151CB435C5C101BC2647FA57390035CC7C4607EECE07DE20811481A2BE2897B6D133E21A2995C2892AF5332AA15116067BBC308234EE74EC1BB6B384895B28092A4DB5FC58A9904C81F0D0C44666817CB622A1DEB709A630B9A778B60E6063B523E9F080B48A0991BA78F28627D4B555AB8BC14EE975A95BA05FE2A76F677BDE402B7BA29BD59CB8AA8F0A485DB15B49CA6AD38A5226689F5EB1A5147344E8427C460674147CA43D439CA73625B0468CF5238029B08C2C543524093829138FA55CBF5C80BEE0635D7FB25B4C5001A8A210DA22BAEE359F4655A8E04502D694AC292C5842A7F2BA07DC27C8DCEC52C2C906349884C976ACBACA833BD6B53E4D8354A0352A2D199F8A57CD5C7B963B62C8E87BB8448527DB820D1335A01BB7DDA871C7B05016B92A0AC544547C1BCBB6A59C9315951017521389AFD2247E89B812E130E9B1794950CD0EF2C3585B93192136EE898A3E4369E515A05D0348AAAD04AFE0032782920F5203C670846F2030D07B68F74C9A5A97C2F16A5C3B9928F60E6BD6E118C8AB9633A6513EE886C18B603BDF62E5A802B58A4ADD29132C3E57A31C1C51C5380C480CF3B057B8840A27F36481E2CCC1004403C6664CB025B094380D9D0060611CB2DF33E35D67C6FD2CEEC16022BA264D4856F7EA8A69A584389DB16FA93AE5AF228EDF565A28358182A2C03E2919CFB5507F95798BB7D12247625283B8C091509646182DBC29C16CFAD045BB1C225EA4850EDA780996A0463CA0F3C9304127288FAF8BB9334CE43C64DCE872B71A938A5447E127630B2D1B409521925B3866E236F8C60B71F96C85067483E73150B50089032A9F998267B7C6AB0FABB3FCB348AB2714E196E6DE31C8A707A3698B26EE016FE6985B848B65AA585C6E5BBAE154681F7B51AB4CBB06B90E5211F79736B41B2502CF87121A915BDD4C145F2CF63C87253D52F574B484D18507AA70840E1772191896E5A536A216AD0172A7C1DF7821AA33697514247BF1C718AC7B9050BAB2844EBFCA3B3DB238DA2A6B96F1AEB46C9047001DB8024923676195CBD98AA02B7CFDA94048E37072B4A70DC622EB8E4D3B1E4C2A5058D379BF69B44B47CF947039476", "AF7272A13BBC90ABCA25AD6F374D039B8C86E67993445A7E45D41989574D7C28322B8D39A7F2FD4866D5E00C8E347D6D54D2D249000E6843E8C7A44001EBBF3224A857B8EDF9875E2EC54064973DCD469C05B2ABC7EDD564984E46DC36959E6220E2369B9229110E051ABC35C5518020D659B1AE482930D0D742E34BBBF9EE1C225481D4EBC1F0E4EFCC88C79A17B436D45ADED652FD1EA9C1F5694EFB753875E056083C5C4A00D4B5CACCF426839114DAC8AD8FAFD3380086B6F5752B45B13715D70E33DB7F293B17B48FDAB58C07CDA4B1F3C4FB6E1157B932D21CE8CE592E2A5306F5D432BC37637899E3CE08D3D87C9A5A4CD1A7D457529C2C9E0D66627CEE04DB037C7EE48B5A90A22855C5CE8C1A1641578B59E7FF4890276D19639DE8BA67915E0AA4CC6C07645F5F61867259357767B726D117EB8C05735108FC329FC687AB74E7D893A84A3B49CC211FB2E373A900E198BBF43442E68D3C4E90391D8F646D0F46309FA1D6F908275076A815A4BA4517AEFC3D11EBC1216E5F981FAEBBE1AFB4199F7619BE5E646E85D64A2D3D56C51F475D9B45012FA7EBF0A1035583817B7DAEEA41F862563F1BDF4B9B08B202145E17F1C113701099C64FFBF9C09436791929B3CE568C2B994567DB27353B05111243DA88FCA27CDC03711EA86D91B43E043490D8056C7E69888CA1FC2AC8010F035DA9B64D6B9DA6DDF94E15F151B68C4D192F3F39E86C573C32E1BA96F2C403A26712AA094C5A4F1F4E766DD4E37D1953FE970270536D1043758D37C31455325DCCEDAB1D98708C21B8A6949E72AC6E82DFD3E8C9805666C0F62DF84AC5D23E55BB2BC6947F10586D1E44EBD0C434AD3BC7188DD9B256A48D774F5FE1DF2AFB379B36F2F4CFF45C323BADCAA36D634C1FAF43A2EF1D698BE57284AF702C4684BE88754CFFD3BD14320D9E8A68F26BFB28287F1484C8557387D80F0BB03E8D088D8E10693DF74B7F0965F51EF85862383FC1E60F6FBC3EEDEE99AEC22FE2852E3858A03ED5D5FE7032FBB7925F9D84FB8793DCAAA6209A551409DC414B3E34A99AFF5BFF8D12911722AF96435710A7803FA56E475750219213D3BE864F03F3B9032F3C4B23E40AF96F420481DB1EF11892B60382655B810DB6041D0152A08CDF0B3FA009E0DEF4777A423502697F43CAEC506F0C5A07FF7F3633A50BB7A70177DDC4FF8627B00F4665717F0B4789212CECFBEB960E3D2A39794048504CEC9502B429C6901C115404FFEB22F3BE50ECE3EC8FA8BE935177996C605DE58C31297F1EC73182F44E1C1453F635979C1D05A6C7D84861C673FF817AB4D64A6C021030C536DB278D9AC8F7831322183C941438B4375A5033169A087B6485CB3ABE85D8B0259C667BA28721928E77D91775BA3B1D534DDCC038FBDEBF559374330667887A838D141AAE9975FB5BD4D39874D6A21A3BB9E2EBB83F8BF590C9DBE913730D8E83D6FD92D3817976631DCA0F6E0E0791EF803AFA30D8C2407017DF85E9256CA171DA3E37EBA869D9592FB42B", "C3CDD63DC36E0312D22C5C7CF6667D2F7CED36F73497D9905B8BA75BB79BE4A4"), + ( + "F0E83DD90A2966E3C6CA6625FE8392A575B501B760218C3889A4B6285580843085E01732D109B273064361561DA38832EFE42DF982BEAA16191681538D218D11234891C43398F3351E24599F028AF1A507ABA0891A606CC3F987BA0B5F92B0951E322A32F80109365F12A35C8DF2699598B5DC36B5F4191E970589124567BA56A531FA71D37773BBDB89F0F054F5C35B9D62C2C9641962EAAB57AB4FFC91A4C3F715E8DB1FE969B7B2101952D0BA27D303C1AB1A91B12D435C3B0C201A2FE89AA600A260F9BAA0415B45423D08597F2D20316950C6DA11C4E5837AA3D94DA141CBB4959DD5D11C925915B15255151B88A3158318B0CEDEBA0765B17FCB60BCDA4C17BC904FED129058265822BC733252A62796CFB290CFFD55080726678B3675A7EB8E23DC34CAE425F9109832D05DFA68C1EDC13B2EB69F23B23BFF92266CB4B0AB300836747D69D03D45F7CEF1C10F14EC8C927A393600228F4BC2CEE75232D68D4E85084FBA3B3716B110256B22C7318BF3889F015C7D6069958A6E4089A75898AF00F46B9DE6565B241B5E9370DBD4823C3841CC743E9DC1CBCEB71FF20005EA8A23B4BB15566C51E7E67D4407729BE416D013C03F792F26000B71C9849A467361716A00F1628E0B3F85B700C4D53452B09CD4A623AF0BC4A5B192B3B469FCCB3FF68A87A325A25F4B221658CBBDFAA26F9642006A4B25176B278C59EB365159656E32E99D990486661AAF5215A8179400179608B4589D31C3877A25387B645CAA667B1843B2C0F482D158AD6577C5B2E7816311046AC518FC8B8ECD866039078AEEF966120B32608B8D0FD9C058F9926679388B829EE42C17CD8483274064D89304630607437A123AF4BCE25BBEE57325060880818B2C15157D55E8B989565D8D64C80FF0BE18904D842134EDE6CA72A31D50AC6CF205345BB93EB5C806F6F216F8071701B1674A179BE2D9342929706D5944F7FC0E3BE6BF0679A1AADC4F5261A971C61DDEF2B17A31B236EC4649E9A113754F7329BE27151698AA544994BC0B016C7AC65737C24CE815330BFC6D715C2921609923D9B7B6B53DA483C409942246F294E204C918B6353BEC721683236C49B70D66AE1CFC37BE451A7B190DF7579E40515B0043826C7926FA9B5A35676D6F294405B41E5237CE09928C6D70C844C17716C33DA2C4B6C92A219307371626855858761A133370C616024C837A4A26ED6B01A5514EC1AA0BA5A0B8C7986D7C168E8C6160D18229A6A727EB7ABC118CC64C31A605351B972635A6A906393A9282471BF5E3C67B6C8138E93BE276170D844D02E234034352D3086E6AEC30E306B1E3C3A55D2935D02B05D065567ACBB0EE6C7E12A30C22B443DF187310E9C6529C4C5CFC48BBF0358748863A79B3860319C0D9321DA780E5E8AFCCE027DD8777A3A06465B95DA642BE729B12FF63C75DCA6AD253066E9C081808B57E6B1DAA15A3CF37B4FD2B13B684A7A7566F8123A397383905F58C541420AA2833D96233EFE35198CBAA577C693A0963FA2475A73348F15262B9B08B43497613A608AD22B661B3159539355265BC7CD903CBBB841424230E164AAFB538F43582C345179D168BC955B36F93BBB2E9559F0780CB7BBFB6C920B8806ED54252237B3905C07A6EA75797D48DB6310CD6A9448EAB4AA8FC60F78C679F4630AFF35DBBBA1FFF5612DC220BE92C55BC803F75B6B0656080628186ED022229792736CB47F29A4C2E9A4EC3A5583E0B0E629B3BAF0AC2E5F7C0E1529A49B231B006902506C80D7B29B87313F51AB9C7E5C97FC98C3F8A404D559D21DA3AF0E01D88C356284738AD17365EBC997D3B1C515452C3884F27234FB9C351DB1118C6396917F30344BC15D399CCE60C9B8A109D8DD86CB2781673C21F083C1339B4730EB92FA2935AF78C60197233ED955E4190CCB354732D039CB3FBBB0DEBC5BF046A91D7BA67622BEE9A3AB5E3B1CDD64C3C3BC305506A500A51B6B9B249489094677D1A021397610A4171143AD6CCB6769354D5C0ACE964CF552BB4BBCFE4F86AECA44138010A9D0074C9853564E74D46EB88C9FCCAFA24BE5431C0914C377BD4A7A2FA0A44561454925106265283602FBF319AC4284FAD3321DC99C5EC603E388BB60B426733A6C6DEE3463F5071107B061CC01DD3D673C3529495805522BA51EA0B9C4B1271417A06505230579B7BBFA45B1CB6BA127031FBE7BB98A032E6EBBCC1B4C4E19107DF6878D4F35B1D221A600B454230857F42718E2914172151CB435C5C101BC2647FA57390035CC7C4607EECE07DE20811481A2BE2897B6D133E21A2995C2892AF5332AA15116067BBC308234EE74EC1BB6B384895B28092A4DB5FC58A9904C81F0D0C44666817CB622A1DEB709A630B9A778B60E6063B523E9F080B48A0991BA78F28627D4B555AB8BC14EE975A95BA05FE2A76F677BDE402B7BA29BD59CB8AA8F0A485DB15B49CA6AD38A5226689F5EB1A5147344E8427C460674147CA43D439CA73625B0468CF5238029B08C2C543524093829138FA55CBF5C80BEE0635D7FB25B4C5001A8A210DA22BAEE359F4655A8E04502D694AC292C5842A7F2BA07DC27C8DCEC52C2C906349884C976ACBACA833BD6B53E4D8354A0352A2D199F8A57CD5C7B963B62C8E87BB8448527DB820D1335A01BB7DDA871C7B05016B92A0AC544547C1BCBB6A59C9315951017521389AFD2247E89B812E130E9B1794950CD0EF2C3585B93192136EE898A3E4369E515A05D0348AAAD04AFE0032782920F5203C670846F2030D07B68F74C9A5A97C2F16A5C3B9928F60E6BD6E118C8AB9633A6513EE886C18B603BDF62E5A802B58A4ADD29132C3E57A31C1C51C5380C480CF3B057B8840A27F36481E2CCC1004403C6664CB025B094380D9D0060611CB2DF33E35D67C6FD2CEEC16022BA264D4856F7EA8A69A584389DB16FA93AE5AF228EDF565A28358182A2C03E2919CFB5507F95798BB7D12247625283B8C091509646182DBC29C16CFAD045BB1C225EA4850EDA780996A0463CA0F3C9304127288FAF8BB9334CE43C64DCE872B71A938A5447E127630B2D1B409521925B3866E236F8C60B71F96C85067483E73150B50089032A9F998267B7C6AB0FABB3FCB348AB2714E196E6DE31C8A707A3698B26EE016FE6985B848B65AA585C6E5BBAE154681F7B51AB4CBB06B90E5211F79736B41B2502CF87121A915BDD4C145F2CF63C87253D52F574B484D18507AA70840E1772191896E5A536A216AD0172A7C1DF7821AA33697514247BF1C718AC7B9050BAB2844EBFCA3B3DB238DA2A6B96F1AEB46C9047001DB8024923676195CBD98AA02B7CFDA94048E37072B4A70DC622EB8E4D3B1E4C2A5058D379BF69B44B47CF947039476", + "AF7272A13BBC90ABCA25AD6F374D039B8C86E67993445A7E45D41989574D7C28322B8D39A7F2FD4866D5E00C8E347D6D54D2D249000E6843E8C7A44001EBBF3224A857B8EDF9875E2EC54064973DCD469C05B2ABC7EDD564984E46DC36959E6220E2369B9229110E051ABC35C5518020D659B1AE482930D0D742E34BBBF9EE1C225481D4EBC1F0E4EFCC88C79A17B436D45ADED652FD1EA9C1F5694EFB753875E056083C5C4A00D4B5CACCF426839114DAC8AD8FAFD3380086B6F5752B45B13715D70E33DB7F293B17B48FDAB58C07CDA4B1F3C4FB6E1157B932D21CE8CE592E2A5306F5D432BC37637899E3CE08D3D87C9A5A4CD1A7D457529C2C9E0D66627CEE04DB037C7EE48B5A90A22855C5CE8C1A1641578B59E7FF4890276D19639DE8BA67915E0AA4CC6C07645F5F61867259357767B726D117EB8C05735108FC329FC687AB74E7D893A84A3B49CC211FB2E373A900E198BBF43442E68D3C4E90391D8F646D0F46309FA1D6F908275076A815A4BA4517AEFC3D11EBC1216E5F981FAEBBE1AFB4199F7619BE5E646E85D64A2D3D56C51F475D9B45012FA7EBF0A1035583817B7DAEEA41F862563F1BDF4B9B08B202145E17F1C113701099C64FFBF9C09436791929B3CE568C2B994567DB27353B05111243DA88FCA27CDC03711EA86D91B43E043490D8056C7E69888CA1FC2AC8010F035DA9B64D6B9DA6DDF94E15F151B68C4D192F3F39E86C573C32E1BA96F2C403A26712AA094C5A4F1F4E766DD4E37D1953FE970270536D1043758D37C31455325DCCEDAB1D98708C21B8A6949E72AC6E82DFD3E8C9805666C0F62DF84AC5D23E55BB2BC6947F10586D1E44EBD0C434AD3BC7188DD9B256A48D774F5FE1DF2AFB379B36F2F4CFF45C323BADCAA36D634C1FAF43A2EF1D698BE57284AF702C4684BE88754CFFD3BD14320D9E8A68F26BFB28287F1484C8557387D80F0BB03E8D088D8E10693DF74B7F0965F51EF85862383FC1E60F6FBC3EEDEE99AEC22FE2852E3858A03ED5D5FE7032FBB7925F9D84FB8793DCAAA6209A551409DC414B3E34A99AFF5BFF8D12911722AF96435710A7803FA56E475750219213D3BE864F03F3B9032F3C4B23E40AF96F420481DB1EF11892B60382655B810DB6041D0152A08CDF0B3FA009E0DEF4777A423502697F43CAEC506F0C5A07FF7F3633A50BB7A70177DDC4FF8627B00F4665717F0B4789212CECFBEB960E3D2A39794048504CEC9502B429C6901C115404FFEB22F3BE50ECE3EC8FA8BE935177996C605DE58C31297F1EC73182F44E1C1453F635979C1D05A6C7D84861C673FF817AB4D64A6C021030C536DB278D9AC8F7831322183C941438B4375A5033169A087B6485CB3ABE85D8B0259C667BA28721928E77D91775BA3B1D534DDCC038FBDEBF559374330667887A838D141AAE9975FB5BD4D39874D6A21A3BB9E2EBB83F8BF590C9DBE913730D8E83D6FD92D3817976631DCA0F6E0E0791EF803AFA30D8C2407017DF85E9256CA171DA3E37EBA869D9592FB42B", + "C3CDD63DC36E0312D22C5C7CF6667D2F7CED36F73497D9905B8BA75BB79BE4A4", + ), // modified ciphertext - ("5FE5453ED8CBA2A17C2D32836F1B1E9DE665C32ACBE6AC0D58A543535C71056676E031B6EDDAB19CFAAB135346DE73AAECA10BB1299276611F66EB4A16733BA7004E0B24A269C00A6FC4BA4AD1BB393705F69888881B75A5024D45ECC69F50695D641392AB6207839D506C05D5D92646D34AA0AA3D9C49363D20873F8678C5CA77C4500DC9182100D1B04B80CB6ED15DF6752AD68CBB1FCC9456513484941E4E5732F367A5301A98E9772B09A65529F5CE4891BB86F3AB45D623F02A4EBAF74D4576389C31580224374DE79F5F260ED323793E54786AC06E0BD1CF7CC25B1856C04F54B99ADA9B138C53AE84184C383F7ED952C20469C6C4B1758AB15BA42082B7100B296A0167BAC9CA94CE454B0A646E487CC5D244883F7C50DEF18E44A02095B28DD80BBB13E950C0D20EEC821A21FC9B94608CE2C615E222269A6BAD1A09ABD0B70B4592BF052837BC56BE0E7348EFB01575D29F30746F8F496924A198376AB4977491945A539626A4B1880352D88565737FA42310BB645B9C184A3F1C58CA1C6D61223422DBBFA44180E30B841FC9BA11C92275BCB5641B77AA5290B8C288186989909958A3E058CB8BC8D3592F39B260CC382A2DC02C2EB17201CA01C3433A15A47C41527687954CB742BFA2C1B542295320ABC004D74EA43C35745AAFE0268CB5771A51156AB9E153FC905FF8B1C8C74C546EA72347E1A8DA6475F287AD4B5396C2AAB9623C7B60EC53523270809C9870A07D2EAB9262C330F0B551F56A992C1164BFE098059366D2A7C80603CBB3B0130169BAF8C5670206092713C380E33AB6E1BDEB847FE6C216E5BC35670333298194659989F2216B5C2781FB77829AB6B0E5833F6078139A37269AD754D8F97C8BF59F8020C32CB92B7690236F032585342DB542CF63C95BCBC95F0060901CF288E8A90668474ECAFC8BA92A4D70F96E007D800D4ACF6B9AC93A859CEBBBB0D9544B17254901439D54CA77AF7C0A1F76995599021F9812D20852D2A450DD6126F4A0C427E67A8492BEA2AB8814B0393D382ADE903067D4BB90527A07D31CCC30A0B8D4AFB8BC6E55A512345B637DABB8F18B6665E0AE8D23A7CF70110B4B967B691F9DD86758E348AC615E4F33112EB5A173F21430508CE4A00ABCE6B1ADC1C062F11D6B49C20EBA51336531A00B840CD3B1B9741CCE083E0B515F0ED24EF466AE365228F2D854B3F5C579F7C9F407AE94A62504B41B169CC4B3F7B9CCE79E7D625A7DE9025A6041877C9AA1D81F38AA44ABC11469441DB7B6B61E180E3ADCC21D561E2866011E0031F6D26F17A31FA211236AF3AC19A97B71119E7A27577D18B35A975A46DA5AFB1BA40940C58D72A250648B7F537E22101241E0B78BB01F3181C5DA765C75036A8379CD99283F4FB82236EC846C7B11526C09574A9E7E24BC4006A52A635975B6712E7428EC08B6F115352FA42CEC02617274C0C9E06D16EA7E3C294A1AFC68EBDB808AC7942FC7958CB57A5076B1DDE56080B63CCA8BC8E2341698EA83FDAA3394F86AA06BA48A0A697013A59954B0DC83CE28C02A74AC11430C17CA00AF9A7946DB75C14902B3E5611FC2D99755DA4C1F4C3EA9FA6B9983C3C9325CE119AC713068F5F3A025078558469CDB552DE834B48F23661B68A463769EA7509F4BA9C51BEB605D915970C79AF9D19DF84B9090165257495E717541D5D695120B3E42000DB47831C94AA5F485C0EBCB9E583B6181F3CA07904C54911D680701F85947443784E1B67F0568042636B81286781F10A892D60FC25B8E656483CE62524918678ED29CAD3703767851E8F43469C8B4A3F07E61B6A44C418ACA1183DF861901646AC6BAB3BD607847F5A5C2B76A0512750B0908ABB986B2388711542C663B2BA565B3D6D34082B85B9E683583B195865A9889C0110AAA00B87336A767AC81D70E0B422619FB95EE05736CB53A25E43300A953F59CA49AD61C6CC423CA69648D6C18A2B30B5C8432D2E92D403B71D7865EBB3529A4F0641BA817C43C4C90FC18C832699C896FA41497CAB46604643E31979CA75487E1EB0428AB0BE5C09428D3A0F1183064BC1139D163197BB297F3C5DC83CF7F7A577D759D336233E4982BC8029936B34E73F274FD244032935FE5C6B75D99CD244742961663F10B1C12C48428D53E83DB69E1595B72861093492D133B269DE7AD7ECBC6BCF55C67F919ED65CB6D83A239554DE452C65FD472144A5D6544C95429BD4425032BA901B4305FBFE50C645663D3C24471503B32E44A0B0AAFE91C41F6F03A4E04A57BD66DC9160FB6158823BA5B35EA06C6659728670366175E2846C75FD3A547C711E545534A056AE5BC6F5E23BE89D4C69E288C6218800DB4BF47F57082B27A0B07A88B976780C965661B8362672185A94E3031970E973AA8E728E2D3175CE10BD9328ED280B7BE66CB8071A57DC12532721F4C4627930A923B7C9AA169C7C3F63660C18C543B5177166646773F5F464FC8CCA6E0DA2B8B3327687ABDD73940CAD24AA4430D39C7457FD32F789B6C77A3BD94878B0183BB85DC71E7E99CB4E99D4E19433831343F2C9451A74A45FB7366CC7A27A136FAEA49CA676D6802C3C445AB2361469F5431568593B26A56B643BFDAFA6A1EA6B8D7F36637EB2E72C32C58BC8F88B04F3AE8480B971798F731AA3AA74122277AF876B8B51FC823589E73CC3DC2AD3154205228107A42AC0033BB8B794E96A464453CA2683B38E27333425827A4B81367053A25560C4EF41A0879C00CCA2709425FC0B89DFF5223BBCACF2ADB74ABD115ECD214F251730F8BA244E23542ACA707C22ADB4AAABD39A2ADA713095A3E9CFB834F780200909573994E583359A9469130B389681B8A5E7790A2D346AF485986E8B7C5C9B0FC21CAC735A0DDF82D140C24017187982291257C4B993367C07133AFDB9BAEB39664122F4CDCABBC9475F8A77AE522BE8EE639D66A6E2528700882652BF661E510C2C4C31384B556A8F3BACDA57707419299A8BC0DB76D737826CF9BCDB24BB63778389C9897C55235C2136AB8D31CF102990E11063E11036CAC27F11B633A7017701C6163D43A6837A5104A6BE104C6EE28CC6DA72AE318745EF3C4B64289E68CB7AF0C177E9C6D814096FB5416E9EC3B2ABB44141361AF6AB0FA73A521C289721766BB802AE411BDD53B0B94633B998C3E5761C11048A9F2416D51278154FBAE191C8D993C8CC48704B1A380132B0E86DC043FEB6470A45F67E7C19585018E43A1E1C80B6C3C9231BA6BA3DAB2B2EA0E8D5E194BF65B2CDA35C7226B7644FEA747BB4330C2DE4AB95D4730A2203C5F466F50F7038948608BAB6818D712E4F1C858394471CE53E6A718AD5D839F8B22AA8FDE7303FFE3A02EC7789E62121B10FD30", "9FD50E7D85B73CFE4F5D945CEC7E04B09415BF39204179EB8FE46EDC221EABCE1B9DFA89668BDE0974EB23FD4FEDAB0CA3D67862BA9C50FA957FAB872F42BF2C538A04BC6C420BE8133A521006721F6C051B4AB58A407A0FDEB826A643804934E3CB5D770A9804843CAA6D6AC371216A0EEF95AAB97E690F98E1964AB1E2BFB94228CA3C8F1AF91ABF6E57391ACC15418947DE7F8E23E3C789001740FDF2AEAF4CC34CA2940E118617FD5F9685AD89C1DBAEA3F8457D6A5567B752A67E39AF603D7327605E26CBC9CB1D7915504B459C3F22ADC014CA19DA5B8BA5909983EFE8238E28FFD37A074065357A1DB649ABAA9C48B4E33CD87C33F8725AECDA8C828A68AB823D2D34121774F2B9A691502739DB30F29D6324431E44C866BEFB44760A142C9C8945D3C0FD14470A16CE659370391B92B853AA2A2C8B24A8ED8D41CF8F3611C3AACD2F30FFAE25DBFF4A2FA63DC067ACDDD9FC472D873BE44F80D5EAFE9417ADFB3F585B2DDC6BB5A766D6B4D28AF9692614E6C92F3FF7CBFE51D752A3255513BC729825A4F3EC417E4B7F7E0B4FC6E527BC4D67920368D7328D49AF05FD8A632A91A891815B04FAB1E22EB97EF4686E3B39B4646B7DAB4D6B661FDDFB6FF5D4CB428B77757EA79141A05DF2E8943FCA6B7935FD26351E827D1E6C7551AE4AC04E3F99C4ABF5D36F0AA134E06243F7416C667BFF00543A1E2DB17EB9ECDA705A49A4B4C6D272274612D3AF4CAB32B3A59F88EEF845A191CAA53042E00A6C58F856BDD7C1F65841226D005DFC7683CE4866814295769A6B37BB7141308E183A30A99CE95979437B47022056EE6E96424548A05242CD38A9C4676BE085AA265968DA4246B0B964A011615D06480EEFF7B3436DA24B5754859A85EBCB888AFFA8A0F8E514D89D6AB9C2C02AB502C39AFE430D85FE52865E6E6E4D65041CE5F3B60EC4FE7E0A47D05735922DF8DAAD7FD44299BB85ED6B9E8B40918F3360AD741CAB6F7F425D1BD1D6C905E1E968031633C563E52A587973A7729036BF462986CC3E4D31269FAA1907643034E703B62A45B31E6687D3EF0571EA522C2E43449901FA24C2AFBD50E4E350F01A65A6D5EDB7D3C83B1258E8E8FFD63C068BD73DF68BBB16FB1700530EC982247A033595A3B87469EDF357CF4349F02364CEEE63A94C3AE78BBF31BFD57B74FCC2DDC20D230567E4ABE91EC864021D4D6B62D976C6CABBDB92F0B310C5BF00AC1F7CF260EEE566687740DD8E85D271D0B21E5F0AE3E71EE00B1CEB5A63A29AB42F75A25096CEE78067636136701CDFE230C0A61FB2687F3556B53445B2D63F712C7584606336B5CD87793D8D769F6CD39CC7C1D3A86E7F7F71B8DE346DB46B1644EB380DBAEAA3491F6B60DCE302B2F8BA1C0DB608BF6C0E065B22627966EB3794BFCDEC83FB3E8B1FBE164284D32EBA7402853079A8BC0BF6EE3AF42BFCC00B6DFC70C4894EA265D31AA49F80FE9502BA3C37D972AFB8F50E0AE83FA76455507B4F5F0DDEE0AD5F363E4CD161CB7C79EF7858FC", "6DB14F5FE746946F068C779CB745A6AC15F1037D9A85BBE7DC9A66DFF620FEFF"), + ( + "5FE5453ED8CBA2A17C2D32836F1B1E9DE665C32ACBE6AC0D58A543535C71056676E031B6EDDAB19CFAAB135346DE73AAECA10BB1299276611F66EB4A16733BA7004E0B24A269C00A6FC4BA4AD1BB393705F69888881B75A5024D45ECC69F50695D641392AB6207839D506C05D5D92646D34AA0AA3D9C49363D20873F8678C5CA77C4500DC9182100D1B04B80CB6ED15DF6752AD68CBB1FCC9456513484941E4E5732F367A5301A98E9772B09A65529F5CE4891BB86F3AB45D623F02A4EBAF74D4576389C31580224374DE79F5F260ED323793E54786AC06E0BD1CF7CC25B1856C04F54B99ADA9B138C53AE84184C383F7ED952C20469C6C4B1758AB15BA42082B7100B296A0167BAC9CA94CE454B0A646E487CC5D244883F7C50DEF18E44A02095B28DD80BBB13E950C0D20EEC821A21FC9B94608CE2C615E222269A6BAD1A09ABD0B70B4592BF052837BC56BE0E7348EFB01575D29F30746F8F496924A198376AB4977491945A539626A4B1880352D88565737FA42310BB645B9C184A3F1C58CA1C6D61223422DBBFA44180E30B841FC9BA11C92275BCB5641B77AA5290B8C288186989909958A3E058CB8BC8D3592F39B260CC382A2DC02C2EB17201CA01C3433A15A47C41527687954CB742BFA2C1B542295320ABC004D74EA43C35745AAFE0268CB5771A51156AB9E153FC905FF8B1C8C74C546EA72347E1A8DA6475F287AD4B5396C2AAB9623C7B60EC53523270809C9870A07D2EAB9262C330F0B551F56A992C1164BFE098059366D2A7C80603CBB3B0130169BAF8C5670206092713C380E33AB6E1BDEB847FE6C216E5BC35670333298194659989F2216B5C2781FB77829AB6B0E5833F6078139A37269AD754D8F97C8BF59F8020C32CB92B7690236F032585342DB542CF63C95BCBC95F0060901CF288E8A90668474ECAFC8BA92A4D70F96E007D800D4ACF6B9AC93A859CEBBBB0D9544B17254901439D54CA77AF7C0A1F76995599021F9812D20852D2A450DD6126F4A0C427E67A8492BEA2AB8814B0393D382ADE903067D4BB90527A07D31CCC30A0B8D4AFB8BC6E55A512345B637DABB8F18B6665E0AE8D23A7CF70110B4B967B691F9DD86758E348AC615E4F33112EB5A173F21430508CE4A00ABCE6B1ADC1C062F11D6B49C20EBA51336531A00B840CD3B1B9741CCE083E0B515F0ED24EF466AE365228F2D854B3F5C579F7C9F407AE94A62504B41B169CC4B3F7B9CCE79E7D625A7DE9025A6041877C9AA1D81F38AA44ABC11469441DB7B6B61E180E3ADCC21D561E2866011E0031F6D26F17A31FA211236AF3AC19A97B71119E7A27577D18B35A975A46DA5AFB1BA40940C58D72A250648B7F537E22101241E0B78BB01F3181C5DA765C75036A8379CD99283F4FB82236EC846C7B11526C09574A9E7E24BC4006A52A635975B6712E7428EC08B6F115352FA42CEC02617274C0C9E06D16EA7E3C294A1AFC68EBDB808AC7942FC7958CB57A5076B1DDE56080B63CCA8BC8E2341698EA83FDAA3394F86AA06BA48A0A697013A59954B0DC83CE28C02A74AC11430C17CA00AF9A7946DB75C14902B3E5611FC2D99755DA4C1F4C3EA9FA6B9983C3C9325CE119AC713068F5F3A025078558469CDB552DE834B48F23661B68A463769EA7509F4BA9C51BEB605D915970C79AF9D19DF84B9090165257495E717541D5D695120B3E42000DB47831C94AA5F485C0EBCB9E583B6181F3CA07904C54911D680701F85947443784E1B67F0568042636B81286781F10A892D60FC25B8E656483CE62524918678ED29CAD3703767851E8F43469C8B4A3F07E61B6A44C418ACA1183DF861901646AC6BAB3BD607847F5A5C2B76A0512750B0908ABB986B2388711542C663B2BA565B3D6D34082B85B9E683583B195865A9889C0110AAA00B87336A767AC81D70E0B422619FB95EE05736CB53A25E43300A953F59CA49AD61C6CC423CA69648D6C18A2B30B5C8432D2E92D403B71D7865EBB3529A4F0641BA817C43C4C90FC18C832699C896FA41497CAB46604643E31979CA75487E1EB0428AB0BE5C09428D3A0F1183064BC1139D163197BB297F3C5DC83CF7F7A577D759D336233E4982BC8029936B34E73F274FD244032935FE5C6B75D99CD244742961663F10B1C12C48428D53E83DB69E1595B72861093492D133B269DE7AD7ECBC6BCF55C67F919ED65CB6D83A239554DE452C65FD472144A5D6544C95429BD4425032BA901B4305FBFE50C645663D3C24471503B32E44A0B0AAFE91C41F6F03A4E04A57BD66DC9160FB6158823BA5B35EA06C6659728670366175E2846C75FD3A547C711E545534A056AE5BC6F5E23BE89D4C69E288C6218800DB4BF47F57082B27A0B07A88B976780C965661B8362672185A94E3031970E973AA8E728E2D3175CE10BD9328ED280B7BE66CB8071A57DC12532721F4C4627930A923B7C9AA169C7C3F63660C18C543B5177166646773F5F464FC8CCA6E0DA2B8B3327687ABDD73940CAD24AA4430D39C7457FD32F789B6C77A3BD94878B0183BB85DC71E7E99CB4E99D4E19433831343F2C9451A74A45FB7366CC7A27A136FAEA49CA676D6802C3C445AB2361469F5431568593B26A56B643BFDAFA6A1EA6B8D7F36637EB2E72C32C58BC8F88B04F3AE8480B971798F731AA3AA74122277AF876B8B51FC823589E73CC3DC2AD3154205228107A42AC0033BB8B794E96A464453CA2683B38E27333425827A4B81367053A25560C4EF41A0879C00CCA2709425FC0B89DFF5223BBCACF2ADB74ABD115ECD214F251730F8BA244E23542ACA707C22ADB4AAABD39A2ADA713095A3E9CFB834F780200909573994E583359A9469130B389681B8A5E7790A2D346AF485986E8B7C5C9B0FC21CAC735A0DDF82D140C24017187982291257C4B993367C07133AFDB9BAEB39664122F4CDCABBC9475F8A77AE522BE8EE639D66A6E2528700882652BF661E510C2C4C31384B556A8F3BACDA57707419299A8BC0DB76D737826CF9BCDB24BB63778389C9897C55235C2136AB8D31CF102990E11063E11036CAC27F11B633A7017701C6163D43A6837A5104A6BE104C6EE28CC6DA72AE318745EF3C4B64289E68CB7AF0C177E9C6D814096FB5416E9EC3B2ABB44141361AF6AB0FA73A521C289721766BB802AE411BDD53B0B94633B998C3E5761C11048A9F2416D51278154FBAE191C8D993C8CC48704B1A380132B0E86DC043FEB6470A45F67E7C19585018E43A1E1C80B6C3C9231BA6BA3DAB2B2EA0E8D5E194BF65B2CDA35C7226B7644FEA747BB4330C2DE4AB95D4730A2203C5F466F50F7038948608BAB6818D712E4F1C858394471CE53E6A718AD5D839F8B22AA8FDE7303FFE3A02EC7789E62121B10FD30", + "9FD50E7D85B73CFE4F5D945CEC7E04B09415BF39204179EB8FE46EDC221EABCE1B9DFA89668BDE0974EB23FD4FEDAB0CA3D67862BA9C50FA957FAB872F42BF2C538A04BC6C420BE8133A521006721F6C051B4AB58A407A0FDEB826A643804934E3CB5D770A9804843CAA6D6AC371216A0EEF95AAB97E690F98E1964AB1E2BFB94228CA3C8F1AF91ABF6E57391ACC15418947DE7F8E23E3C789001740FDF2AEAF4CC34CA2940E118617FD5F9685AD89C1DBAEA3F8457D6A5567B752A67E39AF603D7327605E26CBC9CB1D7915504B459C3F22ADC014CA19DA5B8BA5909983EFE8238E28FFD37A074065357A1DB649ABAA9C48B4E33CD87C33F8725AECDA8C828A68AB823D2D34121774F2B9A691502739DB30F29D6324431E44C866BEFB44760A142C9C8945D3C0FD14470A16CE659370391B92B853AA2A2C8B24A8ED8D41CF8F3611C3AACD2F30FFAE25DBFF4A2FA63DC067ACDDD9FC472D873BE44F80D5EAFE9417ADFB3F585B2DDC6BB5A766D6B4D28AF9692614E6C92F3FF7CBFE51D752A3255513BC729825A4F3EC417E4B7F7E0B4FC6E527BC4D67920368D7328D49AF05FD8A632A91A891815B04FAB1E22EB97EF4686E3B39B4646B7DAB4D6B661FDDFB6FF5D4CB428B77757EA79141A05DF2E8943FCA6B7935FD26351E827D1E6C7551AE4AC04E3F99C4ABF5D36F0AA134E06243F7416C667BFF00543A1E2DB17EB9ECDA705A49A4B4C6D272274612D3AF4CAB32B3A59F88EEF845A191CAA53042E00A6C58F856BDD7C1F65841226D005DFC7683CE4866814295769A6B37BB7141308E183A30A99CE95979437B47022056EE6E96424548A05242CD38A9C4676BE085AA265968DA4246B0B964A011615D06480EEFF7B3436DA24B5754859A85EBCB888AFFA8A0F8E514D89D6AB9C2C02AB502C39AFE430D85FE52865E6E6E4D65041CE5F3B60EC4FE7E0A47D05735922DF8DAAD7FD44299BB85ED6B9E8B40918F3360AD741CAB6F7F425D1BD1D6C905E1E968031633C563E52A587973A7729036BF462986CC3E4D31269FAA1907643034E703B62A45B31E6687D3EF0571EA522C2E43449901FA24C2AFBD50E4E350F01A65A6D5EDB7D3C83B1258E8E8FFD63C068BD73DF68BBB16FB1700530EC982247A033595A3B87469EDF357CF4349F02364CEEE63A94C3AE78BBF31BFD57B74FCC2DDC20D230567E4ABE91EC864021D4D6B62D976C6CABBDB92F0B310C5BF00AC1F7CF260EEE566687740DD8E85D271D0B21E5F0AE3E71EE00B1CEB5A63A29AB42F75A25096CEE78067636136701CDFE230C0A61FB2687F3556B53445B2D63F712C7584606336B5CD87793D8D769F6CD39CC7C1D3A86E7F7F71B8DE346DB46B1644EB380DBAEAA3491F6B60DCE302B2F8BA1C0DB608BF6C0E065B22627966EB3794BFCDEC83FB3E8B1FBE164284D32EBA7402853079A8BC0BF6EE3AF42BFCC00B6DFC70C4894EA265D31AA49F80FE9502BA3C37D972AFB8F50E0AE83FA76455507B4F5F0DDEE0AD5F363E4CD161CB7C79EF7858FC", + "6DB14F5FE746946F068C779CB745A6AC15F1037D9A85BBE7DC9A66DFF620FEFF", + ), // modified ciphertext - ("17643D9422A420CB6561F5A2FCF64B86B39A5AB353AA6A2799F16011528B7FD02432B33CF41602AE297338EB45E6B91E003DCE3041C3F380BFA4224C6D0B14B73C4178A1969F9B6DF2765E0389387566910568B561E7151AA12D3C8B6BA37C8A77A4BDDE28A4DA197CD199AAEC07AE2890A5EDD9694D6C3737620BA52B1E13494E356187AB9029AD25CD4F16CA49882E48DC54C3F355C99569004B53AE7882395324CF1C34CAB02B253B3280D9C0F3AB3EE2627618704C781C4735A780745C1B9097B6A4075DA0A98D18B0244D459DE8A2223B75993AA6530AF4570525623E339698427EA1DB625FC9658A72C6D2A57D0A4018199C84305BBC80C72DB3C3606E7856098B96A5E6358AD19CC232968D8468A5BBC1160ACE74D28FAF0985485232CD14ADCCB2976FE5C38C21C26AF51EE8E13AE4C7764505C282552684D40D1D30163B126EF79C29B822A37175C3D8142122C97AC2AA084F6225F1A839F4B799631946FC08B609C1C57D678B59C8BA24BAAE56113857832430E0CD372543536B40DBDB4721161A91E53E4A35072BB3CC55254C611002A874472A549DD6D77CB7F1A566DA09F75C3D16BC374B2A1F82D12DA2A7B08506C2D6E6912B13C36AE9692D93776FF57122409AD1C1AC1624A5B633750502417F5C8670B8BE798548E4201F4A7BAA55DC0F52B564AE2342A2091E2E35B3A01532336A4A46E49E498C7D5D2B0CF047263ECCC31118CEBC743C7B582BB93242ABC7A15C2A88FCF3722A9612430335B993B8DE99865B6A67F430CE64F77C352A87E1237640C51D8DD9ABD1035A9542043F63AE89C094E3DC9F37A498169137B5A6A7B4083FD0C4033359500798595C703599F4BC442A1221370ADC8BC03C94744B1B1366EA14D8E41CF01C78F69BB987547912047F668B00D35001FAB3091B239CBEE18260B55AA6B1AD3C84A6610B8ADA683256353F67E93884D253EF04870554BB70508A3AC642CBDC1DC16838B457B3EA2CCEEB9332291605049039BDE507193723BE731B428574B7089BCFF047B708213D8BA676E790169492C7A57C0796544F4CC779043848F7C881C6517ED0A37BBA8B3983A76DB56D1164A0BD846CBA551A4C8032E0D35D6D6BB8BE706893551528E0B2F86A4C7B3354F6FC52EB0130928BC5ADE0113605AE9F36BDFC9990B1302E09B44B592B467FE24672E46F5E9B472030156BDB21533C559B161689C979D0D4C5C039447F628537073CF17B8AFDC44735A4C553466F205BB528386E5C0A9ECE76933E665D7A517EB1D5637B949FAE965499293791A63B6EAACFC5D63BDCB0C21C3ACB605848019ACFB1F95B783803776B00F143ABEBE2ADCE561E517AA34197A7B7F88A6A1061E50178C1921A296039C9DB3397807C74C681845999D2073BB8F17F86C16980542282E86148DC6FAC842E9722CBF2BC7182C7B70152856C52B09C748ECED14F22625F52475ED0C559E7688A944372DA013D08A23B99B534BEFC21A72758EA67757576CDD5690873D5AFA388C60A7B635542A02852324318CA92CB899F98BB2AD09034D396F7B9A64B76CA7D86118728085F1A60F2E273EE1401518500FA46068735B7B6F1C075C89A0446A04BA169047BAD23C68F30585EEFD895BB6C1305B325993B69368B7F4B1151E9490210EC18E8090E47F171523A6EDD21809AF6ACBF899CC63A49673B45DF90681414C904B0280773CF0CEB3FDB206BD8CB0CC864A3B8F6C69F3930BE71868028A738588700535663E34642E11176CBBAB5D71BED896683F31A4A849700BC97BBB47540D54439504370E205ECABAD4A151B1AE05428E91FE7C2805A374ADD1C4EF642683FD2ABA5D82E83192BE43A184DB41F9FB8B62C0C90FF0502FC37891F13C6C2752EC3F9636DD6879BF0A6ADFAAA8DB66B0C248FD4A73B51760444F67CF5140EA1405600A119FC086966592660E465B4886A12B03F5749799E31906A6ACCF7F964D1160E1D3B64F55413E4E04251DBB295245BF671442B46C3BCB150F242B545F279624B4C14990EC30A79EF2A815A261E049193B317BEA986675510B6609BB127C0457FF0CC56DC85BE863634851D30D5CB1550116315450D7A82E5248C0CA1B68B1B17793903F67B5F86F83374A2CFD3DCBEE3D1781C7480F7697A7FFC8BF813CB7D2933CEC6CA075C6CAAAC19D0C83324A034E8F94A37E99FDAB7918A8587AAB8AAFC75C5C0147B088407ED82CE482C1AF36911BC701122E3B6D5EB2F4BD806295ABB17E208B2C7218D5B624D39A570BA30B34678B0E5C97247B2C0C18D377174B56977D5D7247F4A8936F9A07DE2553111C0CCFA07B512B0E0A572559AB5CDFC95C99949B6240A118AB9F6BC540B427C8E5A19A95AB1A0DAABA1D63581802F41110FDA1A675705CB1A43790BD6593EC331EE54A303DCB4C82A28B32421D06C453FBC346A97B0A7482F01DBBADB136FB1DB7B8A5B7837A2C988B9A534484BCB5CC3814A438C432E88788C47E40E88735FC40B542631189B3BB8C08C052511AD2D9034D4CB6A0C352590680D0BC70EC4051BE0B25B45D4C8CD645AF6F99A90E780E574544D1AB1A3E3652606790EFA36880C983F864B954179172B90F7BCCB79EB59EA25A4A51B95950745A3CC6BE515CE1174458E8BA64769275283B63842781645187125737AF1CC01B58267B128E2C93E4E53C5F0FBCD6EA8398F287CF2708FB86A0566B118D64984CBC4AB62580DBB9CC3866360B0153F6D135648529C28428A3C297170E32AA0211E6B55734D45037CA7194EBBBF5476A6978488B617887FA2974DB459B1D868B6D4AC21B1ACB0299FACAAC10D76361363082FEB5DA0261886B59801CB324341113942625613A343B4475F11AE2C7C2CAC4739F1F423C6992A5B24B5481B2D8DC72FC4625851D69C249255B6E72D2B141FC1183D051784E6CC5DB627CE0B542060978F3A3083B0E25CDEBC4E77FC549F80162979C83946AD92000E0E531FE5784AD32C75BA23C941E60B81A52488C0AF99C6BA9EA04ECA1BCF017731FB54C0A125B71C20C04156472E697F85D406B9E95374B68156B22935F31AB91585DCF85D18B4C49ADAC4D44015BCF54CBFCC7A8BA93CE7285744DCC57B22303242243FB4247A3113E3DB1343226D914B017341C983516292487EC05760E4FA758D7373AC9937E966348D5309DC078C824382246BC3A81A23AF9A59A0482DD7865CFD73BAB6239F67B4641ECC42E9375CABA5B228F87D610AC1FF9B2F96CA78A84167FAF7898276110D3ED9DFA9C6E07D2A99226F735B798A734225D807F831FE53CE823C0E68D78A7F6DA83D16A655753E24DACB617C25D329B6716FDAC6A334A7F542BDCC7893905DB3D5C01054AFA79EAF44951CFBB6F8F4F68489BF99474A5434", "DBC1AE2567D1980DAB86D677692C088D90626920E556C5C7B79A91899600A6C931915A3908579A3E491DF61668012C34F8CD08D999F86A8D60D4DBB5E01D2B5971CC3EFB60DD33EA13A04C827C8D9E19093DB5A267596579F031930F2EFFAF400F33F37C6357BFB55FAD4D144A233DEAF34ED99635FEAABC0249D7FE6CF7038F434D6C06CD8D3603DAC6C39EDF4798699CE8966B6AC7E8E93A6B04966B543D326782DC4854B59E1D5F39A7CA7FB8BDF297B4F4CB96244A1B0A2DF353C79626868DBC3C310C8C406D1667707FB21C6A3A537EC39EA63E430AC72A48B36C3DB460463E28A5487DC6B68A88119DFAA7FF0E62C01B16D4AEDF3B7622E7A212797545AD1F521DA4FEEBC0DFE5F118466A7F40C32416D2CFE143CB4674298E965F3E104885D5E260F41812E06AA44E6CD6AEB415D5EE5DDBCA1FBF0A78ACFB47C826016A124645395A432F8A2903C48C31BA394C9D2F20197551A96D7794324811CC1A696A00CC11B811837A12D058205690CBD7B64D625573CB004324FC0BCF2AB2804AAF34D4971BD0016282111D480FA545DA30D8A185D8FED891F6A42A66190FD3AF48DAB05531E7E9DA4C5EE03B30CFC96114DA679529459C4854C5E06FC0DE1953181293A9E6E8F3ABE56C58C31BF930237D72F25640714AB06E93A061370C5D2B8A7656C00CEFBDE1B4C3D934F2A57B6BBD78EBA66D6AD73C4448CC4A8DB51CB6DD4C8E1FCDD667DB9D9A16AF69F870B32DEAA611E94BF3CD59E82CE66DE287EAFEA4799D178A6D6BE1C7061EB5DCD93E34195601CB0F48E8C7163F0B3096E5D8FE0397F84BB9E8E849D3E34DC8599514107EDB43D25C69AB8F61431053DC326DF5E9C776F67F1A7B101179CD1B450D0108EB7AAC7BFB95597614D83034CB66B5D878F7200C7158046870B79F5F41024A5654DC5D51011F6C176BE6EAD856404530EF43AC5506A1269F5A3E934DD5F045E60325252DD8BFAE7F10D15B4A3DB919DD493083315E958D3B7ED09904B2B587167E889498025E910A6808DB76FF5A4B6CDDA73D95B4DAE32AF8144D27E1DFF1523A90D48B1CD81D0585B77C1E9723B424F7A380D95046E266DB8A1EBA08F15FD158758852F2BA7A5FFC096106735FF3BCF1E8D8C99049DEEBCE5739BBD3CB28D19DF136AF4C6F38FFB572F72D8B67D969D13889C5F6BAF2E64BAE7D5B7BE2B567335ABE0C6F55C285A6B78BCD2385448F469BF1C6F03124B67434FF2E929175B7D195917A9ED3B8A8806BAD9A6B522A40C1BC3B4F454E3B00A7E745BBB8FF6D191C6EB59CD3BE0FF5944B0E7F3BB3543674699FA90779D089190A482CD50D4B72B9C9B2B43A8CA18DE8C61E23A5E637B61281A333B54CD7A2DE2A249A998D36BDB6F1F85E49BBCEAD253D4447398349706DA1AA4D0FDFE55A283EFBDCCFFBA8A502F9FE5CB3B16176B3B3CA1B5825325809C6CB60A178CB703FF9ABC6BD2A7FC95514A7693AA32F039436BAB30BE90F289D61579B38FB0005632850419AC6E31709C0E8AE6C8EF1905AB5B2A4D22D", "F624D36A15FCA03A832E0C39D0A0019C1CF08A53516080038A086E83F75E8C5D"), + ( + "17643D9422A420CB6561F5A2FCF64B86B39A5AB353AA6A2799F16011528B7FD02432B33CF41602AE297338EB45E6B91E003DCE3041C3F380BFA4224C6D0B14B73C4178A1969F9B6DF2765E0389387566910568B561E7151AA12D3C8B6BA37C8A77A4BDDE28A4DA197CD199AAEC07AE2890A5EDD9694D6C3737620BA52B1E13494E356187AB9029AD25CD4F16CA49882E48DC54C3F355C99569004B53AE7882395324CF1C34CAB02B253B3280D9C0F3AB3EE2627618704C781C4735A780745C1B9097B6A4075DA0A98D18B0244D459DE8A2223B75993AA6530AF4570525623E339698427EA1DB625FC9658A72C6D2A57D0A4018199C84305BBC80C72DB3C3606E7856098B96A5E6358AD19CC232968D8468A5BBC1160ACE74D28FAF0985485232CD14ADCCB2976FE5C38C21C26AF51EE8E13AE4C7764505C282552684D40D1D30163B126EF79C29B822A37175C3D8142122C97AC2AA084F6225F1A839F4B799631946FC08B609C1C57D678B59C8BA24BAAE56113857832430E0CD372543536B40DBDB4721161A91E53E4A35072BB3CC55254C611002A874472A549DD6D77CB7F1A566DA09F75C3D16BC374B2A1F82D12DA2A7B08506C2D6E6912B13C36AE9692D93776FF57122409AD1C1AC1624A5B633750502417F5C8670B8BE798548E4201F4A7BAA55DC0F52B564AE2342A2091E2E35B3A01532336A4A46E49E498C7D5D2B0CF047263ECCC31118CEBC743C7B582BB93242ABC7A15C2A88FCF3722A9612430335B993B8DE99865B6A67F430CE64F77C352A87E1237640C51D8DD9ABD1035A9542043F63AE89C094E3DC9F37A498169137B5A6A7B4083FD0C4033359500798595C703599F4BC442A1221370ADC8BC03C94744B1B1366EA14D8E41CF01C78F69BB987547912047F668B00D35001FAB3091B239CBEE18260B55AA6B1AD3C84A6610B8ADA683256353F67E93884D253EF04870554BB70508A3AC642CBDC1DC16838B457B3EA2CCEEB9332291605049039BDE507193723BE731B428574B7089BCFF047B708213D8BA676E790169492C7A57C0796544F4CC779043848F7C881C6517ED0A37BBA8B3983A76DB56D1164A0BD846CBA551A4C8032E0D35D6D6BB8BE706893551528E0B2F86A4C7B3354F6FC52EB0130928BC5ADE0113605AE9F36BDFC9990B1302E09B44B592B467FE24672E46F5E9B472030156BDB21533C559B161689C979D0D4C5C039447F628537073CF17B8AFDC44735A4C553466F205BB528386E5C0A9ECE76933E665D7A517EB1D5637B949FAE965499293791A63B6EAACFC5D63BDCB0C21C3ACB605848019ACFB1F95B783803776B00F143ABEBE2ADCE561E517AA34197A7B7F88A6A1061E50178C1921A296039C9DB3397807C74C681845999D2073BB8F17F86C16980542282E86148DC6FAC842E9722CBF2BC7182C7B70152856C52B09C748ECED14F22625F52475ED0C559E7688A944372DA013D08A23B99B534BEFC21A72758EA67757576CDD5690873D5AFA388C60A7B635542A02852324318CA92CB899F98BB2AD09034D396F7B9A64B76CA7D86118728085F1A60F2E273EE1401518500FA46068735B7B6F1C075C89A0446A04BA169047BAD23C68F30585EEFD895BB6C1305B325993B69368B7F4B1151E9490210EC18E8090E47F171523A6EDD21809AF6ACBF899CC63A49673B45DF90681414C904B0280773CF0CEB3FDB206BD8CB0CC864A3B8F6C69F3930BE71868028A738588700535663E34642E11176CBBAB5D71BED896683F31A4A849700BC97BBB47540D54439504370E205ECABAD4A151B1AE05428E91FE7C2805A374ADD1C4EF642683FD2ABA5D82E83192BE43A184DB41F9FB8B62C0C90FF0502FC37891F13C6C2752EC3F9636DD6879BF0A6ADFAAA8DB66B0C248FD4A73B51760444F67CF5140EA1405600A119FC086966592660E465B4886A12B03F5749799E31906A6ACCF7F964D1160E1D3B64F55413E4E04251DBB295245BF671442B46C3BCB150F242B545F279624B4C14990EC30A79EF2A815A261E049193B317BEA986675510B6609BB127C0457FF0CC56DC85BE863634851D30D5CB1550116315450D7A82E5248C0CA1B68B1B17793903F67B5F86F83374A2CFD3DCBEE3D1781C7480F7697A7FFC8BF813CB7D2933CEC6CA075C6CAAAC19D0C83324A034E8F94A37E99FDAB7918A8587AAB8AAFC75C5C0147B088407ED82CE482C1AF36911BC701122E3B6D5EB2F4BD806295ABB17E208B2C7218D5B624D39A570BA30B34678B0E5C97247B2C0C18D377174B56977D5D7247F4A8936F9A07DE2553111C0CCFA07B512B0E0A572559AB5CDFC95C99949B6240A118AB9F6BC540B427C8E5A19A95AB1A0DAABA1D63581802F41110FDA1A675705CB1A43790BD6593EC331EE54A303DCB4C82A28B32421D06C453FBC346A97B0A7482F01DBBADB136FB1DB7B8A5B7837A2C988B9A534484BCB5CC3814A438C432E88788C47E40E88735FC40B542631189B3BB8C08C052511AD2D9034D4CB6A0C352590680D0BC70EC4051BE0B25B45D4C8CD645AF6F99A90E780E574544D1AB1A3E3652606790EFA36880C983F864B954179172B90F7BCCB79EB59EA25A4A51B95950745A3CC6BE515CE1174458E8BA64769275283B63842781645187125737AF1CC01B58267B128E2C93E4E53C5F0FBCD6EA8398F287CF2708FB86A0566B118D64984CBC4AB62580DBB9CC3866360B0153F6D135648529C28428A3C297170E32AA0211E6B55734D45037CA7194EBBBF5476A6978488B617887FA2974DB459B1D868B6D4AC21B1ACB0299FACAAC10D76361363082FEB5DA0261886B59801CB324341113942625613A343B4475F11AE2C7C2CAC4739F1F423C6992A5B24B5481B2D8DC72FC4625851D69C249255B6E72D2B141FC1183D051784E6CC5DB627CE0B542060978F3A3083B0E25CDEBC4E77FC549F80162979C83946AD92000E0E531FE5784AD32C75BA23C941E60B81A52488C0AF99C6BA9EA04ECA1BCF017731FB54C0A125B71C20C04156472E697F85D406B9E95374B68156B22935F31AB91585DCF85D18B4C49ADAC4D44015BCF54CBFCC7A8BA93CE7285744DCC57B22303242243FB4247A3113E3DB1343226D914B017341C983516292487EC05760E4FA758D7373AC9937E966348D5309DC078C824382246BC3A81A23AF9A59A0482DD7865CFD73BAB6239F67B4641ECC42E9375CABA5B228F87D610AC1FF9B2F96CA78A84167FAF7898276110D3ED9DFA9C6E07D2A99226F735B798A734225D807F831FE53CE823C0E68D78A7F6DA83D16A655753E24DACB617C25D329B6716FDAC6A334A7F542BDCC7893905DB3D5C01054AFA79EAF44951CFBB6F8F4F68489BF99474A5434", + "DBC1AE2567D1980DAB86D677692C088D90626920E556C5C7B79A91899600A6C931915A3908579A3E491DF61668012C34F8CD08D999F86A8D60D4DBB5E01D2B5971CC3EFB60DD33EA13A04C827C8D9E19093DB5A267596579F031930F2EFFAF400F33F37C6357BFB55FAD4D144A233DEAF34ED99635FEAABC0249D7FE6CF7038F434D6C06CD8D3603DAC6C39EDF4798699CE8966B6AC7E8E93A6B04966B543D326782DC4854B59E1D5F39A7CA7FB8BDF297B4F4CB96244A1B0A2DF353C79626868DBC3C310C8C406D1667707FB21C6A3A537EC39EA63E430AC72A48B36C3DB460463E28A5487DC6B68A88119DFAA7FF0E62C01B16D4AEDF3B7622E7A212797545AD1F521DA4FEEBC0DFE5F118466A7F40C32416D2CFE143CB4674298E965F3E104885D5E260F41812E06AA44E6CD6AEB415D5EE5DDBCA1FBF0A78ACFB47C826016A124645395A432F8A2903C48C31BA394C9D2F20197551A96D7794324811CC1A696A00CC11B811837A12D058205690CBD7B64D625573CB004324FC0BCF2AB2804AAF34D4971BD0016282111D480FA545DA30D8A185D8FED891F6A42A66190FD3AF48DAB05531E7E9DA4C5EE03B30CFC96114DA679529459C4854C5E06FC0DE1953181293A9E6E8F3ABE56C58C31BF930237D72F25640714AB06E93A061370C5D2B8A7656C00CEFBDE1B4C3D934F2A57B6BBD78EBA66D6AD73C4448CC4A8DB51CB6DD4C8E1FCDD667DB9D9A16AF69F870B32DEAA611E94BF3CD59E82CE66DE287EAFEA4799D178A6D6BE1C7061EB5DCD93E34195601CB0F48E8C7163F0B3096E5D8FE0397F84BB9E8E849D3E34DC8599514107EDB43D25C69AB8F61431053DC326DF5E9C776F67F1A7B101179CD1B450D0108EB7AAC7BFB95597614D83034CB66B5D878F7200C7158046870B79F5F41024A5654DC5D51011F6C176BE6EAD856404530EF43AC5506A1269F5A3E934DD5F045E60325252DD8BFAE7F10D15B4A3DB919DD493083315E958D3B7ED09904B2B587167E889498025E910A6808DB76FF5A4B6CDDA73D95B4DAE32AF8144D27E1DFF1523A90D48B1CD81D0585B77C1E9723B424F7A380D95046E266DB8A1EBA08F15FD158758852F2BA7A5FFC096106735FF3BCF1E8D8C99049DEEBCE5739BBD3CB28D19DF136AF4C6F38FFB572F72D8B67D969D13889C5F6BAF2E64BAE7D5B7BE2B567335ABE0C6F55C285A6B78BCD2385448F469BF1C6F03124B67434FF2E929175B7D195917A9ED3B8A8806BAD9A6B522A40C1BC3B4F454E3B00A7E745BBB8FF6D191C6EB59CD3BE0FF5944B0E7F3BB3543674699FA90779D089190A482CD50D4B72B9C9B2B43A8CA18DE8C61E23A5E637B61281A333B54CD7A2DE2A249A998D36BDB6F1F85E49BBCEAD253D4447398349706DA1AA4D0FDFE55A283EFBDCCFFBA8A502F9FE5CB3B16176B3B3CA1B5825325809C6CB60A178CB703FF9ABC6BD2A7FC95514A7693AA32F039436BAB30BE90F289D61579B38FB0005632850419AC6E31709C0E8AE6C8EF1905AB5B2A4D22D", + "F624D36A15FCA03A832E0C39D0A0019C1CF08A53516080038A086E83F75E8C5D", + ), ]; for (dk, c, k) in cases { let dk: [u8; 2400] = unhex(dk).try_into().unwrap(); @@ -192,7 +375,10 @@ fn kat_mlkem768_rejects_malformed_ek() { ek[1] |= 0x0F; let m = [0u8; 32]; assert!( - matches!(mlkem768_encaps(&ek, &m), Err(Error::InvalidEncapsulationKey)), + matches!( + mlkem768_encaps(&ek, &m), + Err(Error::InvalidEncapsulationKey) + ), "비정규 ek 미거부" ); } @@ -201,11 +387,36 @@ fn kat_mlkem768_rejects_malformed_ek() { fn kat_mlkem1024_keygen() { // (d, z, ek, dk) let cases: &[(&str, &str, &str, &str)] = &[ - ("F3A706FAF090C03DB506863AB0B20BD8A1627956318E88C67EB875E8E7266009", "35D2BC43DD1CC879F765BF2A0C5E297889DDE910E57E2BB0EAE417B90AB7A275", "8D0923CA8A2DA2B4146EC25321122B8A5AA8AFE0C03415273008A46EE83031E98AAAA125ABC75D3B30322560C197E75DD0E48A348099F7B2144D7B8A8660A4A97BCF19C0583BD9BB2123033CD7BB5A14B08B817831A673A28170F5F6443C0551913A327CBA18C3A053C4040250403B70AB9588832403AA0FC37665E04980FE1602E7D2715D9CBC00515DF432A4F5B32B3BC92AE3F31700166D498123E94576509B712B18491B1435EE7AB7AEB1AD30D72348C3CC083ABE24A8B12097BF32F792476288EECC3BF630ADCDAC6ACA7950D9839501A448500742BAE37F109203A809B2B960A307E25347A32C3EAB79288173A878789B296E9E8C1C28C5BB3AC472601C9765F7B77225A810C7B85370BEF4A5B079D2015ADA54236B8F33840675F9B2EB427A1B5974CD5C61B24010886C5A7BDA5BBED974AF7217F3338AD719CB308A8BCB1B6D6ED2A1643736C29095E8A8452A3A36C7BB5AE58CBFDC61529466A90F454ED6895B0861083DD1371999B2F559A3A487CF59A074FB49215EA6A6BE656F9AF17B121A2447CB7985590E9738842B899BA57AC311810AE2D9794F37483DD6BCCB64AF6D56588AE94665961C025C3AA2861974C236BCA4BD8FF5509F7AB774593E7C5549E57C2F18D15C0515094AD9A0DFAA0601E524F8231156B627BB25A0DAE04DACD0A66C041CEF400583FC13BAE640291A39A5C5CA8BCA1AD5C683CDD8290891A76940817DD8C9F52678780548E37A05806600801426DBB950C3B2BA34E24CC77864DD91B39F1408C716A69DF63342854E50A245FB50977B9410DED2C93F86B1F9D5A78B87BF81E51CA620A7E8566B19AB700964A40E3266415228D432156E5CBDF52364A90483A55C39B3FB16FC7465A3F8AC801B70B9FB28B583444BA5C1A73722D417A9D6D9B7DEB08BC6B330FF27CF61AB8831E27758C64AF3B12150CB7B33ABC29858106D63686D8762459ABF9413850AE53ED6313F76F83D0FB8AB34374E7DF693E4A1B3E5A8AD0CE820AFE1CF401ACDE650A8101B0946022D52178E19613C42B88B07CC04EAA81DFB28AC9DC076236B67219A30F8F945DD57BD2F335C52D59372308D38993467DB53DA3382B74867B616481BD0091A2232C1116DC88A589DB9107224A681008C67C589186A6929549BEEF92253DB02B0C8AA9F9C875A670266C72BCBDB4F5625043703C1A0457395832E4C335180462ED2220C59E7361903C107D85457F6CD82EB820D0855D97675C2E0151CDB73C2885DDB7849D74541580124E890116A65BC068093B57914E20C937C60A3EB25576F1A976A9583839B672144CD4A45C3477A45C29B4E0BC2BDBD206585C9B7A7741C8B6B5793A92797A15AE7A5B73A74B2971463634BA52AA792AF05530730B6D0A89A346156B733677932BD36593A7496130CC458DCC5CA987C21960604EC8A8C5396056680CBF3F1AAC4F401AA5029FB2150434BB4706C31A2D54E4297939FA7C9C6F85700613CEB65C7F03AC56EB86E2D27CCC6DCB7B9394DCDB942FF222D86958A996C0CB6A8A44F97A70441C95FA71250116EEC20863C0B5A643458788AB001F8869D909922F51EE547A1E889255B3A0599C65842E5AB8D73872F053BC62392EA53896D328102D460BF1609583C22C3B43780EC6DAD0319EB4A5A65B4756C3CB40EAA935183BF8BD46ABE76BA46E199103A5313C3235F49C915E097BCA804DE680781D8365731BEAC6789A9203FB8787C4C070E00A13A6722A66A28236DB179825653D33CCF898B72C6B8450D97AFD3276BB13340519CBEDA708D12A858F54C49F4547195B7788A9150B2649E36AA394121926D568A488B16D3557B2A32AF57D11FC3373F80A28C0723273D362502E7C428AB44D3CBABF9EA585FD1BD0C9846556A1E196B78CF951592984A0A8487A78C2317D7ACA4118E1049750A0788F0D66AD9E48E34731130ABA0B427360A856D96D80B3F028FDD3ABA9035C10106BA1C0934BED36C6D7C7434249654EA89FC22137F4AB903653B75FB25B6F01635E6CC7D39CF1508690562826B49B6FFC59E0DD35022E541F8BA0D304AA5B4E20606907C424395666C54ABC2B8FB009847C86317685000C231215C8C15945860F6A85DDB98A8C3A527F2749D3C027E694E8F0B0F0FA454913AADB635AADD452F7128BF7752569669A8B93290EB92E78F6ADFF23E89F57F3890753B51F12F3F3A8A654E677847", "6CBBB491D7B3A3439F53C9027BD727A0E1C78004B4CDB66760E50F1BF97FFF0139F82862F1F3178A077B6AF77735CB802E91316BC207B9F1485FF7A868891E003BB9C6558BE6052C0826AF1CB5836B94C7D5D714EEFC7CFD983E9884B71348499DCA05DEF33B49D1C14BBBB95AB195BD72219B18BD0C704275CC94AB06653BBB9E0EEC74181903A0081AAE068B81EBAE8925335B8134B1480A60F758282C2F011B909641266774513A19090BC672EB3A0708CB577F134151C155C1E560D9B335A35A4D75F223502752CD250C9A7778EFC76A52709813A24679B987A6172F1714641EA6902D364F96357E92B44275EBCCE7F6207BB0A215E98B3AA92DAA46A39AD9132F3874C6239391E716BA600E42BC7084425051F865756A89DAB46A76104E183A12031B99BE86257B583C2E88104FEC3BFCAA50F238BEEC20ABF31219685885F2C3CE2F80C09B874E73A8712D35B8EE819D84B1C59A37947C0B9E25F066EE030ED28172FD5914B562408BA9B9DE9BAB2B95A4F30C22ECE458E133371A693D9458058265A75AD6CA5707617B38A1B6F396FB199CC1C77100741DE37500F1BBB10B9204EEB7C3F5B91A1CC91B6D4072AF410957358ACB0625488B9F6A9915758B7CC9B4AFE7D5AD6A95231A7A711B3A8996E23AFB3695B45B6D919C771D7685D90A7550A9B5B1E7538A365451A5A815009E49326C6541CAF0BA685056BF82EA4AB504161B555568583802800554A9480602437B0B9677FAC638A2508154A658E0623C62666E078D21A5A545A818D66C9547B9749458B735A4AD51812950E8A625A1B2A753ABBBA16415006300AD32D34C8B0B3B2AF4D1CFBDE08B4F06C70DE4BA28FB3763745CCE133DD2E488F89A51E7A586A86AC928867B0541438AF5645DE3211AA94ACA4A1095896EBC363C374A85565C1EBA030B5EA21B9B8A140E131ED07580D5B8590446988DA65939A823A84661B26CA12461AFE373519F43C13987A96A9069CD6B9177B205B25B5197DBCDE66C6457892C1DF96FE50AB7EF3B3DAB0A87436B3451476C44AA22AEF918AE5A1A8AE46920263032E3BC5DEBCFEE43B8AA290CEDF06966939EE6CC7C6AF83CB9D90CC889AB8B5A3D4C224E73916AE057BF5455528CACBED31B554043C6B0EA196854461D2644461470EFC55080E326FED49D62B5A4BB06B14F505EB4486C9A1B033B927836DA656C2475E5B8B823986DCA99B298512D94B77FE0588DBB96BA53A67369B82C2BEAB2CCC0103E549317A99DD97B598FA0A506184EB55C96B17302533C7EC3334E3CEC2BBB22BE0F89442840884FAB4A766A41BC8061D4ABCAC5190C729309AC6151BD434B818BC1E7D0CD435C9CA6DC7182228FF93867134A5CC65C6167C4A8BB830E7FEB863C15BF87B259EB0518FF5248792A625F37C589589799746D36197DC63AABD134A00EF1B49C79A2DF67B28E6B6E989CC9D0577EA5AA1B2FE64B322C7A8B241D503523D875CCDC479D28E3C01BD8383DCB2B684281E6949171F518EA97009237B5CD42AC8FC8A571716EB784B336529DB67728499988B73B80EF5C6D33F3B42ADA7A0A36A7C6A8A0DFB804B3D7BB05803957B913827A16F52ACDD7650EE2971A85DC21F6A94A5B31988E4438A9C1733AB8C48C32653FE800BF81BA66658CD2001ACF3A83E3A0426AB6C3B2600A53621277A43D723AB96758B120F120248C15EDF4294FC38F96EC490BD52094655841D9355038A379D8B666875171F39218E21D37023F5108B6D15389F07C1241E15071C43CD7D160EB5341B719C2055424EF6CBF66815F84943C9481CD1DE685A74C5B9DB37A0713952FD4A67BEC8CA7496F6C9C5FB84A8EAFE68D54654C6BAC9C3AB712C2BC32C0CAB072C18DE3CABBD9224DFAA305DFDB3F85019D7B988577B40F27514822DB42D87489502C53CCE6A095B486D9143D6D160B1C7313BF288913657698117EB145C0C9C55D66C15BF1A2B268F6A5FA259139551ABFE3468F1CC49C01B4232543DEF0BF9AE503B998536310751A8BC302B4B4440B835233797C967E7F9308155B27BA63C65E08BDD1946828196B30C042CDA0B56E4008215AC51F373604F5930BB66297C32BE41A162CA21471CC86657B5CB5307D89091DA9A5137F1C1433AB8AC1D59DE421C7C17A883EAB99BA4714F841998D0923CA8A2DA2B4146EC25321122B8A5AA8AFE0C03415273008A46EE83031E98AAAA125ABC75D3B30322560C197E75DD0E48A348099F7B2144D7B8A8660A4A97BCF19C0583BD9BB2123033CD7BB5A14B08B817831A673A28170F5F6443C0551913A327CBA18C3A053C4040250403B70AB9588832403AA0FC37665E04980FE1602E7D2715D9CBC00515DF432A4F5B32B3BC92AE3F31700166D498123E94576509B712B18491B1435EE7AB7AEB1AD30D72348C3CC083ABE24A8B12097BF32F792476288EECC3BF630ADCDAC6ACA7950D9839501A448500742BAE37F109203A809B2B960A307E25347A32C3EAB79288173A878789B296E9E8C1C28C5BB3AC472601C9765F7B77225A810C7B85370BEF4A5B079D2015ADA54236B8F33840675F9B2EB427A1B5974CD5C61B24010886C5A7BDA5BBED974AF7217F3338AD719CB308A8BCB1B6D6ED2A1643736C29095E8A8452A3A36C7BB5AE58CBFDC61529466A90F454ED6895B0861083DD1371999B2F559A3A487CF59A074FB49215EA6A6BE656F9AF17B121A2447CB7985590E9738842B899BA57AC311810AE2D9794F37483DD6BCCB64AF6D56588AE94665961C025C3AA2861974C236BCA4BD8FF5509F7AB774593E7C5549E57C2F18D15C0515094AD9A0DFAA0601E524F8231156B627BB25A0DAE04DACD0A66C041CEF400583FC13BAE640291A39A5C5CA8BCA1AD5C683CDD8290891A76940817DD8C9F52678780548E37A05806600801426DBB950C3B2BA34E24CC77864DD91B39F1408C716A69DF63342854E50A245FB50977B9410DED2C93F86B1F9D5A78B87BF81E51CA620A7E8566B19AB700964A40E3266415228D432156E5CBDF52364A90483A55C39B3FB16FC7465A3F8AC801B70B9FB28B583444BA5C1A73722D417A9D6D9B7DEB08BC6B330FF27CF61AB8831E27758C64AF3B12150CB7B33ABC29858106D63686D8762459ABF9413850AE53ED6313F76F83D0FB8AB34374E7DF693E4A1B3E5A8AD0CE820AFE1CF401ACDE650A8101B0946022D52178E19613C42B88B07CC04EAA81DFB28AC9DC076236B67219A30F8F945DD57BD2F335C52D59372308D38993467DB53DA3382B74867B616481BD0091A2232C1116DC88A589DB9107224A681008C67C589186A6929549BEEF92253DB02B0C8AA9F9C875A670266C72BCBDB4F5625043703C1A0457395832E4C335180462ED2220C59E7361903C107D85457F6CD82EB820D0855D97675C2E0151CDB73C2885DDB7849D74541580124E890116A65BC068093B57914E20C937C60A3EB25576F1A976A9583839B672144CD4A45C3477A45C29B4E0BC2BDBD206585C9B7A7741C8B6B5793A92797A15AE7A5B73A74B2971463634BA52AA792AF05530730B6D0A89A346156B733677932BD36593A7496130CC458DCC5CA987C21960604EC8A8C5396056680CBF3F1AAC4F401AA5029FB2150434BB4706C31A2D54E4297939FA7C9C6F85700613CEB65C7F03AC56EB86E2D27CCC6DCB7B9394DCDB942FF222D86958A996C0CB6A8A44F97A70441C95FA71250116EEC20863C0B5A643458788AB001F8869D909922F51EE547A1E889255B3A0599C65842E5AB8D73872F053BC62392EA53896D328102D460BF1609583C22C3B43780EC6DAD0319EB4A5A65B4756C3CB40EAA935183BF8BD46ABE76BA46E199103A5313C3235F49C915E097BCA804DE680781D8365731BEAC6789A9203FB8787C4C070E00A13A6722A66A28236DB179825653D33CCF898B72C6B8450D97AFD3276BB13340519CBEDA708D12A858F54C49F4547195B7788A9150B2649E36AA394121926D568A488B16D3557B2A32AF57D11FC3373F80A28C0723273D362502E7C428AB44D3CBABF9EA585FD1BD0C9846556A1E196B78CF951592984A0A8487A78C2317D7ACA4118E1049750A0788F0D66AD9E48E34731130ABA0B427360A856D96D80B3F028FDD3ABA9035C10106BA1C0934BED36C6D7C7434249654EA89FC22137F4AB903653B75FB25B6F01635E6CC7D39CF1508690562826B49B6FFC59E0DD35022E541F8BA0D304AA5B4E20606907C424395666C54ABC2B8FB009847C86317685000C231215C8C15945860F6A85DDB98A8C3A527F2749D3C027E694E8F0B0F0FA454913AADB635AADD452F7128BF7752569669A8B93290EB92E78F6ADFF23E89F57F3890753B51F12F3F3A8A654E6778479370FE5B05DDC92C939F62CBDE4C0FEA36F45CD20C5748CF3AC891A4C260449635D2BC43DD1CC879F765BF2A0C5E297889DDE910E57E2BB0EAE417B90AB7A275"), - ("52F2DF12173088140CA73333D1DB4341949BE452049028EF0DD904BC7E0DDEF7", "CC96DE9E1F0506BF490A154A6DB46BD2BD1413C3D1DC163190FF171EF0D11E22", "88C29C2A91CE80B13EEBE182AA709A1694A456B139DEB1977D531AD2722AC7D2B356AA2684D767C60012B1A2345A69AC52C7A814858E8C741A80C4B5E942CEEE0045B957748E2847EA835E1FE17C70456DBEA13FCEE0384BF431225C8A6B499624F086BF71518C8C269640B436E7783057372825768670B2478C96C4A1A2ADCB1F83A057263362476197C5D878D76B025F83AC802CCC02296A99296CF6B23C5617AA95387C2B0071C23AB7BF614371BC7001E29D7109057DC03053FB38C0084C48890612369C649C0BA3C2603E377C84D15B6DB6BB6E9513ADA9A47210861AA055D14683F0556729192CAEA8AE9A538FF810596D64BC0964B380CBC2F5D706A0A8135C1B87C3480C6D54CFC28A53C339666E24213D33A62EC872D803103987A36256840DD98DA8BC17ED42C34F5C008F6C678C748F3F7404D17B46F29012F079A46A0ABF77C04538254ED4B6BDEA929724AA60014674C427C55B1B0192CB284ED02194F9615417B08A9580DCA78944F43094E48AEBBC8BF0126310D9AA79530457E04DBA9B0D02B045AB28391F923C25CAA69AD038B4586350613D9AE22896F2979D56BBAFDAA294041AA42C21B9AC59E41CBEDBC33AD6996C3451B7DF5B9AE2D003F5092B91847290336A62E804B5F167D7CC4E31F40F6C640C10EB99D7F5519C80147B77843CD083B3B5C03718B1A7EA2DBE25ADBD56762E9150B46B6B4E82B769C384CAB39B3610070C6B6E5981C3B182105981A50567514CA15EB44620C231740FEB6BCBF4980F7B1824A5602A982A68BC8961BBC441B111C93B2374EC88FEF30D4931AB0CE3B5BEA680B1099CFA79B3FFF2B56BB3031A8C07519C7DF4F75A8C887C6A1B7249A700717B2798C02F0EA04E720752D8323376E1635331B07435CDD5BA6D7228281612BF060C694BAB6A00A8700977518BBAC8195853FDD34D44E29FEA7286DCFABDF47C9B2F2347EE7B3133CC275E601AA1874BD6974894C99D9659C1910030AD033138A57F8D9B312208873F733DB9C73007F45C49A007BFAB03AAAC3F107C8C0A032CBE24602A2BCAE4D2658D21899EA89EC5E832C0565701E5BAF0DA89E1B55ECAB7802EC830B8268F6D9CA3883C97AACC20FC4861EB59707AFA18A7B24534956301F238DFFB68BD20A8264ACB6BE4918C85282F63CBACA87A3530515041C5A00CADAC1A0B365B8A6CF626B30A49338461E94B44E20C250A23C7473952A20404FC17055E124792621D417CC7E20381BB24A701D428B1643BC0054B5FF46BA9129C0126889156BC6418A4C3B8C6D41B3D8646A915592305C30886B3995400A2A80771A494795D4B4B9EA6C21E0B21E0933F9F7C5FCB3B4C20AA303F655CB003526F8B04B3C85260C00C8E316DB495417DA22BD3A975FE784434C04DFFA545F91555F01033696BC4B87ABC0665B7A5C972D9A08FC8911147DA3CEF156B5CE85391161F32D09F6F8C0F8A845E268AA044E150944247E2B4354CDC72D3EA01B5970A6061B4F83563390BB33CF1CF9F910858DCCDD6C65E463607DF5289A3684440546816D4C2D59B214A15347EA9580310786A182EC6C2ABB73062FA281AC6806A6A042C0E95312027AD66EA6E245A9F2681A24CF505D5DB43F8A8C39BDC12E07C9F805634C9E5725E18A4229A20A681A0CA5144A8611DC372BFDDF102E5254AC1955450B94CE601AD68B5156996AC8C8A1BF1962A2758B4CBAAAB3A341E9C7C19005AB82BD236CB6294AEF22F1A1BBB32DC6EBD1C15FEB409E6C495EF1013E979981DB92BFF97BBB3B34DC385176C440B29637F68540BD10143AC33CDE4D2331AAB1DA5D9C7A41344A073B41FE536D5890239A96A319852593671F6A22E9480775F265C66D68521A73E9EC83DC6AC2963D3B70C3453861766048B18A135374ED53521A458E1FB024100116F434D1A261CA1C579F4311549243AD629525EBC465F8779C7E1B50344009C487D84A7B2683298EC39C27AF80FB3F28C61D388959A99721857CFD30F98F43A19716CD9E698EC946E69FAC477CBCB8E060EB9C96D3EE64B894C748133A9BDB5758CE45DDF791A68D87D1EF70D9AF626C3A909451A132C0702F0731D5D5959A732684C1759D8D31092669E70ECAB9F804CEF4CA416563AF1F27A219B3FD6BC482901201ADA9836B824AF9BB870DB2C9CCDAC525933E58A16C00B373DD81A3B89C39E9893960D7F6CC246A53DFFB9FE", "09766134E63C40A74D70107A121B44651CA43F837316F63F01290286304DA07B45FBCC418092B9FEC00EA7C88A4F20A0ADF9188E23707C88CFFDD6CD86F35A9E9A3D4F842C84A5C8662048395099037ACE5FBA75495A6370B9118D11324788BD4A841C90D219AA6BBC5C829EF2C291AF3AC99CF08672D95225DCC4AF7AAA428B7EE7201EEF8966A10B88067988F62C27FB52470C036535D762DF868BDAD167A383C74246B218CA68EF9B5106979E30BA4B278A731EC370750C2B37135D39B32F811AC667E717BE098B3F278F93815B2270C01723C322F1A3F4D8B1026A39808A9DE51B8453507464E283694BB937778161E604E127CA85F10CEA05CD28458503F100CAA530874BA500A3102CAA3802291DBCF72D457637C557C8F70012EB78B9DF1948F6055974C81AF1FB6DEE601FE40C3D185251441437582B57AB29627BCA1804C3B53EA76325B925982C8CEBF50474A5BD00E43274437229D94494B7536BF473435A1F3C552C45064F4DF31CAE2338394BAFFC8354D1005CDF0CC7D5A8782906135739079DDC727B55A67515C609223FA3C34B2EC24D033397B0D8C6777547E5C635F5E546DF52A207D2CE1AC3A9F85179A8898050078E93093B24122A7F553CEC39BF42B35A07F128288A23C91C9232B835A31288461640CFAB5EAF9A9BAA63AD40044160369C899737FFC8590C83A20CAA997FDA5BE4367DD00B91131C0F4EF61B0D207EEC750A8F4ABD5CACA994B779183051C6F52DCA8696EDC65807758ABABB04203234113052C7F894A478B564EA69D14A1970D2BBB437908F2288D0084EE7D538A3FC628E311D5D558CC2113DD1762DC3FCB336A899B583CEB457A8D55935F6926E1B5A2968352D6B1A35C986915C248E8C193EA3816300C67173930560E8455C560F3817C58848CC9FB3888037B449AA9268876AE4F665B5838329387431408068125F7DA65027917AFD42BD01389FE1F64BEF5B0EC688396CE58D622975D69818888029F6C0005082B9AA14B0EBF9188CE93C3B127237597FEFCC4DCF15B7F70518D8A9B34F1826C5B01CA9F48D1122AD41139C2E625F37DBB431957CD4DB6818292436846745A933014200EFB80FA9B4A4D38824CDD05C1422243A440126855DA2948024E136A23732B98A3319809948B9AE25ABC9B6A5A96AEC8A50167DF81C475EE586DC137DD5ABB7D91A0211258A82982DE2114A808A4DA29B79483A26F3CB73E5985BD68010F233AD7C0464C39CB0EF02938F036E1B099313318E81089CE78C1D008496B69764C08C7BA7DC00328846268B0709104AEBB8C757F22A8A45274CF75E80B1B377A971940852DBE1579B59B7B7901E06ABB7972B60C9357CCC0024C14341EEE51502D79D51D87A6F42897F5955A2B44C73766662F940D47B7DCEE02E22B21E138014D735404A13523EC23ADEF21A3184939AEB0BDA5264798146E1702F42C58185C3B063E1B884455C2B508DE26C5D334C0020C7430D774AAA85B2F9007FA41495A50BAD7315C4CAD429BF2B5B733690A8E422506C36FBC877708BB9F51CB899A48D3885B73933972F97A31DF12C37E2CCFF5608FE500F0B44585CFA0DFFD6932A2096CFE13518FA06B2E93C6F31CFC6E5C1973C2FFEAB73C0D5AA0F7A2C1B922A62065A423187B30B1DD261104933A036A89BF1357961008707D7C4819C6746E137531797E2B554E4C06F57A1811F9B1C0B459B2CBBC4C90293821AC6627B20A1BB8F93752222BA795E0448735840DE552AE26C2B8BBCAFEFF61E0B7898EF606DF88156E9540CCDDB6F4ED180E8E95D2404CD7FC15DACDC4873A2276DCC1892A36B4EB8C973D6858A478FA669052A04CA890C4FEED830553670E5F07FC43C59A0584802E541C26AC4C3C5774F79CF2251274C54BDBFA5B726CA710530C87EE49918A41BC50A3CE5A67C05689646516F2E77A832622EC11C9A69F70B4D014FF35182FD8B6CF2E181710C2EFDA85358712962AA2F04C58360756469AC4BFE861EA019923B68A4B63975064596F39B83FA7083620C20F578B7C96CCCF4FB5D485726DECCBDFBBC6EEEE2185FB46379575B5A875B28653A6945A7409C0402EA40A386A4F0F47D5BB1C972E96216384F9C48489CBB9DEFA92B2B983E11B796F0F176C6D5264290AC88DB13AEB93C022BC8C2266E88C29C2A91CE80B13EEBE182AA709A1694A456B139DEB1977D531AD2722AC7D2B356AA2684D767C60012B1A2345A69AC52C7A814858E8C741A80C4B5E942CEEE0045B957748E2847EA835E1FE17C70456DBEA13FCEE0384BF431225C8A6B499624F086BF71518C8C269640B436E7783057372825768670B2478C96C4A1A2ADCB1F83A057263362476197C5D878D76B025F83AC802CCC02296A99296CF6B23C5617AA95387C2B0071C23AB7BF614371BC7001E29D7109057DC03053FB38C0084C48890612369C649C0BA3C2603E377C84D15B6DB6BB6E9513ADA9A47210861AA055D14683F0556729192CAEA8AE9A538FF810596D64BC0964B380CBC2F5D706A0A8135C1B87C3480C6D54CFC28A53C339666E24213D33A62EC872D803103987A36256840DD98DA8BC17ED42C34F5C008F6C678C748F3F7404D17B46F29012F079A46A0ABF77C04538254ED4B6BDEA929724AA60014674C427C55B1B0192CB284ED02194F9615417B08A9580DCA78944F43094E48AEBBC8BF0126310D9AA79530457E04DBA9B0D02B045AB28391F923C25CAA69AD038B4586350613D9AE22896F2979D56BBAFDAA294041AA42C21B9AC59E41CBEDBC33AD6996C3451B7DF5B9AE2D003F5092B91847290336A62E804B5F167D7CC4E31F40F6C640C10EB99D7F5519C80147B77843CD083B3B5C03718B1A7EA2DBE25ADBD56762E9150B46B6B4E82B769C384CAB39B3610070C6B6E5981C3B182105981A50567514CA15EB44620C231740FEB6BCBF4980F7B1824A5602A982A68BC8961BBC441B111C93B2374EC88FEF30D4931AB0CE3B5BEA680B1099CFA79B3FFF2B56BB3031A8C07519C7DF4F75A8C887C6A1B7249A700717B2798C02F0EA04E720752D8323376E1635331B07435CDD5BA6D7228281612BF060C694BAB6A00A8700977518BBAC8195853FDD34D44E29FEA7286DCFABDF47C9B2F2347EE7B3133CC275E601AA1874BD6974894C99D9659C1910030AD033138A57F8D9B312208873F733DB9C73007F45C49A007BFAB03AAAC3F107C8C0A032CBE24602A2BCAE4D2658D21899EA89EC5E832C0565701E5BAF0DA89E1B55ECAB7802EC830B8268F6D9CA3883C97AACC20FC4861EB59707AFA18A7B24534956301F238DFFB68BD20A8264ACB6BE4918C85282F63CBACA87A3530515041C5A00CADAC1A0B365B8A6CF626B30A49338461E94B44E20C250A23C7473952A20404FC17055E124792621D417CC7E20381BB24A701D428B1643BC0054B5FF46BA9129C0126889156BC6418A4C3B8C6D41B3D8646A915592305C30886B3995400A2A80771A494795D4B4B9EA6C21E0B21E0933F9F7C5FCB3B4C20AA303F655CB003526F8B04B3C85260C00C8E316DB495417DA22BD3A975FE784434C04DFFA545F91555F01033696BC4B87ABC0665B7A5C972D9A08FC8911147DA3CEF156B5CE85391161F32D09F6F8C0F8A845E268AA044E150944247E2B4354CDC72D3EA01B5970A6061B4F83563390BB33CF1CF9F910858DCCDD6C65E463607DF5289A3684440546816D4C2D59B214A15347EA9580310786A182EC6C2ABB73062FA281AC6806A6A042C0E95312027AD66EA6E245A9F2681A24CF505D5DB43F8A8C39BDC12E07C9F805634C9E5725E18A4229A20A681A0CA5144A8611DC372BFDDF102E5254AC1955450B94CE601AD68B5156996AC8C8A1BF1962A2758B4CBAAAB3A341E9C7C19005AB82BD236CB6294AEF22F1A1BBB32DC6EBD1C15FEB409E6C495EF1013E979981DB92BFF97BBB3B34DC385176C440B29637F68540BD10143AC33CDE4D2331AAB1DA5D9C7A41344A073B41FE536D5890239A96A319852593671F6A22E9480775F265C66D68521A73E9EC83DC6AC2963D3B70C3453861766048B18A135374ED53521A458E1FB024100116F434D1A261CA1C579F4311549243AD629525EBC465F8779C7E1B50344009C487D84A7B2683298EC39C27AF80FB3F28C61D388959A99721857CFD30F98F43A19716CD9E698EC946E69FAC477CBCB8E060EB9C96D3EE64B894C748133A9BDB5758CE45DDF791A68D87D1EF70D9AF626C3A909451A132C0702F0731D5D5959A732684C1759D8D31092669E70ECAB9F804CEF4CA416563AF1F27A219B3FD6BC482901201ADA9836B824AF9BB870DB2C9CCDAC525933E58A16C00B373DD81A3B89C39E9893960D7F6CC246A53DFFB9FE7708C928659DFE465AE813012DF3FF6EECB1E081391B0A94172AFF81AA3296C4CC96DE9E1F0506BF490A154A6DB46BD2BD1413C3D1DC163190FF171EF0D11E22"), - ("F2A1E83CAD0055A85EB52F1BC1B9EE24F7F58D9C2FD099B5D38C9F75AD657578", "26D7035CD963C74AB142936A91EABD3074C583654945F9F5FDC29F754B06EBF2", "9331AFCAFB47F0E805E29B98A2A3C2D9D49AFEA806FB1CCD5E24105FCA6C3CCC1551B613927AB4CEE284CB53968DB3767C2154295CC479F06A4560385BB16B29273386320FABC520E5B36FE4D85814079E7E835D7935172DC893FF9277B177B7D8A9ABF9C66DC6A52C25852639540220FBB4E2E576440351F281712910CBF6A02F5A1B786B006DCE399794191D9F6BAFCEF1545E3A960CD204868463FF2B8A20944211408CA82AC182D05AB4C32D6A556FC36381BF163F69377E0028C8E8C70F0AD7A526CA274F9B65BD3ABA9164BF34F74B47C56966E4BF8E1B889E54CA29A037E67C28FE2A2B6710554D3813B55131B2B75DE0A419AB8888151876CD7B476CCA91FD83480D150CB3206F6A40B95E3412F9E7288EF051D3C99ED40B9E29A0A65CA1739F88122C72CB15950EB99302CCF18EDD17446F165930396F85E84E52E221B5668431B55CFA08D048A1386D450938D48CC2A94B38EC1F3F6AB2FA5565BF3B4BF1638AEB6A2440E5CD840C564183331E1AB7CB114A409CAB6D88C84C4545396360B1FB37B36CC405451D7FBA970FA7433B879EA8CA0BFF7C4BB0B98B255412AF695511A9CE168BC4FD50C27E121CA0221DE918778B941019D32020EC2F306C7BDAB38F9518BD51FC374F84B5E4B700F05785AC39CF9CBC0DA4C52150C9485D4A6BB4CC3C54A21DF63C2044B97C8709A5DAA82933197F1EA24A994370A2C9495E0B6660A10AA62BAB0BDC4DA284CC39B7C0E0464798A7365EA106793815B7B2BF02EA70C5B506CA343C38386BEFB69D6E31A1678BBD497BB1E64075B4158205280802234AE612A588CC8B2EC24AF74C5EAC085EEEDB13B827340AE29F348A9E31A20244D20A4B2B41F46A6A4C8293A2B8C5E3D3C49CA1AC0724969360BDD8A0822DF35D16A7CA48F90BB2C7972CD70B8DFB6CF5F1453F69266B04C2C32987DE0565BDA632F4B257CAC7CE7953A70B29681BC4602F3ACC3CFB3BFB63A135A758E461647E344A454AB3A1A72D234268D8870ADB84AAC250127E2AA593E79828848C112C26592A89C4DA358BF20571144C3C715449B8AE46606A51E150F4CA3593BC9ECC8A0382532610328253F488E733965C1742A36626524809C548397A922E79B82FC35B1D0EE41975CBCFF6F38BAAE28F0B452BBF792CD9023EB893767FD36E855774F1600399DA75889B03A95A21F32C0162540CBAB08B82B8346CA43B3E07B44F8C5AE6255CC379B3FE631A434349533B85C164B44D7805DE49442D113D238A9856E1897F0352130369387C3F2BE95435885A117072EC762656BCAB6EC1BE33C80C7835C4670B1D142767E23BC0AE6247529A3C8F348BA37ABE3507AC93C939560A445CE696881248F2A6C0B19B3900DB2E803A4BE0D4231F41A65702502241BBC724AFA441991A0A46DE7C62F6A220E457B2D876C44BB0A5F3116868E2B6D63A44A5C55F53F83E10E534606B76108CA4BF12A1B97A61A28B1BD184A9C2057655D460B65C407CB93272F44509C432DAD7B03A7393CFC160861CAAD4E9C81B9CB1085539D8A2C1855421346ACA46F46C3D12696237B237398B4015467CE7A6030A9666736717E6A740152BC2939998724018525D74724C5BD35B2D1887F9D2C6733A1173129DF3AB625E48C6664207C4D4931AF4130CC1CA0EF9160898890B49674F92A9464837F5597A7CA26BA854A4E0FB756572766DE2B9FAB3820818CBC600AEEF588B59B416F9568E19FC4B03902CC13547DC18197CB1AB23190424EC80633A5D58D861D9BCA5450102E52247A74594936153723C0A561B64F4E75D90E805DD7087FFD3420B4A0CC54CBB32944693A393511CA69E2C2B7A3A5B58784F2E999840BBA8C2C994852151569CA0949616DAF73328B220DBEB7D43794B1373CA3E8AC69178653E889D67E8546D35A40478B9C6410D9376CB28D6080AD41FE834B970403B8687C29011C6C8E24137F22FB7192443190BC2B095594C85C55389D4D49722124F07067FD9F34319565392799C27550934127538524E1655A03F697313130CB0F6799DE565A5DB8B7C23C7A8F53AA7714C7094A86E67894A75A489E6C414B67C52030F40EC5C599969D8E00B7C03492C8CA6DFBA8D641307921712C2840591FA9A847B39794BC22747830FD956C0C17B5C974FDCE96F565355B500A70C3D86EA32C9290514B1373184AAEDB75D90976FE6765F4747BFC946F1", "8EC50C8143608F9186988602849A965D48645CA331C1815E506A1C51D73AAF18BF92D39BB18A5E681447EB78883CE48E6C375AA8E36CA82ABA3E188D8D795DDEA50B655B2E9409C35931C934BCB5820C6D2CE975FD955A9BF40695650004B4C62447AF3E1082DEF45A4AEC121DF9C8D36C8264034E55F926BD5053421C809041C1701650EC0959E4EA6D09898B8BCB90F95209C3397525D984F6079063901BB82478F9B1CE08875449355FB16A3921A8CCC9547349C5650BF49D3D9BB523092607F2B4BB437B1A969C35525F321333E9436B66A08699A9CE4017C6D7485145C408FD412E69463A558927F0763ECD8B24C344640535CE10DC41780213A3DB5930DC40CBA012CA5B22B2052C3192B31C9230F4D12FD5767EC1700775C478E263220E40C65EEB36FAA1C18B530B6C5ABF6B11397E694763C8C781CBB3C6C767F24287BCA224370C9D5E9066609078BB4B7BAFA46328F9C203D7AB0419B9177295DA08B802D93A97DB3F1FE964BA7B78F4A5404657603E5A7B95971A1DC2A1B00B54E7FB1C180CCE49917278C961FB61009200070ED90BB752A55661B011E35382A01C57793AFD864AD8D3ABCD752F78168EF12238CC520A6845CE3CA5771D207B3A2A594E687867783E80B057423118753BBCD3926B6640321938A40E793BADA8476D64858296348BD7038DA24E65D4B6FC4C907ED69E0159C76AB07F9111035B6187A41024AB26966F7B64673860EC9AAFEF29C0D7DC8A4DE31344C41D84D07B3A6525A5A884BCA8870E758E4A6C02150505A7587EB6288C80910C0E999C0309A34DC7C47053AA1F01848BC15059E65307207F4C4974DE0ACE30C2A3021B5927B1B0787002AD0A4CABCCC2D30865DB9150F657025658C3A14CC7D088BB1247CB2097A521FC46E75B158C55509D0A6DC8FA6B53016F6C28939C889943075E1B26646E3BAE5B0A52D3553E3D24AD62396AD18CB53C896E8FFC148F57CE11A39677052C134B8D0BA186B6F9BB94A2C96D06627ABC7E29660B9DF9C9B9E1786B39B9148A982B456F4F426368845D0377C043036252A4BA1B61487958A9E05CB272072CBD0927B4667BA7E4837E4B7405B9CD3EE43F7575B8209BCF122622E7E99B414C3AA32C9DEF1B200444B82BCBBA8EBC079FB42E00A830D921C45387BE4F86BC05D64BCE8B948BC61D6FB0CBC9552591F99622C50C498376A106414A6109354184EDAB57D8BC27F6761E18E12A12335BE89944B737C3B3BCC3227148A5A60EA32B83C8C5692B35BD68D72DF7CC264B519C52437B357899B3409239C02251F83460F6AF9B7C73AED1B4EDEB322F794555A0CF5AFC233260A10C6529B5AC55582B153461C25401027748B39260015E00184D553F45372613631E2ED0B4633A0CC52B6F62454DA414A49B17BC88225EC9EAAFBD121494652C925BCE19CB53E3216040B879F409049B780098A189668A7C46751C714B38A6E59EE4D7620AA3B3DA3BB3FF3C4C04E45F3ABB3CEC417CD80908EA701D90A21E22210CB7188D739CB13DFA03E35327C1912453C5709C1B6FEE2CBC5F8714E1A43AEA7422D6E63F247C8065A0CDFB782BDD2CAA781B4B8B76BB7885C7B446BB40A52BB2C8112F3C5DDD077FAC625D54DC26B0A3B52109C8C80137302738B511377376A74E07C1D2C3108180C6ACE97E61F6A343D6C1CFB76D5B18B756179A66D51483B904E5B88FFD700971A37E913829A2F366B8386BC342BB49411890C958040B7E66CCC5FF9C55CBC85209784282F865C7132FD0F82A4B016A9CBB64B03CCDA3F64FE6E760DFA764A7D2A17027B18C8909FDE5C9686608670871CD95B2B1DCCD99C34B22034147F1471BB38D3043ACA4C727BD971C4676BB7DBB987E015084F16E05686994A776704A161C7407007623964359E0F77214B03898C51B4EA6474A7B0711E505F0060C220A80A9C591481643A9796C2AD651ED7783F38633C308B33490867DF28DFD054AE9C49CFF78B508234E17538EF58172E92642F43A5D421B2929210BBEA122853918EFBB91A0446405519D43DA86031A6DBB3A9E24BCBE4E686EE9EAAD71A42A4D27907E89326FB811A5663D18002F7294CED499A29EBA328F5460F80512D42AB48B726E798778A2C2692BCBA2A0052F9A56C1A0631741A6BC846A8C0BC309B7E9709331AFCAFB47F0E805E29B98A2A3C2D9D49AFEA806FB1CCD5E24105FCA6C3CCC1551B613927AB4CEE284CB53968DB3767C2154295CC479F06A4560385BB16B29273386320FABC520E5B36FE4D85814079E7E835D7935172DC893FF9277B177B7D8A9ABF9C66DC6A52C25852639540220FBB4E2E576440351F281712910CBF6A02F5A1B786B006DCE399794191D9F6BAFCEF1545E3A960CD204868463FF2B8A20944211408CA82AC182D05AB4C32D6A556FC36381BF163F69377E0028C8E8C70F0AD7A526CA274F9B65BD3ABA9164BF34F74B47C56966E4BF8E1B889E54CA29A037E67C28FE2A2B6710554D3813B55131B2B75DE0A419AB8888151876CD7B476CCA91FD83480D150CB3206F6A40B95E3412F9E7288EF051D3C99ED40B9E29A0A65CA1739F88122C72CB15950EB99302CCF18EDD17446F165930396F85E84E52E221B5668431B55CFA08D048A1386D450938D48CC2A94B38EC1F3F6AB2FA5565BF3B4BF1638AEB6A2440E5CD840C564183331E1AB7CB114A409CAB6D88C84C4545396360B1FB37B36CC405451D7FBA970FA7433B879EA8CA0BFF7C4BB0B98B255412AF695511A9CE168BC4FD50C27E121CA0221DE918778B941019D32020EC2F306C7BDAB38F9518BD51FC374F84B5E4B700F05785AC39CF9CBC0DA4C52150C9485D4A6BB4CC3C54A21DF63C2044B97C8709A5DAA82933197F1EA24A994370A2C9495E0B6660A10AA62BAB0BDC4DA284CC39B7C0E0464798A7365EA106793815B7B2BF02EA70C5B506CA343C38386BEFB69D6E31A1678BBD497BB1E64075B4158205280802234AE612A588CC8B2EC24AF74C5EAC085EEEDB13B827340AE29F348A9E31A20244D20A4B2B41F46A6A4C8293A2B8C5E3D3C49CA1AC0724969360BDD8A0822DF35D16A7CA48F90BB2C7972CD70B8DFB6CF5F1453F69266B04C2C32987DE0565BDA632F4B257CAC7CE7953A70B29681BC4602F3ACC3CFB3BFB63A135A758E461647E344A454AB3A1A72D234268D8870ADB84AAC250127E2AA593E79828848C112C26592A89C4DA358BF20571144C3C715449B8AE46606A51E150F4CA3593BC9ECC8A0382532610328253F488E733965C1742A36626524809C548397A922E79B82FC35B1D0EE41975CBCFF6F38BAAE28F0B452BBF792CD9023EB893767FD36E855774F1600399DA75889B03A95A21F32C0162540CBAB08B82B8346CA43B3E07B44F8C5AE6255CC379B3FE631A434349533B85C164B44D7805DE49442D113D238A9856E1897F0352130369387C3F2BE95435885A117072EC762656BCAB6EC1BE33C80C7835C4670B1D142767E23BC0AE6247529A3C8F348BA37ABE3507AC93C939560A445CE696881248F2A6C0B19B3900DB2E803A4BE0D4231F41A65702502241BBC724AFA441991A0A46DE7C62F6A220E457B2D876C44BB0A5F3116868E2B6D63A44A5C55F53F83E10E534606B76108CA4BF12A1B97A61A28B1BD184A9C2057655D460B65C407CB93272F44509C432DAD7B03A7393CFC160861CAAD4E9C81B9CB1085539D8A2C1855421346ACA46F46C3D12696237B237398B4015467CE7A6030A9666736717E6A740152BC2939998724018525D74724C5BD35B2D1887F9D2C6733A1173129DF3AB625E48C6664207C4D4931AF4130CC1CA0EF9160898890B49674F92A9464837F5597A7CA26BA854A4E0FB756572766DE2B9FAB3820818CBC600AEEF588B59B416F9568E19FC4B03902CC13547DC18197CB1AB23190424EC80633A5D58D861D9BCA5450102E52247A74594936153723C0A561B64F4E75D90E805DD7087FFD3420B4A0CC54CBB32944693A393511CA69E2C2B7A3A5B58784F2E999840BBA8C2C994852151569CA0949616DAF73328B220DBEB7D43794B1373CA3E8AC69178653E889D67E8546D35A40478B9C6410D9376CB28D6080AD41FE834B970403B8687C29011C6C8E24137F22FB7192443190BC2B095594C85C55389D4D49722124F07067FD9F34319565392799C27550934127538524E1655A03F697313130CB0F6799DE565A5DB8B7C23C7A8F53AA7714C7094A86E67894A75A489E6C414B67C52030F40EC5C599969D8E00B7C03492C8CA6DFBA8D641307921712C2840591FA9A847B39794BC22747830FD956C0C17B5C974FDCE96F565355B500A70C3D86EA32C9290514B1373184AAEDB75D90976FE6765F4747BFC946F1249DBFD72887FAACDB1B900CFCB5B3955EF374D86072D38CE29DD9E906AFD6E326D7035CD963C74AB142936A91EABD3074C583654945F9F5FDC29F754B06EBF2"), - ("145BE4E69C65F82CB31290C14E279EB77246AA0DEEB4729E7B21DB0ABC4A15B5", "E6748703B1B4742E044D723FADA7E44091113AF9D0FE599C5E969A1194FC7925", "F2F223B0E4789C392E69386897E95825B0B6E38C821F806C1890CBDF500FE1A5B8AA139B4189A7D720360852B514CAA73BE8CAEE5C8828027F945768595280A4B869CF154E2507D09855B9D3897FF1587C30162DBB49C443025912D3A950A4711B06BC64EA2216D81C7862066061A33F776A0F9C69B52C8EE328B726A6BA415376D5AC8E36503E51067F19A53D601B0ECEC648D1A256DD452472DA94C6656DD68CCE745C4CC04826887340AF46053592CCC701630AE172B522725F546D4E244A3421079456AC75745C4AD289557249E9F29A5194677AF159E824AF31687D09876277A3643DA93B6103A7FAE9644CA127B6FA9BC80A248575253F4C813F900856A4445741119E729E29466866BC14E26527933248B286AA4D872EBD2CBFCF1A67A5D90240CCA09255C811D2B97B3987C4A472D552AEAC6616E122427C365D435501EC66C451826ED7719D8888241DB93A7FC82FC43908F70151243A30B9308C600A61F6B3248657838DD74B07255219D32903E8A850B997F8A329F56026FA2C0C43E322D4A5A5727100A89542E8D57DDEC2B02029306FA108B5481570D98ED9D29FB3025F5160A6F83C8FCCF509890AB6A3B6403920894DFC9F7B50CF50883448A41DE86C381DCCA7F0F431CD12494D515938358A39216549C804D3F91C3236019AA805B4FAB7928482B27125DEE89053201ECDF0B907786CCFB11B9E24890F099E4711950D00BC126680A7F45CDDF4115310A42B813DE03C6B77743BD66473C587C6FAC769B293645F601150D42A9C741F740071146A942AEA330D78132A6813BDA844ACD73A2CA311B1E84EE3B093E171382E7B353AA542D3BB3B8222B256D7AD3A21042B68BFA2668867351190E4CFE60C22DDF8BD922A77933CB4F3A5C012A72CDD42C686815F876407ADAB033BC742CB811CD82C8098847EA44853AE71494B63CBDEA66A8A08A55F75AE873A0D61F487A94987BE25C0E2455593F01ACBE322DF69532F5308C1076FB228610D6AC8CFE45F390957B427A4E95A6FF5A494A2FA2A0F92A05EBA713127CD47A53F4A835EDEEB702412C8EBF1BC18C2009C7A8BD8995D7E804F35964E63AC2EA51922E1564868A95D2B99AC887C8DF985092E0ABE26B4A468C01C3653B34A097F0489142245836F2CB504AA7E0A6536B46A8569C3366FC9460D838CE40AB4322006E841445754A00957667156633DFA5F3FB6A9B6D28337A85B9C34B41E37A0DA8956D3640E97337354841205C98316242A22D81F979063FE1CB02688AB4383577F022B55581444EA8137696E44B6AAA4D079BF6381EE55253CE1023BEC17A7325B04B11D8AA55F6F1429E661961C92B88A104322988624DB6155E836979B3162377D91B10C58D2265A32165B7509990013CDE36136F7B86299C6A0F06C037C35E54164580961218787B8D46C56C84F0C6ABA065C6E4B6078DF35BD8604B9097668BC4460EFAC7790D50FF605AC3AD108EC045D76962C4E3C6E59C292F89A5CD0832BDCE59BCE898391BC71D5353BF89B366F28BA1C4934AA1041CEE3325389A10B4A5E558C3F2BE8988A6C60F9977661D39F38F663B1AA9C26479BFEA67EC17048E3D3C236522BE6535C1870CE0BB721249205546BC00FD2B86FD852CE0647D6882393CB11EF22A5CEBA1CD5C2A7F0B0174DC52D66E8CF1C567C89E128C2B29EEADBC7B8E2704E80398DDB2B89D1CB6B6570C15826F5C855F9D24FF25937233C84EF49249A9A739C540592C618D63CBB83E2BCA108ADA6AABD3E93AF538B5C6EC29FDC146C0950817CC80C2258AA4BAC0A984B621305C082903643851D623C8B8D6A90FD269B65524B8396A0EE192CBB4202634209223241DEC3BC66054446F12AB6DBC9BF27BE16EB993E0552BCF1C4CC8C5AE4449D49E508DAE71FE13A7876352C58230C5EB2861DC237DC799CFD3588E2A6AD06D1A665881FB7620C2AB50BEC766ABD441790E23C28D75FFF43CD88AA405D5B607E551E0B182D42136A16508EDC90BE9AD159262436F406803AE580C4C0B4552AC6267712E30A39F66396DCABB4E864CBAC9AA9F5226F8B421842811876F39AC648C73D282A45C4A99BC90F2E52563881C22BC3858B928382D5416056B969191316A7356A07B46B146D2DB360BBD89C63399585A7CA72279938A2B22FC8CC7F6E7F3A757BC90E65D8A8F8F49202F696137573B13BA0088FFC801833FBCA9E", "821865EAEABD3E684D94942229295D93B09DA166B59342C25287246B0102075A3C69476570239F9DFCB6A8F16F06F45A9BC2A225A47FDAD6287115CA8524C8402C04FDF3AE9386C31B7707CE863F068872182480EA3243AA654968D49C1FE52D5F8CC0128C1757B9CBF816420FEABD17233AEF955B4C77A6777A16BA35745EC6587C37376F042ED3DA49CDE7BA8A31960CAC4266D51DE764CB694880B0E5A2EF0789791962E67CAF1C152E8ED05A63B0B77F56CA99490D321785C540A2C814B77291165574460D908F6826BCCAD3B33633A2693A2302E34B5BC442ED8744FEA370054A0846B9B09AD111F38A3E46015FF431064F586B8C43939979437F59AC401C6C6D3B4E14A9675D54CD1DD2B2437345FF54AC616CB10501656810BBE085AB75B0CC24124BBAF1B3C8504C46A44A8AB4215CC22DF5644294F3A2D3135AF558191080A49AE7CDD711C503FC6DFC2771D2FBBED7A6B5435C07AE290677656E9524A26C9455B21704C653A395D34521E830E7514D7B77612AD2CB8EC6A7AB6889F471740D4B943D3048412655D2FA32451742CA8B4312241FAF151648D65028D5C08E717ECE286B7157BF05B8A1956C52BA215D40273238999DD8B7C9171293C1D82095ABB00B09C99ABB2A8B402181B23F8EC668BFB2C33A656A767BB77D8316517208CB44426A94B642154E7A5A73E62AB75B000DD479C13FC7A3CD5322F1BA2D970B05FCE795E41BC5A8135A37A85E01E777F452BD7E0284D07C5D41D34D4698C510F674E3A9AE59833960F02D39B9B5E4EA2B73378157783C8556B5DC6215087C4A73FB2ED5A62FDDB23006E05FF14B87E6CC4D63F379E7358641B69D9FE48CA01AB2C210B99C436D89D84A2DBA4CF43122B9912478259A4F5B7B1E6CAC6934975A82588071C079689686BAA105E3B1CC436A7268B211BBA7833B2BF4C85E99D5CC65A40A34949138DC14DB83418A8A68AC74C993224028159B386055EA7597905995A52989D865BD54C358A19519F07051BCF97BDFDA2A5FD50CEE13CE6D2484CB297D3C2238772B85354B906E3AC9502B7B8DF4C32FAB4DC5553228D71EC8FB86089243753A7D7EF29398B92744ACA4B25819D4CC1FAF4BA5C09416FBE8C6893175ACC3C242D136F85636FFA1A2147216F2D100807877DB7628843336BE3176FBF0A4B73AB9CFB65284C38F1C883AA251B72CD46E7FE6CD117891D9294D8000388FBA12DA851A945C497AA2A797F25103DB1B68996355071A06C9AB0E122F25748FCE59A22F68AC8EB4635195BDBF07120AE27C9D857B4111C9DF825B1513CCB409CD02A37E6393025521492CB9707BD059E7C29672449478B17DDF57B8E4913F5DC00D0955980F7708650BC22E3588FE6A0F2CF016BD73BB86C7CE7F39381F20A4884A3FC1DC5397A4BB30199B47F73908D58ED40491DDEA36CCA15AC9CA337D5C05EAEB4647A9B88635451CA58C99C67DC5A5168E0C44751857C3277BE9DAAB366755DBD1137C4CC8CF97A0E5E99816D96FF1984BEDEC44867B1570C50772BB704BF54AE3F5150BD89B7DDA095FB939AAD0C3FA1CBCF25B93B40B1EBD26B8074871B2210B48F28FE4C384AFDC6ABC531150E81516376406600C181C4D200C5B7D23003DB65FDB401C8094C110C53E3998156294514F990249269C5D9A1077B2C180A97F0D833FE085A5FE48102EC59B52929E249A82518C2FA6EB5D45941DB3971CCDBCB60D69097E12AB226965FE33ADFD407102C052A4478325271F3E28C450B20F51EB1DD70358F8F18C260340C2080CA568A10060A2A4903796A39E4FCA1084D1859C66722A41AE14456DCE2BC753D29732F29984B85E49C60A1C595794FCC6CD593E84F7B1ECB40426836ECC9394043900C286131A785B5666BA6C2B76764C4C6BEBA7F5C12690FC844AD4559F0222261A4F3961753C7A1EDB1619ECB73FC088180ABA68CF623D09E5CB1B144F0F9BBB2DF9C4CCC430B8383480500959392EAA1C99DD16C4774650410BBD9328BC306746432B50C9A4B2B0A57E1D0B5BA92963DC055F5009592A722DA9504212EC4533D463C3F1850E41A4F2D6092D87C1434B7BD4416A109621BC44129BBC0F3D972882FB50F8DA3585D7C0920A1D3C8B5BC3B3BD0F948C2D550765DA6A98579E7FBC29FAFABF0D20872DCA1BF2F223B0E4789C392E69386897E95825B0B6E38C821F806C1890CBDF500FE1A5B8AA139B4189A7D720360852B514CAA73BE8CAEE5C8828027F945768595280A4B869CF154E2507D09855B9D3897FF1587C30162DBB49C443025912D3A950A4711B06BC64EA2216D81C7862066061A33F776A0F9C69B52C8EE328B726A6BA415376D5AC8E36503E51067F19A53D601B0ECEC648D1A256DD452472DA94C6656DD68CCE745C4CC04826887340AF46053592CCC701630AE172B522725F546D4E244A3421079456AC75745C4AD289557249E9F29A5194677AF159E824AF31687D09876277A3643DA93B6103A7FAE9644CA127B6FA9BC80A248575253F4C813F900856A4445741119E729E29466866BC14E26527933248B286AA4D872EBD2CBFCF1A67A5D90240CCA09255C811D2B97B3987C4A472D552AEAC6616E122427C365D435501EC66C451826ED7719D8888241DB93A7FC82FC43908F70151243A30B9308C600A61F6B3248657838DD74B07255219D32903E8A850B997F8A329F56026FA2C0C43E322D4A5A5727100A89542E8D57DDEC2B02029306FA108B5481570D98ED9D29FB3025F5160A6F83C8FCCF509890AB6A3B6403920894DFC9F7B50CF50883448A41DE86C381DCCA7F0F431CD12494D515938358A39216549C804D3F91C3236019AA805B4FAB7928482B27125DEE89053201ECDF0B907786CCFB11B9E24890F099E4711950D00BC126680A7F45CDDF4115310A42B813DE03C6B77743BD66473C587C6FAC769B293645F601150D42A9C741F740071146A942AEA330D78132A6813BDA844ACD73A2CA311B1E84EE3B093E171382E7B353AA542D3BB3B8222B256D7AD3A21042B68BFA2668867351190E4CFE60C22DDF8BD922A77933CB4F3A5C012A72CDD42C686815F876407ADAB033BC742CB811CD82C8098847EA44853AE71494B63CBDEA66A8A08A55F75AE873A0D61F487A94987BE25C0E2455593F01ACBE322DF69532F5308C1076FB228610D6AC8CFE45F390957B427A4E95A6FF5A494A2FA2A0F92A05EBA713127CD47A53F4A835EDEEB702412C8EBF1BC18C2009C7A8BD8995D7E804F35964E63AC2EA51922E1564868A95D2B99AC887C8DF985092E0ABE26B4A468C01C3653B34A097F0489142245836F2CB504AA7E0A6536B46A8569C3366FC9460D838CE40AB4322006E841445754A00957667156633DFA5F3FB6A9B6D28337A85B9C34B41E37A0DA8956D3640E97337354841205C98316242A22D81F979063FE1CB02688AB4383577F022B55581444EA8137696E44B6AAA4D079BF6381EE55253CE1023BEC17A7325B04B11D8AA55F6F1429E661961C92B88A104322988624DB6155E836979B3162377D91B10C58D2265A32165B7509990013CDE36136F7B86299C6A0F06C037C35E54164580961218787B8D46C56C84F0C6ABA065C6E4B6078DF35BD8604B9097668BC4460EFAC7790D50FF605AC3AD108EC045D76962C4E3C6E59C292F89A5CD0832BDCE59BCE898391BC71D5353BF89B366F28BA1C4934AA1041CEE3325389A10B4A5E558C3F2BE8988A6C60F9977661D39F38F663B1AA9C26479BFEA67EC17048E3D3C236522BE6535C1870CE0BB721249205546BC00FD2B86FD852CE0647D6882393CB11EF22A5CEBA1CD5C2A7F0B0174DC52D66E8CF1C567C89E128C2B29EEADBC7B8E2704E80398DDB2B89D1CB6B6570C15826F5C855F9D24FF25937233C84EF49249A9A739C540592C618D63CBB83E2BCA108ADA6AABD3E93AF538B5C6EC29FDC146C0950817CC80C2258AA4BAC0A984B621305C082903643851D623C8B8D6A90FD269B65524B8396A0EE192CBB4202634209223241DEC3BC66054446F12AB6DBC9BF27BE16EB993E0552BCF1C4CC8C5AE4449D49E508DAE71FE13A7876352C58230C5EB2861DC237DC799CFD3588E2A6AD06D1A665881FB7620C2AB50BEC766ABD441790E23C28D75FFF43CD88AA405D5B607E551E0B182D42136A16508EDC90BE9AD159262436F406803AE580C4C0B4552AC6267712E30A39F66396DCABB4E864CBAC9AA9F5226F8B421842811876F39AC648C73D282A45C4A99BC90F2E52563881C22BC3858B928382D5416056B969191316A7356A07B46B146D2DB360BBD89C63399585A7CA72279938A2B22FC8CC7F6E7F3A757BC90E65D8A8F8F49202F696137573B13BA0088FFC801833FBCA9E105F2F6732DCA0DEF151216C79A5370A63D0C239D8FC99819CE1FE5A4984F08DE6748703B1B4742E044D723FADA7E44091113AF9D0FE599C5E969A1194FC7925"), - ("FE8C4F8EEF555AA563EB748B5F55D72CF68AB6C81E9D541A8651ED7EF2EF1352", "BEABA3A6A443866994CD9C1C2B70005D87AFF90F4D00A7A232F3D175BFEF72FF", "D9B50B583278A2EA947B027BF85885CA3398471A81CD153CF9675481EC3D657A623852C8F72A02EB3A950F493471334B871915022B1510506418849C6E52529335B00969B63C6A8F1D8727E0FB89D7E61D6D5C4765B6A3593C3948AA2B2C2B909E1CC766A879EA278A921C2BC876B572F3435CC39CD541C390109CACD51946254CE6E62A3698AA5AE1C01DD220777C9DB8D89F30258A856332C4F540C1CB4CA67B6C3CE2782088B7DD072BDBB1102F403BC534C7AC2A1628261B0EB9A19452118E791220680AF79189002A9E9FA7A54AE38FACCA03E870C48C8B9A58F4060AB60322509E568371A9B3A959E5C55E5B99A0FA9ACB47C422336431D08915046644148ED3F87F873309E2FC684B5AA6BA627E883093D90351CC737E26364AD3251F5E320FFC905F2134825B7C7AB87651A656A9CD49ABEE5772745B798E08B00DC5C41B3C535890433DEB7316167C5873724B7B730923A92C87316EEC59A4192441E1A7EA88762D797D73F6193309B47AA0589707C342450A79550C67876D952A3DC6A65C4A7A3820116BD5F70B8081AA5EF8CFEA09CD4B156808582CD7D078FA2B13CFD5771AC18B76022AB72364111235841343497468F2AB3A134016041437D02237C997095FF0A016E6C48566241ED873DF4C47A05904A8852E56CC82000C5DF3F2769719761D3165942C3D7D3B39FF151C69793B33F699791A4691A934E98386B330340A396C95947DB2017DD2862321D53B88014066697614865F75512E08C63F52002FF96999D5CAAA31128EF89270DAB59A1E909991220C2E9C66C7563A7B572544871F29170D71E29B7A1433AE239F76B21FEB6B9D49E863FFD8463EABBAE5425C0E527A36FA70D4464324AB0CA35A3AEE68369491A434C3243335909BAA6B689B228D9540733B6DCA05015C927ADEBBA90B17014D5268DDF8A06D9AA979FA0917CA4A4C871C4CE6A3908AB158664AF5B5AB6FAA95E0E593BF55A65B710824FB64D7B7A421431A75E78EB7D61BC899628994935D4778CFDA77E9168307B2B6CB0164085634D94CBCA4C8B53E793A00DD8D2FF6A800C55E028012BD6912C48B13432C485B8AA0EE61555D5699AC5AB6944120D1EA030FF2CF785573B0E84FB772B9F3E473CCD47921B6111AF6A690F088C58C7267978BC5757CA9E542A441983BA113654A8448D7025AF4BBBA0306D56403A12568FCC8B892D9AAD06B809D5681B949A19BFBA4C55A26E417B164309E1E48AE245A81734C141D639D89677A29E231B5255072A37C90925AF0D36EB39737396C1A02B513EB98BC110870CB9565F2898690ECB0DB7807E3E858C57BB642CCC5EF0C8F00D733B3581382994E0C2C74A8B140D6EBC02405835986C2D913077B6C7BFFCB230BA6181D765620B0AEC43B8D15F64F51ABC92DBA2E5CD53021537A6BC06091D2C1E47B2E049A1207617D9A6703196623A0CA85054373D4283EAEA0A867B308F362BA8B26652FD346B4386685E6169D0946233BB186646D3B322EB726142A4C6DACD9AC8E866EB11708EED22ABCD86E2008814B96C976FB67A9399CF837529F18461ED9047F073B0DA1C0DBC3BDE180BD4AB75A81288B1D64474F4797AAF9170C674188061872E4B1BEB2CE82FC968EC9442DE65B59972FE55A56C81CB6B6584F86E566F5202CFD6400579C8242DA9CCECA68972563F155A9CAA2790219846ABA47000927B4231FD4E1131EA23A5B595B4760013020BE7DF38725CC502C2B0C33CC2593B44543F06829AA12B8CB6836068FB0E697FAD760F0251318934A1355AB75497C88D2BBC0F9678E4CA6E930CB7D445B530C7061488999C530B5325CD3F16824C034274B77320CC4191663C1E58A2A321952FB6A4350624AF068B9D95FF58A5E247887BBBB275D65C417357A0C29805450B2F2B859BCB612F6D3401FC2C4A93091C582207AA8512551AA33E4CB896AC42443A33DE84919F131575A3EA1607EB27B6E570A889FD6C38029BDDB6A0E9C77690C9855DFDB483CE35855A484BA791B151759D272925505765F546A67490859E28EA625A252C1C7F80686042B1BE9931E4C7A33166729481644E546BC45293786C10524DBB7DE822E51A44A64C51EACF0A743CA8566310BDFE61902FB7CE2A46950055E1869BEB391695E48259E11B30D34109D63C1D41FE491A35244C5AEA207E40081576B15C73098F375C0A5ECBDFF849EC24D79", "1BF4705C214F0912CC183A44471542F69602C5B544EBD526E472CDF6A04F9F856E4931BA4D818753D3ABDD92C4AB72B03A43C36A969B2FE92938D27BF4452323B973797260FD93745CE95237B4C4853BCFB49B70B25A7E4F54B72B49CF313889DB8C95A1512AC8F516AC22AB3CC59B2E9BA59508CE1A1526846C22AAE6A63FF89955CB48EE5063711559CDC36C64131DD0A97F16854FE4550FB115A57454CD8D22B8E7634E65D421DF6603E3244A0E082247B4C25DA971E0C5193D77B11A661C0E150A7A614196911A95D89C0E5919FB5AA2B3A3CC5C068730818CAE910A1650060026B3B4103ABDF6B1788656FAE87AA5DB97BB6B6F87986617C1986E06A0F055BFBD73923396C334E6AEDC2C018F5A8EC282C980B95E73CB33DB85556894A5855207780816912B1FE35BCF02754671778B00CD9F03DC21439A83EEBC4875EB5290A41BB6075934C2735C054A16E83E77A4844324A5A7E92E1D681D4396B0F600CF7ED694FE56635F51193DEC76B4BA4887F53A107534EE318137C38F02801A15848F1AD79CD1E75E889C2BBCF6256FE239F04B34714053F3D25D5927B42798BEBC6430D541C8E7618160B133E1D42A4E069B86B47469771F0EF54B22550B5868479F07275D297EC233090CC133ED388AE239BD990A1899E778BAC687361B3C275C1ABC4C1046900D80EB5B94C1868A2870BCD12E36D3AF37150E6FE3B2718419D5EA1F4D81CFD152740B51CE77B8125A2229A6204BEFC44C14CB193F438F995ABA01931095433E24250802113A63E15F52B370D78731E7B1551B33BE53E80E780B499E91452049BE2EC33016F268E9A525A8E4253BB592FEF952954B7794F4B3AEC4143235BFBAD30BC22B78CE5BB39CA840A2E5B49DBB355D700314D8893D71A48EB3AF3562944586372677654A948151A9CC8C88086755C2CC175637068545323BFE2388B08899FC2810433231FF98CCF7D77FD00919B0B63561924267327C8E0C614F0476A99727FE51BF6FE9CEAAEB9D8DF13CB50C70A5C0AEE61CB77155B9E6B010AA34C84B6898DA28C5F8A2B0B7B63247A245E1FC1551A529B7B498C35464A4305ADBCB79688525EF0216963409CBC4CB5C37313E5CB1188401259439A1F15A1BEA223C4641340BC408E043D48A80B0B5B6BF180093230960AC8EAD883283120422D58E102242B548A65AD537F3D008EB0397818A2776B3A2E6AC9D363372D899980418AB03B3C6BFC40AD6222CBFD39C22B62222A3B5A992A25AB828C233C6ADF9412B4A7EEAB68B0772635A1977071A831B462B40D84CF87792EE9B0A81D7BC6B2205F09137D0232F3D2AB140C5C86510B07F506A9FB6200F795027866144C419453A3BF2F3CFD2737748AAADCBD64E5CF85CFA202EB808B0AFE73BFF626B179C86524068BC1C799C41558BEC0CA2E4CEB3DBA3F93A9D6D137DEC573FBC127576D6C0E569B4451BCE7D958CC32750625B1185765666774602938EC1F08E0A7A0EC91BCF452165002D422FA5A187F0345F1A43589B333DC70E0860C24C1779A5EAC9B9F863CFB2ADC976A8FFF0C8AC37CFE1370E1EE235D40503E8A52C4954836EEA809AD7CD4E2043011586E6A8BF3E76551D576D60F741AB6C5D3DB2B99527775CBAA2CCE5A2B2570BC3F759565409D19A9A604712AE80448DA8A7DB80413A177137915F4AE413EA050AF8FB2FDB4B6C99B8A2F3401C84409C7ED39BB6A45B318852E3F245F8596177C369BCC22E95F828FBB7796B127E1D6B314F30291B57212661B3E23CC349FBC05984C9F38C7A1205C8C5E86E7BC3577AC59ADD11B4ECB038FF74BB9853B1825B63FA842CB1C251C25BB41974C8C753A25017219B0816729C09905365565939DC677AE0788950789D66C7766A035000688AD401435E762ACA4527BD67965C44966130B7254B8AAC7059710B3B9397860EA243AC567869424EA8D5CA90A240E7D53ACC8693EF452884223BD72A61D6085FF2D6209E39163317949834579A833207E65ACF2458B1F71715379CBD828F0FD26864663D0268A6EE6723E9B7B1AFF4A4B6A61DA07887626B8F35F0342097C25CA5B8BCCA2A7760871CD8C6DCB346D006A489094A78E368FFF9450C6A099E9CADFD0B861077C80FA8BF6C7C191CA2178532B7D92347F77225FAB1669675B024E586D9B50B583278A2EA947B027BF85885CA3398471A81CD153CF9675481EC3D657A623852C8F72A02EB3A950F493471334B871915022B1510506418849C6E52529335B00969B63C6A8F1D8727E0FB89D7E61D6D5C4765B6A3593C3948AA2B2C2B909E1CC766A879EA278A921C2BC876B572F3435CC39CD541C390109CACD51946254CE6E62A3698AA5AE1C01DD220777C9DB8D89F30258A856332C4F540C1CB4CA67B6C3CE2782088B7DD072BDBB1102F403BC534C7AC2A1628261B0EB9A19452118E791220680AF79189002A9E9FA7A54AE38FACCA03E870C48C8B9A58F4060AB60322509E568371A9B3A959E5C55E5B99A0FA9ACB47C422336431D08915046644148ED3F87F873309E2FC684B5AA6BA627E883093D90351CC737E26364AD3251F5E320FFC905F2134825B7C7AB87651A656A9CD49ABEE5772745B798E08B00DC5C41B3C535890433DEB7316167C5873724B7B730923A92C87316EEC59A4192441E1A7EA88762D797D73F6193309B47AA0589707C342450A79550C67876D952A3DC6A65C4A7A3820116BD5F70B8081AA5EF8CFEA09CD4B156808582CD7D078FA2B13CFD5771AC18B76022AB72364111235841343497468F2AB3A134016041437D02237C997095FF0A016E6C48566241ED873DF4C47A05904A8852E56CC82000C5DF3F2769719761D3165942C3D7D3B39FF151C69793B33F699791A4691A934E98386B330340A396C95947DB2017DD2862321D53B88014066697614865F75512E08C63F52002FF96999D5CAAA31128EF89270DAB59A1E909991220C2E9C66C7563A7B572544871F29170D71E29B7A1433AE239F76B21FEB6B9D49E863FFD8463EABBAE5425C0E527A36FA70D4464324AB0CA35A3AEE68369491A434C3243335909BAA6B689B228D9540733B6DCA05015C927ADEBBA90B17014D5268DDF8A06D9AA979FA0917CA4A4C871C4CE6A3908AB158664AF5B5AB6FAA95E0E593BF55A65B710824FB64D7B7A421431A75E78EB7D61BC899628994935D4778CFDA77E9168307B2B6CB0164085634D94CBCA4C8B53E793A00DD8D2FF6A800C55E028012BD6912C48B13432C485B8AA0EE61555D5699AC5AB6944120D1EA030FF2CF785573B0E84FB772B9F3E473CCD47921B6111AF6A690F088C58C7267978BC5757CA9E542A441983BA113654A8448D7025AF4BBBA0306D56403A12568FCC8B892D9AAD06B809D5681B949A19BFBA4C55A26E417B164309E1E48AE245A81734C141D639D89677A29E231B5255072A37C90925AF0D36EB39737396C1A02B513EB98BC110870CB9565F2898690ECB0DB7807E3E858C57BB642CCC5EF0C8F00D733B3581382994E0C2C74A8B140D6EBC02405835986C2D913077B6C7BFFCB230BA6181D765620B0AEC43B8D15F64F51ABC92DBA2E5CD53021537A6BC06091D2C1E47B2E049A1207617D9A6703196623A0CA85054373D4283EAEA0A867B308F362BA8B26652FD346B4386685E6169D0946233BB186646D3B322EB726142A4C6DACD9AC8E866EB11708EED22ABCD86E2008814B96C976FB67A9399CF837529F18461ED9047F073B0DA1C0DBC3BDE180BD4AB75A81288B1D64474F4797AAF9170C674188061872E4B1BEB2CE82FC968EC9442DE65B59972FE55A56C81CB6B6584F86E566F5202CFD6400579C8242DA9CCECA68972563F155A9CAA2790219846ABA47000927B4231FD4E1131EA23A5B595B4760013020BE7DF38725CC502C2B0C33CC2593B44543F06829AA12B8CB6836068FB0E697FAD760F0251318934A1355AB75497C88D2BBC0F9678E4CA6E930CB7D445B530C7061488999C530B5325CD3F16824C034274B77320CC4191663C1E58A2A321952FB6A4350624AF068B9D95FF58A5E247887BBBB275D65C417357A0C29805450B2F2B859BCB612F6D3401FC2C4A93091C582207AA8512551AA33E4CB896AC42443A33DE84919F131575A3EA1607EB27B6E570A889FD6C38029BDDB6A0E9C77690C9855DFDB483CE35855A484BA791B151759D272925505765F546A67490859E28EA625A252C1C7F80686042B1BE9931E4C7A33166729481644E546BC45293786C10524DBB7DE822E51A44A64C51EACF0A743CA8566310BDFE61902FB7CE2A46950055E1869BEB391695E48259E11B30D34109D63C1D41FE491A35244C5AEA207E40081576B15C73098F375C0A5ECBDFF849EC24D79666D7CA68B92E449435BA817F8C3FA9AB2E0EABA14809C7B4484DF747A446D36BEABA3A6A443866994CD9C1C2B70005D87AFF90F4D00A7A232F3D175BFEF72FF"), + ( + "F3A706FAF090C03DB506863AB0B20BD8A1627956318E88C67EB875E8E7266009", + "35D2BC43DD1CC879F765BF2A0C5E297889DDE910E57E2BB0EAE417B90AB7A275", + "8D0923CA8A2DA2B4146EC25321122B8A5AA8AFE0C03415273008A46EE83031E98AAAA125ABC75D3B30322560C197E75DD0E48A348099F7B2144D7B8A8660A4A97BCF19C0583BD9BB2123033CD7BB5A14B08B817831A673A28170F5F6443C0551913A327CBA18C3A053C4040250403B70AB9588832403AA0FC37665E04980FE1602E7D2715D9CBC00515DF432A4F5B32B3BC92AE3F31700166D498123E94576509B712B18491B1435EE7AB7AEB1AD30D72348C3CC083ABE24A8B12097BF32F792476288EECC3BF630ADCDAC6ACA7950D9839501A448500742BAE37F109203A809B2B960A307E25347A32C3EAB79288173A878789B296E9E8C1C28C5BB3AC472601C9765F7B77225A810C7B85370BEF4A5B079D2015ADA54236B8F33840675F9B2EB427A1B5974CD5C61B24010886C5A7BDA5BBED974AF7217F3338AD719CB308A8BCB1B6D6ED2A1643736C29095E8A8452A3A36C7BB5AE58CBFDC61529466A90F454ED6895B0861083DD1371999B2F559A3A487CF59A074FB49215EA6A6BE656F9AF17B121A2447CB7985590E9738842B899BA57AC311810AE2D9794F37483DD6BCCB64AF6D56588AE94665961C025C3AA2861974C236BCA4BD8FF5509F7AB774593E7C5549E57C2F18D15C0515094AD9A0DFAA0601E524F8231156B627BB25A0DAE04DACD0A66C041CEF400583FC13BAE640291A39A5C5CA8BCA1AD5C683CDD8290891A76940817DD8C9F52678780548E37A05806600801426DBB950C3B2BA34E24CC77864DD91B39F1408C716A69DF63342854E50A245FB50977B9410DED2C93F86B1F9D5A78B87BF81E51CA620A7E8566B19AB700964A40E3266415228D432156E5CBDF52364A90483A55C39B3FB16FC7465A3F8AC801B70B9FB28B583444BA5C1A73722D417A9D6D9B7DEB08BC6B330FF27CF61AB8831E27758C64AF3B12150CB7B33ABC29858106D63686D8762459ABF9413850AE53ED6313F76F83D0FB8AB34374E7DF693E4A1B3E5A8AD0CE820AFE1CF401ACDE650A8101B0946022D52178E19613C42B88B07CC04EAA81DFB28AC9DC076236B67219A30F8F945DD57BD2F335C52D59372308D38993467DB53DA3382B74867B616481BD0091A2232C1116DC88A589DB9107224A681008C67C589186A6929549BEEF92253DB02B0C8AA9F9C875A670266C72BCBDB4F5625043703C1A0457395832E4C335180462ED2220C59E7361903C107D85457F6CD82EB820D0855D97675C2E0151CDB73C2885DDB7849D74541580124E890116A65BC068093B57914E20C937C60A3EB25576F1A976A9583839B672144CD4A45C3477A45C29B4E0BC2BDBD206585C9B7A7741C8B6B5793A92797A15AE7A5B73A74B2971463634BA52AA792AF05530730B6D0A89A346156B733677932BD36593A7496130CC458DCC5CA987C21960604EC8A8C5396056680CBF3F1AAC4F401AA5029FB2150434BB4706C31A2D54E4297939FA7C9C6F85700613CEB65C7F03AC56EB86E2D27CCC6DCB7B9394DCDB942FF222D86958A996C0CB6A8A44F97A70441C95FA71250116EEC20863C0B5A643458788AB001F8869D909922F51EE547A1E889255B3A0599C65842E5AB8D73872F053BC62392EA53896D328102D460BF1609583C22C3B43780EC6DAD0319EB4A5A65B4756C3CB40EAA935183BF8BD46ABE76BA46E199103A5313C3235F49C915E097BCA804DE680781D8365731BEAC6789A9203FB8787C4C070E00A13A6722A66A28236DB179825653D33CCF898B72C6B8450D97AFD3276BB13340519CBEDA708D12A858F54C49F4547195B7788A9150B2649E36AA394121926D568A488B16D3557B2A32AF57D11FC3373F80A28C0723273D362502E7C428AB44D3CBABF9EA585FD1BD0C9846556A1E196B78CF951592984A0A8487A78C2317D7ACA4118E1049750A0788F0D66AD9E48E34731130ABA0B427360A856D96D80B3F028FDD3ABA9035C10106BA1C0934BED36C6D7C7434249654EA89FC22137F4AB903653B75FB25B6F01635E6CC7D39CF1508690562826B49B6FFC59E0DD35022E541F8BA0D304AA5B4E20606907C424395666C54ABC2B8FB009847C86317685000C231215C8C15945860F6A85DDB98A8C3A527F2749D3C027E694E8F0B0F0FA454913AADB635AADD452F7128BF7752569669A8B93290EB92E78F6ADFF23E89F57F3890753B51F12F3F3A8A654E677847", + "6CBBB491D7B3A3439F53C9027BD727A0E1C78004B4CDB66760E50F1BF97FFF0139F82862F1F3178A077B6AF77735CB802E91316BC207B9F1485FF7A868891E003BB9C6558BE6052C0826AF1CB5836B94C7D5D714EEFC7CFD983E9884B71348499DCA05DEF33B49D1C14BBBB95AB195BD72219B18BD0C704275CC94AB06653BBB9E0EEC74181903A0081AAE068B81EBAE8925335B8134B1480A60F758282C2F011B909641266774513A19090BC672EB3A0708CB577F134151C155C1E560D9B335A35A4D75F223502752CD250C9A7778EFC76A52709813A24679B987A6172F1714641EA6902D364F96357E92B44275EBCCE7F6207BB0A215E98B3AA92DAA46A39AD9132F3874C6239391E716BA600E42BC7084425051F865756A89DAB46A76104E183A12031B99BE86257B583C2E88104FEC3BFCAA50F238BEEC20ABF31219685885F2C3CE2F80C09B874E73A8712D35B8EE819D84B1C59A37947C0B9E25F066EE030ED28172FD5914B562408BA9B9DE9BAB2B95A4F30C22ECE458E133371A693D9458058265A75AD6CA5707617B38A1B6F396FB199CC1C77100741DE37500F1BBB10B9204EEB7C3F5B91A1CC91B6D4072AF410957358ACB0625488B9F6A9915758B7CC9B4AFE7D5AD6A95231A7A711B3A8996E23AFB3695B45B6D919C771D7685D90A7550A9B5B1E7538A365451A5A815009E49326C6541CAF0BA685056BF82EA4AB504161B555568583802800554A9480602437B0B9677FAC638A2508154A658E0623C62666E078D21A5A545A818D66C9547B9749458B735A4AD51812950E8A625A1B2A753ABBBA16415006300AD32D34C8B0B3B2AF4D1CFBDE08B4F06C70DE4BA28FB3763745CCE133DD2E488F89A51E7A586A86AC928867B0541438AF5645DE3211AA94ACA4A1095896EBC363C374A85565C1EBA030B5EA21B9B8A140E131ED07580D5B8590446988DA65939A823A84661B26CA12461AFE373519F43C13987A96A9069CD6B9177B205B25B5197DBCDE66C6457892C1DF96FE50AB7EF3B3DAB0A87436B3451476C44AA22AEF918AE5A1A8AE46920263032E3BC5DEBCFEE43B8AA290CEDF06966939EE6CC7C6AF83CB9D90CC889AB8B5A3D4C224E73916AE057BF5455528CACBED31B554043C6B0EA196854461D2644461470EFC55080E326FED49D62B5A4BB06B14F505EB4486C9A1B033B927836DA656C2475E5B8B823986DCA99B298512D94B77FE0588DBB96BA53A67369B82C2BEAB2CCC0103E549317A99DD97B598FA0A506184EB55C96B17302533C7EC3334E3CEC2BBB22BE0F89442840884FAB4A766A41BC8061D4ABCAC5190C729309AC6151BD434B818BC1E7D0CD435C9CA6DC7182228FF93867134A5CC65C6167C4A8BB830E7FEB863C15BF87B259EB0518FF5248792A625F37C589589799746D36197DC63AABD134A00EF1B49C79A2DF67B28E6B6E989CC9D0577EA5AA1B2FE64B322C7A8B241D503523D875CCDC479D28E3C01BD8383DCB2B684281E6949171F518EA97009237B5CD42AC8FC8A571716EB784B336529DB67728499988B73B80EF5C6D33F3B42ADA7A0A36A7C6A8A0DFB804B3D7BB05803957B913827A16F52ACDD7650EE2971A85DC21F6A94A5B31988E4438A9C1733AB8C48C32653FE800BF81BA66658CD2001ACF3A83E3A0426AB6C3B2600A53621277A43D723AB96758B120F120248C15EDF4294FC38F96EC490BD52094655841D9355038A379D8B666875171F39218E21D37023F5108B6D15389F07C1241E15071C43CD7D160EB5341B719C2055424EF6CBF66815F84943C9481CD1DE685A74C5B9DB37A0713952FD4A67BEC8CA7496F6C9C5FB84A8EAFE68D54654C6BAC9C3AB712C2BC32C0CAB072C18DE3CABBD9224DFAA305DFDB3F85019D7B988577B40F27514822DB42D87489502C53CCE6A095B486D9143D6D160B1C7313BF288913657698117EB145C0C9C55D66C15BF1A2B268F6A5FA259139551ABFE3468F1CC49C01B4232543DEF0BF9AE503B998536310751A8BC302B4B4440B835233797C967E7F9308155B27BA63C65E08BDD1946828196B30C042CDA0B56E4008215AC51F373604F5930BB66297C32BE41A162CA21471CC86657B5CB5307D89091DA9A5137F1C1433AB8AC1D59DE421C7C17A883EAB99BA4714F841998D0923CA8A2DA2B4146EC25321122B8A5AA8AFE0C03415273008A46EE83031E98AAAA125ABC75D3B30322560C197E75DD0E48A348099F7B2144D7B8A8660A4A97BCF19C0583BD9BB2123033CD7BB5A14B08B817831A673A28170F5F6443C0551913A327CBA18C3A053C4040250403B70AB9588832403AA0FC37665E04980FE1602E7D2715D9CBC00515DF432A4F5B32B3BC92AE3F31700166D498123E94576509B712B18491B1435EE7AB7AEB1AD30D72348C3CC083ABE24A8B12097BF32F792476288EECC3BF630ADCDAC6ACA7950D9839501A448500742BAE37F109203A809B2B960A307E25347A32C3EAB79288173A878789B296E9E8C1C28C5BB3AC472601C9765F7B77225A810C7B85370BEF4A5B079D2015ADA54236B8F33840675F9B2EB427A1B5974CD5C61B24010886C5A7BDA5BBED974AF7217F3338AD719CB308A8BCB1B6D6ED2A1643736C29095E8A8452A3A36C7BB5AE58CBFDC61529466A90F454ED6895B0861083DD1371999B2F559A3A487CF59A074FB49215EA6A6BE656F9AF17B121A2447CB7985590E9738842B899BA57AC311810AE2D9794F37483DD6BCCB64AF6D56588AE94665961C025C3AA2861974C236BCA4BD8FF5509F7AB774593E7C5549E57C2F18D15C0515094AD9A0DFAA0601E524F8231156B627BB25A0DAE04DACD0A66C041CEF400583FC13BAE640291A39A5C5CA8BCA1AD5C683CDD8290891A76940817DD8C9F52678780548E37A05806600801426DBB950C3B2BA34E24CC77864DD91B39F1408C716A69DF63342854E50A245FB50977B9410DED2C93F86B1F9D5A78B87BF81E51CA620A7E8566B19AB700964A40E3266415228D432156E5CBDF52364A90483A55C39B3FB16FC7465A3F8AC801B70B9FB28B583444BA5C1A73722D417A9D6D9B7DEB08BC6B330FF27CF61AB8831E27758C64AF3B12150CB7B33ABC29858106D63686D8762459ABF9413850AE53ED6313F76F83D0FB8AB34374E7DF693E4A1B3E5A8AD0CE820AFE1CF401ACDE650A8101B0946022D52178E19613C42B88B07CC04EAA81DFB28AC9DC076236B67219A30F8F945DD57BD2F335C52D59372308D38993467DB53DA3382B74867B616481BD0091A2232C1116DC88A589DB9107224A681008C67C589186A6929549BEEF92253DB02B0C8AA9F9C875A670266C72BCBDB4F5625043703C1A0457395832E4C335180462ED2220C59E7361903C107D85457F6CD82EB820D0855D97675C2E0151CDB73C2885DDB7849D74541580124E890116A65BC068093B57914E20C937C60A3EB25576F1A976A9583839B672144CD4A45C3477A45C29B4E0BC2BDBD206585C9B7A7741C8B6B5793A92797A15AE7A5B73A74B2971463634BA52AA792AF05530730B6D0A89A346156B733677932BD36593A7496130CC458DCC5CA987C21960604EC8A8C5396056680CBF3F1AAC4F401AA5029FB2150434BB4706C31A2D54E4297939FA7C9C6F85700613CEB65C7F03AC56EB86E2D27CCC6DCB7B9394DCDB942FF222D86958A996C0CB6A8A44F97A70441C95FA71250116EEC20863C0B5A643458788AB001F8869D909922F51EE547A1E889255B3A0599C65842E5AB8D73872F053BC62392EA53896D328102D460BF1609583C22C3B43780EC6DAD0319EB4A5A65B4756C3CB40EAA935183BF8BD46ABE76BA46E199103A5313C3235F49C915E097BCA804DE680781D8365731BEAC6789A9203FB8787C4C070E00A13A6722A66A28236DB179825653D33CCF898B72C6B8450D97AFD3276BB13340519CBEDA708D12A858F54C49F4547195B7788A9150B2649E36AA394121926D568A488B16D3557B2A32AF57D11FC3373F80A28C0723273D362502E7C428AB44D3CBABF9EA585FD1BD0C9846556A1E196B78CF951592984A0A8487A78C2317D7ACA4118E1049750A0788F0D66AD9E48E34731130ABA0B427360A856D96D80B3F028FDD3ABA9035C10106BA1C0934BED36C6D7C7434249654EA89FC22137F4AB903653B75FB25B6F01635E6CC7D39CF1508690562826B49B6FFC59E0DD35022E541F8BA0D304AA5B4E20606907C424395666C54ABC2B8FB009847C86317685000C231215C8C15945860F6A85DDB98A8C3A527F2749D3C027E694E8F0B0F0FA454913AADB635AADD452F7128BF7752569669A8B93290EB92E78F6ADFF23E89F57F3890753B51F12F3F3A8A654E6778479370FE5B05DDC92C939F62CBDE4C0FEA36F45CD20C5748CF3AC891A4C260449635D2BC43DD1CC879F765BF2A0C5E297889DDE910E57E2BB0EAE417B90AB7A275", + ), + ( + "52F2DF12173088140CA73333D1DB4341949BE452049028EF0DD904BC7E0DDEF7", + "CC96DE9E1F0506BF490A154A6DB46BD2BD1413C3D1DC163190FF171EF0D11E22", + "88C29C2A91CE80B13EEBE182AA709A1694A456B139DEB1977D531AD2722AC7D2B356AA2684D767C60012B1A2345A69AC52C7A814858E8C741A80C4B5E942CEEE0045B957748E2847EA835E1FE17C70456DBEA13FCEE0384BF431225C8A6B499624F086BF71518C8C269640B436E7783057372825768670B2478C96C4A1A2ADCB1F83A057263362476197C5D878D76B025F83AC802CCC02296A99296CF6B23C5617AA95387C2B0071C23AB7BF614371BC7001E29D7109057DC03053FB38C0084C48890612369C649C0BA3C2603E377C84D15B6DB6BB6E9513ADA9A47210861AA055D14683F0556729192CAEA8AE9A538FF810596D64BC0964B380CBC2F5D706A0A8135C1B87C3480C6D54CFC28A53C339666E24213D33A62EC872D803103987A36256840DD98DA8BC17ED42C34F5C008F6C678C748F3F7404D17B46F29012F079A46A0ABF77C04538254ED4B6BDEA929724AA60014674C427C55B1B0192CB284ED02194F9615417B08A9580DCA78944F43094E48AEBBC8BF0126310D9AA79530457E04DBA9B0D02B045AB28391F923C25CAA69AD038B4586350613D9AE22896F2979D56BBAFDAA294041AA42C21B9AC59E41CBEDBC33AD6996C3451B7DF5B9AE2D003F5092B91847290336A62E804B5F167D7CC4E31F40F6C640C10EB99D7F5519C80147B77843CD083B3B5C03718B1A7EA2DBE25ADBD56762E9150B46B6B4E82B769C384CAB39B3610070C6B6E5981C3B182105981A50567514CA15EB44620C231740FEB6BCBF4980F7B1824A5602A982A68BC8961BBC441B111C93B2374EC88FEF30D4931AB0CE3B5BEA680B1099CFA79B3FFF2B56BB3031A8C07519C7DF4F75A8C887C6A1B7249A700717B2798C02F0EA04E720752D8323376E1635331B07435CDD5BA6D7228281612BF060C694BAB6A00A8700977518BBAC8195853FDD34D44E29FEA7286DCFABDF47C9B2F2347EE7B3133CC275E601AA1874BD6974894C99D9659C1910030AD033138A57F8D9B312208873F733DB9C73007F45C49A007BFAB03AAAC3F107C8C0A032CBE24602A2BCAE4D2658D21899EA89EC5E832C0565701E5BAF0DA89E1B55ECAB7802EC830B8268F6D9CA3883C97AACC20FC4861EB59707AFA18A7B24534956301F238DFFB68BD20A8264ACB6BE4918C85282F63CBACA87A3530515041C5A00CADAC1A0B365B8A6CF626B30A49338461E94B44E20C250A23C7473952A20404FC17055E124792621D417CC7E20381BB24A701D428B1643BC0054B5FF46BA9129C0126889156BC6418A4C3B8C6D41B3D8646A915592305C30886B3995400A2A80771A494795D4B4B9EA6C21E0B21E0933F9F7C5FCB3B4C20AA303F655CB003526F8B04B3C85260C00C8E316DB495417DA22BD3A975FE784434C04DFFA545F91555F01033696BC4B87ABC0665B7A5C972D9A08FC8911147DA3CEF156B5CE85391161F32D09F6F8C0F8A845E268AA044E150944247E2B4354CDC72D3EA01B5970A6061B4F83563390BB33CF1CF9F910858DCCDD6C65E463607DF5289A3684440546816D4C2D59B214A15347EA9580310786A182EC6C2ABB73062FA281AC6806A6A042C0E95312027AD66EA6E245A9F2681A24CF505D5DB43F8A8C39BDC12E07C9F805634C9E5725E18A4229A20A681A0CA5144A8611DC372BFDDF102E5254AC1955450B94CE601AD68B5156996AC8C8A1BF1962A2758B4CBAAAB3A341E9C7C19005AB82BD236CB6294AEF22F1A1BBB32DC6EBD1C15FEB409E6C495EF1013E979981DB92BFF97BBB3B34DC385176C440B29637F68540BD10143AC33CDE4D2331AAB1DA5D9C7A41344A073B41FE536D5890239A96A319852593671F6A22E9480775F265C66D68521A73E9EC83DC6AC2963D3B70C3453861766048B18A135374ED53521A458E1FB024100116F434D1A261CA1C579F4311549243AD629525EBC465F8779C7E1B50344009C487D84A7B2683298EC39C27AF80FB3F28C61D388959A99721857CFD30F98F43A19716CD9E698EC946E69FAC477CBCB8E060EB9C96D3EE64B894C748133A9BDB5758CE45DDF791A68D87D1EF70D9AF626C3A909451A132C0702F0731D5D5959A732684C1759D8D31092669E70ECAB9F804CEF4CA416563AF1F27A219B3FD6BC482901201ADA9836B824AF9BB870DB2C9CCDAC525933E58A16C00B373DD81A3B89C39E9893960D7F6CC246A53DFFB9FE", + "09766134E63C40A74D70107A121B44651CA43F837316F63F01290286304DA07B45FBCC418092B9FEC00EA7C88A4F20A0ADF9188E23707C88CFFDD6CD86F35A9E9A3D4F842C84A5C8662048395099037ACE5FBA75495A6370B9118D11324788BD4A841C90D219AA6BBC5C829EF2C291AF3AC99CF08672D95225DCC4AF7AAA428B7EE7201EEF8966A10B88067988F62C27FB52470C036535D762DF868BDAD167A383C74246B218CA68EF9B5106979E30BA4B278A731EC370750C2B37135D39B32F811AC667E717BE098B3F278F93815B2270C01723C322F1A3F4D8B1026A39808A9DE51B8453507464E283694BB937778161E604E127CA85F10CEA05CD28458503F100CAA530874BA500A3102CAA3802291DBCF72D457637C557C8F70012EB78B9DF1948F6055974C81AF1FB6DEE601FE40C3D185251441437582B57AB29627BCA1804C3B53EA76325B925982C8CEBF50474A5BD00E43274437229D94494B7536BF473435A1F3C552C45064F4DF31CAE2338394BAFFC8354D1005CDF0CC7D5A8782906135739079DDC727B55A67515C609223FA3C34B2EC24D033397B0D8C6777547E5C635F5E546DF52A207D2CE1AC3A9F85179A8898050078E93093B24122A7F553CEC39BF42B35A07F128288A23C91C9232B835A31288461640CFAB5EAF9A9BAA63AD40044160369C899737FFC8590C83A20CAA997FDA5BE4367DD00B91131C0F4EF61B0D207EEC750A8F4ABD5CACA994B779183051C6F52DCA8696EDC65807758ABABB04203234113052C7F894A478B564EA69D14A1970D2BBB437908F2288D0084EE7D538A3FC628E311D5D558CC2113DD1762DC3FCB336A899B583CEB457A8D55935F6926E1B5A2968352D6B1A35C986915C248E8C193EA3816300C67173930560E8455C560F3817C58848CC9FB3888037B449AA9268876AE4F665B5838329387431408068125F7DA65027917AFD42BD01389FE1F64BEF5B0EC688396CE58D622975D69818888029F6C0005082B9AA14B0EBF9188CE93C3B127237597FEFCC4DCF15B7F70518D8A9B34F1826C5B01CA9F48D1122AD41139C2E625F37DBB431957CD4DB6818292436846745A933014200EFB80FA9B4A4D38824CDD05C1422243A440126855DA2948024E136A23732B98A3319809948B9AE25ABC9B6A5A96AEC8A50167DF81C475EE586DC137DD5ABB7D91A0211258A82982DE2114A808A4DA29B79483A26F3CB73E5985BD68010F233AD7C0464C39CB0EF02938F036E1B099313318E81089CE78C1D008496B69764C08C7BA7DC00328846268B0709104AEBB8C757F22A8A45274CF75E80B1B377A971940852DBE1579B59B7B7901E06ABB7972B60C9357CCC0024C14341EEE51502D79D51D87A6F42897F5955A2B44C73766662F940D47B7DCEE02E22B21E138014D735404A13523EC23ADEF21A3184939AEB0BDA5264798146E1702F42C58185C3B063E1B884455C2B508DE26C5D334C0020C7430D774AAA85B2F9007FA41495A50BAD7315C4CAD429BF2B5B733690A8E422506C36FBC877708BB9F51CB899A48D3885B73933972F97A31DF12C37E2CCFF5608FE500F0B44585CFA0DFFD6932A2096CFE13518FA06B2E93C6F31CFC6E5C1973C2FFEAB73C0D5AA0F7A2C1B922A62065A423187B30B1DD261104933A036A89BF1357961008707D7C4819C6746E137531797E2B554E4C06F57A1811F9B1C0B459B2CBBC4C90293821AC6627B20A1BB8F93752222BA795E0448735840DE552AE26C2B8BBCAFEFF61E0B7898EF606DF88156E9540CCDDB6F4ED180E8E95D2404CD7FC15DACDC4873A2276DCC1892A36B4EB8C973D6858A478FA669052A04CA890C4FEED830553670E5F07FC43C59A0584802E541C26AC4C3C5774F79CF2251274C54BDBFA5B726CA710530C87EE49918A41BC50A3CE5A67C05689646516F2E77A832622EC11C9A69F70B4D014FF35182FD8B6CF2E181710C2EFDA85358712962AA2F04C58360756469AC4BFE861EA019923B68A4B63975064596F39B83FA7083620C20F578B7C96CCCF4FB5D485726DECCBDFBBC6EEEE2185FB46379575B5A875B28653A6945A7409C0402EA40A386A4F0F47D5BB1C972E96216384F9C48489CBB9DEFA92B2B983E11B796F0F176C6D5264290AC88DB13AEB93C022BC8C2266E88C29C2A91CE80B13EEBE182AA709A1694A456B139DEB1977D531AD2722AC7D2B356AA2684D767C60012B1A2345A69AC52C7A814858E8C741A80C4B5E942CEEE0045B957748E2847EA835E1FE17C70456DBEA13FCEE0384BF431225C8A6B499624F086BF71518C8C269640B436E7783057372825768670B2478C96C4A1A2ADCB1F83A057263362476197C5D878D76B025F83AC802CCC02296A99296CF6B23C5617AA95387C2B0071C23AB7BF614371BC7001E29D7109057DC03053FB38C0084C48890612369C649C0BA3C2603E377C84D15B6DB6BB6E9513ADA9A47210861AA055D14683F0556729192CAEA8AE9A538FF810596D64BC0964B380CBC2F5D706A0A8135C1B87C3480C6D54CFC28A53C339666E24213D33A62EC872D803103987A36256840DD98DA8BC17ED42C34F5C008F6C678C748F3F7404D17B46F29012F079A46A0ABF77C04538254ED4B6BDEA929724AA60014674C427C55B1B0192CB284ED02194F9615417B08A9580DCA78944F43094E48AEBBC8BF0126310D9AA79530457E04DBA9B0D02B045AB28391F923C25CAA69AD038B4586350613D9AE22896F2979D56BBAFDAA294041AA42C21B9AC59E41CBEDBC33AD6996C3451B7DF5B9AE2D003F5092B91847290336A62E804B5F167D7CC4E31F40F6C640C10EB99D7F5519C80147B77843CD083B3B5C03718B1A7EA2DBE25ADBD56762E9150B46B6B4E82B769C384CAB39B3610070C6B6E5981C3B182105981A50567514CA15EB44620C231740FEB6BCBF4980F7B1824A5602A982A68BC8961BBC441B111C93B2374EC88FEF30D4931AB0CE3B5BEA680B1099CFA79B3FFF2B56BB3031A8C07519C7DF4F75A8C887C6A1B7249A700717B2798C02F0EA04E720752D8323376E1635331B07435CDD5BA6D7228281612BF060C694BAB6A00A8700977518BBAC8195853FDD34D44E29FEA7286DCFABDF47C9B2F2347EE7B3133CC275E601AA1874BD6974894C99D9659C1910030AD033138A57F8D9B312208873F733DB9C73007F45C49A007BFAB03AAAC3F107C8C0A032CBE24602A2BCAE4D2658D21899EA89EC5E832C0565701E5BAF0DA89E1B55ECAB7802EC830B8268F6D9CA3883C97AACC20FC4861EB59707AFA18A7B24534956301F238DFFB68BD20A8264ACB6BE4918C85282F63CBACA87A3530515041C5A00CADAC1A0B365B8A6CF626B30A49338461E94B44E20C250A23C7473952A20404FC17055E124792621D417CC7E20381BB24A701D428B1643BC0054B5FF46BA9129C0126889156BC6418A4C3B8C6D41B3D8646A915592305C30886B3995400A2A80771A494795D4B4B9EA6C21E0B21E0933F9F7C5FCB3B4C20AA303F655CB003526F8B04B3C85260C00C8E316DB495417DA22BD3A975FE784434C04DFFA545F91555F01033696BC4B87ABC0665B7A5C972D9A08FC8911147DA3CEF156B5CE85391161F32D09F6F8C0F8A845E268AA044E150944247E2B4354CDC72D3EA01B5970A6061B4F83563390BB33CF1CF9F910858DCCDD6C65E463607DF5289A3684440546816D4C2D59B214A15347EA9580310786A182EC6C2ABB73062FA281AC6806A6A042C0E95312027AD66EA6E245A9F2681A24CF505D5DB43F8A8C39BDC12E07C9F805634C9E5725E18A4229A20A681A0CA5144A8611DC372BFDDF102E5254AC1955450B94CE601AD68B5156996AC8C8A1BF1962A2758B4CBAAAB3A341E9C7C19005AB82BD236CB6294AEF22F1A1BBB32DC6EBD1C15FEB409E6C495EF1013E979981DB92BFF97BBB3B34DC385176C440B29637F68540BD10143AC33CDE4D2331AAB1DA5D9C7A41344A073B41FE536D5890239A96A319852593671F6A22E9480775F265C66D68521A73E9EC83DC6AC2963D3B70C3453861766048B18A135374ED53521A458E1FB024100116F434D1A261CA1C579F4311549243AD629525EBC465F8779C7E1B50344009C487D84A7B2683298EC39C27AF80FB3F28C61D388959A99721857CFD30F98F43A19716CD9E698EC946E69FAC477CBCB8E060EB9C96D3EE64B894C748133A9BDB5758CE45DDF791A68D87D1EF70D9AF626C3A909451A132C0702F0731D5D5959A732684C1759D8D31092669E70ECAB9F804CEF4CA416563AF1F27A219B3FD6BC482901201ADA9836B824AF9BB870DB2C9CCDAC525933E58A16C00B373DD81A3B89C39E9893960D7F6CC246A53DFFB9FE7708C928659DFE465AE813012DF3FF6EECB1E081391B0A94172AFF81AA3296C4CC96DE9E1F0506BF490A154A6DB46BD2BD1413C3D1DC163190FF171EF0D11E22", + ), + ( + "F2A1E83CAD0055A85EB52F1BC1B9EE24F7F58D9C2FD099B5D38C9F75AD657578", + "26D7035CD963C74AB142936A91EABD3074C583654945F9F5FDC29F754B06EBF2", + "9331AFCAFB47F0E805E29B98A2A3C2D9D49AFEA806FB1CCD5E24105FCA6C3CCC1551B613927AB4CEE284CB53968DB3767C2154295CC479F06A4560385BB16B29273386320FABC520E5B36FE4D85814079E7E835D7935172DC893FF9277B177B7D8A9ABF9C66DC6A52C25852639540220FBB4E2E576440351F281712910CBF6A02F5A1B786B006DCE399794191D9F6BAFCEF1545E3A960CD204868463FF2B8A20944211408CA82AC182D05AB4C32D6A556FC36381BF163F69377E0028C8E8C70F0AD7A526CA274F9B65BD3ABA9164BF34F74B47C56966E4BF8E1B889E54CA29A037E67C28FE2A2B6710554D3813B55131B2B75DE0A419AB8888151876CD7B476CCA91FD83480D150CB3206F6A40B95E3412F9E7288EF051D3C99ED40B9E29A0A65CA1739F88122C72CB15950EB99302CCF18EDD17446F165930396F85E84E52E221B5668431B55CFA08D048A1386D450938D48CC2A94B38EC1F3F6AB2FA5565BF3B4BF1638AEB6A2440E5CD840C564183331E1AB7CB114A409CAB6D88C84C4545396360B1FB37B36CC405451D7FBA970FA7433B879EA8CA0BFF7C4BB0B98B255412AF695511A9CE168BC4FD50C27E121CA0221DE918778B941019D32020EC2F306C7BDAB38F9518BD51FC374F84B5E4B700F05785AC39CF9CBC0DA4C52150C9485D4A6BB4CC3C54A21DF63C2044B97C8709A5DAA82933197F1EA24A994370A2C9495E0B6660A10AA62BAB0BDC4DA284CC39B7C0E0464798A7365EA106793815B7B2BF02EA70C5B506CA343C38386BEFB69D6E31A1678BBD497BB1E64075B4158205280802234AE612A588CC8B2EC24AF74C5EAC085EEEDB13B827340AE29F348A9E31A20244D20A4B2B41F46A6A4C8293A2B8C5E3D3C49CA1AC0724969360BDD8A0822DF35D16A7CA48F90BB2C7972CD70B8DFB6CF5F1453F69266B04C2C32987DE0565BDA632F4B257CAC7CE7953A70B29681BC4602F3ACC3CFB3BFB63A135A758E461647E344A454AB3A1A72D234268D8870ADB84AAC250127E2AA593E79828848C112C26592A89C4DA358BF20571144C3C715449B8AE46606A51E150F4CA3593BC9ECC8A0382532610328253F488E733965C1742A36626524809C548397A922E79B82FC35B1D0EE41975CBCFF6F38BAAE28F0B452BBF792CD9023EB893767FD36E855774F1600399DA75889B03A95A21F32C0162540CBAB08B82B8346CA43B3E07B44F8C5AE6255CC379B3FE631A434349533B85C164B44D7805DE49442D113D238A9856E1897F0352130369387C3F2BE95435885A117072EC762656BCAB6EC1BE33C80C7835C4670B1D142767E23BC0AE6247529A3C8F348BA37ABE3507AC93C939560A445CE696881248F2A6C0B19B3900DB2E803A4BE0D4231F41A65702502241BBC724AFA441991A0A46DE7C62F6A220E457B2D876C44BB0A5F3116868E2B6D63A44A5C55F53F83E10E534606B76108CA4BF12A1B97A61A28B1BD184A9C2057655D460B65C407CB93272F44509C432DAD7B03A7393CFC160861CAAD4E9C81B9CB1085539D8A2C1855421346ACA46F46C3D12696237B237398B4015467CE7A6030A9666736717E6A740152BC2939998724018525D74724C5BD35B2D1887F9D2C6733A1173129DF3AB625E48C6664207C4D4931AF4130CC1CA0EF9160898890B49674F92A9464837F5597A7CA26BA854A4E0FB756572766DE2B9FAB3820818CBC600AEEF588B59B416F9568E19FC4B03902CC13547DC18197CB1AB23190424EC80633A5D58D861D9BCA5450102E52247A74594936153723C0A561B64F4E75D90E805DD7087FFD3420B4A0CC54CBB32944693A393511CA69E2C2B7A3A5B58784F2E999840BBA8C2C994852151569CA0949616DAF73328B220DBEB7D43794B1373CA3E8AC69178653E889D67E8546D35A40478B9C6410D9376CB28D6080AD41FE834B970403B8687C29011C6C8E24137F22FB7192443190BC2B095594C85C55389D4D49722124F07067FD9F34319565392799C27550934127538524E1655A03F697313130CB0F6799DE565A5DB8B7C23C7A8F53AA7714C7094A86E67894A75A489E6C414B67C52030F40EC5C599969D8E00B7C03492C8CA6DFBA8D641307921712C2840591FA9A847B39794BC22747830FD956C0C17B5C974FDCE96F565355B500A70C3D86EA32C9290514B1373184AAEDB75D90976FE6765F4747BFC946F1", + "8EC50C8143608F9186988602849A965D48645CA331C1815E506A1C51D73AAF18BF92D39BB18A5E681447EB78883CE48E6C375AA8E36CA82ABA3E188D8D795DDEA50B655B2E9409C35931C934BCB5820C6D2CE975FD955A9BF40695650004B4C62447AF3E1082DEF45A4AEC121DF9C8D36C8264034E55F926BD5053421C809041C1701650EC0959E4EA6D09898B8BCB90F95209C3397525D984F6079063901BB82478F9B1CE08875449355FB16A3921A8CCC9547349C5650BF49D3D9BB523092607F2B4BB437B1A969C35525F321333E9436B66A08699A9CE4017C6D7485145C408FD412E69463A558927F0763ECD8B24C344640535CE10DC41780213A3DB5930DC40CBA012CA5B22B2052C3192B31C9230F4D12FD5767EC1700775C478E263220E40C65EEB36FAA1C18B530B6C5ABF6B11397E694763C8C781CBB3C6C767F24287BCA224370C9D5E9066609078BB4B7BAFA46328F9C203D7AB0419B9177295DA08B802D93A97DB3F1FE964BA7B78F4A5404657603E5A7B95971A1DC2A1B00B54E7FB1C180CCE49917278C961FB61009200070ED90BB752A55661B011E35382A01C57793AFD864AD8D3ABCD752F78168EF12238CC520A6845CE3CA5771D207B3A2A594E687867783E80B057423118753BBCD3926B6640321938A40E793BADA8476D64858296348BD7038DA24E65D4B6FC4C907ED69E0159C76AB07F9111035B6187A41024AB26966F7B64673860EC9AAFEF29C0D7DC8A4DE31344C41D84D07B3A6525A5A884BCA8870E758E4A6C02150505A7587EB6288C80910C0E999C0309A34DC7C47053AA1F01848BC15059E65307207F4C4974DE0ACE30C2A3021B5927B1B0787002AD0A4CABCCC2D30865DB9150F657025658C3A14CC7D088BB1247CB2097A521FC46E75B158C55509D0A6DC8FA6B53016F6C28939C889943075E1B26646E3BAE5B0A52D3553E3D24AD62396AD18CB53C896E8FFC148F57CE11A39677052C134B8D0BA186B6F9BB94A2C96D06627ABC7E29660B9DF9C9B9E1786B39B9148A982B456F4F426368845D0377C043036252A4BA1B61487958A9E05CB272072CBD0927B4667BA7E4837E4B7405B9CD3EE43F7575B8209BCF122622E7E99B414C3AA32C9DEF1B200444B82BCBBA8EBC079FB42E00A830D921C45387BE4F86BC05D64BCE8B948BC61D6FB0CBC9552591F99622C50C498376A106414A6109354184EDAB57D8BC27F6761E18E12A12335BE89944B737C3B3BCC3227148A5A60EA32B83C8C5692B35BD68D72DF7CC264B519C52437B357899B3409239C02251F83460F6AF9B7C73AED1B4EDEB322F794555A0CF5AFC233260A10C6529B5AC55582B153461C25401027748B39260015E00184D553F45372613631E2ED0B4633A0CC52B6F62454DA414A49B17BC88225EC9EAAFBD121494652C925BCE19CB53E3216040B879F409049B780098A189668A7C46751C714B38A6E59EE4D7620AA3B3DA3BB3FF3C4C04E45F3ABB3CEC417CD80908EA701D90A21E22210CB7188D739CB13DFA03E35327C1912453C5709C1B6FEE2CBC5F8714E1A43AEA7422D6E63F247C8065A0CDFB782BDD2CAA781B4B8B76BB7885C7B446BB40A52BB2C8112F3C5DDD077FAC625D54DC26B0A3B52109C8C80137302738B511377376A74E07C1D2C3108180C6ACE97E61F6A343D6C1CFB76D5B18B756179A66D51483B904E5B88FFD700971A37E913829A2F366B8386BC342BB49411890C958040B7E66CCC5FF9C55CBC85209784282F865C7132FD0F82A4B016A9CBB64B03CCDA3F64FE6E760DFA764A7D2A17027B18C8909FDE5C9686608670871CD95B2B1DCCD99C34B22034147F1471BB38D3043ACA4C727BD971C4676BB7DBB987E015084F16E05686994A776704A161C7407007623964359E0F77214B03898C51B4EA6474A7B0711E505F0060C220A80A9C591481643A9796C2AD651ED7783F38633C308B33490867DF28DFD054AE9C49CFF78B508234E17538EF58172E92642F43A5D421B2929210BBEA122853918EFBB91A0446405519D43DA86031A6DBB3A9E24BCBE4E686EE9EAAD71A42A4D27907E89326FB811A5663D18002F7294CED499A29EBA328F5460F80512D42AB48B726E798778A2C2692BCBA2A0052F9A56C1A0631741A6BC846A8C0BC309B7E9709331AFCAFB47F0E805E29B98A2A3C2D9D49AFEA806FB1CCD5E24105FCA6C3CCC1551B613927AB4CEE284CB53968DB3767C2154295CC479F06A4560385BB16B29273386320FABC520E5B36FE4D85814079E7E835D7935172DC893FF9277B177B7D8A9ABF9C66DC6A52C25852639540220FBB4E2E576440351F281712910CBF6A02F5A1B786B006DCE399794191D9F6BAFCEF1545E3A960CD204868463FF2B8A20944211408CA82AC182D05AB4C32D6A556FC36381BF163F69377E0028C8E8C70F0AD7A526CA274F9B65BD3ABA9164BF34F74B47C56966E4BF8E1B889E54CA29A037E67C28FE2A2B6710554D3813B55131B2B75DE0A419AB8888151876CD7B476CCA91FD83480D150CB3206F6A40B95E3412F9E7288EF051D3C99ED40B9E29A0A65CA1739F88122C72CB15950EB99302CCF18EDD17446F165930396F85E84E52E221B5668431B55CFA08D048A1386D450938D48CC2A94B38EC1F3F6AB2FA5565BF3B4BF1638AEB6A2440E5CD840C564183331E1AB7CB114A409CAB6D88C84C4545396360B1FB37B36CC405451D7FBA970FA7433B879EA8CA0BFF7C4BB0B98B255412AF695511A9CE168BC4FD50C27E121CA0221DE918778B941019D32020EC2F306C7BDAB38F9518BD51FC374F84B5E4B700F05785AC39CF9CBC0DA4C52150C9485D4A6BB4CC3C54A21DF63C2044B97C8709A5DAA82933197F1EA24A994370A2C9495E0B6660A10AA62BAB0BDC4DA284CC39B7C0E0464798A7365EA106793815B7B2BF02EA70C5B506CA343C38386BEFB69D6E31A1678BBD497BB1E64075B4158205280802234AE612A588CC8B2EC24AF74C5EAC085EEEDB13B827340AE29F348A9E31A20244D20A4B2B41F46A6A4C8293A2B8C5E3D3C49CA1AC0724969360BDD8A0822DF35D16A7CA48F90BB2C7972CD70B8DFB6CF5F1453F69266B04C2C32987DE0565BDA632F4B257CAC7CE7953A70B29681BC4602F3ACC3CFB3BFB63A135A758E461647E344A454AB3A1A72D234268D8870ADB84AAC250127E2AA593E79828848C112C26592A89C4DA358BF20571144C3C715449B8AE46606A51E150F4CA3593BC9ECC8A0382532610328253F488E733965C1742A36626524809C548397A922E79B82FC35B1D0EE41975CBCFF6F38BAAE28F0B452BBF792CD9023EB893767FD36E855774F1600399DA75889B03A95A21F32C0162540CBAB08B82B8346CA43B3E07B44F8C5AE6255CC379B3FE631A434349533B85C164B44D7805DE49442D113D238A9856E1897F0352130369387C3F2BE95435885A117072EC762656BCAB6EC1BE33C80C7835C4670B1D142767E23BC0AE6247529A3C8F348BA37ABE3507AC93C939560A445CE696881248F2A6C0B19B3900DB2E803A4BE0D4231F41A65702502241BBC724AFA441991A0A46DE7C62F6A220E457B2D876C44BB0A5F3116868E2B6D63A44A5C55F53F83E10E534606B76108CA4BF12A1B97A61A28B1BD184A9C2057655D460B65C407CB93272F44509C432DAD7B03A7393CFC160861CAAD4E9C81B9CB1085539D8A2C1855421346ACA46F46C3D12696237B237398B4015467CE7A6030A9666736717E6A740152BC2939998724018525D74724C5BD35B2D1887F9D2C6733A1173129DF3AB625E48C6664207C4D4931AF4130CC1CA0EF9160898890B49674F92A9464837F5597A7CA26BA854A4E0FB756572766DE2B9FAB3820818CBC600AEEF588B59B416F9568E19FC4B03902CC13547DC18197CB1AB23190424EC80633A5D58D861D9BCA5450102E52247A74594936153723C0A561B64F4E75D90E805DD7087FFD3420B4A0CC54CBB32944693A393511CA69E2C2B7A3A5B58784F2E999840BBA8C2C994852151569CA0949616DAF73328B220DBEB7D43794B1373CA3E8AC69178653E889D67E8546D35A40478B9C6410D9376CB28D6080AD41FE834B970403B8687C29011C6C8E24137F22FB7192443190BC2B095594C85C55389D4D49722124F07067FD9F34319565392799C27550934127538524E1655A03F697313130CB0F6799DE565A5DB8B7C23C7A8F53AA7714C7094A86E67894A75A489E6C414B67C52030F40EC5C599969D8E00B7C03492C8CA6DFBA8D641307921712C2840591FA9A847B39794BC22747830FD956C0C17B5C974FDCE96F565355B500A70C3D86EA32C9290514B1373184AAEDB75D90976FE6765F4747BFC946F1249DBFD72887FAACDB1B900CFCB5B3955EF374D86072D38CE29DD9E906AFD6E326D7035CD963C74AB142936A91EABD3074C583654945F9F5FDC29F754B06EBF2", + ), + ( + "145BE4E69C65F82CB31290C14E279EB77246AA0DEEB4729E7B21DB0ABC4A15B5", + "E6748703B1B4742E044D723FADA7E44091113AF9D0FE599C5E969A1194FC7925", + "F2F223B0E4789C392E69386897E95825B0B6E38C821F806C1890CBDF500FE1A5B8AA139B4189A7D720360852B514CAA73BE8CAEE5C8828027F945768595280A4B869CF154E2507D09855B9D3897FF1587C30162DBB49C443025912D3A950A4711B06BC64EA2216D81C7862066061A33F776A0F9C69B52C8EE328B726A6BA415376D5AC8E36503E51067F19A53D601B0ECEC648D1A256DD452472DA94C6656DD68CCE745C4CC04826887340AF46053592CCC701630AE172B522725F546D4E244A3421079456AC75745C4AD289557249E9F29A5194677AF159E824AF31687D09876277A3643DA93B6103A7FAE9644CA127B6FA9BC80A248575253F4C813F900856A4445741119E729E29466866BC14E26527933248B286AA4D872EBD2CBFCF1A67A5D90240CCA09255C811D2B97B3987C4A472D552AEAC6616E122427C365D435501EC66C451826ED7719D8888241DB93A7FC82FC43908F70151243A30B9308C600A61F6B3248657838DD74B07255219D32903E8A850B997F8A329F56026FA2C0C43E322D4A5A5727100A89542E8D57DDEC2B02029306FA108B5481570D98ED9D29FB3025F5160A6F83C8FCCF509890AB6A3B6403920894DFC9F7B50CF50883448A41DE86C381DCCA7F0F431CD12494D515938358A39216549C804D3F91C3236019AA805B4FAB7928482B27125DEE89053201ECDF0B907786CCFB11B9E24890F099E4711950D00BC126680A7F45CDDF4115310A42B813DE03C6B77743BD66473C587C6FAC769B293645F601150D42A9C741F740071146A942AEA330D78132A6813BDA844ACD73A2CA311B1E84EE3B093E171382E7B353AA542D3BB3B8222B256D7AD3A21042B68BFA2668867351190E4CFE60C22DDF8BD922A77933CB4F3A5C012A72CDD42C686815F876407ADAB033BC742CB811CD82C8098847EA44853AE71494B63CBDEA66A8A08A55F75AE873A0D61F487A94987BE25C0E2455593F01ACBE322DF69532F5308C1076FB228610D6AC8CFE45F390957B427A4E95A6FF5A494A2FA2A0F92A05EBA713127CD47A53F4A835EDEEB702412C8EBF1BC18C2009C7A8BD8995D7E804F35964E63AC2EA51922E1564868A95D2B99AC887C8DF985092E0ABE26B4A468C01C3653B34A097F0489142245836F2CB504AA7E0A6536B46A8569C3366FC9460D838CE40AB4322006E841445754A00957667156633DFA5F3FB6A9B6D28337A85B9C34B41E37A0DA8956D3640E97337354841205C98316242A22D81F979063FE1CB02688AB4383577F022B55581444EA8137696E44B6AAA4D079BF6381EE55253CE1023BEC17A7325B04B11D8AA55F6F1429E661961C92B88A104322988624DB6155E836979B3162377D91B10C58D2265A32165B7509990013CDE36136F7B86299C6A0F06C037C35E54164580961218787B8D46C56C84F0C6ABA065C6E4B6078DF35BD8604B9097668BC4460EFAC7790D50FF605AC3AD108EC045D76962C4E3C6E59C292F89A5CD0832BDCE59BCE898391BC71D5353BF89B366F28BA1C4934AA1041CEE3325389A10B4A5E558C3F2BE8988A6C60F9977661D39F38F663B1AA9C26479BFEA67EC17048E3D3C236522BE6535C1870CE0BB721249205546BC00FD2B86FD852CE0647D6882393CB11EF22A5CEBA1CD5C2A7F0B0174DC52D66E8CF1C567C89E128C2B29EEADBC7B8E2704E80398DDB2B89D1CB6B6570C15826F5C855F9D24FF25937233C84EF49249A9A739C540592C618D63CBB83E2BCA108ADA6AABD3E93AF538B5C6EC29FDC146C0950817CC80C2258AA4BAC0A984B621305C082903643851D623C8B8D6A90FD269B65524B8396A0EE192CBB4202634209223241DEC3BC66054446F12AB6DBC9BF27BE16EB993E0552BCF1C4CC8C5AE4449D49E508DAE71FE13A7876352C58230C5EB2861DC237DC799CFD3588E2A6AD06D1A665881FB7620C2AB50BEC766ABD441790E23C28D75FFF43CD88AA405D5B607E551E0B182D42136A16508EDC90BE9AD159262436F406803AE580C4C0B4552AC6267712E30A39F66396DCABB4E864CBAC9AA9F5226F8B421842811876F39AC648C73D282A45C4A99BC90F2E52563881C22BC3858B928382D5416056B969191316A7356A07B46B146D2DB360BBD89C63399585A7CA72279938A2B22FC8CC7F6E7F3A757BC90E65D8A8F8F49202F696137573B13BA0088FFC801833FBCA9E", + "821865EAEABD3E684D94942229295D93B09DA166B59342C25287246B0102075A3C69476570239F9DFCB6A8F16F06F45A9BC2A225A47FDAD6287115CA8524C8402C04FDF3AE9386C31B7707CE863F068872182480EA3243AA654968D49C1FE52D5F8CC0128C1757B9CBF816420FEABD17233AEF955B4C77A6777A16BA35745EC6587C37376F042ED3DA49CDE7BA8A31960CAC4266D51DE764CB694880B0E5A2EF0789791962E67CAF1C152E8ED05A63B0B77F56CA99490D321785C540A2C814B77291165574460D908F6826BCCAD3B33633A2693A2302E34B5BC442ED8744FEA370054A0846B9B09AD111F38A3E46015FF431064F586B8C43939979437F59AC401C6C6D3B4E14A9675D54CD1DD2B2437345FF54AC616CB10501656810BBE085AB75B0CC24124BBAF1B3C8504C46A44A8AB4215CC22DF5644294F3A2D3135AF558191080A49AE7CDD711C503FC6DFC2771D2FBBED7A6B5435C07AE290677656E9524A26C9455B21704C653A395D34521E830E7514D7B77612AD2CB8EC6A7AB6889F471740D4B943D3048412655D2FA32451742CA8B4312241FAF151648D65028D5C08E717ECE286B7157BF05B8A1956C52BA215D40273238999DD8B7C9171293C1D82095ABB00B09C99ABB2A8B402181B23F8EC668BFB2C33A656A767BB77D8316517208CB44426A94B642154E7A5A73E62AB75B000DD479C13FC7A3CD5322F1BA2D970B05FCE795E41BC5A8135A37A85E01E777F452BD7E0284D07C5D41D34D4698C510F674E3A9AE59833960F02D39B9B5E4EA2B73378157783C8556B5DC6215087C4A73FB2ED5A62FDDB23006E05FF14B87E6CC4D63F379E7358641B69D9FE48CA01AB2C210B99C436D89D84A2DBA4CF43122B9912478259A4F5B7B1E6CAC6934975A82588071C079689686BAA105E3B1CC436A7268B211BBA7833B2BF4C85E99D5CC65A40A34949138DC14DB83418A8A68AC74C993224028159B386055EA7597905995A52989D865BD54C358A19519F07051BCF97BDFDA2A5FD50CEE13CE6D2484CB297D3C2238772B85354B906E3AC9502B7B8DF4C32FAB4DC5553228D71EC8FB86089243753A7D7EF29398B92744ACA4B25819D4CC1FAF4BA5C09416FBE8C6893175ACC3C242D136F85636FFA1A2147216F2D100807877DB7628843336BE3176FBF0A4B73AB9CFB65284C38F1C883AA251B72CD46E7FE6CD117891D9294D8000388FBA12DA851A945C497AA2A797F25103DB1B68996355071A06C9AB0E122F25748FCE59A22F68AC8EB4635195BDBF07120AE27C9D857B4111C9DF825B1513CCB409CD02A37E6393025521492CB9707BD059E7C29672449478B17DDF57B8E4913F5DC00D0955980F7708650BC22E3588FE6A0F2CF016BD73BB86C7CE7F39381F20A4884A3FC1DC5397A4BB30199B47F73908D58ED40491DDEA36CCA15AC9CA337D5C05EAEB4647A9B88635451CA58C99C67DC5A5168E0C44751857C3277BE9DAAB366755DBD1137C4CC8CF97A0E5E99816D96FF1984BEDEC44867B1570C50772BB704BF54AE3F5150BD89B7DDA095FB939AAD0C3FA1CBCF25B93B40B1EBD26B8074871B2210B48F28FE4C384AFDC6ABC531150E81516376406600C181C4D200C5B7D23003DB65FDB401C8094C110C53E3998156294514F990249269C5D9A1077B2C180A97F0D833FE085A5FE48102EC59B52929E249A82518C2FA6EB5D45941DB3971CCDBCB60D69097E12AB226965FE33ADFD407102C052A4478325271F3E28C450B20F51EB1DD70358F8F18C260340C2080CA568A10060A2A4903796A39E4FCA1084D1859C66722A41AE14456DCE2BC753D29732F29984B85E49C60A1C595794FCC6CD593E84F7B1ECB40426836ECC9394043900C286131A785B5666BA6C2B76764C4C6BEBA7F5C12690FC844AD4559F0222261A4F3961753C7A1EDB1619ECB73FC088180ABA68CF623D09E5CB1B144F0F9BBB2DF9C4CCC430B8383480500959392EAA1C99DD16C4774650410BBD9328BC306746432B50C9A4B2B0A57E1D0B5BA92963DC055F5009592A722DA9504212EC4533D463C3F1850E41A4F2D6092D87C1434B7BD4416A109621BC44129BBC0F3D972882FB50F8DA3585D7C0920A1D3C8B5BC3B3BD0F948C2D550765DA6A98579E7FBC29FAFABF0D20872DCA1BF2F223B0E4789C392E69386897E95825B0B6E38C821F806C1890CBDF500FE1A5B8AA139B4189A7D720360852B514CAA73BE8CAEE5C8828027F945768595280A4B869CF154E2507D09855B9D3897FF1587C30162DBB49C443025912D3A950A4711B06BC64EA2216D81C7862066061A33F776A0F9C69B52C8EE328B726A6BA415376D5AC8E36503E51067F19A53D601B0ECEC648D1A256DD452472DA94C6656DD68CCE745C4CC04826887340AF46053592CCC701630AE172B522725F546D4E244A3421079456AC75745C4AD289557249E9F29A5194677AF159E824AF31687D09876277A3643DA93B6103A7FAE9644CA127B6FA9BC80A248575253F4C813F900856A4445741119E729E29466866BC14E26527933248B286AA4D872EBD2CBFCF1A67A5D90240CCA09255C811D2B97B3987C4A472D552AEAC6616E122427C365D435501EC66C451826ED7719D8888241DB93A7FC82FC43908F70151243A30B9308C600A61F6B3248657838DD74B07255219D32903E8A850B997F8A329F56026FA2C0C43E322D4A5A5727100A89542E8D57DDEC2B02029306FA108B5481570D98ED9D29FB3025F5160A6F83C8FCCF509890AB6A3B6403920894DFC9F7B50CF50883448A41DE86C381DCCA7F0F431CD12494D515938358A39216549C804D3F91C3236019AA805B4FAB7928482B27125DEE89053201ECDF0B907786CCFB11B9E24890F099E4711950D00BC126680A7F45CDDF4115310A42B813DE03C6B77743BD66473C587C6FAC769B293645F601150D42A9C741F740071146A942AEA330D78132A6813BDA844ACD73A2CA311B1E84EE3B093E171382E7B353AA542D3BB3B8222B256D7AD3A21042B68BFA2668867351190E4CFE60C22DDF8BD922A77933CB4F3A5C012A72CDD42C686815F876407ADAB033BC742CB811CD82C8098847EA44853AE71494B63CBDEA66A8A08A55F75AE873A0D61F487A94987BE25C0E2455593F01ACBE322DF69532F5308C1076FB228610D6AC8CFE45F390957B427A4E95A6FF5A494A2FA2A0F92A05EBA713127CD47A53F4A835EDEEB702412C8EBF1BC18C2009C7A8BD8995D7E804F35964E63AC2EA51922E1564868A95D2B99AC887C8DF985092E0ABE26B4A468C01C3653B34A097F0489142245836F2CB504AA7E0A6536B46A8569C3366FC9460D838CE40AB4322006E841445754A00957667156633DFA5F3FB6A9B6D28337A85B9C34B41E37A0DA8956D3640E97337354841205C98316242A22D81F979063FE1CB02688AB4383577F022B55581444EA8137696E44B6AAA4D079BF6381EE55253CE1023BEC17A7325B04B11D8AA55F6F1429E661961C92B88A104322988624DB6155E836979B3162377D91B10C58D2265A32165B7509990013CDE36136F7B86299C6A0F06C037C35E54164580961218787B8D46C56C84F0C6ABA065C6E4B6078DF35BD8604B9097668BC4460EFAC7790D50FF605AC3AD108EC045D76962C4E3C6E59C292F89A5CD0832BDCE59BCE898391BC71D5353BF89B366F28BA1C4934AA1041CEE3325389A10B4A5E558C3F2BE8988A6C60F9977661D39F38F663B1AA9C26479BFEA67EC17048E3D3C236522BE6535C1870CE0BB721249205546BC00FD2B86FD852CE0647D6882393CB11EF22A5CEBA1CD5C2A7F0B0174DC52D66E8CF1C567C89E128C2B29EEADBC7B8E2704E80398DDB2B89D1CB6B6570C15826F5C855F9D24FF25937233C84EF49249A9A739C540592C618D63CBB83E2BCA108ADA6AABD3E93AF538B5C6EC29FDC146C0950817CC80C2258AA4BAC0A984B621305C082903643851D623C8B8D6A90FD269B65524B8396A0EE192CBB4202634209223241DEC3BC66054446F12AB6DBC9BF27BE16EB993E0552BCF1C4CC8C5AE4449D49E508DAE71FE13A7876352C58230C5EB2861DC237DC799CFD3588E2A6AD06D1A665881FB7620C2AB50BEC766ABD441790E23C28D75FFF43CD88AA405D5B607E551E0B182D42136A16508EDC90BE9AD159262436F406803AE580C4C0B4552AC6267712E30A39F66396DCABB4E864CBAC9AA9F5226F8B421842811876F39AC648C73D282A45C4A99BC90F2E52563881C22BC3858B928382D5416056B969191316A7356A07B46B146D2DB360BBD89C63399585A7CA72279938A2B22FC8CC7F6E7F3A757BC90E65D8A8F8F49202F696137573B13BA0088FFC801833FBCA9E105F2F6732DCA0DEF151216C79A5370A63D0C239D8FC99819CE1FE5A4984F08DE6748703B1B4742E044D723FADA7E44091113AF9D0FE599C5E969A1194FC7925", + ), + ( + "FE8C4F8EEF555AA563EB748B5F55D72CF68AB6C81E9D541A8651ED7EF2EF1352", + "BEABA3A6A443866994CD9C1C2B70005D87AFF90F4D00A7A232F3D175BFEF72FF", + "D9B50B583278A2EA947B027BF85885CA3398471A81CD153CF9675481EC3D657A623852C8F72A02EB3A950F493471334B871915022B1510506418849C6E52529335B00969B63C6A8F1D8727E0FB89D7E61D6D5C4765B6A3593C3948AA2B2C2B909E1CC766A879EA278A921C2BC876B572F3435CC39CD541C390109CACD51946254CE6E62A3698AA5AE1C01DD220777C9DB8D89F30258A856332C4F540C1CB4CA67B6C3CE2782088B7DD072BDBB1102F403BC534C7AC2A1628261B0EB9A19452118E791220680AF79189002A9E9FA7A54AE38FACCA03E870C48C8B9A58F4060AB60322509E568371A9B3A959E5C55E5B99A0FA9ACB47C422336431D08915046644148ED3F87F873309E2FC684B5AA6BA627E883093D90351CC737E26364AD3251F5E320FFC905F2134825B7C7AB87651A656A9CD49ABEE5772745B798E08B00DC5C41B3C535890433DEB7316167C5873724B7B730923A92C87316EEC59A4192441E1A7EA88762D797D73F6193309B47AA0589707C342450A79550C67876D952A3DC6A65C4A7A3820116BD5F70B8081AA5EF8CFEA09CD4B156808582CD7D078FA2B13CFD5771AC18B76022AB72364111235841343497468F2AB3A134016041437D02237C997095FF0A016E6C48566241ED873DF4C47A05904A8852E56CC82000C5DF3F2769719761D3165942C3D7D3B39FF151C69793B33F699791A4691A934E98386B330340A396C95947DB2017DD2862321D53B88014066697614865F75512E08C63F52002FF96999D5CAAA31128EF89270DAB59A1E909991220C2E9C66C7563A7B572544871F29170D71E29B7A1433AE239F76B21FEB6B9D49E863FFD8463EABBAE5425C0E527A36FA70D4464324AB0CA35A3AEE68369491A434C3243335909BAA6B689B228D9540733B6DCA05015C927ADEBBA90B17014D5268DDF8A06D9AA979FA0917CA4A4C871C4CE6A3908AB158664AF5B5AB6FAA95E0E593BF55A65B710824FB64D7B7A421431A75E78EB7D61BC899628994935D4778CFDA77E9168307B2B6CB0164085634D94CBCA4C8B53E793A00DD8D2FF6A800C55E028012BD6912C48B13432C485B8AA0EE61555D5699AC5AB6944120D1EA030FF2CF785573B0E84FB772B9F3E473CCD47921B6111AF6A690F088C58C7267978BC5757CA9E542A441983BA113654A8448D7025AF4BBBA0306D56403A12568FCC8B892D9AAD06B809D5681B949A19BFBA4C55A26E417B164309E1E48AE245A81734C141D639D89677A29E231B5255072A37C90925AF0D36EB39737396C1A02B513EB98BC110870CB9565F2898690ECB0DB7807E3E858C57BB642CCC5EF0C8F00D733B3581382994E0C2C74A8B140D6EBC02405835986C2D913077B6C7BFFCB230BA6181D765620B0AEC43B8D15F64F51ABC92DBA2E5CD53021537A6BC06091D2C1E47B2E049A1207617D9A6703196623A0CA85054373D4283EAEA0A867B308F362BA8B26652FD346B4386685E6169D0946233BB186646D3B322EB726142A4C6DACD9AC8E866EB11708EED22ABCD86E2008814B96C976FB67A9399CF837529F18461ED9047F073B0DA1C0DBC3BDE180BD4AB75A81288B1D64474F4797AAF9170C674188061872E4B1BEB2CE82FC968EC9442DE65B59972FE55A56C81CB6B6584F86E566F5202CFD6400579C8242DA9CCECA68972563F155A9CAA2790219846ABA47000927B4231FD4E1131EA23A5B595B4760013020BE7DF38725CC502C2B0C33CC2593B44543F06829AA12B8CB6836068FB0E697FAD760F0251318934A1355AB75497C88D2BBC0F9678E4CA6E930CB7D445B530C7061488999C530B5325CD3F16824C034274B77320CC4191663C1E58A2A321952FB6A4350624AF068B9D95FF58A5E247887BBBB275D65C417357A0C29805450B2F2B859BCB612F6D3401FC2C4A93091C582207AA8512551AA33E4CB896AC42443A33DE84919F131575A3EA1607EB27B6E570A889FD6C38029BDDB6A0E9C77690C9855DFDB483CE35855A484BA791B151759D272925505765F546A67490859E28EA625A252C1C7F80686042B1BE9931E4C7A33166729481644E546BC45293786C10524DBB7DE822E51A44A64C51EACF0A743CA8566310BDFE61902FB7CE2A46950055E1869BEB391695E48259E11B30D34109D63C1D41FE491A35244C5AEA207E40081576B15C73098F375C0A5ECBDFF849EC24D79", + "1BF4705C214F0912CC183A44471542F69602C5B544EBD526E472CDF6A04F9F856E4931BA4D818753D3ABDD92C4AB72B03A43C36A969B2FE92938D27BF4452323B973797260FD93745CE95237B4C4853BCFB49B70B25A7E4F54B72B49CF313889DB8C95A1512AC8F516AC22AB3CC59B2E9BA59508CE1A1526846C22AAE6A63FF89955CB48EE5063711559CDC36C64131DD0A97F16854FE4550FB115A57454CD8D22B8E7634E65D421DF6603E3244A0E082247B4C25DA971E0C5193D77B11A661C0E150A7A614196911A95D89C0E5919FB5AA2B3A3CC5C068730818CAE910A1650060026B3B4103ABDF6B1788656FAE87AA5DB97BB6B6F87986617C1986E06A0F055BFBD73923396C334E6AEDC2C018F5A8EC282C980B95E73CB33DB85556894A5855207780816912B1FE35BCF02754671778B00CD9F03DC21439A83EEBC4875EB5290A41BB6075934C2735C054A16E83E77A4844324A5A7E92E1D681D4396B0F600CF7ED694FE56635F51193DEC76B4BA4887F53A107534EE318137C38F02801A15848F1AD79CD1E75E889C2BBCF6256FE239F04B34714053F3D25D5927B42798BEBC6430D541C8E7618160B133E1D42A4E069B86B47469771F0EF54B22550B5868479F07275D297EC233090CC133ED388AE239BD990A1899E778BAC687361B3C275C1ABC4C1046900D80EB5B94C1868A2870BCD12E36D3AF37150E6FE3B2718419D5EA1F4D81CFD152740B51CE77B8125A2229A6204BEFC44C14CB193F438F995ABA01931095433E24250802113A63E15F52B370D78731E7B1551B33BE53E80E780B499E91452049BE2EC33016F268E9A525A8E4253BB592FEF952954B7794F4B3AEC4143235BFBAD30BC22B78CE5BB39CA840A2E5B49DBB355D700314D8893D71A48EB3AF3562944586372677654A948151A9CC8C88086755C2CC175637068545323BFE2388B08899FC2810433231FF98CCF7D77FD00919B0B63561924267327C8E0C614F0476A99727FE51BF6FE9CEAAEB9D8DF13CB50C70A5C0AEE61CB77155B9E6B010AA34C84B6898DA28C5F8A2B0B7B63247A245E1FC1551A529B7B498C35464A4305ADBCB79688525EF0216963409CBC4CB5C37313E5CB1188401259439A1F15A1BEA223C4641340BC408E043D48A80B0B5B6BF180093230960AC8EAD883283120422D58E102242B548A65AD537F3D008EB0397818A2776B3A2E6AC9D363372D899980418AB03B3C6BFC40AD6222CBFD39C22B62222A3B5A992A25AB828C233C6ADF9412B4A7EEAB68B0772635A1977071A831B462B40D84CF87792EE9B0A81D7BC6B2205F09137D0232F3D2AB140C5C86510B07F506A9FB6200F795027866144C419453A3BF2F3CFD2737748AAADCBD64E5CF85CFA202EB808B0AFE73BFF626B179C86524068BC1C799C41558BEC0CA2E4CEB3DBA3F93A9D6D137DEC573FBC127576D6C0E569B4451BCE7D958CC32750625B1185765666774602938EC1F08E0A7A0EC91BCF452165002D422FA5A187F0345F1A43589B333DC70E0860C24C1779A5EAC9B9F863CFB2ADC976A8FFF0C8AC37CFE1370E1EE235D40503E8A52C4954836EEA809AD7CD4E2043011586E6A8BF3E76551D576D60F741AB6C5D3DB2B99527775CBAA2CCE5A2B2570BC3F759565409D19A9A604712AE80448DA8A7DB80413A177137915F4AE413EA050AF8FB2FDB4B6C99B8A2F3401C84409C7ED39BB6A45B318852E3F245F8596177C369BCC22E95F828FBB7796B127E1D6B314F30291B57212661B3E23CC349FBC05984C9F38C7A1205C8C5E86E7BC3577AC59ADD11B4ECB038FF74BB9853B1825B63FA842CB1C251C25BB41974C8C753A25017219B0816729C09905365565939DC677AE0788950789D66C7766A035000688AD401435E762ACA4527BD67965C44966130B7254B8AAC7059710B3B9397860EA243AC567869424EA8D5CA90A240E7D53ACC8693EF452884223BD72A61D6085FF2D6209E39163317949834579A833207E65ACF2458B1F71715379CBD828F0FD26864663D0268A6EE6723E9B7B1AFF4A4B6A61DA07887626B8F35F0342097C25CA5B8BCCA2A7760871CD8C6DCB346D006A489094A78E368FFF9450C6A099E9CADFD0B861077C80FA8BF6C7C191CA2178532B7D92347F77225FAB1669675B024E586D9B50B583278A2EA947B027BF85885CA3398471A81CD153CF9675481EC3D657A623852C8F72A02EB3A950F493471334B871915022B1510506418849C6E52529335B00969B63C6A8F1D8727E0FB89D7E61D6D5C4765B6A3593C3948AA2B2C2B909E1CC766A879EA278A921C2BC876B572F3435CC39CD541C390109CACD51946254CE6E62A3698AA5AE1C01DD220777C9DB8D89F30258A856332C4F540C1CB4CA67B6C3CE2782088B7DD072BDBB1102F403BC534C7AC2A1628261B0EB9A19452118E791220680AF79189002A9E9FA7A54AE38FACCA03E870C48C8B9A58F4060AB60322509E568371A9B3A959E5C55E5B99A0FA9ACB47C422336431D08915046644148ED3F87F873309E2FC684B5AA6BA627E883093D90351CC737E26364AD3251F5E320FFC905F2134825B7C7AB87651A656A9CD49ABEE5772745B798E08B00DC5C41B3C535890433DEB7316167C5873724B7B730923A92C87316EEC59A4192441E1A7EA88762D797D73F6193309B47AA0589707C342450A79550C67876D952A3DC6A65C4A7A3820116BD5F70B8081AA5EF8CFEA09CD4B156808582CD7D078FA2B13CFD5771AC18B76022AB72364111235841343497468F2AB3A134016041437D02237C997095FF0A016E6C48566241ED873DF4C47A05904A8852E56CC82000C5DF3F2769719761D3165942C3D7D3B39FF151C69793B33F699791A4691A934E98386B330340A396C95947DB2017DD2862321D53B88014066697614865F75512E08C63F52002FF96999D5CAAA31128EF89270DAB59A1E909991220C2E9C66C7563A7B572544871F29170D71E29B7A1433AE239F76B21FEB6B9D49E863FFD8463EABBAE5425C0E527A36FA70D4464324AB0CA35A3AEE68369491A434C3243335909BAA6B689B228D9540733B6DCA05015C927ADEBBA90B17014D5268DDF8A06D9AA979FA0917CA4A4C871C4CE6A3908AB158664AF5B5AB6FAA95E0E593BF55A65B710824FB64D7B7A421431A75E78EB7D61BC899628994935D4778CFDA77E9168307B2B6CB0164085634D94CBCA4C8B53E793A00DD8D2FF6A800C55E028012BD6912C48B13432C485B8AA0EE61555D5699AC5AB6944120D1EA030FF2CF785573B0E84FB772B9F3E473CCD47921B6111AF6A690F088C58C7267978BC5757CA9E542A441983BA113654A8448D7025AF4BBBA0306D56403A12568FCC8B892D9AAD06B809D5681B949A19BFBA4C55A26E417B164309E1E48AE245A81734C141D639D89677A29E231B5255072A37C90925AF0D36EB39737396C1A02B513EB98BC110870CB9565F2898690ECB0DB7807E3E858C57BB642CCC5EF0C8F00D733B3581382994E0C2C74A8B140D6EBC02405835986C2D913077B6C7BFFCB230BA6181D765620B0AEC43B8D15F64F51ABC92DBA2E5CD53021537A6BC06091D2C1E47B2E049A1207617D9A6703196623A0CA85054373D4283EAEA0A867B308F362BA8B26652FD346B4386685E6169D0946233BB186646D3B322EB726142A4C6DACD9AC8E866EB11708EED22ABCD86E2008814B96C976FB67A9399CF837529F18461ED9047F073B0DA1C0DBC3BDE180BD4AB75A81288B1D64474F4797AAF9170C674188061872E4B1BEB2CE82FC968EC9442DE65B59972FE55A56C81CB6B6584F86E566F5202CFD6400579C8242DA9CCECA68972563F155A9CAA2790219846ABA47000927B4231FD4E1131EA23A5B595B4760013020BE7DF38725CC502C2B0C33CC2593B44543F06829AA12B8CB6836068FB0E697FAD760F0251318934A1355AB75497C88D2BBC0F9678E4CA6E930CB7D445B530C7061488999C530B5325CD3F16824C034274B77320CC4191663C1E58A2A321952FB6A4350624AF068B9D95FF58A5E247887BBBB275D65C417357A0C29805450B2F2B859BCB612F6D3401FC2C4A93091C582207AA8512551AA33E4CB896AC42443A33DE84919F131575A3EA1607EB27B6E570A889FD6C38029BDDB6A0E9C77690C9855DFDB483CE35855A484BA791B151759D272925505765F546A67490859E28EA625A252C1C7F80686042B1BE9931E4C7A33166729481644E546BC45293786C10524DBB7DE822E51A44A64C51EACF0A743CA8566310BDFE61902FB7CE2A46950055E1869BEB391695E48259E11B30D34109D63C1D41FE491A35244C5AEA207E40081576B15C73098F375C0A5ECBDFF849EC24D79666D7CA68B92E449435BA817F8C3FA9AB2E0EABA14809C7B4484DF747A446D36BEABA3A6A443866994CD9C1C2B70005D87AFF90F4D00A7A232F3D175BFEF72FF", + ), ]; for (d, z, ek, dk) in cases { let d: [u8; 32] = unhex(d).try_into().unwrap(); @@ -220,11 +431,36 @@ fn kat_mlkem1024_keygen() { fn kat_mlkem1024_encaps() { // (ek, m, c, k) let cases: &[(&str, &str, &str, &str)] = &[ - ("F6690E77251A47A0126F3CAE222B8873EB0EC9669AA7B08F4854AA72C0731649CE60EAA8B08198B8712158238DBE83CAEFD7641434CE1A1346F6900A53C105462B4F1E909E680476000C8E8FF43DFF9AC7F36657BF852262D383E68CA2C8764B59441523F009B5A752278491DBF89192FAB92BC873DDFC5B5FB49F9160769BD228844127FDDBA4949A1CE958B6FC1388DEE6CAD9F549287B7E0C85300D636AC1A68C1F0860AF00CE50963E3DB200F651147C4425DF62467FB4BFBD3B89DA337AA74444409BA3FA73B8CD1454F7E7C730352F894387E79C7547C077C19496322817D3D540E496922F1063235B78EA66BA23918E79115277965E67760A2FFC91530AA85575474DC344F11130FC77CEEC03ADF4937FD7FBC838C05BF88263A9903BA10718B6C43E97047902827A491ABFA8D83780191C96FC8924817FA3AB625C76132CEA34273B3D74589E91114216989C8E98C5B4EC2E5AA01805BB598CB7137E61CB6907952A51BA54C52E9E8C629305B7982779D0CC9D43E14C254277A5124E06257CF7A61EFC186118A808CE4BA0FCD31B34559D77643BE90BD017B5C90C235615B61258D6432EE6138A90A0B3DA38B6C8C20157644C010B6BBC327AC4B515E2AD5C46AD63BA0871946B6455A303448662A1CBCCBA1A462389B7EA4AAFD4A411E2A45D92146FE4508094C8E4F2747E918A2E57874C934A5C654F137695C67084A3EC50F3E7C777DC42AAC0AA9727015546CAE376C68504AA99A8AE585C2BE6E84E510124B5EA9169C356BE0239CD8C900DA93203716C3557BAB802C846C31948B7B2EB064E2DE01E9FCA837D4986E43A019BD351DCE5218FC1386A46684F2347F6EA999C07586AE66DA4A129FE3C36E1435B0715B155477000768EF7877C618579CCD958DCF74323E9C065A04F0F1B23ACE41B0FAA69DFD53EA2C62AF467B0DF64800E79C4FE72419958AB1D470250B5122A915EB00A44F4545DC83B78A0942702B71D9339115E007FAB9684FB3880B0E97221D5867D4CB0A4921FA29A81A939231F2C1CC5D329652052C3484602316B792ABCD55036E1F41FDEE702BA1216AB71918F469503EC85E9321AB8D4287BD537194560E1F140FE3900D1864F588B4AAC648D1647612837CD11EC6B730768528A96D825BD06033A4734B04B2AA6F26586A345605E59781F046738C51747564183B139E772B94194B3EB64407C462DC54C14CFC81BAB301F9091950B5A97BFF836990C0A419AB1C8F08A8D200AA3B968A19224E03B140C998A58B86221CC258C407F0DDA752A890D58425D6269B4CB03052DA31CD0ECB04B36A42D30A1EDF98570A31CEAA81EC9382F6DC012F63728F38A5983B50DF7C65491934F2CBB9CDD714284C0706FF79BB1F879B6EA423C71198BFA0AFB628469BB0E6F79C174463FDDA6BA4A78A886EC690FDA63CD93B66CB01E384395FE0281A7DCB55661B2DFAA7990CC19BEB96BCC29740F305FF3C72281A5991F3B39135B066389C1DAB7B0A2BABBEEE76BD99B394C82A28EBB275643C288E421BFFC94AB317DD5191220E9527549670C67AA4D26CE8A6AA1C8629601631B35FB9FB881C1FF03CA68C2BE4D913269274B04668305A6ACE7129654C51DC9C78EA9B07B489241A0E4C83FD5B2857A13AB4AA9D7139202AC08E2877624D07AEE9C6954DC7E0BA693F260B714B80C92F6B411D510E6276061451D99E54AD55954993B9624FAB31DEBB0FF700B10FA3D7C3952E48082F4D87DDB703E55CB65577450A375C56644501364B871766DDFE57CEFA187745CA3B6F87890C2AC65B89DDBB9AE7AF90F9BAAA353043CD382A4089C5744D11E78D71A61D01F4C345C5CAA9B385C7AF35A72545C677BCB363B129A0392033956064FC199CFF4BAAFD03A3413C65B025FC186475690AE93699DB6D0B71B285ABA96201D149968574613865CFA26CFEAE7C249FA5D5FB18480A752C368681A5969AEE20DA1F0ACCE6281852441862B1B4C01A1C0352211D49204A46701F72C52287E4C8A8FFD8668E0227329E84B1E0A8D47418A4AF44A6E80CBA470A4A04C1817156A87F4AB7055389AB3B51DEC6CD64CB8D7890B2D4BB85E4AB20AC3ACA6762B0100640F92040BE73906E68E8103A57669532120312FE50DDD47AC0B19151FD6137B16A09324948629B84FA2E7C8EC150FB8C7DB1D800C0825AC18915BE2529771B8390945E6DA65F324", "BF233CF6121D41585B4AF0EA74B35DF7ED52BB5782107A8259CD4AECC3587E61", "EFDB7F7E56302A199A7BE79C00715F670A5024C0FD357BFD446B4A9371079C5A04964F5F4D0C8FB3D12AD1F6F2007D26F698C49DA91E3B107D2332C7612DF3D7A216FE4F07C5AD8A1E9E2DE80E3ED3B79BDBA3E5388FBDA4311F79401798D193082B17692BFE3286CA442C5F7D4D63327CF40D7B0C420084C636A283E83483A469CF88E669129CF868F013F8796FD76AE474ED75AF8832BF0A3658A4EDB3FD2F37DCA44586C78728834226E5DED1732A2B3ED1C0DBF4A825818C344CFD60D4DB460E2E5B2B1931500DD22C098CF45F407D73CB04E83CA866090C244A11D08613BA174B06F7C58633BC01629BE2F41A695E8F9AD2E09C2BAB4ED34AACA0303B58F3B3E55D16F4A84B4BCDC6D7C4C20170DB0DD0FD20F4B7FCF8E6DF8C7BBB75075EEA2BA10EAFC071A83DF4B2401CADD328FB729CC5071C1C869DEACC3FCD8B60FB157B1C424491D3AB36DFE8D15CF3DADE14A109A014D157CAE91F1F7FEE9CAFAC003F2A6FA9F3E4C6960299C71BDFC168770A7D034FF52120599CA005886EE97FD4F15AF42BA209052A6A93069CDCCD91A380156A0CE7BCA73851D37B916C1C93F5A45AB48DF44DD2B1D6B23C91DE70F480C397B4C05A8B93E199D4EF247BE533FCED03CCEF20D313E2FE313967E4F4DE65817710D4C6947FF06A9508777712F4C66987883BCBA57509C588E8A71C3A8C7D70CF50A945DA7BA742B16CFA756856127E04D0AE0E82CBB39AE7ADFB8A688ABF811D08928EEA3304753596BA04471B8E3CA5F330E98CD3CD9EE6EBE9A12B9D58445894D5A5C1846BDB20D322545ACEC6972E829CF671B81B79FC0B140EC5940DB91C479C46B7E92572D8A77064784422E04E273375AFAB75982F6444092A2BC507097BDC84312C27F72158F70C3679FBED151796E8086825B15287857E19640459119732FB4469415B96DB7DA85462C2D269174BC7BF4D4B992D0B6A4DA92BAA2BE7F88AD0AC06BFBED1A5854F8348BAF9946728CBB42BB61FA288C80F07CFE669F9617747C975C835EA476C4E8FF288F65BA82F50AF0B9E9340960BAE41C6A6A6FAAD0FA15785A2CB77CD67AFE488902C1DB7FB2EAB64E1657DB282533DC909CCAA267A16502E0025A8B4C45BB6BFAB262E67B388FC62530950D93699FF078D21C9621B4937CDAEB45858959D64999F393BE59E8304DFA079663B9B6E126966F6DD39C54AA193C9DD8785EC4E5C13858E2914DECC4153C99B1AD8B8211C1DFE35858CF9736A4CA5B259D0B1AA528D52181DFE54061D960C939F6BE3EC1E8BEA3B9AD579B1EC13D4CC7FDC3166AE370C49D45FFA4AD483DB3071865007E2DD079518A2FA738DCFD94D79D36AF57C2DF26AC4DF777579E18DB9C107CB0022D0249D2BDB7B4503CB374E4A1A59061F05CF0EC6020AC0FA1965ADA850CAA5B89822E0FF50AC8AFA464B73E7CB8BFDE7CA305245E867ABBB35D3E4086E64355922B977CBDAECD0CDF272506A50F2E1D97E8DB39C6443F76A0D7EA0B4E19EBE7EEF0851C45714B59F74171114D2D5D589DA46E4D5DE7B83C484531AB0D4B76B786CE7CA99A22FC70F8CA901579E38CDF7443B44B4CCCC70C901EC3FE034B64C78F6B12CB5426E74724EAA75289FDDCCDC653D998A3A00C8C64CB47B5814130AACBAD79F55B14A0EF81E440D4E2FE1C8EB4D8888FAEC6898E23AEE45B4B8F6E5E64CBC38E498D57034DEC3F2D44426824635CC624A4B49BDD745BC52B1F151C193CA6855778FEF4F39E72C576DAEDA9F6041682AA1BBB2F134218028DDFC82589A2179A9AB70ECF613593EA6E04A47E83DD88235578610206CF6034159F376530266A461FAF89A6363269F6EA266F9A406E852F3B2081F1D36B6CA41D2DC0C4C644E4263AE36EE79081B352700A2D1C7A7BADC4BA24291271CF4AD3A02A8789D25E16F9E3755E2F61D9F018D79D4C7F2E5FD640E738CE055535FDCFF651B2FE70344168619BFBDB1F5ACF0222FD689070D0AD758C11D485121ECC0D2D04B1D91FDBE8B1F315F991CEC0E5CD0EE3065B0BB3D9627677A7D4D4CA0D539E7552D6B2D89758D8EC4A9AFE41C1FD896D4912F71CBA5E49F550118EABD0C67497C162F42052C4A6AA4009E2A210C5D4A3AE2165DC44D5072123354C542C80E57F86BFEC55500181C7E057D1884047A5E6815A9545B642BD631C5A46579A507253FFAFEC18581381D50FE491D319A1AB4A0D19689", "BCF2EFED1E45C35C5FAFE170AAC3F4F5B3EF11220EA6B9A254F0B90EE8D56B94"), - ("E877B205F0147851540681B6D2477F3D265BC02868E7868405BA3083151FBFE1AC548060B555005F4610FE37235E253E2B78CAE5036AFA411E6DF3334929595A019D0FBA542AC52D2572035A3C08B0572A2DAC4FC8C0022190C72354A35655732D57214E2202B570277810B76E56283F358B6F34AAAC70337E72A11D1AAAFAF7AAEDF8002D0BAFEE6026CBE0B216B8BFCF031DECE9157476B57C29BD1368A620828C9D71C9B8740A3C7C1BFE10CE23D82B44EAAB3287CCC09033D2EB9971B04DAC2859BF00C02CD464B82C44AB1632F53C625858A36F163F3C4065AA0693D96896BE4C04E51024C6089255B82B5E19479F13C836998A016A2A38EBBB36839903A3CEA6B0766503AB3FD86BA7F890AA831F449AAFB87C6A404A51FD50B68A4C134FB520BF437DB5D402BE5342BC463E59025F8D70C52BD5C714C0A09AC07DD44B77136A37C7D7859DC49DF85A5F2799836154973205AC75CB5635E5B7A9F94CD1A77C1F16CBD360A51C5B06869CCE70A1A92632132C26064B3A818C73A50D15421833C1D8F07D7D26CFCB2C6881F4C41E2B0C9C0A8EE205AF66D26FB1128461ECC5FA5283D03161AFC77F35E3383AC4499F48A40B72AF393A4A346005E5C6659BC90131301374921849BBCAF569AF99F401BF98380F99878D51287B876C4D7639D66611B820CED1C979BA399C6A69AFF436196014C465A2596CFB345F06B677966A2E476E2341703734067C32B1C8A16A50C96D4F552529D4AD65C25DBB293D223BAE9C1A2181E3234E64241A860F03112C0AD81E761A0443AA69E165B996065C98D15DE3BC7E5302C0999C82E1457762F98CF30556503C2BD2C1099BDB29AD9CAD83A150C2B92121E005CBE52BB8006500776CFA7C18B9B763F902A465215628B03E41898EA73AA5865965F52B0FDA601EE59C962E7461EB3751E73C148E00541658AA6DE35A80B35520056C4FB17AC0633E271C3529F574B39608FAFBAD8472BC5677C1198A04FEB9A623A200EB42988D945F6033BED6790019EC6C14299C7F5941FD08D043A2859A456DE4EC8A15090364048BB3924C7294A6E2B14C116349216396DD8280ED0A97B2930970C9BB545345761B7E64CC809A4BAC062631C7953CCC068A6133999120CEF5A2CA7FD452E69BB62FECC1CE665D9A62BFA6489482E4628F967E4E0150AE8726AE979DB805C9C78A3E48698B83F5032F44AAA2E2C768A8B2D641961AE48A1D04760444AA0504928A681A08D832E247953FA67382CCA563B76009C05CE3A3A06579781B30B944596C034C6C0F94426A70C7B2766654E4A2A6C6CBF3AC8E20C52A2554A23CB8B8355B76DA6C60AA5AC186F5612EE466FCF543C3493D79D4A0D8F3AFC66706033C13501352074052C44511B5F17B21CBC2D4899B61D31D32EA1A08B17C962A0F3A7B386DE7654E3C4DE7B0160B1C6BBE527C595640FCD8601E7A8728537469102734D98A97C86961DA2762369640F18EC0A7476BF95D96E7C9CFA3111585B2763A3E6C2694D96C0A08C4460AE4347C6C7B29748A18E3A52BB13F771843DB7BB503D82924636E8527009052AECD97701C97BD66890199049440498F01D1577346448D14A34B4230DBA54FDFFAABBBB462D86BA3BC5AC042C912FF4816091A3CF6260663E5A7AC9C9854316AC94C7C0915A526C809F6F1A90DF984C07C8486FB9B2156355CCACBB9B85294E8A584FC0AC02CB2B41A913A9895ED44011C8C99AF5869B1AA9C2A5C723D560E5ADC55E23292010407532147C1771B8125BF11B23B64EC16C299C25B40C0977C1A71C444A130B60FCA6F8EB98173287CAC7634F4E7693AF9AADF049E7FF86A777266AA3973A98C09AFA74F4C2463A64C8CA02796F17A11A907257F684185067F2C13643A72137D85092E880DFA83554A181F3F45B5F2A3131AD479CF788C3DE63C8A3C233909A5A9CA8E93B0A67F77556E8C20AF678ED7E5642176ADDB2B3462801BFE561073B42875130735E261131141B382BD80C7B11B007E22B979CCE1C1A08A4EA4F0BCEE75B047F58CA22C8727B57EC632ADCC64969FFB794E6932C6F74C37BBBBC18518D22767D112B262991D8BA77C3F3364A95728556754EE537C084CBCF185B068AA9FCCEC1899F6204B56628B592925E570A83775BB6C13239C5B9C61ACDA9B3A5028AE1979E83517ACCE3AF699E51F74A3D50991FD864BDC2268C6B24E0E4C01", "6E2DC22D76B36E1FC7978AC7678BD7C381E6B65627B72CF3E4077A86FC75949B", "526C69DD30685A694D35131C5A8C409F94659F33E29BA7F95054FEA09223F4B5AEFDF8463BC80A8FD1FD8B5E044EE8351A84027677EE6AC185EEEA7A70860F790075ABC13A8F7251A118D78FD8DEFCC7451823DDA959882AFBEA1D5817EB339F367152FD7E3A29E024C89D89053859D17D42AA1A78F30C135C01414C4E330129065970D87463BE54F17CB9C53DC25F8ACF8B556B177C0DB19CA2483563098108AA2E5207BFBF6DDA093CC5F89C583F7207F112805D29B4782CD6FA606B52356954A1452E3E0DBA5F225F32418047906CA389B87AC3CACF74355D1A97236CF082C7BB383CF6497A36A4A00640B79307C1F86462F100967F21D081593DF20D925EE88490C9C6720C2199577A48A7790647581459E6EBE303B623F6B0618C63CAF970CDC2946D7A879C33DFF88DB4548FDB9427D129C7B6D4DFD9BEC056F56C8D7D914F9E218CFFD89716C3DA29515272FA944696B50D37A007DAF4306F4189DFF3E0D96660E407E2C71CF26AA58DAD99FE0182789768E2EE24DDD3FE84D2EF896DE4FA81D39906A03EC7CE8780FD084CF6884B4E1CCD34CCA16D61BFF6C1B508EAFFDCFAB62AA6A34106944A0B5001C80DF368C0BB050B8CEB46F0601176DBA177BB83F1150A88C4A1AF3C0D8589F6BEA3ED78F60A290C114C74895069EE7B976FDF21EA9D6A32C8B4C432017A534DC818DD2972E49DF45A800BCB57392B7187498970F07AA4583F7FC99250F735CF3D390EDF2059AEC57AB135BC8CDC10B1FBD16D6D247F2DD2C524A3052C9EE85F93149FFCE8D95D4CA3EA4AE7B75CADD097C756C595B7901DC05AA8EAAE7590443CCFB12EA42F0893B21050FC286694E0990E02FAAAC9CCF762D298D5EBC7401FABED89625C3DF1F9D55860768782EC34B2A71C06E5E74582EAA846B5C1160199A9052188A5ACAE4C0C5257FA864E890EA562D396CDB35EC60864BB8FCD8C70A9009AC6913524C1FEE7036E0CEB7832E76C56C3715D152A3224D7EF51F427CBFD39286FE3D7FCBB9F4515C0ED3A874FF26F22B86BDC21622B314AA46924DC3DE6A2CAB5945EB34ECE830BAB383AADF6A20B852519EFD2E0CFA6E14E89C49FA99F28A234A91BF648471D93C3DFD7456A8EA347DF9A2DCC98F44B9DCE277ECF0FD46FB3A8F71334267AE8A35A564A17CEC14561ABE358350DBECB145FAC4FA8B01BBE0A5C119CE9BBE99F4B0A72FA7AF8B59F60787914E89068E492F1C8422D0641D17110F6BB3B3E5794BD4E536683538C4758273D6B25A16004B825F17720639F3974A5439541FDA29A501670F4AE549CDC33AB048ED15141A39D52C4F933FDD6ECEE573C70DF657677738F6E95A6C2727A192C6196CF6F5908E20CBE178F5CC4DA4533605D6ADF22501964D20183B877940F886CDC925EF61B797A35F5D71C39141888819763CF8201BFA236C50AA38935E5FA2569CDF8A092E043FA9838C00C1ED91B93963CACFD8103A9603BE5CF0B07541C1C31D6AC9404F369044446E6B853127F1BA6782D25CE1E2F97666D6572D7F4C3F9B5EC9D50F712F01CC46D3650522CDC6C64BD89087B459592CA9403016B0B4FBB327C1B5CAB60C834FE2810B3E9046F1BBA60B20C31B9D61E14754EF08D91073AC098D6BFFB9BC5959B412BE9FAB115C1ED59D6C592B82AD4FEFEA935053C3CA96895EF9657092615F00C5475AA9685E96E23EAD84B957CB7E82B0F58EAEB8699E9F3021333E93E33DF27383243D51CB991178E7287C17EAA5AA174BE83BB81B9E7F781E2B85CAD4F9718A2B2C90C2D1F2F860550BCAEB15F5B4E44B2696ABA3FD455CDFEBAC66863B1F470FFF2F629A07E306940210E8478FFC9FB6A7894CE292FBA95BF07FC3555DD0B8A41102D2DFB5AFA8E638EBB466C9EC432D84FB5FDED2A47BCB61248CAD67C8578F4E5978CF1DD19E81977AC4C4518E9E1D6BE17D5E18F30BA6E67F78F2EB9C65B36B0A80AC2BE0D8751826F5DC22C9C51FBA01D2B3740BFCEEE370207DCE7FEF8622CD072551896A4E0ED66BAA12F7FEE6EAC04CA511D1C8A5797252A71BC49E2354A0892201570C9A4A96C992E3E6452B21C841EACBD0F78217F78DE61D877B7D5CD49986C8B2A73A78A004E92F973E6043D5AA657AA1714E8BDC87383F1CD1D5D1318C0531A1C7C08BAD0E6B8C19837FA96E893DB134609FFB7551ADCCC10F6D5E74C85EC8F97FED1F9F248CC0F221C6F211B", "EFE60E221C3BAB00072536698A43C8D03B093F44FE9B3B8DE0F56B48C2570C38"), - ("9520B1AB029949EC4F0BD73F2DC7B511F242583778025B6FD0E7CB81958FFE1086DF36BB7A39696808883B5A16F076618444846B307AED6961AAA27DE8659E03E677E19217D0D05D80AA46ED532684493A7DAA2DC9FA07BB2287B4A07FE00B5AD7994CC0D604AA30686922C56CB992516C9FBCF87A31C4337387612A500BEB9616680B2B1547396669239899BE7AB3685443CE9D5A40AB32501898C98FD173682126C268A19D24813171031032A8E17780ECB9CDE40C36D3223998FA7D3B77BC99E945C2990523F1C07748048123243E26BB0278A588A7549050645F315852E891E2939619A84D18143213BA64E4E116315CBFD66A691ABC29FABB973EB0AA18169631976CF4CA4D36FAADD06CCE8BF2B15D851F74E622C924B921546289459C4F680401162D7BC10E2333B25C34BB4F8038E4EB2E6F2174580A76086CCA02C0503F506649E17C3C867A05DA66844402BC55AD6BA521D509059E6907C60A43E13B3AAA572048978E052179367C0E682C9C465A22F9B8363F634E857C094162BAAFD90E0177442AD81A2C83C1B872165AB5B5D52BB8C194A6A074B3C7DB1E7066925C3988615992C5041FAE24C44120CE6BCCCECB977E5F39C957570EB0972719E1645B80A6F9F071F7722071DB0706129DE5A1523B1A641B1502750842BE8A8905923DF4E77755869F55776BAD53947E5560735CB71643259E56367D287D48A1353D88224A280A0537643EA66FA133837C0165EFF6022CF634861299C0B3429CB20477517C51F60C127344E58C5B2AF7C05B141C73E8128BD5839FCC4F0DC3AEA1C283EAF3C15AB82685F4C8C89B73FA3C5DBB128B524C1D801BA080C93F8A0921848C5725EB46125844B5F1488749B85207A16100A4B2502F41054FC5C4B5433B6E5D37AAC7F3334192AC6ED1924347C0A4ECA08CD040DC15C083859C55241BF1AB9F5F43547D728ABA853EAC12A83019072A993D83179FF5E9C4D926C2E4C8A3EF9983D4401871F7109FB12251B4C865A987BA584E7E2221BAE52593269F7E149BA9FA8820B85EED036028092B0557A7BE27CF9B09CD1A1A5CEB768BAFE39E617A661C3B8A0748109002A21368616C67C211C252B3DB377BFC947EB22010C9C36297B45CCA2F64D626E43A9AF646C68AF5875099141D02701A94C03B980A37B09071F20DA6A1C707993C5757B5BCDB02B5F01498A761CD7439B8DA9880C3651D00356BE84AF95052941805EAD56479A66A4BA672C8A7A629429DA75314E5933C8C149033969AF46CC99DD4B1FB7B52DDF519BAB02799869EF14B83E085667716825F771D9E80B2B19B3412FA850FA9B19ED71B99DA4B1F6BC80B0AA5FFF77A53E77F014449877C1E7823912B979AD2516821A349D071B69E5382432BBA7E10C9B2975D80EA9236B35F57C869A21B5C194BA32DA3142FB58B9ED78608B3C30E8C29EED254CFD4BA012B7630725B5E6A15352C80B8DBBFDD62048C9695264C5E1864044ED79359666A9CB2642ECA5D80C9058AD70AB726000CB342334074C1967BCBE2319E83AADCF07733C13F25A4859A0034CB016725C81D0053A9E1FA605536489F23C71604A086CA3C60220DD9236213A8C103B9CE41463CAA792A5A320FCBC83B4276A1FAD411E2D9168ABBAC22F1932995C9D99420550484F014370C9186BE1C3EDF1C40C1A3411672B08FB104A5C551E20A2D3F4B791EB3A4B8E1CD48AC0C7734C4117B53CA7C8B1F0C697D448F7BA19EF4E90381C94257AC370FC15468C86213B7C6094704EA317EA7679858562F3B45A412B1C92BD37934951D2DEA27D3521391434A143CA58A904FC91743ECF47CFEA14B67B773CB7989D281C61D88751AF9C3A0344156085B7EA74CFBEA67D862C0561ABF70E17719C44483957D52580100C997F4F0099A98204E6BB7CA08BDEAB072DE920B70226037C31728F18AA19504158A46FDE9C42A9272B5CB525A8259D317157CC802A002C464A2CC68D3BE6C790F52D1C3BCDA495D8C5DC6F50B8B64481B70213DD46CECF33908785E80EC9FF5D561436B7B3A1963CD3C460089678F0C08998C4A8CA37D5B1624E85C1CF2911DFF7239E0328A76FB162811CF10904E949B09A3499DB3C9BD0FA1B0D935B7C455C8424330B7075DA6B51A17E283DD8373527C770F193116D08254D87F7AD7F57F47BFC3833D8C6CB0E549E6505914D07EEE50DADF41B39D3907E38E3E", "85962FDD6D092A8551968684B9C9EEEE736ADD8877D876C8EA62A2D5CC83E147", "851F92892D2E0E11102DBF8C99F61A5B7710EC70E41E677732DAA983CE86D0C396A1729D72217BA452C4592498024490D73EB7AD2D2F2E95BAA1B204E621EAD69513554261461D6740CEC83EB5344B55F5D11CBDD98752DC471C20C69F66AD51523428B19E56F7DA32F86E9662AF37CB65477B8058599D2077743ED4BA28E4CFB3B9D6DFBC895B11C7C81E0761D1AAA56A38CE776DF99064C1E3676C04D1E116120490BDED5AE01070A8614949F48DCA91082A9A809518040ADDB1AD7F5A9FA1BB5FC704D73CD66A0FE6C0B34AA39EC3FCEF531F66303AC672F5C9C6ED013A45167DA34553087AF1E78C62E2CD214D34536297E2BF3EA97B258F261373E936EEB6C4D17B7994542E4158BBDA941F5C107BA14B2C819E2AB671DB2A5D99A20294581B3F6D4983B820A4FDCAAAD28A9859ADEA0CC2C4CD4A415A91342387F7909C6904897EAE97F1A79D07D93F9A0597B89E3E80768A6C4B1B56025D56374E3AA67C1515B7FDB872F100CB844CD54B368FF5FBDE0E476378F2A683BF3875EA22AB4B46A83372AF9AE6CBE41753F69C98C65E07E7B9C4E8861CC2504916EFC70672CC567006342264383934BCD1000ABA678548F4C400E37F50E948FABD67895C3D6CCAA48DA27235BF1D7BAD8804FE0F32F66CC112F5171139D37562349A6507720DA44DDE0133122275C1905613BE83A083CBA43D1BA240FC7BCB8AB589BE229D3A219E029FB4306444BCF76BC0C9B6408C1E228E58B8B5872FD582040618B056277B659637938BCC2DEC198D27F2FCDA216C8046D74B3D035332CDCA51356BD275EF5FF9AB6DB9C2DF2151D9134EEB12421B427246F287761402A0B4D13D9B176DAB7E91D36723D0DB6A9D0FD25A4D2059327418BC3087ABE250B2A0ED03765758531F522C74A38EA705ECAA1100AA385B8FAA9AE4CC56D8E039543A1F51B6AD4A749F8C5B2A8472C6D4833A0E51E2475112BF9E079DAA72B36594DB6F5845123576DDBCD0AEECCC9E2DD4108FF2EF6C4701EC385A6E5F5412974D9DEDC635C3496EEC4DE3823CBF3EAB43798E37791F65585313E34E704A88B677E900AB8615D4DE9ABC7C3F450A4F71EA7D533570229FAE1C4151176FF303849D6C422A7CDC94D5152A17907E1D1C966B8357F59512BFE1CF1F93F6B15705C43579FD10C6C5527C07BCE41826C3B007688E2595E0FCB980602DEF50EE0426AD34218FF43EB5F1A049D33479A9C4A60F70900AE8F8226A4CE8BC6841ED50E375E563FB66EA358229BF2BD4A310EA58794AFF3572FDDEE92EF5D15BAAD8BC702E0ECEAEA0DFCC8B761FD671E75D3354548E031D16A5DDB45EC7B570A4A169DCE11ECB7E9A5F71236DE5837DAE1A075C258134C281DA01C7FCF15D41605F91F9D16C4B8BFC9291040365A623F61407F22F6143222C1B16D0D6C5E2C1F243E241D76BF3608FF32701A93F4C50F27D6FD7EBF8F502293433310598AA16D1C0C1C453089A622C2E6FD91D17A217853F942692B5748CE80839AA05E1BB31D2CF9AB3CC9BEDE428374DD9E99DA6441BB6BFB50FB5E850FFB463B7DD980C2DBB8A5CCB96F669489E553BE84D233E0316C3940EED75C172451CB0AF4EBD2B9CB05E3CFE292D0F106533DDEC77CAA49358FA79F3E9B5B609AFF64E3D1085A4C7D1E25FF47035E3923DAA1E9F5ABF700663D2A5F1AC9728FFD552404B63EBE406207ED8D1E0D8DEF1C761FC87432067885887F4E05136BC497B1BBBD4C0D36BE054108E90AF353E4296AECED037A7EC2CD99FAA76769283524324651CD18CACB401FA7C556EEC851B3F4DE090397C1D5893EBA09ACB224F18A0BBD5A5AF04D2BD1911288FFCF3B0C774EDBECC3C9E7F38723A07AEC75EC7F4055C353599C31B84878CBAFAE305E6DD9AB3B60A7A2EF0186639245F8D8EADEE7B54CA9A52ADAA9930B43CE4A7ED5AB1B2440C488D56B00A7C758F329824BFB7928470A3E3D0E8DC21955BD978BCCEAB0DF503EF4CA56DDBCFF8794F17D9A24F19173FB6804DA17BBA07DDBACCB483D3AE1DC8C4108DB95FD4E2B7D62B047C9859232E8BE14CC84B42A14F6B63450D981C30DB3FF0A62EBD7BDCCDD728D94CBF946B143BE39D73E28AA1EC0734E28E6D22AA3BC9855235BF6EB041A94C91C4B5FD274B4D09A284ABDD623D94AA6225B3E090195681784ED210A19744DAE1053A2AA6E3DC5848964624FE1A7CF73E73025E9F7A", "DAD9ADC089ADE86D6457A8833D41B235DE0579273F24874D689903199C296842"), - ("F89A32FDF01452B2ADFF6028F73C16510347DEC0400E16242CDAA614FC9A5EC69373D2A99C500414026B5CB2B67995B21C528E43A1B945D7A796AC2C0EB3BEA43665909585D3452F898374727A9DB92322A8116E18B20E0A3328B029995BC4B320F930318AA4966A125E3A42A4A3624C4939D0603E8ADA4863B9200D0A78BFC5AEF42A96EF188F30B7C153F83695B0CE226C2DFDC84CD724AF310733E4A420BFC4A405248647377EA71A2D3249BC11D15CF0C75FF083B453C338A2C26978B1607F5CCA9CD93315E2AF0631786B2424AB9B69711B7BEB790C51314C3147865C8910D9B22233636478F7070AD6BBBE983B51798A0211CC56A89BDBC2C3A0C80E47030FF074CDE1F080481809989BCB3FE05348D717C27CC32A4C7760BBC9EED52CAF3099E4F9B43435A673AA80895566D3DC8B9D1427604054A4932F9D68C115CACFCF3C2CB25B22EACB45C57064B9BC0D55D508BFA101BA01C06DA07450810E1E7CA9238A9358AA8592E7A89EC25CF8B32ECB81A38773489B4A6ED6F85603358F8F58833600044B488401EA9D60F99F4F58A292337124F15F47EC8C67DB7AA1098551880AD1D21736F83246AA921F18AE1D0529AA274CACF703C1E9394044B0DD3A0C2F776989161026729778435FB72AABEFC71AA96CB53F5411A4740202A177FF13AF362024B53AAF43395B53DA505FAAA1CA42297E968D27F1338DA6698C153A340A85EF6831991BAB36E3CB5B02A33B6C47B76989615C1DBE080639CC2A3C1550DC5C83A982A9F8A391071C6D74CB25C4A7BD7815BBEA662F5C51935ED501F8169D3005BA5E51C54053851545A4C22A8AED2AC7EEE60D4CB494EAF30B31504AAE91A085AA5F6A34ABFC2741C414952446BEBDF289C76C3EF6D25D47AC2D7281690B1051113A64536B05DD1936469811FC5ABB4714C9BAA371B00413E672AFFFB9AFE5A61B8B5758FADA01C681937BB2C23359A0EE9A0A9968994AAB8C7052A89454724409D05AC18ECC2151C534197028CABB521D3E929C54FB9AE61873C7517120766725B25078F414E8B2A672D85F23577ECAE849316CA660BA09EF7300F7D77E01F513BA4B628F12639DD9700EECB79997500D2AC36399917B05A433E51C28929C4C8A091F3C3239B6891DA2759EC004E6D9CDE60C527A31CC04B2261EA686BCF86191B18A7FE97BC4D2083FD71A914C52A36564F6C41BB509C76ED34C3D7C9502BB1570916E255532663939534A1A986C8F297ABB10799A0CA508E2B7358F961D0EEC71BAF67A32F953511499773651FFF526F4408EE11939D739B88BC97551C21BC2C665EC49BA5E258C2C3BC4763C361DDA35D0F35B83CB54B12C15BC894DF1B399EFC193CCC175448C71ADA17E48828BDBF09ADA393DC9F90FA0790FD9B8ADACB566BB23B4298900625C78147C2CE5460B388739223AAF2267A3309C635DD9486894263AC40F86B1222961C717C341BA514029DBC8DB0546CEEC03FE4A733E17345B240437E38D6A6676C5613480FCAEB29A5492B7839C000EE6720A72FA92582745499A4531218D92CA480D5565E15B3B65B01939636F2BCA54B4A855991664BE4A1293424E710787C6966F61C586EB559885FC819699088E525B815653BD431308424A50F30844161A01C331505C3F2AE6C95CA8ADF323310CF43965B2AD0CC770CE291827FAA218CB273947CFE7CB0838150C204520CA92C7B92373868B0B91C141D96B67F98C0888EB4B5A8695839C3E52317D94505BA4E654B6850C1F9A7F0156566FE462FA6C417AC8604F6B42C5B49C14E08CAD344FE64C7D787AC3FB03757D651AD49898FADA528D9407AE2024018C419D3C514DBC11099418C93235D4F823AB5A086CBB77ADBC92AF69C415A04BDCF1B82CF35B45B04304613767020372A145BB109FAD2442C495A650651F74C293D9E16F1E57485875A6E9FB5E16798090B41B5F30072DCC3C8A25BEB4E2B771D48C95B79BC481A5330A3AAB329C257A6126D8881FF075DF6C4554D621BA5AA865770A1B1CB24896B15E5A34D6B34F20D769F7FA293C13013643C89433980E8C9DE1A7CF718422814A4EC44B1658FC69BCC825969BC3AD9B7C8474511A58252246A7EAE2A8520B47C18A812A9B794F80392372340E564591201365FC9FA2D62926957F6F0158F7CA8725A61ECAF6F3465947A776EB3EE8202C718CEED80FC1925D5EAE3853A7A0C3F4", "8C65EF16D1258A6C0705225DB7A5AC1956DD97D4768C5CD3FF9FC52998A9DF02", "C923048E8DBFF58C2504A2061D9A8412C8A070B1AB07BD7B7CFD010DBEDDC6FE2AAC3D157EE7C56FE934759032DBB053401308F0956F7986A2EAADAA2D543E1DAA8496D4C3F43086DD6EF523D16E21D28EB63E0D4B7A22BD08AAD709B9BBE5D513E99BDB4CFC93D89B5638BFC0A58AF607109D273866F1F29ED2B5334ED884CF6716E3086AFDDA7740FB395057DB7E9A4AE16F87FD3A0ABF51055DFC2F9CAE85E5B264A4C0475F6DFC8758BFB19CB033D071E6538018101F3990B7BA0B951A2ED69707DD61AB3B27E70C43F31E3870416AE1A5B27D2083F544E3F327DD68AE18449D035A0809A59702FB7AF84302A9B1C3B1626951FEA1F918AE20551DDD938A842449FA4F475387085C2725BB282640D235FA8C9045990E72AA68B11F403C40AC4835050830800B2673068F1E164A2B0CBEA439989FBDB8EEC4F86BD147350BE0FE378C2A8D6A07333BB004EF367EF49E76832E758471291CAE425E8CE2B5AF5BA239CAB8E3E6A8A0191D0C9CDFC325466F02832059C39813D602AEC76B27C3A6238FA59A78289213A7FDFFFE0F1E48287400B347DEA82189FB0847D538D08617F5344113E4E3D255D4A1C3F55D10FB0403B3F9DF5F859E93E2FFC5DB530E0E44A52AD26022E40856D55714C580C7B9B893DC1D7E9FCF3FD4550F2D746E4CC281E151E944FAFA2D867F79A10B458B2CE0BED298CB2A2080CC51F6CBD83C02391BFFEB472940FABBC3FFE73AAE6C5A1057CDC4B984DA49A2EC9312594DAD1CBA7A21057117A0039F97FA26824FA9E5AD8350A4022A677BBE8BD423E81CD5A217FB2FAB858B745C9F3882F7AEECD141918D72A8F708B33D2E1C7F2FCCC9AB62AFD1BF6DFCE09262FD1E1F6B33DFFA19AC556F8168ED06CB39110D5141EDFC4274667E7F8D99248C7B73D001A31E0557802FD9915932AA029DF07C1B67DC429306AFFED25CCD2C230E5758719AFB043B742281D4EBE08BBE4CBF86E47E81E10669C992CF94EDB30539946859931C5F4F7FCA855361C081957A1871F695CB3902DDE061610FD1EB3A2B110B954A99EAF083ADCCAA47A521027076A05C20D325CF21F7E8D6AC6DA58438EECC5F03133B7F53F37EFED2604772A8D807CB182B81BDC77C0B7113DC5677954F58056B0D22C876FAA9712EADA80D0A6165E2FBC09882E4CC4EC7F222A88693D58DADCE94F1C1C19C4F40DAF7900E869741593458FA685B41FF9C667790FB096A60D98CA2049F42EDE9245F36E52722100855D1F3A31C8265450C9189F75A2986090FD324F62A43CAF94D494376A4B99C0C6DE5D77E316671AEA390CB412E094372A8F966D7A4631D42A44C8EE4E5D6C6ADCE93C573BB709AE24D6B7C6BD8E3DADBCD53DFD10C6BA7EA4887DE7E676E2FEDA3589EA35022D15B04709FAB44580B10D04C40C3519C5BF6D963A7929CAFF87413BD63EAE1D463C29BEA36DFF36C4D46477052B0DBB43ED78D3C472FE90AF0A1C8C0770B7F689B028F5C06A0E4EBD4A110C23BF4C1CFC171BDDCC16153D0FED91282007EF3924050B728610982D753281C5D0BD395123952C6B10EBF98F65E08741400AA09436D7D0015B71B0E7851E8D98EDC71281B502DD93C95E520F11ABC0D78D87F45E20E046411368CE2B494B765D2F6AAFCF1F89F4A8B6680265F0C5C597D3D0031BA33BCCAF421DDE1C6064B19F8EA4E93372B6984D525172E976C2677D8F178E72945235530A0CEC6676CB3427ABD090CA8870F3291D3BD10820F8D47EEBDECD784C06554E9A47FCCB9261FE09AE13A32A677DFDEC7206670BEF74DD09145F4AAF038CF12F228004677822F5BD24DD833DB12B9298655989DB96B6147106776B4BD7B8E4F547363AB5850BD685B13497DC98DB454701258E08C6017EB66C15CAD5D472E09A72EBE2738E1791861E831690830DA37F090C156D5424706736E53C1A94ECDA15C5C00C7079AE018CD708D14103EA224F3F07782DD59185C17D1EFB39984A1EB6A86D5661C4B8AC328602E16FEE80AFFEA654A3F9D68F4CEB440B81E27976F4A4520D72E59AA69BA38AEEDD2C01495C2CA736202B4500115262787C6E8F179D1D32BDB168E66F820210D8028AFAFE2058D1450F71DF21EB16E31986ADEC5A1A30FBD919486EAB01C8F232B91D32D52F61961485E08EE23EE3AA533FF92041778005D147B2A36AA0231FAF1CAE710DE693C280694FD44203183FE90581A", "640E9C78F8CC336881BAE27ABAF2B6A8761C3D902A7E0658D444F36D19C489E1"), - ("88DC9E381C952DC3A5ABD9B485D9198B644D97AA7293A70B76C986A58B673964050737810AC77F658B30A5DC4D6ED0533340B40CDACC8593A7F3737CBA319A25015C1ED3C25E0277B725495F47711E78BF3B76CCE7268E1694CA8EF80F3043BF4E854157375578916D0F0C6FB4ABCE3B730788B75C1FD0076227CEA787AF7F04B680A4CEE0E4A97821C5185126B8763562B0A4ADC45E57A217F6D250065A18591C0D7BC39582059B8AA5907B2C10E393B73F423964D29775367CC772892ECC11A8C81C35749E69BA15DAE56867C90819D0043652A2B706A55DFA961331876862049CE854B6E379CB94A849D170B5038B61693A254B076A55595042B74803475496723E4790BACA6AC3E93628464F6677BA892235345CA41C624E3B9A8EE5A890157B6517B075C65B6C0A8BA946FB524FF64AA3360BC0F4C258407A697A77BBE36CEC5C00D1A179C5C34F542C497499454CE1618DA95D6C478CBEB52D4550586081813761A245775538764F3EC42DA4CC01F93945C186B6C46C2BA503529F42688262120ABC27551C04F656515B7C161B9775F7296408C898CA6AB86FA1781F5988081777FF9AB929D717B0C63295637255A369B93656CFB9464CD260F458B9838040EAFB381829929DB77EFF79791D98AD61AB98D8F6694DF5014E6B00757A448509481E06361D3031678607DC7764D76225CDF5202494C43EEB861AA495AFB1BEA346CAEBE2B3E0A5069A3882DBC6181B5575948C76D2176D1B6746A6D8BA23A405D651C3294BCB3A33236D9068D416012F225C0FB62F9427B298B33C0D217919F534BFFBB7752674801B3946173B7F007B32AC822DA3A1CFE711636246B48AAA9F9C40370574456A89EBFB45565739868C810E3CC5728CAC33982024292033D34FBBC0B16C25A4A7C3C0DB085EFA293C3AA525FF1856D9E8676E8235958C3F4EB6A4C8C86B97C78E5D7022F74C085EA6925EA78BAE44574BC7CB2709AF112B4DD46AACB20453F3DA1CD5D651C5B37B46E5AC46591767B9222711C8DCCCC2B49723F351B4F6519871200EFD3796C502135F4999F74379A176B35F2B761FC2ADCB698227C0C93E4C45FB270E28FC3558120B92D52002CA38B4E7201C38346857890A2B1337B9B14E3A4EAEC98D221B038249A133E732DC224BDBE7967ECC5809C91490A90655C38A9CDB99CA2B552DFC3A1A230A7C0918FB08743E11346FDC353C663024FA34591C5002797E1BD662F9FC3685333226188F2EB1C599D2C8ACA8BBF53A9248C9A677E43180A27363182BFCAABC5065CEC7C257F300C1F6A2A0B160C4DD6A90106289F508BF26D0A3C0206CCD41A390C5452911C03AEB0B43E5628B67B89593122583C68AC847A1CA4048F31146E1B39F26AF91ACBD42E8496A3952F6015247142468C42B9D75550A684D0A2B5508508B0D312D81B40C08A06C8B6681D5CB697CE871AD15C3987542C92AC4C71C685D802E50F7471791306C29507795AB2C90996933C1CB576F680C86E7A321C5B829AEA9924362B4BC61B91DF16A97025C93A8544DDA6A3799AC0D81C9525AB7601382444B780BE4578F573DA2AABC5E032201B47E2798C815368F3AA4B17970747506B4CD861291F280BAB83BEDC253FB3164DA84455924C724F24E99B2A8EA61AD9D775D3EF516D5E65BA8304FE9660D829AA793D4C7C09804FED3B5298C16313A216276A09AD1185BA59819B65814398CACB1A2EBB9BAD874224BD92E79E693AF9487AE964D599186E449821DD081C16C6B29C95754740F86B07A433681C06B695EBB0189659F3F02A96BA9A792E22BD2856E5B4818FED815B40092CF4B0EEE20288F2266D765AFC8658913D7CEFA353C03C54A5BC671A8091C77CC67ECE5487193685D58C5B66C50A434BE96542D6202B51134107219B6C6E636F2F96521279648F1261BDB900D607303015D05C4459667C91C3539B08B8D667980DE1220C9D7722CEC5488A442E835B7E29000CF908D5C7587CB47BAFAD144C3C79828B9B4DCD140E9583E47F32B0E86A3DBF6087F40042C2A9612F73969080B35A0193D3A58E6F891222B28C351A2EA802F27AB79D5C76D29895AC6695A472615D07852E26A15840739E48C6C69CAC6EB1544D2D957B192C630B4A61E2527367B130B3C55AA6B51887BA1E8D08BC2291A57ECE2E03632B65EE430C5E77CE844C1BBDA7FC5DB7813E0E4A79DAE35B4DC59", "2C29A4DF2135AD034D6172BEF7A0A30906A82EDFD7E1E6FC5E5A00C18B90CAD7", "BE180A0AB0F9C995BE4A132A8AEB443B06871F20BA11E523B8D773E969F01993B736AA164938A19B45CA65CE882BE28737536850299094466CD896E913D1BFB19DEF04AAF36ACA42C7905593C7A985593B4975547110A26E278EE15848C6EB53A4A119686F8AAE0ADDC948C8C3EA4B9C57C3CDDD0D2703E6DA55E48CD014B65E17189E95F8EB1342BE96C69903B687FD904A0A3545927A92022CD564C1BBC81B907C88A2518DDBA16FD3617EEEAC93CF24682365686E61E18285EECEF9844A4E2A9841FBF3606B84ECFF32AC3540A36F986EC4D8DF50688F80CF73896E2380FF2E5BB2594A8F1F51079EFC121ECDD50E18943109BD60CCB25529ECBB495AD312636CBECB51696388F811E0EAD5FFD6FB17F12C2C41CF0039098F77ACA2C2C18E84474C2AB4614070145E786F4F31095DAF16D1A01E3F32A0E39A7B4A3050C530CAFF4ED10B188D7DA519B6C79441896E3CF954BB6D055C59E13D307B90653D1E712E1679249F4B0AEF1F8C821109977F1FFD8FEC705F18B0E98768BD5BE2F1E822608E01438A23706823A9B7AF23349C75D96D143A7DA1F10753967614D3C61BB4422DF5AE2B5555EEC97EF35CB859B108637AF1514998B7C310404794D2E992843DCE5FD7002A09A9183A0B4C3BCC4100A26C45D67C7B0B035C4B38CCB27BE3060D862E888BC0806D66CF78DFB48BEC69FB3FF477A2E78DF79359E4B740AE8F32F97EBCAF0DEFE57E80760F991884F564C2BBF779AFB5C0E475C1BCC7B904F2503B4D1D467BC40DB7857FBDB5D43DC8F0DAA5DA2F09A743546C6D75DA831549EAADD42B9A7C8F690883035B0163A811A748A7F8438CD053AD66B78CC8E68C44207620FC52B878E0854E8B32F14C1AF93D84B53D33DEDD915CBB639C70E57B8C36E2707FAE9BF0EEB9CBD92C381C2DB04F24197EC03999F6BF6696932D2A24DB7821808F80F324DBDD1E54C8D1F557FCAAAA16E6EB0C3A07B61FE130BF5313CF9285D7DE7FC0DC53770F3F76FCA52538F128A0CECBF16B5C67544AC6998088F5047A2074691346A457C0A6C06EA571F881A573F20571F0BA4938997D4B7B46BE93FAA96F984206F87CC0DC47DE66E84D0C66DA2585CD6F721B9BC0ED71EE491866FE54E6EAD69F666C7B7D2C4DE76E2F1EC41B7BAD71A9876F4D002BC263D113E479F926051463F5F51642D396428A3295486502DACA6C6EDC27AFA2F9A1590C3B436FF23DCAD070922AAE811A408A3155AFA02C4EE738357851FA190FC42F2CC46F88EEA43A565CB88D432EBDC6772416BADE9D49B68B53D783F4FCE20F7E7FB0533039C63A9F46C073BC2B811D730ADDFEDF24C59024D246993C0D8E6465FBC3AE84011FB14923493EC110F94393A4CB3D1A475F5576BB78C0DF7744009FE534753772D88892C71BAADAAC58CA9C038CF43006E1AFF5B3FF1FE095AA6F0026E76DC15FB1B95661B7952C52D535D18C9BA1832921002DEF8F1BA00C7D6396C0DE3E851A43ECC99268C169450FD96EA8BD988032FDE42C3877119AB8AB6E3115534C3F212F4F4EE984667F51FBB6C2C4B1D7AE8D4AC9C977514943B26C23E30E48D45415879306B41253F7C4D807601C096149ED9619005CB5C4D6A12B8282C5F1BA3F94D0EA15EA96FC63973A11227489E0D5C3CCAA4662240279C87E9F2DC498FCC4AF71C7569D2A15481EA98BD9FC6043AE722D1EBD0920F145E34641950C576D11385E6AA10942C3DB0C6C961494C3552BB533AE90EBE4CB5EA114A999475A0CCC2250C40670D0061FD08B16B627F5D6B44030A9C4B5DB9CBA6D7F8156F807FA22459791F763035844F1078E06E595731C487BF165F59910A294C9A8FB42115BF495A5E1EED6FEB180F36B766CFB6518F91A5A9111D5E1CA1D56471FEF1A8DCFB6ADD0A85A63A953673BEDFF64F93E0E7D69CEE76E07FF3054C4D8BD3DF0B76D3251448F6D1F89D7AFB751910627D88DA9A3166A5A847D274C7EDCC44956EC4ED73B762DC2931B47782E580E080E30AE50A539446355342571560D283D0F51E0728A7284680FA6EF8D4B22D15D48CBF93E631E642EE7A3366D3908C5343BDC00D251296E55C237943EADC3290B1023C8797F81F7ED47A532CD7CD3786BFCB9CFFF954559369E30EA309B0A7A39E8D2492DF848733E95164DA419328069E98CACE7F04CFEE37BCF8508E83ECB3CF091734DB8039551BFAF71A6492924E939", "27B870279BABD8038A4B68BF3E1529FB88EDE8776388499BABF6CD371419FF20"), + ( + "F6690E77251A47A0126F3CAE222B8873EB0EC9669AA7B08F4854AA72C0731649CE60EAA8B08198B8712158238DBE83CAEFD7641434CE1A1346F6900A53C105462B4F1E909E680476000C8E8FF43DFF9AC7F36657BF852262D383E68CA2C8764B59441523F009B5A752278491DBF89192FAB92BC873DDFC5B5FB49F9160769BD228844127FDDBA4949A1CE958B6FC1388DEE6CAD9F549287B7E0C85300D636AC1A68C1F0860AF00CE50963E3DB200F651147C4425DF62467FB4BFBD3B89DA337AA74444409BA3FA73B8CD1454F7E7C730352F894387E79C7547C077C19496322817D3D540E496922F1063235B78EA66BA23918E79115277965E67760A2FFC91530AA85575474DC344F11130FC77CEEC03ADF4937FD7FBC838C05BF88263A9903BA10718B6C43E97047902827A491ABFA8D83780191C96FC8924817FA3AB625C76132CEA34273B3D74589E91114216989C8E98C5B4EC2E5AA01805BB598CB7137E61CB6907952A51BA54C52E9E8C629305B7982779D0CC9D43E14C254277A5124E06257CF7A61EFC186118A808CE4BA0FCD31B34559D77643BE90BD017B5C90C235615B61258D6432EE6138A90A0B3DA38B6C8C20157644C010B6BBC327AC4B515E2AD5C46AD63BA0871946B6455A303448662A1CBCCBA1A462389B7EA4AAFD4A411E2A45D92146FE4508094C8E4F2747E918A2E57874C934A5C654F137695C67084A3EC50F3E7C777DC42AAC0AA9727015546CAE376C68504AA99A8AE585C2BE6E84E510124B5EA9169C356BE0239CD8C900DA93203716C3557BAB802C846C31948B7B2EB064E2DE01E9FCA837D4986E43A019BD351DCE5218FC1386A46684F2347F6EA999C07586AE66DA4A129FE3C36E1435B0715B155477000768EF7877C618579CCD958DCF74323E9C065A04F0F1B23ACE41B0FAA69DFD53EA2C62AF467B0DF64800E79C4FE72419958AB1D470250B5122A915EB00A44F4545DC83B78A0942702B71D9339115E007FAB9684FB3880B0E97221D5867D4CB0A4921FA29A81A939231F2C1CC5D329652052C3484602316B792ABCD55036E1F41FDEE702BA1216AB71918F469503EC85E9321AB8D4287BD537194560E1F140FE3900D1864F588B4AAC648D1647612837CD11EC6B730768528A96D825BD06033A4734B04B2AA6F26586A345605E59781F046738C51747564183B139E772B94194B3EB64407C462DC54C14CFC81BAB301F9091950B5A97BFF836990C0A419AB1C8F08A8D200AA3B968A19224E03B140C998A58B86221CC258C407F0DDA752A890D58425D6269B4CB03052DA31CD0ECB04B36A42D30A1EDF98570A31CEAA81EC9382F6DC012F63728F38A5983B50DF7C65491934F2CBB9CDD714284C0706FF79BB1F879B6EA423C71198BFA0AFB628469BB0E6F79C174463FDDA6BA4A78A886EC690FDA63CD93B66CB01E384395FE0281A7DCB55661B2DFAA7990CC19BEB96BCC29740F305FF3C72281A5991F3B39135B066389C1DAB7B0A2BABBEEE76BD99B394C82A28EBB275643C288E421BFFC94AB317DD5191220E9527549670C67AA4D26CE8A6AA1C8629601631B35FB9FB881C1FF03CA68C2BE4D913269274B04668305A6ACE7129654C51DC9C78EA9B07B489241A0E4C83FD5B2857A13AB4AA9D7139202AC08E2877624D07AEE9C6954DC7E0BA693F260B714B80C92F6B411D510E6276061451D99E54AD55954993B9624FAB31DEBB0FF700B10FA3D7C3952E48082F4D87DDB703E55CB65577450A375C56644501364B871766DDFE57CEFA187745CA3B6F87890C2AC65B89DDBB9AE7AF90F9BAAA353043CD382A4089C5744D11E78D71A61D01F4C345C5CAA9B385C7AF35A72545C677BCB363B129A0392033956064FC199CFF4BAAFD03A3413C65B025FC186475690AE93699DB6D0B71B285ABA96201D149968574613865CFA26CFEAE7C249FA5D5FB18480A752C368681A5969AEE20DA1F0ACCE6281852441862B1B4C01A1C0352211D49204A46701F72C52287E4C8A8FFD8668E0227329E84B1E0A8D47418A4AF44A6E80CBA470A4A04C1817156A87F4AB7055389AB3B51DEC6CD64CB8D7890B2D4BB85E4AB20AC3ACA6762B0100640F92040BE73906E68E8103A57669532120312FE50DDD47AC0B19151FD6137B16A09324948629B84FA2E7C8EC150FB8C7DB1D800C0825AC18915BE2529771B8390945E6DA65F324", + "BF233CF6121D41585B4AF0EA74B35DF7ED52BB5782107A8259CD4AECC3587E61", + "EFDB7F7E56302A199A7BE79C00715F670A5024C0FD357BFD446B4A9371079C5A04964F5F4D0C8FB3D12AD1F6F2007D26F698C49DA91E3B107D2332C7612DF3D7A216FE4F07C5AD8A1E9E2DE80E3ED3B79BDBA3E5388FBDA4311F79401798D193082B17692BFE3286CA442C5F7D4D63327CF40D7B0C420084C636A283E83483A469CF88E669129CF868F013F8796FD76AE474ED75AF8832BF0A3658A4EDB3FD2F37DCA44586C78728834226E5DED1732A2B3ED1C0DBF4A825818C344CFD60D4DB460E2E5B2B1931500DD22C098CF45F407D73CB04E83CA866090C244A11D08613BA174B06F7C58633BC01629BE2F41A695E8F9AD2E09C2BAB4ED34AACA0303B58F3B3E55D16F4A84B4BCDC6D7C4C20170DB0DD0FD20F4B7FCF8E6DF8C7BBB75075EEA2BA10EAFC071A83DF4B2401CADD328FB729CC5071C1C869DEACC3FCD8B60FB157B1C424491D3AB36DFE8D15CF3DADE14A109A014D157CAE91F1F7FEE9CAFAC003F2A6FA9F3E4C6960299C71BDFC168770A7D034FF52120599CA005886EE97FD4F15AF42BA209052A6A93069CDCCD91A380156A0CE7BCA73851D37B916C1C93F5A45AB48DF44DD2B1D6B23C91DE70F480C397B4C05A8B93E199D4EF247BE533FCED03CCEF20D313E2FE313967E4F4DE65817710D4C6947FF06A9508777712F4C66987883BCBA57509C588E8A71C3A8C7D70CF50A945DA7BA742B16CFA756856127E04D0AE0E82CBB39AE7ADFB8A688ABF811D08928EEA3304753596BA04471B8E3CA5F330E98CD3CD9EE6EBE9A12B9D58445894D5A5C1846BDB20D322545ACEC6972E829CF671B81B79FC0B140EC5940DB91C479C46B7E92572D8A77064784422E04E273375AFAB75982F6444092A2BC507097BDC84312C27F72158F70C3679FBED151796E8086825B15287857E19640459119732FB4469415B96DB7DA85462C2D269174BC7BF4D4B992D0B6A4DA92BAA2BE7F88AD0AC06BFBED1A5854F8348BAF9946728CBB42BB61FA288C80F07CFE669F9617747C975C835EA476C4E8FF288F65BA82F50AF0B9E9340960BAE41C6A6A6FAAD0FA15785A2CB77CD67AFE488902C1DB7FB2EAB64E1657DB282533DC909CCAA267A16502E0025A8B4C45BB6BFAB262E67B388FC62530950D93699FF078D21C9621B4937CDAEB45858959D64999F393BE59E8304DFA079663B9B6E126966F6DD39C54AA193C9DD8785EC4E5C13858E2914DECC4153C99B1AD8B8211C1DFE35858CF9736A4CA5B259D0B1AA528D52181DFE54061D960C939F6BE3EC1E8BEA3B9AD579B1EC13D4CC7FDC3166AE370C49D45FFA4AD483DB3071865007E2DD079518A2FA738DCFD94D79D36AF57C2DF26AC4DF777579E18DB9C107CB0022D0249D2BDB7B4503CB374E4A1A59061F05CF0EC6020AC0FA1965ADA850CAA5B89822E0FF50AC8AFA464B73E7CB8BFDE7CA305245E867ABBB35D3E4086E64355922B977CBDAECD0CDF272506A50F2E1D97E8DB39C6443F76A0D7EA0B4E19EBE7EEF0851C45714B59F74171114D2D5D589DA46E4D5DE7B83C484531AB0D4B76B786CE7CA99A22FC70F8CA901579E38CDF7443B44B4CCCC70C901EC3FE034B64C78F6B12CB5426E74724EAA75289FDDCCDC653D998A3A00C8C64CB47B5814130AACBAD79F55B14A0EF81E440D4E2FE1C8EB4D8888FAEC6898E23AEE45B4B8F6E5E64CBC38E498D57034DEC3F2D44426824635CC624A4B49BDD745BC52B1F151C193CA6855778FEF4F39E72C576DAEDA9F6041682AA1BBB2F134218028DDFC82589A2179A9AB70ECF613593EA6E04A47E83DD88235578610206CF6034159F376530266A461FAF89A6363269F6EA266F9A406E852F3B2081F1D36B6CA41D2DC0C4C644E4263AE36EE79081B352700A2D1C7A7BADC4BA24291271CF4AD3A02A8789D25E16F9E3755E2F61D9F018D79D4C7F2E5FD640E738CE055535FDCFF651B2FE70344168619BFBDB1F5ACF0222FD689070D0AD758C11D485121ECC0D2D04B1D91FDBE8B1F315F991CEC0E5CD0EE3065B0BB3D9627677A7D4D4CA0D539E7552D6B2D89758D8EC4A9AFE41C1FD896D4912F71CBA5E49F550118EABD0C67497C162F42052C4A6AA4009E2A210C5D4A3AE2165DC44D5072123354C542C80E57F86BFEC55500181C7E057D1884047A5E6815A9545B642BD631C5A46579A507253FFAFEC18581381D50FE491D319A1AB4A0D19689", + "BCF2EFED1E45C35C5FAFE170AAC3F4F5B3EF11220EA6B9A254F0B90EE8D56B94", + ), + ( + "E877B205F0147851540681B6D2477F3D265BC02868E7868405BA3083151FBFE1AC548060B555005F4610FE37235E253E2B78CAE5036AFA411E6DF3334929595A019D0FBA542AC52D2572035A3C08B0572A2DAC4FC8C0022190C72354A35655732D57214E2202B570277810B76E56283F358B6F34AAAC70337E72A11D1AAAFAF7AAEDF8002D0BAFEE6026CBE0B216B8BFCF031DECE9157476B57C29BD1368A620828C9D71C9B8740A3C7C1BFE10CE23D82B44EAAB3287CCC09033D2EB9971B04DAC2859BF00C02CD464B82C44AB1632F53C625858A36F163F3C4065AA0693D96896BE4C04E51024C6089255B82B5E19479F13C836998A016A2A38EBBB36839903A3CEA6B0766503AB3FD86BA7F890AA831F449AAFB87C6A404A51FD50B68A4C134FB520BF437DB5D402BE5342BC463E59025F8D70C52BD5C714C0A09AC07DD44B77136A37C7D7859DC49DF85A5F2799836154973205AC75CB5635E5B7A9F94CD1A77C1F16CBD360A51C5B06869CCE70A1A92632132C26064B3A818C73A50D15421833C1D8F07D7D26CFCB2C6881F4C41E2B0C9C0A8EE205AF66D26FB1128461ECC5FA5283D03161AFC77F35E3383AC4499F48A40B72AF393A4A346005E5C6659BC90131301374921849BBCAF569AF99F401BF98380F99878D51287B876C4D7639D66611B820CED1C979BA399C6A69AFF436196014C465A2596CFB345F06B677966A2E476E2341703734067C32B1C8A16A50C96D4F552529D4AD65C25DBB293D223BAE9C1A2181E3234E64241A860F03112C0AD81E761A0443AA69E165B996065C98D15DE3BC7E5302C0999C82E1457762F98CF30556503C2BD2C1099BDB29AD9CAD83A150C2B92121E005CBE52BB8006500776CFA7C18B9B763F902A465215628B03E41898EA73AA5865965F52B0FDA601EE59C962E7461EB3751E73C148E00541658AA6DE35A80B35520056C4FB17AC0633E271C3529F574B39608FAFBAD8472BC5677C1198A04FEB9A623A200EB42988D945F6033BED6790019EC6C14299C7F5941FD08D043A2859A456DE4EC8A15090364048BB3924C7294A6E2B14C116349216396DD8280ED0A97B2930970C9BB545345761B7E64CC809A4BAC062631C7953CCC068A6133999120CEF5A2CA7FD452E69BB62FECC1CE665D9A62BFA6489482E4628F967E4E0150AE8726AE979DB805C9C78A3E48698B83F5032F44AAA2E2C768A8B2D641961AE48A1D04760444AA0504928A681A08D832E247953FA67382CCA563B76009C05CE3A3A06579781B30B944596C034C6C0F94426A70C7B2766654E4A2A6C6CBF3AC8E20C52A2554A23CB8B8355B76DA6C60AA5AC186F5612EE466FCF543C3493D79D4A0D8F3AFC66706033C13501352074052C44511B5F17B21CBC2D4899B61D31D32EA1A08B17C962A0F3A7B386DE7654E3C4DE7B0160B1C6BBE527C595640FCD8601E7A8728537469102734D98A97C86961DA2762369640F18EC0A7476BF95D96E7C9CFA3111585B2763A3E6C2694D96C0A08C4460AE4347C6C7B29748A18E3A52BB13F771843DB7BB503D82924636E8527009052AECD97701C97BD66890199049440498F01D1577346448D14A34B4230DBA54FDFFAABBBB462D86BA3BC5AC042C912FF4816091A3CF6260663E5A7AC9C9854316AC94C7C0915A526C809F6F1A90DF984C07C8486FB9B2156355CCACBB9B85294E8A584FC0AC02CB2B41A913A9895ED44011C8C99AF5869B1AA9C2A5C723D560E5ADC55E23292010407532147C1771B8125BF11B23B64EC16C299C25B40C0977C1A71C444A130B60FCA6F8EB98173287CAC7634F4E7693AF9AADF049E7FF86A777266AA3973A98C09AFA74F4C2463A64C8CA02796F17A11A907257F684185067F2C13643A72137D85092E880DFA83554A181F3F45B5F2A3131AD479CF788C3DE63C8A3C233909A5A9CA8E93B0A67F77556E8C20AF678ED7E5642176ADDB2B3462801BFE561073B42875130735E261131141B382BD80C7B11B007E22B979CCE1C1A08A4EA4F0BCEE75B047F58CA22C8727B57EC632ADCC64969FFB794E6932C6F74C37BBBBC18518D22767D112B262991D8BA77C3F3364A95728556754EE537C084CBCF185B068AA9FCCEC1899F6204B56628B592925E570A83775BB6C13239C5B9C61ACDA9B3A5028AE1979E83517ACCE3AF699E51F74A3D50991FD864BDC2268C6B24E0E4C01", + "6E2DC22D76B36E1FC7978AC7678BD7C381E6B65627B72CF3E4077A86FC75949B", + "526C69DD30685A694D35131C5A8C409F94659F33E29BA7F95054FEA09223F4B5AEFDF8463BC80A8FD1FD8B5E044EE8351A84027677EE6AC185EEEA7A70860F790075ABC13A8F7251A118D78FD8DEFCC7451823DDA959882AFBEA1D5817EB339F367152FD7E3A29E024C89D89053859D17D42AA1A78F30C135C01414C4E330129065970D87463BE54F17CB9C53DC25F8ACF8B556B177C0DB19CA2483563098108AA2E5207BFBF6DDA093CC5F89C583F7207F112805D29B4782CD6FA606B52356954A1452E3E0DBA5F225F32418047906CA389B87AC3CACF74355D1A97236CF082C7BB383CF6497A36A4A00640B79307C1F86462F100967F21D081593DF20D925EE88490C9C6720C2199577A48A7790647581459E6EBE303B623F6B0618C63CAF970CDC2946D7A879C33DFF88DB4548FDB9427D129C7B6D4DFD9BEC056F56C8D7D914F9E218CFFD89716C3DA29515272FA944696B50D37A007DAF4306F4189DFF3E0D96660E407E2C71CF26AA58DAD99FE0182789768E2EE24DDD3FE84D2EF896DE4FA81D39906A03EC7CE8780FD084CF6884B4E1CCD34CCA16D61BFF6C1B508EAFFDCFAB62AA6A34106944A0B5001C80DF368C0BB050B8CEB46F0601176DBA177BB83F1150A88C4A1AF3C0D8589F6BEA3ED78F60A290C114C74895069EE7B976FDF21EA9D6A32C8B4C432017A534DC818DD2972E49DF45A800BCB57392B7187498970F07AA4583F7FC99250F735CF3D390EDF2059AEC57AB135BC8CDC10B1FBD16D6D247F2DD2C524A3052C9EE85F93149FFCE8D95D4CA3EA4AE7B75CADD097C756C595B7901DC05AA8EAAE7590443CCFB12EA42F0893B21050FC286694E0990E02FAAAC9CCF762D298D5EBC7401FABED89625C3DF1F9D55860768782EC34B2A71C06E5E74582EAA846B5C1160199A9052188A5ACAE4C0C5257FA864E890EA562D396CDB35EC60864BB8FCD8C70A9009AC6913524C1FEE7036E0CEB7832E76C56C3715D152A3224D7EF51F427CBFD39286FE3D7FCBB9F4515C0ED3A874FF26F22B86BDC21622B314AA46924DC3DE6A2CAB5945EB34ECE830BAB383AADF6A20B852519EFD2E0CFA6E14E89C49FA99F28A234A91BF648471D93C3DFD7456A8EA347DF9A2DCC98F44B9DCE277ECF0FD46FB3A8F71334267AE8A35A564A17CEC14561ABE358350DBECB145FAC4FA8B01BBE0A5C119CE9BBE99F4B0A72FA7AF8B59F60787914E89068E492F1C8422D0641D17110F6BB3B3E5794BD4E536683538C4758273D6B25A16004B825F17720639F3974A5439541FDA29A501670F4AE549CDC33AB048ED15141A39D52C4F933FDD6ECEE573C70DF657677738F6E95A6C2727A192C6196CF6F5908E20CBE178F5CC4DA4533605D6ADF22501964D20183B877940F886CDC925EF61B797A35F5D71C39141888819763CF8201BFA236C50AA38935E5FA2569CDF8A092E043FA9838C00C1ED91B93963CACFD8103A9603BE5CF0B07541C1C31D6AC9404F369044446E6B853127F1BA6782D25CE1E2F97666D6572D7F4C3F9B5EC9D50F712F01CC46D3650522CDC6C64BD89087B459592CA9403016B0B4FBB327C1B5CAB60C834FE2810B3E9046F1BBA60B20C31B9D61E14754EF08D91073AC098D6BFFB9BC5959B412BE9FAB115C1ED59D6C592B82AD4FEFEA935053C3CA96895EF9657092615F00C5475AA9685E96E23EAD84B957CB7E82B0F58EAEB8699E9F3021333E93E33DF27383243D51CB991178E7287C17EAA5AA174BE83BB81B9E7F781E2B85CAD4F9718A2B2C90C2D1F2F860550BCAEB15F5B4E44B2696ABA3FD455CDFEBAC66863B1F470FFF2F629A07E306940210E8478FFC9FB6A7894CE292FBA95BF07FC3555DD0B8A41102D2DFB5AFA8E638EBB466C9EC432D84FB5FDED2A47BCB61248CAD67C8578F4E5978CF1DD19E81977AC4C4518E9E1D6BE17D5E18F30BA6E67F78F2EB9C65B36B0A80AC2BE0D8751826F5DC22C9C51FBA01D2B3740BFCEEE370207DCE7FEF8622CD072551896A4E0ED66BAA12F7FEE6EAC04CA511D1C8A5797252A71BC49E2354A0892201570C9A4A96C992E3E6452B21C841EACBD0F78217F78DE61D877B7D5CD49986C8B2A73A78A004E92F973E6043D5AA657AA1714E8BDC87383F1CD1D5D1318C0531A1C7C08BAD0E6B8C19837FA96E893DB134609FFB7551ADCCC10F6D5E74C85EC8F97FED1F9F248CC0F221C6F211B", + "EFE60E221C3BAB00072536698A43C8D03B093F44FE9B3B8DE0F56B48C2570C38", + ), + ( + "9520B1AB029949EC4F0BD73F2DC7B511F242583778025B6FD0E7CB81958FFE1086DF36BB7A39696808883B5A16F076618444846B307AED6961AAA27DE8659E03E677E19217D0D05D80AA46ED532684493A7DAA2DC9FA07BB2287B4A07FE00B5AD7994CC0D604AA30686922C56CB992516C9FBCF87A31C4337387612A500BEB9616680B2B1547396669239899BE7AB3685443CE9D5A40AB32501898C98FD173682126C268A19D24813171031032A8E17780ECB9CDE40C36D3223998FA7D3B77BC99E945C2990523F1C07748048123243E26BB0278A588A7549050645F315852E891E2939619A84D18143213BA64E4E116315CBFD66A691ABC29FABB973EB0AA18169631976CF4CA4D36FAADD06CCE8BF2B15D851F74E622C924B921546289459C4F680401162D7BC10E2333B25C34BB4F8038E4EB2E6F2174580A76086CCA02C0503F506649E17C3C867A05DA66844402BC55AD6BA521D509059E6907C60A43E13B3AAA572048978E052179367C0E682C9C465A22F9B8363F634E857C094162BAAFD90E0177442AD81A2C83C1B872165AB5B5D52BB8C194A6A074B3C7DB1E7066925C3988615992C5041FAE24C44120CE6BCCCECB977E5F39C957570EB0972719E1645B80A6F9F071F7722071DB0706129DE5A1523B1A641B1502750842BE8A8905923DF4E77755869F55776BAD53947E5560735CB71643259E56367D287D48A1353D88224A280A0537643EA66FA133837C0165EFF6022CF634861299C0B3429CB20477517C51F60C127344E58C5B2AF7C05B141C73E8128BD5839FCC4F0DC3AEA1C283EAF3C15AB82685F4C8C89B73FA3C5DBB128B524C1D801BA080C93F8A0921848C5725EB46125844B5F1488749B85207A16100A4B2502F41054FC5C4B5433B6E5D37AAC7F3334192AC6ED1924347C0A4ECA08CD040DC15C083859C55241BF1AB9F5F43547D728ABA853EAC12A83019072A993D83179FF5E9C4D926C2E4C8A3EF9983D4401871F7109FB12251B4C865A987BA584E7E2221BAE52593269F7E149BA9FA8820B85EED036028092B0557A7BE27CF9B09CD1A1A5CEB768BAFE39E617A661C3B8A0748109002A21368616C67C211C252B3DB377BFC947EB22010C9C36297B45CCA2F64D626E43A9AF646C68AF5875099141D02701A94C03B980A37B09071F20DA6A1C707993C5757B5BCDB02B5F01498A761CD7439B8DA9880C3651D00356BE84AF95052941805EAD56479A66A4BA672C8A7A629429DA75314E5933C8C149033969AF46CC99DD4B1FB7B52DDF519BAB02799869EF14B83E085667716825F771D9E80B2B19B3412FA850FA9B19ED71B99DA4B1F6BC80B0AA5FFF77A53E77F014449877C1E7823912B979AD2516821A349D071B69E5382432BBA7E10C9B2975D80EA9236B35F57C869A21B5C194BA32DA3142FB58B9ED78608B3C30E8C29EED254CFD4BA012B7630725B5E6A15352C80B8DBBFDD62048C9695264C5E1864044ED79359666A9CB2642ECA5D80C9058AD70AB726000CB342334074C1967BCBE2319E83AADCF07733C13F25A4859A0034CB016725C81D0053A9E1FA605536489F23C71604A086CA3C60220DD9236213A8C103B9CE41463CAA792A5A320FCBC83B4276A1FAD411E2D9168ABBAC22F1932995C9D99420550484F014370C9186BE1C3EDF1C40C1A3411672B08FB104A5C551E20A2D3F4B791EB3A4B8E1CD48AC0C7734C4117B53CA7C8B1F0C697D448F7BA19EF4E90381C94257AC370FC15468C86213B7C6094704EA317EA7679858562F3B45A412B1C92BD37934951D2DEA27D3521391434A143CA58A904FC91743ECF47CFEA14B67B773CB7989D281C61D88751AF9C3A0344156085B7EA74CFBEA67D862C0561ABF70E17719C44483957D52580100C997F4F0099A98204E6BB7CA08BDEAB072DE920B70226037C31728F18AA19504158A46FDE9C42A9272B5CB525A8259D317157CC802A002C464A2CC68D3BE6C790F52D1C3BCDA495D8C5DC6F50B8B64481B70213DD46CECF33908785E80EC9FF5D561436B7B3A1963CD3C460089678F0C08998C4A8CA37D5B1624E85C1CF2911DFF7239E0328A76FB162811CF10904E949B09A3499DB3C9BD0FA1B0D935B7C455C8424330B7075DA6B51A17E283DD8373527C770F193116D08254D87F7AD7F57F47BFC3833D8C6CB0E549E6505914D07EEE50DADF41B39D3907E38E3E", + "85962FDD6D092A8551968684B9C9EEEE736ADD8877D876C8EA62A2D5CC83E147", + "851F92892D2E0E11102DBF8C99F61A5B7710EC70E41E677732DAA983CE86D0C396A1729D72217BA452C4592498024490D73EB7AD2D2F2E95BAA1B204E621EAD69513554261461D6740CEC83EB5344B55F5D11CBDD98752DC471C20C69F66AD51523428B19E56F7DA32F86E9662AF37CB65477B8058599D2077743ED4BA28E4CFB3B9D6DFBC895B11C7C81E0761D1AAA56A38CE776DF99064C1E3676C04D1E116120490BDED5AE01070A8614949F48DCA91082A9A809518040ADDB1AD7F5A9FA1BB5FC704D73CD66A0FE6C0B34AA39EC3FCEF531F66303AC672F5C9C6ED013A45167DA34553087AF1E78C62E2CD214D34536297E2BF3EA97B258F261373E936EEB6C4D17B7994542E4158BBDA941F5C107BA14B2C819E2AB671DB2A5D99A20294581B3F6D4983B820A4FDCAAAD28A9859ADEA0CC2C4CD4A415A91342387F7909C6904897EAE97F1A79D07D93F9A0597B89E3E80768A6C4B1B56025D56374E3AA67C1515B7FDB872F100CB844CD54B368FF5FBDE0E476378F2A683BF3875EA22AB4B46A83372AF9AE6CBE41753F69C98C65E07E7B9C4E8861CC2504916EFC70672CC567006342264383934BCD1000ABA678548F4C400E37F50E948FABD67895C3D6CCAA48DA27235BF1D7BAD8804FE0F32F66CC112F5171139D37562349A6507720DA44DDE0133122275C1905613BE83A083CBA43D1BA240FC7BCB8AB589BE229D3A219E029FB4306444BCF76BC0C9B6408C1E228E58B8B5872FD582040618B056277B659637938BCC2DEC198D27F2FCDA216C8046D74B3D035332CDCA51356BD275EF5FF9AB6DB9C2DF2151D9134EEB12421B427246F287761402A0B4D13D9B176DAB7E91D36723D0DB6A9D0FD25A4D2059327418BC3087ABE250B2A0ED03765758531F522C74A38EA705ECAA1100AA385B8FAA9AE4CC56D8E039543A1F51B6AD4A749F8C5B2A8472C6D4833A0E51E2475112BF9E079DAA72B36594DB6F5845123576DDBCD0AEECCC9E2DD4108FF2EF6C4701EC385A6E5F5412974D9DEDC635C3496EEC4DE3823CBF3EAB43798E37791F65585313E34E704A88B677E900AB8615D4DE9ABC7C3F450A4F71EA7D533570229FAE1C4151176FF303849D6C422A7CDC94D5152A17907E1D1C966B8357F59512BFE1CF1F93F6B15705C43579FD10C6C5527C07BCE41826C3B007688E2595E0FCB980602DEF50EE0426AD34218FF43EB5F1A049D33479A9C4A60F70900AE8F8226A4CE8BC6841ED50E375E563FB66EA358229BF2BD4A310EA58794AFF3572FDDEE92EF5D15BAAD8BC702E0ECEAEA0DFCC8B761FD671E75D3354548E031D16A5DDB45EC7B570A4A169DCE11ECB7E9A5F71236DE5837DAE1A075C258134C281DA01C7FCF15D41605F91F9D16C4B8BFC9291040365A623F61407F22F6143222C1B16D0D6C5E2C1F243E241D76BF3608FF32701A93F4C50F27D6FD7EBF8F502293433310598AA16D1C0C1C453089A622C2E6FD91D17A217853F942692B5748CE80839AA05E1BB31D2CF9AB3CC9BEDE428374DD9E99DA6441BB6BFB50FB5E850FFB463B7DD980C2DBB8A5CCB96F669489E553BE84D233E0316C3940EED75C172451CB0AF4EBD2B9CB05E3CFE292D0F106533DDEC77CAA49358FA79F3E9B5B609AFF64E3D1085A4C7D1E25FF47035E3923DAA1E9F5ABF700663D2A5F1AC9728FFD552404B63EBE406207ED8D1E0D8DEF1C761FC87432067885887F4E05136BC497B1BBBD4C0D36BE054108E90AF353E4296AECED037A7EC2CD99FAA76769283524324651CD18CACB401FA7C556EEC851B3F4DE090397C1D5893EBA09ACB224F18A0BBD5A5AF04D2BD1911288FFCF3B0C774EDBECC3C9E7F38723A07AEC75EC7F4055C353599C31B84878CBAFAE305E6DD9AB3B60A7A2EF0186639245F8D8EADEE7B54CA9A52ADAA9930B43CE4A7ED5AB1B2440C488D56B00A7C758F329824BFB7928470A3E3D0E8DC21955BD978BCCEAB0DF503EF4CA56DDBCFF8794F17D9A24F19173FB6804DA17BBA07DDBACCB483D3AE1DC8C4108DB95FD4E2B7D62B047C9859232E8BE14CC84B42A14F6B63450D981C30DB3FF0A62EBD7BDCCDD728D94CBF946B143BE39D73E28AA1EC0734E28E6D22AA3BC9855235BF6EB041A94C91C4B5FD274B4D09A284ABDD623D94AA6225B3E090195681784ED210A19744DAE1053A2AA6E3DC5848964624FE1A7CF73E73025E9F7A", + "DAD9ADC089ADE86D6457A8833D41B235DE0579273F24874D689903199C296842", + ), + ( + "F89A32FDF01452B2ADFF6028F73C16510347DEC0400E16242CDAA614FC9A5EC69373D2A99C500414026B5CB2B67995B21C528E43A1B945D7A796AC2C0EB3BEA43665909585D3452F898374727A9DB92322A8116E18B20E0A3328B029995BC4B320F930318AA4966A125E3A42A4A3624C4939D0603E8ADA4863B9200D0A78BFC5AEF42A96EF188F30B7C153F83695B0CE226C2DFDC84CD724AF310733E4A420BFC4A405248647377EA71A2D3249BC11D15CF0C75FF083B453C338A2C26978B1607F5CCA9CD93315E2AF0631786B2424AB9B69711B7BEB790C51314C3147865C8910D9B22233636478F7070AD6BBBE983B51798A0211CC56A89BDBC2C3A0C80E47030FF074CDE1F080481809989BCB3FE05348D717C27CC32A4C7760BBC9EED52CAF3099E4F9B43435A673AA80895566D3DC8B9D1427604054A4932F9D68C115CACFCF3C2CB25B22EACB45C57064B9BC0D55D508BFA101BA01C06DA07450810E1E7CA9238A9358AA8592E7A89EC25CF8B32ECB81A38773489B4A6ED6F85603358F8F58833600044B488401EA9D60F99F4F58A292337124F15F47EC8C67DB7AA1098551880AD1D21736F83246AA921F18AE1D0529AA274CACF703C1E9394044B0DD3A0C2F776989161026729778435FB72AABEFC71AA96CB53F5411A4740202A177FF13AF362024B53AAF43395B53DA505FAAA1CA42297E968D27F1338DA6698C153A340A85EF6831991BAB36E3CB5B02A33B6C47B76989615C1DBE080639CC2A3C1550DC5C83A982A9F8A391071C6D74CB25C4A7BD7815BBEA662F5C51935ED501F8169D3005BA5E51C54053851545A4C22A8AED2AC7EEE60D4CB494EAF30B31504AAE91A085AA5F6A34ABFC2741C414952446BEBDF289C76C3EF6D25D47AC2D7281690B1051113A64536B05DD1936469811FC5ABB4714C9BAA371B00413E672AFFFB9AFE5A61B8B5758FADA01C681937BB2C23359A0EE9A0A9968994AAB8C7052A89454724409D05AC18ECC2151C534197028CABB521D3E929C54FB9AE61873C7517120766725B25078F414E8B2A672D85F23577ECAE849316CA660BA09EF7300F7D77E01F513BA4B628F12639DD9700EECB79997500D2AC36399917B05A433E51C28929C4C8A091F3C3239B6891DA2759EC004E6D9CDE60C527A31CC04B2261EA686BCF86191B18A7FE97BC4D2083FD71A914C52A36564F6C41BB509C76ED34C3D7C9502BB1570916E255532663939534A1A986C8F297ABB10799A0CA508E2B7358F961D0EEC71BAF67A32F953511499773651FFF526F4408EE11939D739B88BC97551C21BC2C665EC49BA5E258C2C3BC4763C361DDA35D0F35B83CB54B12C15BC894DF1B399EFC193CCC175448C71ADA17E48828BDBF09ADA393DC9F90FA0790FD9B8ADACB566BB23B4298900625C78147C2CE5460B388739223AAF2267A3309C635DD9486894263AC40F86B1222961C717C341BA514029DBC8DB0546CEEC03FE4A733E17345B240437E38D6A6676C5613480FCAEB29A5492B7839C000EE6720A72FA92582745499A4531218D92CA480D5565E15B3B65B01939636F2BCA54B4A855991664BE4A1293424E710787C6966F61C586EB559885FC819699088E525B815653BD431308424A50F30844161A01C331505C3F2AE6C95CA8ADF323310CF43965B2AD0CC770CE291827FAA218CB273947CFE7CB0838150C204520CA92C7B92373868B0B91C141D96B67F98C0888EB4B5A8695839C3E52317D94505BA4E654B6850C1F9A7F0156566FE462FA6C417AC8604F6B42C5B49C14E08CAD344FE64C7D787AC3FB03757D651AD49898FADA528D9407AE2024018C419D3C514DBC11099418C93235D4F823AB5A086CBB77ADBC92AF69C415A04BDCF1B82CF35B45B04304613767020372A145BB109FAD2442C495A650651F74C293D9E16F1E57485875A6E9FB5E16798090B41B5F30072DCC3C8A25BEB4E2B771D48C95B79BC481A5330A3AAB329C257A6126D8881FF075DF6C4554D621BA5AA865770A1B1CB24896B15E5A34D6B34F20D769F7FA293C13013643C89433980E8C9DE1A7CF718422814A4EC44B1658FC69BCC825969BC3AD9B7C8474511A58252246A7EAE2A8520B47C18A812A9B794F80392372340E564591201365FC9FA2D62926957F6F0158F7CA8725A61ECAF6F3465947A776EB3EE8202C718CEED80FC1925D5EAE3853A7A0C3F4", + "8C65EF16D1258A6C0705225DB7A5AC1956DD97D4768C5CD3FF9FC52998A9DF02", + "C923048E8DBFF58C2504A2061D9A8412C8A070B1AB07BD7B7CFD010DBEDDC6FE2AAC3D157EE7C56FE934759032DBB053401308F0956F7986A2EAADAA2D543E1DAA8496D4C3F43086DD6EF523D16E21D28EB63E0D4B7A22BD08AAD709B9BBE5D513E99BDB4CFC93D89B5638BFC0A58AF607109D273866F1F29ED2B5334ED884CF6716E3086AFDDA7740FB395057DB7E9A4AE16F87FD3A0ABF51055DFC2F9CAE85E5B264A4C0475F6DFC8758BFB19CB033D071E6538018101F3990B7BA0B951A2ED69707DD61AB3B27E70C43F31E3870416AE1A5B27D2083F544E3F327DD68AE18449D035A0809A59702FB7AF84302A9B1C3B1626951FEA1F918AE20551DDD938A842449FA4F475387085C2725BB282640D235FA8C9045990E72AA68B11F403C40AC4835050830800B2673068F1E164A2B0CBEA439989FBDB8EEC4F86BD147350BE0FE378C2A8D6A07333BB004EF367EF49E76832E758471291CAE425E8CE2B5AF5BA239CAB8E3E6A8A0191D0C9CDFC325466F02832059C39813D602AEC76B27C3A6238FA59A78289213A7FDFFFE0F1E48287400B347DEA82189FB0847D538D08617F5344113E4E3D255D4A1C3F55D10FB0403B3F9DF5F859E93E2FFC5DB530E0E44A52AD26022E40856D55714C580C7B9B893DC1D7E9FCF3FD4550F2D746E4CC281E151E944FAFA2D867F79A10B458B2CE0BED298CB2A2080CC51F6CBD83C02391BFFEB472940FABBC3FFE73AAE6C5A1057CDC4B984DA49A2EC9312594DAD1CBA7A21057117A0039F97FA26824FA9E5AD8350A4022A677BBE8BD423E81CD5A217FB2FAB858B745C9F3882F7AEECD141918D72A8F708B33D2E1C7F2FCCC9AB62AFD1BF6DFCE09262FD1E1F6B33DFFA19AC556F8168ED06CB39110D5141EDFC4274667E7F8D99248C7B73D001A31E0557802FD9915932AA029DF07C1B67DC429306AFFED25CCD2C230E5758719AFB043B742281D4EBE08BBE4CBF86E47E81E10669C992CF94EDB30539946859931C5F4F7FCA855361C081957A1871F695CB3902DDE061610FD1EB3A2B110B954A99EAF083ADCCAA47A521027076A05C20D325CF21F7E8D6AC6DA58438EECC5F03133B7F53F37EFED2604772A8D807CB182B81BDC77C0B7113DC5677954F58056B0D22C876FAA9712EADA80D0A6165E2FBC09882E4CC4EC7F222A88693D58DADCE94F1C1C19C4F40DAF7900E869741593458FA685B41FF9C667790FB096A60D98CA2049F42EDE9245F36E52722100855D1F3A31C8265450C9189F75A2986090FD324F62A43CAF94D494376A4B99C0C6DE5D77E316671AEA390CB412E094372A8F966D7A4631D42A44C8EE4E5D6C6ADCE93C573BB709AE24D6B7C6BD8E3DADBCD53DFD10C6BA7EA4887DE7E676E2FEDA3589EA35022D15B04709FAB44580B10D04C40C3519C5BF6D963A7929CAFF87413BD63EAE1D463C29BEA36DFF36C4D46477052B0DBB43ED78D3C472FE90AF0A1C8C0770B7F689B028F5C06A0E4EBD4A110C23BF4C1CFC171BDDCC16153D0FED91282007EF3924050B728610982D753281C5D0BD395123952C6B10EBF98F65E08741400AA09436D7D0015B71B0E7851E8D98EDC71281B502DD93C95E520F11ABC0D78D87F45E20E046411368CE2B494B765D2F6AAFCF1F89F4A8B6680265F0C5C597D3D0031BA33BCCAF421DDE1C6064B19F8EA4E93372B6984D525172E976C2677D8F178E72945235530A0CEC6676CB3427ABD090CA8870F3291D3BD10820F8D47EEBDECD784C06554E9A47FCCB9261FE09AE13A32A677DFDEC7206670BEF74DD09145F4AAF038CF12F228004677822F5BD24DD833DB12B9298655989DB96B6147106776B4BD7B8E4F547363AB5850BD685B13497DC98DB454701258E08C6017EB66C15CAD5D472E09A72EBE2738E1791861E831690830DA37F090C156D5424706736E53C1A94ECDA15C5C00C7079AE018CD708D14103EA224F3F07782DD59185C17D1EFB39984A1EB6A86D5661C4B8AC328602E16FEE80AFFEA654A3F9D68F4CEB440B81E27976F4A4520D72E59AA69BA38AEEDD2C01495C2CA736202B4500115262787C6E8F179D1D32BDB168E66F820210D8028AFAFE2058D1450F71DF21EB16E31986ADEC5A1A30FBD919486EAB01C8F232B91D32D52F61961485E08EE23EE3AA533FF92041778005D147B2A36AA0231FAF1CAE710DE693C280694FD44203183FE90581A", + "640E9C78F8CC336881BAE27ABAF2B6A8761C3D902A7E0658D444F36D19C489E1", + ), + ( + "88DC9E381C952DC3A5ABD9B485D9198B644D97AA7293A70B76C986A58B673964050737810AC77F658B30A5DC4D6ED0533340B40CDACC8593A7F3737CBA319A25015C1ED3C25E0277B725495F47711E78BF3B76CCE7268E1694CA8EF80F3043BF4E854157375578916D0F0C6FB4ABCE3B730788B75C1FD0076227CEA787AF7F04B680A4CEE0E4A97821C5185126B8763562B0A4ADC45E57A217F6D250065A18591C0D7BC39582059B8AA5907B2C10E393B73F423964D29775367CC772892ECC11A8C81C35749E69BA15DAE56867C90819D0043652A2B706A55DFA961331876862049CE854B6E379CB94A849D170B5038B61693A254B076A55595042B74803475496723E4790BACA6AC3E93628464F6677BA892235345CA41C624E3B9A8EE5A890157B6517B075C65B6C0A8BA946FB524FF64AA3360BC0F4C258407A697A77BBE36CEC5C00D1A179C5C34F542C497499454CE1618DA95D6C478CBEB52D4550586081813761A245775538764F3EC42DA4CC01F93945C186B6C46C2BA503529F42688262120ABC27551C04F656515B7C161B9775F7296408C898CA6AB86FA1781F5988081777FF9AB929D717B0C63295637255A369B93656CFB9464CD260F458B9838040EAFB381829929DB77EFF79791D98AD61AB98D8F6694DF5014E6B00757A448509481E06361D3031678607DC7764D76225CDF5202494C43EEB861AA495AFB1BEA346CAEBE2B3E0A5069A3882DBC6181B5575948C76D2176D1B6746A6D8BA23A405D651C3294BCB3A33236D9068D416012F225C0FB62F9427B298B33C0D217919F534BFFBB7752674801B3946173B7F007B32AC822DA3A1CFE711636246B48AAA9F9C40370574456A89EBFB45565739868C810E3CC5728CAC33982024292033D34FBBC0B16C25A4A7C3C0DB085EFA293C3AA525FF1856D9E8676E8235958C3F4EB6A4C8C86B97C78E5D7022F74C085EA6925EA78BAE44574BC7CB2709AF112B4DD46AACB20453F3DA1CD5D651C5B37B46E5AC46591767B9222711C8DCCCC2B49723F351B4F6519871200EFD3796C502135F4999F74379A176B35F2B761FC2ADCB698227C0C93E4C45FB270E28FC3558120B92D52002CA38B4E7201C38346857890A2B1337B9B14E3A4EAEC98D221B038249A133E732DC224BDBE7967ECC5809C91490A90655C38A9CDB99CA2B552DFC3A1A230A7C0918FB08743E11346FDC353C663024FA34591C5002797E1BD662F9FC3685333226188F2EB1C599D2C8ACA8BBF53A9248C9A677E43180A27363182BFCAABC5065CEC7C257F300C1F6A2A0B160C4DD6A90106289F508BF26D0A3C0206CCD41A390C5452911C03AEB0B43E5628B67B89593122583C68AC847A1CA4048F31146E1B39F26AF91ACBD42E8496A3952F6015247142468C42B9D75550A684D0A2B5508508B0D312D81B40C08A06C8B6681D5CB697CE871AD15C3987542C92AC4C71C685D802E50F7471791306C29507795AB2C90996933C1CB576F680C86E7A321C5B829AEA9924362B4BC61B91DF16A97025C93A8544DDA6A3799AC0D81C9525AB7601382444B780BE4578F573DA2AABC5E032201B47E2798C815368F3AA4B17970747506B4CD861291F280BAB83BEDC253FB3164DA84455924C724F24E99B2A8EA61AD9D775D3EF516D5E65BA8304FE9660D829AA793D4C7C09804FED3B5298C16313A216276A09AD1185BA59819B65814398CACB1A2EBB9BAD874224BD92E79E693AF9487AE964D599186E449821DD081C16C6B29C95754740F86B07A433681C06B695EBB0189659F3F02A96BA9A792E22BD2856E5B4818FED815B40092CF4B0EEE20288F2266D765AFC8658913D7CEFA353C03C54A5BC671A8091C77CC67ECE5487193685D58C5B66C50A434BE96542D6202B51134107219B6C6E636F2F96521279648F1261BDB900D607303015D05C4459667C91C3539B08B8D667980DE1220C9D7722CEC5488A442E835B7E29000CF908D5C7587CB47BAFAD144C3C79828B9B4DCD140E9583E47F32B0E86A3DBF6087F40042C2A9612F73969080B35A0193D3A58E6F891222B28C351A2EA802F27AB79D5C76D29895AC6695A472615D07852E26A15840739E48C6C69CAC6EB1544D2D957B192C630B4A61E2527367B130B3C55AA6B51887BA1E8D08BC2291A57ECE2E03632B65EE430C5E77CE844C1BBDA7FC5DB7813E0E4A79DAE35B4DC59", + "2C29A4DF2135AD034D6172BEF7A0A30906A82EDFD7E1E6FC5E5A00C18B90CAD7", + "BE180A0AB0F9C995BE4A132A8AEB443B06871F20BA11E523B8D773E969F01993B736AA164938A19B45CA65CE882BE28737536850299094466CD896E913D1BFB19DEF04AAF36ACA42C7905593C7A985593B4975547110A26E278EE15848C6EB53A4A119686F8AAE0ADDC948C8C3EA4B9C57C3CDDD0D2703E6DA55E48CD014B65E17189E95F8EB1342BE96C69903B687FD904A0A3545927A92022CD564C1BBC81B907C88A2518DDBA16FD3617EEEAC93CF24682365686E61E18285EECEF9844A4E2A9841FBF3606B84ECFF32AC3540A36F986EC4D8DF50688F80CF73896E2380FF2E5BB2594A8F1F51079EFC121ECDD50E18943109BD60CCB25529ECBB495AD312636CBECB51696388F811E0EAD5FFD6FB17F12C2C41CF0039098F77ACA2C2C18E84474C2AB4614070145E786F4F31095DAF16D1A01E3F32A0E39A7B4A3050C530CAFF4ED10B188D7DA519B6C79441896E3CF954BB6D055C59E13D307B90653D1E712E1679249F4B0AEF1F8C821109977F1FFD8FEC705F18B0E98768BD5BE2F1E822608E01438A23706823A9B7AF23349C75D96D143A7DA1F10753967614D3C61BB4422DF5AE2B5555EEC97EF35CB859B108637AF1514998B7C310404794D2E992843DCE5FD7002A09A9183A0B4C3BCC4100A26C45D67C7B0B035C4B38CCB27BE3060D862E888BC0806D66CF78DFB48BEC69FB3FF477A2E78DF79359E4B740AE8F32F97EBCAF0DEFE57E80760F991884F564C2BBF779AFB5C0E475C1BCC7B904F2503B4D1D467BC40DB7857FBDB5D43DC8F0DAA5DA2F09A743546C6D75DA831549EAADD42B9A7C8F690883035B0163A811A748A7F8438CD053AD66B78CC8E68C44207620FC52B878E0854E8B32F14C1AF93D84B53D33DEDD915CBB639C70E57B8C36E2707FAE9BF0EEB9CBD92C381C2DB04F24197EC03999F6BF6696932D2A24DB7821808F80F324DBDD1E54C8D1F557FCAAAA16E6EB0C3A07B61FE130BF5313CF9285D7DE7FC0DC53770F3F76FCA52538F128A0CECBF16B5C67544AC6998088F5047A2074691346A457C0A6C06EA571F881A573F20571F0BA4938997D4B7B46BE93FAA96F984206F87CC0DC47DE66E84D0C66DA2585CD6F721B9BC0ED71EE491866FE54E6EAD69F666C7B7D2C4DE76E2F1EC41B7BAD71A9876F4D002BC263D113E479F926051463F5F51642D396428A3295486502DACA6C6EDC27AFA2F9A1590C3B436FF23DCAD070922AAE811A408A3155AFA02C4EE738357851FA190FC42F2CC46F88EEA43A565CB88D432EBDC6772416BADE9D49B68B53D783F4FCE20F7E7FB0533039C63A9F46C073BC2B811D730ADDFEDF24C59024D246993C0D8E6465FBC3AE84011FB14923493EC110F94393A4CB3D1A475F5576BB78C0DF7744009FE534753772D88892C71BAADAAC58CA9C038CF43006E1AFF5B3FF1FE095AA6F0026E76DC15FB1B95661B7952C52D535D18C9BA1832921002DEF8F1BA00C7D6396C0DE3E851A43ECC99268C169450FD96EA8BD988032FDE42C3877119AB8AB6E3115534C3F212F4F4EE984667F51FBB6C2C4B1D7AE8D4AC9C977514943B26C23E30E48D45415879306B41253F7C4D807601C096149ED9619005CB5C4D6A12B8282C5F1BA3F94D0EA15EA96FC63973A11227489E0D5C3CCAA4662240279C87E9F2DC498FCC4AF71C7569D2A15481EA98BD9FC6043AE722D1EBD0920F145E34641950C576D11385E6AA10942C3DB0C6C961494C3552BB533AE90EBE4CB5EA114A999475A0CCC2250C40670D0061FD08B16B627F5D6B44030A9C4B5DB9CBA6D7F8156F807FA22459791F763035844F1078E06E595731C487BF165F59910A294C9A8FB42115BF495A5E1EED6FEB180F36B766CFB6518F91A5A9111D5E1CA1D56471FEF1A8DCFB6ADD0A85A63A953673BEDFF64F93E0E7D69CEE76E07FF3054C4D8BD3DF0B76D3251448F6D1F89D7AFB751910627D88DA9A3166A5A847D274C7EDCC44956EC4ED73B762DC2931B47782E580E080E30AE50A539446355342571560D283D0F51E0728A7284680FA6EF8D4B22D15D48CBF93E631E642EE7A3366D3908C5343BDC00D251296E55C237943EADC3290B1023C8797F81F7ED47A532CD7CD3786BFCB9CFFF954559369E30EA309B0A7A39E8D2492DF848733E95164DA419328069E98CACE7F04CFEE37BCF8508E83ECB3CF091734DB8039551BFAF71A6492924E939", + "27B870279BABD8038A4B68BF3E1529FB88EDE8776388499BABF6CD371419FF20", + ), ]; for (ek, m, c, k) in cases { let ek: [u8; 1568] = unhex(ek).try_into().unwrap(); @@ -240,25 +476,65 @@ fn kat_mlkem1024_decaps() { // (dk, c, k) — 정상 복호화 및 변조 암호문(암묵적 거부) 혼합 let cases: &[(&str, &str, &str)] = &[ // valid decapsulation - ("E4B354B3103658F58F653293EE21661041C205B83F65236B4BFA3B2F4142ECE48F0057117A95119B0B381EB04D3134BF5D7112E7305E32AC956F18B9D2C687DA9A38853154F9DBB00C371EEA194061048C882958990866475A12E00843C3388EF9C57A5A7A7111156B71C07C8C146AE810A20B330412E11DF6F815C03981733B5ED47C230AEBC76AB671B02CC8360A9E30F162A57852548B5B83E32F55A77BC6578C0985BAAE813018137A5880BE719C23AEC5864EB96E5838B129E1762EBC92DC2934CE522F89709302F534D8758DC3A263D0A7CE17A965CF549C861389FFE2CEF21C3ADEFBB658778640588BC73452D5640ABA4C7262F6C314DB1D9B72490CD7B71234CA081388FACBB6C8B8B6222616D6E12CF7EA3449E853EADAB85DF712EA2121C3E39774695966DB4BF3E1C70EB6BEC1B06AEA3A8D2B44895F538CEC7B3305559EE6128DAE02815547AEA76123A3D0C4C9C53B7B25ADBA9485D70661FECC8CE75C88F7C242EECA1CA58C3804EA2C78904BF009810495C37777C334C18B156128D215C943853EB4A61F6A6B53AABA0901A0580FF9A34D028833080181518CD720211C2B7FEB0CC7F3CC4A3402BE3067212052C7D1933E22A893B6400FF457CC9347078FDC49DB4A6BC435497AA1888D2671D34550774C5EA0D7B7F28A322E376A8190C807B35644BC912DFA145BA2140535BD4A91062EC094E7E76D4586665A2C0867451D76D1B0F17264244A52C6FC0495B8533FD9219341567D3971CD0434D4C176AD72424D612B0E3503FBD986C255C994B7C42D46C5C8254055AB86576314C5B9677A75353C11AAADBCC590E4BB7268AC6319C0FD564BE5B1A94ED9012CEA934BCA1A8C038487E5A636210B9BF23E1D9A25E1ABCC6DC619398A0D13B63509C6690C2800278B7D29043B739229206839A4A5555DB694B0995979925D457C3C1151C95630C3D050AE8C79385BFC4E4F515342788260482367DACFAE98881B73A5AB616DC5920414343DEB8603D9296C44CC292FA5C9D4C590BE547326290199C939C73546B6CBB954937B625B28D3D017EC5098792534ABF7C3D3A1BDC4D22D79633EB9514C27A6B81B21076C392C35700218766B7F94A02D948AB63002D5700FDEE34A98002F5441CACCB3862C724D912B7DB5F847F0BAA69E714E34A6BAB270121975A7DFB17681675FD3CC7A614A78D9F322FC335BBEAC38B02A19AF163D06F74200F60FDA372492537159C0AB2F13692A574F6C089F562444FCD60F111A506C75B18E96A395150FA4AC775356C679C08BAD138517A9B994630060AB2CFF631F35A690749512146ABDD9C8268DF0583FD75FC0D31A15813238361CBD112F78D4CA272561C0345948965F2C8A13A0018663D3B28F2C8A0D2627FACC5C591B40C82685F4100832496225D19DECB838D263AEBF236E59B33C25BCCE6FDBC795B643D03C2DEA5C82E82172BBE903E4F2B53A86BE81DC41D74A663288495AD6271D6231C9F06B4BD9A121765DA9A8397F0747C57488DFA3507E0569D6D39990537012B26C298B3E7DA27081D0AA5AAC0885030C94BB418F463E98E2124F7721D2418F2BC5CA0858AC49924524873C1EC20376D5099B27BC835AB424C46B2FF142F316AA6BB1C0A92B0F67B68707C2863A5812C6CC0C88B92B3C8B4CC1EC34A2766E5B1B9EEA4B367CC51EDB5B2E359A563F54848B7674A33513636172F166BC8B3211140C89DA979DEE0CB6E60031FB4907D0B81A8E0999C7C301A1094CE0B8612185BCE6A8308FE10378F5AE6FD413049833E263287898BE233B7FAB6B4782AA19D063A3DB126EBE50A7F458A3DF3511FD315658B2417770A072449C16A89DFC20AC6B94C9F83430B25894F9891D718328DA407BB1A478072C04EF4318B745CC394B12AC0C3E493342F4C822DB117CB99C5BD024692C769D545B398D207096436123BCC0C3F611D065BC049CCAEE90566D60B5408A863F444329863547D331CF9638E25054B8A92E42038CC1388FA65947D0A57AFE54C893E115304C3A78A807872C550B7774DD22BD60806C0F57C7422CB247719CA47B65A6B3C0FF16B022B7815FA4A022C66BC3536301D91F90000CCF5963505769C96828EF013E80356C1C0694DF3A6F1BC8794BDBB00764C414736E153B4D35188BB4B93EC95C37BFA70E8F55239B6A1A6DB5230DD11D44FA8BBBD5C8FEA397736A254B86A458288ED23735D7CCB69FB7752F53722EA16DE41C5F3C71CBFF5A1A92B728C750520DC877A3DCB3855BA51C740F3E2769EFF2050A433EADAA364C4145EC20999BC70A5E73C69FB84307488CE2C116E6CB1C4190A7728A6647927E016225EA13A4669CC5A089452AF94DED501EF3C72341F4C2E868C1B6846C3D86CEA575B5AF5273B0A4591C7849CA88CA93CCB83E5229B2363C2D33436CE4781BA2134C3047F3246AB9AC0A739054A444CC60D724263A52AEB92BBE6CCA490A2240C76FE84A58F0DB3CF2625F4347991FF72319A642717A5556C73B97185DDB805713712517291DB52C2A98E468A852C0D1B527831AAB4025BBC6D1BF2A6B2D5A20587B327DA81204B56771131742B88C7CB907A3BC6C2845FCA65A609E5B9B132E6A6004AC4074CA0E1953212AB62B841842A452AE2DE0588B22BAEC68B8CFA428DCB993AF056AFED8745DF90737533467C194CD72AB39F694DE930C5A971109109EF3011DAA8A3FF6DC6CEEF03F57987F09B23D576C4F55E576F849AB41C4187F3C95CD7084B8D24FC6601746C73990CB7DA8507659E3409B709E4836043E473ED3871B953846A4666EE1D29253325F96028AD2B8B00770A2144A32876856E1D34DC251CA6B40C637FA46CD0A0232D2B8D5B94AFFD3248F7879CFD3048EA4CCDB51A20BCB9D1EF88F323CC24E909D29CB295D854120C00CCDE0883EE4BFA486889D05481FE658FE958273A89C06E6897C546824FB8B87F06AF5487884B885281A17E9FAAE223A6CD8667206271869232E084806755B11DB924B2EC99C26BB0285711D74D1C7208229A6E82938A03497E1016FBA846D29B79131CBB5686C1E465E90E33A0AE71990329D0C488612C83505A2B599592096E84E2A579C09899DC8405256C03355BC89A35A9BA075B596A644BD825280066798C50E437BBD556562A85485F7CA66A14B8084540E86D58722BB4013CA02B4428AC48724CB68B77E415B157B5F4134580212387B31BE7A76B5B9869B17C424C2DB76EDC81A46547E19DA263CC6983AE36FB0D07D972AA3CE3534C6C6418F684FDCF59A1CBBA81F229137A79516D2098979014C274B830C9381463A9BA4496C623E18A36DAE521A0C183C25470DE64CBD4A7956C69234F7616E00CDC5F5C836FD806BC0374C9A209F707888C953C2C4F3318C197119D0926B08A8CDCB0C5E848EA89381B2E248872280B6B28CF5917BE56B1E0FC3A291F37CC3387CD7E715CF8C2455449EDC103520C4B59E39AF1866205EA9A6A0C12A1F5AA5E9289EFE4984070BBF553B18F3593009F19AFA2460D75BB0EFB44657DC2F831B791833A2DC6BCA97417E5592882ADB071F28543CF1BBEDFCA19D7ACD363C4BD1029E3D659D89598E5121236403AAD7326B3385700E840BBA28B388E5A16FD882912A54997ABCF215255F024B6DB62A4723B228D5519AD7731B7C64F8F5322D628E4E9B03E0914D628C73CF812BCF0745615B70A0B704831306E032C4678C9978134398A40A0A369B9CF6AE2E054913981C176B4918AC7D59C19CCC7805A865CBEB8194223CA6C4A095588A54853487A4C74F582454AFAB306C02880B2C0896A522F5EA3F93D9A1692356D9AB9BE1284B8D6271892584635913EF08928F50AF5000BAB12CCFABD0C6CFE6AC3A3202360B10A6767BC4A281B9C27780A38AB370CCEE65C40493A9CFD56B9AD047BF483F52A6155C2C3588039E0A7B0D6DCB3239C366EBC742EE275A0805C52734CB7D5727A6512315249D31FC2147B82E2ED07C0D31033C194B3D7C78DF98A060603A2F3A126A493A9E63311EAA5F63FB26211410F0D52304B35B9C104515D23FD8EB485132B5D55AA05AE5754359763FFA1EA4208868E6ABD4EA19CD052AE5E490A5D7B309B88FFF788318D060D4841C7B7031F5726D3CD8066F702BC070A2FCC91809B17708FBA51227CAF2F97522F3CD508A8FF460BA1D453F11790BCE7604CE254935A2726352CE7A14B1CC26342F6A8F9DC341B4EB7D6EB00C84C56EA6E8B948F9B10498683B3B737CC367FF54CBF729984E6C66A2E00130F28DE5E2AD39471A4DE910CA9A1C386618B5E6A8806CCADAF230D6D765C7120A68833DFA5A45E6F6787E802266740333685384772D5924F4DAA74B421D42C31A2448EFB0B174EFB7E3EA56A579F85625F1589138BC6A1077BBDBC56551E9852973E8C8D78765A165E2E0400B555957865B794E22CA92EECC1857A6C4049902F0F5C3AA95DD774D628B67924499A23C6F1CBA", "91B411C6C8DE923DD689138E85D8919624987853AC214C821E7A9221BA6EB24CFCA62D997E9861FF34BD9C1826A5933E326CFE627080AD0491F62845C03B402977349855D79C15551BE0170C9A2A93952A9D4E3EA039E1644567D1CA6017BCDEB89343DB32793355A0454CD6777CC08C47146FF02C9666BF9687E032F6CCCD1C44718031508B03A3FF0648FD59B7CCA2C09DC4F3DE90A61BC73731F37C01870F96F69DE032970C2A8CE803A76EAA514B64D9174E65C6FFE0DEFBE34E0DC5C5E90359B2205431EB2DDEE399BE7C24DB27321502D0798459A77444B9AA68FAF7E9E35E11A3799B1D01D552F7CE0EB82E71130D52DA6D1A9AF51EC46D94327DAD227595ADA79A13094E8CDE25441D2ABE3D2BE3514BC2C4DC25B5C0FA08C2E5B0ECCC0DD5C565D2DA22DD70120DA74E938290989225D034AEFE483D650152C877C37B66B9EB145367D23537117F52A58C4E6FE747036C12235D921472EFB4232687BCA48783DDCEFE436256A1A37EBD894DA918B5117FDF80493266411BA9DAF489372282DC99824F9782F226FFFAA905FD71B4BE1EB1E18B308AC9A601EC2E0138E077E0D18F76D6907BE0340CA00516D3F0C2C260A12C4AFBD3955B5465E72416709A887DDAE8F412FD0F894101C82C8FB742426A5941053AAA715F89753D7FB5AE79625E58B3ADB943106F0701B5AAEF7AD359B3DDCC963B446D8D8CA091FA13F68A2682FD3FD04EDB7C2B12BD791B39113FBDFE2862387D61DCD88CD47F240C293A89F994DB2FEAE33FE226ADC2C1B81F55290F9F8FA44782B009AC84F4FB35E20876DEC20A4C152ABC853E72BFEA5811CD02AAAAB5BC9A6AA2EFF4ADBE9CEADAE1FD3F55CEDCE3E20F95B3A05A0D3A112C73A1BA99457A0ECFB4049CB88565556C5A2F67B1DE90FECE15DFEB01A6AEC912323B506D03D3D174275A2BE623893EED0252AD97F15D83D850CBB38C8C901E9370C8E4591F4C29199388D581AE7474674FF25FB132DB0D31B82FF7B504EE95D6999972BA3A7EFE180C3BA9A78DF242DA7AF6AD4E35F9C0639629835ABFA123C065DEEF398E4E2D0D2ABB59B9C26192A2FB2D9AE018A92D120297BD88527D6A84E89A1A6D1550D77653289E72DB935518CDD69336013040025C72DAD96F316EC37618ED6AD626C83E0DF50267C20DFF55D34E27CF359F77FB19C9C80D6569961CCC62273931F1E835CAC96FC60BF5BC94A38179E7517697C5A345018743AF1512D298167F6EADD9D6A0C4C6D026F798533221109869CEB9F0BAF5FEE25426D970131E119EC07217D7972C82AEAA3CB6987FF72687E1181CC4A09904837E5DF55D587386F32784A9A698139C247045E2DE3DF9678CBA80368695DD8F2202B25837E396130967AC97DB24FB6C0A3E7E8AF729DCA6702A413E3F91D043505C5794C0D7802213C5BC2BE75437F2435F56500B5B13EE5ED02E403F8029B73534D22212882CD4CE47D6BF3C1CAB6307A54D4173F3E8460E4C88911607EFD9DA7C211D771F70A5603F737902CEB54FDD9762BBA0D1329F5D2500865E21D642842288F9B436C5659609501C9272637ADE2F8554D13E298580590B9BD671CADB32726F345FD2BBDEA1D10DA1550D15E05C9EB993C6EAAE924C675734D6E0A327E0B16D67663066FA4ED40C6C80BE8034020D9269BE72FD495601DCAD2B0E81A4BF5DF23B105BA02660D9B44C3DCB0909644252628DF97ADC86A0FDAB739886145E0C8F9267B80E1DC22360F5F31693F9E7B1B7D61492D61AABE7CAE2F4016BA3A955B223294894BD76811B257447442BFC16712B6D567A9FAB49022865D01B1AC9CE19B692633759BFB021273E1768D53BC7A4A77F146733BD8B275BB72D05E07590ED1E682EE17AA867DBD42969E49CAB16521A6E8D9320C1F9F3F0168D02029AAB43F2EE970E2C45A980368DB21E88E9647C430107D281B0AA1F8D45B3E3AEFCDBF8AE7CA416BE952BA117BABE736E766F0D3FBE460FCD2E99B013153CFDD8ED40C6ECA243D41B6F80B0EFD03AD458CE0B70BF99961D7FB4AAB9B1A6E4C1FEB18943292923E8462353EC2A85D84B2B386CB75507D01D99DD9504A4667438663E27774CF7A888BAE219D11ED51919D643F7AEA8213639605DEE93A77FFC0F1E13F13F374BA6BE0B7421A71906C05C15508621A5FCDE99E62E63FA0B83BD590490A589B97BCFBBED391D28E07F76752CDAD3F6", "2FC3B59E84AF82399B9BCA78A25E779857F2E543C4711AB974E28551FE28C048"), + ( + "E4B354B3103658F58F653293EE21661041C205B83F65236B4BFA3B2F4142ECE48F0057117A95119B0B381EB04D3134BF5D7112E7305E32AC956F18B9D2C687DA9A38853154F9DBB00C371EEA194061048C882958990866475A12E00843C3388EF9C57A5A7A7111156B71C07C8C146AE810A20B330412E11DF6F815C03981733B5ED47C230AEBC76AB671B02CC8360A9E30F162A57852548B5B83E32F55A77BC6578C0985BAAE813018137A5880BE719C23AEC5864EB96E5838B129E1762EBC92DC2934CE522F89709302F534D8758DC3A263D0A7CE17A965CF549C861389FFE2CEF21C3ADEFBB658778640588BC73452D5640ABA4C7262F6C314DB1D9B72490CD7B71234CA081388FACBB6C8B8B6222616D6E12CF7EA3449E853EADAB85DF712EA2121C3E39774695966DB4BF3E1C70EB6BEC1B06AEA3A8D2B44895F538CEC7B3305559EE6128DAE02815547AEA76123A3D0C4C9C53B7B25ADBA9485D70661FECC8CE75C88F7C242EECA1CA58C3804EA2C78904BF009810495C37777C334C18B156128D215C943853EB4A61F6A6B53AABA0901A0580FF9A34D028833080181518CD720211C2B7FEB0CC7F3CC4A3402BE3067212052C7D1933E22A893B6400FF457CC9347078FDC49DB4A6BC435497AA1888D2671D34550774C5EA0D7B7F28A322E376A8190C807B35644BC912DFA145BA2140535BD4A91062EC094E7E76D4586665A2C0867451D76D1B0F17264244A52C6FC0495B8533FD9219341567D3971CD0434D4C176AD72424D612B0E3503FBD986C255C994B7C42D46C5C8254055AB86576314C5B9677A75353C11AAADBCC590E4BB7268AC6319C0FD564BE5B1A94ED9012CEA934BCA1A8C038487E5A636210B9BF23E1D9A25E1ABCC6DC619398A0D13B63509C6690C2800278B7D29043B739229206839A4A5555DB694B0995979925D457C3C1151C95630C3D050AE8C79385BFC4E4F515342788260482367DACFAE98881B73A5AB616DC5920414343DEB8603D9296C44CC292FA5C9D4C590BE547326290199C939C73546B6CBB954937B625B28D3D017EC5098792534ABF7C3D3A1BDC4D22D79633EB9514C27A6B81B21076C392C35700218766B7F94A02D948AB63002D5700FDEE34A98002F5441CACCB3862C724D912B7DB5F847F0BAA69E714E34A6BAB270121975A7DFB17681675FD3CC7A614A78D9F322FC335BBEAC38B02A19AF163D06F74200F60FDA372492537159C0AB2F13692A574F6C089F562444FCD60F111A506C75B18E96A395150FA4AC775356C679C08BAD138517A9B994630060AB2CFF631F35A690749512146ABDD9C8268DF0583FD75FC0D31A15813238361CBD112F78D4CA272561C0345948965F2C8A13A0018663D3B28F2C8A0D2627FACC5C591B40C82685F4100832496225D19DECB838D263AEBF236E59B33C25BCCE6FDBC795B643D03C2DEA5C82E82172BBE903E4F2B53A86BE81DC41D74A663288495AD6271D6231C9F06B4BD9A121765DA9A8397F0747C57488DFA3507E0569D6D39990537012B26C298B3E7DA27081D0AA5AAC0885030C94BB418F463E98E2124F7721D2418F2BC5CA0858AC49924524873C1EC20376D5099B27BC835AB424C46B2FF142F316AA6BB1C0A92B0F67B68707C2863A5812C6CC0C88B92B3C8B4CC1EC34A2766E5B1B9EEA4B367CC51EDB5B2E359A563F54848B7674A33513636172F166BC8B3211140C89DA979DEE0CB6E60031FB4907D0B81A8E0999C7C301A1094CE0B8612185BCE6A8308FE10378F5AE6FD413049833E263287898BE233B7FAB6B4782AA19D063A3DB126EBE50A7F458A3DF3511FD315658B2417770A072449C16A89DFC20AC6B94C9F83430B25894F9891D718328DA407BB1A478072C04EF4318B745CC394B12AC0C3E493342F4C822DB117CB99C5BD024692C769D545B398D207096436123BCC0C3F611D065BC049CCAEE90566D60B5408A863F444329863547D331CF9638E25054B8A92E42038CC1388FA65947D0A57AFE54C893E115304C3A78A807872C550B7774DD22BD60806C0F57C7422CB247719CA47B65A6B3C0FF16B022B7815FA4A022C66BC3536301D91F90000CCF5963505769C96828EF013E80356C1C0694DF3A6F1BC8794BDBB00764C414736E153B4D35188BB4B93EC95C37BFA70E8F55239B6A1A6DB5230DD11D44FA8BBBD5C8FEA397736A254B86A458288ED23735D7CCB69FB7752F53722EA16DE41C5F3C71CBFF5A1A92B728C750520DC877A3DCB3855BA51C740F3E2769EFF2050A433EADAA364C4145EC20999BC70A5E73C69FB84307488CE2C116E6CB1C4190A7728A6647927E016225EA13A4669CC5A089452AF94DED501EF3C72341F4C2E868C1B6846C3D86CEA575B5AF5273B0A4591C7849CA88CA93CCB83E5229B2363C2D33436CE4781BA2134C3047F3246AB9AC0A739054A444CC60D724263A52AEB92BBE6CCA490A2240C76FE84A58F0DB3CF2625F4347991FF72319A642717A5556C73B97185DDB805713712517291DB52C2A98E468A852C0D1B527831AAB4025BBC6D1BF2A6B2D5A20587B327DA81204B56771131742B88C7CB907A3BC6C2845FCA65A609E5B9B132E6A6004AC4074CA0E1953212AB62B841842A452AE2DE0588B22BAEC68B8CFA428DCB993AF056AFED8745DF90737533467C194CD72AB39F694DE930C5A971109109EF3011DAA8A3FF6DC6CEEF03F57987F09B23D576C4F55E576F849AB41C4187F3C95CD7084B8D24FC6601746C73990CB7DA8507659E3409B709E4836043E473ED3871B953846A4666EE1D29253325F96028AD2B8B00770A2144A32876856E1D34DC251CA6B40C637FA46CD0A0232D2B8D5B94AFFD3248F7879CFD3048EA4CCDB51A20BCB9D1EF88F323CC24E909D29CB295D854120C00CCDE0883EE4BFA486889D05481FE658FE958273A89C06E6897C546824FB8B87F06AF5487884B885281A17E9FAAE223A6CD8667206271869232E084806755B11DB924B2EC99C26BB0285711D74D1C7208229A6E82938A03497E1016FBA846D29B79131CBB5686C1E465E90E33A0AE71990329D0C488612C83505A2B599592096E84E2A579C09899DC8405256C03355BC89A35A9BA075B596A644BD825280066798C50E437BBD556562A85485F7CA66A14B8084540E86D58722BB4013CA02B4428AC48724CB68B77E415B157B5F4134580212387B31BE7A76B5B9869B17C424C2DB76EDC81A46547E19DA263CC6983AE36FB0D07D972AA3CE3534C6C6418F684FDCF59A1CBBA81F229137A79516D2098979014C274B830C9381463A9BA4496C623E18A36DAE521A0C183C25470DE64CBD4A7956C69234F7616E00CDC5F5C836FD806BC0374C9A209F707888C953C2C4F3318C197119D0926B08A8CDCB0C5E848EA89381B2E248872280B6B28CF5917BE56B1E0FC3A291F37CC3387CD7E715CF8C2455449EDC103520C4B59E39AF1866205EA9A6A0C12A1F5AA5E9289EFE4984070BBF553B18F3593009F19AFA2460D75BB0EFB44657DC2F831B791833A2DC6BCA97417E5592882ADB071F28543CF1BBEDFCA19D7ACD363C4BD1029E3D659D89598E5121236403AAD7326B3385700E840BBA28B388E5A16FD882912A54997ABCF215255F024B6DB62A4723B228D5519AD7731B7C64F8F5322D628E4E9B03E0914D628C73CF812BCF0745615B70A0B704831306E032C4678C9978134398A40A0A369B9CF6AE2E054913981C176B4918AC7D59C19CCC7805A865CBEB8194223CA6C4A095588A54853487A4C74F582454AFAB306C02880B2C0896A522F5EA3F93D9A1692356D9AB9BE1284B8D6271892584635913EF08928F50AF5000BAB12CCFABD0C6CFE6AC3A3202360B10A6767BC4A281B9C27780A38AB370CCEE65C40493A9CFD56B9AD047BF483F52A6155C2C3588039E0A7B0D6DCB3239C366EBC742EE275A0805C52734CB7D5727A6512315249D31FC2147B82E2ED07C0D31033C194B3D7C78DF98A060603A2F3A126A493A9E63311EAA5F63FB26211410F0D52304B35B9C104515D23FD8EB485132B5D55AA05AE5754359763FFA1EA4208868E6ABD4EA19CD052AE5E490A5D7B309B88FFF788318D060D4841C7B7031F5726D3CD8066F702BC070A2FCC91809B17708FBA51227CAF2F97522F3CD508A8FF460BA1D453F11790BCE7604CE254935A2726352CE7A14B1CC26342F6A8F9DC341B4EB7D6EB00C84C56EA6E8B948F9B10498683B3B737CC367FF54CBF729984E6C66A2E00130F28DE5E2AD39471A4DE910CA9A1C386618B5E6A8806CCADAF230D6D765C7120A68833DFA5A45E6F6787E802266740333685384772D5924F4DAA74B421D42C31A2448EFB0B174EFB7E3EA56A579F85625F1589138BC6A1077BBDBC56551E9852973E8C8D78765A165E2E0400B555957865B794E22CA92EECC1857A6C4049902F0F5C3AA95DD774D628B67924499A23C6F1CBA", + "91B411C6C8DE923DD689138E85D8919624987853AC214C821E7A9221BA6EB24CFCA62D997E9861FF34BD9C1826A5933E326CFE627080AD0491F62845C03B402977349855D79C15551BE0170C9A2A93952A9D4E3EA039E1644567D1CA6017BCDEB89343DB32793355A0454CD6777CC08C47146FF02C9666BF9687E032F6CCCD1C44718031508B03A3FF0648FD59B7CCA2C09DC4F3DE90A61BC73731F37C01870F96F69DE032970C2A8CE803A76EAA514B64D9174E65C6FFE0DEFBE34E0DC5C5E90359B2205431EB2DDEE399BE7C24DB27321502D0798459A77444B9AA68FAF7E9E35E11A3799B1D01D552F7CE0EB82E71130D52DA6D1A9AF51EC46D94327DAD227595ADA79A13094E8CDE25441D2ABE3D2BE3514BC2C4DC25B5C0FA08C2E5B0ECCC0DD5C565D2DA22DD70120DA74E938290989225D034AEFE483D650152C877C37B66B9EB145367D23537117F52A58C4E6FE747036C12235D921472EFB4232687BCA48783DDCEFE436256A1A37EBD894DA918B5117FDF80493266411BA9DAF489372282DC99824F9782F226FFFAA905FD71B4BE1EB1E18B308AC9A601EC2E0138E077E0D18F76D6907BE0340CA00516D3F0C2C260A12C4AFBD3955B5465E72416709A887DDAE8F412FD0F894101C82C8FB742426A5941053AAA715F89753D7FB5AE79625E58B3ADB943106F0701B5AAEF7AD359B3DDCC963B446D8D8CA091FA13F68A2682FD3FD04EDB7C2B12BD791B39113FBDFE2862387D61DCD88CD47F240C293A89F994DB2FEAE33FE226ADC2C1B81F55290F9F8FA44782B009AC84F4FB35E20876DEC20A4C152ABC853E72BFEA5811CD02AAAAB5BC9A6AA2EFF4ADBE9CEADAE1FD3F55CEDCE3E20F95B3A05A0D3A112C73A1BA99457A0ECFB4049CB88565556C5A2F67B1DE90FECE15DFEB01A6AEC912323B506D03D3D174275A2BE623893EED0252AD97F15D83D850CBB38C8C901E9370C8E4591F4C29199388D581AE7474674FF25FB132DB0D31B82FF7B504EE95D6999972BA3A7EFE180C3BA9A78DF242DA7AF6AD4E35F9C0639629835ABFA123C065DEEF398E4E2D0D2ABB59B9C26192A2FB2D9AE018A92D120297BD88527D6A84E89A1A6D1550D77653289E72DB935518CDD69336013040025C72DAD96F316EC37618ED6AD626C83E0DF50267C20DFF55D34E27CF359F77FB19C9C80D6569961CCC62273931F1E835CAC96FC60BF5BC94A38179E7517697C5A345018743AF1512D298167F6EADD9D6A0C4C6D026F798533221109869CEB9F0BAF5FEE25426D970131E119EC07217D7972C82AEAA3CB6987FF72687E1181CC4A09904837E5DF55D587386F32784A9A698139C247045E2DE3DF9678CBA80368695DD8F2202B25837E396130967AC97DB24FB6C0A3E7E8AF729DCA6702A413E3F91D043505C5794C0D7802213C5BC2BE75437F2435F56500B5B13EE5ED02E403F8029B73534D22212882CD4CE47D6BF3C1CAB6307A54D4173F3E8460E4C88911607EFD9DA7C211D771F70A5603F737902CEB54FDD9762BBA0D1329F5D2500865E21D642842288F9B436C5659609501C9272637ADE2F8554D13E298580590B9BD671CADB32726F345FD2BBDEA1D10DA1550D15E05C9EB993C6EAAE924C675734D6E0A327E0B16D67663066FA4ED40C6C80BE8034020D9269BE72FD495601DCAD2B0E81A4BF5DF23B105BA02660D9B44C3DCB0909644252628DF97ADC86A0FDAB739886145E0C8F9267B80E1DC22360F5F31693F9E7B1B7D61492D61AABE7CAE2F4016BA3A955B223294894BD76811B257447442BFC16712B6D567A9FAB49022865D01B1AC9CE19B692633759BFB021273E1768D53BC7A4A77F146733BD8B275BB72D05E07590ED1E682EE17AA867DBD42969E49CAB16521A6E8D9320C1F9F3F0168D02029AAB43F2EE970E2C45A980368DB21E88E9647C430107D281B0AA1F8D45B3E3AEFCDBF8AE7CA416BE952BA117BABE736E766F0D3FBE460FCD2E99B013153CFDD8ED40C6ECA243D41B6F80B0EFD03AD458CE0B70BF99961D7FB4AAB9B1A6E4C1FEB18943292923E8462353EC2A85D84B2B386CB75507D01D99DD9504A4667438663E27774CF7A888BAE219D11ED51919D643F7AEA8213639605DEE93A77FFC0F1E13F13F374BA6BE0B7421A71906C05C15508621A5FCDE99E62E63FA0B83BD590490A589B97BCFBBED391D28E07F76752CDAD3F6", + "2FC3B59E84AF82399B9BCA78A25E779857F2E543C4711AB974E28551FE28C048", + ), // valid decapsulation - ("B4306A44BCA1166395EB988AF887C87DB1A1B0E1A3E5B176D9831D02793796A02F1E23535521264529339116B4011BB8582145FA81AC2826A62EC526CEBCC7088724E211B2C4674AF04C014956B90CB86AD80B84C2EC04017279A9785B1B502817C5AE31FC772EE9CFF6243A3B661E7F8B298772A2CF7C6A93054A6C5C2D3E9A62E4C783551414EA7A3124952DE61B495DE75496C9000DA647B42BA0E9E38C5A66C57F3584F65B6851509F0F03569D23AD78D3C476D7740F0C9D2FA4814127BAB780B1FE4C505D96053175708932BE963416681189A5E99B708BC287668794282935BBBA958C401D46708EB8595BB58A7335672DB2883A04533D50B90E622EFFD0380F0778021624D82260BA5B30A9C276EC13500DFA5328115FF6F80F86048E23C2CCD0495551253CDC737C1B182346A64DDC79AA49A32FB3360EFC31CF9BC21255B5CFC3B4C65CB82693205DEE46BE44722048EA0B8EC699F0DB514B50CE694B5471EA903771606069848C68197A0B5C5CB94199B66E64113A3C40A35F65B7A1870B4FBA5439B099F5433C54291AF03B64FD2B0F2B828E01A1AD36C6427CD6932AD65D6325AB53178619427E6AC4B4E514110D332516A782E4E138E2B181B9049F8F4991BD4142C2E330591C8A669C32F1E913C957CAA7163CB3FC3C2890485CB5CCA337441A12998EDC77BF672D8D8160981BB12BE35D2C78AA3CB7882B00944351422C564BEDCCA480033D660C94A9A05A35C991A88935BB084768978E2C408AC51BAB47C28D6F01106BB8A233D3A871E6A24F6443ECF0B8E5901526C8A45C1143E5E5AC8322C2019AB059CC9A9F37AB21F93207F65A1EA991CD960A6E3437D9F325C9714AB1F1C7BCFA7EFD1C45E13CBBC322B9CF9A841124612CD88DD79525E379B48BB8103FA0B2041B515983C12581281ACA4B9DFA9EF93C8E59F8301DD61CE470BFD306A092C434AF02B778F54D2E7243E53922D34B9A1BF991E793B33F1AC691D9B2D2F7937668B3B87C47F4375968D93F75A03D3815C9499361301869E18322685322A6671C45833DB1401E37C7638ED2B870111A2C5923C8F97B8F66C7102CCCCB0A84A87477025CC209227F7587C16282B298AA27609364928A561BF7354924C3B85BC62923AB81841FF7F68BB8F99C2E8C48451C0C25C677B9B8C208A07AAD936A1516A5EDA52F1FBC57BAD12A50746BCF0B06D7CA3BBEB0CC4843A9586C9608885BF9C7A93408593DAB2AF5DC12DFBB1BDD9B0EC83C62D09ACDB076060035BB432333CF0324C2EB97C9926AA7520B7BBBCCBC95A4C0F1754CB85ED3440B0732CBA1AC9AC58BCF38E446FA16B44B1485888678143A93D026B1CCE20C8D0A5483A9A2CE514623142382D8C5D49B45301AA540592EE2776EC20889249B204371780E5AAE15151CC15AB91232CFAE272FE061007EFC4C4537087C4C9FAFF97D59C6926CFA4BA23964D99191E84218FBAA513CD16B7BCA1219C377B57C34CA1B5D6571C0D4A659BFA8699768116314191EBB5F96320758A5A160A35CB54161B4C8C877FA65EC4BC3D8150B49A223E56078FC573521A66C118291612354FAA71B3647C0CC6C7E6D531F7F7C1F0B8923099706CB01CC980A7964D759D3473C8C4458D7A0C62B192E3C052F0A905D663C23F9A2556C1975621A9C9F4A8D7205BBE20BC2FBB217AB69A8C21B9D5B8A4F886546258241EC107E491CB6D6880AA4C1C6523A902EFA672EA1674A5384C436691B02618CA98FA1A8CE3632861D881DF1856FE2C82A60D2AB36076401991DF874BD57E72140F4AB6DC5718D536036FA373B137E56043526D2416CE828E0D13029B64F0230774C092AB8A3CC0C348752F5925E78BDDE7223C0B11CF35936BFB5C370E353E6A49F06D23E3C14BF2153995854A57A0C89FE365576957A82C77D1493071C60B77DA40820483A33B413694A67EFE50E90B75595D083E4BC47F68A3A93EB7B47824C5FA586C82594E93487FF151F9A774B91DC754BB604DAC34F8A4157FEA51EE33627E824339BC34BE5ABAE248CCFCBE185357958ABAB68E352C88DF04D4419A5B2F90BFB8C5B041928ED4A26C5A92CB264BA42365A55B34111F910B66B4AB72C3F7B56351CB6B0D6714B4A014D2EEAB5B65259D2409F7E0395A44194E3366B87ACCB8377A18DC7C27C455A7155658456CD8735851C53C5E794BEFBEA7E04245874C438FB56125C9679D7580113D1A10440ADAA0B4383C404FFA369E5F4A063B45C8EF2784E0CA3290582FB680A2C80AD5AB1CBE61BCFBF032C5CC535E539A59BB82B26ECA8CD74BC240BB14E803A1B35C058753A55E0292FD671B383872C0290FBB01F72524AE5EBA649723FA6B09B6DD5C6CA800D1E65840A0980E2517223046ABF8A2E7C5369315A0D400804D813A27A2276FC6543816081B1997884470E2460244AF392B0968504A70420334D162ABEACB61741E1CA7B63494F39084CC431668864B97402B16780A989A7176AAC4D549E5DF7945B000B797625959190D8A6904973A89465644F98A511C1445E2341333578BA984F5670B8E35002BB3175A2421B3402564AD5210DEB04C1CA91F7CA9DF2ECCCC29A2E96204E857A85333260FBC899331AC72118C2D5D3A65D7363BAAB3AEDB31D08D922AAB026869C9A7B823302801EEDE61BC8D055D99667829C32459402FD1C2C58E458BC1C16D77124F7362B3607189AD83E00810613E97067CA9205924D0B3431286640C0E4406A2B14D2C097078A3AACEC8188B06B4BC0BE96D36E0428059DE82447588975B17EC43C3B218420AB9359E637814B841E6DCBB222B04094759DB54A9DC66BAB07567180F13F9C594821B404B4A726529C57307A79FC074CBB74A7DA8CA0BE441B27935EA16252B3091D86C79C8FDB0661B37973B15F8905001C2399E4D38AE6F071559B334D155A1461CAF0C17A4A2149F3408B3B19B407361E9FF4825CB4882C738DEDB58FCDDCA165D62FB9A06D1290C687E18C5FB2C18F2A964B532A7CE31ED3178DCF898A0AD46B78A75AAD4768DB0246AE39862BB71D11FC97B5682349DB2ACA337563292B8BE3A7AE067D2DB7476449060442CDA95B366D551E6CF83FB348CF84574E4DEB84FF07BE20AB484EEA426E18B1AC87C5E89405D218B2992C1FB4633AA43BCE61D3196C5C0E5B14CF8E936AAA0413EE6AB4CC2C9F164995BAF643D6B2441473185E356CF201882FD95A4644BF2A8B560019A1C751125C5B4EFBCCA9DE5A70AA32AFA5A57C7843C953F80B474A59C33077B04172B1B05A2F895AC1D74463569FDB50C747A3B25D5690E3775CC1C353BFF30696A2AFB1310ACD0B61C5E39EB2992E3770AE3DA3016186211B8B9207B075E9C12EEE4C602E341A95742C3ABA8833563AF1970D367472BD821D65C951C65AC92123A0DF4C171B2733569810CB2285DFF7408772117422C59924291A6337FB3864882B27A2555F7BD0BF6D8A17691BB0ABCB1B0300CB378392673441A026259EE08DAF7A62A70B6D3436BFB46948A319B8630873880C8A13F7ADC0EBBFE3C217C4463EC64596EEC5B1AA622B1F33211F399D8E5CA6CB9C7D0CA8CBDC5976B50626E6A07E10861A5819772F6199786B99D75B4461212110115117121CA6217C6F3AC51B6344970990E661C94696CCA04856F1A6707AF319DA63903BE9384FB9A51FA6464B6825DD47B9879C4570B919AE3BC9765484F8394F99EB6D5DF5464C2B810B9108E8582043C3035B746195957A0F2C900A1AC0D06892AD9613FCFA246A117760B78C4C2A7E1E0306E3F462BBA337A36C7FF6173AF5B15448A13D89C42BC6C1146DDB65B6096D90C9341ED1B4ECB8CA13EB10036566B889191A6855CAA959B6B40D2C818A990433EF4BCAEA9CB71ED34964B39F37A78823E467297668DCE00ADFBB48521A9E9A4B8B68E74A5CC6C80116CDE764356661AA0B47A2A2167D23E6A9129254C78BA32BC46F41862EBC69084F97954FC16E0292CFE1840CBCA13ABF192D81A5175AF6CF23B04E11FB24F5385CB397A8D1219DFBD44CB1305CE2A0B7F4B25A2CFA3A2B3A9C34A08C1A712838E39D5FE73F6A5AB319130CB4D29BDC448E849A1CD9A96BE8B1C6640C96227043B14C50629B608BEC7EA5E13C21FA1EBF328FD498975CE61B3EF87F9095AE9008216D36090DF71E921146FE4923112895BCB64B4D3A27A75506C3448F3C3A3456324DFE1C075DC3CB04C9141C9AA572595C8F078FE6A8C3B5BAC2D987521ED316DDD3434B8774F1098D875215CBB671FAC19F270492FDDC2663F3A2EEC7C9F1D839D3EBC0E2459DACBB572CEAB25389AA2AB1CC01C78876D270E3067F710B47D6475AC33821EC5DD4F34CB3DE989CBB20F81CCC1735DE903B5F92F9405E7281E8B16B88108F3ECA7BCCF4A296121CCD35D8AF1E9892ADFDE9F5DAA565877023AFD05D7A4C88A5422EE9FFE6D708D96C311C0B75E31A7D339872A4FCB254C2C7A8C23E", "B38AB6DEA7699D86673A0CA79722F5A3C063407D38F624084B548195ACCAD81AC9272510EEEB8EB5077DAA74BC6D3A81AE6856F3B9B328BA87951C003F6BEBAB1F7D82E593DBB77D720A47F2D302C4217345DE18ECDE850CCBAB5420FCC9E794750F8718AA015F5BED946266EE1E197E6D71D4145C0A990F6B57941C3050D8CD215B9BB29569EA46A9DE2CAFA669D53F407A339DC058493FC9DC805CD62C046BD3A92ECC390ED762261E82D66B304E14930E445AC84C91E312A819EE2C16363941F8436C71C080CE2476B7F7802320F680F6ABEE1647B24604AEF8C639B36AFF3F98EBB2AF9A96D2F93493EE19F94F20B6861EBFCBBC32198665FA1452466437E0372B9D84629579C55215255CD88EB4D5DDFCE01FA3990905CF19C3484E0BBE08F8E696EA338B670024061BC137657D908A2C9DF20FF36B961BDD406F13E2BCF2B009842B3BF1FF522BF080EF1CFEFB70E03B572CD25999EEAF9644AC375E44ECFA72FAD4F1F35B38787B27518F611CEBA8A3E1A35153E44819E24B0CA63105314E140BFCA81244DC2938A3125B51ABEFC96BEA2E20C7A61654856AE7C113D5AA59C4780DED19520EF47E7E8BABEB8DC9D724244FBE7E8EED986A7E75B5C97B254A2773E4FD087AF505B7C294F30389E938457790059399377C547890EFB9434210E46C1CC35C7EF452306AF0BB63FAF93D511788A446544C85375757242F87265ACF11F21E1BB2D7C564F767C86225AB6A3E5A4645C8E52BEB6125ED4A6A09515B8BC3CCE7A9F54DDE4BE85CA574C6BBC849E8A4C22A85B201B295C62C5BC394F4159CCF1BAEC594F9013AF623976EF62AFE5FC87E27E91A6F4131CE41ED196FF62841167F60F7FDB84A32D6289AF323A5AC791A1D64516FBC98BFF53FB72654CE3B1DE1D52D8C8CA57A925DC7C6A197238F32E22EB6678AEB7A77A986A8D1AB8B4F5CE080144699E982B651B2F73765A61BB85CAC9CA2DFAFE9655DFBCE73AC92BD735502B0A11DBC46DF6927C1AF154A11E66C741B45A5CADB4EEA646E1B3A81CE082C59FA0C51692ED8E55C8F5DAFC108941955F6872712FC80F930DCF0C0E6E228DA8801B63520D743FDAD7CD2448D9432077E4EF4993D2074C65B69B98869825330F42D384366F5A08F1BDE05DF2C5AC27F29965B8C9482CD7793226A4A1DFE67EBE9BCCA19E24A27DDA19414E9E9D21E5D8985FB8EFACFF343E7FA5416D899742DE5E006B14C30EBF264B9AEE843426D4639F71C32527BCEFCF2363DF881687AA81D17FA58C9CC5790FD23C6D49894DF0B75C8FA815C437CF4FEE67F12A519644674747E2E6220D0D388B651C83AF3EE269CD726C6F8BA7232646A98B474252935D6554A011ED1E50CE402E01A8A7A0DFB9AF46907634B32024A1005215CEC5A926F5B2751429B6E65568355DD8414D0855AFBB55242A150D2A7034486CB68408333FECC5E4C79C0CD1D06005DFED6464AB782E9D097AE1650EF54FF860EBDE385506BA8EF064AFACAA5193C0D1175A6098D5071B5A7BFBCEBE23F68CEDC71235958BC7F250122F6E0341D6A3519CAE2302902EF9E1518B3DD5862F33D0D5A953D9FDE9E9224B44F50B0B55041DCDFE3513CEDD7EE155B78638851D5DD444CC82D5FA19AE5FB275E452BC0849CBDC14CEAD4D848D8CAF4E8262DAD352BD4F7175C6653339814BD03D1234A0E64D6769254493EE84637F540FA8F95007688197CA2AD0931F7CD305CECBADC92C78DB5218BE04614C28E1A7675E0D33352914061A9F515A3752A18363A9FC276D988D9A8D567B833EE3FB419E3B21F84DAB6C24DF6045C072CD8E957608CAFF2D6FFE630BBDB3CD3E827413CB6B87D462BEAF30AF55D7C5E739AAF015E7D746F76D8BD066B7B7F8229DF9D97C0D25919F2BB415326FE1B9AD7197640FCEFF0FB3832EF5EB0E37C50111F56FF0AA1E885E0E80648E983B88FAAC15CEEB615325D4ADBDCD957843A30630FBD59C6AF4ABE46DAA6772A86904D4125687A6FD3FBB0015BC280F4CEF6E0720DB418B82BCF66008AB049E3BAADBDA0BCA04C8B725BA5C246D99C2708B4FB9207F1B8F4B6A40B8B9B458CBD0B0D56D3C34C20D3C20669A615C7104F1E3F2B6C77A8EBD8C37F5CBB06998608D0BE18079469F6A82B59507A2181D5C289AF0512EECCCE1F81DE52BE3B1322D72EF64BC88C1D60E86A338E09F8D7DE7409A0CCBA9C897971052EB9", "DC8F97D3C3F8B2744F66F336E73D7240F67626EBF61591C771A743FAF2AE9B35"), + ( + "B4306A44BCA1166395EB988AF887C87DB1A1B0E1A3E5B176D9831D02793796A02F1E23535521264529339116B4011BB8582145FA81AC2826A62EC526CEBCC7088724E211B2C4674AF04C014956B90CB86AD80B84C2EC04017279A9785B1B502817C5AE31FC772EE9CFF6243A3B661E7F8B298772A2CF7C6A93054A6C5C2D3E9A62E4C783551414EA7A3124952DE61B495DE75496C9000DA647B42BA0E9E38C5A66C57F3584F65B6851509F0F03569D23AD78D3C476D7740F0C9D2FA4814127BAB780B1FE4C505D96053175708932BE963416681189A5E99B708BC287668794282935BBBA958C401D46708EB8595BB58A7335672DB2883A04533D50B90E622EFFD0380F0778021624D82260BA5B30A9C276EC13500DFA5328115FF6F80F86048E23C2CCD0495551253CDC737C1B182346A64DDC79AA49A32FB3360EFC31CF9BC21255B5CFC3B4C65CB82693205DEE46BE44722048EA0B8EC699F0DB514B50CE694B5471EA903771606069848C68197A0B5C5CB94199B66E64113A3C40A35F65B7A1870B4FBA5439B099F5433C54291AF03B64FD2B0F2B828E01A1AD36C6427CD6932AD65D6325AB53178619427E6AC4B4E514110D332516A782E4E138E2B181B9049F8F4991BD4142C2E330591C8A669C32F1E913C957CAA7163CB3FC3C2890485CB5CCA337441A12998EDC77BF672D8D8160981BB12BE35D2C78AA3CB7882B00944351422C564BEDCCA480033D660C94A9A05A35C991A88935BB084768978E2C408AC51BAB47C28D6F01106BB8A233D3A871E6A24F6443ECF0B8E5901526C8A45C1143E5E5AC8322C2019AB059CC9A9F37AB21F93207F65A1EA991CD960A6E3437D9F325C9714AB1F1C7BCFA7EFD1C45E13CBBC322B9CF9A841124612CD88DD79525E379B48BB8103FA0B2041B515983C12581281ACA4B9DFA9EF93C8E59F8301DD61CE470BFD306A092C434AF02B778F54D2E7243E53922D34B9A1BF991E793B33F1AC691D9B2D2F7937668B3B87C47F4375968D93F75A03D3815C9499361301869E18322685322A6671C45833DB1401E37C7638ED2B870111A2C5923C8F97B8F66C7102CCCCB0A84A87477025CC209227F7587C16282B298AA27609364928A561BF7354924C3B85BC62923AB81841FF7F68BB8F99C2E8C48451C0C25C677B9B8C208A07AAD936A1516A5EDA52F1FBC57BAD12A50746BCF0B06D7CA3BBEB0CC4843A9586C9608885BF9C7A93408593DAB2AF5DC12DFBB1BDD9B0EC83C62D09ACDB076060035BB432333CF0324C2EB97C9926AA7520B7BBBCCBC95A4C0F1754CB85ED3440B0732CBA1AC9AC58BCF38E446FA16B44B1485888678143A93D026B1CCE20C8D0A5483A9A2CE514623142382D8C5D49B45301AA540592EE2776EC20889249B204371780E5AAE15151CC15AB91232CFAE272FE061007EFC4C4537087C4C9FAFF97D59C6926CFA4BA23964D99191E84218FBAA513CD16B7BCA1219C377B57C34CA1B5D6571C0D4A659BFA8699768116314191EBB5F96320758A5A160A35CB54161B4C8C877FA65EC4BC3D8150B49A223E56078FC573521A66C118291612354FAA71B3647C0CC6C7E6D531F7F7C1F0B8923099706CB01CC980A7964D759D3473C8C4458D7A0C62B192E3C052F0A905D663C23F9A2556C1975621A9C9F4A8D7205BBE20BC2FBB217AB69A8C21B9D5B8A4F886546258241EC107E491CB6D6880AA4C1C6523A902EFA672EA1674A5384C436691B02618CA98FA1A8CE3632861D881DF1856FE2C82A60D2AB36076401991DF874BD57E72140F4AB6DC5718D536036FA373B137E56043526D2416CE828E0D13029B64F0230774C092AB8A3CC0C348752F5925E78BDDE7223C0B11CF35936BFB5C370E353E6A49F06D23E3C14BF2153995854A57A0C89FE365576957A82C77D1493071C60B77DA40820483A33B413694A67EFE50E90B75595D083E4BC47F68A3A93EB7B47824C5FA586C82594E93487FF151F9A774B91DC754BB604DAC34F8A4157FEA51EE33627E824339BC34BE5ABAE248CCFCBE185357958ABAB68E352C88DF04D4419A5B2F90BFB8C5B041928ED4A26C5A92CB264BA42365A55B34111F910B66B4AB72C3F7B56351CB6B0D6714B4A014D2EEAB5B65259D2409F7E0395A44194E3366B87ACCB8377A18DC7C27C455A7155658456CD8735851C53C5E794BEFBEA7E04245874C438FB56125C9679D7580113D1A10440ADAA0B4383C404FFA369E5F4A063B45C8EF2784E0CA3290582FB680A2C80AD5AB1CBE61BCFBF032C5CC535E539A59BB82B26ECA8CD74BC240BB14E803A1B35C058753A55E0292FD671B383872C0290FBB01F72524AE5EBA649723FA6B09B6DD5C6CA800D1E65840A0980E2517223046ABF8A2E7C5369315A0D400804D813A27A2276FC6543816081B1997884470E2460244AF392B0968504A70420334D162ABEACB61741E1CA7B63494F39084CC431668864B97402B16780A989A7176AAC4D549E5DF7945B000B797625959190D8A6904973A89465644F98A511C1445E2341333578BA984F5670B8E35002BB3175A2421B3402564AD5210DEB04C1CA91F7CA9DF2ECCCC29A2E96204E857A85333260FBC899331AC72118C2D5D3A65D7363BAAB3AEDB31D08D922AAB026869C9A7B823302801EEDE61BC8D055D99667829C32459402FD1C2C58E458BC1C16D77124F7362B3607189AD83E00810613E97067CA9205924D0B3431286640C0E4406A2B14D2C097078A3AACEC8188B06B4BC0BE96D36E0428059DE82447588975B17EC43C3B218420AB9359E637814B841E6DCBB222B04094759DB54A9DC66BAB07567180F13F9C594821B404B4A726529C57307A79FC074CBB74A7DA8CA0BE441B27935EA16252B3091D86C79C8FDB0661B37973B15F8905001C2399E4D38AE6F071559B334D155A1461CAF0C17A4A2149F3408B3B19B407361E9FF4825CB4882C738DEDB58FCDDCA165D62FB9A06D1290C687E18C5FB2C18F2A964B532A7CE31ED3178DCF898A0AD46B78A75AAD4768DB0246AE39862BB71D11FC97B5682349DB2ACA337563292B8BE3A7AE067D2DB7476449060442CDA95B366D551E6CF83FB348CF84574E4DEB84FF07BE20AB484EEA426E18B1AC87C5E89405D218B2992C1FB4633AA43BCE61D3196C5C0E5B14CF8E936AAA0413EE6AB4CC2C9F164995BAF643D6B2441473185E356CF201882FD95A4644BF2A8B560019A1C751125C5B4EFBCCA9DE5A70AA32AFA5A57C7843C953F80B474A59C33077B04172B1B05A2F895AC1D74463569FDB50C747A3B25D5690E3775CC1C353BFF30696A2AFB1310ACD0B61C5E39EB2992E3770AE3DA3016186211B8B9207B075E9C12EEE4C602E341A95742C3ABA8833563AF1970D367472BD821D65C951C65AC92123A0DF4C171B2733569810CB2285DFF7408772117422C59924291A6337FB3864882B27A2555F7BD0BF6D8A17691BB0ABCB1B0300CB378392673441A026259EE08DAF7A62A70B6D3436BFB46948A319B8630873880C8A13F7ADC0EBBFE3C217C4463EC64596EEC5B1AA622B1F33211F399D8E5CA6CB9C7D0CA8CBDC5976B50626E6A07E10861A5819772F6199786B99D75B4461212110115117121CA6217C6F3AC51B6344970990E661C94696CCA04856F1A6707AF319DA63903BE9384FB9A51FA6464B6825DD47B9879C4570B919AE3BC9765484F8394F99EB6D5DF5464C2B810B9108E8582043C3035B746195957A0F2C900A1AC0D06892AD9613FCFA246A117760B78C4C2A7E1E0306E3F462BBA337A36C7FF6173AF5B15448A13D89C42BC6C1146DDB65B6096D90C9341ED1B4ECB8CA13EB10036566B889191A6855CAA959B6B40D2C818A990433EF4BCAEA9CB71ED34964B39F37A78823E467297668DCE00ADFBB48521A9E9A4B8B68E74A5CC6C80116CDE764356661AA0B47A2A2167D23E6A9129254C78BA32BC46F41862EBC69084F97954FC16E0292CFE1840CBCA13ABF192D81A5175AF6CF23B04E11FB24F5385CB397A8D1219DFBD44CB1305CE2A0B7F4B25A2CFA3A2B3A9C34A08C1A712838E39D5FE73F6A5AB319130CB4D29BDC448E849A1CD9A96BE8B1C6640C96227043B14C50629B608BEC7EA5E13C21FA1EBF328FD498975CE61B3EF87F9095AE9008216D36090DF71E921146FE4923112895BCB64B4D3A27A75506C3448F3C3A3456324DFE1C075DC3CB04C9141C9AA572595C8F078FE6A8C3B5BAC2D987521ED316DDD3434B8774F1098D875215CBB671FAC19F270492FDDC2663F3A2EEC7C9F1D839D3EBC0E2459DACBB572CEAB25389AA2AB1CC01C78876D270E3067F710B47D6475AC33821EC5DD4F34CB3DE989CBB20F81CCC1735DE903B5F92F9405E7281E8B16B88108F3ECA7BCCF4A296121CCD35D8AF1E9892ADFDE9F5DAA565877023AFD05D7A4C88A5422EE9FFE6D708D96C311C0B75E31A7D339872A4FCB254C2C7A8C23E", + "B38AB6DEA7699D86673A0CA79722F5A3C063407D38F624084B548195ACCAD81AC9272510EEEB8EB5077DAA74BC6D3A81AE6856F3B9B328BA87951C003F6BEBAB1F7D82E593DBB77D720A47F2D302C4217345DE18ECDE850CCBAB5420FCC9E794750F8718AA015F5BED946266EE1E197E6D71D4145C0A990F6B57941C3050D8CD215B9BB29569EA46A9DE2CAFA669D53F407A339DC058493FC9DC805CD62C046BD3A92ECC390ED762261E82D66B304E14930E445AC84C91E312A819EE2C16363941F8436C71C080CE2476B7F7802320F680F6ABEE1647B24604AEF8C639B36AFF3F98EBB2AF9A96D2F93493EE19F94F20B6861EBFCBBC32198665FA1452466437E0372B9D84629579C55215255CD88EB4D5DDFCE01FA3990905CF19C3484E0BBE08F8E696EA338B670024061BC137657D908A2C9DF20FF36B961BDD406F13E2BCF2B009842B3BF1FF522BF080EF1CFEFB70E03B572CD25999EEAF9644AC375E44ECFA72FAD4F1F35B38787B27518F611CEBA8A3E1A35153E44819E24B0CA63105314E140BFCA81244DC2938A3125B51ABEFC96BEA2E20C7A61654856AE7C113D5AA59C4780DED19520EF47E7E8BABEB8DC9D724244FBE7E8EED986A7E75B5C97B254A2773E4FD087AF505B7C294F30389E938457790059399377C547890EFB9434210E46C1CC35C7EF452306AF0BB63FAF93D511788A446544C85375757242F87265ACF11F21E1BB2D7C564F767C86225AB6A3E5A4645C8E52BEB6125ED4A6A09515B8BC3CCE7A9F54DDE4BE85CA574C6BBC849E8A4C22A85B201B295C62C5BC394F4159CCF1BAEC594F9013AF623976EF62AFE5FC87E27E91A6F4131CE41ED196FF62841167F60F7FDB84A32D6289AF323A5AC791A1D64516FBC98BFF53FB72654CE3B1DE1D52D8C8CA57A925DC7C6A197238F32E22EB6678AEB7A77A986A8D1AB8B4F5CE080144699E982B651B2F73765A61BB85CAC9CA2DFAFE9655DFBCE73AC92BD735502B0A11DBC46DF6927C1AF154A11E66C741B45A5CADB4EEA646E1B3A81CE082C59FA0C51692ED8E55C8F5DAFC108941955F6872712FC80F930DCF0C0E6E228DA8801B63520D743FDAD7CD2448D9432077E4EF4993D2074C65B69B98869825330F42D384366F5A08F1BDE05DF2C5AC27F29965B8C9482CD7793226A4A1DFE67EBE9BCCA19E24A27DDA19414E9E9D21E5D8985FB8EFACFF343E7FA5416D899742DE5E006B14C30EBF264B9AEE843426D4639F71C32527BCEFCF2363DF881687AA81D17FA58C9CC5790FD23C6D49894DF0B75C8FA815C437CF4FEE67F12A519644674747E2E6220D0D388B651C83AF3EE269CD726C6F8BA7232646A98B474252935D6554A011ED1E50CE402E01A8A7A0DFB9AF46907634B32024A1005215CEC5A926F5B2751429B6E65568355DD8414D0855AFBB55242A150D2A7034486CB68408333FECC5E4C79C0CD1D06005DFED6464AB782E9D097AE1650EF54FF860EBDE385506BA8EF064AFACAA5193C0D1175A6098D5071B5A7BFBCEBE23F68CEDC71235958BC7F250122F6E0341D6A3519CAE2302902EF9E1518B3DD5862F33D0D5A953D9FDE9E9224B44F50B0B55041DCDFE3513CEDD7EE155B78638851D5DD444CC82D5FA19AE5FB275E452BC0849CBDC14CEAD4D848D8CAF4E8262DAD352BD4F7175C6653339814BD03D1234A0E64D6769254493EE84637F540FA8F95007688197CA2AD0931F7CD305CECBADC92C78DB5218BE04614C28E1A7675E0D33352914061A9F515A3752A18363A9FC276D988D9A8D567B833EE3FB419E3B21F84DAB6C24DF6045C072CD8E957608CAFF2D6FFE630BBDB3CD3E827413CB6B87D462BEAF30AF55D7C5E739AAF015E7D746F76D8BD066B7B7F8229DF9D97C0D25919F2BB415326FE1B9AD7197640FCEFF0FB3832EF5EB0E37C50111F56FF0AA1E885E0E80648E983B88FAAC15CEEB615325D4ADBDCD957843A30630FBD59C6AF4ABE46DAA6772A86904D4125687A6FD3FBB0015BC280F4CEF6E0720DB418B82BCF66008AB049E3BAADBDA0BCA04C8B725BA5C246D99C2708B4FB9207F1B8F4B6A40B8B9B458CBD0B0D56D3C34C20D3C20669A615C7104F1E3F2B6C77A8EBD8C37F5CBB06998608D0BE18079469F6A82B59507A2181D5C289AF0512EECCCE1F81DE52BE3B1322D72EF64BC88C1D60E86A338E09F8D7DE7409A0CCBA9C897971052EB9", + "DC8F97D3C3F8B2744F66F336E73D7240F67626EBF61591C771A743FAF2AE9B35", + ), // valid decapsulation - ("6089C5991C02AEDA3E58C1641BAA81C60892A10629E685B8D738076B5C7CF5CC7C22F7A5B9160D753A27A6A89E09447EAC260F32A52090D81CD8FC2980802786117803B79C34B3355DDAB65188440696314B50B01FF666251B4890A351103B6550CC8E4118AFE7A5373BA04747712B5C08BED91A207D241E0E52686261A1B70C0699004B1FF5573976ADB44170E8141EEEE8A60F18B5D7AC5649F134CCD34066DCACF9116CD4F73880B74978F3B890985C42175DBF8681461A27E21222D3D908352902C515226817BCB1C1845CD6CED354A8C42838B5F21D56D484AE085BE0AAA6B40917B34A86FBB28CBD8407C5911194191BD568AFD5CC998000878C922D94A7092EBA29E48909E53C8DBCF3CF438097573442D9E42AD35C08FBA201CB8B4E9A167F33988DCE521E14902B8810B05D0C5690F86E0B01A2DD604001438BEC40BF957468C4032D0CC71AF2D18716387A586215B4BB6940881AE401CC054010C74634484B86D4441F92F0230794319D14A0E1D220AAE02B132A74943C2936AB3E7E2626F71512BE180D63A0CA1BA5294DE77C5C5A12EFEB546BF48BE8BA6A51A015420249A25533DF2A7917765779C62AD3E0B472E87AE9B3406EEC22CE2478128002E0478B8518BB285034ACD141AD817B05036A9317BBD198563BC066002B7D68716ED4AA138C271B556C9751708C807685AC304ED4574B0B477A4BD3088096B51CF9B8985A482C6288B1A128F603B3289116B4E047BA86956D426C38FA1DE57BC267136E41901CACDC5ED34729A1FAB5C00829A2697C368148B92C7775EACA83AB3C5AD5637D7C2084FB2FF3526D5BF9086DAB1AF58A3E531B50DA3C765FE9894CB516045A8B7EFC0013738F978120489B72BAE41DFDA636E5E208BDBB29F17BA6CB510A3EA2ABA59A0703EC1DB9058D3A9750B267348FE77FD76B9E24755A5D9433766C359A71463488B55FF2390C39A842FA63B2C121FF0A76F6E97A8AE2C7B6936E662C4976092896A500F533C4079341A39CB43BAA89BAE16EC7D5818DC442E982295A9A10B2E9473061B7634204D0B37FF4935E64878D3F601F80C686E3FC0A0523154EA04C6C548FED3A1772399BE67780E944449C217B2CE0B9214920E67BC5AE8A7345CC9C5D001889D8883721A693A72268154D98BC965C83BE7AB952A4651F3719241A2CCEDE48A6683685AE123989530F8160A9F3E368DB7163A3AA1AB98C0045110FC83A73515921377710445441F1539CD07129B6560867B5A7A562B597D5C8CBBB3043927874468EDF7258934C7A851AAE9E91292827269F3B8C308874E4316BA936B43DF89102D63BABD8327D204F27B81BBEC28C72EA0F23068FB64CC7992A410717AEDC3CA093FCC8F85810FC1AC80405767C003DEC097F0F3260BEA578E82415C6A0AE7DB39C96FC4FF15BCA5DEB4AE66949BD120B7BBC14064BB2D1687E5E896643DA1491037C00B7180F342836271A0CAA1CC680C9FA361986D1B3C6E9C588808A92D44DB00C5E8C569E45BCABA025C3CF477B6303CC48641A2BC241B57778C7773257F389EB041BF9A5A4AE9842891BB9211C1D960A61D48CC1BCF59F2179692111814EF87B42A4BED6653AF787A04E2AA719588637D126A6598B5EE3AD39F92139943D373C91B979B6A5D43C5B64CBA1563D0BE124E271C19538596FD1991FD022AE967D4F0C0B31CA8A8D87599F929784AB02E6956873504D7602B2181A360323AE50586FA5F8BFBB1AC61581C535955FB572484284A27DC856C5F77F3EA08DD224407D458C66279E466CC9FE11B3555073279B86DA992BD2122E58F1BA0C29CC1522225D0B6BB7656EC120935752851D66C2D9A6A3EFB46353F93B9C83C6F13B9038855B2C15BA4C2A938D1CB550A1541DC630C2096BB5096C1FDB739CAB67B049A3DB005552E65E64A0C3CC52ACF7B132EB7613230A64C82684C8246189034FB7565BD280A2AE1040A6221A9878CAF03216CF9209D467BF4254AF4A888159418A857053F661810D66A155D199CE2B3D4974C32FBB0319531770CB0D65C54851A058DE2983E103A40404531B18908697C0145565B05C6FA5A2635C5BA185427BDD0499D281BFFD38C7533C51E839419B1081C6ABA0BCC4612AE46E4E661CA6C506E3285C6BE68F7552BACE2B88736C7DFF15283883C23A055376021B4F763D83488C6BE84706E66DA73637BC818E53394A07A977E4FA58B21C2CD8DACC9EA6BB8DF16807F1425B6C36B4D3091810A6D9A815E6EACED1A548D0D988F9E42299D121F148CC30D0188821A808B672BD881029E471CAD6CD8E4AC918F448E525C7414131E749378561185D05CCA73A21B0609CB65A62129ACB16647153A41C366A4D2D16C9D5D4A2DB322808E13E7B383D6700A1845780A0869B069A5A317187AAB336420A938A71631B28A72A2A7EB2E5B360046AD975209D3A8C489110D9A7124FCC5298B5C8BC489178CB5C1BCA5D31F32127AC6E69B1754647B11561C0A5270AD7E3618994CB8057453DC4C678C088A323A5B6C3CE350C08EB984540DAA53FD51117B17179F3C477FA7E0B5A8927F88A46030B1CDC73C2C467860267021283C1E5BA1F54740B925904E09CF95A7F898A65AA454B68787DB967C683480D1287C9B0270377FB5B8A463185C80E6A2C8124130DF3616A624575E106A2E25A919EF05319B0A9B92C7C3DC15D9F80994B7A7F180C74FD081A62485ED4549D16A9C7F9C9BAC2B1ACBC42664C99AA41640435CC6498CC3AE8B29ADAD5C24E88A8C359055C1814B40746698882A8A34FDD698C740709282B4D330BB8555009EDAC11ADBC2B05A56DED17850304A7DE7B324E7736E71A58B1385EAE97AFCDE64B76BA0B48D15BD53A10C65CA2359273A70C69F499A75D0589B3602CC040211D327699C62AEFC6AFEF4B20A4FC9B277A494CEAC316D36CA1A8A406C9616C13B4AB719E2D42163EB64CFBC773E290777D1AB617C0BCEDFB22D9F4BE5BB735E3583C8BBC96B8C680C3D87BD490513C5472B1654DC0403094F137B7C8BE7D101708D9CEB5673E78EA8415773CA7B858E90601DB439F8D3A5C33F0721348C6767391D5039C7AAB2107B54D3837CC3779B533678A73D20D229577DCD170B471A89B95467CD609B000B956F034A9FC5D08D36F1E119181743696A52AE1EC07B26CC7600806892A91C9813BA2DB9BEB32733EEC9BEE87262D4C70578C710804B7C131929C8786213960E5C021D5F3C1FCAB587F408F57A0C5E5F1127DA4B12715BC73208DFE703B44DC33B0DB22E6615B8404A936D97868C146473538B63421EE40BD4094C6164B8CA0D3C7FAF41108C62547023689CB8B83F5B8807532277A45C109D06F905692A605DC0706FA50A04E3B0387C62601E23956ABA544484F5566B4AC61091F42637C6C6D58286733B1C2F7C79CC3F673EA3426B2B8375D680480DB8F4C65ABF1E116D6B5340EF27FC912BB05307D5F5CCA35417E76C699E5809C34947C6817265508417BD61462970248A758E8911F50623BDD9725B4C46AC970708DC727DFB25ED2976B52125417AC9106DBB8FF464D6C692BBD65C960220A658671FD800B97128F5FBA9D9AE39FC20062B20866D9681E8F38003B0C7618DCC1A53A6520752EE6500F56D4041F8BCA7D879640B13C77A38A7F17705DBBBE890CABBB312F7CA3C74DF67F2A7B311876C629A00D79442EE000567A73A20D59210DB928C3E5BA1310BC39412D6EA02507C80EDA114D4BE3A12A475A5B4976CC1B76522949B7208324111015593FEDC4A2E070A8DC10556CC2A4F8F2C808E39020B540D806AA340A186EF42452E99BEF230E687B072E15ABECFA6A70B84AE7749E5EAC77AF19CEF3F6B6145161A1B61B03B72E287646142268C12C7427D8A79BF4A94BA59B2764778DB684032265B578BA917C31620715292B87B32256BBF82E5C1A1B624B47D3AB3702A8B7B132CE85A3B00454BFFCF07784CB791B14C4A53B5B3DE55DE1766EC5E81437BB7259D7437842C819CC35310431CD6A5BC8688A018584CB1C2E1261B6D0EB20E4D740AE9173B803CFC358A3550956E1D2036DD9B277BAA5208B0F88BC3D3743A78C10B7AB901D44C841D09B955D18C05F51C7B60BB555EC2A960A09097C8ED90B56106AA3643CACFF722AE98395EF0808A5ECAEF6D7BFEA8081E7BC28ECCC070A7956FE91375BB1B5C53C74AAC60BC845985DC555274200C931C005CACEB3D001E6372E5BAB6E8FEA357B412CD670666021693FE1BB287B087EECC484F3CC1734B79AA9A9F83B395B771BE29438F09A243E7B1CD746BB3EE25FDCC78DBB4C56604C703B4C28FB636CDA877291994682E1A47EFD79F0472AC5B19D9ADC5FDC2A044136758E93FB5B60EC50F2EBBD9D85E718310067347921E764D25488E072E7C540352C52E83641AD90DD61708D243ACA3DA15A0CD58AFFF73AA0D30E40D593181DF72D08E083618496DB0EF23E4E2D3E06", "BAFBDA04E42603DB0AEF4D066953731882C2CA44028B616CA4283D4E898DEE8F3CD6759BF161EA58887E37C182BA7B4B4C1D07727553FD260E01E1EB8DDA0043B81726F6C2CCED79209B6CB7510672D627FAD4D173BD73FB56A539BF1C7A750170F1308DA2CEE2224323520B389055EC19EF7E316DA4192E9C271A79D2FBB31CD436527B09888352BED7C70DA43871FDAB2752B6A00AA04FD873C8B1E817AD212DF28810B137E2D0A4826B49083BF30725CDE1C3662A073D15128F0D42A90BDDC9DB0DFFA5D3D7FFAAE3F008021BCAD97F3AB1C0D533C69B58E0F6A1A33A4FCA2BB3DDDEC97AE5529B1D772BA59DA60D79656281E9570AC871626704C88D8089B584C2483C56665CD2B1EB2187A52D9824663238284FC34A929855F450F52DAD62A2793FB5A219EEA4AA070252DD674882C88B4E1F66111B1264AA0FF885D0B6B28085FB8A140970B11DB2EC89616E74365F1CC1A97783B1B5AC9AF92D62556E214D1DABF1488E2AB22FAB3E284D424A73599C61D3C0316881DCBFE2ADC555717C41100EF4448C3BC52592EA0A31344B85D21FB323940C847A2FFF14182712C5FBE81E604EE417434067F8A167D226176125001CEF46253453121EB33B825C45C499D43223FCE1E0840CEDAFD168003191A2A63CF190BB615687D796B6DAE66938189F792CB088C630AC7DE0D477B552191092B39A62A4C4503830C06D1CBC304B292974518AC18A058CF8E90AF2E3504060B19BAFF0E4F81A055253750574168EC91A66FA0907C710CCC251E09C14EFD92708D853401E5ACEECC45C9DD7DECF0BFEE6642F5516D8E61257A07612DBDDC906D6D76F76652027E242F9D303BB6A7163E43E84BFE1DB0D35C046ACDA50EC0CB1A3246543258C2F96BE8F2A64086846FDD405706E2089F831690F6032796B4D6159E75A1D9950F70E5D0DCE79E39D49F8F8E8BC44BB64A381CA871C6951AD046BF6B3EECDC12CF830EE7D7866F9BD7C5AC8DB05F0A83EB83FA79AE3DF778F3F6171A81069EECF2D893E0ECE89F20032F85FB128FABB1809778350AF007DB3A426664804D6EDBE314C156430A93DD933D18AA84D3BE0550713C8888AF5F144EE609279E80B7FBE7D5B021A9D6A0BFAB74EFF59756829763EDC9EA83D863BB2BF98640451D7B40A35C4532E4FA35ED208B4B6B9656D0FF230058A27DD0A36AB9D2FEDF738F25ECC71A63A041E50AD2B843A5994C13E846A2F1EDCA9A4FCD875BD1EF43E05C98B0F9D9EB78E3D4A592201DED27CEC2471A600E34DABCD8B790E3F914BC03A9687D7D5F519A17727F14EFD421104F418AC914663E100C1EA2B9711637E94491741F852F65FDB49DAD006604B5892319CB472A1DCDF36D7E696B39F874C9598F493BA5E61F4F675479024425CE8A273031E9F04320434A4FC6FFE765521EB2DB3BD8DB85BAE25CC109777793F5A9566EF5020A97C5D13EA8027BC1276AD3C1B9470D05B148582B92DD01C706BAD780264470317C7BF92B267C34AD4753B25C31571B203DCB8F0328854EE4C488F4265161514D19C7A4DD31D26A4A840EF57E847F405D4D81EB59E240CA27538C8259DE934B111FBACBD197FFB3DB4E0E46D3983ACA2110E5B06A3B4E286A6AEB51AF8143214F8B116CAEC619ABF45180150C34B138BB953F128F2286AF51B35ADADF695627C7267B15389A2940C54A9799796FCAF47033924F6192EE81A4A91B1A6B4660F0106E541582F3D105386DE518E7274317733619BB5D3C5458DFD32F21832CE57199E13581D422D90198D252B5A2B02BE72556DDBDADDF804485155B8828925DED0C5EBBCF75123551CC95271EBCDB9C0F6F5EDCA28DD4A4E84DE1D3B5A70021FBF3E3C2AF90F799E2C869BB84D20178A97CA62A3F1A626BA4B1F69979278AB8F1DB88E66B3DEE136A8F6E5EE942F2897C5D2CA81B5A076535D55DA5AC6C90E75C4DF33D919160B39441D2F7FB37B66A635F1FE30A73F29A3974E8DEA34DA7F19F328947F68E4C7F9DE6D11DB1FDEBF528FD29589B84FA5E0164D6982E134D9C0F3B110335DD66881B32858E9F6518CA4FE7C225258614EE258D2BE076CB07C9FD51F49BAD6B9423C036DA919E0816AF6DB7EA4FEB51C4847287EC0F0432E2A9661D6AAD442019EA206D6B8A48ADBDDF09914E2C39CD58451BF5A0E8924E3ACF729E8EF4669A7B9676BF6AED9371AEB82AEB82108E05B45E27F", "57ED4E4F37FDFCB169BD0A749D40E6D58654534342E065EED3D273981B9B7053"), + ( + "6089C5991C02AEDA3E58C1641BAA81C60892A10629E685B8D738076B5C7CF5CC7C22F7A5B9160D753A27A6A89E09447EAC260F32A52090D81CD8FC2980802786117803B79C34B3355DDAB65188440696314B50B01FF666251B4890A351103B6550CC8E4118AFE7A5373BA04747712B5C08BED91A207D241E0E52686261A1B70C0699004B1FF5573976ADB44170E8141EEEE8A60F18B5D7AC5649F134CCD34066DCACF9116CD4F73880B74978F3B890985C42175DBF8681461A27E21222D3D908352902C515226817BCB1C1845CD6CED354A8C42838B5F21D56D484AE085BE0AAA6B40917B34A86FBB28CBD8407C5911194191BD568AFD5CC998000878C922D94A7092EBA29E48909E53C8DBCF3CF438097573442D9E42AD35C08FBA201CB8B4E9A167F33988DCE521E14902B8810B05D0C5690F86E0B01A2DD604001438BEC40BF957468C4032D0CC71AF2D18716387A586215B4BB6940881AE401CC054010C74634484B86D4441F92F0230794319D14A0E1D220AAE02B132A74943C2936AB3E7E2626F71512BE180D63A0CA1BA5294DE77C5C5A12EFEB546BF48BE8BA6A51A015420249A25533DF2A7917765779C62AD3E0B472E87AE9B3406EEC22CE2478128002E0478B8518BB285034ACD141AD817B05036A9317BBD198563BC066002B7D68716ED4AA138C271B556C9751708C807685AC304ED4574B0B477A4BD3088096B51CF9B8985A482C6288B1A128F603B3289116B4E047BA86956D426C38FA1DE57BC267136E41901CACDC5ED34729A1FAB5C00829A2697C368148B92C7775EACA83AB3C5AD5637D7C2084FB2FF3526D5BF9086DAB1AF58A3E531B50DA3C765FE9894CB516045A8B7EFC0013738F978120489B72BAE41DFDA636E5E208BDBB29F17BA6CB510A3EA2ABA59A0703EC1DB9058D3A9750B267348FE77FD76B9E24755A5D9433766C359A71463488B55FF2390C39A842FA63B2C121FF0A76F6E97A8AE2C7B6936E662C4976092896A500F533C4079341A39CB43BAA89BAE16EC7D5818DC442E982295A9A10B2E9473061B7634204D0B37FF4935E64878D3F601F80C686E3FC0A0523154EA04C6C548FED3A1772399BE67780E944449C217B2CE0B9214920E67BC5AE8A7345CC9C5D001889D8883721A693A72268154D98BC965C83BE7AB952A4651F3719241A2CCEDE48A6683685AE123989530F8160A9F3E368DB7163A3AA1AB98C0045110FC83A73515921377710445441F1539CD07129B6560867B5A7A562B597D5C8CBBB3043927874468EDF7258934C7A851AAE9E91292827269F3B8C308874E4316BA936B43DF89102D63BABD8327D204F27B81BBEC28C72EA0F23068FB64CC7992A410717AEDC3CA093FCC8F85810FC1AC80405767C003DEC097F0F3260BEA578E82415C6A0AE7DB39C96FC4FF15BCA5DEB4AE66949BD120B7BBC14064BB2D1687E5E896643DA1491037C00B7180F342836271A0CAA1CC680C9FA361986D1B3C6E9C588808A92D44DB00C5E8C569E45BCABA025C3CF477B6303CC48641A2BC241B57778C7773257F389EB041BF9A5A4AE9842891BB9211C1D960A61D48CC1BCF59F2179692111814EF87B42A4BED6653AF787A04E2AA719588637D126A6598B5EE3AD39F92139943D373C91B979B6A5D43C5B64CBA1563D0BE124E271C19538596FD1991FD022AE967D4F0C0B31CA8A8D87599F929784AB02E6956873504D7602B2181A360323AE50586FA5F8BFBB1AC61581C535955FB572484284A27DC856C5F77F3EA08DD224407D458C66279E466CC9FE11B3555073279B86DA992BD2122E58F1BA0C29CC1522225D0B6BB7656EC120935752851D66C2D9A6A3EFB46353F93B9C83C6F13B9038855B2C15BA4C2A938D1CB550A1541DC630C2096BB5096C1FDB739CAB67B049A3DB005552E65E64A0C3CC52ACF7B132EB7613230A64C82684C8246189034FB7565BD280A2AE1040A6221A9878CAF03216CF9209D467BF4254AF4A888159418A857053F661810D66A155D199CE2B3D4974C32FBB0319531770CB0D65C54851A058DE2983E103A40404531B18908697C0145565B05C6FA5A2635C5BA185427BDD0499D281BFFD38C7533C51E839419B1081C6ABA0BCC4612AE46E4E661CA6C506E3285C6BE68F7552BACE2B88736C7DFF15283883C23A055376021B4F763D83488C6BE84706E66DA73637BC818E53394A07A977E4FA58B21C2CD8DACC9EA6BB8DF16807F1425B6C36B4D3091810A6D9A815E6EACED1A548D0D988F9E42299D121F148CC30D0188821A808B672BD881029E471CAD6CD8E4AC918F448E525C7414131E749378561185D05CCA73A21B0609CB65A62129ACB16647153A41C366A4D2D16C9D5D4A2DB322808E13E7B383D6700A1845780A0869B069A5A317187AAB336420A938A71631B28A72A2A7EB2E5B360046AD975209D3A8C489110D9A7124FCC5298B5C8BC489178CB5C1BCA5D31F32127AC6E69B1754647B11561C0A5270AD7E3618994CB8057453DC4C678C088A323A5B6C3CE350C08EB984540DAA53FD51117B17179F3C477FA7E0B5A8927F88A46030B1CDC73C2C467860267021283C1E5BA1F54740B925904E09CF95A7F898A65AA454B68787DB967C683480D1287C9B0270377FB5B8A463185C80E6A2C8124130DF3616A624575E106A2E25A919EF05319B0A9B92C7C3DC15D9F80994B7A7F180C74FD081A62485ED4549D16A9C7F9C9BAC2B1ACBC42664C99AA41640435CC6498CC3AE8B29ADAD5C24E88A8C359055C1814B40746698882A8A34FDD698C740709282B4D330BB8555009EDAC11ADBC2B05A56DED17850304A7DE7B324E7736E71A58B1385EAE97AFCDE64B76BA0B48D15BD53A10C65CA2359273A70C69F499A75D0589B3602CC040211D327699C62AEFC6AFEF4B20A4FC9B277A494CEAC316D36CA1A8A406C9616C13B4AB719E2D42163EB64CFBC773E290777D1AB617C0BCEDFB22D9F4BE5BB735E3583C8BBC96B8C680C3D87BD490513C5472B1654DC0403094F137B7C8BE7D101708D9CEB5673E78EA8415773CA7B858E90601DB439F8D3A5C33F0721348C6767391D5039C7AAB2107B54D3837CC3779B533678A73D20D229577DCD170B471A89B95467CD609B000B956F034A9FC5D08D36F1E119181743696A52AE1EC07B26CC7600806892A91C9813BA2DB9BEB32733EEC9BEE87262D4C70578C710804B7C131929C8786213960E5C021D5F3C1FCAB587F408F57A0C5E5F1127DA4B12715BC73208DFE703B44DC33B0DB22E6615B8404A936D97868C146473538B63421EE40BD4094C6164B8CA0D3C7FAF41108C62547023689CB8B83F5B8807532277A45C109D06F905692A605DC0706FA50A04E3B0387C62601E23956ABA544484F5566B4AC61091F42637C6C6D58286733B1C2F7C79CC3F673EA3426B2B8375D680480DB8F4C65ABF1E116D6B5340EF27FC912BB05307D5F5CCA35417E76C699E5809C34947C6817265508417BD61462970248A758E8911F50623BDD9725B4C46AC970708DC727DFB25ED2976B52125417AC9106DBB8FF464D6C692BBD65C960220A658671FD800B97128F5FBA9D9AE39FC20062B20866D9681E8F38003B0C7618DCC1A53A6520752EE6500F56D4041F8BCA7D879640B13C77A38A7F17705DBBBE890CABBB312F7CA3C74DF67F2A7B311876C629A00D79442EE000567A73A20D59210DB928C3E5BA1310BC39412D6EA02507C80EDA114D4BE3A12A475A5B4976CC1B76522949B7208324111015593FEDC4A2E070A8DC10556CC2A4F8F2C808E39020B540D806AA340A186EF42452E99BEF230E687B072E15ABECFA6A70B84AE7749E5EAC77AF19CEF3F6B6145161A1B61B03B72E287646142268C12C7427D8A79BF4A94BA59B2764778DB684032265B578BA917C31620715292B87B32256BBF82E5C1A1B624B47D3AB3702A8B7B132CE85A3B00454BFFCF07784CB791B14C4A53B5B3DE55DE1766EC5E81437BB7259D7437842C819CC35310431CD6A5BC8688A018584CB1C2E1261B6D0EB20E4D740AE9173B803CFC358A3550956E1D2036DD9B277BAA5208B0F88BC3D3743A78C10B7AB901D44C841D09B955D18C05F51C7B60BB555EC2A960A09097C8ED90B56106AA3643CACFF722AE98395EF0808A5ECAEF6D7BFEA8081E7BC28ECCC070A7956FE91375BB1B5C53C74AAC60BC845985DC555274200C931C005CACEB3D001E6372E5BAB6E8FEA357B412CD670666021693FE1BB287B087EECC484F3CC1734B79AA9A9F83B395B771BE29438F09A243E7B1CD746BB3EE25FDCC78DBB4C56604C703B4C28FB636CDA877291994682E1A47EFD79F0472AC5B19D9ADC5FDC2A044136758E93FB5B60EC50F2EBBD9D85E718310067347921E764D25488E072E7C540352C52E83641AD90DD61708D243ACA3DA15A0CD58AFFF73AA0D30E40D593181DF72D08E083618496DB0EF23E4E2D3E06", + "BAFBDA04E42603DB0AEF4D066953731882C2CA44028B616CA4283D4E898DEE8F3CD6759BF161EA58887E37C182BA7B4B4C1D07727553FD260E01E1EB8DDA0043B81726F6C2CCED79209B6CB7510672D627FAD4D173BD73FB56A539BF1C7A750170F1308DA2CEE2224323520B389055EC19EF7E316DA4192E9C271A79D2FBB31CD436527B09888352BED7C70DA43871FDAB2752B6A00AA04FD873C8B1E817AD212DF28810B137E2D0A4826B49083BF30725CDE1C3662A073D15128F0D42A90BDDC9DB0DFFA5D3D7FFAAE3F008021BCAD97F3AB1C0D533C69B58E0F6A1A33A4FCA2BB3DDDEC97AE5529B1D772BA59DA60D79656281E9570AC871626704C88D8089B584C2483C56665CD2B1EB2187A52D9824663238284FC34A929855F450F52DAD62A2793FB5A219EEA4AA070252DD674882C88B4E1F66111B1264AA0FF885D0B6B28085FB8A140970B11DB2EC89616E74365F1CC1A97783B1B5AC9AF92D62556E214D1DABF1488E2AB22FAB3E284D424A73599C61D3C0316881DCBFE2ADC555717C41100EF4448C3BC52592EA0A31344B85D21FB323940C847A2FFF14182712C5FBE81E604EE417434067F8A167D226176125001CEF46253453121EB33B825C45C499D43223FCE1E0840CEDAFD168003191A2A63CF190BB615687D796B6DAE66938189F792CB088C630AC7DE0D477B552191092B39A62A4C4503830C06D1CBC304B292974518AC18A058CF8E90AF2E3504060B19BAFF0E4F81A055253750574168EC91A66FA0907C710CCC251E09C14EFD92708D853401E5ACEECC45C9DD7DECF0BFEE6642F5516D8E61257A07612DBDDC906D6D76F76652027E242F9D303BB6A7163E43E84BFE1DB0D35C046ACDA50EC0CB1A3246543258C2F96BE8F2A64086846FDD405706E2089F831690F6032796B4D6159E75A1D9950F70E5D0DCE79E39D49F8F8E8BC44BB64A381CA871C6951AD046BF6B3EECDC12CF830EE7D7866F9BD7C5AC8DB05F0A83EB83FA79AE3DF778F3F6171A81069EECF2D893E0ECE89F20032F85FB128FABB1809778350AF007DB3A426664804D6EDBE314C156430A93DD933D18AA84D3BE0550713C8888AF5F144EE609279E80B7FBE7D5B021A9D6A0BFAB74EFF59756829763EDC9EA83D863BB2BF98640451D7B40A35C4532E4FA35ED208B4B6B9656D0FF230058A27DD0A36AB9D2FEDF738F25ECC71A63A041E50AD2B843A5994C13E846A2F1EDCA9A4FCD875BD1EF43E05C98B0F9D9EB78E3D4A592201DED27CEC2471A600E34DABCD8B790E3F914BC03A9687D7D5F519A17727F14EFD421104F418AC914663E100C1EA2B9711637E94491741F852F65FDB49DAD006604B5892319CB472A1DCDF36D7E696B39F874C9598F493BA5E61F4F675479024425CE8A273031E9F04320434A4FC6FFE765521EB2DB3BD8DB85BAE25CC109777793F5A9566EF5020A97C5D13EA8027BC1276AD3C1B9470D05B148582B92DD01C706BAD780264470317C7BF92B267C34AD4753B25C31571B203DCB8F0328854EE4C488F4265161514D19C7A4DD31D26A4A840EF57E847F405D4D81EB59E240CA27538C8259DE934B111FBACBD197FFB3DB4E0E46D3983ACA2110E5B06A3B4E286A6AEB51AF8143214F8B116CAEC619ABF45180150C34B138BB953F128F2286AF51B35ADADF695627C7267B15389A2940C54A9799796FCAF47033924F6192EE81A4A91B1A6B4660F0106E541582F3D105386DE518E7274317733619BB5D3C5458DFD32F21832CE57199E13581D422D90198D252B5A2B02BE72556DDBDADDF804485155B8828925DED0C5EBBCF75123551CC95271EBCDB9C0F6F5EDCA28DD4A4E84DE1D3B5A70021FBF3E3C2AF90F799E2C869BB84D20178A97CA62A3F1A626BA4B1F69979278AB8F1DB88E66B3DEE136A8F6E5EE942F2897C5D2CA81B5A076535D55DA5AC6C90E75C4DF33D919160B39441D2F7FB37B66A635F1FE30A73F29A3974E8DEA34DA7F19F328947F68E4C7F9DE6D11DB1FDEBF528FD29589B84FA5E0164D6982E134D9C0F3B110335DD66881B32858E9F6518CA4FE7C225258614EE258D2BE076CB07C9FD51F49BAD6B9423C036DA919E0816AF6DB7EA4FEB51C4847287EC0F0432E2A9661D6AAD442019EA206D6B8A48ADBDDF09914E2C39CD58451BF5A0E8924E3ACF729E8EF4669A7B9676BF6AED9371AEB82AEB82108E05B45E27F", + "57ED4E4F37FDFCB169BD0A749D40E6D58654534342E065EED3D273981B9B7053", + ), // valid decapsulation - ("48EBB6D98494EDF34B2C941BCB61366B80C185863146543C1F76CC5027AD20BC8814397D03E26DB9F4844CEC1DB7B9B6C8B14C65829052AC6F9D47712802094E978BC9A5989F747B599B26057A04661027B7FBCE9DE019D384B2FD206DC0780F58F0AC9E0B1635F93D77C7C20563C0900803E119B011D750D4C9AF6C037C6E32250EEA3CEFB977AF934207870A0BB965C61C490EF82CD12AA1E33897F3F4C643B1A05F91BA0BD420FFAABC44D38F66F9956D777256D726CA01278FE018BD0713462AAC61AC5A9F5734988153E523090A89265A811B4BA26F01B3B056EB32879743DD583FCE067F90EC63B80B97EC985FE78A739BEB0B0643910D618135CC23C600BACF5438AD899CAAE3A9CC2AB8DC30732B678585D3B55228967310BEEE4CC269658992C7B2CD359F7ED630F0552170D702D6A646ECE00728CBBF09EC11E9F1999052169347ABEAE482F1B13EB8B439CA018E6AB4763C1AAF5848C1601451CB93550957C435FA16B79B504D4172319B459B86C868E981870200FB21522EB4647DE52E2D68835D6546DB18AF048284232C39415A8AD86929D2381128856CD6845691212E7148A9C2B95D468A176264655663CD495632C480C654051859BA020CA01DF1B248914C90319069F17B6EDD9C58BFCC49B911C2890BC58946ACAAB40BEC05294FD5B6681CBB398AAE59AA208BE24E5DD35A4D39BBA28B35979359E93A69D9E5AC098076E3D17B97A5BA8796316A627C83956B12616AA8380C45D845C67658141A5BA6A83D6E96246BE71ABF93A014332CAEC404CB7884B88A23D92BBFDE7B22F3A89B0E5738B7A0979EDA99501B1C12A882368897B7919A5F70397384659603259A5847B11B20E4924432B2A9AA885088B40764B1A548A256DEB659D994BA1725544FD29B11B78FECEC0D07A031694465FBC6082510CE41090792785ABBE607E33603EC1A5902C51765179B3F78B7393A93D189AD40F73A5E5357A4A35FAC3B0561EA9A5EAAADE6B16313F90569514227C52A362A52645B52F875C4FDBC001CA65ED05616DB563BE4027F5B1850B1A529EEAC5552891E169663CFD3A4CC2ABD13C37FF6A7082480B9E652313275A26F671C9694A241CA79B3373F02CB698CB609C1D9551B054D4705874CE452CB8AC7E5A38271970C6E3B9137A7865C858577A656181072FD65961391BB34177455371DC81032DE824ED0BC21B2B9AB5E0A5D615B23D5C850191C7BDA998C4C445A439ACF2392A5879CBE1D39638A26534ACB2BD5CBB04ABB1E618740A2CC907F514B95EC4986704F5E8788E862960DE766B576BBA8AC0EAE34BBB075AD9DB09D5E529FBD1A4EA7E6213C4B2835E449A05138DCAB0924D060AEA337BD781E2CF1B3E7BB372AA2292E048B9ED415AD4AB1080389DE762165EC95A4D4691465260B9CBA9B98684C755981D42E287258C3E427652365DA9414897A603AE02861D27967AA6A044A787B53B6D20215C083570F5058DEABA170E38FC3677B22ACC5B5D17AA3521DCF528D564C9303ABBE87C9BD115A6AED2C5C5764BC8E4B7CEFCB558357B4FAD3892E71BF27B87754FA0C519415960A6DF6EA5C6E59B521704FD4A67076271D087CB5F5B51D5D020DB13926F1406A69DA0CA257084838465EE152B9794AC4567B53246526438D0B6845FEA5202AB868C3C35EC07B3A1DEA558BC7BD7A05BCFEFB5F6C9AA1CFB3989EC59667169284B22A42B49A65929140BAA0A8552706FA76E66B4267F074C934B39C1360D82962AD331CBDAC30F8B27C0302B586640A3492B341FA3D967B5E1C328B88EA7669337AB0EB3E862898F746305A73A9EE77B03E776D9FF02DFCA8BF8763182C65A90A478129560A59CCB97FF186478327CB534C22142836B21AFBB558FBD527EB9A3A8249C3CA3A299544169199B05347C04938B558C22103A64F04A4C74A698B5484BC65A16D3CB573179188DE3ABCC879A2C0150D92554E0766A7EC084E4A0B0911F9729860C2CB02CBF4CC05FE9429BDC979779080391CCF5B4A4088175A2372CF43DA804AE473663CC55CCB4331969FC42ACE54EBB24082473EE95E9552C03364AE70D4CE6CA114F8577EDE550247F859FFA679277C82E15822FD87890A6737C5C2A273036F343198F33395426709201699E8345C23B2933F05115350C45232A6DCD8C5F07B9FD3F07157762384A11C02D00A8789751B08CA158A5015DA200CB4652A565959E15ABFEC60B6B6AF601C882FEB0148D643D43433FB768735C314B71CA1F1CA3888D47F6B9050306633BEA2AFC8F8CE704A5FBCA947E40657C2BA6DCDDA9979EA326080648BE5C2B2828BF9670FC60528678A21E8334CE6B0A292B94833C2128A009DB77221CF69A233624D25AB5CDE3A6C80D4C8B6A7830E7B81CCA0955FD9A863A90A0A9B8A705408261076D4E89D99757157B1BD99F41D3B220E85395129B8564EA95B0526B9BC3C0CC4C374B4427E8756A22BD03EC671B1FF899055DC56CAAC8AFA0993E5D58DE201C4BFA4C1C586A0C2AC79DFA385132380C4395B6DA128D86695100C178AF4499B2439270951509C11E1D01061640F4AB67C3FA10FCE37CD21E67385E3AD189C4FB4289200751577C4256511828FA63AF4C352AE7B46AC0A2B3AEB35CBF859C7A43C4AE4206F639A3718170C697CE0BC24855A4DE2153480B89D3DC6317F391E978589C953A8759260C50835FC340DB89CB176440F53D580B3FB2685102E4FF201A45BB736005FE10744BF76133696CCC70448D17013CA173B6380485A307CA02531619A20AC057AD5662A5B6A9DB420122C5917AEEB14CF164F337439A4A816D3B00896524A041BCE0AD65DAA27A9C287170043B0C8B86C5FC207EFAA5DDCD238E31AC9154420B69C7E60E2B98F5752B343336C112115FC6F3B479E63898DF6593BBC355631762D11E3BA7DF7B705FB1876EC0F309701EAB02C01B6BD935881CF7074CF6B3EF5BA6210C2CCF0543AB6811496969F75E4452841210137101BC45B7A8B5E7BB1A58084903E6076B8F10DCFDB175112B89C58CE16F3622FB5AC00C4778B300243438BFF37CF1A41CF09FA4726C311B65B0E5ED56BBADA4847D7A4E8F4135ED1B6AD917C66261ED6261E88FC52375055EEDA7A4B2A7547025E48D6499E2867CB3339EE83B2637564B1A1BE574928DD838514FCCEFD6453F3C30BD050226AC1B7C21356E1314D1A72324D843C13D355E9F745A1053B64C3B2002BAEB4786C8778A36AB0CAB3DA7407426EA557259507A5F7A622A0095DBEA07200D204A8C7A3F9C5B2421952DA569966DB2E08577A3D94A947963E75C810FEB61E58816A3E9901B09CBD75E291036302D572199DD12984BCA14D38B65CB5A8155C5228B099DCA122FDC7734F976A9D8187FBA1B0B8C36B2223C5D971B9A77B978DE967CD35ABA457446DECB3E5085C68B31DFE2038A2C0A0E0000737976227769F36D04E5DDB405683AA4E6B6CAFEC5BB56381A835967C7A49D6CC4F1BA85F7493893544766C0C6B72A935957BC070D309C8E7B5F1648282361FA2908DCCF99A19046175A91B8A492E678A67A8CA1DFE132539170FD08A6910001F737484BBE107C8E5BA16E5920675CF59E6590253739B918AFF83337D6ACB1FC965FAC58325918695F01C4567253CE87844383998B5297AA35A2683761BD1783D37C504C51E072312309606EFBA759DA8505F3C16D36812DA070C7BB771E3C4A7D2E33B79BB4ACDB35F4FE97A5442B073203810012DA4E98A1C70329D036CEF462A2AC239A1DB38E1550F2884ADD3FB8FE999412E86A96A9B6DDF24A94A4A309CFC4B0C0A2A13D93CF97570FD485E08F7A681C33F2E6632DBCC2DAF552C7DE4C27ED153AEEA5E8DA07E458110CA4C7516244D4D6915D0566AAF615C6FB682D59728B14A0170E4366B85C2EF5C8596269EF9BA73947A6D4EC6530BD556374470AD472BCD495C68CC95E7F806B4B121B6134B092C73B3682FE0214EE2A276A82A44DA277E5E898A1D15184909440D81A4FB3BB9C6016285A05E421CC9531897493ABFE4D71B0D3B1559950D178A2014EBBE3E24B2029A9EF00036D5789F1D3348992C3F5E633A2116BBC54786FD865E07D7BF8A4B014EA6582FAC1FCC973BE9981FE7390FD27774DC87743A539552B854010382739CA3F4619A1B329AFD70670502B50C419983E11E98F86A38A2A141164DFF19373AB57EC69640965821C97671D5A15C6C901F42865601686466F78177AA7A07AC0D2ED47969D31C6EF8CC0AE409ECD11D2218276E0668A8B695AAA72305C93450659E681B31D3D5BA7F282AE21444FEA9365008434B97755126C7B9B39E89C402D0F46BF13D37BCA014ACEF97024244958D3FE0C6693E7623F85915AD4252EDF029363DEDED6EB939D9224D09BC547B224415C8D66C307D207043865E10BD4ABE38DC14701C818DDE435C49AE143489E2AC84F1CB4648881455B0E4B29CBEB7D91379CF", "A675E7A91ACAE3A48801C9ABB1EE076D420018C2CC1D845953F8CCE50358EABEC28724EC974F3D400EB8B95F8B6D6E4CCE6E6E343344B85FF3009A8D070F176FEB29FB225F246C7FE6B3228C20E13F37B9810B3108DBD12343277822DA97849FC1360FCAE325FD5EB6B9AB958338EF1A60FDC5D237F3CCD2D8F9AD5755AB2217535A3A42C660DD13EE50A9AB75A2838DD99820B296742E85FA0687487E4F0D6D94B634070F54805B1E8672B4287EE92197E57B6E391A7DE7EE5029B7B2E7F270E7D5B906453ED3D693FD21B7C5424A00678C0EA5B51252ED613F9E1D4F08FDA8388F2E7E38B7DEC158FFD8B9445658CAF129AA15C8B9361967096C5B4B3BF7B4F04DD246E985FFFBE00BDB5E0705CB7B7D39FC65B574B8A3D9221C4A4A3F93BE2C5BE670B5C450EA5040599BD60A57AD8A293B659161416BD61E5A9A9A11C8C8C1280F02AA98C62BAE7C60E5F5F79FA0B5C8A6596695506520CA933629A8377350769EA66E7F258C92421BFBC49EC13F7EC678A7F4A91E77E04B45B6DBB68F8B78C7B38982FA666E23C8E5F5B06438196416710EE02E1D834B191DC263133C8F31A062F8377A513D0C47569507F7332B09D1DD2035910621665A9AF641C2BA04E3AE11F61DB20883BA3E700C4C6FB0427D22FAE5192062024CF36BA99FAE7C198932E05A77C8F191DDC3F3EB21482C7E5F2805E1EB818763F70E2F510D9FBBF4011F242B6902EDD9117CBCBB89F3708D6F98C5163753CAC8DF8179CCA270BEAB19F451EB504D8A73F610FCA02F61E6DABA6F08703C2EF130B73A79A91F94D30865D3C38F85B34CCDED331249A4F2ADA591B1D2FE012F14547C0ED8AF91DBDDA42DB729FEB5453BA8AFFDC08938425C6BD796B6811EAE718B86AD90B6C6D42BCCE0E4648A5F37CC43184148F88C092CAD5234BB2B17EB035BDAA8200574CBD0C1F622C2712E9440AB127A9409421E6ACCB93740B8B956A093A414655AD96FF4D19CB0A324550AF74A14E4583F8DC140B3A0D7AFDF4B7F4A46476942D8010B42C6D40CCEED6D20C01B256DFA74D16511FEE9951FC9CFB9AC4735749D287095184050718203734C00E780FB94F49D2DD22DA630AE698EBC0F9EFDCCD9F3BFC6BE7700EEF1ADE288602C77E9E44B5D53F4A0A45E6EF6803030BE8C353D43D704F307CA53AB623245DADED26173E75EA6C93F72A1A50FBFD5B6C91CD1A7EF7142A88CB263E8F554E2C34C28FE8691A7F01BBFB751FEE5C5369D49D456511C3EA6E1232EF85AE89DB45844DAD959B81DA0663268BAC3BA9CD1CC170D6175495EC77230176D740A284E0F53B7F6002B238BD6516C4ED2CF050FA7AD5405AA085D4759672278CF6B99D2C292F1CDE7AF00DB21EEB6AB5694E73C7EC058D4CE4B506D436DC0DA748994543F846EF29A2CA790F2C8882CA59CBF22EFBFDE86AB15E7422806A6D6A43F1DD6C6BCCDA77A69034FD4860D69E969E34AB891DBE139412F6B82A95BF04260D883ADF6FDAEF5CE0E68E40E749FEEB5F413A5EA8B2B8A449A527D0451A221AD2F314D5BB141DE57A9ACCE35EE50904D3BA238CD99387DB657456311ACDCA9B8BAB42FFF4716E59AC2247CF6C13E170297C11534AA17E5FBC7162C74E5E8E07A22C4717C0CAC287043F8BE194D53227DA674233FFF0B9D48B83A56659DFB64AD498B2DA63D4B7A38C826D06E834B23D57EE374AD4A6F3AF1E7E8628C98763252613E7E7B34A83D763E883DBB9BCBC903CC3E59878799A935425C540D3DF70DEE2D1A8BD5D2BCB541588AB0DDA0BEBF8DE7D6C2322A8B186DA3CFFCF9C17DFBD10209721F2C1B88D3C7366F8D23DA8273775D7C5E9F135022691036385661D8A2E1614988089640C6B3803D9C9D7C9C3E87DBB7F6AB289BD2399E8CE4CB3D6AD042E1783C9C39E620CFD718D312AEB5D780EA6B409C69F6091F83B081F2BFB32DDB8EEB6B88C91B5F340737C8601C2295D94CFF384A99C7F49818E0BF571FF325132A34869A0A6F060003A2DE43B3986F98A263228189F617F30A0481589AE5E396673FF1C412C18AD181C11D6C52B8B4EDB47A10BBF2E3E83AE368FC55F88B7FD16A9EAE8154B7F7AF88A10CE3DB69B7A23048458764622F8C25CDAC47B8CABAC8A5F72B03FA8376EE03D937827EC3D3B56CE3819BAC3603AC5E1D2DFD0306E4B468C8BDF1C24BA6C0145D57839D6EE91C7DB40DA88A54EE8C736D1C", "235360D7AB0AD5022F1DB7F526206F4EE98199740DA36EBB5A77BB78D3FB9195"), + ( + "48EBB6D98494EDF34B2C941BCB61366B80C185863146543C1F76CC5027AD20BC8814397D03E26DB9F4844CEC1DB7B9B6C8B14C65829052AC6F9D47712802094E978BC9A5989F747B599B26057A04661027B7FBCE9DE019D384B2FD206DC0780F58F0AC9E0B1635F93D77C7C20563C0900803E119B011D750D4C9AF6C037C6E32250EEA3CEFB977AF934207870A0BB965C61C490EF82CD12AA1E33897F3F4C643B1A05F91BA0BD420FFAABC44D38F66F9956D777256D726CA01278FE018BD0713462AAC61AC5A9F5734988153E523090A89265A811B4BA26F01B3B056EB32879743DD583FCE067F90EC63B80B97EC985FE78A739BEB0B0643910D618135CC23C600BACF5438AD899CAAE3A9CC2AB8DC30732B678585D3B55228967310BEEE4CC269658992C7B2CD359F7ED630F0552170D702D6A646ECE00728CBBF09EC11E9F1999052169347ABEAE482F1B13EB8B439CA018E6AB4763C1AAF5848C1601451CB93550957C435FA16B79B504D4172319B459B86C868E981870200FB21522EB4647DE52E2D68835D6546DB18AF048284232C39415A8AD86929D2381128856CD6845691212E7148A9C2B95D468A176264655663CD495632C480C654051859BA020CA01DF1B248914C90319069F17B6EDD9C58BFCC49B911C2890BC58946ACAAB40BEC05294FD5B6681CBB398AAE59AA208BE24E5DD35A4D39BBA28B35979359E93A69D9E5AC098076E3D17B97A5BA8796316A627C83956B12616AA8380C45D845C67658141A5BA6A83D6E96246BE71ABF93A014332CAEC404CB7884B88A23D92BBFDE7B22F3A89B0E5738B7A0979EDA99501B1C12A882368897B7919A5F70397384659603259A5847B11B20E4924432B2A9AA885088B40764B1A548A256DEB659D994BA1725544FD29B11B78FECEC0D07A031694465FBC6082510CE41090792785ABBE607E33603EC1A5902C51765179B3F78B7393A93D189AD40F73A5E5357A4A35FAC3B0561EA9A5EAAADE6B16313F90569514227C52A362A52645B52F875C4FDBC001CA65ED05616DB563BE4027F5B1850B1A529EEAC5552891E169663CFD3A4CC2ABD13C37FF6A7082480B9E652313275A26F671C9694A241CA79B3373F02CB698CB609C1D9551B054D4705874CE452CB8AC7E5A38271970C6E3B9137A7865C858577A656181072FD65961391BB34177455371DC81032DE824ED0BC21B2B9AB5E0A5D615B23D5C850191C7BDA998C4C445A439ACF2392A5879CBE1D39638A26534ACB2BD5CBB04ABB1E618740A2CC907F514B95EC4986704F5E8788E862960DE766B576BBA8AC0EAE34BBB075AD9DB09D5E529FBD1A4EA7E6213C4B2835E449A05138DCAB0924D060AEA337BD781E2CF1B3E7BB372AA2292E048B9ED415AD4AB1080389DE762165EC95A4D4691465260B9CBA9B98684C755981D42E287258C3E427652365DA9414897A603AE02861D27967AA6A044A787B53B6D20215C083570F5058DEABA170E38FC3677B22ACC5B5D17AA3521DCF528D564C9303ABBE87C9BD115A6AED2C5C5764BC8E4B7CEFCB558357B4FAD3892E71BF27B87754FA0C519415960A6DF6EA5C6E59B521704FD4A67076271D087CB5F5B51D5D020DB13926F1406A69DA0CA257084838465EE152B9794AC4567B53246526438D0B6845FEA5202AB868C3C35EC07B3A1DEA558BC7BD7A05BCFEFB5F6C9AA1CFB3989EC59667169284B22A42B49A65929140BAA0A8552706FA76E66B4267F074C934B39C1360D82962AD331CBDAC30F8B27C0302B586640A3492B341FA3D967B5E1C328B88EA7669337AB0EB3E862898F746305A73A9EE77B03E776D9FF02DFCA8BF8763182C65A90A478129560A59CCB97FF186478327CB534C22142836B21AFBB558FBD527EB9A3A8249C3CA3A299544169199B05347C04938B558C22103A64F04A4C74A698B5484BC65A16D3CB573179188DE3ABCC879A2C0150D92554E0766A7EC084E4A0B0911F9729860C2CB02CBF4CC05FE9429BDC979779080391CCF5B4A4088175A2372CF43DA804AE473663CC55CCB4331969FC42ACE54EBB24082473EE95E9552C03364AE70D4CE6CA114F8577EDE550247F859FFA679277C82E15822FD87890A6737C5C2A273036F343198F33395426709201699E8345C23B2933F05115350C45232A6DCD8C5F07B9FD3F07157762384A11C02D00A8789751B08CA158A5015DA200CB4652A565959E15ABFEC60B6B6AF601C882FEB0148D643D43433FB768735C314B71CA1F1CA3888D47F6B9050306633BEA2AFC8F8CE704A5FBCA947E40657C2BA6DCDDA9979EA326080648BE5C2B2828BF9670FC60528678A21E8334CE6B0A292B94833C2128A009DB77221CF69A233624D25AB5CDE3A6C80D4C8B6A7830E7B81CCA0955FD9A863A90A0A9B8A705408261076D4E89D99757157B1BD99F41D3B220E85395129B8564EA95B0526B9BC3C0CC4C374B4427E8756A22BD03EC671B1FF899055DC56CAAC8AFA0993E5D58DE201C4BFA4C1C586A0C2AC79DFA385132380C4395B6DA128D86695100C178AF4499B2439270951509C11E1D01061640F4AB67C3FA10FCE37CD21E67385E3AD189C4FB4289200751577C4256511828FA63AF4C352AE7B46AC0A2B3AEB35CBF859C7A43C4AE4206F639A3718170C697CE0BC24855A4DE2153480B89D3DC6317F391E978589C953A8759260C50835FC340DB89CB176440F53D580B3FB2685102E4FF201A45BB736005FE10744BF76133696CCC70448D17013CA173B6380485A307CA02531619A20AC057AD5662A5B6A9DB420122C5917AEEB14CF164F337439A4A816D3B00896524A041BCE0AD65DAA27A9C287170043B0C8B86C5FC207EFAA5DDCD238E31AC9154420B69C7E60E2B98F5752B343336C112115FC6F3B479E63898DF6593BBC355631762D11E3BA7DF7B705FB1876EC0F309701EAB02C01B6BD935881CF7074CF6B3EF5BA6210C2CCF0543AB6811496969F75E4452841210137101BC45B7A8B5E7BB1A58084903E6076B8F10DCFDB175112B89C58CE16F3622FB5AC00C4778B300243438BFF37CF1A41CF09FA4726C311B65B0E5ED56BBADA4847D7A4E8F4135ED1B6AD917C66261ED6261E88FC52375055EEDA7A4B2A7547025E48D6499E2867CB3339EE83B2637564B1A1BE574928DD838514FCCEFD6453F3C30BD050226AC1B7C21356E1314D1A72324D843C13D355E9F745A1053B64C3B2002BAEB4786C8778A36AB0CAB3DA7407426EA557259507A5F7A622A0095DBEA07200D204A8C7A3F9C5B2421952DA569966DB2E08577A3D94A947963E75C810FEB61E58816A3E9901B09CBD75E291036302D572199DD12984BCA14D38B65CB5A8155C5228B099DCA122FDC7734F976A9D8187FBA1B0B8C36B2223C5D971B9A77B978DE967CD35ABA457446DECB3E5085C68B31DFE2038A2C0A0E0000737976227769F36D04E5DDB405683AA4E6B6CAFEC5BB56381A835967C7A49D6CC4F1BA85F7493893544766C0C6B72A935957BC070D309C8E7B5F1648282361FA2908DCCF99A19046175A91B8A492E678A67A8CA1DFE132539170FD08A6910001F737484BBE107C8E5BA16E5920675CF59E6590253739B918AFF83337D6ACB1FC965FAC58325918695F01C4567253CE87844383998B5297AA35A2683761BD1783D37C504C51E072312309606EFBA759DA8505F3C16D36812DA070C7BB771E3C4A7D2E33B79BB4ACDB35F4FE97A5442B073203810012DA4E98A1C70329D036CEF462A2AC239A1DB38E1550F2884ADD3FB8FE999412E86A96A9B6DDF24A94A4A309CFC4B0C0A2A13D93CF97570FD485E08F7A681C33F2E6632DBCC2DAF552C7DE4C27ED153AEEA5E8DA07E458110CA4C7516244D4D6915D0566AAF615C6FB682D59728B14A0170E4366B85C2EF5C8596269EF9BA73947A6D4EC6530BD556374470AD472BCD495C68CC95E7F806B4B121B6134B092C73B3682FE0214EE2A276A82A44DA277E5E898A1D15184909440D81A4FB3BB9C6016285A05E421CC9531897493ABFE4D71B0D3B1559950D178A2014EBBE3E24B2029A9EF00036D5789F1D3348992C3F5E633A2116BBC54786FD865E07D7BF8A4B014EA6582FAC1FCC973BE9981FE7390FD27774DC87743A539552B854010382739CA3F4619A1B329AFD70670502B50C419983E11E98F86A38A2A141164DFF19373AB57EC69640965821C97671D5A15C6C901F42865601686466F78177AA7A07AC0D2ED47969D31C6EF8CC0AE409ECD11D2218276E0668A8B695AAA72305C93450659E681B31D3D5BA7F282AE21444FEA9365008434B97755126C7B9B39E89C402D0F46BF13D37BCA014ACEF97024244958D3FE0C6693E7623F85915AD4252EDF029363DEDED6EB939D9224D09BC547B224415C8D66C307D207043865E10BD4ABE38DC14701C818DDE435C49AE143489E2AC84F1CB4648881455B0E4B29CBEB7D91379CF", + "A675E7A91ACAE3A48801C9ABB1EE076D420018C2CC1D845953F8CCE50358EABEC28724EC974F3D400EB8B95F8B6D6E4CCE6E6E343344B85FF3009A8D070F176FEB29FB225F246C7FE6B3228C20E13F37B9810B3108DBD12343277822DA97849FC1360FCAE325FD5EB6B9AB958338EF1A60FDC5D237F3CCD2D8F9AD5755AB2217535A3A42C660DD13EE50A9AB75A2838DD99820B296742E85FA0687487E4F0D6D94B634070F54805B1E8672B4287EE92197E57B6E391A7DE7EE5029B7B2E7F270E7D5B906453ED3D693FD21B7C5424A00678C0EA5B51252ED613F9E1D4F08FDA8388F2E7E38B7DEC158FFD8B9445658CAF129AA15C8B9361967096C5B4B3BF7B4F04DD246E985FFFBE00BDB5E0705CB7B7D39FC65B574B8A3D9221C4A4A3F93BE2C5BE670B5C450EA5040599BD60A57AD8A293B659161416BD61E5A9A9A11C8C8C1280F02AA98C62BAE7C60E5F5F79FA0B5C8A6596695506520CA933629A8377350769EA66E7F258C92421BFBC49EC13F7EC678A7F4A91E77E04B45B6DBB68F8B78C7B38982FA666E23C8E5F5B06438196416710EE02E1D834B191DC263133C8F31A062F8377A513D0C47569507F7332B09D1DD2035910621665A9AF641C2BA04E3AE11F61DB20883BA3E700C4C6FB0427D22FAE5192062024CF36BA99FAE7C198932E05A77C8F191DDC3F3EB21482C7E5F2805E1EB818763F70E2F510D9FBBF4011F242B6902EDD9117CBCBB89F3708D6F98C5163753CAC8DF8179CCA270BEAB19F451EB504D8A73F610FCA02F61E6DABA6F08703C2EF130B73A79A91F94D30865D3C38F85B34CCDED331249A4F2ADA591B1D2FE012F14547C0ED8AF91DBDDA42DB729FEB5453BA8AFFDC08938425C6BD796B6811EAE718B86AD90B6C6D42BCCE0E4648A5F37CC43184148F88C092CAD5234BB2B17EB035BDAA8200574CBD0C1F622C2712E9440AB127A9409421E6ACCB93740B8B956A093A414655AD96FF4D19CB0A324550AF74A14E4583F8DC140B3A0D7AFDF4B7F4A46476942D8010B42C6D40CCEED6D20C01B256DFA74D16511FEE9951FC9CFB9AC4735749D287095184050718203734C00E780FB94F49D2DD22DA630AE698EBC0F9EFDCCD9F3BFC6BE7700EEF1ADE288602C77E9E44B5D53F4A0A45E6EF6803030BE8C353D43D704F307CA53AB623245DADED26173E75EA6C93F72A1A50FBFD5B6C91CD1A7EF7142A88CB263E8F554E2C34C28FE8691A7F01BBFB751FEE5C5369D49D456511C3EA6E1232EF85AE89DB45844DAD959B81DA0663268BAC3BA9CD1CC170D6175495EC77230176D740A284E0F53B7F6002B238BD6516C4ED2CF050FA7AD5405AA085D4759672278CF6B99D2C292F1CDE7AF00DB21EEB6AB5694E73C7EC058D4CE4B506D436DC0DA748994543F846EF29A2CA790F2C8882CA59CBF22EFBFDE86AB15E7422806A6D6A43F1DD6C6BCCDA77A69034FD4860D69E969E34AB891DBE139412F6B82A95BF04260D883ADF6FDAEF5CE0E68E40E749FEEB5F413A5EA8B2B8A449A527D0451A221AD2F314D5BB141DE57A9ACCE35EE50904D3BA238CD99387DB657456311ACDCA9B8BAB42FFF4716E59AC2247CF6C13E170297C11534AA17E5FBC7162C74E5E8E07A22C4717C0CAC287043F8BE194D53227DA674233FFF0B9D48B83A56659DFB64AD498B2DA63D4B7A38C826D06E834B23D57EE374AD4A6F3AF1E7E8628C98763252613E7E7B34A83D763E883DBB9BCBC903CC3E59878799A935425C540D3DF70DEE2D1A8BD5D2BCB541588AB0DDA0BEBF8DE7D6C2322A8B186DA3CFFCF9C17DFBD10209721F2C1B88D3C7366F8D23DA8273775D7C5E9F135022691036385661D8A2E1614988089640C6B3803D9C9D7C9C3E87DBB7F6AB289BD2399E8CE4CB3D6AD042E1783C9C39E620CFD718D312AEB5D780EA6B409C69F6091F83B081F2BFB32DDB8EEB6B88C91B5F340737C8601C2295D94CFF384A99C7F49818E0BF571FF325132A34869A0A6F060003A2DE43B3986F98A263228189F617F30A0481589AE5E396673FF1C412C18AD181C11D6C52B8B4EDB47A10BBF2E3E83AE368FC55F88B7FD16A9EAE8154B7F7AF88A10CE3DB69B7A23048458764622F8C25CDAC47B8CABAC8A5F72B03FA8376EE03D937827EC3D3B56CE3819BAC3603AC5E1D2DFD0306E4B468C8BDF1C24BA6C0145D57839D6EE91C7DB40DA88A54EE8C736D1C", + "235360D7AB0AD5022F1DB7F526206F4EE98199740DA36EBB5A77BB78D3FB9195", + ), // valid decapsulation - ("9AA71AA2A41ADC5799CF8598440A0B22F66BE33203411B8D8CF63863665D3297973C034125FA23EBB6279EE9A2A9C235DFE12495B0C5B6B822DD971102632E27678A619994A2BB9AC28516995335E0F57D925357C9A0289CBBBCD74060CF8B9BD77508AF520059BB243F8C99D85A37298908BF988C514457320200C444C00DAB905C61842B92AD331011C4FA45410A462C9AA5D189B5A69820988A08563ABD81D520E288462E7596E922579301A817B440396B89F8FBCDE2A794EA25798E436F94D43434D7267AD306F1009B50D46B53158E7921C50D5407D4D266C62C9CBB0086F41C6CD5A252648B97468C4DC16694A4253CF79141952447CD7AB88179C843BB5A7B158D4E5919789987937ACD5DB73CACF65A4B359975B4780D574A47424BB14904E6F16D17E255FEC75EAE4660348564CA8261AD826A00B6C845E1871A8C059B5A8607E8CAA3074EB654A624AA1FE0D9CB9C7C09483025179B9BFBE3392AFA627455460CBA582B11373CB0BD0BC16609725F453B4C585665A3F32D0A692DFF567FCA659356CA6A56F204FD3435361AA57B57B414B92A5EF14D575A07EA768EAD04A6F066573D21792768AD6219A308E52B925CBE5743510ADA541AB440D2F69B062A83E117CD840791AA55702A211849F2069DFAC98D35567812755D264CBB828D8C089E180640698A3E3BB737CC9C79391BA83248BB1F82AE12D80F38B5678A9A369E19AD60B3A70F447581FC75DA0291B9E18BCBCC81485A1CB09210BDAABCCCBC07C5B563C0F84F210B9198DB1AC4A92FEF4504A788CE245954881A94D0F825E1D722E68C1300CBB6C3E828691C69ACF6C2BB896805565A7C3504CC347B2401A2AB767F26EB1C63352377AC2AB05A2AB30017C4B35D16CCC9858542276407C9B7C86BD37585B384B66CA158FC540431621CA40CB9919C9694522ECAB7C8D9A7E01413F14CACB44B1207919D8EC808F1816E61235F0AAA110AA5177C5184E220C89C66B8F80C71AF2B7C67834DBD8709B03844399658DB19CBAF6A8298F8402B4A2EE5A6244C639A4456146029A09AF3C2FC1A2BEFE87752180240934D5722734D1BB4E6CCC7C9634C52A83EBFF20674E9982A93AA32121CBCD52153B9B03CC6A099FB77B6D3C15D66B7D945C1D3E524D02712A5733F6FFC3322A808D1833B6D78CF3088A3818038316639950C4B14464F55B1642AB59693601958CC4DD50C3E33626BFA83ACC8F82880A86290F60D23171B81F9B7FA304181AA91B6B8AC4F3132FB58660BD06FF982A3925CAAEB1867E2B60A52905E07196592A0A2E1DA636E56296177436F017FCE2B3878C98E4479A5762AA7C11928C9298921C3AB3427A9CAE828CC67CC08DB754DBB2F485905B114B085A3B6146728048714C8A0C8A8CCA95D6593937B916BA2A8E6A6287808111B828686427955C741CDBB4E6C5BB720873370D089CE3753418102B0BB5B95648CF42936610399A528982695BCB90182CE31C5D2256852FB1FB85B549387294AD1C78AE455B5586F5CD24EB3493ADB1682D2169C34A7B117A9759533038A85AFFB40C222FC9C1CC96A09879298671E6EF0BE221336F1505C1B3275319A056191A99B160010C51D93492123D07E5DC6B7A851ACC63C58E2FAB86A893881F32725AB8F4BA8262BF21EC4AB5A7275B17914B45E102E5FC71602D48142B6745F9BCB5D871932627CA3A1721FC462BED89E90CA92A25A87DE762C90CC5E7DC9514DA74D971122AD378C866B6C058CCFF1BA5ACCF5AC84BC2A4CCB4FD4F577EDEC0B11D55849E6A6A1505797C0218AD7A84A4C759845A1B6A44DADF4C05B88CD1689C59BCC078896C292EA960C6C1E30DCB7CC4C9D5A96257385B48FDA842DD69BE60B5C15272A26683A1B214C1B1C3C9D55095E7A39FFB038AA827250C5AB229BC6BF47A7BA0ACBAC552AE6516E5E3B9DCCA4B788C39AD7F5B8415B149021C231282E6A012F06E02843A5B389AC225C871D333C8E3F8C311DB81866E49A99978BC9C7910114C58F179B667AAC17ACC676954A77C916B5B4616E67BEC103675662A7186B17A7F4B05B00A572726E4AD40BEA38BFB2C825D9525AB0B099FC07561F7C2E44C5810F8531BF634A71EC83FF85320C1592969451C0346916A497E0BA374DFB2020A68EC156BD9E3926F4B4A26C5404E4446B42F90263611F706C1860F093BD195599B761BB0B73AFCAA8E78167202747C3696D68B3854E4A7657290710686D31F8838342BC0BB2333C112805BCBAF8C70B0B108FD877CC3619B4665751A26B315C9C49CEF4199EE55464522E0CC7C324E7C9289ACC1A2163DC950EAB210BE14461F823395D66849E5B73B963126C6A95B3CCC75454325768B72957C6BFFC62B36AB20924B0E7A88C6A403D70343F6F23182210116BCC8FEBAA8861365129226B0D2BBE366AA92D7A365E4B2B4CF2BC32D62A000CA3F501BC1D7CCBE6A5B872C65A7C145ABDA15243662838568644DB8F889C445152C49C47C16B902A797AAFA0608C8377097A627DA66A37D77ABFD3B86CD5B265101276E7F79ECAC1A7F038367FF001F6516A5FC13AF95059BF530EBADA24271B1B914095B5DABE4BBB7CFAA9AD95B976A5C1BE62C35E8D60127960204D5B71CB707952F69CF1AB8FD95873C358AE9ECC34557ACC6EDB45FEB640E9B23DB3F438B079ACDD0038A547CD928496ECA44C10576A9A9C5B7810483DB1661397A0816A5D01253657E12CC867AA40B0A80F5414FB30B5805A4BB01806A185BE9B7B8B30C8704FDBBC80B5C6EE914165F16DA9F613C4A391611A8CC895AE42789FA58C873E10812F237FC5A4C374E708181B66DB99547250373BC842313697F64B50D177C9467A83EA909F74BA0F6A387352031CCF2B7EE26255816629E61314B6C702DF825CD603C5DAF4700D3948AACA815A7663098214A9C91849047AF07BA1703A4FEF4391D0565F28C807C4F57300E4728FA2456837773B7C5A911C75ADE8C268A16898FB60A6A66B6E083D9D8BC6017627A8C2732C05C526F822A68B60F16024DC33C7D93C6F3B6A325145B71D6C28C82556D695C279F4988525C98273833311A7A06674C5299CC3FC5E0A25408B51C5D91ACA7BF81DE5C5923C7282F6D82E78BCAD8CD8CE9244AC5D8756ADDB8A28440055F253FAB00979738C60A44C86CA4BA3F9C31716C3B621C9CE402CA827B097EC1750B3A158AA681E1C1093A84793703C7CC0AA43D37F91C0683B206ECA2168CC392ABE42BB32F9C9B9451C377A6A9C195845ACC4B2AC80CE7535467C3D9595C1C7C121F459BC73C3A4F4C069B3215A02BA4D45A1ADF6DA4C9D59AFB73844D86BC1ED3AB0FC62B5F685B83C79C417D7408B562AC4B344C9638BE54695CABAB21AB262E470570D842A4F958DF7C8B1FD5CC0EC0287B094810F148831AA424CC930A0B7C5AB21887E363804B26F985BB673169C4B86AA0D66AD044327697C45CC0485F3DB40EFAC2852C26E81F70687CC3D2774AEB8033FF63C0846A2862287621DF8B5BE08956381AE18A20EAB49C3EE3A1C2A82B54B86696EB3B6400BBBB8F52F739B323B8BBB9C1C269FD488E22B9A8D41227556168F253278D00475A07B7DE404EAC80118696F98E77B65C515F643C8E2ACA6786157DBDB439EE48D17865B59BC3535336CEB9442A6D5C8491095879AB90B5CA372B18E7359113DEBA51AB2C2392B92DA062E1B250DB639BD709543DC4703B3521A5613C8E89AAFF76CB8F93373D840A3BA87B56AE10995CABC86AC2B5892C343BA8835A433E32B59CBF90BB1CAC0CF9BBB9B0CC659D0681840867F9187C5260D5069C7CFA617E523947495A278D6AD32F9B19A276B1831AA5EFC0445209D2CC3CA952B206F86B93767201D271130E2B495D1A66F22392A58160A314FAF423A3060014A159F124053ED79B2AE832B1703CD5D76A67AD64E1992394ADA88FC68A8674A20B09066F12736F665CDBC30CB51A5A651EABD2DE987E2FB0CA1B121B0277BBAE3BE230637DBB6AD55CB927916CB2E8869A80C36C8242F6A46C5B4B57094F76A6DDA1438DB975FFCC2EC9CB0BBB78376798297747104189AE4DA13365B093268A41BD304FCE5ADD66512CC076F8DE7791DD9ABFA756E6D1C04EE1A4FD7C141028A7B6F3AAD3781B280E4252DB85D0D7795EF77C4AC391355EBC180506A94766B4D13594F50666199A949F694C4571A93C6952DE2C8E55335B8607DC0F84D1D62B8B9569597448B9EFB5A6EA82C39B5C6F2581F6B3109C673A4D28A86FF6298AEE75A01ABB15039BAEDF4C2E347A89FA614BAB3A8B338C5184A993CE82CAFB842741039F45725713030E923118459CF5C5C2B0603F02C089DC58D7C7AFCF9A3DBDB7CBF2FCC47046FA4924A893ECEE82853F3AE705BCBE4507ACB82D811FCF2667F124208BAA6F3165FD2F6A77C62C392B20FD160AFD9A075A59D74A9C1122F73700049F1B411D124572E9029B2C3D57D7E28", "57C9278E2250D688E41E9F98CBA4074DAF22128126E0B09D7826D982461828CCF5B4D8FCA19072A6B796BB4825B4E7D9F3E2C922836ACEB6672C76B6E7603EBD665DBC30FFBA215CD317E0D16C2C892BC56948EF165632E5C2544A55B119005F7768BF1A635949A61F00D5904621C4DB978A7AC566216089C25C32AECC265EFE13030ACAFACED4E3DB63C93FE7849AD1FA842A1C3D77DDB37073AA593AFD15D46EC86067CED8DA5C5BC771ACC41A950F98E50DAF35A86951C4D928D3C1D05251E533B0BEC57C74C58DE7EE2429ED08E2E8BE5CB1132A69DFCD1255720FAF9BFDA2552B049DF4A7CD7B5B12D66E7584ED527C0F6B3E7C38143130FA98EC2C94E309DCBFFC56C6CDC15934D1FBF74DEAFFA6EE46C0B7424D5949EF084238180B3FE8C9058D48219BD08D4367B00B74E28AE54AE8C64C329296A1A35AFEF745F87CAB25BCCF38394E91BDAA15C8D711763D58540660EAE86BC9B6704879AA4FBA36613B166880788F8139507711D1D4A46A070A0B5EAD8CE70825BEE250478C390D5149B7D7613E9D6D2B3EEC0ABBA67C501EA67BDB1EDE0112599BBAC417E58B88D8973C26078466EA11CCFEE22630B37620574CE4A0412302780CC546C9405D5B9EE9193D31645303A31392D05F3AFC6959F2D0335ABE5F5EE0F1B81880A0695825913ED6D791DA8B5DCB8A86450825F661B5774DF45F7FDD7D5F8F80DDD3C135A619C271E3ACA5715F2A81D8DFA4B4397FEFE724D2A92B7D4D085C84F7A71C226BE195416F998131B2EC983373DDBC20248D82E2A48250D37BA76384E96379B1A41FC159952D37D453BDDDF24D85C73FEDBC2BA63BAED075F35C77A26803ABD014380B4397EB202FD796FCBDD3F68DF299349C019BB14A497DA31150047601BA4A167AC74A79C53EF32B1698BDFB31A5EBF2C8F3FA80AC67DDDE04CD545F7B8AE2076F95B3CAF9B396D323E29C6314537CA02B668DC719AFF7E688DAD24EADC1A866AD7C32F93BE2DFFB51C344D8415636A96550B0512E0491E480A893B638B3F0F645B1473E83CA424DDD7B58C6F62AAF7D8736B31B6913272E057261F222BA59343B25D63C311C4AD984FB336D14D354BBA085A27884D3F0830D972235BB8FA95B97F7F57F5D4706DE378621BFA3A2995A03626F9608EC48819786C89C611A4E3366E167E7F3ED5A19207E7098E49935EED462D21623230AD670BE46F207705FDC64190D48FD7B9EC34F37133E94D672B995DCB08362002E3943E6DD9AB7D146862E0CF3DAA0FA80DB65E7F4C846534910B48D79E892CFB4CF1388F3A5CAF7E8E1C5338D6D7D7EF3D96F4F89A1B8D31EA11750EB7985998543516B5D75D744B843818398737CB62D24BE242B6273DA9E23EC59DF862EAA7A8303FD093B7977B3E3E8DDE64E2AE7025377462D65230572E0AC9A84D81001931995D9FF84ECE6BBF4B172D92C3164C1B67C30A7F6C007E59C92DFDF37670B0C4CBCABC0DE454844B9DCE266B1EAED757AD5518F6496048366962A0B9190AD2A721597C98B77769D7FBA9CF993B7462E2492D5062D3AD7D3D452F1730F51EEBAE0D932FC09E9ACB21E547E87ACD013E8E82AC9A52AA16D8AF0F956A769C6E575A59A294993FE5FBDBE46E4DFB11A6CC393FF4EC236C8155C0C1026AF72FF519EAE5D2BBAC2B24F94432F1098FD4A3AA6A6BE75F4133CCF1A94EC22DA0AFC29F67CBF11685F63CFDB827801AC60A0328B9F1D3404E63AA255B1F44750FC9C1B7535D8358AA1AD27284535D0FE9C127738880055C0713D265F7117D835CBF65C9DB7879E36D3E7DFC1BD61EE6851B62463FBEEA44F6E706168AEB7B15A52FDA2B385D47DDA96E2FD12D98BD0F19AC87747FA4BB88A4575DE60789BD44CD8EC2D15D547E8E11AD2A80BE7F13AE9C18DDD7F570C9928F478D6D72328AA7F1F7939D6B8D1C64194DDA26A20C819C478023377E207FA3CBBEDCFFF19DEC883F1D38F5620957E0C7D66829B7A2D490927106F7C2E425684F20C5F91FF763F35A92F4344343C5AA9783119EF7BACEE928A756E51CCF05BD607D1DE15DAF663B2C8F26E83A4B19CD97E20EB3D89E131B4B15C5B362046443E795D47B0D9032A693AD5AE938FC166792E88B943EF9FAE6A36C7CC9EC889570C5D314A246CED6648FB43DA6877BD8700EF40CEE8657E6640E984B96351B7FE265133EEED5AEED0C096AE94B21DC42947A043BA", "A1CA2B2B5C45CD210FF16BA9CC49BF1F6E34C0E1E1DD3C8DEC45532074DF3069"), + ( + "9AA71AA2A41ADC5799CF8598440A0B22F66BE33203411B8D8CF63863665D3297973C034125FA23EBB6279EE9A2A9C235DFE12495B0C5B6B822DD971102632E27678A619994A2BB9AC28516995335E0F57D925357C9A0289CBBBCD74060CF8B9BD77508AF520059BB243F8C99D85A37298908BF988C514457320200C444C00DAB905C61842B92AD331011C4FA45410A462C9AA5D189B5A69820988A08563ABD81D520E288462E7596E922579301A817B440396B89F8FBCDE2A794EA25798E436F94D43434D7267AD306F1009B50D46B53158E7921C50D5407D4D266C62C9CBB0086F41C6CD5A252648B97468C4DC16694A4253CF79141952447CD7AB88179C843BB5A7B158D4E5919789987937ACD5DB73CACF65A4B359975B4780D574A47424BB14904E6F16D17E255FEC75EAE4660348564CA8261AD826A00B6C845E1871A8C059B5A8607E8CAA3074EB654A624AA1FE0D9CB9C7C09483025179B9BFBE3392AFA627455460CBA582B11373CB0BD0BC16609725F453B4C585665A3F32D0A692DFF567FCA659356CA6A56F204FD3435361AA57B57B414B92A5EF14D575A07EA768EAD04A6F066573D21792768AD6219A308E52B925CBE5743510ADA541AB440D2F69B062A83E117CD840791AA55702A211849F2069DFAC98D35567812755D264CBB828D8C089E180640698A3E3BB737CC9C79391BA83248BB1F82AE12D80F38B5678A9A369E19AD60B3A70F447581FC75DA0291B9E18BCBCC81485A1CB09210BDAABCCCBC07C5B563C0F84F210B9198DB1AC4A92FEF4504A788CE245954881A94D0F825E1D722E68C1300CBB6C3E828691C69ACF6C2BB896805565A7C3504CC347B2401A2AB767F26EB1C63352377AC2AB05A2AB30017C4B35D16CCC9858542276407C9B7C86BD37585B384B66CA158FC540431621CA40CB9919C9694522ECAB7C8D9A7E01413F14CACB44B1207919D8EC808F1816E61235F0AAA110AA5177C5184E220C89C66B8F80C71AF2B7C67834DBD8709B03844399658DB19CBAF6A8298F8402B4A2EE5A6244C639A4456146029A09AF3C2FC1A2BEFE87752180240934D5722734D1BB4E6CCC7C9634C52A83EBFF20674E9982A93AA32121CBCD52153B9B03CC6A099FB77B6D3C15D66B7D945C1D3E524D02712A5733F6FFC3322A808D1833B6D78CF3088A3818038316639950C4B14464F55B1642AB59693601958CC4DD50C3E33626BFA83ACC8F82880A86290F60D23171B81F9B7FA304181AA91B6B8AC4F3132FB58660BD06FF982A3925CAAEB1867E2B60A52905E07196592A0A2E1DA636E56296177436F017FCE2B3878C98E4479A5762AA7C11928C9298921C3AB3427A9CAE828CC67CC08DB754DBB2F485905B114B085A3B6146728048714C8A0C8A8CCA95D6593937B916BA2A8E6A6287808111B828686427955C741CDBB4E6C5BB720873370D089CE3753418102B0BB5B95648CF42936610399A528982695BCB90182CE31C5D2256852FB1FB85B549387294AD1C78AE455B5586F5CD24EB3493ADB1682D2169C34A7B117A9759533038A85AFFB40C222FC9C1CC96A09879298671E6EF0BE221336F1505C1B3275319A056191A99B160010C51D93492123D07E5DC6B7A851ACC63C58E2FAB86A893881F32725AB8F4BA8262BF21EC4AB5A7275B17914B45E102E5FC71602D48142B6745F9BCB5D871932627CA3A1721FC462BED89E90CA92A25A87DE762C90CC5E7DC9514DA74D971122AD378C866B6C058CCFF1BA5ACCF5AC84BC2A4CCB4FD4F577EDEC0B11D55849E6A6A1505797C0218AD7A84A4C759845A1B6A44DADF4C05B88CD1689C59BCC078896C292EA960C6C1E30DCB7CC4C9D5A96257385B48FDA842DD69BE60B5C15272A26683A1B214C1B1C3C9D55095E7A39FFB038AA827250C5AB229BC6BF47A7BA0ACBAC552AE6516E5E3B9DCCA4B788C39AD7F5B8415B149021C231282E6A012F06E02843A5B389AC225C871D333C8E3F8C311DB81866E49A99978BC9C7910114C58F179B667AAC17ACC676954A77C916B5B4616E67BEC103675662A7186B17A7F4B05B00A572726E4AD40BEA38BFB2C825D9525AB0B099FC07561F7C2E44C5810F8531BF634A71EC83FF85320C1592969451C0346916A497E0BA374DFB2020A68EC156BD9E3926F4B4A26C5404E4446B42F90263611F706C1860F093BD195599B761BB0B73AFCAA8E78167202747C3696D68B3854E4A7657290710686D31F8838342BC0BB2333C112805BCBAF8C70B0B108FD877CC3619B4665751A26B315C9C49CEF4199EE55464522E0CC7C324E7C9289ACC1A2163DC950EAB210BE14461F823395D66849E5B73B963126C6A95B3CCC75454325768B72957C6BFFC62B36AB20924B0E7A88C6A403D70343F6F23182210116BCC8FEBAA8861365129226B0D2BBE366AA92D7A365E4B2B4CF2BC32D62A000CA3F501BC1D7CCBE6A5B872C65A7C145ABDA15243662838568644DB8F889C445152C49C47C16B902A797AAFA0608C8377097A627DA66A37D77ABFD3B86CD5B265101276E7F79ECAC1A7F038367FF001F6516A5FC13AF95059BF530EBADA24271B1B914095B5DABE4BBB7CFAA9AD95B976A5C1BE62C35E8D60127960204D5B71CB707952F69CF1AB8FD95873C358AE9ECC34557ACC6EDB45FEB640E9B23DB3F438B079ACDD0038A547CD928496ECA44C10576A9A9C5B7810483DB1661397A0816A5D01253657E12CC867AA40B0A80F5414FB30B5805A4BB01806A185BE9B7B8B30C8704FDBBC80B5C6EE914165F16DA9F613C4A391611A8CC895AE42789FA58C873E10812F237FC5A4C374E708181B66DB99547250373BC842313697F64B50D177C9467A83EA909F74BA0F6A387352031CCF2B7EE26255816629E61314B6C702DF825CD603C5DAF4700D3948AACA815A7663098214A9C91849047AF07BA1703A4FEF4391D0565F28C807C4F57300E4728FA2456837773B7C5A911C75ADE8C268A16898FB60A6A66B6E083D9D8BC6017627A8C2732C05C526F822A68B60F16024DC33C7D93C6F3B6A325145B71D6C28C82556D695C279F4988525C98273833311A7A06674C5299CC3FC5E0A25408B51C5D91ACA7BF81DE5C5923C7282F6D82E78BCAD8CD8CE9244AC5D8756ADDB8A28440055F253FAB00979738C60A44C86CA4BA3F9C31716C3B621C9CE402CA827B097EC1750B3A158AA681E1C1093A84793703C7CC0AA43D37F91C0683B206ECA2168CC392ABE42BB32F9C9B9451C377A6A9C195845ACC4B2AC80CE7535467C3D9595C1C7C121F459BC73C3A4F4C069B3215A02BA4D45A1ADF6DA4C9D59AFB73844D86BC1ED3AB0FC62B5F685B83C79C417D7408B562AC4B344C9638BE54695CABAB21AB262E470570D842A4F958DF7C8B1FD5CC0EC0287B094810F148831AA424CC930A0B7C5AB21887E363804B26F985BB673169C4B86AA0D66AD044327697C45CC0485F3DB40EFAC2852C26E81F70687CC3D2774AEB8033FF63C0846A2862287621DF8B5BE08956381AE18A20EAB49C3EE3A1C2A82B54B86696EB3B6400BBBB8F52F739B323B8BBB9C1C269FD488E22B9A8D41227556168F253278D00475A07B7DE404EAC80118696F98E77B65C515F643C8E2ACA6786157DBDB439EE48D17865B59BC3535336CEB9442A6D5C8491095879AB90B5CA372B18E7359113DEBA51AB2C2392B92DA062E1B250DB639BD709543DC4703B3521A5613C8E89AAFF76CB8F93373D840A3BA87B56AE10995CABC86AC2B5892C343BA8835A433E32B59CBF90BB1CAC0CF9BBB9B0CC659D0681840867F9187C5260D5069C7CFA617E523947495A278D6AD32F9B19A276B1831AA5EFC0445209D2CC3CA952B206F86B93767201D271130E2B495D1A66F22392A58160A314FAF423A3060014A159F124053ED79B2AE832B1703CD5D76A67AD64E1992394ADA88FC68A8674A20B09066F12736F665CDBC30CB51A5A651EABD2DE987E2FB0CA1B121B0277BBAE3BE230637DBB6AD55CB927916CB2E8869A80C36C8242F6A46C5B4B57094F76A6DDA1438DB975FFCC2EC9CB0BBB78376798297747104189AE4DA13365B093268A41BD304FCE5ADD66512CC076F8DE7791DD9ABFA756E6D1C04EE1A4FD7C141028A7B6F3AAD3781B280E4252DB85D0D7795EF77C4AC391355EBC180506A94766B4D13594F50666199A949F694C4571A93C6952DE2C8E55335B8607DC0F84D1D62B8B9569597448B9EFB5A6EA82C39B5C6F2581F6B3109C673A4D28A86FF6298AEE75A01ABB15039BAEDF4C2E347A89FA614BAB3A8B338C5184A993CE82CAFB842741039F45725713030E923118459CF5C5C2B0603F02C089DC58D7C7AFCF9A3DBDB7CBF2FCC47046FA4924A893ECEE82853F3AE705BCBE4507ACB82D811FCF2667F124208BAA6F3165FD2F6A77C62C392B20FD160AFD9A075A59D74A9C1122F73700049F1B411D124572E9029B2C3D57D7E28", + "57C9278E2250D688E41E9F98CBA4074DAF22128126E0B09D7826D982461828CCF5B4D8FCA19072A6B796BB4825B4E7D9F3E2C922836ACEB6672C76B6E7603EBD665DBC30FFBA215CD317E0D16C2C892BC56948EF165632E5C2544A55B119005F7768BF1A635949A61F00D5904621C4DB978A7AC566216089C25C32AECC265EFE13030ACAFACED4E3DB63C93FE7849AD1FA842A1C3D77DDB37073AA593AFD15D46EC86067CED8DA5C5BC771ACC41A950F98E50DAF35A86951C4D928D3C1D05251E533B0BEC57C74C58DE7EE2429ED08E2E8BE5CB1132A69DFCD1255720FAF9BFDA2552B049DF4A7CD7B5B12D66E7584ED527C0F6B3E7C38143130FA98EC2C94E309DCBFFC56C6CDC15934D1FBF74DEAFFA6EE46C0B7424D5949EF084238180B3FE8C9058D48219BD08D4367B00B74E28AE54AE8C64C329296A1A35AFEF745F87CAB25BCCF38394E91BDAA15C8D711763D58540660EAE86BC9B6704879AA4FBA36613B166880788F8139507711D1D4A46A070A0B5EAD8CE70825BEE250478C390D5149B7D7613E9D6D2B3EEC0ABBA67C501EA67BDB1EDE0112599BBAC417E58B88D8973C26078466EA11CCFEE22630B37620574CE4A0412302780CC546C9405D5B9EE9193D31645303A31392D05F3AFC6959F2D0335ABE5F5EE0F1B81880A0695825913ED6D791DA8B5DCB8A86450825F661B5774DF45F7FDD7D5F8F80DDD3C135A619C271E3ACA5715F2A81D8DFA4B4397FEFE724D2A92B7D4D085C84F7A71C226BE195416F998131B2EC983373DDBC20248D82E2A48250D37BA76384E96379B1A41FC159952D37D453BDDDF24D85C73FEDBC2BA63BAED075F35C77A26803ABD014380B4397EB202FD796FCBDD3F68DF299349C019BB14A497DA31150047601BA4A167AC74A79C53EF32B1698BDFB31A5EBF2C8F3FA80AC67DDDE04CD545F7B8AE2076F95B3CAF9B396D323E29C6314537CA02B668DC719AFF7E688DAD24EADC1A866AD7C32F93BE2DFFB51C344D8415636A96550B0512E0491E480A893B638B3F0F645B1473E83CA424DDD7B58C6F62AAF7D8736B31B6913272E057261F222BA59343B25D63C311C4AD984FB336D14D354BBA085A27884D3F0830D972235BB8FA95B97F7F57F5D4706DE378621BFA3A2995A03626F9608EC48819786C89C611A4E3366E167E7F3ED5A19207E7098E49935EED462D21623230AD670BE46F207705FDC64190D48FD7B9EC34F37133E94D672B995DCB08362002E3943E6DD9AB7D146862E0CF3DAA0FA80DB65E7F4C846534910B48D79E892CFB4CF1388F3A5CAF7E8E1C5338D6D7D7EF3D96F4F89A1B8D31EA11750EB7985998543516B5D75D744B843818398737CB62D24BE242B6273DA9E23EC59DF862EAA7A8303FD093B7977B3E3E8DDE64E2AE7025377462D65230572E0AC9A84D81001931995D9FF84ECE6BBF4B172D92C3164C1B67C30A7F6C007E59C92DFDF37670B0C4CBCABC0DE454844B9DCE266B1EAED757AD5518F6496048366962A0B9190AD2A721597C98B77769D7FBA9CF993B7462E2492D5062D3AD7D3D452F1730F51EEBAE0D932FC09E9ACB21E547E87ACD013E8E82AC9A52AA16D8AF0F956A769C6E575A59A294993FE5FBDBE46E4DFB11A6CC393FF4EC236C8155C0C1026AF72FF519EAE5D2BBAC2B24F94432F1098FD4A3AA6A6BE75F4133CCF1A94EC22DA0AFC29F67CBF11685F63CFDB827801AC60A0328B9F1D3404E63AA255B1F44750FC9C1B7535D8358AA1AD27284535D0FE9C127738880055C0713D265F7117D835CBF65C9DB7879E36D3E7DFC1BD61EE6851B62463FBEEA44F6E706168AEB7B15A52FDA2B385D47DDA96E2FD12D98BD0F19AC87747FA4BB88A4575DE60789BD44CD8EC2D15D547E8E11AD2A80BE7F13AE9C18DDD7F570C9928F478D6D72328AA7F1F7939D6B8D1C64194DDA26A20C819C478023377E207FA3CBBEDCFFF19DEC883F1D38F5620957E0C7D66829B7A2D490927106F7C2E425684F20C5F91FF763F35A92F4344343C5AA9783119EF7BACEE928A756E51CCF05BD607D1DE15DAF663B2C8F26E83A4B19CD97E20EB3D89E131B4B15C5B362046443E795D47B0D9032A693AD5AE938FC166792E88B943EF9FAE6A36C7CC9EC889570C5D314A246CED6648FB43DA6877BD8700EF40CEE8657E6640E984B96351B7FE265133EEED5AEED0C096AE94B21DC42947A043BA", + "A1CA2B2B5C45CD210FF16BA9CC49BF1F6E34C0E1E1DD3C8DEC45532074DF3069", + ), // modified ciphertext - ("5D75CE24C45E5C5C6EF29A047A632869E8001D28CEFC0A7B15F8A7ABB027707183C007506BC2A2C4966C5B5BB993176EDFFB50BC41B8A7FBBD14C0180937C9B35224795343A0F782DFA118444338E7C20E1C7A4D93751B0598C87914C26C8304A011C42692C76028670AF61474A40AF93196DF037A32053FE88C173CB878BF467DABE7973397BA3D667ED2BB38672975FE6294F41077EB9B78DA999B3FC25CB753251DF09E32131E5343B8E8B5898548C84A336A796447A0C5A2DC1CC2F6F15DC5D480CCA771FDF668DAA367B78AC079305E16387EEEF58079A9070B020DAC4CB1AA25B6E5D19DFE4BCE6C4303A2469B9A850D35B846BC29AAD8455ED5E1662D56B82F3606CDE7CBABE8840E26487D9080E0134A7A4B0D79D69C53059F6449BB4FD51A99F9313E9446124218A30738FB01CAE8C58545E22DA97991500A781F810437BA4904FA964DC916D75247CF686091C180DCC87861F7B7067B6D08283E3641BE61531FBA8B4F6D1B417E8B312C093FEA0C17F8F5BBEAAA2E118C6E51F52698671C93C5BF3E36BA3C4596F5A33D460C163A903FC76872FDB9075A593BFC05D0CBD1BCDC276C453C2775497E77B093D1034330231B9FC8BAB4EA7EC9832E5776677325397A1608BCC2B60D573D28A13452495D6F8302D6A6B5C72C6D44A19FDB9488CA580F63222F6D15A1F190B8FABC555248AA3B4B84DCB5C811E717CE636B86EBA7BCA0B92139C01F49AB26322E5771C35ED307A0856397285DF6B5CEABA543A67C33F8AC61365691BDBA67003C1B04403450C63E790B9ADC3BA6EB861A5DA04898EC5C349C9C46D542E1413DA82137FDC2A53A483B924709D0D0CBAF18C39787584C0CC3C4F05F943C89B59089BCD7A160D04418B27916946DBADB813DC9577FA837A037CEDA32BC47E9ADC385654667904EC429198925EFFB7E0F4591CF4439EE16330F5A7F6688248A416B79316231426094A990E3F44C49A11C5B15191C08B914D42A96D04B6C3651131BA5EA80291F49A06F30A07F944E81A9A7A7642BB8D711BDE11F57F0A7F1D5CFC853046338CE8BB36DA038AF1CE5C308F36D4F02AABF9ABDC4F13377F755E3E2C1A1FC542544199078922BC110A8E96A8C235E50689C63343513685BA21B9F806C729293365A2C7066773B9A1972413A342BE3029CB61B8DC951A53423F621CE7FA16072D30F675C63538CA5891085610155D2211D2B25C7B7F347E08566D7328E7E62B8A6CB62240BA74F9751B69544E937BA9611A4214472BAB01B5D53A070E32082C057A1773B149C9BF1115835B6497B2187AD7C4B3679296F016C3B2B25EB451D2B976DDDF73195026DB23A6DC87B4108D1961BCA1B4A6885CB54C2BD737ABC390CBF69A356A452740CB323F38995857DCF2106E567ADA74281863C8EB264CC17221481B8228978246C2C83B9E9064420A64FB7A30D061B9FC610C7762412E475C8318559B108B7895F20E88C8B09839EC88CD6140F5F9AC46DD08D6B62C1F82B3909F68B8F7C85CD7BCD8378CD7C1B5A34D74A10F3CEABE98C2BEBBCBE9C82B46314C9F0288CE50FF8B36E28677A6CB311ACB7A4A873AA257544562943306964A1753DB2B57CF0641DC111B1227342A64B47B88B6056916521D82F75E9A6E2D8150B102E170200BE1A27177AC9D4C49BF1AB70966109FAD83E448384A6FB3C126A5F19C0A484B3297A329AF32A31FC52691EAA447F081A34F1086CD4B1E959089EF64DBE8756BF52A81F925E0D06B222F3C2F767050D7B475E5C2FF1F62C68F05596A96AD9E3269CF0666CB50A4E421FEFA61B255A9E65FCC0149B736BC163B4A76E98EA6EA0ECB08EF364E6A455E0C47B2E29B6C429C11DF04DDA036554AA25B51A5F795C6B2B54C8799460CEB171C2CA6BC5405B919933217B16AF189C47C940E0FC7EF83051411A440CE9C4692B4607C672CE9B52C7F03336711E088173DB4649D16058517461A162188C489623FA0945EC5EF9481CF5D7AB175C9010E5CD98B11F736B9C4896539FA32C7BC79897B0BC6C062B0CBA0E5431A2ACE28E8AB09AB27AC9ED4535372026241CADC23C6A2BB4564F6B833437AD300A039CA81377F1A67D4B55215735FE776FE23195E71B665F0B5D694807DED1A55706AC9E3184F24657450964DD834515BAB90A2421A11B5EB141615707B888AA3E6FB89C0C70BA66348E59BCA5463B0AD051C47E50944142A6A10B176FC8ADB9EA3D61C570FD2AB344AA3C4B7A7C15C0B333477A8EE974EC832023A5976A443282FC2C384436ADD1BEB5F09FC8BA63B4207B74FC0CA226CB22585C74E02537187681717808984E52C30734730CFDA686F9B463C69529D42622FAD40AF3899F584AA57685ACADD221068858DE6CC99E22B602AA87FBB079BB1C114E9370D7667DF5A38B65773FC3C0A29D48AB2E78853133049336B99CB3CC3B849793A50EFB587A640858CE4A203C1410660A3267E570BB5A2041913C522CC9E8F772F42C8B6B75A900ABCF4B850C3FC260A5CCB29DF87122362CB7379CC0B7AD6D6CBA07D0BF674B330136547FB7AED92C4DEAA33F7B57A11840324014AB88C077DC933154A576DBA70C9A5B8ABBC5A606C52DEDF12112C7C086D8C01FD59C6DDA7A77ACC94AB182BD70273D3A63B53CBA6A489DD890CF635438734916D34BB0FBA0290BC57246967B774105F8F542D716899E456C26B89E8C1B5A4A0211A2E11E9FBB9321387CC0A85417DCCD5A49A9F5000C77E26EE68BCF2B2AB12D966E30BC1DB5C42BEE2905758798A7A611E217C391ABBB2F953AB2E1C48E19B274468C7FD51F681A2AC5E0298E18693F892ACF746A8AE1B3D974134E55B25EC97D75D192A266564A1A25BE6C53EA34C0463A9CD324824C32B5D24B513476C606DAACBA506892B0827786065915A10F628D68E66837092EF4446A85E99F89A24EDA7410391C811CD41737D763C0A825115B81477843A4F75572E09D96D0945C7A871725C37F9C3D865487BE7C9529A0BD1DA9AFA4D0790CD9C98155BC3442AB990C077ADB40F10C66A3365D958BAEEF5CBEAD780C8C769595017FC6AAA74E3774B7F6850D8B0D560061F70A10CF39A77CE498042A04C6748F25478553F2CE959C30C0D541D23B8233A1CA3F4C7611413D357702E5E80A6856A589F75D9B5410D9D2BE44A9000DB6C8A1EA33CAC5B96854C0637236A22A555CC12100D30AB3D62DC9C614ED806C6B848EFD650728F95DDE762F3DD04B2CC756E24B893D640D85B5B7DA466FF32283FB03C4917858F5B99552E0147DB52880E4825CD6A83AD23CA5FC62324314009CC66F6C32F09C8C2DAAB96969CE457B5B0F9A13B329C13DE02D6573ADA3C67789E4C8CB12B509513B664C47FF769ABF626631398AEC44584FDB2C0CBB04252083BD2A94DED51BF26727E395C21AAB2C1D95CDFCB541F4E5CBF2B0B1716CB24872C8A7E04D38656EC438533450B51194936C3073F3CB36BB20BA69E73B16881A7284B2AE7A5C682439792B1FD5548DFA3954A7459ED08B7387A5169AB74354A916F3EA800DD54C1600061DD7CAC51020E61AA6A1B05E67C196668133EED1C540DA386B8CC2A4DA2CAF6458EA545AFEE0601D55617FF1422D09B04DFCC45F511174449EE701CCAB390B9C4749CEF942C4D14B480AAA656A684A4464A9519F1150B6D0861874A2AB687796EABC2A1E19032B8A1EBE729F6B7AA28CD7864DA1C679A1C17DFAB80DBA8B467714BD05ACF8E5BBFC3848CB715F9C9C2E93628479B9474CD83EE1E7BDA5EB530A590D22C1572EFB9A91595A91D4289865C9D36BBA0D422E622898EDF10BFD0CB7495A9A5705538B9B0E8A41860B13B4600373B0920964F7B4FC6179FC3736404108DD8291F7082E1A6C12C8C8876EE69AE03C2DB3B23AA0A5B5B1D7B62BF72FF165CF8395707325B5E699C8CB33118383C3058C2469009B1500A4AD420D390B9DDF7293CA4471D5B7CAAC6B2600658C39FB899C232602893476262A2D2461C11B153C958EF882241779806CA52A0ABB07AEE02E642B0FB8853119A86EFF2330CA81CBFDFB1063E244941339F5095BB1299D8C2042602B7EB775C4F910A5E1BA2C54E95BEF761309BCB38B0A3ECE189F2F3B96CB24337A152C5503AAC5468B88865E34A89691E7C0B7E7828B37C96484154CC9A880F057D49205D1F90168A8797D8C8766D359CA8651A6A0AF78D664AAB6A6FC11A36D163C489897DE46BED6A1C0BAC85EC5F655DD554EAD258D77B246535CA9C9C3C5B5820B80283E6DF085D7210F8980679332707785BE8C0B06C7BACA199C521D2AB782E00B51F92650CC4F8FE344284504E80909BB908AE1A2C83EB98042FD77ED5ADEDB4BE173D3F447AF43618F28CAB933B776E6E5D9AE3125F37631F8637A3F654CA48E2CEB4B8B545B234EC16CAA1B038DECCA14FE422461ED4FE90843E6E2FF29BCEE59D578987568CB1EA598F98827948703D7CC47B66D", "957F5498662E9DBD8572E38A41425944A689DBA3B33C4952402ABA16A85426C21B3F546D600F1538B253DB963EA62213506AD803096C8260E91EC1A6B86CCCA5360BDD10F04E2818F3F5365556C24BD21417AC1709FABC6EC0EAD0A3E2DAFDB99085F122CB29D7BA5C4896FB2877D4049663131BAC5741C20D7ED5BC7582CDE842B6E8B8559B21D724E863B615F487D8E3C9D45374761370DA4AE1C759B8650A4364A3ACA55673B0E882F25AAE83A96A9DEB90BDBC52D45730E29A7D5D3596240A1B47D108CC88FD8C150DEA42F8AECDF310264ADFBE1D405A89E504D9ED58F2A78D213DCDCA3108B6B5A1D2403F41A0EE5EDD70B38DF4EA583EA05BB13A054FA9B71911B5569943B023B11B446F99B2E90523146580BD60EB8E3D29A15ACDA34710715CA8410BB5A1D33E517FE41603A11D4E4400020D4E2A36E5D798311164E99E9886525505DABD3815BA49A1D62AC87F57CFE24E0350CCAC4A0D6E0D0B130A3DA54502D50A737EE21B7A4DF2176F5D3A98799543EE0EEDCC3B27E89A4CF3130EC39DF7D354A54C76AE997D0137489BBEA6FB3718FBE71301687AAA5EC7DAA1779559A1A98D3933E18CA643E4B25EB38824AA8C6A2CEF5B1D971D8D6E415FB37FC5299005E77E6AC58DBC6C10797D592C12AFAE542ED85269F0F82843E2D7EBC7B4A85BF1F180886ABC04E8500597C185196BE6034908DFB056E1EC31BFC0E444AB86DC466CD42B8E80B7FE9B39A6BD394EFF8F2E209A196FB05441D176CD6D6F270C3C0D9FC5196FC50439EB7DA149F7C90644652305741EA91F95895A4EEA851AC81F0313C7C28932EDED5B431E4A58A0EA0A12B72DE9153A5048639C7C8948409AEC78B1CF12FB7FD4894FFD35BE583E343626BBEE46556648EE200FA737994BC6995FCA11AA3CD1AA9A699CF3AB186FC38C6D12B8CC81CA5D8E74DD207254ED10EF08A7CB047C1926F4CEB596790E3BBE674CB89327D45F12E722DDE0D5C2B156549E441C1C82EF4BEEE3810A759E2D77575BE64D3DDF424E8BC760C9A44DC6B7A244A192ABD0CC87BB036CBCE8EE88970B07865C778189DDA7E7B9C4BB49D14CB472AA6470A4A77BEE5B486FEF967D92B5D9571B6084E13A2C5823C8216B900BB667E0DC11E3E32057E309861A6A772B2A36FC8D302882ED2A73AC68CF24FC1E3F478F466A479D167B822E2E4288DB77D63D21F0390D91671C1D81A9420B0B6879593A78613582E77CBBAC50C721FE48C820D062F809CE24A664921777C69D9936035F93FCA198C227149DA84683A2A63DFAC89FB8C75230EE03ACF32436E66619C3B9D217B71990EB932BE5B78F6CD7D129B2BC654263E01F2CFD0248C85D46BA36BD1E3B33A0077E20B101AD95C3B52B3F7689F0076AD7833F26F5FD98898428E5434F78878EB67E19C6C8C5A2689B06778DF0182F5C8AAA6CDCB93E2618EFE368A589037D9DAB880F18C48CC949241C248F2FEDCEDE448F303120FC80E49CB64D8C6E9C2519BC8DA87B66C6831A0E74CC8606A00296E548427F9278160AB662F38FCBEA07C68889DCE7F226CEFFD864D5084FCB37904C78A52DEAAF8F498DA1227EED15CFB4D127187CFEC58152A4A484BB743DCA8A76DA3DED32507C4B54328275308E5DB31AB1568A6811C903BE759D552F1B071264523F62CF74F6C0411035E5B8D6C2F84A1B5D7DCE5541B01D8363E09F4F7DAFD92EF56DD2A37B3B844B1E37C008FBB98B43C1AA057BE68891FEEE396AC409D108DCCA43D7BA59EC02124FF7D76C348B6F5F8945ED2C8AE1453EDE13290F979F61B5D8C859959F5CF8FAD62CC9C6EDABE6283AB8DA5A74DA0F6B2B56F93AB56931EB3512413B20224355D4F2AC37338A012E04E654904C91A977DCD4508A23C9A450FCD1D712066BE01B8418560530C4AE3108BD0FC77798FE11BE866EC6A8D4D60AC8958459DECB1386AB1831C1A01A681193DF12115A231F57ECD18FC56EEDFCA7BD3AB1C2CCB87559A8EA06717A5850B32408DB58CE17AEBC9A2DF9DDB6096E53242473DC799826C6D9EB9FFB541858232CEA0CC49872BDACD0F37FDE76AF707827D70A5E78C3D573D167112BD76CE60F00720113F8539EFAFF253323AE0CE0ACAAB6D6AA93EC538E4C614AD9FF2FDCA7E2F8280DEF5C1BEB7913CB55D1C7323365AD86B4849B6F919F2CD6F0AFCB2578BD8854DED96462DA8D29154E9D421EE4648C3F", "B5B87791EAD3CE7F695BE94D4CD34D5F33F031492DC765A4EF6B3C94539DB193"), + ( + "5D75CE24C45E5C5C6EF29A047A632869E8001D28CEFC0A7B15F8A7ABB027707183C007506BC2A2C4966C5B5BB993176EDFFB50BC41B8A7FBBD14C0180937C9B35224795343A0F782DFA118444338E7C20E1C7A4D93751B0598C87914C26C8304A011C42692C76028670AF61474A40AF93196DF037A32053FE88C173CB878BF467DABE7973397BA3D667ED2BB38672975FE6294F41077EB9B78DA999B3FC25CB753251DF09E32131E5343B8E8B5898548C84A336A796447A0C5A2DC1CC2F6F15DC5D480CCA771FDF668DAA367B78AC079305E16387EEEF58079A9070B020DAC4CB1AA25B6E5D19DFE4BCE6C4303A2469B9A850D35B846BC29AAD8455ED5E1662D56B82F3606CDE7CBABE8840E26487D9080E0134A7A4B0D79D69C53059F6449BB4FD51A99F9313E9446124218A30738FB01CAE8C58545E22DA97991500A781F810437BA4904FA964DC916D75247CF686091C180DCC87861F7B7067B6D08283E3641BE61531FBA8B4F6D1B417E8B312C093FEA0C17F8F5BBEAAA2E118C6E51F52698671C93C5BF3E36BA3C4596F5A33D460C163A903FC76872FDB9075A593BFC05D0CBD1BCDC276C453C2775497E77B093D1034330231B9FC8BAB4EA7EC9832E5776677325397A1608BCC2B60D573D28A13452495D6F8302D6A6B5C72C6D44A19FDB9488CA580F63222F6D15A1F190B8FABC555248AA3B4B84DCB5C811E717CE636B86EBA7BCA0B92139C01F49AB26322E5771C35ED307A0856397285DF6B5CEABA543A67C33F8AC61365691BDBA67003C1B04403450C63E790B9ADC3BA6EB861A5DA04898EC5C349C9C46D542E1413DA82137FDC2A53A483B924709D0D0CBAF18C39787584C0CC3C4F05F943C89B59089BCD7A160D04418B27916946DBADB813DC9577FA837A037CEDA32BC47E9ADC385654667904EC429198925EFFB7E0F4591CF4439EE16330F5A7F6688248A416B79316231426094A990E3F44C49A11C5B15191C08B914D42A96D04B6C3651131BA5EA80291F49A06F30A07F944E81A9A7A7642BB8D711BDE11F57F0A7F1D5CFC853046338CE8BB36DA038AF1CE5C308F36D4F02AABF9ABDC4F13377F755E3E2C1A1FC542544199078922BC110A8E96A8C235E50689C63343513685BA21B9F806C729293365A2C7066773B9A1972413A342BE3029CB61B8DC951A53423F621CE7FA16072D30F675C63538CA5891085610155D2211D2B25C7B7F347E08566D7328E7E62B8A6CB62240BA74F9751B69544E937BA9611A4214472BAB01B5D53A070E32082C057A1773B149C9BF1115835B6497B2187AD7C4B3679296F016C3B2B25EB451D2B976DDDF73195026DB23A6DC87B4108D1961BCA1B4A6885CB54C2BD737ABC390CBF69A356A452740CB323F38995857DCF2106E567ADA74281863C8EB264CC17221481B8228978246C2C83B9E9064420A64FB7A30D061B9FC610C7762412E475C8318559B108B7895F20E88C8B09839EC88CD6140F5F9AC46DD08D6B62C1F82B3909F68B8F7C85CD7BCD8378CD7C1B5A34D74A10F3CEABE98C2BEBBCBE9C82B46314C9F0288CE50FF8B36E28677A6CB311ACB7A4A873AA257544562943306964A1753DB2B57CF0641DC111B1227342A64B47B88B6056916521D82F75E9A6E2D8150B102E170200BE1A27177AC9D4C49BF1AB70966109FAD83E448384A6FB3C126A5F19C0A484B3297A329AF32A31FC52691EAA447F081A34F1086CD4B1E959089EF64DBE8756BF52A81F925E0D06B222F3C2F767050D7B475E5C2FF1F62C68F05596A96AD9E3269CF0666CB50A4E421FEFA61B255A9E65FCC0149B736BC163B4A76E98EA6EA0ECB08EF364E6A455E0C47B2E29B6C429C11DF04DDA036554AA25B51A5F795C6B2B54C8799460CEB171C2CA6BC5405B919933217B16AF189C47C940E0FC7EF83051411A440CE9C4692B4607C672CE9B52C7F03336711E088173DB4649D16058517461A162188C489623FA0945EC5EF9481CF5D7AB175C9010E5CD98B11F736B9C4896539FA32C7BC79897B0BC6C062B0CBA0E5431A2ACE28E8AB09AB27AC9ED4535372026241CADC23C6A2BB4564F6B833437AD300A039CA81377F1A67D4B55215735FE776FE23195E71B665F0B5D694807DED1A55706AC9E3184F24657450964DD834515BAB90A2421A11B5EB141615707B888AA3E6FB89C0C70BA66348E59BCA5463B0AD051C47E50944142A6A10B176FC8ADB9EA3D61C570FD2AB344AA3C4B7A7C15C0B333477A8EE974EC832023A5976A443282FC2C384436ADD1BEB5F09FC8BA63B4207B74FC0CA226CB22585C74E02537187681717808984E52C30734730CFDA686F9B463C69529D42622FAD40AF3899F584AA57685ACADD221068858DE6CC99E22B602AA87FBB079BB1C114E9370D7667DF5A38B65773FC3C0A29D48AB2E78853133049336B99CB3CC3B849793A50EFB587A640858CE4A203C1410660A3267E570BB5A2041913C522CC9E8F772F42C8B6B75A900ABCF4B850C3FC260A5CCB29DF87122362CB7379CC0B7AD6D6CBA07D0BF674B330136547FB7AED92C4DEAA33F7B57A11840324014AB88C077DC933154A576DBA70C9A5B8ABBC5A606C52DEDF12112C7C086D8C01FD59C6DDA7A77ACC94AB182BD70273D3A63B53CBA6A489DD890CF635438734916D34BB0FBA0290BC57246967B774105F8F542D716899E456C26B89E8C1B5A4A0211A2E11E9FBB9321387CC0A85417DCCD5A49A9F5000C77E26EE68BCF2B2AB12D966E30BC1DB5C42BEE2905758798A7A611E217C391ABBB2F953AB2E1C48E19B274468C7FD51F681A2AC5E0298E18693F892ACF746A8AE1B3D974134E55B25EC97D75D192A266564A1A25BE6C53EA34C0463A9CD324824C32B5D24B513476C606DAACBA506892B0827786065915A10F628D68E66837092EF4446A85E99F89A24EDA7410391C811CD41737D763C0A825115B81477843A4F75572E09D96D0945C7A871725C37F9C3D865487BE7C9529A0BD1DA9AFA4D0790CD9C98155BC3442AB990C077ADB40F10C66A3365D958BAEEF5CBEAD780C8C769595017FC6AAA74E3774B7F6850D8B0D560061F70A10CF39A77CE498042A04C6748F25478553F2CE959C30C0D541D23B8233A1CA3F4C7611413D357702E5E80A6856A589F75D9B5410D9D2BE44A9000DB6C8A1EA33CAC5B96854C0637236A22A555CC12100D30AB3D62DC9C614ED806C6B848EFD650728F95DDE762F3DD04B2CC756E24B893D640D85B5B7DA466FF32283FB03C4917858F5B99552E0147DB52880E4825CD6A83AD23CA5FC62324314009CC66F6C32F09C8C2DAAB96969CE457B5B0F9A13B329C13DE02D6573ADA3C67789E4C8CB12B509513B664C47FF769ABF626631398AEC44584FDB2C0CBB04252083BD2A94DED51BF26727E395C21AAB2C1D95CDFCB541F4E5CBF2B0B1716CB24872C8A7E04D38656EC438533450B51194936C3073F3CB36BB20BA69E73B16881A7284B2AE7A5C682439792B1FD5548DFA3954A7459ED08B7387A5169AB74354A916F3EA800DD54C1600061DD7CAC51020E61AA6A1B05E67C196668133EED1C540DA386B8CC2A4DA2CAF6458EA545AFEE0601D55617FF1422D09B04DFCC45F511174449EE701CCAB390B9C4749CEF942C4D14B480AAA656A684A4464A9519F1150B6D0861874A2AB687796EABC2A1E19032B8A1EBE729F6B7AA28CD7864DA1C679A1C17DFAB80DBA8B467714BD05ACF8E5BBFC3848CB715F9C9C2E93628479B9474CD83EE1E7BDA5EB530A590D22C1572EFB9A91595A91D4289865C9D36BBA0D422E622898EDF10BFD0CB7495A9A5705538B9B0E8A41860B13B4600373B0920964F7B4FC6179FC3736404108DD8291F7082E1A6C12C8C8876EE69AE03C2DB3B23AA0A5B5B1D7B62BF72FF165CF8395707325B5E699C8CB33118383C3058C2469009B1500A4AD420D390B9DDF7293CA4471D5B7CAAC6B2600658C39FB899C232602893476262A2D2461C11B153C958EF882241779806CA52A0ABB07AEE02E642B0FB8853119A86EFF2330CA81CBFDFB1063E244941339F5095BB1299D8C2042602B7EB775C4F910A5E1BA2C54E95BEF761309BCB38B0A3ECE189F2F3B96CB24337A152C5503AAC5468B88865E34A89691E7C0B7E7828B37C96484154CC9A880F057D49205D1F90168A8797D8C8766D359CA8651A6A0AF78D664AAB6A6FC11A36D163C489897DE46BED6A1C0BAC85EC5F655DD554EAD258D77B246535CA9C9C3C5B5820B80283E6DF085D7210F8980679332707785BE8C0B06C7BACA199C521D2AB782E00B51F92650CC4F8FE344284504E80909BB908AE1A2C83EB98042FD77ED5ADEDB4BE173D3F447AF43618F28CAB933B776E6E5D9AE3125F37631F8637A3F654CA48E2CEB4B8B545B234EC16CAA1B038DECCA14FE422461ED4FE90843E6E2FF29BCEE59D578987568CB1EA598F98827948703D7CC47B66D", + "957F5498662E9DBD8572E38A41425944A689DBA3B33C4952402ABA16A85426C21B3F546D600F1538B253DB963EA62213506AD803096C8260E91EC1A6B86CCCA5360BDD10F04E2818F3F5365556C24BD21417AC1709FABC6EC0EAD0A3E2DAFDB99085F122CB29D7BA5C4896FB2877D4049663131BAC5741C20D7ED5BC7582CDE842B6E8B8559B21D724E863B615F487D8E3C9D45374761370DA4AE1C759B8650A4364A3ACA55673B0E882F25AAE83A96A9DEB90BDBC52D45730E29A7D5D3596240A1B47D108CC88FD8C150DEA42F8AECDF310264ADFBE1D405A89E504D9ED58F2A78D213DCDCA3108B6B5A1D2403F41A0EE5EDD70B38DF4EA583EA05BB13A054FA9B71911B5569943B023B11B446F99B2E90523146580BD60EB8E3D29A15ACDA34710715CA8410BB5A1D33E517FE41603A11D4E4400020D4E2A36E5D798311164E99E9886525505DABD3815BA49A1D62AC87F57CFE24E0350CCAC4A0D6E0D0B130A3DA54502D50A737EE21B7A4DF2176F5D3A98799543EE0EEDCC3B27E89A4CF3130EC39DF7D354A54C76AE997D0137489BBEA6FB3718FBE71301687AAA5EC7DAA1779559A1A98D3933E18CA643E4B25EB38824AA8C6A2CEF5B1D971D8D6E415FB37FC5299005E77E6AC58DBC6C10797D592C12AFAE542ED85269F0F82843E2D7EBC7B4A85BF1F180886ABC04E8500597C185196BE6034908DFB056E1EC31BFC0E444AB86DC466CD42B8E80B7FE9B39A6BD394EFF8F2E209A196FB05441D176CD6D6F270C3C0D9FC5196FC50439EB7DA149F7C90644652305741EA91F95895A4EEA851AC81F0313C7C28932EDED5B431E4A58A0EA0A12B72DE9153A5048639C7C8948409AEC78B1CF12FB7FD4894FFD35BE583E343626BBEE46556648EE200FA737994BC6995FCA11AA3CD1AA9A699CF3AB186FC38C6D12B8CC81CA5D8E74DD207254ED10EF08A7CB047C1926F4CEB596790E3BBE674CB89327D45F12E722DDE0D5C2B156549E441C1C82EF4BEEE3810A759E2D77575BE64D3DDF424E8BC760C9A44DC6B7A244A192ABD0CC87BB036CBCE8EE88970B07865C778189DDA7E7B9C4BB49D14CB472AA6470A4A77BEE5B486FEF967D92B5D9571B6084E13A2C5823C8216B900BB667E0DC11E3E32057E309861A6A772B2A36FC8D302882ED2A73AC68CF24FC1E3F478F466A479D167B822E2E4288DB77D63D21F0390D91671C1D81A9420B0B6879593A78613582E77CBBAC50C721FE48C820D062F809CE24A664921777C69D9936035F93FCA198C227149DA84683A2A63DFAC89FB8C75230EE03ACF32436E66619C3B9D217B71990EB932BE5B78F6CD7D129B2BC654263E01F2CFD0248C85D46BA36BD1E3B33A0077E20B101AD95C3B52B3F7689F0076AD7833F26F5FD98898428E5434F78878EB67E19C6C8C5A2689B06778DF0182F5C8AAA6CDCB93E2618EFE368A589037D9DAB880F18C48CC949241C248F2FEDCEDE448F303120FC80E49CB64D8C6E9C2519BC8DA87B66C6831A0E74CC8606A00296E548427F9278160AB662F38FCBEA07C68889DCE7F226CEFFD864D5084FCB37904C78A52DEAAF8F498DA1227EED15CFB4D127187CFEC58152A4A484BB743DCA8A76DA3DED32507C4B54328275308E5DB31AB1568A6811C903BE759D552F1B071264523F62CF74F6C0411035E5B8D6C2F84A1B5D7DCE5541B01D8363E09F4F7DAFD92EF56DD2A37B3B844B1E37C008FBB98B43C1AA057BE68891FEEE396AC409D108DCCA43D7BA59EC02124FF7D76C348B6F5F8945ED2C8AE1453EDE13290F979F61B5D8C859959F5CF8FAD62CC9C6EDABE6283AB8DA5A74DA0F6B2B56F93AB56931EB3512413B20224355D4F2AC37338A012E04E654904C91A977DCD4508A23C9A450FCD1D712066BE01B8418560530C4AE3108BD0FC77798FE11BE866EC6A8D4D60AC8958459DECB1386AB1831C1A01A681193DF12115A231F57ECD18FC56EEDFCA7BD3AB1C2CCB87559A8EA06717A5850B32408DB58CE17AEBC9A2DF9DDB6096E53242473DC799826C6D9EB9FFB541858232CEA0CC49872BDACD0F37FDE76AF707827D70A5E78C3D573D167112BD76CE60F00720113F8539EFAFF253323AE0CE0ACAAB6D6AA93EC538E4C614AD9FF2FDCA7E2F8280DEF5C1BEB7913CB55D1C7323365AD86B4849B6F919F2CD6F0AFCB2578BD8854DED96462DA8D29154E9D421EE4648C3F", + "B5B87791EAD3CE7F695BE94D4CD34D5F33F031492DC765A4EF6B3C94539DB193", + ), // modified ciphertext - ("26973201103B1C61BF9BD2AA805C0CA5C261605937DA88B24BF0973FA21B3DC94AACEB4D6199C113C61B690B0DB0713E42436A6B5C1CCBD53F3496C3DFD6CE6ACA7275D4866D46C29E25809C1826C27449C9DC64CB52B02CA53F5B8A9FC565384D331B1D1151F1F9A5177C086D2A3BF4A4B77FE848F841215C18A2C83511B9614A3CF212C9386DD3BA424F815EC0843CC6305FF97342FF9642DA6BA638C38EC30875E3FC3F2F3C4111C35AF3327453DC223F9294D3CA4722E07164561F4DC0765DD57B2C2CA3B3649B914B9C403011543C7B71644E3DC1040F7994FCB41BA3EC36ADB5349AD195DBFC4051606AA242B3A78A41B0335A29AC3C26375885F5101188A1A628A6206147F46037C8496E73D353EC10CFAC97920C156B499CC800C9090A29ACA2D7C41BF7892D25B933DA7543A5CBC0842BDA45861170C963558636A219241C52190723C133641CA43CCECA008015B866FC533B11007A85B648A24F7A933C2D31321DDA531ADC9847089539A57A4358826C08C7E23B8018584F70E361A57C60232A0058E335BB87A8E5141F14E57CF72446CD412792F8876063661C616F48FA06361479675218C14A5A87C91C27876DCD25C2DBE79CD625042BFA5709050D360C95A8F7B1D30881F6C0C68D4A6612BA8914CAB9E587340AC789EB851E721746E71578A295C407F78C5FB72E4855649BF72A0B12BEF08047444551045253CBE27039C54F2C72B6A7C9ACB2299B9A0515B6E152C3ECB3B6D12366B652B6C2CC70DB088D699924674ABA777DA6888114156E67C5126704B922223AC5000BE1E23F966A26099C664293C9CEB8CC49B42EBF4A2B1CC85271D556EF1231C933C45BCC8B1AB5BC6BCB014C770B25CB6DC769945DB2CC119B91AAB65427249E1940C38C57C9DD64CA7FA55CDABC2D1F07862281865EEC479BC29EE9067F47A6237B18525C39295D996D77736ECDF5780E76820EBB77EF403DDEC18979890FBBE99537D3CAD3A77B252A81D8E6031CB254FA4735654C74C59C5FBA58906F94657B42340AE11A527317FAB8AD3B380658EB47BD99619A4211DD791055CC59B83B538F170E6BEC0053DCAFCCD9BBA263783822CD93733F4403C1D011848467493FDC3F2728CC93D66BE34434C43B0C9AD72570FB46783283132116CFDB17DD36683EDC2FF175CD2FAB7D6CD768432C6B4FD1394DA77A45F90AF3E47905553FECD421DDE68A3071A8838703569023AF1ABC223273D2C392C3A3818B494E04D49FDAD957AD2A93DD96684C1289CA9B03C1F7BDB0953787BBC94BBB228AAA27A4C72E9A3B1923DC5A1F2B535CE546E900844E008EE8A9392F317DBBE90FF0475FDB27BE9984C4666472FE38A649DC70019361E3F042B28A224E567608F346E89390D243B185139A5CB5A99809CDF35485EB5AB92D1C0817C01F337569C0EB50FA5C60CE30841005C63D217933F84B70A16162737C6A11C62E017610D30BFD6C44C74906B1806C72C7C0C7EA2DA4A3B838900B1611124468A4DB459541560C9C1A759836496099572310BFB873100C02C10B6746F4E0CD51921FE4B99316811CA49A031ECB296E94CAF6903A1493B749225F19D526F2B5987EE82F1AD29A8E002CE69AA74A418F51A4C2B6E65BD3925D927B433E43026A069D6B170AD6A57AD0D0C1C32376837CAEB1698A5E387A4909B1D4F2CFE5D4A14B36772F5BA75C282422BAB0C1F43C57BA9A9437618C10AA39449275178031F0C0029174A4C8A5562076B4F40401212E84254D048748D3086691542CCB607A15F818FCA12ADDCB4DF73C59480C5E0B4CAC73D64D73A3CFC3E1031DD2A3DB20CE902924C9A8C7F64106AF3181BF978029BC3CE080C4A4613602C0AA0C6109E5F3A813771A54F7473F393022956B673B8DD0262905A8C2CE50CE2F09483BB337E647C9CE772A79C529C8134EFE47CCF6D67054F64D020468E5B695FF656D1CB4B1AEA1A9DDFC90EE0BCE897556DBC158DD6830C4B8C1660CA86D8B3C663214C69B39CB73A74C6890EA6279D14961CCFABBF197207B146AF048C2D8236039BA6B4244530E88229FE96614B834D0452EA19C2B0541C8461990DE9C3AF162B764CCBAA02A5AF389992BD91B35D9581FF76FCEB9817C16B2287242A78142C9C988C12356BCE63FF7585953B0111C127664C26882B97596628D32368F51EB9777517BE1353C52CC864F6372B78192C9FC865098B48DE4071A573348812F92D201144CCF216240368983027A303A54B4F15614AD67158B46AC6E03530016B907109B02FA274B8BA2F0356BA722404F919E03B3761EBB1ECBF25CE1B98EEB21BE7C5B03BA1631E3A774B488362515563FB312A07A197DDB4A8CE1CC7C006085541424A813CF2115E8A9B51F064A61C44B670B80047035FBD095F4ABB8ED760E33F90E5D90040FF0768D990282F58FE67989893C7A1F66935D17CD9571612C2BB98707C707EAAD6CDB982295BD7538ADC744149813A4D9730841337A58EAAEB95C2D4CC52A8C19ADADA071E068172CF285EDAA44F72261B37044203A5CD171A81417A4AF8196D9A86B0E5606B802C44A8B6EFBE894842B8CA15619BB73CBD4CABEDD83556DC80028295DE580024699AB67D6829FA362D710475BA922CBA322CF777A5191403005A86053A26C707F76D97040070E824276B4A4BB789ACE86F5A18BDB2C5B21B8E2A7A0533ABF1CACC050F01DA8DC55D8DBC269E975BD552184A56A1AE45B872A9184E5C5D34210F992B424167817F89CD14CCC284CA22B83088BB5B4B48A9C9172AA7CEACFF093BE6C846F7FECC75BF726DC5B2AA4214B1FA35B834BBAD2998D6F680E7E666122C3324F8C54ACF6C2944948BCF3512E3927976C4B9133B8C1EA16C1F94C4D155144805379D451767B1C042A6E0A576A15FC7B5A9B84DF32012A2B2F2012113EA4827448A854D457E1D880CAC253C01883B509BC1C2783F1F814A785921BB70AABEB05D9C3B21A5A5611F6992EEB04E018B6C4BA9BFBDB5F138B599C3C2872202043183609A76AC622066FB5BCB20B557480289A0695156B327C48CB6D536A67DA4F86B9860F150D82B72648D1AA0213552F27512101154A4045EADC9CA394C0DA8877EDF3766472C67844A8B8A9B5D7F4C4ECA597ABA46306262646A5616C092D29152281B787EF873DF76B4BA1257F43F96B033C3829231DB8A1BB2676B7ADDAB1C7DC91B9936E54926FDBE85C1CB09287F44DF9BB0E2BDB61827137BDFC6CBC849CD73B062D8C3A2A473AD599629BD6A072E4C9D49B356E712982879188C5B2D5847C870779E748B1AB382999221B7153C321B95FFAF7122A1A67DC688D34239CE43618A84285BC86B62DC9940BAABB08C399B66A0010316F8692C511D74B1CC810DDEC509ED095C5E2922272C636BA4301F416B0F4883441570DF4663438BEC7355E0F826B47481B532AAEBB06AF985B57B6DA04BA89A0F8B4ADEE69BFE76318B2246B3A405AE106AE63B44A1D0B644991B8F24B2F80327130586A2DE138E34150158C0DDE0106E064971598AB1C400029D18C7CA48DCED35244A3AEBC0C2BFCBBA9F384AC462C7447495C601C7463C039FA2A633695691596A8CE948BDAF8B5C8E937A8E37A7F7B20DCC7A25F07A527E80A71CA92FC128771942C18A9319545B59117B629198DFEE0457DA2B513572D0F9189A44743A1183FE78A97A254B4C3A722AC52A03289135BB17084DC3F056A0635081A8F5862F36A47B740090134C6DD315A3A6245710C519E811BC3095518965B9BFA9BD9E5221AC436B7B5667DBC06768B2F929A6B35706D4A238DE158BAF1451944B1BDC9EB14546B266FA3AD2F3CA3AD874121E54E8FEC6A121B3B6722B947B80091873EDE468AB9083884C7B5ADDC93A4AC4C5D477325114E2E04B8C0EA6D5736833EC4C462361F1AD21839C59897C2B749C14192463AF547612D980CE6084696A51674B57A3250070AF2045F2697CD5A38080B4429D9869C31A18F996AD3832227592C7C768EE9DB342440C88CD98E22D730FE037123C306736A2D4D2602C0AC1DF35931F2E8CB66B8A22A9A010D89007F36CD4719A68C68849706A0344CBEE36342A7A6A8B8C81971C8B08C8B4C718517C6065D6D729EC3765391705A4E8C84F6ABBA345A50A8FA5B47751E0A3C61C1A56D413A5CB57425AEF4075D46534639BB62957434DB405540269349B9A16B8013002F2B051E7C1CBCEA19737B8C8AB8CBA1A87A9B2D46180B783C353B369A58173F7139A383A31EF9C98F06495FB45EF20244AC04BFED903A8CA978321819B5E3ADA52A77F3ACBF8642C570072AE9A30C218118C544115F026CF6257886C1440703DAE33C9BE8B8132E9AD514312460757C3DDBCE268A08FEC902D41139788F020BF6135F3C786EC0A1C21B3945EA2AF22A0DD9D29E6EB48CE816BC73708B2F73C696DE5528B0A05FD1F75F54E8CD022B4DF4B74DE767F1E8B795F42AEA9CA9", "6E90BDC6477D2F2A0806CA44E1AD7189B06ED9A51752FFE174E74BF9C16325076BB59D2E85DD74B7F4D377FA80BABD334FE85465377CAC0128B037D48FC3674B2E48D45AE03B0CCEEDB9C98C34F3A38C8FD791165CE9841131439ABF3F0105E1D8B5AE8B14F17211272B3FA4A78F8038C9368A88CB33696597594D4543FF23D29FD79882BDA21A92D493C9790E5BF5DA93D8DEBF1107076E8B50459DC6E26EB27B22B01D4C96F02F9C1A31D589FE9CB083A403CCBE56DCA78D96C3F1702BECB4D28DF89654F4F3F4287F26665FEC25B4E1426F40364EEF0914914AF3666B098A9527458FB7E8FE4F0FAF1CD77F036D23B3239DAA701C2B5BADDE0C0B9370BC17C2A74B282B94309C83D31991C8F30D41C580E503F0613D59DD5FF651D949590A8C8F9A4A30DBC241DB68E749E5D3BE6DB4051C283FB130CCDFFDA160BB5CA574110583E0DCDF1087A2D83B27B33E9D3865A88E8E4FC31D174DF468B88631B579F2773F9CBCA9712A840FB9437BD639D611E020809C0C1D96536493FE4B0CA3DBD2041C207C6C8F23077F53FDB78A18F59F30CCB304A73059F49CA4624BC9A40FD7BA17D9903B7F5E534913113EA28F40A11E1F64C18B5F676AD71F183AF87C23DF02CDB33DC10875408B8A7C335A2BEEE144E2B83B0931C8892D6C6F61AE22BD76F8C293B5153C6CF6FF11DED7ECA0EC5F1E51024DEA7FE85248D36A8A1C58EFBBD092D66B3903E1B8AEAD44BE2728C5D0AB142B1ED2BD32791C1C3F7443386B239FFE569D3119A18F497A069EE671617E6AFA3BC1038B954CD184B896FF3D21FAC0DE7E49A55A87C6648E4174B960E73AE8D73682CF08C6E58DD834C8B897FA991A4D6C966CCE9900F9B911B6E9519C5AC052232D6B9A43A0425D56EE054B93B4A6033EEE05FD8604ECFD7F43D407FE5FE40A468053428E610C2DFE0A15B76759D769269FD7964B1E382E654E0B229D14295662EDDED202391B0438796AC27EEAF1E6C7874D06E6D72BDC5E56310EDB1299779E56E1795E4AA4E3AFA96FA3D5C03B6C3C562D4F680BD367CCAE9D0C8C6153C84623EFD4D4479F95E603270782656D9F5F94EB1E1C51CA44B495C4AD8DBE83E57879D1DDD2E9CFEBB4FEA3114C223ED4BACD36E4C5F09457106601AE4CFC9667EA6A0BCE36D3CF2C5E9622A1BD3628AFD9F9357F2FA02D352B56F0DE2D73308041E6AA84B543EA858970FD0EC5CFE935C206FD3AB7031F410D417A427C72B2E42BBDC504276AA0A3836CC1B9294D5DE7A959522BCCE471BC3C3DCE7D9FF0AA4A5138D6052B09C7CAFB5308F8C92FA0BBCAB1D2ACA0551CA41CAE040705FBA6084B5FB92C8AFB35AFF2627273A6AABB43F9EA2CE86C31B6E00D60A837A0F5B016CD811062D5E647EDE2686DED705F1FCB034DC32E01B4AE167DD823AB5A1B25997097969CF235460A114C6BE6CB1482C5B550B2DE35FAE7957E425D7C7BA590EEB0675E7C819B487AA7CFA59CB7A52FE443E6EC8AF9114600A1F7A9CAABB2AE3481DEFDE3B3E7DB595CD12DF20D02D4D74157477390DCDEE70BBA98BCC88DCD03AEFE71F4BFB817901EF1147F1F481267788CE540D191F0F706DC3F28CE35E571F61CAC7BAD74A8E0282AB91F9400B3B6637336DF339E0C1BEF7AC7ABADA3B1A9E25DA4EE5BA390BE761F15D390A3ADE467822D55AC1C38CB176B8E22CA4E60AEF53075EA83C91BB7109F4387A49B097EE2DF365FCE5BD8CC7D1A87BF391D238A2ED7C64DA6CC2DF988FDFD899B11626BAF823396642586BAC1A4BDBCA92D508F2D72A466617B2D71D574B718EDC5498A83A7A2C5CD5CB08462210EF225D080062283F14CBFD3E81A995252DC31F0A45417EB97E898E05C5CD535564F026DCA06F5B07637D686A57B937ECEAF407FA0CF90EB6D9B33A1A65D7922A4F5C62152E049BAADA59CCC202A3F089476BF0A96E037C621890C765899A44D5E494C268A4891B3CD08AC4A0C10BD44D21ACCA0C79BF3BF1CF5E9D1EFA387067432E7034ADD4379E833E4D25B7D4007E44438CEC98D15F7926A676EB263BAAB1DB2DCCA55CE7F3B02E5F5C6CF01AD3C71F2BD6240FE124A041EFB0ACC3CDB14206C24F71253DC0A07596E50E84728637A6FAEE440E7BBA374914306C2CE8630CBA58F1162E7724AB52E232F490BD16B9187169DA935E706870D6EA62B286C7BA9F1AE0D9DCAFBDB74AD6B8EFF0FF1CEBA42F4", "A50BDEAA22355A6D1AE1025FF97A2EEF84CAE20383E395BF2636757E1E3826AF"), + ( + "26973201103B1C61BF9BD2AA805C0CA5C261605937DA88B24BF0973FA21B3DC94AACEB4D6199C113C61B690B0DB0713E42436A6B5C1CCBD53F3496C3DFD6CE6ACA7275D4866D46C29E25809C1826C27449C9DC64CB52B02CA53F5B8A9FC565384D331B1D1151F1F9A5177C086D2A3BF4A4B77FE848F841215C18A2C83511B9614A3CF212C9386DD3BA424F815EC0843CC6305FF97342FF9642DA6BA638C38EC30875E3FC3F2F3C4111C35AF3327453DC223F9294D3CA4722E07164561F4DC0765DD57B2C2CA3B3649B914B9C403011543C7B71644E3DC1040F7994FCB41BA3EC36ADB5349AD195DBFC4051606AA242B3A78A41B0335A29AC3C26375885F5101188A1A628A6206147F46037C8496E73D353EC10CFAC97920C156B499CC800C9090A29ACA2D7C41BF7892D25B933DA7543A5CBC0842BDA45861170C963558636A219241C52190723C133641CA43CCECA008015B866FC533B11007A85B648A24F7A933C2D31321DDA531ADC9847089539A57A4358826C08C7E23B8018584F70E361A57C60232A0058E335BB87A8E5141F14E57CF72446CD412792F8876063661C616F48FA06361479675218C14A5A87C91C27876DCD25C2DBE79CD625042BFA5709050D360C95A8F7B1D30881F6C0C68D4A6612BA8914CAB9E587340AC789EB851E721746E71578A295C407F78C5FB72E4855649BF72A0B12BEF08047444551045253CBE27039C54F2C72B6A7C9ACB2299B9A0515B6E152C3ECB3B6D12366B652B6C2CC70DB088D699924674ABA777DA6888114156E67C5126704B922223AC5000BE1E23F966A26099C664293C9CEB8CC49B42EBF4A2B1CC85271D556EF1231C933C45BCC8B1AB5BC6BCB014C770B25CB6DC769945DB2CC119B91AAB65427249E1940C38C57C9DD64CA7FA55CDABC2D1F07862281865EEC479BC29EE9067F47A6237B18525C39295D996D77736ECDF5780E76820EBB77EF403DDEC18979890FBBE99537D3CAD3A77B252A81D8E6031CB254FA4735654C74C59C5FBA58906F94657B42340AE11A527317FAB8AD3B380658EB47BD99619A4211DD791055CC59B83B538F170E6BEC0053DCAFCCD9BBA263783822CD93733F4403C1D011848467493FDC3F2728CC93D66BE34434C43B0C9AD72570FB46783283132116CFDB17DD36683EDC2FF175CD2FAB7D6CD768432C6B4FD1394DA77A45F90AF3E47905553FECD421DDE68A3071A8838703569023AF1ABC223273D2C392C3A3818B494E04D49FDAD957AD2A93DD96684C1289CA9B03C1F7BDB0953787BBC94BBB228AAA27A4C72E9A3B1923DC5A1F2B535CE546E900844E008EE8A9392F317DBBE90FF0475FDB27BE9984C4666472FE38A649DC70019361E3F042B28A224E567608F346E89390D243B185139A5CB5A99809CDF35485EB5AB92D1C0817C01F337569C0EB50FA5C60CE30841005C63D217933F84B70A16162737C6A11C62E017610D30BFD6C44C74906B1806C72C7C0C7EA2DA4A3B838900B1611124468A4DB459541560C9C1A759836496099572310BFB873100C02C10B6746F4E0CD51921FE4B99316811CA49A031ECB296E94CAF6903A1493B749225F19D526F2B5987EE82F1AD29A8E002CE69AA74A418F51A4C2B6E65BD3925D927B433E43026A069D6B170AD6A57AD0D0C1C32376837CAEB1698A5E387A4909B1D4F2CFE5D4A14B36772F5BA75C282422BAB0C1F43C57BA9A9437618C10AA39449275178031F0C0029174A4C8A5562076B4F40401212E84254D048748D3086691542CCB607A15F818FCA12ADDCB4DF73C59480C5E0B4CAC73D64D73A3CFC3E1031DD2A3DB20CE902924C9A8C7F64106AF3181BF978029BC3CE080C4A4613602C0AA0C6109E5F3A813771A54F7473F393022956B673B8DD0262905A8C2CE50CE2F09483BB337E647C9CE772A79C529C8134EFE47CCF6D67054F64D020468E5B695FF656D1CB4B1AEA1A9DDFC90EE0BCE897556DBC158DD6830C4B8C1660CA86D8B3C663214C69B39CB73A74C6890EA6279D14961CCFABBF197207B146AF048C2D8236039BA6B4244530E88229FE96614B834D0452EA19C2B0541C8461990DE9C3AF162B764CCBAA02A5AF389992BD91B35D9581FF76FCEB9817C16B2287242A78142C9C988C12356BCE63FF7585953B0111C127664C26882B97596628D32368F51EB9777517BE1353C52CC864F6372B78192C9FC865098B48DE4071A573348812F92D201144CCF216240368983027A303A54B4F15614AD67158B46AC6E03530016B907109B02FA274B8BA2F0356BA722404F919E03B3761EBB1ECBF25CE1B98EEB21BE7C5B03BA1631E3A774B488362515563FB312A07A197DDB4A8CE1CC7C006085541424A813CF2115E8A9B51F064A61C44B670B80047035FBD095F4ABB8ED760E33F90E5D90040FF0768D990282F58FE67989893C7A1F66935D17CD9571612C2BB98707C707EAAD6CDB982295BD7538ADC744149813A4D9730841337A58EAAEB95C2D4CC52A8C19ADADA071E068172CF285EDAA44F72261B37044203A5CD171A81417A4AF8196D9A86B0E5606B802C44A8B6EFBE894842B8CA15619BB73CBD4CABEDD83556DC80028295DE580024699AB67D6829FA362D710475BA922CBA322CF777A5191403005A86053A26C707F76D97040070E824276B4A4BB789ACE86F5A18BDB2C5B21B8E2A7A0533ABF1CACC050F01DA8DC55D8DBC269E975BD552184A56A1AE45B872A9184E5C5D34210F992B424167817F89CD14CCC284CA22B83088BB5B4B48A9C9172AA7CEACFF093BE6C846F7FECC75BF726DC5B2AA4214B1FA35B834BBAD2998D6F680E7E666122C3324F8C54ACF6C2944948BCF3512E3927976C4B9133B8C1EA16C1F94C4D155144805379D451767B1C042A6E0A576A15FC7B5A9B84DF32012A2B2F2012113EA4827448A854D457E1D880CAC253C01883B509BC1C2783F1F814A785921BB70AABEB05D9C3B21A5A5611F6992EEB04E018B6C4BA9BFBDB5F138B599C3C2872202043183609A76AC622066FB5BCB20B557480289A0695156B327C48CB6D536A67DA4F86B9860F150D82B72648D1AA0213552F27512101154A4045EADC9CA394C0DA8877EDF3766472C67844A8B8A9B5D7F4C4ECA597ABA46306262646A5616C092D29152281B787EF873DF76B4BA1257F43F96B033C3829231DB8A1BB2676B7ADDAB1C7DC91B9936E54926FDBE85C1CB09287F44DF9BB0E2BDB61827137BDFC6CBC849CD73B062D8C3A2A473AD599629BD6A072E4C9D49B356E712982879188C5B2D5847C870779E748B1AB382999221B7153C321B95FFAF7122A1A67DC688D34239CE43618A84285BC86B62DC9940BAABB08C399B66A0010316F8692C511D74B1CC810DDEC509ED095C5E2922272C636BA4301F416B0F4883441570DF4663438BEC7355E0F826B47481B532AAEBB06AF985B57B6DA04BA89A0F8B4ADEE69BFE76318B2246B3A405AE106AE63B44A1D0B644991B8F24B2F80327130586A2DE138E34150158C0DDE0106E064971598AB1C400029D18C7CA48DCED35244A3AEBC0C2BFCBBA9F384AC462C7447495C601C7463C039FA2A633695691596A8CE948BDAF8B5C8E937A8E37A7F7B20DCC7A25F07A527E80A71CA92FC128771942C18A9319545B59117B629198DFEE0457DA2B513572D0F9189A44743A1183FE78A97A254B4C3A722AC52A03289135BB17084DC3F056A0635081A8F5862F36A47B740090134C6DD315A3A6245710C519E811BC3095518965B9BFA9BD9E5221AC436B7B5667DBC06768B2F929A6B35706D4A238DE158BAF1451944B1BDC9EB14546B266FA3AD2F3CA3AD874121E54E8FEC6A121B3B6722B947B80091873EDE468AB9083884C7B5ADDC93A4AC4C5D477325114E2E04B8C0EA6D5736833EC4C462361F1AD21839C59897C2B749C14192463AF547612D980CE6084696A51674B57A3250070AF2045F2697CD5A38080B4429D9869C31A18F996AD3832227592C7C768EE9DB342440C88CD98E22D730FE037123C306736A2D4D2602C0AC1DF35931F2E8CB66B8A22A9A010D89007F36CD4719A68C68849706A0344CBEE36342A7A6A8B8C81971C8B08C8B4C718517C6065D6D729EC3765391705A4E8C84F6ABBA345A50A8FA5B47751E0A3C61C1A56D413A5CB57425AEF4075D46534639BB62957434DB405540269349B9A16B8013002F2B051E7C1CBCEA19737B8C8AB8CBA1A87A9B2D46180B783C353B369A58173F7139A383A31EF9C98F06495FB45EF20244AC04BFED903A8CA978321819B5E3ADA52A77F3ACBF8642C570072AE9A30C218118C544115F026CF6257886C1440703DAE33C9BE8B8132E9AD514312460757C3DDBCE268A08FEC902D41139788F020BF6135F3C786EC0A1C21B3945EA2AF22A0DD9D29E6EB48CE816BC73708B2F73C696DE5528B0A05FD1F75F54E8CD022B4DF4B74DE767F1E8B795F42AEA9CA9", + "6E90BDC6477D2F2A0806CA44E1AD7189B06ED9A51752FFE174E74BF9C16325076BB59D2E85DD74B7F4D377FA80BABD334FE85465377CAC0128B037D48FC3674B2E48D45AE03B0CCEEDB9C98C34F3A38C8FD791165CE9841131439ABF3F0105E1D8B5AE8B14F17211272B3FA4A78F8038C9368A88CB33696597594D4543FF23D29FD79882BDA21A92D493C9790E5BF5DA93D8DEBF1107076E8B50459DC6E26EB27B22B01D4C96F02F9C1A31D589FE9CB083A403CCBE56DCA78D96C3F1702BECB4D28DF89654F4F3F4287F26665FEC25B4E1426F40364EEF0914914AF3666B098A9527458FB7E8FE4F0FAF1CD77F036D23B3239DAA701C2B5BADDE0C0B9370BC17C2A74B282B94309C83D31991C8F30D41C580E503F0613D59DD5FF651D949590A8C8F9A4A30DBC241DB68E749E5D3BE6DB4051C283FB130CCDFFDA160BB5CA574110583E0DCDF1087A2D83B27B33E9D3865A88E8E4FC31D174DF468B88631B579F2773F9CBCA9712A840FB9437BD639D611E020809C0C1D96536493FE4B0CA3DBD2041C207C6C8F23077F53FDB78A18F59F30CCB304A73059F49CA4624BC9A40FD7BA17D9903B7F5E534913113EA28F40A11E1F64C18B5F676AD71F183AF87C23DF02CDB33DC10875408B8A7C335A2BEEE144E2B83B0931C8892D6C6F61AE22BD76F8C293B5153C6CF6FF11DED7ECA0EC5F1E51024DEA7FE85248D36A8A1C58EFBBD092D66B3903E1B8AEAD44BE2728C5D0AB142B1ED2BD32791C1C3F7443386B239FFE569D3119A18F497A069EE671617E6AFA3BC1038B954CD184B896FF3D21FAC0DE7E49A55A87C6648E4174B960E73AE8D73682CF08C6E58DD834C8B897FA991A4D6C966CCE9900F9B911B6E9519C5AC052232D6B9A43A0425D56EE054B93B4A6033EEE05FD8604ECFD7F43D407FE5FE40A468053428E610C2DFE0A15B76759D769269FD7964B1E382E654E0B229D14295662EDDED202391B0438796AC27EEAF1E6C7874D06E6D72BDC5E56310EDB1299779E56E1795E4AA4E3AFA96FA3D5C03B6C3C562D4F680BD367CCAE9D0C8C6153C84623EFD4D4479F95E603270782656D9F5F94EB1E1C51CA44B495C4AD8DBE83E57879D1DDD2E9CFEBB4FEA3114C223ED4BACD36E4C5F09457106601AE4CFC9667EA6A0BCE36D3CF2C5E9622A1BD3628AFD9F9357F2FA02D352B56F0DE2D73308041E6AA84B543EA858970FD0EC5CFE935C206FD3AB7031F410D417A427C72B2E42BBDC504276AA0A3836CC1B9294D5DE7A959522BCCE471BC3C3DCE7D9FF0AA4A5138D6052B09C7CAFB5308F8C92FA0BBCAB1D2ACA0551CA41CAE040705FBA6084B5FB92C8AFB35AFF2627273A6AABB43F9EA2CE86C31B6E00D60A837A0F5B016CD811062D5E647EDE2686DED705F1FCB034DC32E01B4AE167DD823AB5A1B25997097969CF235460A114C6BE6CB1482C5B550B2DE35FAE7957E425D7C7BA590EEB0675E7C819B487AA7CFA59CB7A52FE443E6EC8AF9114600A1F7A9CAABB2AE3481DEFDE3B3E7DB595CD12DF20D02D4D74157477390DCDEE70BBA98BCC88DCD03AEFE71F4BFB817901EF1147F1F481267788CE540D191F0F706DC3F28CE35E571F61CAC7BAD74A8E0282AB91F9400B3B6637336DF339E0C1BEF7AC7ABADA3B1A9E25DA4EE5BA390BE761F15D390A3ADE467822D55AC1C38CB176B8E22CA4E60AEF53075EA83C91BB7109F4387A49B097EE2DF365FCE5BD8CC7D1A87BF391D238A2ED7C64DA6CC2DF988FDFD899B11626BAF823396642586BAC1A4BDBCA92D508F2D72A466617B2D71D574B718EDC5498A83A7A2C5CD5CB08462210EF225D080062283F14CBFD3E81A995252DC31F0A45417EB97E898E05C5CD535564F026DCA06F5B07637D686A57B937ECEAF407FA0CF90EB6D9B33A1A65D7922A4F5C62152E049BAADA59CCC202A3F089476BF0A96E037C621890C765899A44D5E494C268A4891B3CD08AC4A0C10BD44D21ACCA0C79BF3BF1CF5E9D1EFA387067432E7034ADD4379E833E4D25B7D4007E44438CEC98D15F7926A676EB263BAAB1DB2DCCA55CE7F3B02E5F5C6CF01AD3C71F2BD6240FE124A041EFB0ACC3CDB14206C24F71253DC0A07596E50E84728637A6FAEE440E7BBA374914306C2CE8630CBA58F1162E7724AB52E232F490BD16B9187169DA935E706870D6EA62B286C7BA9F1AE0D9DCAFBDB74AD6B8EFF0FF1CEBA42F4", + "A50BDEAA22355A6D1AE1025FF97A2EEF84CAE20383E395BF2636757E1E3826AF", + ), // modified ciphertext - ("044843AECAB03CA3AD8387106D4587446199A01101088A95A6C98FC7726BA642A2D1E0647BDB88D941606EB67E4BAA89D951658A9127B3B08A52E29E5F76061DE0BD8985C43A0B9C57202B3C59BC0DA5223436C5BC83718E8C5EB3B0C4FA977FB52C77C88743CE3006807C6913E05A516906B0828F3B91812950A1CF703E89560DCDAA2CA800AF5A44B995F7C64650CEB6477273F1B6AC0B35FD030D0EF24D4E7132F2788EFDE0A9E215291859426F70B7E4550D17F815FB71CAB59348B6C4278180A4AA28744EF2B195FA4ECEDAC8910C4C041958A702518833A6934A446117BB45159ED4BA42D1421D52071B0D9BA6879906CF582971EA30F1A63C64A52222F8898E080B56A98A33287963B4C4D11916CF716E7B079D5CA33ED5D7C75138821706475BBAAC396BA31D17CA5705A9B04044343064BBD5CAF0A6C57A45CE3B216CFFE47345F97DE9440048E175C6324959423534548375378933C29B2DB02779AA5F6B92796D61A18F89917228A4AE8113E9744C750B6EBABB4ADF1B46379891ECA75FFC291A56636496B97C27BB5F8E118B84DA5B3194146B375B9E210D1C1B2969B53C34E889B49C481819442848BF30E70B6ADB751D098A0122951DE6C8833081B5B047D0517EB3A819567729292C09DA47B3F529C19D6630C5A9330FA8AE6E176D3C4A08326B1915DC0D00A68E01E10972E8B7459B41A30205A0168F266821BD17A3EB63C1830687A3EB6880D3352A7785424C064D4A9549B40203B3B43C34064FD1986AE6516E10BCE0662022077DE2987A48557091B3863F1B741BC03D70E4C219847D54B414009993D0E60A13A61E0C651148998D23B12B2C05AAFD15B76EA03503277A4A0368B063027C9AA303693A3DDA241019B5C850C4D16745D31C6E421614A4C092634383F2327B25A29EAEECB7B9E81B35940EA3234F043164D7658440DC1967518E79801D1814811DC2BE50CB3129D984EEF44CDE0549BBC31E147B1B5330A0BE001E865414A1CA98FCC557EDCA065DA97BD1F46FBF64A77D95A7E3FC6C02B74904D3932C86198B73B3B13B667D6C3B9D1B305D9949E590B9863956C6567C3338A51B5C376481A971E7BFBCF8BAE9793B87D2B94A32A250B98A982B413CB33F8098598FF6888F31A3C7E6A548F05B24A77254F6798EB88FB838B09241B92EE034A4E16EF0048B37291E36631F50359EAE237BD7B3382C7B4DE3A95845CC6E8D5642A42B0A3BD4A27CD3B5B4078D4B427FA5070289D5CE8F022956827B1158594BB238C23B70F2CABD00B882A194544248C7D997792E30727D612C04689ED716353ADC4D93D1BFDF2B91E767A87B779F81F4A295A112D1087B03D71E4D9AB2C83B96632128A154396621459012B5DB39136F96826DABA6EF68627223684FB13F4FE7937EE859B22890874C811A32460725920111BFDB5C4882524C544277FF72CF127A840D07AE93FA526F7432195316CDBAC5C93822AF11C738755C3AAA37AC8C24BAAA51EE049BA17C543665486FD50562DBC5FA14C65F63A47BD1CBA840CE4EC57CDBA0B49E020ACE272A95B64A4B37583A6861DC8017327C4D14EBAD5C47AA28F6C524E961047C3167C934F411A7A9E75C2382654225753F480D717B933A4997FD45773C09AE01B5CFC212AB91341387E18B18472AEFFAA9A2986D0429B756A25DBE026E0137339EF82D10B3618B3C5EAB4240996274896174E6FA7443B753DDC27B15909B25959AB376437CC316D9561B91310BD7C575DE15C7E0228BAF8B4695495B36D056CCE58732D40823DCA59597980A168054311D32B19C154989B867830AF61C78988640A47C1FA71058213A4D93B6899507CED783126C88CC2524FFCB8E93FA40BCA06BC9F2372D92622BC04ECE69CC64042F66ECBE660BC72533C75DF0BEAB4B9EF91431CA251A06D4173CA9391F718DB2F36C1DAC70F9651C7F122988B78D73CA65DAD2983417C89BA36F9CD711023A7561E7492752A4749CACD59ABF1763C404452FB3E1B63708A0D561B5549CC3D35A953E497852A6138D9B367EDC09F0A23B9F11B66EB29A401907B57BA46B3BA411A04FE706BC342980232AB225E81AD6B4B5A95A0763B30BAD646985995F8B779B015A7B53714EA314B2BA006561A40195F4CFF8B155025BB289E05C760253FD761623106103767B75F0A08BCC593AC5A5BDC114F89C3DE7005E044A167874C549E0A9CD387C8147179C52A81F028B6161570B68A9A9236B05CAC19E037BECC7425D5C35DBB671FEF281ECB581BEA03A13887246191E2F1302A59BC4069297C3224723C3082E4424866A77DD0BB48CA5B0BAF7C54B816E6C2140968AA16EF906474C26581010D15B79899CAE94FC149E15421EE23DDB00A1D90A1061DA94E8DA8389F35EE2E990F5188127332C81E908DCB51DF501A203AB25708279AC4663241916F1499F498C5BF019A31618A2D2BBCE12B613FF5791E0925922226FECC1BAC3BAB0474B06C6044A26A2B53E4362AFE55198420A4C0240E3BB28548C7772D999A1155A6AB36567E463C905317C67C0A1274024CC05C002B3E1D75FD0F2C25980961F6A2329ECC71244684A0B92B0829D7CC2BE6AE3102ED7B3F9BA2F867B1F4A4B97F44C0C9A01B0E9C3C6CDF1528FE8220952742326647BE33A9459888D14C26E2C0E778C9314B845E1001699B0655FB1379EF29EC7492919A162AC900F816986B1B814092A9C16BC37DEA359B9D6B6A6B8670E242EE4147162A80A0E73053928156D1333E4D3ACCE0A084D2B6FB4501BD1A90B9731345CD823923A068940505E830A2207A1D1060824108E63909EA3B0055ECC83255565D644A91D047AC69426B6819BAC5A880BF9815AF0B57BC3615161A455D7A7E33C05F61461AC2C1886C47B3A9C51805AB2DE3AB4D89CAAD618274988BB26B4B789CA7C6236A5FDD1082B87BBA523BCE4906BF14A31E96A7D7B466471F233EB70A48BAA481B61480E726875C6CA95678EDF79B4CFC12A81C3A99BA255A32A2DEC84CC3E47A3A0444A8D378C8DFB4866E15DCFF229D4EB2A8E083BDDF68DA6336960A9C2A5714E3EA69CDE48958A37CDD26CA7A762A41CD3C194C4C039DA7FB0433BA2C8892C8247601368AC1965EB085D38C363EC8179C55A835644B7233C2173D6231EEA81DC64CC6E4330F3955F1564464F664B4267805DBBB2A0E98DD6259C7D1507EFE8C3C8876CD68AB3E400A2E3C90F5967B10B9CAB0B82B6222108E1F9CA670AB0DB49B227433A8FD6862BBC866D23615CBAB366B972E227CA8E3B156081CC3E74128BC1360E750F35119CBBE2C950B8C6F785B2AD6A58611716EB9C5534B683549003A3613267847B2B8865BBD2BE3EDBCF1FEC41EBA0BE97000E20BB41479991DEBBABBA19CBF7152E1311741C99928883313CA747E108387B648A4E416DE4CBBCCDE40A91BC6009376D6D9CBA77E7A810D7B56A21B1142C81DDD4123532CEDAC6A304394BFA9B1312A40D9560C8C010935C6AABC9C16EB30659C19A9AFA1593A0780E138411DB9B9D0532496788679A0A8595879C14F95C234A6FE20200605841E741792C6225CD2C15E868142731BB102666B5B8193FC0C45C2903E22C9119208D2BC671C5070A06335FDE9C5F8D2A3BE0B60A57A12DC4DA0BFB31A10F3A684061192CC02A3A91120DE6C873514081C515A2C001C8EC4738608FCBEC374C5465F41C558768AD3BC269FDF835B604C1C369BEBE0CB29A4AB5CE2C7F40D232D62C2E93D230FBB361F119C025C3143C841CC1E17DC3F1B9FF5199DED839C044887E256A0A01145453817234816FF45462005A8A064ACB9AB692832D55EB0715C35228906DE053BDA1B9486D272A78637B78635070587771805258F823D46047C3D69611E327AC288549FA7AFD41046CD90BBE535D8BDC698259B2EA691EEACA0BA2FA2BFC38A12D62154566A3C3054B81F3CE46337F1FA4836BB45167D9937C53A8D0C6585A36AA1E1128D8642339C8745E4943B9164CECCCB1B1AC6E17FA22422B85FFC9211AA875813C097838AFDAF74B496A9503D63289356DFBBA499B703136070C78E193237638F80844B4C6BA9B4B85A36007553246C9C543EC8AC4F4E8B0412084EA83A68EAB52F3924CD9DA9427285E25F5A68D097CAF262372663D41A681F2101AD7795E75D123619491262C72CC80C6EDD5AA7A4823B65CCBC4979E02236C02D206E1EBB63232AE1D28AC492AB8CC7BABCDA32D6ADA6E8ABA9DF53C6B63F61CEA1ACF8C7C5ED4DB51DDF4BE3570B225521B1B92C7ACFC010283070D5C7D3562CA45B48911770D5AD06939A61D6B98629AF14CEDD744BE8317C4636808242D94A20031B07F75B6322620A6D992DADB43FB2F7A2FBAFCEF48B3B8E1D80D3E4ABD720CB72988D306183A19868C108AB6DE51C56FF5A1593FCF8D74090736A253EF8661106C168684E2948C6EDFC4C555517825EC4AF4E33836BA1F51888984D0FCF5D5", "4FCFFF02345D88F72600A06D9023E2B4728C9615FF675E97FB31F418ECC450F7187DDF6CD2FFA4AC97B501780C81A21CE6EA5401B84E382B045BA6DD7ABCDE9816CE4639D4C787A74087E05C26745F565A533DC2686C2032100E4DD0795EC2EAE8ED0E3BD781603C50734C51BE3743131F238CAE2F6A41BE4D333A3E4BB2EFAF857B329C65C58708FF757D9186A0BA021DE5C8F83157EE91B3B41802990B03FDC6839321607B7F3E77D622BF771F05FB3B424F312F83BC7BC5D57EE6E3D2CE8D8CCA2A1553CD56F58D2D730F3FD94565A3ED8C00783AC3EC7894C3B1C5C80EFBC2FCE36744F5A8ACAE654FB8239DFB841AAE3761C373200B663D94AA8445C97C0ED59EFEABE23BDAFBB59FCF932081A7F8711E2F97007CB8D44290AE5A562AF1FCAB608C13ADB02E9A86C6BB3122BB753C193AB24DAEF459CB55A9F82AEB025B77405F6F792FF5C155DA36576019D13C2D9F3BB349356E2D4F66970884597821FA2F8578E777085EB3657711AEBBB92C19F837E73EF169252C5CDA16A249FD2AFE0E643CE513A1EF9CB8B921F5F2C92B75510EA831008681D01DC7755861874DB8F4A4066C4B3BCA6C309E0987BB88BAC0EC5D903CEC9D23981DE54BD5947FDF8448E9B39CC5DB6972A976097AE432DE4E5749151A0B1552FD4D7D8FD8FD255F9E781BA60E768534A87F55F2F0ABC5BB62C44ADF6F3CF8EEEB24AEB01073E3ED6A7D94B7284ABF0789A9EB3ADC7A1B6AFBB8EA64261EA3B260270607B30E2235728BA0B3C0DBC7093FE88053EC651FF98EB06C7CD25556FF815A92A9C43A11C540FA17DA529C78500FB77BE69A80C88E34D377D0D3A817FA6EA783C4A0FCEF69B689FF2135A80E871FC7260CD8B143CF0D3A251723ADCACE2DFD93BAEDC6D6DDEC139E56620DC7863EA41C4CEAFDCFBEEB8799DBD69C486AA32AB0A0EE7FAE05BA80B76627B7EE9DA48D7A69C586577A897502177A236EE3FAF1D6B1BD50DD050BA6F7B1ABE2D7D987FAD1692950A2D07EA7678C13A6788DC668235AD16A0A775E79D88B165E7439AF9ACCAB04A577157AF4183A438BC2D045AB50652D320848BF7D4D83DFC0CA140ECCE29955A0CDFE4E5BF9143FEC9503E6E6DC35747261548FEB5B9B148DB989CC37FAE7DAE57BE1B0E181C32B831E3698DE7D95B638401D6403E092455D18F23E45B63E1EAD2ADC2DB3C01B8DFD46E97D13381B3278D85BBEDE057554D6271063B36F962E4DFC1904DDA5FB46278E3BDF17754E737DD00B7A374BC5DDD474A6392E0630765287C50E13E0DDAB6D1B244B13C59AC6BD925C175970B678BE7302806BB79D0E96118A5E80F6C585B8B5C1792DD295D1285AA7BB4929A88A2E93C58ADF713BE38E3BAF5E20D630AF697232A11CE1FE4B3207FA0FBBE135780166CF9329D5D4EFC777DF35C79DCAE1940533CAD14FCC3C9F044BAFDA64FD8EDD81BD24B388940C9130AC1C57560475AE5C9214D824274F1B9EAFB7BCEF0761DB486357B010D5A1D9D997BA77FD3128A22105ED604A862DFF6FB40CAF0C2E76613324E9F4434CF6E6F06326DBA3E66840F9715F0762D4D21C70D87A55D4D28264F0DE34F072C7B9A42677BFC3B9450DBC7BF44E8728B97AD2B4BA0439113F3173BD470312B0AD3B9AA7D0AD809AC4C26B6F6BD5EE0AFF7E415A043FF255073066EE11F32028CCD068C0CB6B0923AE9B70072777597E00767701CE132E13552F16B73C56348119D28F34D5E5E1B05E28F20EDAB616D01DDC2D3651EAF0D8DCE3DE710BBFF7B2A0750FACAC8429FFD2CC848891F50BAD4C37C739ACBB7121864E4C139C5533E3F56861C0769495941603D172D2992EC980C40083A398BB6DC3693D66BD8F58028D1FD302F47C68E502BC3D34EC29934FDCC6907F0184B3DB61D0F41F5C10F6532C9592AC25FBDA069609A91D74781F69F3BD2B931453B9B4F88C12397C398E1C1F16C9C2B1C7E290721B9C89B919527DC3231193271C1552236F468744DD930013D172802D1EF3B603DBAAF2A36C8298A59C56A4731CCEF5E403E8358C3A878C5927EC4C322E61AE0AF4BABE85D8C4F4A71893AFA1428B54D269F4C9CDD95962AB77E14050A224E33922C366407B329B00153D14FE6CA8DB365D9AA29C1DAAF50644F825DBAB5ACB30EE6328AE585AE4986D38BDAF932B08F390E079BD00D2CCFA9534DB862621D30EF56EA663EB938F2BFB213167", "6F27F20F7092A1CFA3C21542CA2DF326BDEA92D1745B2CBEE8513ABD55BD970C"), + ( + "044843AECAB03CA3AD8387106D4587446199A01101088A95A6C98FC7726BA642A2D1E0647BDB88D941606EB67E4BAA89D951658A9127B3B08A52E29E5F76061DE0BD8985C43A0B9C57202B3C59BC0DA5223436C5BC83718E8C5EB3B0C4FA977FB52C77C88743CE3006807C6913E05A516906B0828F3B91812950A1CF703E89560DCDAA2CA800AF5A44B995F7C64650CEB6477273F1B6AC0B35FD030D0EF24D4E7132F2788EFDE0A9E215291859426F70B7E4550D17F815FB71CAB59348B6C4278180A4AA28744EF2B195FA4ECEDAC8910C4C041958A702518833A6934A446117BB45159ED4BA42D1421D52071B0D9BA6879906CF582971EA30F1A63C64A52222F8898E080B56A98A33287963B4C4D11916CF716E7B079D5CA33ED5D7C75138821706475BBAAC396BA31D17CA5705A9B04044343064BBD5CAF0A6C57A45CE3B216CFFE47345F97DE9440048E175C6324959423534548375378933C29B2DB02779AA5F6B92796D61A18F89917228A4AE8113E9744C750B6EBABB4ADF1B46379891ECA75FFC291A56636496B97C27BB5F8E118B84DA5B3194146B375B9E210D1C1B2969B53C34E889B49C481819442848BF30E70B6ADB751D098A0122951DE6C8833081B5B047D0517EB3A819567729292C09DA47B3F529C19D6630C5A9330FA8AE6E176D3C4A08326B1915DC0D00A68E01E10972E8B7459B41A30205A0168F266821BD17A3EB63C1830687A3EB6880D3352A7785424C064D4A9549B40203B3B43C34064FD1986AE6516E10BCE0662022077DE2987A48557091B3863F1B741BC03D70E4C219847D54B414009993D0E60A13A61E0C651148998D23B12B2C05AAFD15B76EA03503277A4A0368B063027C9AA303693A3DDA241019B5C850C4D16745D31C6E421614A4C092634383F2327B25A29EAEECB7B9E81B35940EA3234F043164D7658440DC1967518E79801D1814811DC2BE50CB3129D984EEF44CDE0549BBC31E147B1B5330A0BE001E865414A1CA98FCC557EDCA065DA97BD1F46FBF64A77D95A7E3FC6C02B74904D3932C86198B73B3B13B667D6C3B9D1B305D9949E590B9863956C6567C3338A51B5C376481A971E7BFBCF8BAE9793B87D2B94A32A250B98A982B413CB33F8098598FF6888F31A3C7E6A548F05B24A77254F6798EB88FB838B09241B92EE034A4E16EF0048B37291E36631F50359EAE237BD7B3382C7B4DE3A95845CC6E8D5642A42B0A3BD4A27CD3B5B4078D4B427FA5070289D5CE8F022956827B1158594BB238C23B70F2CABD00B882A194544248C7D997792E30727D612C04689ED716353ADC4D93D1BFDF2B91E767A87B779F81F4A295A112D1087B03D71E4D9AB2C83B96632128A154396621459012B5DB39136F96826DABA6EF68627223684FB13F4FE7937EE859B22890874C811A32460725920111BFDB5C4882524C544277FF72CF127A840D07AE93FA526F7432195316CDBAC5C93822AF11C738755C3AAA37AC8C24BAAA51EE049BA17C543665486FD50562DBC5FA14C65F63A47BD1CBA840CE4EC57CDBA0B49E020ACE272A95B64A4B37583A6861DC8017327C4D14EBAD5C47AA28F6C524E961047C3167C934F411A7A9E75C2382654225753F480D717B933A4997FD45773C09AE01B5CFC212AB91341387E18B18472AEFFAA9A2986D0429B756A25DBE026E0137339EF82D10B3618B3C5EAB4240996274896174E6FA7443B753DDC27B15909B25959AB376437CC316D9561B91310BD7C575DE15C7E0228BAF8B4695495B36D056CCE58732D40823DCA59597980A168054311D32B19C154989B867830AF61C78988640A47C1FA71058213A4D93B6899507CED783126C88CC2524FFCB8E93FA40BCA06BC9F2372D92622BC04ECE69CC64042F66ECBE660BC72533C75DF0BEAB4B9EF91431CA251A06D4173CA9391F718DB2F36C1DAC70F9651C7F122988B78D73CA65DAD2983417C89BA36F9CD711023A7561E7492752A4749CACD59ABF1763C404452FB3E1B63708A0D561B5549CC3D35A953E497852A6138D9B367EDC09F0A23B9F11B66EB29A401907B57BA46B3BA411A04FE706BC342980232AB225E81AD6B4B5A95A0763B30BAD646985995F8B779B015A7B53714EA314B2BA006561A40195F4CFF8B155025BB289E05C760253FD761623106103767B75F0A08BCC593AC5A5BDC114F89C3DE7005E044A167874C549E0A9CD387C8147179C52A81F028B6161570B68A9A9236B05CAC19E037BECC7425D5C35DBB671FEF281ECB581BEA03A13887246191E2F1302A59BC4069297C3224723C3082E4424866A77DD0BB48CA5B0BAF7C54B816E6C2140968AA16EF906474C26581010D15B79899CAE94FC149E15421EE23DDB00A1D90A1061DA94E8DA8389F35EE2E990F5188127332C81E908DCB51DF501A203AB25708279AC4663241916F1499F498C5BF019A31618A2D2BBCE12B613FF5791E0925922226FECC1BAC3BAB0474B06C6044A26A2B53E4362AFE55198420A4C0240E3BB28548C7772D999A1155A6AB36567E463C905317C67C0A1274024CC05C002B3E1D75FD0F2C25980961F6A2329ECC71244684A0B92B0829D7CC2BE6AE3102ED7B3F9BA2F867B1F4A4B97F44C0C9A01B0E9C3C6CDF1528FE8220952742326647BE33A9459888D14C26E2C0E778C9314B845E1001699B0655FB1379EF29EC7492919A162AC900F816986B1B814092A9C16BC37DEA359B9D6B6A6B8670E242EE4147162A80A0E73053928156D1333E4D3ACCE0A084D2B6FB4501BD1A90B9731345CD823923A068940505E830A2207A1D1060824108E63909EA3B0055ECC83255565D644A91D047AC69426B6819BAC5A880BF9815AF0B57BC3615161A455D7A7E33C05F61461AC2C1886C47B3A9C51805AB2DE3AB4D89CAAD618274988BB26B4B789CA7C6236A5FDD1082B87BBA523BCE4906BF14A31E96A7D7B466471F233EB70A48BAA481B61480E726875C6CA95678EDF79B4CFC12A81C3A99BA255A32A2DEC84CC3E47A3A0444A8D378C8DFB4866E15DCFF229D4EB2A8E083BDDF68DA6336960A9C2A5714E3EA69CDE48958A37CDD26CA7A762A41CD3C194C4C039DA7FB0433BA2C8892C8247601368AC1965EB085D38C363EC8179C55A835644B7233C2173D6231EEA81DC64CC6E4330F3955F1564464F664B4267805DBBB2A0E98DD6259C7D1507EFE8C3C8876CD68AB3E400A2E3C90F5967B10B9CAB0B82B6222108E1F9CA670AB0DB49B227433A8FD6862BBC866D23615CBAB366B972E227CA8E3B156081CC3E74128BC1360E750F35119CBBE2C950B8C6F785B2AD6A58611716EB9C5534B683549003A3613267847B2B8865BBD2BE3EDBCF1FEC41EBA0BE97000E20BB41479991DEBBABBA19CBF7152E1311741C99928883313CA747E108387B648A4E416DE4CBBCCDE40A91BC6009376D6D9CBA77E7A810D7B56A21B1142C81DDD4123532CEDAC6A304394BFA9B1312A40D9560C8C010935C6AABC9C16EB30659C19A9AFA1593A0780E138411DB9B9D0532496788679A0A8595879C14F95C234A6FE20200605841E741792C6225CD2C15E868142731BB102666B5B8193FC0C45C2903E22C9119208D2BC671C5070A06335FDE9C5F8D2A3BE0B60A57A12DC4DA0BFB31A10F3A684061192CC02A3A91120DE6C873514081C515A2C001C8EC4738608FCBEC374C5465F41C558768AD3BC269FDF835B604C1C369BEBE0CB29A4AB5CE2C7F40D232D62C2E93D230FBB361F119C025C3143C841CC1E17DC3F1B9FF5199DED839C044887E256A0A01145453817234816FF45462005A8A064ACB9AB692832D55EB0715C35228906DE053BDA1B9486D272A78637B78635070587771805258F823D46047C3D69611E327AC288549FA7AFD41046CD90BBE535D8BDC698259B2EA691EEACA0BA2FA2BFC38A12D62154566A3C3054B81F3CE46337F1FA4836BB45167D9937C53A8D0C6585A36AA1E1128D8642339C8745E4943B9164CECCCB1B1AC6E17FA22422B85FFC9211AA875813C097838AFDAF74B496A9503D63289356DFBBA499B703136070C78E193237638F80844B4C6BA9B4B85A36007553246C9C543EC8AC4F4E8B0412084EA83A68EAB52F3924CD9DA9427285E25F5A68D097CAF262372663D41A681F2101AD7795E75D123619491262C72CC80C6EDD5AA7A4823B65CCBC4979E02236C02D206E1EBB63232AE1D28AC492AB8CC7BABCDA32D6ADA6E8ABA9DF53C6B63F61CEA1ACF8C7C5ED4DB51DDF4BE3570B225521B1B92C7ACFC010283070D5C7D3562CA45B48911770D5AD06939A61D6B98629AF14CEDD744BE8317C4636808242D94A20031B07F75B6322620A6D992DADB43FB2F7A2FBAFCEF48B3B8E1D80D3E4ABD720CB72988D306183A19868C108AB6DE51C56FF5A1593FCF8D74090736A253EF8661106C168684E2948C6EDFC4C555517825EC4AF4E33836BA1F51888984D0FCF5D5", + "4FCFFF02345D88F72600A06D9023E2B4728C9615FF675E97FB31F418ECC450F7187DDF6CD2FFA4AC97B501780C81A21CE6EA5401B84E382B045BA6DD7ABCDE9816CE4639D4C787A74087E05C26745F565A533DC2686C2032100E4DD0795EC2EAE8ED0E3BD781603C50734C51BE3743131F238CAE2F6A41BE4D333A3E4BB2EFAF857B329C65C58708FF757D9186A0BA021DE5C8F83157EE91B3B41802990B03FDC6839321607B7F3E77D622BF771F05FB3B424F312F83BC7BC5D57EE6E3D2CE8D8CCA2A1553CD56F58D2D730F3FD94565A3ED8C00783AC3EC7894C3B1C5C80EFBC2FCE36744F5A8ACAE654FB8239DFB841AAE3761C373200B663D94AA8445C97C0ED59EFEABE23BDAFBB59FCF932081A7F8711E2F97007CB8D44290AE5A562AF1FCAB608C13ADB02E9A86C6BB3122BB753C193AB24DAEF459CB55A9F82AEB025B77405F6F792FF5C155DA36576019D13C2D9F3BB349356E2D4F66970884597821FA2F8578E777085EB3657711AEBBB92C19F837E73EF169252C5CDA16A249FD2AFE0E643CE513A1EF9CB8B921F5F2C92B75510EA831008681D01DC7755861874DB8F4A4066C4B3BCA6C309E0987BB88BAC0EC5D903CEC9D23981DE54BD5947FDF8448E9B39CC5DB6972A976097AE432DE4E5749151A0B1552FD4D7D8FD8FD255F9E781BA60E768534A87F55F2F0ABC5BB62C44ADF6F3CF8EEEB24AEB01073E3ED6A7D94B7284ABF0789A9EB3ADC7A1B6AFBB8EA64261EA3B260270607B30E2235728BA0B3C0DBC7093FE88053EC651FF98EB06C7CD25556FF815A92A9C43A11C540FA17DA529C78500FB77BE69A80C88E34D377D0D3A817FA6EA783C4A0FCEF69B689FF2135A80E871FC7260CD8B143CF0D3A251723ADCACE2DFD93BAEDC6D6DDEC139E56620DC7863EA41C4CEAFDCFBEEB8799DBD69C486AA32AB0A0EE7FAE05BA80B76627B7EE9DA48D7A69C586577A897502177A236EE3FAF1D6B1BD50DD050BA6F7B1ABE2D7D987FAD1692950A2D07EA7678C13A6788DC668235AD16A0A775E79D88B165E7439AF9ACCAB04A577157AF4183A438BC2D045AB50652D320848BF7D4D83DFC0CA140ECCE29955A0CDFE4E5BF9143FEC9503E6E6DC35747261548FEB5B9B148DB989CC37FAE7DAE57BE1B0E181C32B831E3698DE7D95B638401D6403E092455D18F23E45B63E1EAD2ADC2DB3C01B8DFD46E97D13381B3278D85BBEDE057554D6271063B36F962E4DFC1904DDA5FB46278E3BDF17754E737DD00B7A374BC5DDD474A6392E0630765287C50E13E0DDAB6D1B244B13C59AC6BD925C175970B678BE7302806BB79D0E96118A5E80F6C585B8B5C1792DD295D1285AA7BB4929A88A2E93C58ADF713BE38E3BAF5E20D630AF697232A11CE1FE4B3207FA0FBBE135780166CF9329D5D4EFC777DF35C79DCAE1940533CAD14FCC3C9F044BAFDA64FD8EDD81BD24B388940C9130AC1C57560475AE5C9214D824274F1B9EAFB7BCEF0761DB486357B010D5A1D9D997BA77FD3128A22105ED604A862DFF6FB40CAF0C2E76613324E9F4434CF6E6F06326DBA3E66840F9715F0762D4D21C70D87A55D4D28264F0DE34F072C7B9A42677BFC3B9450DBC7BF44E8728B97AD2B4BA0439113F3173BD470312B0AD3B9AA7D0AD809AC4C26B6F6BD5EE0AFF7E415A043FF255073066EE11F32028CCD068C0CB6B0923AE9B70072777597E00767701CE132E13552F16B73C56348119D28F34D5E5E1B05E28F20EDAB616D01DDC2D3651EAF0D8DCE3DE710BBFF7B2A0750FACAC8429FFD2CC848891F50BAD4C37C739ACBB7121864E4C139C5533E3F56861C0769495941603D172D2992EC980C40083A398BB6DC3693D66BD8F58028D1FD302F47C68E502BC3D34EC29934FDCC6907F0184B3DB61D0F41F5C10F6532C9592AC25FBDA069609A91D74781F69F3BD2B931453B9B4F88C12397C398E1C1F16C9C2B1C7E290721B9C89B919527DC3231193271C1552236F468744DD930013D172802D1EF3B603DBAAF2A36C8298A59C56A4731CCEF5E403E8358C3A878C5927EC4C322E61AE0AF4BABE85D8C4F4A71893AFA1428B54D269F4C9CDD95962AB77E14050A224E33922C366407B329B00153D14FE6CA8DB365D9AA29C1DAAF50644F825DBAB5ACB30EE6328AE585AE4986D38BDAF932B08F390E079BD00D2CCFA9534DB862621D30EF56EA663EB938F2BFB213167", + "6F27F20F7092A1CFA3C21542CA2DF326BDEA92D1745B2CBEE8513ABD55BD970C", + ), // modified ciphertext - ("715A321C531ED2B3CF63C018FF403FD51C41E9212C73D3BA70694D548910EE98CA382120364347779155C937C01D858D63E0C645A804B22960FDDA499452C169F48422BAC0E17743A11699CB884AD02C379E96C8E2985E79E1A85C316FA872BAFA8A1F599B36B4250ED556896D66BB16F70A9A3912CC509D8FB4B0393559CFF75702125ED006D0A0E8C6039015791A7A2CD0950EEC7326F472594B13C7CC0C536C23B9EB06D504A37F31A4920B6E7F67C2F9A36EF9BB0A2A2447E32071C8DAA0EE3A38BDA28D31F024927C364B99C2F71809420301A5F70AD4B91BA533364307194F94150E9B2B1EBCC10EF04ABFA478DB559082E41AAD8477C1A541D74A091CA3988D128FDD6568D52CC12B016DEF8C8748AA3248954BC3A79977995EA444BDAAE879FFA793F7D024B59CB5CEF600ACF79DFE1A3FF9726B0E5B0729CC91DCF6B11014B9D7281E6D848E22820B44D257BFE16E3D16796B98CB560B7A7A1312DD6A07762B343E70C677E76D01A2A36C2712E9D59480E14284FC099AB79C5A47585B151D0CAAC0E01193AD139C9C37A05BAB317DC5B83A504F7D999C347796D6A584E6229FB401733C44593894423C60A1B2B11047E11858F1838059AD56951F27F77E85303FF9A46FE5D490C9009513F2AD0B635A30EA43E6FC321F33932D4B6DF086A56AE87134820D406030A751782D68068D726A68A35B54A790C7EB38B1D73B8715409B4B79F29BB240830CA8D195FFE7098810118ED14B01184CF08537B4B178621B84FBF093252687775C1940DA9EF0A209C3D8A0EC5C174B01A6D1858BA0E86F30684093C18E0A3B135C303782C804E8E9B67E24228830816B750E5A247D76D4483444BB1A6254985A79295B431A16C5AC0AB9B7A02EBCB505B5E7AE6DFB1D722C5AA4F4CD9F131655FC00C20484A3F7CA15EAC37E1C722CC48BAD325ABED4966666CACA039D40F23F24E1A7AFC4CEE076999A2BCF6675C27CF654D08998CE64954C835109C65656F08A6062B0A6C820231A4F1F3C2828E8A2C1606FE4E2280A6A0047153BCFA20D16C501C6B9BB62780365B412CD2656285A3014821F7E7A26F7CB0B4A4B13F3F24858A0A349E385612A9B05D15F17824FF8D901C8C3BDACC3166AE785A0031CBD8B12ED942047572C8798C1995C7332AC57D8726944C5CA75244012C889C673AD5E8A9F8892B53A7A6C9CB49199812C39389A8033077F069287622F8DE0C1B647C9025A13300631DDE5A780E50E0A193B845A16B01384FF99A5781B070A962ABBC8BF794A47815AB29C26475E72760B967974E26523564A4E54C139324808B494EFD9640D4A287591AF1B414E77894BC61A09C18520971714D47AC6E736BF9B91A706A05125195FD104C74A5534C5E21C7DD66D0FFA0C13ABBAB447BD12C1B654F4BFB510896934AD050B17F490374F81B62B192C5F1653879B1D098727485C558698996C125796E3C1528860CEA9B3CD625CC9E2B7A7484C87D655861944BDF59132C20BEC705F0C79B4FE9838BF6B48ECF7CF8230C33241C57F82C1F3F31554C13607790AEDF759951A5579DA3FB1018F3F28B934158F6F36AA1516A260A0BA5F822155E18DDA6318A6832F002B43C4B274C482CDC314053C5809D8181D68311BE5BA5A9922582717CB9B751FD946589DB313C7551C26A7BA9BFA8C97D43FE8D6AC2E2B8B4AC802F0C33BED275D242673A1912EE1C5468573A1DDF68D63E05858C15A8D6C69F604CA134069B53A3F4F0765E814654612B19261198A25C15353229D4261A531CB7EA508F1D72319C119334AC9A6FC4B8398606198896EE37AFC4BC71801C16E97B016DB9799E750FCA836FC1099DE6163F3B9BFBB8864E43888C9766CD022CF203B76E937947AEA5D0FC08A5A75CBF9610B3B4B92E8E534F3B4343A4109D10631A0909239B878BEC48C2F202F41A562ECB594D74A1626ABA554D2349611314529A293D0A8F8D69C045149D21A35875CA53E0A3D84A771DA39150AC572AFEB7DBB1C4A755646587635DBB449575494628ACBE9090B50782CB3F2528D4C982DB95C664376A6245241605AAFBCC0D1D3C4AB5C5B8C54969C977DA8F95C4C93A02562AA5F5BB3E2058E56B0BCA002CA3C7A88790C5B9BC60D2B46A80D681CED7283D1A46511E55569EB5BFE06357AF020BB19484908601FA981F86842D850B79D9B2702B7527E55CD948BB7ECF069E9BA289C8717C6D3C64A3AB1FDEB88E97010E2F5089FF13291E85DD0C8ABA58BC935CB1B98D779BC00BD4D0AB894845B5AB101800336E99A99341845CF731A40337FB7D3B78FBA276914C41FE6BB1F47A9895B9B8849CDB07A63983A869C252209C858F6A267F5408533A25CEAFA908248A46A4821357801AFE6BE53F5135975B5CEC1316D46A8AB195280FBADD0855A6DCB5FB3761E22780DCB3B3134E9AFBA09A59FBC74FB41C76FE73709F02793446C8CF578FC9408F053C28EA271F304A507092ECE34823E19086143A42666632D6193CAD57E2ED370868C0E6E794E5D432CE07B9ABFD9ADBB900DC8B51B301BC2F4D89D1BDA34DF8889B2E22CB34BC4AB77778B0A8303A9C8D8B4309295548FD83555A221B1D86337468FB7A698921420FE538EA772CF6CC655B106250B0B3F7C4C3CAE404BECD46C80352D893C9F0F67306572B89AA3C9CF92A53DC941AFA27932B24A1989B8DBB8756F298CA8A93EA3CA6711471068817B9DA78DB7773E06A4CC75B3541C4287C3946D44572D43FC58F104AEB421AD3D21BD0C567368E60D9772B73402406939117C45C2AF0508A9005F42C0852D52B08850CB23C2660D5C5C5C5881374B12A75623DF343B6C336921B218147B28E244AF9E6C866133CC6C589882A29CF1CA07261562D946ABCB66734C8685B60C5A27D0480CB99BD2864085EB57CA982197066412434735988A12B5177949B71542356439483E8A7D3E54025F821DCE34C110FAC635806C36FA9690E11801B08EFCC920E7827B3F7959FC580BDE654EEB5512CD1C3CB5029BC8D0CDF201976FF5991DBC5950392999786F9E62000CB45C3C8B7EA2DA6C9F0618741BB1A1B3B9D7B83D7390B1ABEB6C74099129CA1ED1D230E0C1C676F9313AA32A3F25B9BFE07FF3B485A67424627501D2371DEF9CC349DCA5187578BA3A1B9BB9649068981FC75E21F17DE24CAF0896CA5040238E5CB76762BE3391790C7B6C4DF795E3A1850DC289E45C0B4C66931924126A6725D7E551A82B33D700234EA5ACD00AAE1F844F28173B6F31A8BE870D53D230EBF3C8F202C1D0B579CEDBA9AE61A89B94CC3034375E086694D6A59EF176156C52D7E458ABB317D0C417EAE7625DC21D5409293E35AD77AA347789B69127AC9A975B5BF74A48D82D819600E69A963FCCBC36E071CDDA374026CF8E5C3E4D97341611CAA52A5763BA5D0C9028D073788B3809CEECBEECA8172B37511826AF08BC3CAD547D5F9A8F3256B3CAC5015D5ABD9BD26CF9431C4CBC498BC015A67052911776810A5CE84041243BBE16634128FBC1395B045EEBA274B22097E2982A767353B0535A90B0A2D6CC6394B177A17C568CC2174911043A4A3C92467FD34C7A3C0E4851625E862DB9B876335684E1ABAA5D9B98E952A3CC8B6A80254B3251637E40BAA47B5C386C345E5B37BF992B6424CD4A699E2135CA96F2A03D05C5934384B3B03067565DAFF43C0D884EF8149551AA46A01A870B7B266F26072E7B8106EB399AA22E950429AF522C03F57CA7D0784CF790BABA1DE7B8B85BD24713925DED64C824E3C163C77C677B4BEB9BB25077851A76412C8608E9547CE2A93D271ACA89B0A1A205A1E6F06A8276296C704BBD8A97E96C1E101363AF2724DCB693F255AE466A3CF777C166FC7A5B211EA6E0CD5FF77D2564ACE3A3C69051951A6B243F7C7E3C8B00CFD80ABFC4A6357749C6FC38C926A268ECBB5A585F364A9938D100CB125061430554DA0468A59709143AD7F930DB772F61984A29EA9FC16C7058FB7118C0BF0FB91DEAE8CFED9B9C0665C147E37EE7E7AF0F2BCDB0C07CBCE448B15A910DC83F71678DBB794927E0A75EA4914ADC59CEB527145C37CC03199326149F5559CC54381E5786DC73C7B3302769FA4AEE488F79E06069F06C29D84AC1939070FB6B05824FF717BD5B7C5C81DB0F58F9141D32BE0DAA8D5C69A383181F2BE14400EA79B3C321C6A87FCB743D1FA15C1C625A6F2C205E47ABEED35CA818170F15417E908DF0EC88BB1B29820484C5A14892502E13F2AB4F85811B0B3512E66A6F2505C76409FF36181BA0BDDE8B3E4EDC723F073B9B244E1D86AC241752979CA9E6036A78C0A2E0D6CEF3A4726848133166BAE2EE8E8B26CF1AE15B074C62CE7CEBCFB77640E471DF9054A487F7274D7A16074F00F74E93BB83EF23A8C8396ABF557DC52BDD3304102142A46ED26C49BE675691CE797D22DAD9754A023521341437B8BB438AB6B75D8ED29258F7B045", "E36A4E6574282A20D31E5C7901C5CF76FAB307935B10D47F28718BE945DBCC69F6DE7F904AC4C0C312014854D262044AA4258BBB4AA3276AAC69C9001509CB009C69634B9D1574351E3A1D10EF9FDF7DB56DC9387A6F4C545F29798A7714635B492FD81C678FE58C5BD1710DD91B4F841657B6CCA691F360A49304AA12DACE954613CE2333356C2D90AF8851D58BBF5CB792E3F3322944026B78EB5BD2F5BC10499A7CC07F53CB6ADB9B448046A432FC90125919FB7A71ADD744B997C66CC817EB61CD79B27DA95CF84FA6E046A2FA66CD3A91CFC27D5F9DBCE86825F5AA657D5C18C0ED3E506A0892FCF21826B7FD31F4CF5F37A198CB18AAF2F85A64BDCD859F1B353449AFC3D80CE73A7E1518983205A32ACAD694C67B292EF0FBFB2BC89D7166A3F7791EC5DB5AD2FC1466637F7922162331D645E00DD14C1E6A209C5FCBDBDEF2A9FAA97BD6C3D9EE81E6E5CF96855B64C9B601CF864F535DAA5211A58B6FEC8FDA0F38ACD98120EC178CAF41FDD4C756D665AC532D40EB7BD2CA187360AB665EF2C1AFC7AFE13E0BC3E6A64A8E92AF0582F29ECF3147A467F81AD04669C194DEDD94540CA79A6205513CD410352D4E4C0EAFFEF84F5C8836EA402325801DC3EBBC47432C8FBF34E50CC3AB3743EDEE3889B97BF39FE52D9A3094FB532FA128A3260F27A01459AE3F92E5BD3741FD9FCC9C6AAC158E352B0CAF8F444C4D935A63454617410E2CECCE7034662AF2DAA8BCC4D36F5C07F69FE5A638CB406997ACC02B686AE4878F3D036524D7C2739B002DDE011CEFC4A163A053D8327D490239AAE2A8E84EDB45A3678E2FC2BD0A5F073C232F3718D1462D421554DF3C0E83A7B7BC528C5F5678CDB59E1A09E1511F7B95744EA087E35F65CB053FF5CBAD86E37A65EEAF2FB15F5C1ECF632002A4F15637BA6F5A79526C51D2268DCDB1E7C01EF333B8C8CAB1A36D6E9291092C50D4DDF1A601498398BBF8CE06A92453394F237B4C068EA27258AA6E28A92DFF1F5518ABD343DA2345F3E288F21B87DEA760510D6646FF80894E87C153B1C522396A7D1B5A01294E8A2913E6C10A88C8081FCDE677FA050B5D8B380FCAC6E76338A4F0E05C5E10E914746575413A934D21BEA2C34AAA56DEF2E0CDA2569319D3DE6B780D9079A47173CF232B82102B6865581A9B4A6DFF4BF96DB1F2A996596DB01B70CF9A53C3515CC08CCD880739A0229F032D38102DA631A7F117F1449E8DBD345F87946E6D86AEC9C3C6B5784268CFC983C966185EAF4C7D8FA8288FED7597692008B8F962D7A26C6F7F4712008AD49A441A42D0B512564068ED455F3DE379C54221CAE6485745C435A28C274AB6DB1B7249EA148C2532FD4BD990C9C9F0BF6361791D2B0C6086A881857EB5DF4BDCC2C538CCF5BBEB9105BE762DFC965C253A7B99144BDA0D8A8BF8060E391215B9B93B7395E53F6CF9F7098F61EED7B2EF7F324CB56DD7BF4BCA2626B4D423316AF099F6E48CCC068013668E89D6B814CD793201844513CB60A6ECFB3426D783C4FFF63CFAC7007708C044E07D052A65FE4A5ADC6A78F60C48E53C63223E58E5DC32722620F3DAF9FFD0F43BB86A2BD7CF4FC9F0F67F097907E74906515269C2519D282C77BAD0B4C67191C9320E807E4F89B20A83AAF218269A37F32BA3EC209E868833CF3969522D41AF3EE7163F4043B08C1C5FB270C0DC7EDDE21217CEEE35A845123EF813466C387D67C60704A6564A24A0C0607A8684771CB07E603C26B37CB94AB836880E3D1F2D2F63264E2379A02FAB78BD3BBAB535BEC2E21F876E4F88707C720FDAA833DC46F0F1FD29D22025111828DF4E51744F2A2D360B22394267BC6FC145D1BDA0B8912282428DF6E72AC8CF21F648D063BEB5F09AEF722872A9C5CFEDCACF1BC0FB82367FE92A65F8EB30299414985CAFFEE56E459DDEB0564EB4BACCD6B45AF8E059E9FEB33D61C88BC1C55D3645A5B2A8C4588C5D4931BD13696F06F0A45CFA72AA923D06DBC916A8DDC0856BA7F597DF5D3EC44E6EFBE067B7AD896E56D6326F4E275F93C8A03526B2D7E96D7ADD5DCFAF59E17FCAD5E6B98F8F91A6E363889536010AC2E008229462783748D38CF1DDE993B754C8E46403CC378E5F5CB322CDEDEAE30648484AF545BCA574F4B157CD178FD17FF1D854FFF04DA8D764497F86CB403B189C3E8812992D327CAE26D8BAC35A56E8A5E830", "32B938D50D2F513CBCF2B3087548923CAF777163B05AB4C333F23CDF1B1E64D7"), + ( + "715A321C531ED2B3CF63C018FF403FD51C41E9212C73D3BA70694D548910EE98CA382120364347779155C937C01D858D63E0C645A804B22960FDDA499452C169F48422BAC0E17743A11699CB884AD02C379E96C8E2985E79E1A85C316FA872BAFA8A1F599B36B4250ED556896D66BB16F70A9A3912CC509D8FB4B0393559CFF75702125ED006D0A0E8C6039015791A7A2CD0950EEC7326F472594B13C7CC0C536C23B9EB06D504A37F31A4920B6E7F67C2F9A36EF9BB0A2A2447E32071C8DAA0EE3A38BDA28D31F024927C364B99C2F71809420301A5F70AD4B91BA533364307194F94150E9B2B1EBCC10EF04ABFA478DB559082E41AAD8477C1A541D74A091CA3988D128FDD6568D52CC12B016DEF8C8748AA3248954BC3A79977995EA444BDAAE879FFA793F7D024B59CB5CEF600ACF79DFE1A3FF9726B0E5B0729CC91DCF6B11014B9D7281E6D848E22820B44D257BFE16E3D16796B98CB560B7A7A1312DD6A07762B343E70C677E76D01A2A36C2712E9D59480E14284FC099AB79C5A47585B151D0CAAC0E01193AD139C9C37A05BAB317DC5B83A504F7D999C347796D6A584E6229FB401733C44593894423C60A1B2B11047E11858F1838059AD56951F27F77E85303FF9A46FE5D490C9009513F2AD0B635A30EA43E6FC321F33932D4B6DF086A56AE87134820D406030A751782D68068D726A68A35B54A790C7EB38B1D73B8715409B4B79F29BB240830CA8D195FFE7098810118ED14B01184CF08537B4B178621B84FBF093252687775C1940DA9EF0A209C3D8A0EC5C174B01A6D1858BA0E86F30684093C18E0A3B135C303782C804E8E9B67E24228830816B750E5A247D76D4483444BB1A6254985A79295B431A16C5AC0AB9B7A02EBCB505B5E7AE6DFB1D722C5AA4F4CD9F131655FC00C20484A3F7CA15EAC37E1C722CC48BAD325ABED4966666CACA039D40F23F24E1A7AFC4CEE076999A2BCF6675C27CF654D08998CE64954C835109C65656F08A6062B0A6C820231A4F1F3C2828E8A2C1606FE4E2280A6A0047153BCFA20D16C501C6B9BB62780365B412CD2656285A3014821F7E7A26F7CB0B4A4B13F3F24858A0A349E385612A9B05D15F17824FF8D901C8C3BDACC3166AE785A0031CBD8B12ED942047572C8798C1995C7332AC57D8726944C5CA75244012C889C673AD5E8A9F8892B53A7A6C9CB49199812C39389A8033077F069287622F8DE0C1B647C9025A13300631DDE5A780E50E0A193B845A16B01384FF99A5781B070A962ABBC8BF794A47815AB29C26475E72760B967974E26523564A4E54C139324808B494EFD9640D4A287591AF1B414E77894BC61A09C18520971714D47AC6E736BF9B91A706A05125195FD104C74A5534C5E21C7DD66D0FFA0C13ABBAB447BD12C1B654F4BFB510896934AD050B17F490374F81B62B192C5F1653879B1D098727485C558698996C125796E3C1528860CEA9B3CD625CC9E2B7A7484C87D655861944BDF59132C20BEC705F0C79B4FE9838BF6B48ECF7CF8230C33241C57F82C1F3F31554C13607790AEDF759951A5579DA3FB1018F3F28B934158F6F36AA1516A260A0BA5F822155E18DDA6318A6832F002B43C4B274C482CDC314053C5809D8181D68311BE5BA5A9922582717CB9B751FD946589DB313C7551C26A7BA9BFA8C97D43FE8D6AC2E2B8B4AC802F0C33BED275D242673A1912EE1C5468573A1DDF68D63E05858C15A8D6C69F604CA134069B53A3F4F0765E814654612B19261198A25C15353229D4261A531CB7EA508F1D72319C119334AC9A6FC4B8398606198896EE37AFC4BC71801C16E97B016DB9799E750FCA836FC1099DE6163F3B9BFBB8864E43888C9766CD022CF203B76E937947AEA5D0FC08A5A75CBF9610B3B4B92E8E534F3B4343A4109D10631A0909239B878BEC48C2F202F41A562ECB594D74A1626ABA554D2349611314529A293D0A8F8D69C045149D21A35875CA53E0A3D84A771DA39150AC572AFEB7DBB1C4A755646587635DBB449575494628ACBE9090B50782CB3F2528D4C982DB95C664376A6245241605AAFBCC0D1D3C4AB5C5B8C54969C977DA8F95C4C93A02562AA5F5BB3E2058E56B0BCA002CA3C7A88790C5B9BC60D2B46A80D681CED7283D1A46511E55569EB5BFE06357AF020BB19484908601FA981F86842D850B79D9B2702B7527E55CD948BB7ECF069E9BA289C8717C6D3C64A3AB1FDEB88E97010E2F5089FF13291E85DD0C8ABA58BC935CB1B98D779BC00BD4D0AB894845B5AB101800336E99A99341845CF731A40337FB7D3B78FBA276914C41FE6BB1F47A9895B9B8849CDB07A63983A869C252209C858F6A267F5408533A25CEAFA908248A46A4821357801AFE6BE53F5135975B5CEC1316D46A8AB195280FBADD0855A6DCB5FB3761E22780DCB3B3134E9AFBA09A59FBC74FB41C76FE73709F02793446C8CF578FC9408F053C28EA271F304A507092ECE34823E19086143A42666632D6193CAD57E2ED370868C0E6E794E5D432CE07B9ABFD9ADBB900DC8B51B301BC2F4D89D1BDA34DF8889B2E22CB34BC4AB77778B0A8303A9C8D8B4309295548FD83555A221B1D86337468FB7A698921420FE538EA772CF6CC655B106250B0B3F7C4C3CAE404BECD46C80352D893C9F0F67306572B89AA3C9CF92A53DC941AFA27932B24A1989B8DBB8756F298CA8A93EA3CA6711471068817B9DA78DB7773E06A4CC75B3541C4287C3946D44572D43FC58F104AEB421AD3D21BD0C567368E60D9772B73402406939117C45C2AF0508A9005F42C0852D52B08850CB23C2660D5C5C5C5881374B12A75623DF343B6C336921B218147B28E244AF9E6C866133CC6C589882A29CF1CA07261562D946ABCB66734C8685B60C5A27D0480CB99BD2864085EB57CA982197066412434735988A12B5177949B71542356439483E8A7D3E54025F821DCE34C110FAC635806C36FA9690E11801B08EFCC920E7827B3F7959FC580BDE654EEB5512CD1C3CB5029BC8D0CDF201976FF5991DBC5950392999786F9E62000CB45C3C8B7EA2DA6C9F0618741BB1A1B3B9D7B83D7390B1ABEB6C74099129CA1ED1D230E0C1C676F9313AA32A3F25B9BFE07FF3B485A67424627501D2371DEF9CC349DCA5187578BA3A1B9BB9649068981FC75E21F17DE24CAF0896CA5040238E5CB76762BE3391790C7B6C4DF795E3A1850DC289E45C0B4C66931924126A6725D7E551A82B33D700234EA5ACD00AAE1F844F28173B6F31A8BE870D53D230EBF3C8F202C1D0B579CEDBA9AE61A89B94CC3034375E086694D6A59EF176156C52D7E458ABB317D0C417EAE7625DC21D5409293E35AD77AA347789B69127AC9A975B5BF74A48D82D819600E69A963FCCBC36E071CDDA374026CF8E5C3E4D97341611CAA52A5763BA5D0C9028D073788B3809CEECBEECA8172B37511826AF08BC3CAD547D5F9A8F3256B3CAC5015D5ABD9BD26CF9431C4CBC498BC015A67052911776810A5CE84041243BBE16634128FBC1395B045EEBA274B22097E2982A767353B0535A90B0A2D6CC6394B177A17C568CC2174911043A4A3C92467FD34C7A3C0E4851625E862DB9B876335684E1ABAA5D9B98E952A3CC8B6A80254B3251637E40BAA47B5C386C345E5B37BF992B6424CD4A699E2135CA96F2A03D05C5934384B3B03067565DAFF43C0D884EF8149551AA46A01A870B7B266F26072E7B8106EB399AA22E950429AF522C03F57CA7D0784CF790BABA1DE7B8B85BD24713925DED64C824E3C163C77C677B4BEB9BB25077851A76412C8608E9547CE2A93D271ACA89B0A1A205A1E6F06A8276296C704BBD8A97E96C1E101363AF2724DCB693F255AE466A3CF777C166FC7A5B211EA6E0CD5FF77D2564ACE3A3C69051951A6B243F7C7E3C8B00CFD80ABFC4A6357749C6FC38C926A268ECBB5A585F364A9938D100CB125061430554DA0468A59709143AD7F930DB772F61984A29EA9FC16C7058FB7118C0BF0FB91DEAE8CFED9B9C0665C147E37EE7E7AF0F2BCDB0C07CBCE448B15A910DC83F71678DBB794927E0A75EA4914ADC59CEB527145C37CC03199326149F5559CC54381E5786DC73C7B3302769FA4AEE488F79E06069F06C29D84AC1939070FB6B05824FF717BD5B7C5C81DB0F58F9141D32BE0DAA8D5C69A383181F2BE14400EA79B3C321C6A87FCB743D1FA15C1C625A6F2C205E47ABEED35CA818170F15417E908DF0EC88BB1B29820484C5A14892502E13F2AB4F85811B0B3512E66A6F2505C76409FF36181BA0BDDE8B3E4EDC723F073B9B244E1D86AC241752979CA9E6036A78C0A2E0D6CEF3A4726848133166BAE2EE8E8B26CF1AE15B074C62CE7CEBCFB77640E471DF9054A487F7274D7A16074F00F74E93BB83EF23A8C8396ABF557DC52BDD3304102142A46ED26C49BE675691CE797D22DAD9754A023521341437B8BB438AB6B75D8ED29258F7B045", + "E36A4E6574282A20D31E5C7901C5CF76FAB307935B10D47F28718BE945DBCC69F6DE7F904AC4C0C312014854D262044AA4258BBB4AA3276AAC69C9001509CB009C69634B9D1574351E3A1D10EF9FDF7DB56DC9387A6F4C545F29798A7714635B492FD81C678FE58C5BD1710DD91B4F841657B6CCA691F360A49304AA12DACE954613CE2333356C2D90AF8851D58BBF5CB792E3F3322944026B78EB5BD2F5BC10499A7CC07F53CB6ADB9B448046A432FC90125919FB7A71ADD744B997C66CC817EB61CD79B27DA95CF84FA6E046A2FA66CD3A91CFC27D5F9DBCE86825F5AA657D5C18C0ED3E506A0892FCF21826B7FD31F4CF5F37A198CB18AAF2F85A64BDCD859F1B353449AFC3D80CE73A7E1518983205A32ACAD694C67B292EF0FBFB2BC89D7166A3F7791EC5DB5AD2FC1466637F7922162331D645E00DD14C1E6A209C5FCBDBDEF2A9FAA97BD6C3D9EE81E6E5CF96855B64C9B601CF864F535DAA5211A58B6FEC8FDA0F38ACD98120EC178CAF41FDD4C756D665AC532D40EB7BD2CA187360AB665EF2C1AFC7AFE13E0BC3E6A64A8E92AF0582F29ECF3147A467F81AD04669C194DEDD94540CA79A6205513CD410352D4E4C0EAFFEF84F5C8836EA402325801DC3EBBC47432C8FBF34E50CC3AB3743EDEE3889B97BF39FE52D9A3094FB532FA128A3260F27A01459AE3F92E5BD3741FD9FCC9C6AAC158E352B0CAF8F444C4D935A63454617410E2CECCE7034662AF2DAA8BCC4D36F5C07F69FE5A638CB406997ACC02B686AE4878F3D036524D7C2739B002DDE011CEFC4A163A053D8327D490239AAE2A8E84EDB45A3678E2FC2BD0A5F073C232F3718D1462D421554DF3C0E83A7B7BC528C5F5678CDB59E1A09E1511F7B95744EA087E35F65CB053FF5CBAD86E37A65EEAF2FB15F5C1ECF632002A4F15637BA6F5A79526C51D2268DCDB1E7C01EF333B8C8CAB1A36D6E9291092C50D4DDF1A601498398BBF8CE06A92453394F237B4C068EA27258AA6E28A92DFF1F5518ABD343DA2345F3E288F21B87DEA760510D6646FF80894E87C153B1C522396A7D1B5A01294E8A2913E6C10A88C8081FCDE677FA050B5D8B380FCAC6E76338A4F0E05C5E10E914746575413A934D21BEA2C34AAA56DEF2E0CDA2569319D3DE6B780D9079A47173CF232B82102B6865581A9B4A6DFF4BF96DB1F2A996596DB01B70CF9A53C3515CC08CCD880739A0229F032D38102DA631A7F117F1449E8DBD345F87946E6D86AEC9C3C6B5784268CFC983C966185EAF4C7D8FA8288FED7597692008B8F962D7A26C6F7F4712008AD49A441A42D0B512564068ED455F3DE379C54221CAE6485745C435A28C274AB6DB1B7249EA148C2532FD4BD990C9C9F0BF6361791D2B0C6086A881857EB5DF4BDCC2C538CCF5BBEB9105BE762DFC965C253A7B99144BDA0D8A8BF8060E391215B9B93B7395E53F6CF9F7098F61EED7B2EF7F324CB56DD7BF4BCA2626B4D423316AF099F6E48CCC068013668E89D6B814CD793201844513CB60A6ECFB3426D783C4FFF63CFAC7007708C044E07D052A65FE4A5ADC6A78F60C48E53C63223E58E5DC32722620F3DAF9FFD0F43BB86A2BD7CF4FC9F0F67F097907E74906515269C2519D282C77BAD0B4C67191C9320E807E4F89B20A83AAF218269A37F32BA3EC209E868833CF3969522D41AF3EE7163F4043B08C1C5FB270C0DC7EDDE21217CEEE35A845123EF813466C387D67C60704A6564A24A0C0607A8684771CB07E603C26B37CB94AB836880E3D1F2D2F63264E2379A02FAB78BD3BBAB535BEC2E21F876E4F88707C720FDAA833DC46F0F1FD29D22025111828DF4E51744F2A2D360B22394267BC6FC145D1BDA0B8912282428DF6E72AC8CF21F648D063BEB5F09AEF722872A9C5CFEDCACF1BC0FB82367FE92A65F8EB30299414985CAFFEE56E459DDEB0564EB4BACCD6B45AF8E059E9FEB33D61C88BC1C55D3645A5B2A8C4588C5D4931BD13696F06F0A45CFA72AA923D06DBC916A8DDC0856BA7F597DF5D3EC44E6EFBE067B7AD896E56D6326F4E275F93C8A03526B2D7E96D7ADD5DCFAF59E17FCAD5E6B98F8F91A6E363889536010AC2E008229462783748D38CF1DDE993B754C8E46403CC378E5F5CB322CDEDEAE30648484AF545BCA574F4B157CD178FD17FF1D854FFF04DA8D764497F86CB403B189C3E8812992D327CAE26D8BAC35A56E8A5E830", + "32B938D50D2F513CBCF2B3087548923CAF777163B05AB4C333F23CDF1B1E64D7", + ), // modified ciphertext - ("56BA88526AC0BD62310EFAC21B138E6A540340271D16F832A134BCD011C475CC9B2F199A4803870DD158F8314ABCC5C4E313A2605C29AABCAB898C45E5F5A8B08A92755402726ACD20102B35E501055686B71AC4A7C02209F186A449293D28A5C1A8B6DD450F22B0BF57A6681B0696128CB40678373EC57FA7991A47B3299D4790F8F08E5C353D1CCA6D63D14B3CB610F8774B955C15AAFA7039874C9ECA88B6EA5FC604A31888CDF7859D28BA0D9C907AEC2742CFF505B9140B6AB933A3B608E6323D5CE4449C03720C63C799C383A970973989CE87135CC9D8BAE9349E4225090B6C739B630D1808860526B9F9AA0758F5CD6E63B603B68C0580C7D4F7942BC2858E40A8CE6C35C1B75979230D88177B0D3918D5C3C11D6286BF653E00634B8AC76176B79DBC002EFF37AE2FC9A4B6D5CADA9C9A1378A10A84A49D42751BEB8B4A4CC455E64FF35B2F3A024DA76906A9152D98B1902D97CBE35C576EC2A8A148C988A1C90EB18EA0DC9C7C3026324A975A5468DC56B958247C915C4255F3718584BDD5810A5B671BC8423CF62463B0B1C93E6104DDC1BAC0518576F3A7F5E4AD86345A7E11659BDC16EB002A7D1B02B7615741C5C742E2BB0C33083857BB6464072C07C771C224C008CB50656C7F12B0B259881D2101B0D98CEFB3B4CDA02BCF38044E895B48F09DC2854F2F969854463D828709E297181DE2179CB18B99959F3E001A55683E8EF75296430228A442477B7D93F504B2B48EEF0688B3729204563A1121C5831531D54A229E4AC611F77DC1806D1902388D4A92BD7352737776CBB2B0DC7B4C63C0B7C3491CA6A094E58C219999A145D53EE1E5686D73B3B248551B44CFD65689814764BC2480795B5673D11E2CE50626621B2B6C8A0EFC2BE243C8C9E871E1BA5FDCE59BE7C8B2DC1298D2193850A6AD6F856FDAE9C119D2A4FBD6A2A0A48A6AF2777A0644257256B7F02CDD75C36DB971FA4C7A69911E6BBAAEA2067F8CD22E9B198EF7B49761B224A506329C14BB14D10797A81F951AC7BD1553D4A3363FE196795B1F69B42369D4816D4467E8E2820F94CBFDEA97C3F1AFB440C66C085AFB347F71FA057AB0C49B59BE1A899EC8EB282A79CFDACA3B179B162903D060886B27B9AD4F159954564D7AF44BA842367F4CB3BAC6415DDA1A67F78E817308E6D227A4637A853BB6DED24B5E3473D86070A9CA0D23879E51251F0FF86094BC5B48058103E6388654801FBBABCB416ECBC249640B6263661E3F0539D3B01FCE39925D321ECEAB438DC936009C1D4DE52F23326B9A056348569B935604F26538027B777DBA46C1D900208739EAA289066B754B4CB463381F756C4D5DF5A87BB85EBF6A97A83B37E1261E1330AADC6C3F75A2BEBD7935578BAB2B07BD5704379A74B0B2A88E4197482B0CA32BE1043A635016924822A4CEABA54842F8445B3A5E2A78783F38968DCA18F70331C8C1317F73B36C3390C0EA6221A953A578CDB0186D9D45CBED841CCB9453E6AAA46CB019B660788568715C0A854F4005659629661094B5379338C11584B06E71373FFA6B78F87964DF474860F50AEB3544C463739CD74B4ABC20ED7B940A78721F307B08A44EE6BC2660367AAF510975B0265606BA2472C7BDEC10C4F654376B75F5E8B471BAB969A78736D9314A254D0A433B3D44B7C1E09EDC2A426A067F5C950B81F13D4285745C88253F068A8AC346D453499B525610D9201E1B3010874983D37151F299C36B3C6FB02B6EF687B01B9CEB55AEB38C0F6068975FB71143BC3B86C641FC72BC74F67BED967F75113A85204E146A920F1894DA283C81E42147560306AAAE89694ED37B3AC5A1B56FD8B0FCC859D9AB4578E35D36812BC368984B126B3CDBC4D9195445218E07B74BFB07A9887754C4A03F39B58F07344456C51E832578CF152A2AC9C363F775ADA8277965C758F7759DCA303DD310135144DD8CB640F4396E2404AF242440101FD6EA817C43638E74874767B8BF75B0853AB1D434886D2A015661880D3C459E6A293701747F65A1B87333C53AC580B29112C45383EB66970C0AC3A6C2AC5C4B83EB9ABA29A7DCC0BCF8577F2C4C244B024914A79B761B3378FA0DEA260153F837E340688C39B4E0540DF8A5BBD270B3646460F66C4AAC13521B20AB192B75E8F4AB16199547DBAD46C29EE2AA9C3BD21155A1CDE1185A6FA4CEC65871C9082008FCC2D6462E9688A08F21A91C70C2388587263A215992771BC37002C4344BB77A3193C148769624C971525A50CE681C7D4734321A4834CA85A4503704F0BE80F342308C7C34099D0F095521F877E6C01BD5317B555CCED34138DBC45CD9B34695EB3AC78969164274F821AAD04C38739A6CB6D23473802655FC1FB32856698CC93C6B7BA9A59C8F7BB4FC1CA9805B60E8780C89338190963BFEE01E489CC5D7C110156374194B3989D7C49E829C7C402C9D449792923CE810A3D7FAA1A157BDC2F7BDC6963175F2308F5A1972FBCA31C44D69B400306B254D3478012680F3191DA9C6515376C2AE0AA73AF90301ACA5CFB37A1238C7672C5678A01991C773136A113A49A847CAC37BAB8B05AB60B6A66EF5345EFA3486C97BC1E11A51A476BAE4122E0253380F39CDF0F458A6005D64498F6F7CB4475001FA7805F8D66FDBB245689705CF172D7AF4543B573C256BAE89F33391B3600E86B42BFC04366C2F33CC5951F99ABCA4851CD3C84C660D09EAB5ED577C407626BCA69426530FBC61589085811DD066E118B8B2EAC79787184F21C64F182D82DB587AA4CC6A76529C57482F283EC7707F615C7395B89BCBF641565ABE32533F888325401B69EFE5219BDC43A471C18D8526D6151FCEF501B8AA6B200C632B6A93FA308BFF637749459C514949B113C3EC76C140255F923932DC2C2C6067C7E25C244C81B4D0663A6788A9DA105B6119B44AD118079631425B639F9033471186E233C4C9B823F2F370831B50C61CBF66912C4A11B63A34703111A3A1C198ED274CEA03885E7A7F261893835B8812A7B146D80F8DD13D98EB6C75FC9B99E3A63039A7F7A7655042BBCAD324528AB4B9668FE59BC42C193DE8237AE9D8B3C438BDB6812914F17DEEB64C3946608178A2254132275BBD0841692DC4519408A1DF1564772942E04B9FF0AC279680AD4D46145532638FC0648F4065DA663C14D6788D9C1E6189460FA676CD766A2DF42C7408356732331D915D55E147C4D5C54E470516B91A8B965DA6481E3A9B4DA2E5C577B61FFD0C8B6A88928B174654449270C0CF6D92653370290D985F42F1BF2BD633D0613BA4B35C4B85AAA9DB2784C5B7519222A3D55EB9C63F5E630CDD7011B0092ABFD1BA5ECB17FB0132E165732CD6C519D0680F6B9F717330DF16C55EF6CEB7111EFE0763119C0C117A42872B700FEA551D86A555C3BEADA9157D24095400160CD0901C209D6CF64CF0940FB4E27C80659155D25288F91843389689005FF582796E627042D320D148C1779C825ABB114F07C9C0765A24901152C5312846C5B1BB2B0C995AC5B9BF354C1E60459FDD8C0CA99068B635CAB0B87784A528ADA543D17CA75A92A16169819A4B8F335468B2F22B64C33C89597FA5279891F78A5BC633D4642FD877881D4826D5BC14AD6819C19157C5D374F2F112DA276A2EB8A3344B13DD6372B0747AB2A09A17764C591B1589DC05F797822B0727703B1708E0799609B609E169F2D80B6871CB149B447DE702A354B1E68607C3739B67B1C7BE6A8905040816E182FE765446516C25A61CF6F041FC702EC443B3F029CF96464DCD2A6A7038AFBA15857DFA0DA3955A8300694ED2167019B7925BB9B1E8C9EC155C41DA44EE22516D766B88B06BD246352294A865339E796B01EE64AF0B58C2D574225FD1CDDCA36639BC877FD1CE2232482F8AB2F15097B720A553237C30129E0235904383735AB838235745606933D9665573109288E2B28B6627C6DA673B9711BD338D36D2BB4EE28068C6B980B543EAD3241DB93F0DE70E331A9BF5528A5952623F8B41B079BBE1CC551E686934ABB3D4279136F30980A753B4976EA4C1A0C26A309DD08F115C03FB8A5F77E08828D02927973A55960C8F048128A868276C08F308A70A97C137295657654EC7F715B560BB6FDB3A886A17994165F11122D5C77B437C186765C292BC5341B9A7B82A3640F84EBF1A3309B8846A0690401AA8660509F870AF5F038D1B735E82E76DFA702E4621C0FD17567A9194B142379681AE50C8456A829BA04C86F93C524D3C2323394853BA2CB1697797C00BCA68740E4738D9D7ABD0B7A372FB7DDEF05DE800C0EF1B5C5C0C6CB6C4BE063C172165264A8E589512243A985811CFBE3681B283F38E936AFA5BE5CCF6556FD19377DBBEF56EC1F9C8164BA18E0B551E143B18D953AB8D8E28A714B83ABCCE6578A603830622C782A328ECE04BF9F81956854B87723D9CD989F7D5986622A0CC4E", "571642393727EC2180916934F8934B29A6F53D717DB0A5C193A965C306DEF344C123558E6B6A213970E1BE2CBD2A59C7E028F036E1F1F475D21AF822D597C8BC3BE7E51AB6071C6287B0F4F4BC7DE80BBFB57868D89539EC30DCB22AA6950563C6439190D134A1A5C3EE74E189F097554E9A06987F4D9CDEDCEC264FA68081AB9B3755F8FB6C09974E8B4188E1DDEFACB973CCDD39C3B9D83490A125AE2A86BC2F433900893E8EFEA8AECB0B76C12A1D163223014414FA01E4FA4DC2334B98DF9F2BDF5BF28CE94B3FA40E116214B34F4479AF2326EF5FBDCE45688C94374583A98BA741D726D30522FD7FE20CA114A3E07AEFD27BFA25AA07F4C8824BDB819A1986D2D569E69421656A49DBA6649CD3C091E5AFAAE4E0F0B4CDA4C9DE0C53097068796403301844978F36B6F4DFEE07DA2F9BAB283C8E0ED3F4F81F3D7B0D52ED18DA9CC46310EA506284681CCF4AD9AC89AB78C2DE4FF41CB26CD44A9580568B177D46C360CF7AA4C37287CCFE5F5C4B623B800EC57C86986C7AC11FE305F46AA987F06E1B0C2E8302B0A395D13FDD6C5AE9F9C188D3EC94963CFB024343954CE0D066FF9683D2325788903F741AF1E8F5B30EC909AFF0C31C5F0B1194086D2D31275C90E398EB05BF6F0833B49F23AC93537E3E23385923BEFDB7EC484094CA482FE6B2F8E415406A0F9C99FF0FEBBBD3642D21F9A356768605B9D8DCF7B92C7011477043ED041F299D291B65891E34BD7A8C9D7CDA1DFBB39D3FD02E02A34AF3E935E611A353C1F32C2A39270ABD25E6B935039E15A599CADDE031CA8D44EE3698620ED27180D096E4FE7C284E07F1E90A3E37AB3DC4604E84880EDB44232CA26EBD8BE554694C73EB7CC0D61F5DD1A03367E25862302113629E1AB3265D5F8F372E87E71A38E9AE5360F3EB32C836A3CFE896634731AF3FF020B82A5F90081A84A3E11F473E61D2F6C55DCFE304547E3C67DC66C2118DB4AD4B3752A95C8241D20F9234F4255D5A2B30DDBA1D7FA019F37E4D3439287E18074E0209AAF00EE83A3D962A5F35DBD51746016FA0C6946FA5A3E1AE4B58869CCA0E6075867B39CA52F1AEE845A509CE4B06CDD74F44263EED2C9F5EC4866C3A3AA1172C9CF1451F70EF50555F0978F8B2F840FDA2966ABBEFF7233F3291169A69742B899BF8985C65169ED782E581E9540A79C9355521C5C50AF563850B4374BC835A1080CDF00C37209C1A486FE3AF7DB911DB536D195CD4FDEBD1A27A0A06210D804D938A4249DA55DFD1DFDDD9C2CA9A086E3F0DC74910CFBC59AB3CE28A4CFD17FABDE76FFC99AF5320A977EF20CD779AF82CA76385C98702B186D58F664DB5867B1F9525679D4EA7DA3F50D6CE73D2AC2E552CB3BDB6FA9E841774022E93EF4FF594C12FC18F8DD79EF4EDF498FD351BFD22287D9C62DBDC1F1DCF9972D584D757DA5B690F1F3CCC9345E9AD6BDDD2E7924F127F94E5FD56CB84998059BF458218FAE4E50C09165E407823C6E12085F425247A87DB26DE6C1EFEFB52B529D014C6DF1A8FBA5B3183EC2640E23380DCDEA413E622F2EDA9E9FC34378FEF5DE3575C3AAA63D4F3BB524221E355E62EA93E8C8C63225C3FACDB9B3EA280432A274D188CD5FF462D4AEA0B5D56177EA8A9D12C147A32C54911729A50DFC2D15F2C1841592BF64D2AED354DD50ED000AF092EC2C1A185D267087382DE2409679EC6A21645CD1ED615F84DE2FACEF49594A61AE53A0C81E04659A2458B1195E7E8C1DF202746B9D470DF490E7A515541E495CC15D4545C4920CC612E7E6804D0863313CE37846086A55496033EBCA5494B99A30D1878646432911B7FD823450F26FBFB8FE1FE6A5417CD823858CF14C8B29D6C4D5BBB9B1BB1BD9C141EA9B6C8BECCF8502840D412010D47A58589054B965345E37B60F8D9A42B833A1AD6BCA62DF3D3A06376C3DF61AFB4D5AF87A3EA57E429043BB1B1907AADCF075EEAC6A672795FF2E642D26D253EFBC6B41C469A906673230F7CA41311E602F2D7CC9F6F156EC4305EEAEDE9FD1DB895180DC0EF50BFB1C0338B72B11FA82A0BF313497FDAC38DCDE0E375AB6DAF2DC62DB3EB63A59D9B7E6115F60A582D9A7DF0FAF5E362FB34D2B64D63C07C6E606B47520462004AD2857121B374F044FB8A31C945822A61BCF9985A155F850DEDA2FE8B890C9325A6E1DC5EC8B79B88564ED603564890083B27F290", "933D0A130BAFD7E072A625D971F77FA464E5FFBAB9E6D7D7E4FDA08FF685095C"), + ( + "56BA88526AC0BD62310EFAC21B138E6A540340271D16F832A134BCD011C475CC9B2F199A4803870DD158F8314ABCC5C4E313A2605C29AABCAB898C45E5F5A8B08A92755402726ACD20102B35E501055686B71AC4A7C02209F186A449293D28A5C1A8B6DD450F22B0BF57A6681B0696128CB40678373EC57FA7991A47B3299D4790F8F08E5C353D1CCA6D63D14B3CB610F8774B955C15AAFA7039874C9ECA88B6EA5FC604A31888CDF7859D28BA0D9C907AEC2742CFF505B9140B6AB933A3B608E6323D5CE4449C03720C63C799C383A970973989CE87135CC9D8BAE9349E4225090B6C739B630D1808860526B9F9AA0758F5CD6E63B603B68C0580C7D4F7942BC2858E40A8CE6C35C1B75979230D88177B0D3918D5C3C11D6286BF653E00634B8AC76176B79DBC002EFF37AE2FC9A4B6D5CADA9C9A1378A10A84A49D42751BEB8B4A4CC455E64FF35B2F3A024DA76906A9152D98B1902D97CBE35C576EC2A8A148C988A1C90EB18EA0DC9C7C3026324A975A5468DC56B958247C915C4255F3718584BDD5810A5B671BC8423CF62463B0B1C93E6104DDC1BAC0518576F3A7F5E4AD86345A7E11659BDC16EB002A7D1B02B7615741C5C742E2BB0C33083857BB6464072C07C771C224C008CB50656C7F12B0B259881D2101B0D98CEFB3B4CDA02BCF38044E895B48F09DC2854F2F969854463D828709E297181DE2179CB18B99959F3E001A55683E8EF75296430228A442477B7D93F504B2B48EEF0688B3729204563A1121C5831531D54A229E4AC611F77DC1806D1902388D4A92BD7352737776CBB2B0DC7B4C63C0B7C3491CA6A094E58C219999A145D53EE1E5686D73B3B248551B44CFD65689814764BC2480795B5673D11E2CE50626621B2B6C8A0EFC2BE243C8C9E871E1BA5FDCE59BE7C8B2DC1298D2193850A6AD6F856FDAE9C119D2A4FBD6A2A0A48A6AF2777A0644257256B7F02CDD75C36DB971FA4C7A69911E6BBAAEA2067F8CD22E9B198EF7B49761B224A506329C14BB14D10797A81F951AC7BD1553D4A3363FE196795B1F69B42369D4816D4467E8E2820F94CBFDEA97C3F1AFB440C66C085AFB347F71FA057AB0C49B59BE1A899EC8EB282A79CFDACA3B179B162903D060886B27B9AD4F159954564D7AF44BA842367F4CB3BAC6415DDA1A67F78E817308E6D227A4637A853BB6DED24B5E3473D86070A9CA0D23879E51251F0FF86094BC5B48058103E6388654801FBBABCB416ECBC249640B6263661E3F0539D3B01FCE39925D321ECEAB438DC936009C1D4DE52F23326B9A056348569B935604F26538027B777DBA46C1D900208739EAA289066B754B4CB463381F756C4D5DF5A87BB85EBF6A97A83B37E1261E1330AADC6C3F75A2BEBD7935578BAB2B07BD5704379A74B0B2A88E4197482B0CA32BE1043A635016924822A4CEABA54842F8445B3A5E2A78783F38968DCA18F70331C8C1317F73B36C3390C0EA6221A953A578CDB0186D9D45CBED841CCB9453E6AAA46CB019B660788568715C0A854F4005659629661094B5379338C11584B06E71373FFA6B78F87964DF474860F50AEB3544C463739CD74B4ABC20ED7B940A78721F307B08A44EE6BC2660367AAF510975B0265606BA2472C7BDEC10C4F654376B75F5E8B471BAB969A78736D9314A254D0A433B3D44B7C1E09EDC2A426A067F5C950B81F13D4285745C88253F068A8AC346D453499B525610D9201E1B3010874983D37151F299C36B3C6FB02B6EF687B01B9CEB55AEB38C0F6068975FB71143BC3B86C641FC72BC74F67BED967F75113A85204E146A920F1894DA283C81E42147560306AAAE89694ED37B3AC5A1B56FD8B0FCC859D9AB4578E35D36812BC368984B126B3CDBC4D9195445218E07B74BFB07A9887754C4A03F39B58F07344456C51E832578CF152A2AC9C363F775ADA8277965C758F7759DCA303DD310135144DD8CB640F4396E2404AF242440101FD6EA817C43638E74874767B8BF75B0853AB1D434886D2A015661880D3C459E6A293701747F65A1B87333C53AC580B29112C45383EB66970C0AC3A6C2AC5C4B83EB9ABA29A7DCC0BCF8577F2C4C244B024914A79B761B3378FA0DEA260153F837E340688C39B4E0540DF8A5BBD270B3646460F66C4AAC13521B20AB192B75E8F4AB16199547DBAD46C29EE2AA9C3BD21155A1CDE1185A6FA4CEC65871C9082008FCC2D6462E9688A08F21A91C70C2388587263A215992771BC37002C4344BB77A3193C148769624C971525A50CE681C7D4734321A4834CA85A4503704F0BE80F342308C7C34099D0F095521F877E6C01BD5317B555CCED34138DBC45CD9B34695EB3AC78969164274F821AAD04C38739A6CB6D23473802655FC1FB32856698CC93C6B7BA9A59C8F7BB4FC1CA9805B60E8780C89338190963BFEE01E489CC5D7C110156374194B3989D7C49E829C7C402C9D449792923CE810A3D7FAA1A157BDC2F7BDC6963175F2308F5A1972FBCA31C44D69B400306B254D3478012680F3191DA9C6515376C2AE0AA73AF90301ACA5CFB37A1238C7672C5678A01991C773136A113A49A847CAC37BAB8B05AB60B6A66EF5345EFA3486C97BC1E11A51A476BAE4122E0253380F39CDF0F458A6005D64498F6F7CB4475001FA7805F8D66FDBB245689705CF172D7AF4543B573C256BAE89F33391B3600E86B42BFC04366C2F33CC5951F99ABCA4851CD3C84C660D09EAB5ED577C407626BCA69426530FBC61589085811DD066E118B8B2EAC79787184F21C64F182D82DB587AA4CC6A76529C57482F283EC7707F615C7395B89BCBF641565ABE32533F888325401B69EFE5219BDC43A471C18D8526D6151FCEF501B8AA6B200C632B6A93FA308BFF637749459C514949B113C3EC76C140255F923932DC2C2C6067C7E25C244C81B4D0663A6788A9DA105B6119B44AD118079631425B639F9033471186E233C4C9B823F2F370831B50C61CBF66912C4A11B63A34703111A3A1C198ED274CEA03885E7A7F261893835B8812A7B146D80F8DD13D98EB6C75FC9B99E3A63039A7F7A7655042BBCAD324528AB4B9668FE59BC42C193DE8237AE9D8B3C438BDB6812914F17DEEB64C3946608178A2254132275BBD0841692DC4519408A1DF1564772942E04B9FF0AC279680AD4D46145532638FC0648F4065DA663C14D6788D9C1E6189460FA676CD766A2DF42C7408356732331D915D55E147C4D5C54E470516B91A8B965DA6481E3A9B4DA2E5C577B61FFD0C8B6A88928B174654449270C0CF6D92653370290D985F42F1BF2BD633D0613BA4B35C4B85AAA9DB2784C5B7519222A3D55EB9C63F5E630CDD7011B0092ABFD1BA5ECB17FB0132E165732CD6C519D0680F6B9F717330DF16C55EF6CEB7111EFE0763119C0C117A42872B700FEA551D86A555C3BEADA9157D24095400160CD0901C209D6CF64CF0940FB4E27C80659155D25288F91843389689005FF582796E627042D320D148C1779C825ABB114F07C9C0765A24901152C5312846C5B1BB2B0C995AC5B9BF354C1E60459FDD8C0CA99068B635CAB0B87784A528ADA543D17CA75A92A16169819A4B8F335468B2F22B64C33C89597FA5279891F78A5BC633D4642FD877881D4826D5BC14AD6819C19157C5D374F2F112DA276A2EB8A3344B13DD6372B0747AB2A09A17764C591B1589DC05F797822B0727703B1708E0799609B609E169F2D80B6871CB149B447DE702A354B1E68607C3739B67B1C7BE6A8905040816E182FE765446516C25A61CF6F041FC702EC443B3F029CF96464DCD2A6A7038AFBA15857DFA0DA3955A8300694ED2167019B7925BB9B1E8C9EC155C41DA44EE22516D766B88B06BD246352294A865339E796B01EE64AF0B58C2D574225FD1CDDCA36639BC877FD1CE2232482F8AB2F15097B720A553237C30129E0235904383735AB838235745606933D9665573109288E2B28B6627C6DA673B9711BD338D36D2BB4EE28068C6B980B543EAD3241DB93F0DE70E331A9BF5528A5952623F8B41B079BBE1CC551E686934ABB3D4279136F30980A753B4976EA4C1A0C26A309DD08F115C03FB8A5F77E08828D02927973A55960C8F048128A868276C08F308A70A97C137295657654EC7F715B560BB6FDB3A886A17994165F11122D5C77B437C186765C292BC5341B9A7B82A3640F84EBF1A3309B8846A0690401AA8660509F870AF5F038D1B735E82E76DFA702E4621C0FD17567A9194B142379681AE50C8456A829BA04C86F93C524D3C2323394853BA2CB1697797C00BCA68740E4738D9D7ABD0B7A372FB7DDEF05DE800C0EF1B5C5C0C6CB6C4BE063C172165264A8E589512243A985811CFBE3681B283F38E936AFA5BE5CCF6556FD19377DBBEF56EC1F9C8164BA18E0B551E143B18D953AB8D8E28A714B83ABCCE6578A603830622C782A328ECE04BF9F81956854B87723D9CD989F7D5986622A0CC4E", + "571642393727EC2180916934F8934B29A6F53D717DB0A5C193A965C306DEF344C123558E6B6A213970E1BE2CBD2A59C7E028F036E1F1F475D21AF822D597C8BC3BE7E51AB6071C6287B0F4F4BC7DE80BBFB57868D89539EC30DCB22AA6950563C6439190D134A1A5C3EE74E189F097554E9A06987F4D9CDEDCEC264FA68081AB9B3755F8FB6C09974E8B4188E1DDEFACB973CCDD39C3B9D83490A125AE2A86BC2F433900893E8EFEA8AECB0B76C12A1D163223014414FA01E4FA4DC2334B98DF9F2BDF5BF28CE94B3FA40E116214B34F4479AF2326EF5FBDCE45688C94374583A98BA741D726D30522FD7FE20CA114A3E07AEFD27BFA25AA07F4C8824BDB819A1986D2D569E69421656A49DBA6649CD3C091E5AFAAE4E0F0B4CDA4C9DE0C53097068796403301844978F36B6F4DFEE07DA2F9BAB283C8E0ED3F4F81F3D7B0D52ED18DA9CC46310EA506284681CCF4AD9AC89AB78C2DE4FF41CB26CD44A9580568B177D46C360CF7AA4C37287CCFE5F5C4B623B800EC57C86986C7AC11FE305F46AA987F06E1B0C2E8302B0A395D13FDD6C5AE9F9C188D3EC94963CFB024343954CE0D066FF9683D2325788903F741AF1E8F5B30EC909AFF0C31C5F0B1194086D2D31275C90E398EB05BF6F0833B49F23AC93537E3E23385923BEFDB7EC484094CA482FE6B2F8E415406A0F9C99FF0FEBBBD3642D21F9A356768605B9D8DCF7B92C7011477043ED041F299D291B65891E34BD7A8C9D7CDA1DFBB39D3FD02E02A34AF3E935E611A353C1F32C2A39270ABD25E6B935039E15A599CADDE031CA8D44EE3698620ED27180D096E4FE7C284E07F1E90A3E37AB3DC4604E84880EDB44232CA26EBD8BE554694C73EB7CC0D61F5DD1A03367E25862302113629E1AB3265D5F8F372E87E71A38E9AE5360F3EB32C836A3CFE896634731AF3FF020B82A5F90081A84A3E11F473E61D2F6C55DCFE304547E3C67DC66C2118DB4AD4B3752A95C8241D20F9234F4255D5A2B30DDBA1D7FA019F37E4D3439287E18074E0209AAF00EE83A3D962A5F35DBD51746016FA0C6946FA5A3E1AE4B58869CCA0E6075867B39CA52F1AEE845A509CE4B06CDD74F44263EED2C9F5EC4866C3A3AA1172C9CF1451F70EF50555F0978F8B2F840FDA2966ABBEFF7233F3291169A69742B899BF8985C65169ED782E581E9540A79C9355521C5C50AF563850B4374BC835A1080CDF00C37209C1A486FE3AF7DB911DB536D195CD4FDEBD1A27A0A06210D804D938A4249DA55DFD1DFDDD9C2CA9A086E3F0DC74910CFBC59AB3CE28A4CFD17FABDE76FFC99AF5320A977EF20CD779AF82CA76385C98702B186D58F664DB5867B1F9525679D4EA7DA3F50D6CE73D2AC2E552CB3BDB6FA9E841774022E93EF4FF594C12FC18F8DD79EF4EDF498FD351BFD22287D9C62DBDC1F1DCF9972D584D757DA5B690F1F3CCC9345E9AD6BDDD2E7924F127F94E5FD56CB84998059BF458218FAE4E50C09165E407823C6E12085F425247A87DB26DE6C1EFEFB52B529D014C6DF1A8FBA5B3183EC2640E23380DCDEA413E622F2EDA9E9FC34378FEF5DE3575C3AAA63D4F3BB524221E355E62EA93E8C8C63225C3FACDB9B3EA280432A274D188CD5FF462D4AEA0B5D56177EA8A9D12C147A32C54911729A50DFC2D15F2C1841592BF64D2AED354DD50ED000AF092EC2C1A185D267087382DE2409679EC6A21645CD1ED615F84DE2FACEF49594A61AE53A0C81E04659A2458B1195E7E8C1DF202746B9D470DF490E7A515541E495CC15D4545C4920CC612E7E6804D0863313CE37846086A55496033EBCA5494B99A30D1878646432911B7FD823450F26FBFB8FE1FE6A5417CD823858CF14C8B29D6C4D5BBB9B1BB1BD9C141EA9B6C8BECCF8502840D412010D47A58589054B965345E37B60F8D9A42B833A1AD6BCA62DF3D3A06376C3DF61AFB4D5AF87A3EA57E429043BB1B1907AADCF075EEAC6A672795FF2E642D26D253EFBC6B41C469A906673230F7CA41311E602F2D7CC9F6F156EC4305EEAEDE9FD1DB895180DC0EF50BFB1C0338B72B11FA82A0BF313497FDAC38DCDE0E375AB6DAF2DC62DB3EB63A59D9B7E6115F60A582D9A7DF0FAF5E362FB34D2B64D63C07C6E606B47520462004AD2857121B374F044FB8A31C945822A61BCF9985A155F850DEDA2FE8B890C9325A6E1DC5EC8B79B88564ED603564890083B27F290", + "933D0A130BAFD7E072A625D971F77FA464E5FFBAB9E6D7D7E4FDA08FF685095C", + ), ]; for (dk, c, k) in cases { let dk: [u8; 3168] = unhex(dk).try_into().unwrap(); @@ -275,7 +551,10 @@ fn kat_mlkem1024_rejects_malformed_ek() { ek[1] |= 0x0F; let m = [0u8; 32]; assert!( - matches!(mlkem1024_encaps(&ek, &m), Err(Error::InvalidEncapsulationKey)), + matches!( + mlkem1024_encaps(&ek, &m), + Err(Error::InvalidEncapsulationKey) + ), "비정규 ek 미거부" ); } diff --git a/x25519/src/lib.rs b/x25519/src/lib.rs index 8344b32..6a72126 100644 --- a/x25519/src/lib.rs +++ b/x25519/src/lib.rs @@ -63,10 +63,7 @@ impl SecretKey { PublicKey(public_bytes) } - pub fn diffie_hellman( - &self, - their_public: &PublicKey, - ) -> Result { + pub fn diffie_hellman(&self, their_public: &PublicKey) -> Result { let mut shared = x25519(self.0.expose(), &their_public.0); let result = SharedSecret(Secret::new(shared)); shared.zeroize(); @@ -304,7 +301,11 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(alice_secret.diffie_hellman(&bob_pk).expect("정상 공개키는 Ok 여야 함")); + storage.write( + alice_secret + .diffie_hellman(&bob_pk) + .expect("정상 공개키는 Ok 여야 함"), + ); let ptr = storage.assume_init_ref().as_bytes().as_ptr(); let pre = core::slice::from_raw_parts(ptr, 32); From ec0d9481efd70fa283dc1a315a88297e6427da4f Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:37:26 +0900 Subject: [PATCH 33/65] =?UTF-8?q?chacha20=20=EA=B2=80=EC=A6=9D=20=EC=9E=94?= =?UTF-8?q?=EC=97=AC=20=EB=B3=B4=EA=B0=95=20-=20=EC=B9=B4=EC=9A=B4?= =?UTF-8?q?=ED=84=B0=20=EC=86=8C=EC=A7=84=20=EB=B0=8F=20=ED=82=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B0=80=EB=93=9C=20=EB=B6=80=EC=A0=95=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20Poly1305?= =?UTF-8?q?=20=EB=B9=88=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20gitignore=20=EC=97=90=20DS=5FStore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + chacha20/src/chacha20.rs | 26 ++++++++++++++++++++++++++ chacha20/src/poly1305.rs | 13 +++++++++++++ 3 files changed, 40 insertions(+) diff --git a/.gitignore b/.gitignore index 247abf8..2069b64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ target/ .idea/ dist/ +.DS_Store *.iso Cargo.lock CLAUDE.md diff --git a/chacha20/src/chacha20.rs b/chacha20/src/chacha20.rs index fd55567..75c5e5e 100644 --- a/chacha20/src/chacha20.rs +++ b/chacha20/src/chacha20.rs @@ -201,6 +201,32 @@ mod tests { } } + /// 카운터 소진 후 keystream_block 재호출은 키스트림 재사용이므로 어보트해야 함. + #[test] + #[should_panic(expected = "카운터 소진")] + fn test_counter_exhaustion_keystream_block() { + let mut chacha = ChaCha20::new_with_counter(&[0u8; 32], &[0u8; 12], u32::MAX); + let _ = chacha.keystream_block(); + let _ = chacha.keystream_block(); + } + + /// 잔여 카운터 용량을 초과하는 apply_keystream 은 데이터 처리 전에 어보트해야 함. + #[test] + #[should_panic(expected = "카운터 소진")] + fn test_counter_exhaustion_apply_keystream() { + let mut chacha = ChaCha20::new_with_counter(&[0u8; 32], &[0u8; 12], u32::MAX); + let mut data = [0u8; 128]; + chacha.apply_keystream(&mut data); + } + + /// 카운터가 0 이 아닌 상태에서 Poly1305 키 생성은 소진 추적 우회이므로 거부되어야 함. + #[test] + #[should_panic(expected = "블록 카운터 0")] + fn test_poly1305_keygen_requires_counter_zero() { + let mut chacha = ChaCha20::new_with_counter(&[0u8; 32], &[0u8; 12], 1); + let _ = chacha.generate_poly1305_key(); + } + #[test] fn test_chacha20_block_rfc8439_vector() { let key: [u8; 32] = [ diff --git a/chacha20/src/poly1305.rs b/chacha20/src/poly1305.rs index 5394dca..53c42cd 100644 --- a/chacha20/src/poly1305.rs +++ b/chacha20/src/poly1305.rs @@ -385,6 +385,19 @@ mod tests { assert_eq!(tag, expected_tag); } + /// 빈 메시지의 태그는 r 와 무관하게 s 그 자체 (h=0 이므로 tag = s mod 2^128). + #[test] + fn test_poly1305_empty_message() { + let mut key = [0xA5u8; 32]; + key[16..].copy_from_slice(&[ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, 0x10, + ]); + let poly = Poly1305::new(&key); + let tag = poly.finalize(); + assert_eq!(&tag[..], &key[16..], "빈 메시지 태그가 s 와 다름"); + } + #[test] fn test_poly1305_verify_constant_time() { let tag1: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; From d26a7b627fc9d58915f3bb454795b1e8a951f592 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:37:36 +0900 Subject: [PATCH 34/65] =?UTF-8?q?constant-time=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=95=98=EB=84=A4=EC=8A=A4=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=ED=99=94=20-=20dudect=20=EB=AE=A4=ED=85=8D=EC=8A=A4=20?= =?UTF-8?q?=EC=A7=81=EB=A0=AC=ED=99=94=20=EB=B0=8F=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=ED=8F=89=EA=B7=A0=20=EC=B8=A1=EC=A0=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=91=EB=A0=AC=20=EA=B0=84=EC=84=AD=EA=B3=BC=20=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=84=B0=20=EC=96=91=EC=9E=90=ED=99=94=20=EC=9E=A1?= =?UTF-8?q?=EC=9D=8C=20=EC=A0=9C=EA=B1=B0=20-=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=9E=85=EB=A0=A5=20black=5Fbox=20=EA=B2=A9?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EC=BB=B4=ED=8C=8C=EC=9D=BC=EB=9F=AC=20?= =?UTF-8?q?=EA=B0=92=20=EB=B3=91=ED=95=A9=20=EC=95=84=ED=8B=B0=ED=8C=A9?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=9C=EA=B1=B0=20-=20=ED=95=9C=EA=B3=84=20?= =?UTF-8?q?=EC=B4=88=EA=B3=BC=20=EC=8B=9C=20=EB=8F=85=EB=A6=BD=20=EC=9E=AC?= =?UTF-8?q?=EC=B8=A1=EC=A0=95=203=ED=9A=8C=20=EC=9E=AC=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=20=EB=8F=84=EC=9E=85=20=EC=9E=84=EA=B3=84=204.5=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20-=20hsm=5Fslot=5Fzeroize=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=B3=84=20=EC=A0=84=EC=9A=A9=20=EC=B9=B4=EC=9A=B4?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=ED=94=BD=EC=8A=A4=EC=B2=98=20=EA=B2=BD?= =?UTF-8?q?=EC=9F=81=20=EC=A0=9C=EA=B1=B0=20-=20cargo=20config=20=ED=83=80?= =?UTF-8?q?=EA=B2=9F=EC=9D=84=20=EC=8B=A4=EC=A0=9C=20=ED=98=B8=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20aarch64-apple-darwin=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- constant-time/.cargo/config.toml | 2 +- constant-time/tests/dudect.rs | 756 +++++++++++++----------- constant-time/tests/hsm_slot_zeroize.rs | 29 +- 3 files changed, 438 insertions(+), 349 deletions(-) diff --git a/constant-time/.cargo/config.toml b/constant-time/.cargo/config.toml index 47e23cc..3e67504 100644 --- a/constant-time/.cargo/config.toml +++ b/constant-time/.cargo/config.toml @@ -3,4 +3,4 @@ # 테스트 바이너리는 표준 하네스(std)가 필요하므로 호스트 타깃 사용 # 워크스페이스 루트의 target = "x86_64-unknown-none" 설정을 이 크레이트에서 재정의함 # 의존성으로 빌드될 때는 상위 크레이트의 타깃을 따르기 떄문에 커널 빌드에 영향 없음 -target = "x86_64-apple-darwin" +target = "aarch64-apple-darwin" diff --git a/constant-time/tests/dudect.rs b/constant-time/tests/dudect.rs index a14c3fc..300839e 100644 --- a/constant-time/tests/dudect.rs +++ b/constant-time/tests/dudect.rs @@ -68,6 +68,23 @@ mod tests { count } + // + // Test serialisation + // + // The default parallel test harness runs the dudect tests concurrently + // inside one process; contending measurement loops inflate variance and + // produce false FAIL verdicts under load. A file-local mutex serialises + // every measurement section (poison-tolerant so one FAIL does not + // cascade into the remaining tests). + + static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + fn serial() -> std::sync::MutexGuard<'static, ()> { + SERIAL + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] #[inline(always)] fn ticks() -> u64 { @@ -150,10 +167,19 @@ mod tests { /// Iterations discarded to warm instruction caches and branch predictors. const WARMUP: usize = 50_000; /// Timing samples collected per test (split evenly: half per class). - /// Increased from 300k to 1M for better detection of subtle timing leaks. - const MEASUREMENTS: usize = 1_000_000; + const MEASUREMENTS: usize = 200_000; + /// Calls averaged per sample. Single ~ns primitives sit far below the + /// aarch64 CNTVCT_EL0 granularity (24 MHz ≈ 42 ns/tick); averaging a + /// batch lifts the signal above counter quantisation noise while the + /// Welch t-test on batch means stays a valid leak detector. + const BATCH: usize = 32; /// Standard dudect threshold. |t| ≥ 4.5 → timing leak at ~6σ confidence. const T_THRESHOLD: f64 = 4.5; + /// Consecutive re-measurements required before declaring a leak. A + /// single Welch t excursion above 4.5 recurs by chance across 19 tests + /// and repeated runs (scheduler/DVFS noise is autocorrelated); a genuine + /// leak stays above threshold on every independent re-measurement. + const DUDECT_RETRIES: usize = 3; fn report(label: &str, s0: &Stats, s1: &Stats) -> bool { let t = welch_t(s0, s1).abs(); @@ -176,14 +202,16 @@ mod tests { // // Test helpers // - // Measure `f(v)` once, return elapsed ticks. + // Measure `f(v)` BATCH times, return mean elapsed ticks per call. // The argument and return value go through black_box to prevent DCE/hoisting. macro_rules! measure { ($f:expr) => {{ let t0 = ticks(); - let _ = black_box($f); + for _ in 0..BATCH { + let _ = black_box($f); + } let t1 = ticks(); - t1.saturating_sub(t0) as f64 + t1.saturating_sub(t0) as f64 / BATCH as f64 }}; } @@ -200,22 +228,24 @@ mod tests { // and |t| will exceed the threshold. #[test] fn dudect_choice_from_u8() { + let _serial = serial(); let mut s = 0xdead_beef_cafe_0001_u64; - let mut stat = [Stats::default(), Stats::default()]; + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - black_box(Choice::from_u8(black_box(rnd_u8(&mut s)))); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let v = if cl == 0 { rnd_u8(&mut s) } else { 0u8 }; - stat[cl].push(measure!(Choice::from_u8(black_box(v)))); + for _ in 0..WARMUP { + black_box(Choice::from_u8(black_box(rnd_u8(&mut s)))); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let v = if cl == 0 { rnd_u8(&mut s) } else { 0u8 }; + stat[cl].push(measure!(Choice::from_u8(black_box(v)))); + } + if report("Choice::from_u8 (random vs fixed=0)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "Choice::from_u8 (random vs fixed=0)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // @@ -228,52 +258,56 @@ mod tests { // information about which path was taken leaks through the value itself. #[test] fn dudect_select_u32() { + let _serial = serial(); let mut s = 0x1234_5678_abcd_0002_u64; - let mut stat = [Stats::default(), Stats::default()]; + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let (a, b) = (rnd_u32(&mut s), rnd_u32(&mut s)); - black_box(u32::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(rnd_u32(&mut s)); - let b = black_box(rnd_u32(&mut s)); - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!(u32::select(&a, &b, c))); + for _ in 0..WARMUP { + let (a, b) = (rnd_u32(&mut s), rnd_u32(&mut s)); + black_box(u32::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(rnd_u32(&mut s)); + let b = black_box(rnd_u32(&mut s)); + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!(u32::select(&a, &b, c))); + } + if report("u32::select (choice=0 vs choice=1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "u32::select (choice=0 vs choice=1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // u64::select — same as above but for 64-bit values. #[test] fn dudect_select_u64() { + let _serial = serial(); let mut s = 0xfeed_face_dead_0003_u64; - let mut stat = [Stats::default(), Stats::default()]; - - for _ in 0..WARMUP { - black_box(u64::select( - &rnd_u64(&mut s), - &rnd_u64(&mut s), - Choice::from_u8(rnd_u8(&mut s) & 1), - )); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(rnd_u64(&mut s)); - let b = black_box(rnd_u64(&mut s)); - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!(u64::select(&a, &b, c))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + + for _ in 0..WARMUP { + black_box(u64::select( + &rnd_u64(&mut s), + &rnd_u64(&mut s), + Choice::from_u8(rnd_u8(&mut s) & 1), + )); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(rnd_u64(&mut s)); + let b = black_box(rnd_u64(&mut s)); + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!(u64::select(&a, &b, c))); + } + if report("u64::select (choice=0 vs choice=1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "u64::select (choice=0 vs choice=1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // @@ -283,47 +317,51 @@ mod tests { // Class 1: a != b (b = a ^ 1; guaranteed different; result is Choice(0)). #[test] fn dudect_eq_i32() { + let _serial = serial(); let mut s = 0xaaaa_bbbb_0000_0004_u64; - let mut stat = [Stats::default(), Stats::default()]; + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let a = rnd_i32(&mut s); - black_box(CtEqOps::eq(&a, &a)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(rnd_i32(&mut s)); - let b = if cl == 0 { a } else { a ^ 1 }; - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + for _ in 0..WARMUP { + let a = rnd_i32(&mut s); + black_box(CtEqOps::eq(&a, &a)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(rnd_i32(&mut s)); + let b = black_box(if cl == 0 { a } else { a ^ 1 }); + stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + } + if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "CtEqOps::eq (a==a vs a!=a^1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // CtEqOps::eq — 64-bit equality. #[test] fn dudect_eq_u64() { + let _serial = serial(); let mut s = 0x1111_2222_3333_0005_u64; - let mut stat = [Stats::default(), Stats::default()]; + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let a = rnd_u64(&mut s); - black_box(CtEqOps::eq(&a, &a)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(rnd_u64(&mut s)); - let b = if cl == 0 { a } else { a ^ 1 }; - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + for _ in 0..WARMUP { + let a = rnd_u64(&mut s); + black_box(CtEqOps::eq(&a, &a)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(rnd_u64(&mut s)); + let b = black_box(if cl == 0 { a } else { a ^ 1 }); + stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + } + if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "CtEqOps::eq (a==a vs a!=a^1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // @@ -336,30 +374,32 @@ mod tests { // similar across classes (avoiding Hamming-weight correlation artifacts). #[test] fn dudect_gt_u64() { + let _serial = serial(); let mut s = 0x9999_8888_7777_0006_u64; - let mut stat = [Stats::default(), Stats::default()]; - const MSB: u64 = 1u64 << 63; - - for _ in 0..WARMUP { - let raw = rnd_u64(&mut s); - let (a, b) = (raw | MSB, raw & !MSB); - black_box(CtGreeter::gt(&a, &b)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let raw = black_box(rnd_u64(&mut s)); - let (a, b) = if cl == 0 { - (raw | MSB, raw & !MSB) // a > b (unsigned) - } else { - (raw & !MSB, raw | MSB) // a < b - }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + const MSB: u64 = 1u64 << 63; + + for _ in 0..WARMUP { + let raw = rnd_u64(&mut s); + let (a, b) = (raw | MSB, raw & !MSB); + black_box(CtGreeter::gt(&a, &b)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let raw = black_box(rnd_u64(&mut s)); + let (a, b) = if cl == 0 { + (raw | MSB, raw & !MSB) // a > b (unsigned) + } else { + (raw & !MSB, raw | MSB) // a < b + }; + stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + } + if report("CtGreeter::gt (a>b vs a (a>b vs a= T_THRESHOLD"); } // @@ -372,30 +412,32 @@ mod tests { // Since non-negative > negative for signed comparison, ordering is guaranteed. #[test] fn dudect_gt_i64() { + let _serial = serial(); let mut s = 0x5555_4444_3333_0007_u64; - let mut stat = [Stats::default(), Stats::default()]; - const SIGN: u64 = 1u64 << 63; - - for _ in 0..WARMUP { - let raw = rnd_u64(&mut s); - let (a, b) = ((raw & !SIGN) as i64, (raw | SIGN) as i64); - black_box(CtGreeter::gt(&a, &b)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let raw = black_box(rnd_u64(&mut s)); - let (a, b) = if cl == 0 { - ((raw & !SIGN) as i64, (raw | SIGN) as i64) // a ≥ 0 > b (signed) - } else { - ((raw | SIGN) as i64, (raw & !SIGN) as i64) // a < 0 ≤ b (signed) - }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + const SIGN: u64 = 1u64 << 63; + + for _ in 0..WARMUP { + let raw = rnd_u64(&mut s); + let (a, b) = ((raw & !SIGN) as i64, (raw | SIGN) as i64); + black_box(CtGreeter::gt(&a, &b)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let raw = black_box(rnd_u64(&mut s)); + let (a, b) = if cl == 0 { + ((raw & !SIGN) as i64, (raw | SIGN) as i64) // a ≥ 0 > b (signed) + } else { + ((raw | SIGN) as i64, (raw & !SIGN) as i64) // a < 0 ≤ b (signed) + }; + stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + } + if report("CtGreeter::gt (a>b vs a (a>b vs a= T_THRESHOLD"); } // @@ -407,29 +449,31 @@ mod tests { // The low 64-bit word is randomised in both classes. #[test] fn dudect_gt_u128() { + let _serial = serial(); let mut s = 0xcafe_babe_f00d_0008_u64; - let mut stat = [Stats::default(), Stats::default()]; + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let lo = rnd_u64(&mut s) as u128; - let (a, b) = ((u64::MAX as u128) << 64 | lo, lo); - black_box(CtGreeter::gt(&a, &b)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let lo = black_box(rnd_u64(&mut s)) as u128; - let (a, b) = if cl == 0 { - ((u64::MAX as u128) << 64 | lo, lo) // a > b - } else { - (lo, (u64::MAX as u128) << 64 | lo) // a < b - }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + for _ in 0..WARMUP { + let lo = rnd_u64(&mut s) as u128; + let (a, b) = ((u64::MAX as u128) << 64 | lo, lo); + black_box(CtGreeter::gt(&a, &b)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let lo = black_box(rnd_u64(&mut s)) as u128; + let (a, b) = if cl == 0 { + ((u64::MAX as u128) << 64 | lo, lo) // a > b + } else { + (lo, (u64::MAX as u128) << 64 | lo) // a < b + }; + stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + } + if report("CtGreeter::gt (a>b vs a (a>b vs a= T_THRESHOLD"); } // @@ -439,25 +483,31 @@ mod tests { // Class 1: a > b. #[test] fn dudect_lt_u32() { + let _serial = serial(); let mut s = 0x3333_2222_1111_0009_u64; - let mut stat = [Stats::default(), Stats::default()]; - const MSB: u32 = 1u32 << 31; + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + const MSB: u32 = 1u32 << 31; - for _ in 0..WARMUP { - let raw = rnd_u32(&mut s); - black_box(CtLess::lt(&(raw & !MSB), &(raw | MSB))); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let raw = black_box(rnd_u32(&mut s)); - let (a, b) = if cl == 0 { - (raw & !MSB, raw | MSB) // a < b - } else { - (raw | MSB, raw & !MSB) // a > b - }; - stat[cl].push(measure!(CtLess::lt(&a, &b))); + for _ in 0..WARMUP { + let raw = rnd_u32(&mut s); + black_box(CtLess::lt(&(raw & !MSB), &(raw | MSB))); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let raw = black_box(rnd_u32(&mut s)); + let (a, b) = if cl == 0 { + (raw & !MSB, raw | MSB) // a < b + } else { + (raw | MSB, raw & !MSB) // a > b + }; + stat[cl].push(measure!(CtLess::lt(&a, &b))); + } + if report("CtLess::lt (ab)", &stat[0], &stat[1]) { + return; + } } - assert!(report("CtLess::lt (ab)", &stat[0], &stat[1])); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // @@ -469,111 +519,121 @@ mod tests { #[test] fn dudect_select_u8() { + let _serial = serial(); let mut s = 0xa1a1_b2b2_c3c3_0010_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let (a, b) = (rnd_u8(&mut s), rnd_u8(&mut s)); - black_box(u8::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(rnd_u8(&mut s)); - let b = black_box(rnd_u8(&mut s)); - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!(u8::select(&a, &b, c))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let (a, b) = (rnd_u8(&mut s), rnd_u8(&mut s)); + black_box(u8::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(rnd_u8(&mut s)); + let b = black_box(rnd_u8(&mut s)); + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!(u8::select(&a, &b, c))); + } + if report("u8::select (choice=0 vs choice=1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "u8::select (choice=0 vs choice=1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } #[test] fn dudect_select_u16() { + let _serial = serial(); let mut s = 0xb2b2_c3c3_d4d4_0011_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let (a, b) = (rnd_u32(&mut s) as u16, rnd_u32(&mut s) as u16); - black_box(u16::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(rnd_u32(&mut s) as u16); - let b = black_box(rnd_u32(&mut s) as u16); - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!(u16::select(&a, &b, c))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let (a, b) = (rnd_u32(&mut s) as u16, rnd_u32(&mut s) as u16); + black_box(u16::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(rnd_u32(&mut s) as u16); + let b = black_box(rnd_u32(&mut s) as u16); + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!(u16::select(&a, &b, c))); + } + if report("u16::select (choice=0 vs choice=1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "u16::select (choice=0 vs choice=1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } #[test] fn dudect_select_u128() { + let _serial = serial(); let mut s = 0xc3c3_d4d4_e5e5_0012_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let a = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; - let b = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; - black_box(u128::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); - let b = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!(u128::select(&a, &b, c))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let a = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; + let b = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; + black_box(u128::select(&a, &b, Choice::from_u8(rnd_u8(&mut s) & 1))); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); + let b = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!(u128::select(&a, &b, c))); + } + if report("u128::select (choice=0 vs choice=1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "u128::select (choice=0 vs choice=1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } #[test] fn dudect_eq_u32() { + let _serial = serial(); let mut s = 0xd4d4_e5e5_f6f6_0013_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let a = rnd_u32(&mut s); - black_box(CtEqOps::eq(&a, &a)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(rnd_u32(&mut s)); - let b = if cl == 0 { a } else { a ^ 1 }; - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let a = rnd_u32(&mut s); + black_box(CtEqOps::eq(&a, &a)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(rnd_u32(&mut s)); + let b = black_box(if cl == 0 { a } else { a ^ 1 }); + stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + } + if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "CtEqOps::eq (a==a vs a!=a^1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } #[test] fn dudect_eq_u128() { + let _serial = serial(); let mut s = 0xe5e5_f6f6_0707_0014_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let a = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; - black_box(CtEqOps::eq(&a, &a)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let a = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); - let b = if cl == 0 { a } else { a ^ 1 }; - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let a = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; + black_box(CtEqOps::eq(&a, &a)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let a = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); + let b = black_box(if cl == 0 { a } else { a ^ 1 }); + stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + } + if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { + return; + } } - assert!(report( - "CtEqOps::eq (a==a vs a!=a^1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // @@ -583,29 +643,31 @@ mod tests { // Class 1: a < 0, b >= 0 → a < b (signed) #[test] fn dudect_gt_i128() { + let _serial = serial(); let mut s = 0xf6f6_0707_1818_0015_u64; - let mut stat = [Stats::default(), Stats::default()]; - const SIGN128: u128 = 1u128 << 127; - for _ in 0..WARMUP { - let raw = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; - let (a, b) = ((raw & !SIGN128) as i128, (raw | SIGN128) as i128); - black_box(CtGreeter::gt(&a, &b)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let raw = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); - let (a, b) = if cl == 0 { - ((raw & !SIGN128) as i128, (raw | SIGN128) as i128) - } else { - ((raw | SIGN128) as i128, (raw & !SIGN128) as i128) - }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + const SIGN128: u128 = 1u128 << 127; + for _ in 0..WARMUP { + let raw = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; + let (a, b) = ((raw & !SIGN128) as i128, (raw | SIGN128) as i128); + black_box(CtGreeter::gt(&a, &b)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let raw = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); + let (a, b) = if cl == 0 { + ((raw & !SIGN128) as i128, (raw | SIGN128) as i128) + } else { + ((raw | SIGN128) as i128, (raw & !SIGN128) as i128) + }; + stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + } + if report("CtGreeter::gt (a>b vs a (a>b vs a= T_THRESHOLD"); } // @@ -617,58 +679,70 @@ mod tests { // 실행되므로 항상 동일한 cycle 패턴이어야 함. #[test] fn dudect_swap_u64() { + let _serial = serial(); let mut s = 0x0707_1818_2929_0016_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let mut a = rnd_u64(&mut s); - let mut b = rnd_u64(&mut s); - u64::swap(&mut a, &mut b, Choice::from_u8(rnd_u8(&mut s) & 1)); - black_box(&mut a); - black_box(&mut b); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let mut a = black_box(rnd_u64(&mut s)); - let mut b = black_box(rnd_u64(&mut s)); - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!({ - u64::swap(&mut a, &mut b, c); - (a, b) - })); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let mut a = rnd_u64(&mut s); + let mut b = rnd_u64(&mut s); + u64::swap(&mut a, &mut b, Choice::from_u8(rnd_u8(&mut s) & 1)); + black_box(&mut a); + black_box(&mut b); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let mut a = black_box(rnd_u64(&mut s)); + let mut b = black_box(rnd_u64(&mut s)); + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!({ + u64::swap(&mut a, &mut b, c); + (a, b) + })); + } + if report( + "CtSelOps::swap (choice=0 vs choice=1)", + &stat[0], + &stat[1], + ) { + return; + } } - assert!(report( - "CtSelOps::swap (choice=0 vs choice=1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } #[test] fn dudect_swap_u128() { + let _serial = serial(); let mut s = 0x1818_2929_3a3a_0017_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let mut a = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; - let mut b = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; - u128::swap(&mut a, &mut b, Choice::from_u8(rnd_u8(&mut s) & 1)); - black_box(&mut a); - black_box(&mut b); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let mut a = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); - let mut b = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!({ - u128::swap(&mut a, &mut b, c); - (a, b) - })); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let mut a = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; + let mut b = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; + u128::swap(&mut a, &mut b, Choice::from_u8(rnd_u8(&mut s) & 1)); + black_box(&mut a); + black_box(&mut b); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let mut a = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); + let mut b = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!({ + u128::swap(&mut a, &mut b, c); + (a, b) + })); + } + if report( + "CtSelOps::swap (choice=0 vs choice=1)", + &stat[0], + &stat[1], + ) { + return; + } } - assert!(report( - "CtSelOps::swap (choice=0 vs choice=1)", - &stat[0], - &stat[1] - )); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } // @@ -676,40 +750,52 @@ mod tests { // 다른 경로를 만들지 않는지 회귀 가드. #[test] fn dudect_choice_not() { + let _serial = serial(); let mut s = 0x2929_3a3a_4b4b_0018_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let c = Choice::from_u8(rnd_u8(&mut s) & 1); - black_box(!c); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - let c = Choice::from_u8(cl as u8); - stat[cl].push(measure!(!black_box(c))); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let c = Choice::from_u8(rnd_u8(&mut s) & 1); + black_box(!c); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + let c = Choice::from_u8(cl as u8); + stat[cl].push(measure!(!black_box(c))); + } + if report("Choice::not (c=0 vs c=1)", &stat[0], &stat[1]) { + return; + } } - assert!(report("Choice::not (c=0 vs c=1)", &stat[0], &stat[1])); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } #[test] fn dudect_choice_bitops() { + let _serial = serial(); let mut s = 0x3a3a_4b4b_5c5c_0019_u64; - let mut stat = [Stats::default(), Stats::default()]; - for _ in 0..WARMUP { - let x = Choice::from_u8(rnd_u8(&mut s) & 1); - let y = Choice::from_u8(rnd_u8(&mut s) & 1); - black_box((x & y) | (x ^ y)); - } - for i in 0..MEASUREMENTS { - let cl = i & 1; - // 두 클래스 모두 동일 산술량을 수행. 입력만 다름. - let x = Choice::from_u8(((i >> 1) & 1) as u8); - let y = Choice::from_u8(cl as u8); - stat[cl].push(measure!({ - let x = black_box(x); - let y = black_box(y); - (x & y) | (x ^ y) - })); + for _attempt in 0..DUDECT_RETRIES { + let mut stat = [Stats::default(), Stats::default()]; + for _ in 0..WARMUP { + let x = Choice::from_u8(rnd_u8(&mut s) & 1); + let y = Choice::from_u8(rnd_u8(&mut s) & 1); + black_box((x & y) | (x ^ y)); + } + for i in 0..MEASUREMENTS { + let cl = i & 1; + // 두 클래스 모두 동일 산술량을 수행. 입력만 다름. + let x = Choice::from_u8(((i >> 1) & 1) as u8); + let y = Choice::from_u8(cl as u8); + stat[cl].push(measure!({ + let x = black_box(x); + let y = black_box(y); + (x & y) | (x ^ y) + })); + } + if report("Choice::&|^ (mixed inputs)", &stat[0], &stat[1]) { + return; + } } - assert!(report("Choice::&|^ (mixed inputs)", &stat[0], &stat[1])); + panic!("dudect FAIL: DUDECT_RETRIES 회 연속 |t| >= T_THRESHOLD"); } } diff --git a/constant-time/tests/hsm_slot_zeroize.rs b/constant-time/tests/hsm_slot_zeroize.rs index 9d4521a..1e50fa1 100644 --- a/constant-time/tests/hsm_slot_zeroize.rs +++ b/constant-time/tests/hsm_slot_zeroize.rs @@ -5,7 +5,8 @@ mod tests { // 카운팅 fixture — 스택 메모리 재사용으로 raw-pointer 읽기는 UB이므로(RESEARCH §10.6 L1114-1116) // Drop 도달 여부를 관찰 가능한 AtomicUsize 카운터로 잡는다 (Pitfall 4 회피). - static ZEROIZE_COUNT: AtomicUsize = AtomicUsize::new(0); + // 카운터는 테스트별 전용 static 을 슬롯에 주입한다 — 전역 카운터 하나를 공유하면 + // 병렬 테스트 하네스에서 다른 테스트의 store(0) 리셋이 Drop 증가분을 지우는 경쟁이 생긴다. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[repr(u8)] @@ -19,14 +20,16 @@ mod tests { state: HsmSlotState, token: u64, rights: u16, + zeroize_count: &'static AtomicUsize, } impl TestSlot { - fn new_attached(token: u64, rights: u16) -> Self { + fn new_attached(token: u64, rights: u16, zeroize_count: &'static AtomicUsize) -> Self { Self { state: HsmSlotState::Attached, token, rights, + zeroize_count, } } @@ -34,7 +37,7 @@ mod tests { self.token = 0; self.rights = 0; self.state = HsmSlotState::Empty; - ZEROIZE_COUNT.fetch_add(1, Ordering::SeqCst); + self.zeroize_count.fetch_add(1, Ordering::SeqCst); } } @@ -46,15 +49,15 @@ mod tests { self.token = 0; self.rights = 0; self.state = HsmSlotState::Empty; - ZEROIZE_COUNT.fetch_add(1, Ordering::SeqCst); + self.zeroize_count.fetch_add(1, Ordering::SeqCst); } } #[test] fn slot_zeroize_clears_token_and_state() { - ZEROIZE_COUNT.store(0, Ordering::SeqCst); + static COUNT: AtomicUsize = AtomicUsize::new(0); - let mut slot = TestSlot::new_attached(0xDEAD_BEEF_CAFE_BABE, 0x07); + let mut slot = TestSlot::new_attached(0xDEAD_BEEF_CAFE_BABE, 0x07, &COUNT); slot.zeroize_inline(); // 명시 zeroize 직후 fields 가 모두 0 / Empty 인지 확인 (Pitfall 4: black_box 로 DCE 차단). @@ -62,32 +65,32 @@ mod tests { assert_eq!(black_box(slot.rights), 0); assert_eq!(slot.state, HsmSlotState::Empty); // slot 은 본 스코프 종료 시 drop -> 카운터 한 번 더 증가하지만 본 검사는 호출-시점만 본다. - assert!(ZEROIZE_COUNT.load(Ordering::SeqCst) >= 1); + assert!(COUNT.load(Ordering::SeqCst) >= 1); } #[test] fn drop_zeroizes_via_safety_net_counter() { - ZEROIZE_COUNT.store(0, Ordering::SeqCst); + static COUNT: AtomicUsize = AtomicUsize::new(0); { - let slot = TestSlot::new_attached(0x1234_5678_9ABC_DEF0, 0x05); + let slot = TestSlot::new_attached(0x1234_5678_9ABC_DEF0, 0x05, &COUNT); // black_box 로 컴파일러가 slot 을 즉시 dead-code 로 판단해 Drop 을 생략하지 못하도록 잡는다. black_box(&slot); // 스코프 종료 -> Drop -> 카운터 증가. } // Drop 만으로 관측: 명시 zeroize 호출 없이도 안전망이 동작했음. assert!( - ZEROIZE_COUNT.load(Ordering::SeqCst) >= 1, + COUNT.load(Ordering::SeqCst) >= 1, "Drop 안전망이 발화되지 않음 — Zeroize 보장 회귀" ); } #[test] fn early_return_zeroize_path() { - ZEROIZE_COUNT.store(0, Ordering::SeqCst); + static COUNT: AtomicUsize = AtomicUsize::new(0); // 시나리오: detach 진입 후 in-flight 정리 중 early return 경로에서도 // zeroize 가 발생해야 함(D-14: 모든 종료 경로 소거 보장). - let mut slot = TestSlot::new_attached(0xFEED_FACE_F00D_BABE, 0x07); + let mut slot = TestSlot::new_attached(0xFEED_FACE_F00D_BABE, 0x07, &COUNT); // Attached -> Detaching 전이 slot.state = HsmSlotState::Detaching; @@ -98,6 +101,6 @@ mod tests { assert_eq!(slot.state, HsmSlotState::Empty); assert_eq!(black_box(slot.token), 0); assert_eq!(black_box(slot.rights), 0); - assert!(ZEROIZE_COUNT.load(Ordering::SeqCst) >= 1); + assert!(COUNT.load(Ordering::SeqCst) >= 1); } } From a1738bd8fa742c4d3ee59de7d106467ef7562b6c Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:17:30 +0900 Subject: [PATCH 35/65] =?UTF-8?q?aes=20=ED=81=AC=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EC=95=88=20=EA=B0=90=EC=82=AC=20=ED=9B=84?= =?UTF-8?q?=EC=86=8D=20=EC=88=98=EC=A0=95=20-=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20assert=20=EB=A5=BC=20Error=20=EC=97=B4?= =?UTF-8?q?=EA=B1=B0=ED=98=95=20=EC=9D=B4=EB=A5=B8=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20-=20GCM=20h=20?= =?UTF-8?q?=EC=99=80=20GHash=20=EB=82=B4=EB=B6=80=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A5=BC=20Secret=20=EC=9C=BC=EB=A1=9C=20=EC=8A=B9=EA=B2=A9=20?= =?UTF-8?q?-=20gf=5Fmul=20xtime=20=EC=9D=84=20constant-time=20select=20?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98=20-=20AAD=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=20GCM=20KAT=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/src/block.rs | 13 ++-- aes/src/cbc.rs | 80 +++++++++++++++++------- aes/src/ctr.rs | 86 ++++++++++++++++++-------- aes/src/gcm.rs | 158 +++++++++++++++++++++++++++++++++-------------- aes/src/ghash.rs | 29 ++++----- aes/src/lib.rs | 11 +++- 6 files changed, 254 insertions(+), 123 deletions(-) diff --git a/aes/src/block.rs b/aes/src/block.rs index 5a87b24..e4ecb11 100644 --- a/aes/src/block.rs +++ b/aes/src/block.rs @@ -1,6 +1,7 @@ #![allow(clippy::needless_range_loop)] use crate::sbox::{inv_sub_bytes_block, sub_bytes_block}; +use constant_time::{Choice, CtSelOps}; use zeroize::Zeroize; const NB: usize = 4; @@ -113,18 +114,18 @@ fn inv_shift_rows(state: &mut State) { #[inline] fn xtime(x: u8) -> u8 { - let hi = (x >> 7) & 1; - let shifted = x << 1; - shifted ^ (hi * 0x1b) + let hi = Choice::from_u8((x >> 7) & 1); + (x << 1) ^ u8::select(&0x00, &0x1b, hi) } #[inline] fn gf_mul(mut a: u8, mut b: u8) -> u8 { let mut p = 0u8; for _ in 0..8 { - p ^= a & ((b & 1).wrapping_neg()); - let hi = (a >> 7) & 1; - a = (a << 1) ^ (hi * 0x1b); + let bit = Choice::from_u8(b & 1); + p ^= u8::select(&0x00, &a, bit); + let hi = Choice::from_u8((a >> 7) & 1); + a = (a << 1) ^ u8::select(&0x00, &0x1b, hi); b >>= 1; } p diff --git a/aes/src/cbc.rs b/aes/src/cbc.rs index f76a25d..93e4e84 100644 --- a/aes/src/cbc.rs +++ b/aes/src/cbc.rs @@ -1,4 +1,4 @@ -use crate::AES256; +use crate::{AES256, Error}; use zeroize::Zeroize; pub const CBC_IV_SIZE: usize = 16; @@ -15,15 +15,18 @@ impl AES256CBC { } } - pub fn encrypt(&self, iv: &[u8; CBC_IV_SIZE], plaintext: &[u8], ciphertext: &mut [u8]) { - assert!( - plaintext.len().is_multiple_of(16), - "평문 길이가 블록 크기의 배수가 아님" - ); - assert!( - ciphertext.len() >= plaintext.len(), - "암호문 버퍼가 평문보다 작아 무음 절단 발생" - ); + pub fn encrypt( + &self, + iv: &[u8; CBC_IV_SIZE], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<(), Error> { + if !plaintext.len().is_multiple_of(16) { + return Err(Error::InvalidLength); + } + if ciphertext.len() < plaintext.len() { + return Err(Error::BufferTooSmall); + } let mut prev = *iv; @@ -40,17 +43,22 @@ impl AES256CBC { prev = encrypted; block.zeroize(); } + + Ok(()) } - pub fn decrypt(&self, iv: &[u8; CBC_IV_SIZE], ciphertext: &[u8], plaintext: &mut [u8]) { - assert!( - ciphertext.len().is_multiple_of(16), - "암호문 길이가 블록 크기의 배수가 아님" - ); - assert!( - plaintext.len() >= ciphertext.len(), - "평문 버퍼가 암호문보다 작아 무음 절단 발생" - ); + pub fn decrypt( + &self, + iv: &[u8; CBC_IV_SIZE], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result<(), Error> { + if !ciphertext.len().is_multiple_of(16) { + return Err(Error::InvalidLength); + } + if plaintext.len() < ciphertext.len() { + return Err(Error::BufferTooSmall); + } let mut prev = *iv; @@ -68,6 +76,8 @@ impl AES256CBC { decrypted.zeroize(); block.zeroize(); } + + Ok(()) } } @@ -103,11 +113,11 @@ mod tests { let cbc = AES256CBC::new(&key); let mut ciphertext = [0u8; 64]; - cbc.encrypt(&iv, &plaintext, &mut ciphertext); + cbc.encrypt(&iv, &plaintext, &mut ciphertext).unwrap(); assert_eq!(ciphertext, expected_ciphertext); let mut decrypted = [0u8; 64]; - cbc.decrypt(&iv, &ciphertext, &mut decrypted); + cbc.decrypt(&iv, &ciphertext, &mut decrypted).unwrap(); assert_eq!(decrypted, plaintext); } @@ -130,10 +140,34 @@ mod tests { let cbc = AES256CBC::new(&key); let mut ciphertext = [0u8; 32]; - cbc.encrypt(&iv, &plaintext, &mut ciphertext); + cbc.encrypt(&iv, &plaintext, &mut ciphertext).unwrap(); let mut decrypted = [0u8; 32]; - cbc.decrypt(&iv, &ciphertext, &mut decrypted); + cbc.decrypt(&iv, &ciphertext, &mut decrypted).unwrap(); assert_eq!(decrypted, plaintext); } + + #[test] + fn cbc_invalid_length() { + let key = [0x11u8; 32]; + let iv = [0x22u8; 16]; + let plaintext = [0x33u8; 17]; + + let cbc = AES256CBC::new(&key); + let mut ciphertext = [0u8; 32]; + let result = cbc.encrypt(&iv, &plaintext, &mut ciphertext); + assert_eq!(result, Err(Error::InvalidLength)); + } + + #[test] + fn cbc_buffer_too_small() { + let key = [0x11u8; 32]; + let iv = [0x22u8; 16]; + let plaintext = [0x33u8; 32]; + + let cbc = AES256CBC::new(&key); + let mut ciphertext = [0u8; 16]; + let result = cbc.encrypt(&iv, &plaintext, &mut ciphertext); + assert_eq!(result, Err(Error::BufferTooSmall)); + } } diff --git a/aes/src/ctr.rs b/aes/src/ctr.rs index de6bbdd..b3af584 100644 --- a/aes/src/ctr.rs +++ b/aes/src/ctr.rs @@ -1,4 +1,4 @@ -use crate::AES256; +use crate::{AES256, Error}; use zeroize::Zeroize; pub const CTR_NONCE_SIZE: usize = 12; @@ -49,44 +49,64 @@ impl AES256CTR { } } - pub fn apply_iv(&self, iv: &[u8; CTR_IV_SIZE], input: &[u8], output: &mut [u8]) { - assert!( - output.len() >= input.len(), - "출력 버퍼가 입력보다 작아 무음 절단 발생" - ); - assert!( - input.len() as u64 <= CTR_MAX_INPUT_LEN, - "입력 길이 한계(2^32 블록) 초과로 카운터 재사용 발생" - ); + pub fn apply_iv( + &self, + iv: &[u8; CTR_IV_SIZE], + input: &[u8], + output: &mut [u8], + ) -> Result<(), Error> { + if output.len() < input.len() { + return Err(Error::BufferTooSmall); + } + if input.len() as u64 > CTR_MAX_INPUT_LEN { + return Err(Error::InputTooLong); + } let mut counter = *iv; self.apply_internal(&mut counter, input, output); counter.zeroize(); + + Ok(()) } - pub fn apply(&self, nonce: &[u8; CTR_NONCE_SIZE], input: &[u8], output: &mut [u8]) { - assert!( - output.len() >= input.len(), - "출력 버퍼가 입력보다 작아 무음 절단 발생" - ); - assert!( - input.len() as u64 <= CTR_MAX_INPUT_LEN, - "입력 길이 한계(2^32 블록) 초과로 카운터 재사용 발생" - ); + pub fn apply( + &self, + nonce: &[u8; CTR_NONCE_SIZE], + input: &[u8], + output: &mut [u8], + ) -> Result<(), Error> { + if output.len() < input.len() { + return Err(Error::BufferTooSmall); + } + if input.len() as u64 > CTR_MAX_INPUT_LEN { + return Err(Error::InputTooLong); + } let mut counter = [0u8; 16]; counter[..12].copy_from_slice(nonce); counter[12..16].copy_from_slice(&1u32.to_be_bytes()); self.apply_internal(&mut counter, input, output); counter.zeroize(); + + Ok(()) } #[inline] - pub fn encrypt(&self, nonce: &[u8; CTR_NONCE_SIZE], plaintext: &[u8], ciphertext: &mut [u8]) { - self.apply(nonce, plaintext, ciphertext); + pub fn encrypt( + &self, + nonce: &[u8; CTR_NONCE_SIZE], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<(), Error> { + self.apply(nonce, plaintext, ciphertext) } #[inline] - pub fn decrypt(&self, nonce: &[u8; CTR_NONCE_SIZE], ciphertext: &[u8], plaintext: &mut [u8]) { - self.apply(nonce, ciphertext, plaintext); + pub fn decrypt( + &self, + nonce: &[u8; CTR_NONCE_SIZE], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result<(), Error> { + self.apply(nonce, ciphertext, plaintext) } } @@ -122,11 +142,11 @@ mod tests { let ctr = AES256CTR::new(&key); let mut ciphertext = [0u8; 64]; - ctr.apply_iv(&iv, &plaintext, &mut ciphertext); + ctr.apply_iv(&iv, &plaintext, &mut ciphertext).unwrap(); assert_eq!(ciphertext, expected_ciphertext); let mut decrypted = [0u8; 64]; - ctr.apply_iv(&iv, &ciphertext, &mut decrypted); + ctr.apply_iv(&iv, &ciphertext, &mut decrypted).unwrap(); assert_eq!(decrypted, plaintext); } @@ -138,10 +158,22 @@ mod tests { let ctr = AES256CTR::new(&key); let mut ciphertext = [0u8; 20]; - ctr.encrypt(&nonce, &plaintext, &mut ciphertext); + ctr.encrypt(&nonce, &plaintext, &mut ciphertext).unwrap(); let mut decrypted = [0u8; 20]; - ctr.decrypt(&nonce, &ciphertext, &mut decrypted); + ctr.decrypt(&nonce, &ciphertext, &mut decrypted).unwrap(); assert_eq!(decrypted, plaintext); } + + #[test] + fn ctr_buffer_too_small() { + let key = [0x11u8; 32]; + let nonce = [0x22u8; 12]; + let input = [0x33u8; 32]; + + let ctr = AES256CTR::new(&key); + let mut output = [0u8; 16]; + let result = ctr.apply(&nonce, &input, &mut output); + assert_eq!(result, Err(Error::BufferTooSmall)); + } } diff --git a/aes/src/gcm.rs b/aes/src/gcm.rs index 24fbe35..9ce7d7f 100644 --- a/aes/src/gcm.rs +++ b/aes/src/gcm.rs @@ -1,7 +1,7 @@ -use crate::AES256; use crate::ghash::GHash; +use crate::{AES256, Error}; use constant_time::{Choice, CtEqOps}; -use zeroize::Zeroize; +use zeroize::{Secret, Zeroize}; pub const GCM_TAG_SIZE: usize = 16; pub const GCM_NONCE_SIZE: usize = 12; @@ -20,14 +20,14 @@ fn inc32(block: &mut [u8; 16]) { pub struct AES256GCM { cipher: AES256, - h: [u8; 16], + h: Secret<[u8; 16]>, } impl AES256GCM { #[must_use] pub fn new(key: &[u8; 32]) -> Self { let cipher = AES256::new(key); - let h = cipher.encrypt(&[0u8; 16]); + let h = Secret::new(cipher.encrypt(&[0u8; 16])); Self { cipher, h } } @@ -63,7 +63,7 @@ impl AES256GCM { } fn compute_tag(&self, aad: &[u8], ciphertext: &[u8], j0: &[u8; 16]) -> [u8; 16] { - let mut ghash = GHash::new(&self.h); + let mut ghash = GHash::new(self.h.expose()); ghash.update_padded(aad); ghash.update_padded(ciphertext); @@ -100,19 +100,16 @@ impl AES256GCM { plaintext: &[u8], ciphertext: &mut [u8], tag: &mut [u8; GCM_TAG_SIZE], - ) { - assert!( - ciphertext.len() >= plaintext.len(), - "암호문 버퍼가 평문보다 작아 무음 절단 발생" - ); - assert!( - plaintext.len() as u64 <= GCM_MAX_INPUT_LEN, - "SP 800-38D 평문 길이 한계(2^39-256 비트) 초과로 카운터 재사용 발생" - ); - assert!( - aad.len() as u64 <= GCM_MAX_AAD_LEN, - "SP 800-38D AAD 길이 한계(2^64-1 비트) 초과" - ); + ) -> Result<(), Error> { + if ciphertext.len() < plaintext.len() { + return Err(Error::BufferTooSmall); + } + if plaintext.len() as u64 > GCM_MAX_INPUT_LEN { + return Err(Error::InputTooLong); + } + if aad.len() as u64 > GCM_MAX_AAD_LEN { + return Err(Error::AadTooLong); + } let mut j0 = self.compute_j0(nonce); let mut icb = j0; @@ -124,6 +121,8 @@ impl AES256GCM { icb.zeroize(); j0.zeroize(); + + Ok(()) } pub fn decrypt( @@ -133,19 +132,16 @@ impl AES256GCM { ciphertext: &[u8], tag: &[u8; GCM_TAG_SIZE], plaintext: &mut [u8], - ) -> bool { - assert!( - plaintext.len() >= ciphertext.len(), - "평문 버퍼가 암호문보다 작아 무음 절단 발생" - ); - assert!( - ciphertext.len() as u64 <= GCM_MAX_INPUT_LEN, - "SP 800-38D 암호문 길이 한계(2^39-256 비트) 초과로 카운터 재사용 발생" - ); - assert!( - aad.len() as u64 <= GCM_MAX_AAD_LEN, - "SP 800-38D AAD 길이 한계(2^64-1 비트) 초과" - ); + ) -> Result<(), Error> { + if plaintext.len() < ciphertext.len() { + return Err(Error::BufferTooSmall); + } + if ciphertext.len() as u64 > GCM_MAX_INPUT_LEN { + return Err(Error::InputTooLong); + } + if aad.len() as u64 > GCM_MAX_AAD_LEN { + return Err(Error::AadTooLong); + } let mut j0 = self.compute_j0(nonce); @@ -160,7 +156,7 @@ impl AES256GCM { if !authentic { j0.zeroize(); - return false; + return Err(Error::AuthenticationFailed); } let mut icb = j0; @@ -170,14 +166,7 @@ impl AES256GCM { icb.zeroize(); j0.zeroize(); - true - } -} - -impl Drop for AES256GCM { - fn drop(&mut self) { - self.h.zeroize(); - // cipher 필드의 round_keys 는 AES256 의 Drop 에서 Secret::Drop 으로 자동 소거됨 + Ok(()) } } @@ -195,7 +184,7 @@ mod tests { unsafe { storage.write(AES256GCM::new(&key)); - let h_ptr = storage.assume_init_ref().h.as_ptr(); + let h_ptr = storage.assume_init_ref().h.expose().as_ptr(); let rk_ptr = storage .assume_init_ref() .cipher @@ -236,7 +225,8 @@ mod tests { let gcm = AES256GCM::new(&key); let mut ciphertext = [0u8; 0]; let mut tag = [0u8; 16]; - gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag); + gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) + .unwrap(); assert_eq!(tag, expected_tag); } @@ -258,13 +248,14 @@ mod tests { let gcm = AES256GCM::new(&key); let mut ciphertext = [0u8; 16]; let mut tag = [0u8; 16]; - gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag); + gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) + .unwrap(); assert_eq!(ciphertext, expected_ciphertext); assert_eq!(tag, expected_tag); let mut decrypted = [0u8; 16]; let result = gcm.decrypt(&nonce, &aad, &ciphertext, &tag, &mut decrypted); - assert!(result); + assert!(result.is_ok()); assert_eq!(decrypted, plaintext); } @@ -301,14 +292,70 @@ mod tests { let gcm = AES256GCM::new(&key); let mut ciphertext = [0u8; 64]; let mut tag = [0u8; 16]; - gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag); + gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) + .unwrap(); assert_eq!(ciphertext, expected_ciphertext); assert_eq!(tag, expected_tag); let mut decrypted = [0u8; 64]; let result = gcm.decrypt(&nonce, &aad, &ciphertext, &tag, &mut decrypted); - assert!(result); + assert!(result.is_ok()); + assert_eq!(decrypted, plaintext); + } + + /// AAD 가 비어있지 않은 경로(len_block 의 aad_bits)와 60바이트 평문의 GCTR 부분 블록 경로는 이 KAT 만이 커버합니다. + #[test] + fn gcm_test_case_17() { + let key: [u8; 32] = [ + 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: [u8; 20] = [ + 0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad, 0xbe, 0xef, 0xfe, 0xed, 0xfa, 0xce, 0xde, 0xad, + 0xbe, 0xef, 0xab, 0xad, 0xda, 0xd2, + ]; + let plaintext: [u8; 60] = [ + 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, 0x39, + ]; + let expected_ciphertext: [u8; 60] = [ + 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, 0x62, + ]; + let expected_tag: [u8; 16] = [ + 0x76, 0xfc, 0x6e, 0xce, 0x0f, 0x4e, 0x17, 0x68, 0xcd, 0xdf, 0x88, 0x53, 0xbb, 0x2d, + 0x55, 0x1b, + ]; + + let gcm = AES256GCM::new(&key); + let mut ciphertext = [0u8; 60]; + let mut tag = [0u8; 16]; + gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) + .unwrap(); + assert_eq!(ciphertext, expected_ciphertext); + assert_eq!(tag, expected_tag); + + let mut decrypted = [0u8; 60]; + let result = gcm.decrypt(&nonce, &aad, &ciphertext, &tag, &mut decrypted); + assert!(result.is_ok()); assert_eq!(decrypted, plaintext); + + let mut tampered_aad = aad; + tampered_aad[0] ^= 1; + + let mut tampered_out = [0u8; 60]; + let result = gcm.decrypt(&nonce, &tampered_aad, &ciphertext, &tag, &mut tampered_out); + assert_eq!(result, Err(Error::AuthenticationFailed)); } #[test] @@ -321,12 +368,27 @@ mod tests { let gcm = AES256GCM::new(&key); let mut ciphertext = [0u8; 16]; let mut tag = [0u8; 16]; - gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag); + gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) + .unwrap(); tag[0] ^= 1; let mut decrypted = [0u8; 16]; let result = gcm.decrypt(&nonce, &aad, &ciphertext, &tag, &mut decrypted); - assert!(!result); + assert_eq!(result, Err(Error::AuthenticationFailed)); + } + + #[test] + fn gcm_buffer_too_small() { + let key = [0x11u8; 32]; + let nonce = [0x22u8; 12]; + let aad: [u8; 0] = []; + let plaintext = [0x33u8; 32]; + + let gcm = AES256GCM::new(&key); + let mut ciphertext = [0u8; 16]; + let mut tag = [0u8; 16]; + let result = gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag); + assert_eq!(result, Err(Error::BufferTooSmall)); } } diff --git a/aes/src/ghash.rs b/aes/src/ghash.rs index 480b7a0..a2dff03 100644 --- a/aes/src/ghash.rs +++ b/aes/src/ghash.rs @@ -1,4 +1,4 @@ -use zeroize::Zeroize; +use zeroize::{Secret, Zeroize}; // 32 비트 다항식(carryless) 곱셈 // BearSSL `bmul32` 트릭: 피연산자를 4비트 간격 (mask 0x1111_1111) 로 4등분하여 @@ -101,25 +101,25 @@ fn gf128_mul(x: u128, y: u128) -> u128 { pub struct GHash { // 자연 순서로 변환된 H. 매 update 마다의 비트 역변환 비용 제거 - h_n: u128, + h_n: Secret, // 자연 순서 누적 상태. finalize 시점에 GHASH 표기로 역변환 - state_n: u128, + state_n: Secret, } impl GHash { #[must_use] pub fn new(h: &[u8; 16]) -> Self { Self { - h_n: u128::from_be_bytes(*h).reverse_bits(), - state_n: 0, + h_n: Secret::new(u128::from_be_bytes(*h).reverse_bits()), + state_n: Secret::new(0), } } pub fn update(&mut self, block: &[u8; 16]) { let x_n = u128::from_be_bytes(*block).reverse_bits(); - let combined = self.state_n ^ x_n; - let (lo, hi) = poly_mul_128(combined, self.h_n); - self.state_n = reduce_natural(lo, hi); + let combined = *self.state_n.expose() ^ x_n; + let (lo, hi) = poly_mul_128(combined, *self.h_n.expose()); + *self.state_n.expose_mut() = reduce_natural(lo, hi); } pub fn update_padded(&mut self, data: &[u8]) { @@ -139,7 +139,7 @@ impl GHash { #[must_use] pub fn finalize(self) -> [u8; 16] { - self.state_n.reverse_bits().to_be_bytes() + self.state_n.expose().reverse_bits().to_be_bytes() } pub fn reset(&mut self) { @@ -147,13 +147,6 @@ impl GHash { } } -impl Drop for GHash { - fn drop(&mut self) { - self.h_n.zeroize(); - self.state_n.zeroize(); - } -} - #[cfg(test)] mod tests { use super::*; @@ -171,8 +164,8 @@ mod tests { // update 로 state 를 0 이 아닌 값으로 만듦 (*storage.as_mut_ptr()).update(&[0xAAu8; 16]); - let h_ptr = &raw const (*storage.as_ptr()).h_n as *const u8; - let s_ptr = &raw const (*storage.as_ptr()).state_n as *const u8; + let h_ptr = (*storage.as_ptr()).h_n.expose() as *const u128 as *const u8; + let s_ptr = (*storage.as_ptr()).state_n.expose() as *const u128 as *const u8; let pre_h = core::slice::from_raw_parts(h_ptr, 16); let pre_s = core::slice::from_raw_parts(s_ptr, 16); diff --git a/aes/src/lib.rs b/aes/src/lib.rs index 682b175..7773e16 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -20,7 +20,7 @@ //! let gcm = AES256GCM::new(&key); //! let mut ciphertext = [0u8; 16]; //! let mut tag = [0u8; GCM_TAG_SIZE]; -//! gcm.encrypt(&nonce, &[], plaintext, &mut ciphertext, &mut tag); +//! gcm.encrypt(&nonce, &[], plaintext, &mut ciphertext, &mut tag).unwrap(); //! ``` //! //! # Security Note @@ -48,6 +48,15 @@ use block::{decrypt_block, encrypt_block}; use key::expand_key; use zeroize::Secret; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + InvalidLength, + BufferTooSmall, + InputTooLong, + AadTooLong, + AuthenticationFailed, +} + const NB: usize = 4; const NR: usize = 14; From 3ff443dac988362dbad2bc945b94a1ce546da742 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:03:45 +0900 Subject: [PATCH 36/65] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4?= =?UTF-8?q?=EB=A5=BC=20MIT=20=EB=8B=A8=EB=8F=85=EC=97=90=EC=84=9C=20MIT=20?= =?UTF-8?q?OR=20Apache-2.0=20=EB=93=80=EC=96=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20=EB=A3=A8=ED=8A=B8=20LICENSE=20=EB=A5=BC=20LICE?= =?UTF-8?q?NSE-MIT=20LICENSE-APACHE=20=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20-?= =?UTF-8?q?=20Cargo.toml=20workspace=20license=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20README=20README=5FEN=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=84=A0=EC=8A=A4=20=EC=84=B9=EC=85=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 4 +- LICENSE-APACHE | 201 +++++++++++++++++++++++++++++++++++++++++ LICENSE => LICENSE-MIT | 0 README.md | 2 +- README_EN.md | 2 +- 5 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 LICENSE-APACHE rename LICENSE => LICENSE-MIT (100%) diff --git a/Cargo.toml b/Cargo.toml index 1def578..930c7e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,9 @@ resolver = "3" version = "1.0.0" edition = "2024" authors = ["Q. T. Felix "] -# 루트 LICENSE (MIT) 와 동기화. SPDX 식별자로 명시해 다운스트림 +# 루트 LICENSE-MIT / LICENSE-APACHE 와 동기화. SPDX 식별자로 명시해 다운스트림 # 공급망 감사 도구 (cargo-deny 등) 가 인식하도록 함. -license = "MIT" +license = "MIT OR Apache-2.0" [profile.dev] panic = "abort" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f994ba9 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing + the origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Quant Theodore Felix + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE b/LICENSE-MIT similarity index 100% rename from LICENSE rename to LICENSE-MIT diff --git a/README.md b/README.md index bc94c3e..b99d58e 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,4 @@ # 라이선스 -이 프로젝트는 [MIT LICENSE](LICENSE)하에 있습니다. \ No newline at end of file +이 프로젝트는 [MIT LICENSE](LICENSE-MIT) 또는 [Apache License 2.0](LICENSE-APACHE) 중 선택하여 사용할 수 있습니다. \ No newline at end of file diff --git a/README_EN.md b/README_EN.md index 9378e97..709e9b4 100644 --- a/README_EN.md +++ b/README_EN.md @@ -18,4 +18,4 @@ Additionally, information about what scope AI agents are used for in this projec # License -This project is under the [MIT LICENSE](LICENSE). +This project is dual-licensed under either the [MIT LICENSE](LICENSE-MIT) or the [Apache License 2.0](LICENSE-APACHE), at your option. From 737276fc9dd834eb2897c4ab5efc116f4d71f4ea Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:26:41 +0900 Subject: [PATCH 37/65] =?UTF-8?q?aes=20=ED=81=AC=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=A3=BC=EC=84=9D=20=EC=A0=95=EB=A6=AC=20-=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=B0=EA=B2=BD=EC=9D=84=20=EA=B8=B8?= =?UTF-8?q?=EA=B2=8C=20=EC=84=A4=EB=AA=85=ED=95=98=EB=8D=98=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=A0=9C=EA=B1=B0=20-=20core=20mem=20size=5Fof=20?= =?UTF-8?q?=EB=A5=BC=20=ED=94=84=EB=A0=90=EB=A5=98=EB=93=9C=20size=5Fof=20?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20-=20=EB=AC=B8=EC=9E=A5=20?= =?UTF-8?q?=EB=B6=80=ED=98=B8=20=EA=B7=9C=EC=B9=99=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20Docstring=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/src/block.rs | 3 --- aes/src/gcm.rs | 8 ++++---- aes/src/ghash.rs | 34 +++++++--------------------------- aes/src/lib.rs | 11 +++++------ aes/src/sbox.rs | 34 ++++++++++++---------------------- 5 files changed, 28 insertions(+), 62 deletions(-) diff --git a/aes/src/block.rs b/aes/src/block.rs index e4ecb11..c0afd7f 100644 --- a/aes/src/block.rs +++ b/aes/src/block.rs @@ -31,9 +31,6 @@ pub fn state_to_block(state: &State) -> [u8; 16] { block } -// SubBytes 는 셀 위치에 의존 안해서 행렬 -> 16 바이트 -> BP 회로 -> 16 바이트 -// -> 행렬 순서로 우회됌. 비트슬라이스 변환/복원 비용은 라운드 1회 약 60게이트 -// 수준으로, 셀별 256회 스캔(이전) 보다 압도적으로 작다! #[inline] fn sub_bytes(state: &mut State) { let mut bytes = [0u8; 16]; diff --git a/aes/src/gcm.rs b/aes/src/gcm.rs index 9ce7d7f..6a979ee 100644 --- a/aes/src/gcm.rs +++ b/aes/src/gcm.rs @@ -175,8 +175,8 @@ mod tests { use super::*; use core::mem::MaybeUninit; - /// AES256GCM 의 h (해시 서브키 = AES_K(0^128)) 는 키 정보 누출 위험. - /// Drop 후 h 와 내부 AES round_keys 가 0 으로 소거되는지 검증. + // AES256GCM의 h(해시 서브키 = AES_K(0^128))는 키 정보 누출 위험 + // Drop 후 h와 내부 AES round_keys가 0으로 소거되는지 검증 #[test] fn test_aes256gcm_zeroize_on_drop() { let key = [0x5Au8; 32]; @@ -191,7 +191,7 @@ mod tests { .round_keys .expose() .as_ptr() as *const u8; - let rk_len = core::mem::size_of::<[u32; 60]>(); + let rk_len = size_of::<[u32; 60]>(); let pre_h = core::slice::from_raw_parts(h_ptr, 16); assert!(pre_h.iter().any(|&b| b != 0), "GCM h 가 비어 있음"); @@ -303,7 +303,7 @@ mod tests { assert_eq!(decrypted, plaintext); } - /// AAD 가 비어있지 않은 경로(len_block 의 aad_bits)와 60바이트 평문의 GCTR 부분 블록 경로는 이 KAT 만이 커버합니다. + // AAD가 비어있지 않은 경로(len_block 의 aad_bits)와 60바이트 평문의 GCTR 부분 블록 경로는 이 KAT 만이 커버합니다. #[test] fn gcm_test_case_17() { let key: [u8; 32] = [ diff --git a/aes/src/ghash.rs b/aes/src/ghash.rs index a2dff03..0242c8e 100644 --- a/aes/src/ghash.rs +++ b/aes/src/ghash.rs @@ -1,12 +1,5 @@ use zeroize::{Secret, Zeroize}; -// 32 비트 다항식(carryless) 곱셈 -// BearSSL `bmul32` 트릭: 피연산자를 4비트 간격 (mask 0x1111_1111) 로 4등분하여 -// 일반 정수 곱셈으로 GF(2)[X] 곱을 얻음. 32비트 입력 기준 한 lane당 최대 8개 -// 부분곱이 합산되며 (8 < 16), 4비트 lane안에 carry없이 들어가므로 정확히 다항식 비트가 추출 -// -// AES-NI/PCLMULQDQ 미지원 환경 (TCG 등) 에서 정수 곱셈만으로 상수-시간으로 실행됨 -// (AMD64/AArch64 정수 곱셈은 데이터 비종속 시간 보장) #[inline] const fn bmul32(x: u32, y: u32) -> u64 { const MX0: u32 = 0x1111_1111; @@ -36,8 +29,6 @@ const fn bmul32(x: u32, y: u32) -> u64 { (z0 & MZ0) | (z1 & MZ1) | (z2 & MZ2) | (z3 & MZ3) } -// 64 비트 다항식 곱셈을 32 비트 Karatsuba 로 합성 -// (xh·X^32 + xl)(yh·X^32 + yl) = xh·yh·X^64 + (xh·yl ⊕ xl·yh)·X^32 + xl·yl #[inline] fn bmul64(x: u64, y: u64) -> u128 { let xh = (x >> 32) as u32; @@ -53,8 +44,6 @@ fn bmul64(x: u64, y: u64) -> u128 { p_ll ^ (p_mid << 32) ^ (p_hh << 64) } -// 128비트 다항식 곱. 256비트 결과를 (low_128, high_128)로 반환 -// 64비트 Karatsuba 합성: 3회의 bmul64 호출 #[inline] fn poly_mul_128(x: u128, y: u128) -> (u128, u128) { let xh = (x >> 64) as u64; @@ -71,13 +60,7 @@ fn poly_mul_128(x: u128, y: u128) -> (u128, u128) { let hi = p_hh ^ (p_mid >> 64); (lo, hi) } -// (lo, hi) 의 256 비트 다항식을 p(X) = X^128 + X^7 + X^2 + X + 1 로 환원 (자연 순서) -// hi 의 i 번째 비트는 X^(128+i), 그리고 X^128 ≡ X^7 + X^2 + X + 1 (mod p) -// 구글링해보니 -// T = hi · (1 + X + X^2 + X^7), 차수 ≤ 134 -// T_lo = u128폭 안의 비트 (0..127) -// T_hi = 오버플로우 비트 (128..134, 7비트) -// T_hi 를 같은 식으로 한번 더 환원 (차수 ≤ 13, 추가 오버플로우 없음) + #[inline] fn reduce_natural(lo: u128, hi: u128) -> u128 { let t_lo = hi ^ (hi << 1) ^ (hi << 2) ^ (hi << 7); @@ -86,9 +69,9 @@ fn reduce_natural(lo: u128, hi: u128) -> u128 { lo ^ t_lo ^ t2 } -/// 테스트 검증용 GHASH 비트역순 표기에서의 GF(2^128) 곱셈 -/// 입력 `x`, `y` 는 `u128::from_be_bytes` 결과 (NIST SP 800-38D의 비트역순 표현) 를 가정합니다. -/// `GHash::update` 는 H의 사전 변환 결과를 캐시하므로 이 래퍼를 사용하지 않습니다. +/// 테스트 검증용 GHASH 비트-역순 표기에서의 GF(2^128) 곱셈 +/// 입력 `x`, `y`는 `u128::from_be_bytes` 결과(NIST SP 800-38D의 비트역순 표현)를 가정합니다. +/// `GHash::update`는 H의 사전 변환 결과를 캐시하므로 이 래퍼를 사용하지 않습니다. #[inline] #[must_use] #[cfg(test)] @@ -100,9 +83,7 @@ fn gf128_mul(x: u128, y: u128) -> u128 { } pub struct GHash { - // 자연 순서로 변환된 H. 매 update 마다의 비트 역변환 비용 제거 h_n: Secret, - // 자연 순서 누적 상태. finalize 시점에 GHASH 표기로 역변환 state_n: Secret, } @@ -152,8 +133,8 @@ mod tests { use super::*; use core::mem::MaybeUninit; - /// GHash 의 h (인증 서브키) 와 state 는 모두 비밀. - /// Drop 후 메모리가 0 으로 소거되는지 검증. + // GHash 의 h(인증 서브키)와 state는 모두 비밀 + // Drop 후 메모리가 0으로 소거되는지 검증 #[test] fn test_ghash_zeroize_on_drop() { let h: [u8; 16] = [0xDEu8; 16]; @@ -161,7 +142,6 @@ mod tests { unsafe { storage.write(GHash::new(&h)); - // update 로 state 를 0 이 아닌 값으로 만듦 (*storage.as_mut_ptr()).update(&[0xAAu8; 16]); let h_ptr = (*storage.as_ptr()).h_n.expose() as *const u128 as *const u8; @@ -210,7 +190,7 @@ mod tests { assert_eq!(gf128_mul(a, b), expected); } - /// bmul32가 carryless (GF(2)[X]) 다항식 곱과 일치하는지 검증합니다. + /// bmul32가 carryless(GF(2)[X]) 다항식 곱과 일치하는지 검증합니다. #[test] fn bmul32_against_bitserial() { fn bitserial(x: u32, y: u32) -> u64 { diff --git a/aes/src/lib.rs b/aes/src/lib.rs index 7773e16..66c38d1 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -106,8 +106,8 @@ mod tests { use super::*; use core::mem::MaybeUninit; - /// AES256 의 round_keys 는 expand_key 결과로 키 정보를 포함. - /// Drop 후 round_keys 메모리가 0 으로 소거되는지 검증. + // AES256의 round_keys는 expand_key 결과로 키 정보를 포함함 + // Drop 후 round_keys 메모리가 0으로 소거되는지 검증 #[test] fn test_aes256_zeroize_on_drop() { let key = [0xA5u8; KEY_SIZE]; @@ -115,14 +115,13 @@ mod tests { unsafe { storage.write(AES256::new(&key)); - // Secret<[u32; 60]> 의 inner 위치에 round_keys 저장 let ptr = storage.assume_init_ref().round_keys.expose().as_ptr() as *const u8; - let byte_len = core::mem::size_of::<[u32; NB * (NR + 1)]>(); + let byte_len = size_of::<[u32; NB * (NR + 1)]>(); let pre = core::slice::from_raw_parts(ptr, byte_len); assert!( pre.iter().any(|&b| b != 0), - "round_keys 가 비어 있음 — expand_key 가 동작하지 않음" + "round_keys 가 비어 있음, expand_key 가 동작하지 않음" ); storage.assume_init_drop(); @@ -130,7 +129,7 @@ mod tests { let post = core::slice::from_raw_parts(ptr, byte_len); assert!( post.iter().all(|&b| b == 0), - "AES256 round_keys 가 Drop 후 소거되지 않음" + "AES256 round_keys가 Drop 후 소거되지 않음" ); } } diff --git a/aes/src/sbox.rs b/aes/src/sbox.rs index 779279a..18f8d03 100644 --- a/aes/src/sbox.rs +++ b/aes/src/sbox.rs @@ -1,24 +1,16 @@ -//! AES S-box / inverse S-box +//! AES S-box / inverse S-box 연산을 수행하는 모듈입니다. //! -//! 과거 구현에서 종전 구현은 256회 constant-time 스캔 방식이었으나, AES-NI 미지원 환경 (TCG 등) -//! 에서는 라운드 1회당 수만 명령어가 발생해 처리량이 급격히 떨어집니다. -//! Boyar–Peralta 최소 게이트 회로 (논리 게이트 ~115 개)로 교체됐습니다. 모든 연산이 -//! AND/XOR/NOT 비트 연산이라 비밀 의존 분기·메모리 접근이 없으며 본질적으로 상수-시간입니다. +//! 모든 연산이 AND/XOR/NOT 비트 연산이라 비밀 의존 분기 및 메모리 접근이 없으며 +//! 본질적으로 상수-시간입니다. //! -//! 16바이트 블록은 비트슬라이스 표현 [u32; 8]로 변환해 1회의 BP 회로로 16바이트 -//! SubBytes를 일괄 처리합니다. 단일 바이트 sub_byte / inv_sub_byte 는 키 스케쥴 -//! (sub_word) 에서 호출되며 동일 회로를 1비트만 사용하는 형태로 호출한다. -//! -//! 회로 출처: Boyar & Peralta, "A New Combinational Logic Minimization Technique -//! with Applications to Cryptology" (2010). BearSSL `aes_ct.c` 의 동일 회로 기반 -//! TODO: Dosctring 수정 +//! 16바이트 블록은 비트슬라이스 표현 \[u32; 8\]로 변환해 1회의 BP 회로로 16바이트 +//! SubBytes를 일괄 처리합니다. 단일 바이트 sub_byte / inv_sub_byte는 키 스케쥴 +//! (sub_word)에서 호출되며 동일 회로를 1비트만 사용하는 형태로 호출합니다. #![allow(clippy::many_single_char_names, clippy::similar_names)] use zeroize::Zeroize; -// 16바이트 블록을 [u32; 8]비트슬라이스 평면으로 변환 -// q[k]의 i번째 비트 = bytes[i]의 k번째 비트 #[inline] fn bitslice_block(bytes: &[u8; 16]) -> [u32; 8] { let mut q = [0u32; 8]; @@ -195,9 +187,6 @@ fn bitsliced_sbox(q: &mut [u32; 8]) { q[0] = s7; } -// 역 S-box: 입력에 AES affine 의 역 (= 인접 비트 회전 + 상수 0x05) 을 적용하고 -// 정방향 S-box 를 통과시킨 뒤 같은 affine 역변환을 한 번 더 적용 -// (S-box(x) = Affine(Inv(x)) 이므로 InvSBox(y) = Inv(Affine⁻¹(y)) = Affine⁻¹(SBox(Affine⁻¹(y))).) fn bitsliced_inv_sbox(q: &mut [u32; 8]) { inv_affine(q); bitsliced_sbox(q); @@ -224,7 +213,7 @@ fn inv_affine(q: &mut [u32; 8]) { q[0] = q2 ^ q5 ^ q7; } -/// 16 바이트 블록 SubBytes 를 비트슬라이스 BP 회로로 일괄 적용합니다. +/// 16바이트 블록 SubBytes를 비트슬라이스 BP 회로로 일괄 적용하는 함수입니다. pub fn sub_bytes_block(bytes: &mut [u8; 16]) { let mut q = bitslice_block(bytes); bitsliced_sbox(&mut q); @@ -232,7 +221,7 @@ pub fn sub_bytes_block(bytes: &mut [u8; 16]) { q.zeroize(); } -/// 16 바이트 블록 InvSubBytes 를 비트슬라이스 BP 회로로 일괄 적용합니다. +/// 16바이트 블록 InvSubBytes를 비트슬라이스 BP 회로로 일괄 적용하는 함수입니다. pub fn inv_sub_bytes_block(bytes: &mut [u8; 16]) { let mut q = bitslice_block(bytes); bitsliced_inv_sbox(&mut q); @@ -240,8 +229,8 @@ pub fn inv_sub_bytes_block(bytes: &mut [u8; 16]) { q.zeroize(); } -/// 단일 바이트 S-box. 키 스케줄 (sub_word) 에서 호출됩니다. -/// 동일 BP 회로를 비트 1 개만 사용하는 형태로 적용 (회로 자체가 데이터 비종속) +/// 키 스케줄(sub_word)에서 호출되는 단일 바이트 S-box 함수입니다. +/// 동일 BP 회로를 비트 1개만 사용하는 형태로 적용(회로 자체가 데이터 비종속)됩니다. #[inline] #[must_use] pub fn sub_byte(x: u8) -> u8 { @@ -258,7 +247,8 @@ pub fn sub_byte(x: u8) -> u8 { out } -/// 단일 바이트 역 S-box. 테스트와 향후 디코딩 경로의 셀별 호출용 +// 단일 바이트 역 S-box +// 테스트와 향후 디코딩 경로의 셀별 호출용 #[inline] #[must_use] #[cfg(test)] From bb85aae968136d8e5899cf293ccef4fc5b83229c Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:26:41 +0900 Subject: [PATCH 38/65] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4=20?= =?UTF-8?q?=EB=93=80=EC=96=BC=ED=99=94=EC=99=80=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20-=20LICENSE=20=EB=A5=BC=20LICENSE-MIT=20?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AE=EA=B8=B0=EA=B3=A0=20LICENSE-APACHE=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20README=20=EC=97=90=20CMVP=20=EB=AF=B8?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=95=88=EB=82=B4=EC=99=80=201.1.0=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EC=83=81=ED=99=A9=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20cross-confirm=20=EC=97=90=20KCAVP=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80=20-=20gitignore=20?= =?UTF-8?q?=EC=97=90=20cavp=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- LICENSE-APACHE | 201 +++++++++++++++++++++++++++++++++++++++++ LICENSE => LICENSE-MIT | 2 +- README.md | 10 +- README_EN.md | 2 +- cross-confirm.md | 21 ++++- 6 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 LICENSE-APACHE rename LICENSE => LICENSE-MIT (96%) diff --git a/.gitignore b/.gitignore index 2069b64..289b024 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ dist/ *.iso Cargo.lock CLAUDE.md -.planning/ \ No newline at end of file +.planning/ +cavp/ \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f994ba9 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing + the origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Quant Theodore Felix + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE b/LICENSE-MIT similarity index 96% rename from LICENSE rename to LICENSE-MIT index fa27a53..336a2b3 100644 --- a/LICENSE +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2026 Quant Theodore Felix +Copyright (c) 2026 Team Quant Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index f03e3e5..e26a3ad 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ [![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) [![Qu4nt-Space-Discord](https://img.shields.io/badge/Qu4nt_Space-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/9utg4hp3m8) +> [!IMPORTANT] +> 정식 CMVP(Cryptographic Module Validation Program)를 거치지 않은 암호 모듈입니다. 연구 목적으로 사용해주세요. +> 1.1.0 릴리즈 버전에선 각 암호 알고리즘의 구현과 검증, 정형 문서 작성이 이루어집니다. + [Rust 기반 얽힘 라이브러리 네이티브 프로젝트](https://github.com/Quant-Off/entlib-native)는 가장 많이 사용되는 아키텍처에 대해 `std`(및 `no_std`)를 지원하며, 고보안 규격(국제적 규제 등 컴플라이언스)을 준수하는 데 초점을 맞춥니다. 이 모듈은 그런 부분에서 합리적입니다. 이 모듈은 격리형 초경량 마이크로커널 K0(Isolation Lightweight Microkernel K0, ISO-LIGHT-K0)에서 Ring 3 사용자 공간(user space) 내에 데몬(daemon) 형식으로 구동되며 TUI와 IPC 메시지를 통해 암호화 통신합니다. 데몬은 Ring 0 커널 공간(kernel space) 속 IPC 엔드포인트 라우터로 데이터를 전송하는 방식으로 동작합니다. @@ -15,8 +19,10 @@ # 릴리즈 `1.1.0` -과거 `1.0.0` 릴리즈에서 미처 발견하지 못 했던 몇 가지 문제와 컴플라이언스 측면에서의 구현 오류를 바로잡기 위해 릴리즈를 준비 중에 있습니다. 이 작업은 세밀하고 체계적인 교차 검증 작업을 기반으로 수행되며, 그 기록은 [이 문서](cross-confirm.md)에서 확인하실 수 있습니다. 해당 문서는 `1.1.0` 릴리즈가 공개되면 제거됩니다. +과거 `1.0.0` 릴리즈에서 미처 발견하지 못 했던 몇 가지 문제와 컴플라이언스 측면에서의 구현 오류를 바로잡았습니다. 이 작업은 세밀하고 체계적인 교차 검증 작업을 기반으로 수행되며, 그 기록은 [이 문서](cross-confirm.md)에서 확인하실 수 있습니다. 해당 문서는 `1.1.0` 릴리즈가 공개되면 제거됩니다. + +이 릴리즈에선 개별 암호 알고리즘의 개발자 - Fable 5 - 전문가의 교차 검증 작업과 정형 문서 작성을 주력으로 검토했습니다. 이 과정에서 상당수의 보안 문제를 해결했습니다. 모든 구현된 암호 알고리즘이 정상적으로 구현되어 작동하는지 확인하기 위해 KCMVP의 CAVP(Cryptographic Algorithm Validation Program) 검증을 진행합니다. # 라이선스 -이 프로젝트는 [MIT LICENSE](LICENSE)하에 있습니다. \ No newline at end of file +이 프로젝트는 [MIT LICENSE](LICENSE-MIT) 또는 [Apache License 2.0](LICENSE-APACHE) 중 선택하여 사용할 수 있습니다. \ No newline at end of file diff --git a/README_EN.md b/README_EN.md index 77af0fb..d824c4b 100644 --- a/README_EN.md +++ b/README_EN.md @@ -22,4 +22,4 @@ We are preparing a release to correct several issues and compliance-related impl # License -This project is under the [MIT LICENSE](LICENSE). +This project is under the [MIT LICENSE](LICENSE-MIT). diff --git a/cross-confirm.md b/cross-confirm.md index 6bc57bb..489edfc 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -3,7 +3,14 @@ > 타겟 릴리즈: `1.1.0` > 작업 브랜치: `feature/1.1.0-verify` -이 검증 작업은 4명의 보안 및 컴플라이언스 종사자(전문가)와 Claude Fable 5 모델을 통해 수행됩니다. 다음 목록은 현황을 명시한 것입니다. +> [!IMPORTANT] +> 즉, 이 검증 작업이 완료되었다고 해서 이 암호 모듈이 완전히 안전해진 것이 아닙니다. 암호 모듈 검증을 위해선 전문 검증 단체의 승인(CMVP)이 필요하고, 이 작업은 수 개월에서 많게는 수 년까지 소요될 수 있습니다. + +이 문서는 릴리즈 배포 전 제거됩니다. + +## 인간 교차 검증 작업 + +이 검증 작업은 메인테이너 개발자와 4명의 보안 및 컴플라이언스 종사자(전문가)와 Claude Fable 5 모델을 통해 수행됩니다. 다음 목록은 현황을 명시한 것입니다. - [X] AES - [X] BLAKE @@ -20,7 +27,13 @@ - [X] Constant-Time - [X] Zeroize -여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "담당 명세에 따라 정확하게 구현되어 있으며, 정상적으로 동작한다"는 것을 명시하는 것입니다. +여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "담당 명세에 따라 정확하게 구현되어 있다"는 것을 의미합니다. -> [!IMPORTANT] -> 즉, 이 검증 작업이 완료되었다고 해서 이 암호 모듈이 완전히 안전해진 것이 아닙니다. 암호 모듈 검증을 위해선 전문 검증 단체의 승인(CMVP)이 필요하고, 이 작업은 수 개월에서 많게는 수 년까지 소요될 수 있습니다. +## KCAVP 검증 작업 + +- [ ] AES-256 (CBC, CTR, GCM) +- [ ] SHA2 (224, 256, 384, 512) +- [ ] SHA3 (224, 256, 384, 512) +- [ ] RNG (Hash_DRBG) + +여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "정상적으로 작동한다"는 것을 의미합니다. \ No newline at end of file From 9c0893f0bd283f8fe6234f9a4bbfc15d1d91e108 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:45:50 +0900 Subject: [PATCH 39/65] =?UTF-8?q?README=5FEN=20=EC=9D=84=20=ED=95=9C?= =?UTF-8?q?=EA=B8=80=20README=20=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EB=B2=88?= =?UTF-8?q?=EC=97=AD=20=EA=B0=B1=EC=8B=A0=20-=20CMVP=20=EB=AF=B8=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=95=88=EB=82=B4=20=EB=B8=94=EB=A1=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EB=AC=B8?= =?UTF-8?q?=EB=8B=A8=EC=9D=84=20=EC=99=84=EB=A3=8C=ED=98=95=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=ED=95=98=EA=B3=A0=20CAVP=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=20=EB=AC=B8=EB=8B=A8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20=ED=95=9C=EA=B8=80=20README=20=EC=97=90=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=A0=95=ED=98=95=20=EB=AC=B8=EC=84=9C=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EB=B8=94=EB=A1=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README_EN.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README_EN.md b/README_EN.md index d9eafbd..b2c0870 100644 --- a/README_EN.md +++ b/README_EN.md @@ -3,22 +3,25 @@ [![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) [![Qu4nt-Space-Discord](https://img.shields.io/badge/Qu4nt_Space-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/9utg4hp3m8) +> [!IMPORTANT] +> This cryptographic module has not gone through the official CMVP (Cryptographic Module Validation Program). Please use it for research purposes. +> The `1.1.0` release covers the implementation and validation of each cryptographic algorithm, along with the writing of formal documentation. + The [Rust-based Entanglement Library native project](https://github.com/Quant-Off/entlib-native) supports `std` (and `no_std`) for the most widely used architectures and focuses on complying with high-security standards (such as international regulations and compliance). This module is reasonable in that regard. This module runs as a daemon in the Ring 3 user space on an Isolation Lightweight Microkernel K0 (ISO-LIGHT-K0) and communicates for encryption via TUI and IPC messages. The daemon operates by sending data to an IPC endpoint router in the Ring 0 kernel space. Targeting the NT of the `entlib-native` crypto module, it is written 100% in Rust, and despite being lightweight, it still offers strong security. -> [!IMPORTANT] -> This project does not create complex formal documentation (or technical specifications) for each cryptographic function like in `entlib-native`. Instead, the API signatures and usage of the functions are primarily described in Rust documentation comments, and a summary of this will be posted as a `README.md` in the module (or crate) where the functionality is provided. - You can always refer to the [INTRODUCTION_EN.md](INTRODUCTION_EN.md) file for a detailed project introduction. Additionally, information about what scope AI agents are used for in this project, what parts they actually modify, and the skills and prompts used is documented in [AI_SCOPE_EN.md](AI_SCOPE_EN.md). # Release `1.1.0` -We are preparing a release to correct several issues and compliance-related implementation errors that were not discovered in the previous `1.0.0` release. This work is performed based on meticulous and systematic cross-validation, and the records can be found in [this document (Korean)](cross-confirm.md). That document will be removed once the `1.1.0` release is published. +We have corrected several issues and compliance-related implementation errors that went unnoticed in the previous `1.0.0` release. This work is performed based on meticulous and systematic cross-validation, and the records can be found in [this document (Korean)](cross-confirm.md). That document will be removed once the `1.1.0` release is published. + +For this release, we focused on cross-validation between the developer, Fable 5, and outside experts for each cryptographic algorithm, along with the writing of formal documentation. A significant number of security issues were resolved in that process. To confirm that every implemented cryptographic algorithm is correctly implemented and works as intended, we are proceeding with CAVP (Cryptographic Algorithm Validation Program) validation under KCMVP. # License From a406128ed5402649d484f5a3235624ae02f184f9 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:29:28 +0900 Subject: [PATCH 40/65] =?UTF-8?q?sha3=20KCMVP=20MCT=20=EC=9C=88=EB=8F=84?= =?UTF-8?q?=20=EA=B7=9C=EC=95=BD=20=EC=88=98=EC=A0=95=20-=20mct=5Fchain=20?= =?UTF-8?q?=EC=9D=84=20=EA=B3=A0=EC=A0=95=203-=EC=9C=88=EB=8F=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20N=20=3D=20floor(r/n)+1=20=EA=B7=9C=EC=95=BD?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=BC=EB=B0=98=ED=99=94=20-=20KISA=20?= =?UTF-8?q?=EA=B3=B5=EC=8B=9D=20=EB=B2=A1=ED=84=B0=20Seed=20=EC=99=80=20CO?= =?UTF-8?q?UNT=200=20=EC=A0=95=EB=8B=B5=204=EC=A2=85=20KAT=20=EB=82=B4?= =?UTF-8?q?=EC=9E=A5=20-=20=EB=82=A1=EC=9D=80=20SHA3=20MCT=20rsp=204?= =?UTF-8?q?=EC=A2=85=20=EC=9E=AC=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sha3/tests/kcmvp_sha3.rs | 610 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 610 insertions(+) create mode 100644 sha3/tests/kcmvp_sha3.rs diff --git a/sha3/tests/kcmvp_sha3.rs b/sha3/tests/kcmvp_sha3.rs new file mode 100644 index 0000000..45bb184 --- /dev/null +++ b/sha3/tests/kcmvp_sha3.rs @@ -0,0 +1,610 @@ +#![allow(clippy::upper_case_acronyms)] +#![allow(non_camel_case_types)] + +use sha3::{SHA3, SHA3_224, SHA3_256, SHA3_384, SHA3_512}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MCT_COUNTS: usize = 100; +const MCT_INNER: usize = 1000; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Alg { + SHA3_224, + SHA3_256, + SHA3_384, + SHA3_512, +} + +impl Alg { + fn digest_len(self) -> usize { + match self { + Alg::SHA3_224 => 28, + Alg::SHA3_256 => 32, + Alg::SHA3_384 => 48, + Alg::SHA3_512 => 64, + } + } + + fn hash(self, msg: &[u8]) -> Vec { + match self { + Alg::SHA3_224 => one_shot::(msg), + Alg::SHA3_256 => one_shot::(msg), + Alg::SHA3_384 => one_shot::(msg), + Alg::SHA3_512 => one_shot::(msg), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Kind { + LMT, + SMT, + MCT, +} + +#[derive(Default)] +struct RawBlock { + l: String, + len: String, + fields: Vec<(String, String)>, +} + +impl RawBlock { + fn get(&self, name: &str) -> Option<&str> { + let special = match name { + "L" => Some(self.l.as_str()), + "Len" => Some(self.len.as_str()), + _ => None, + }; + if let Some(value) = special { + return (!value.is_empty()).then_some(value); + } + self.fields + .iter() + .find(|(field, _)| field == name) + .map(|(_, value)| value.as_str()) + } +} + +type FieldRow = Vec<(&'static str, String)>; + +fn hex_decode(s: &str, ctx: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn hex_encode(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push( + char::from_digit(u32::from(b >> 4), 16) + .unwrap() + .to_ascii_uppercase(), + ); + out.push( + char::from_digit(u32::from(b & 0x0F), 16) + .unwrap() + .to_ascii_uppercase(), + ); + } + out +} + +fn parse_field(line: &str, block: &mut RawBlock, ctx: &str) { + let (name, value) = line + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 필드 형식 {line:?}")); + let name = name.trim(); + let value = value.trim(); + if name == "L" { + block.l = value.to_string(); + } else if name == "Len" { + block.len = value.to_string(); + } else { + block.fields.push((name.to_string(), value.to_string())); + } +} + +fn parse_file(content: &str, ctx: &str) -> Vec { + let mut block: Option = None; + let mut blocks = Vec::new(); + for line in content.lines() { + if line.trim().is_empty() { + if let Some(pending) = block.take() { + blocks.push(pending); + } + } else { + parse_field(line, block.get_or_insert_default(), ctx); + } + } + if let Some(pending) = block.take() { + blocks.push(pending); + } + blocks +} + +fn hex_field(block: &RawBlock, name: &str, ctx: &str) -> [u8; N] { + let value = block + .get(name) + .unwrap_or_else(|| panic!("{ctx}: {name} 필드 누락")); + hex_decode(value, &format!("{ctx} {name}")) + .try_into() + .unwrap_or_else(|v: Vec| { + panic!("{ctx}: {name} 길이 {}바이트가 {N}바이트와 다름", v.len()) + }) +} + +fn one_shot(msg: &[u8]) -> Vec { + let mut hasher = H::new(); + hasher.update(msg); + hasher.finalize().as_bytes().to_vec() +} + +// KCMVP 해시 MCT 는 KISA 암호알고리즘 검증기준 V3.0 "07. SHA3 검증시스템" 5.4 임의 +// 메시지 검사 규약을 따릅니다. 윈도 크기 N = floor(r/n) + 1 (r = 스펀지 rate 비트, +// n = 다이제스트 비트) 이고 Msg = MD[i-N] || ... || MD[i-1] 이라 SHA3 는 사이즈별로 +// N 이 6/5/3/2 로 달라집니다. SHA2 는 전 사이즈 N=3 이라 sha2 하네스의 고정 3-윈도가 +// 그대로 규약과 일치합니다. NIST SHA3VS 의 단일 연쇄 MD[i] = SHA3(MD[i-1]) 을 쓰면 +// MCT 만 전부 어긋납니다 +fn mct_chain(seed: [u8; N]) -> Vec { + let window = (1600 - 16 * N) / (8 * N) + 1; + let mut seed = seed; + let mut rows = Vec::with_capacity(MCT_COUNTS); + for count in 0..MCT_COUNTS { + let mut md: Vec<[u8; N]> = vec![seed; window]; + for _ in 0..MCT_INNER { + let mut hasher = H::new(); + for part in &md { + hasher.update(part); + } + let digest = hasher.finalize(); + md.rotate_left(1); + md[window - 1] = digest + .as_bytes() + .try_into() + .unwrap_or_else(|_| panic!("MCT 다이제스트 길이가 {N}바이트와 다름")); + } + let last = md[window - 1]; + rows.push(vec![ + ("COUNT", count.to_string()), + ("MD", hex_encode(&last)), + ]); + seed = last; + } + rows +} + +fn expected_blocks(alg: Alg, kind: Kind, req_blocks: &[RawBlock], ctx: &str) -> Vec { + assert!(!req_blocks.is_empty(), "{ctx}: req 블록 없음"); + let header = &req_blocks[0]; + assert!( + header.len.is_empty() && header.fields.is_empty(), + "{ctx}: L 헤더 블록에 잉여 필드" + ); + assert!( + header.l == alg.digest_len().to_string(), + "{ctx}: L 값 {}이 기대 {}와 다름", + header.l, + alg.digest_len() + ); + let mut rows = vec![vec![("L", header.l.clone())]]; + match kind { + Kind::LMT | Kind::SMT => { + for (idx, block) in req_blocks[1..].iter().enumerate() { + let bctx = format!("{ctx} 블록 {idx}"); + assert!(!block.len.is_empty(), "{bctx}: Len 필드 누락"); + let bits: usize = block + .len + .parse() + .unwrap_or_else(|_| panic!("{bctx}: Len 파싱 실패")); + assert!( + bits.is_multiple_of(8), + "{bctx}: Len {bits}이 8의 배수가 아님" + ); + let msg_hex = block + .get("Msg") + .unwrap_or_else(|| panic!("{bctx}: Msg 필드 누락")); + let msg = hex_decode(msg_hex, &format!("{bctx} Msg")); + let body = if bits == 0 { + assert!(msg == [0u8], "{bctx}: Len 0 인데 Msg 가 00 이 아님"); + &[][..] + } else { + assert!( + msg.len() * 8 == bits, + "{bctx}: Msg 길이 {}비트가 Len {bits}과 다름", + msg.len() * 8 + ); + &msg[..] + }; + let md = alg.hash(body); + rows.push(vec![ + ("Len", block.len.clone()), + ("Msg", msg_hex.to_string()), + ("MD", hex_encode(&md)), + ]); + } + } + Kind::MCT => { + assert!( + req_blocks.len() == 2, + "{ctx}: MCT req 블록 수 {} (2여야 함)", + req_blocks.len() + ); + let block = &req_blocks[1]; + let seed_hex = block + .get("Seed") + .unwrap_or_else(|| panic!("{ctx}: Seed 필드 누락")); + rows.push(vec![("Seed", seed_hex.to_string())]); + let chain = match alg { + Alg::SHA3_224 => mct_chain::(hex_field(block, "Seed", ctx)), + Alg::SHA3_256 => mct_chain::(hex_field(block, "Seed", ctx)), + Alg::SHA3_384 => mct_chain::(hex_field(block, "Seed", ctx)), + Alg::SHA3_512 => mct_chain::(hex_field(block, "Seed", ctx)), + }; + rows.extend(chain); + } + } + rows +} + +fn verify_sam(expected: &[FieldRow], sam_content: &str, ctx: &str) -> Vec { + let sam_blocks = parse_file(sam_content, ctx); + assert!( + sam_blocks.len() == expected.len(), + "{ctx}: 기대 {}블록 vs 파일 {}블록", + expected.len(), + sam_blocks.len() + ); + let mut mismatches = Vec::new(); + for (idx, (fields, sam_block)) in expected.iter().zip(sam_blocks.iter()).enumerate() { + for (name, value) in fields { + let sam_value = sam_block + .get(name) + .unwrap_or_else(|| panic!("{ctx} 블록 {idx}: 파일에 {name} 필드 없음")); + if !sam_value.eq_ignore_ascii_case(value) { + mismatches.push(format!( + "블록 {idx} {name}: 계산 {value} vs 파일 {sam_value}" + )); + } + } + } + mismatches +} + +fn fill_template(template: &str, expected: &[FieldRow], ctx: &str) -> String { + let newline = if template.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + let mut out: Vec = Vec::new(); + let mut idx = 0usize; + let mut saw_field = false; + for line in template.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + if saw_field { + idx += 1; + saw_field = false; + } + out.push(line.to_string()); + continue; + } + saw_field = true; + let (name, value) = trimmed + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 템플릿 라인 {line:?}")); + let name = name.trim(); + let value = value.trim(); + assert!( + idx < expected.len(), + "{ctx}: 템플릿 블록 수가 기대 {}블록 초과", + expected.len() + ); + let (_, exp_value) = expected[idx] + .iter() + .find(|(n, _)| *n == name) + .unwrap_or_else(|| panic!("{ctx} 블록 {idx}: 알 수 없는 템플릿 필드 {name}")); + if value == "?" { + out.push(format!("{name} = {exp_value}")); + } else { + assert!( + value.eq_ignore_ascii_case(exp_value), + "{ctx} 블록 {idx}: 템플릿 {name} 값이 req 계산과 불일치" + ); + out.push(line.to_string()); + } + } + let mut rsp = out.join(newline); + if template.ends_with('\n') { + rsp.push_str(newline); + } + rsp +} + +fn synthesize(expected: &[FieldRow], newline: &str) -> String { + let mut out = String::new(); + for (idx, fields) in expected.iter().enumerate() { + if idx > 0 { + out.push_str(newline); + } + for (name, value) in fields { + out.push_str(&format!("{name} = {value}{newline}")); + } + } + out +} + +fn classify(path: &Path) -> Option<(Alg, Kind)> { + let stem = path.file_stem()?.to_str()?.to_ascii_uppercase(); + let alg = if stem.starts_with("SHA3-224") { + Alg::SHA3_224 + } else if stem.starts_with("SHA3-256") { + Alg::SHA3_256 + } else if stem.starts_with("SHA3-384") { + Alg::SHA3_384 + } else if stem.starts_with("SHA3-512") { + Alg::SHA3_512 + } else { + return None; + }; + let kind = if stem.ends_with("_LMT") { + Kind::LMT + } else if stem.ends_with("_SMT") { + Kind::SMT + } else if stem.ends_with("_MCT") { + Kind::MCT + } else { + return None; + }; + Some((alg, kind)) +} + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn collect_req_files(dir: &Path, found: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_req_files(&path, found); + } else if path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e.eq_ignore_ascii_case("req")) + && classify(&path).is_some() + { + found.push(path); + } + } +} + +fn report_mismatches(path: &Path, mismatches: &[String], total: usize, failures: &mut Vec) { + eprintln!( + "{} 대조 실패 {}건 (전체 {total} 블록)", + path.display(), + mismatches.len() + ); + for m in mismatches.iter().take(10) { + eprintln!(" {m}"); + } + failures.push(format!("{}: {}건 불일치", path.display(), mismatches.len())); +} + +// cavp/ 트리의 SHA3 LMT/SMT/MCT req 를 계산해 rsp 파일로 출력합니다 (sam 은 배포 템플릿 그대로 두고 레이아웃·대조 기준으로만 사용, 기존 rsp 는 회귀 대조) +#[test] +fn kcmvp_sha3_req_vs_rsp() { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - KCMVP SHA3 대조 생략"); + return; + }; + let mut req_files = Vec::new(); + collect_req_files(&root, &mut req_files); + req_files.sort(); + if req_files.is_empty() { + eprintln!( + "{}: SHA3 req 파일 없음 - KCMVP SHA3 대조 생략", + root.display() + ); + return; + } + + let mut failures = Vec::new(); + for req in &req_files { + let ctx = req.display().to_string(); + let (alg, kind) = classify(req).unwrap(); + let content = fs::read_to_string(req).unwrap_or_else(|e| panic!("{ctx}: 읽기 실패 {e}")); + let req_blocks = parse_file(&content, &ctx); + let expected = expected_blocks(alg, kind, &req_blocks, &ctx); + let sam_path = req.with_extension("sam"); + let rsp_path = req.with_extension("rsp"); + + let mut layout = None; + let mut sam_ok = true; + match fs::read_to_string(&sam_path) { + Ok(sam) if sam.contains("= ?") => layout = Some(sam), + Ok(sam) => { + let mismatches = verify_sam(&expected, &sam, &ctx); + if mismatches.is_empty() { + println!("{} 대조 통과 ({} 블록)", sam_path.display(), expected.len()); + layout = Some(sam); + } else { + report_mismatches(&sam_path, &mismatches, expected.len(), &mut failures); + sam_ok = false; + } + } + Err(_) => {} + } + if !sam_ok { + continue; + } + + match fs::read_to_string(&rsp_path) { + Ok(rsp) if !rsp.contains("= ?") => { + let mismatches = verify_sam(&expected, &rsp, &ctx); + if mismatches.is_empty() { + println!("{} 대조 통과 ({} 블록)", rsp_path.display(), expected.len()); + } else { + report_mismatches(&rsp_path, &mismatches, expected.len(), &mut failures); + } + } + _ => { + let rsp = match &layout { + Some(template) => fill_template(template, &expected, &ctx), + None => { + let newline = if content.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + synthesize(&expected, newline) + } + }; + fs::write(&rsp_path, rsp).unwrap_or_else(|e| panic!("{ctx}: rsp 쓰기 실패 {e}")); + println!("{} 생성 ({} 블록)", rsp_path.display(), expected.len()); + } + } + } + assert!( + failures.is_empty(), + "rsp·sam 대조 실패\n{}", + failures.join("\n") + ); +} + +// FIPS 202 확정 벡터와 내장 템플릿으로 해시 계산·MCT 체인·템플릿 채움·대조·오염 검출 로직을 자가 검증합니다 +#[test] +fn kcmvp_sha3_self_check() { + let abc = b"abc"; + assert!( + hex_encode(&Alg::SHA3_224.hash(abc)) + == "E642824C3F8CF24AD09234EE7D3C766FC9A3A5168D0C94AD73B46FDF", + "FIPS 202 SHA3-224 불일치" + ); + assert!( + hex_encode(&Alg::SHA3_256.hash(abc)) + == "3A985DA74FE225B2045C172D6BD390BD855F086E3E9D525B46BFE24511431532", + "FIPS 202 SHA3-256 불일치" + ); + assert!( + hex_encode(&Alg::SHA3_384.hash(abc)) + == "EC01498288516FC926459F58E2C6AD8DF9B473CB0FC08C2596DA7CF0E49BE4B298D88CEA927AC7F539F1EDF228376D25", + "FIPS 202 SHA3-384 불일치" + ); + assert!( + hex_encode(&Alg::SHA3_512.hash(abc)) + == "B751850B1A57168A5693CD924B6B096E08F621827444F70D884F5D0240D2712E10E116E9192AF3C91A7EC57647E3934057340B4CF408D5A56592F8274EEC53F0", + "FIPS 202 SHA3-512 불일치" + ); + assert!( + hex_encode(&Alg::SHA3_256.hash(&[])) + == "A7FFC6F8BF1ED76651C14756A061D662F580FF4DE43B49FA82D80A4B80F8434A", + "SHA3-256 빈 메시지 불일치" + ); + + let req = "L = 32\r\n\r\nLen = 24\r\nMsg = 616263\r\n\r\nLen = 0\r\nMsg = 00\r\n\r\n"; + let req_blocks = parse_file(req, "self-smt"); + assert!(req_blocks.len() == 3, "req 파싱 블록 수 불일치"); + let expected = expected_blocks(Alg::SHA3_256, Kind::SMT, &req_blocks, "self-smt"); + assert!(expected.len() == 3, "기대 블록 수 불일치"); + assert!( + expected[1][2].1 == "3A985DA74FE225B2045C172D6BD390BD855F086E3E9D525B46BFE24511431532", + "SMT abc MD 불일치" + ); + assert!( + expected[2][2].1 == "A7FFC6F8BF1ED76651C14756A061D662F580FF4DE43B49FA82D80A4B80F8434A", + "SMT Len 0 MD 불일치" + ); + + let template = + "L = 32\r\n\r\nLen = 24\r\nMsg = 616263\r\nMD = ?\r\n\r\nLen = 0\r\nMsg = 00\r\nMD = ?"; + let filled = fill_template(template, &expected, "self-fill"); + assert!(!filled.contains('?'), "템플릿 미기입 잔존"); + assert!(!filled.ends_with('\n'), "후행 개행 규약 불일치"); + let mismatches = verify_sam(&expected, &filled, "self-verify"); + assert!(mismatches.is_empty(), "생성 rsp 대조 실패 {mismatches:?}"); + + let first_md = &expected[1][2].1; + let corrupted_char = if first_md.ends_with('0') { "1" } else { "0" }; + let corrupted_md = format!("{}{corrupted_char}", &first_md[..first_md.len() - 1]); + let corrupted = filled.replacen(first_md.as_str(), &corrupted_md, 1); + let mismatches = verify_sam(&expected, &corrupted, "self-corrupt"); + assert!(mismatches.len() == 1, "오염 rsp 미검출 {mismatches:?}"); + + let synthesized = synthesize(&expected, "\r\n"); + let mismatches = verify_sam(&expected, &synthesized, "self-synth"); + assert!(mismatches.is_empty(), "합성 rsp 대조 실패 {mismatches:?}"); + + let seed: [u8; 32] = Alg::SHA3_256.hash(abc).try_into().unwrap(); + let rows = mct_chain::(seed); + assert!(rows.len() == MCT_COUNTS, "MCT 행 수 불일치"); + let window_len = (1600 - 16 * 32) / (8 * 32) + 1; + let mut chain_seed = seed.to_vec(); + for (count, row) in rows.iter().take(2).enumerate() { + let mut window = vec![chain_seed.clone(); window_len]; + let mut md = Vec::new(); + for _ in 0..MCT_INNER { + let mut msg = Vec::with_capacity(window_len * chain_seed.len()); + for part in &window { + msg.extend_from_slice(part); + } + md = Alg::SHA3_256.hash(&msg); + window.rotate_left(1); + window[window_len - 1] = md.clone(); + } + assert!( + row[0].1 == count.to_string() && row[1].1 == hex_encode(&md), + "MCT COUNT {count} 독립 재계산 불일치" + ); + chain_seed = md; + } + + // KISA 공식 벡터의 Seed 로 COUNT=0 정답 MD 를 KAT 대조합니다 + fn kisa_kat(name: &str, seed_hex: &str, md0_hex: &str) { + let seed: [u8; N] = hex_decode(seed_hex, name).try_into().unwrap(); + let rows = mct_chain::(seed); + assert!( + rows[0][0].1 == "0" && rows[0][1].1 == md0_hex, + "KISA {name} MCT KAT 불일치" + ); + } + kisa_kat::( + "SHA3-224", + "8C08A9BD0B308661ACE3E41E2B83ED8A996DDA04A12B5C9010EA3AE2", + "DA4273B331B22AAB84068D4773C568BB7EABE0E163A598D3BD186DA2", + ); + kisa_kat::( + "SHA3-256", + "99F9C9BB358B28A4ABD395CE170DAE72A1527597FF0FF72A3CB52C50B8B21FFB", + "9F5B796BFC84008F899FA0921226811FD9DA1A94E55C2ED17FD6BE1859C06ACC", + ); + kisa_kat::( + "SHA3-384", + "AAE8B2B7AA3DF27829BFB1AB7D381F146B30370EF56B392B73B35B1BE5D8BBCF88F499DDA7F3C327B45350B8972991EE", + "AE61D961D32BAD53B7D141EA60920F83CB4DFE3C81B92D5D8C1CE95EC44C3CF03F5EE50981904ECA7038E8F28DDA52A1", + ); + kisa_kat::( + "SHA3-512", + "B7D9D3B4D49894BB27AF615B69C2DFFD7315D2A1534FD4C59F7E4D89095C9F1234D1A12CC6B84F33BFE6C0330968584E4F0D3E5E132510DCFB87D6016CD9352A", + "A54435FE431B16E8BFF29CC67C194D4D05BE1AAC5630D732FDF7D514524726B28C0EAC339FF3FEB34804A45257E3090934D6B94CD81200E58322E5547B2D890A", + ); +} From 08d53c9a92a34d5a92e25d1470f3613b52711e24 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:37:36 +0900 Subject: [PATCH 41/65] =?UTF-8?q?aes=20KCMVP=20GCM=20=EB=8C=80=EC=9D=91=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=20-=20=EC=9E=84=EC=9D=98=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20IV=20=EC=9A=A9=20encrypt=5Fwith=5Fiv=20decrypt=5Fwi?= =?UTF-8?q?th=5Fiv=20=EC=99=80=20GCM=5FMIN=5FTAG=5FSIZE=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20GCM=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=2018=2019=20=EC=99=80=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=A0=88=EB=8B=A8=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20aes/tests=20=EC=97=90=20KC?= =?UTF-8?q?MVP=20CBC=20CTR=20GCM=20=ED=95=98=EB=84=A4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20sbox=20=EC=A3=BC=EC=84=9D=20=EA=B3=B5=EB=B0=B1?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/src/gcm.rs | 186 +++++++++++- aes/src/lib.rs | 2 +- aes/src/sbox.rs | 2 +- aes/tests/kcmvp_cbc_ctr.rs | 576 +++++++++++++++++++++++++++++++++++++ aes/tests/kcmvp_gcm.rs | 563 ++++++++++++++++++++++++++++++++++++ 5 files changed, 1322 insertions(+), 7 deletions(-) create mode 100644 aes/tests/kcmvp_cbc_ctr.rs create mode 100644 aes/tests/kcmvp_gcm.rs diff --git a/aes/src/gcm.rs b/aes/src/gcm.rs index 6a979ee..efc4171 100644 --- a/aes/src/gcm.rs +++ b/aes/src/gcm.rs @@ -4,10 +4,12 @@ use constant_time::{Choice, CtEqOps}; use zeroize::{Secret, Zeroize}; pub const GCM_TAG_SIZE: usize = 16; +pub const GCM_MIN_TAG_SIZE: usize = 12; pub const GCM_NONCE_SIZE: usize = 12; const GCM_MAX_INPUT_LEN: u64 = (1 << 36) - 32; const GCM_MAX_AAD_LEN: u64 = (1 << 61) - 1; +const GCM_MAX_IV_LEN: u64 = (1 << 61) - 1; fn inc32(block: &mut [u8; 16]) { let mut carry = 1u16; @@ -62,6 +64,24 @@ impl AES256GCM { j0 } + fn compute_j0_iv(&self, iv: &[u8]) -> [u8; 16] { + if let Ok(nonce) = <&[u8; GCM_NONCE_SIZE]>::try_from(iv) { + return self.compute_j0(nonce); + } + + let mut ghash = GHash::new(self.h.expose()); + ghash.update_padded(iv); + + let mut len_block = [0u8; 16]; + let iv_bits = (iv.len() as u64) * 8; + len_block[8..].copy_from_slice(&iv_bits.to_be_bytes()); + ghash.update(&len_block); + + let j0 = ghash.finalize(); + len_block.zeroize(); + j0 + } + fn compute_tag(&self, aad: &[u8], ciphertext: &[u8], j0: &[u8; 16]) -> [u8; 16] { let mut ghash = GHash::new(self.h.expose()); @@ -101,6 +121,23 @@ impl AES256GCM { ciphertext: &mut [u8], tag: &mut [u8; GCM_TAG_SIZE], ) -> Result<(), Error> { + self.encrypt_with_iv(nonce, aad, plaintext, ciphertext, tag) + } + + pub fn encrypt_with_iv( + &self, + iv: &[u8], + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + tag: &mut [u8], + ) -> Result<(), Error> { + if iv.is_empty() || iv.len() as u64 > GCM_MAX_IV_LEN { + return Err(Error::InvalidLength); + } + if tag.len() < GCM_MIN_TAG_SIZE || tag.len() > GCM_TAG_SIZE { + return Err(Error::InvalidLength); + } if ciphertext.len() < plaintext.len() { return Err(Error::BufferTooSmall); } @@ -111,14 +148,16 @@ impl AES256GCM { return Err(Error::AadTooLong); } - let mut j0 = self.compute_j0(nonce); + let mut j0 = self.compute_j0_iv(iv); let mut icb = j0; inc32(&mut icb); self.gctr(&icb, plaintext, ciphertext); - *tag = self.compute_tag(aad, &ciphertext[..plaintext.len()], &j0); + let mut full_tag = self.compute_tag(aad, &ciphertext[..plaintext.len()], &j0); + tag.copy_from_slice(&full_tag[..tag.len()]); + full_tag.zeroize(); icb.zeroize(); j0.zeroize(); @@ -133,6 +172,23 @@ impl AES256GCM { tag: &[u8; GCM_TAG_SIZE], plaintext: &mut [u8], ) -> Result<(), Error> { + self.decrypt_with_iv(nonce, aad, ciphertext, tag, plaintext) + } + + pub fn decrypt_with_iv( + &self, + iv: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], + plaintext: &mut [u8], + ) -> Result<(), Error> { + if iv.is_empty() || iv.len() as u64 > GCM_MAX_IV_LEN { + return Err(Error::InvalidLength); + } + if tag.len() < GCM_MIN_TAG_SIZE || tag.len() > GCM_TAG_SIZE { + return Err(Error::InvalidLength); + } if plaintext.len() < ciphertext.len() { return Err(Error::BufferTooSmall); } @@ -143,13 +199,13 @@ impl AES256GCM { return Err(Error::AadTooLong); } - let mut j0 = self.compute_j0(nonce); + let mut j0 = self.compute_j0_iv(iv); let mut expected_tag = self.compute_tag(aad, ciphertext, &j0); let mut eq = Choice::from_u8(1); - for i in 0..16 { - eq &= CtEqOps::eq(&tag[i], &expected_tag[i]); + for (given, expected) in tag.iter().zip(expected_tag.iter()) { + eq &= CtEqOps::eq(given, expected); } let authentic = eq.unwrap_u8() == 1; expected_tag.zeroize(); @@ -391,4 +447,124 @@ mod tests { let result = gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag); assert_eq!(result, Err(Error::BufferTooSmall)); } + + fn hex_arr(s: &str) -> [u8; N] { + assert_eq!(s.len(), N * 2); + let mut out = [0u8; N]; + for (i, byte) in out.iter_mut().enumerate() { + let hi = char::from(s.as_bytes()[2 * i]).to_digit(16).unwrap() as u8; + let lo = char::from(s.as_bytes()[2 * i + 1]).to_digit(16).unwrap() as u8; + *byte = (hi << 4) | lo; + } + out + } + + // GCM 스펙 Test Case 17 64비트 IV로 GHASH 기반 J0 경로 커버 + #[test] + fn gcm_test_case_18() { + let key: [u8; 32] = + hex_arr("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308"); + let iv: [u8; 8] = hex_arr("cafebabefacedbad"); + let aad: [u8; 20] = hex_arr("feedfacedeadbeeffeedfacedeadbeefabaddad2"); + let plaintext: [u8; 60] = hex_arr( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + ); + let expected_ciphertext: [u8; 60] = hex_arr( + "c3762df1ca787d32ae47c13bf19844cbaf1ae14d0b976afac52ff7d79bba9de0feb582d33934a4f0954cc2363bc73f7862ac430e64abe499f47c9b1f", + ); + let expected_tag: [u8; 16] = hex_arr("3a337dbf46a792c45e454913fe2ea8f2"); + + let gcm = AES256GCM::new(&key); + let mut ciphertext = [0u8; 60]; + let mut tag = [0u8; 16]; + gcm.encrypt_with_iv(&iv, &aad, &plaintext, &mut ciphertext, &mut tag) + .unwrap(); + assert_eq!(ciphertext, expected_ciphertext); + assert_eq!(tag, expected_tag); + + let mut decrypted = [0u8; 60]; + gcm.decrypt_with_iv(&iv, &aad, &ciphertext, &tag, &mut decrypted) + .unwrap(); + assert_eq!(decrypted, plaintext); + } + + // GCM 스펙 Test Case 18 60바이트 IV로 GHASH J0 패딩 경로 커버 + #[test] + fn gcm_test_case_19() { + let key: [u8; 32] = + hex_arr("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308"); + let iv: [u8; 60] = hex_arr( + "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", + ); + let aad: [u8; 20] = hex_arr("feedfacedeadbeeffeedfacedeadbeefabaddad2"); + let plaintext: [u8; 60] = hex_arr( + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + ); + let expected_ciphertext: [u8; 60] = hex_arr( + "5a8def2f0c9e53f1f75d7853659e2a20eeb2b22aafde6419a058ab4f6f746bf40fc0c3b780f244452da3ebf1c5d82cdea2418997200ef82e44ae7e3f", + ); + let expected_tag: [u8; 16] = hex_arr("a44a8266ee1c8eb0c8b5d4cf5ae9f19a"); + + let gcm = AES256GCM::new(&key); + let mut ciphertext = [0u8; 60]; + let mut tag = [0u8; 16]; + gcm.encrypt_with_iv(&iv, &aad, &plaintext, &mut ciphertext, &mut tag) + .unwrap(); + assert_eq!(ciphertext, expected_ciphertext); + assert_eq!(tag, expected_tag); + + let mut decrypted = [0u8; 60]; + gcm.decrypt_with_iv(&iv, &aad, &ciphertext, &tag, &mut decrypted) + .unwrap(); + assert_eq!(decrypted, plaintext); + } + + // 절단 태그(96~120비트) 왕복과 파라미터 검증 오류 경로 커버 + #[test] + fn gcm_truncated_tag_and_param_validation() { + let key = [0x11u8; 32]; + let iv = [0x22u8; 12]; + let aad: [u8; 0] = []; + let plaintext = [0x33u8; 24]; + + let gcm = AES256GCM::new(&key); + let mut ciphertext = [0u8; 24]; + let mut full_tag = [0u8; 16]; + gcm.encrypt(&iv, &aad, &plaintext, &mut ciphertext, &mut full_tag) + .unwrap(); + + for tag_len in GCM_MIN_TAG_SIZE..=GCM_TAG_SIZE { + let mut ct = [0u8; 24]; + let mut tag = [0u8; 16]; + gcm.encrypt_with_iv(&iv, &aad, &plaintext, &mut ct, &mut tag[..tag_len]) + .unwrap(); + assert_eq!(ct, ciphertext); + assert_eq!(tag[..tag_len], full_tag[..tag_len]); + + let mut pt = [0u8; 24]; + gcm.decrypt_with_iv(&iv, &aad, &ciphertext, &tag[..tag_len], &mut pt) + .unwrap(); + assert_eq!(pt, plaintext); + + let mut bad = tag; + bad[tag_len - 1] ^= 1; + let mut pt2 = [0u8; 24]; + assert_eq!( + gcm.decrypt_with_iv(&iv, &aad, &ciphertext, &bad[..tag_len], &mut pt2), + Err(Error::AuthenticationFailed) + ); + } + + let mut short_tag = [0u8; GCM_MIN_TAG_SIZE - 1]; + let mut ct = [0u8; 24]; + assert_eq!( + gcm.encrypt_with_iv(&iv, &aad, &plaintext, &mut ct, &mut short_tag), + Err(Error::InvalidLength) + ); + let mut pt = [0u8; 24]; + assert_eq!( + gcm.decrypt_with_iv(&[], &aad, &ciphertext, &full_tag, &mut pt), + Err(Error::InvalidLength) + ); + } } diff --git a/aes/src/lib.rs b/aes/src/lib.rs index 66c38d1..b9ce761 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -41,7 +41,7 @@ mod sbox; pub use cbc::{AES256CBC, CBC_IV_SIZE}; pub use ctr::{AES256CTR, CTR_IV_SIZE, CTR_NONCE_SIZE}; -pub use gcm::{AES256GCM, GCM_NONCE_SIZE, GCM_TAG_SIZE}; +pub use gcm::{AES256GCM, GCM_MIN_TAG_SIZE, GCM_NONCE_SIZE, GCM_TAG_SIZE}; pub use ghash::GHash; use block::{decrypt_block, encrypt_block}; diff --git a/aes/src/sbox.rs b/aes/src/sbox.rs index 18f8d03..cf8f767 100644 --- a/aes/src/sbox.rs +++ b/aes/src/sbox.rs @@ -1,6 +1,6 @@ //! AES S-box / inverse S-box 연산을 수행하는 모듈입니다. //! -//! 모든 연산이 AND/XOR/NOT 비트 연산이라 비밀 의존 분기 및 메모리 접근이 없으며 +//! 모든 연산이 AND/XOR/NOT 비트 연산이라 비밀 의존 분기 및 메모리 접근이 없으며 //! 본질적으로 상수-시간입니다. //! //! 16바이트 블록은 비트슬라이스 표현 \[u32; 8\]로 변환해 1회의 BP 회로로 16바이트 diff --git a/aes/tests/kcmvp_cbc_ctr.rs b/aes/tests/kcmvp_cbc_ctr.rs new file mode 100644 index 0000000..189fe94 --- /dev/null +++ b/aes/tests/kcmvp_cbc_ctr.rs @@ -0,0 +1,576 @@ +#![allow(clippy::upper_case_acronyms)] + +use aes::{AES256CBC, AES256CTR}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MCT_COUNTS: usize = 100; +const MCT_INNER: usize = 1000; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Mode { + CNC, + CTR, +} + +impl Mode { + fn iv_name(self) -> &'static str { + match self { + Mode::CNC => "IV", + Mode::CTR => "CTR", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Kind { + KAT, + MMT, + MCT, +} + +#[derive(Default)] +struct RawBlock { + count: String, + fields: Vec<(String, String)>, +} + +impl RawBlock { + fn get(&self, name: &str) -> Option<&str> { + self.fields + .iter() + .find(|(field, _)| field == name) + .map(|(_, value)| value.as_str()) + } +} + +type FieldRow = Vec<(&'static str, String)>; + +fn hex_decode(s: &str, ctx: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn hex_encode(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push( + char::from_digit(u32::from(b >> 4), 16) + .unwrap() + .to_ascii_uppercase(), + ); + out.push( + char::from_digit(u32::from(b & 0x0F), 16) + .unwrap() + .to_ascii_uppercase(), + ); + } + out +} + +fn parse_field(line: &str, block: &mut RawBlock, ctx: &str) { + let (name, value) = line + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 필드 형식 {line:?}")); + let name = name.trim(); + let value = value.trim(); + if name == "COUNT" { + block.count = value.to_string(); + } else { + block.fields.push((name.to_string(), value.to_string())); + } +} + +fn parse_file(content: &str, ctx: &str) -> Vec { + let mut block: Option = None; + let mut blocks = Vec::new(); + for line in content.lines() { + if line.trim().is_empty() { + if let Some(pending) = block.take() { + blocks.push(pending); + } + } else { + parse_field(line, block.get_or_insert_default(), ctx); + } + } + if let Some(pending) = block.take() { + blocks.push(pending); + } + blocks +} + +fn hex_field(block: &RawBlock, name: &str, ctx: &str) -> [u8; N] { + let value = block + .get(name) + .unwrap_or_else(|| panic!("{ctx}: {name} 필드 누락")); + hex_decode(value, &format!("{ctx} {name}")) + .try_into() + .unwrap_or_else(|v: Vec| { + panic!("{ctx}: {name} 길이 {}바이트가 {N}바이트와 다름", v.len()) + }) +} + +fn one_shot(mode: Mode, key: &[u8; 32], iv: &[u8; 16], pt: &[u8], ctx: &str) -> Vec { + let mut ct = vec![0u8; pt.len()]; + match mode { + Mode::CNC => { + AES256CBC::new(key) + .encrypt(iv, pt, &mut ct) + .unwrap_or_else(|e| panic!("{ctx}: CBC 암호화 실패 {e:?}")); + } + Mode::CTR => { + let cipher = AES256CTR::new(key); + let mut counter = u128::from_be_bytes(*iv); + for (pt_chunk, ct_chunk) in pt.chunks(16).zip(ct.chunks_mut(16)) { + cipher + .apply_iv(&counter.to_be_bytes(), pt_chunk, ct_chunk) + .unwrap_or_else(|e| panic!("{ctx}: CTR 암호화 실패 {e:?}")); + counter = counter.wrapping_add(1); + } + } + } + ct +} + +fn mct_chain(mode: Mode, mut key: [u8; 32], mut iv: [u8; 16], mut pt: [u8; 16]) -> Vec { + let mut counts = Vec::with_capacity(MCT_COUNTS); + for _ in 0..MCT_COUNTS { + let mut row = vec![ + ("KEY", hex_encode(&key)), + (mode.iv_name(), hex_encode(&iv)), + ("PT", hex_encode(&pt)), + ]; + let mut ct_prev = [0u8; 16]; + let mut ct = [0u8; 16]; + match mode { + Mode::CNC => { + let cipher = AES256CBC::new(&key); + let mut chain = iv; + let mut block = pt; + for _ in 0..MCT_INNER { + let mut enc = [0u8; 16]; + cipher.encrypt(&chain, &block, &mut enc).unwrap(); + block = chain; + chain = enc; + ct_prev = ct; + ct = enc; + } + iv = ct; + pt = ct_prev; + } + Mode::CTR => { + let cipher = AES256CTR::new(&key); + let mut counter = u128::from_be_bytes(iv); + let mut block = pt; + for _ in 0..MCT_INNER { + let mut enc = [0u8; 16]; + cipher + .apply_iv(&counter.to_be_bytes(), &block, &mut enc) + .unwrap(); + counter = counter.wrapping_add(1); + ct_prev = ct; + ct = enc; + block = enc; + } + iv = counter.to_be_bytes(); + pt = ct; + } + } + row.push(("CT", hex_encode(&ct))); + counts.push(row); + for (dst, src) in key.iter_mut().zip(ct_prev.iter().chain(ct.iter())) { + *dst ^= src; + } + } + counts +} + +fn expected_blocks(mode: Mode, kind: Kind, req_blocks: &[RawBlock], ctx: &str) -> Vec { + match kind { + Kind::KAT | Kind::MMT => req_blocks + .iter() + .enumerate() + .map(|(idx, block)| { + let bctx = format!("{ctx} 블록 {idx}"); + let key: [u8; 32] = hex_field(block, "KEY", &bctx); + let iv: [u8; 16] = hex_field(block, mode.iv_name(), &bctx); + let pt = hex_decode( + block + .get("PT") + .unwrap_or_else(|| panic!("{bctx}: PT 필드 누락")), + &format!("{bctx} PT"), + ); + let ct = one_shot(mode, &key, &iv, &pt, &bctx); + vec![ + ("KEY", hex_encode(&key)), + (mode.iv_name(), hex_encode(&iv)), + ("PT", hex_encode(&pt)), + ("CT", hex_encode(&ct)), + ] + }) + .collect(), + Kind::MCT => { + assert!( + req_blocks.len() == 1, + "{ctx}: MCT req 블록 수 {} (1이어야 함)", + req_blocks.len() + ); + let block = &req_blocks[0]; + mct_chain( + mode, + hex_field(block, "KEY", ctx), + hex_field(block, mode.iv_name(), ctx), + hex_field(block, "PT", ctx), + ) + } + } +} + +fn verify_sam(expected: &[FieldRow], sam_content: &str, kind: Kind, ctx: &str) -> Vec { + let sam_blocks = parse_file(sam_content, ctx); + assert!( + sam_blocks.len() == expected.len(), + "{ctx}: 기대 {}블록 vs sam {}블록", + expected.len(), + sam_blocks.len() + ); + let mut mismatches = Vec::new(); + for (idx, (fields, sam_block)) in expected.iter().zip(sam_blocks.iter()).enumerate() { + if kind == Kind::MCT { + assert!( + sam_block.count == idx.to_string(), + "{ctx}: COUNT 순서 불일치 (sam {} vs 기대 {idx})", + sam_block.count + ); + } + for (name, value) in fields { + let sam_value = sam_block + .get(name) + .unwrap_or_else(|| panic!("{ctx} 블록 {idx}: sam에 {name} 필드 없음")); + if !sam_value.eq_ignore_ascii_case(value) { + mismatches.push(format!( + "블록 {idx} {name}: 계산 {value} vs sam {sam_value}" + )); + } + } + } + mismatches +} + +fn fill_template(template: &str, expected: &[FieldRow], ctx: &str) -> String { + let newline = if template.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + let mut out: Vec = Vec::new(); + let mut idx = 0usize; + let mut saw_field = false; + for line in template.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + if saw_field { + idx += 1; + saw_field = false; + } + out.push(line.to_string()); + continue; + } + saw_field = true; + let (name, value) = trimmed + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 템플릿 라인 {line:?}")); + let name = name.trim(); + let value = value.trim(); + assert!( + idx < expected.len(), + "{ctx}: 템플릿 블록 수가 기대 {}블록 초과", + expected.len() + ); + if name == "COUNT" { + assert!( + value == idx.to_string(), + "{ctx}: 템플릿 COUNT {value}가 순번 {idx}와 다름" + ); + out.push(line.to_string()); + continue; + } + let (_, exp_value) = expected[idx] + .iter() + .find(|(n, _)| *n == name) + .unwrap_or_else(|| panic!("{ctx} 블록 {idx}: 알 수 없는 템플릿 필드 {name}")); + if value == "?" { + out.push(format!("{name} = {exp_value}")); + } else { + assert!( + value.eq_ignore_ascii_case(exp_value), + "{ctx} 블록 {idx}: 템플릿 {name} 값이 req 계산과 불일치" + ); + out.push(line.to_string()); + } + } + let mut sam = out.join(newline); + if template.ends_with('\n') { + sam.push_str(newline); + } + sam +} + +fn synthesize(kind: Kind, expected: &[FieldRow], newline: &str) -> String { + let mut out = String::new(); + for (idx, fields) in expected.iter().enumerate() { + if idx > 0 { + out.push_str(newline); + } + if kind == Kind::MCT { + out.push_str(&format!("COUNT = {idx}{newline}")); + } + for (name, value) in fields { + out.push_str(&format!("{name} = {value}{newline}")); + } + } + out +} + +fn classify(path: &Path) -> Option<(Mode, Kind)> { + let stem = path.file_stem()?.to_str()?.to_ascii_uppercase(); + if !stem.starts_with("AES-256") { + return None; + } + let mode = if stem.contains("(CBC)") { + Mode::CNC + } else if stem.contains("(CTR)") { + Mode::CTR + } else { + return None; + }; + let kind = if stem.ends_with("_KAT") { + Kind::KAT + } else if stem.ends_with("_MMT") { + Kind::MMT + } else if stem.ends_with("_MCT") { + Kind::MCT + } else { + return None; + }; + Some((mode, kind)) +} + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn collect_req_files(dir: &Path, found: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_req_files(&path, found); + } else if path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e.eq_ignore_ascii_case("req")) + && classify(&path).is_some() + { + found.push(path); + } + } +} + +fn report_mismatches(path: &Path, mismatches: &[String], total: usize, failures: &mut Vec) { + eprintln!( + "{} 대조 실패 {}건 (전체 {total} counts)", + path.display(), + mismatches.len() + ); + for m in mismatches.iter().take(10) { + eprintln!(" {m}"); + } + failures.push(format!("{}: {}건 불일치", path.display(), mismatches.len())); +} + +// cavp/ 트리의 AES-256 CBC·CTR KAT/MMT/MCT req 를 계산해 rsp 파일로 출력합니다 (sam 은 배포 템플릿 그대로 두고 레이아웃·대조 기준으로만 사용, 기존 rsp 는 회귀 대조) +#[test] +fn kcmvp_cbc_ctr_req_vs_rsp() { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - KCMVP CBC/CTR 대조 생략"); + return; + }; + let mut req_files = Vec::new(); + collect_req_files(&root, &mut req_files); + req_files.sort(); + if req_files.is_empty() { + eprintln!( + "{}: CBC·CTR req 파일 없음 - KCMVP CBC/CTR 대조 생략", + root.display() + ); + return; + } + + let mut failures = Vec::new(); + for req in &req_files { + let ctx = req.display().to_string(); + let (mode, kind) = classify(req).unwrap(); + let content = fs::read_to_string(req).unwrap_or_else(|e| panic!("{ctx}: 읽기 실패 {e}")); + let req_blocks = parse_file(&content, &ctx); + let expected = expected_blocks(mode, kind, &req_blocks, &ctx); + let sam_path = req.with_extension("sam"); + let rsp_path = req.with_extension("rsp"); + + let mut layout = None; + let mut sam_ok = true; + match fs::read_to_string(&sam_path) { + Ok(sam) if sam.contains("= ?") => layout = Some(sam), + Ok(sam) => { + let mismatches = verify_sam(&expected, &sam, kind, &ctx); + if mismatches.is_empty() { + println!( + "{} 대조 통과 ({} counts)", + sam_path.display(), + expected.len() + ); + layout = Some(sam); + } else { + report_mismatches(&sam_path, &mismatches, expected.len(), &mut failures); + sam_ok = false; + } + } + Err(_) => {} + } + if !sam_ok { + continue; + } + + match fs::read_to_string(&rsp_path) { + Ok(rsp) if !rsp.contains("= ?") => { + let mismatches = verify_sam(&expected, &rsp, kind, &ctx); + if mismatches.is_empty() { + println!( + "{} 대조 통과 ({} counts)", + rsp_path.display(), + expected.len() + ); + } else { + report_mismatches(&rsp_path, &mismatches, expected.len(), &mut failures); + } + } + _ => { + let rsp = match &layout { + Some(template) => fill_template(template, &expected, &ctx), + None => { + let newline = if content.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + synthesize(kind, &expected, newline) + } + }; + fs::write(&rsp_path, rsp).unwrap_or_else(|e| panic!("{ctx}: rsp 쓰기 실패 {e}")); + println!("{} 생성 ({} counts)", rsp_path.display(), expected.len()); + } + } + } + assert!( + failures.is_empty(), + "rsp·sam 대조 실패\n{}", + failures.join("\n") + ); +} + +// SP 800-38A F.2.5·F.5.5 벡터와 내장 템플릿으로 일회 암호화·템플릿 채움·대조·오염 검출 로직을 자가 검증합니다 +#[test] +fn kcmvp_cbc_ctr_self_check() { + let key_hex = "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4"; + let pt_hex = "6BC1BEE22E409F96E93D7E117393172AAE2D8A571E03AC9C9EB76FAC45AF8E5130C81C46A35CE411E5FBC1191A0A52EFF69F2445DF4F9B17AD2B417BE66C3710"; + let key: [u8; 32] = hex_decode(key_hex, "self").try_into().unwrap(); + let pt = hex_decode(pt_hex, "self"); + + let cbc_iv_hex = "000102030405060708090A0B0C0D0E0F"; + let cbc_iv: [u8; 16] = hex_decode(cbc_iv_hex, "self").try_into().unwrap(); + let cbc_ct = one_shot(Mode::CNC, &key, &cbc_iv, &pt, "self-cbc"); + assert!( + hex_encode(&cbc_ct) + == "F58C4C04D6E5F1BA779EABFB5F7BFBD69CFC4E967EDB808D679F777BC6702C7D39F23369A9D9BACFA530E26304231461B2EB05E2C39BE9FCDA6C19078C6A9D1B", + "SP 800-38A F.2.5 불일치" + ); + + let ctr_iv: [u8; 16] = hex_decode("F0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF", "self") + .try_into() + .unwrap(); + let ctr_ct = one_shot(Mode::CTR, &key, &ctr_iv, &pt, "self-ctr"); + assert!( + hex_encode(&ctr_ct) + == "601EC313775789A5B7A7F504BBF3D228F443E3CA4D62B59ACA84E990CACAF5C52B0930DAA23DE94CE87017BA2D84988DDFC9C58DB67AADA613C2DD08457941A6", + "SP 800-38A F.5.5 불일치" + ); + + let block0 = &pt_hex[..32]; + let block1 = &pt_hex[32..64]; + let req = format!( + "KEY = {key_hex}\r\nIV = {cbc_iv_hex}\r\nPT = {block0}\r\n\r\nKEY = {key_hex}\r\nIV = {cbc_iv_hex}\r\nPT = {block1}\r\n\r\n" + ); + let req_blocks = parse_file(&req, "self-kat"); + assert!(req_blocks.len() == 2, "req 파싱 블록 수 불일치"); + let expected = expected_blocks(Mode::CNC, Kind::KAT, &req_blocks, "self-kat"); + + let template = format!( + "KEY = {key_hex}\r\nIV = {cbc_iv_hex}\r\nPT = {block0}\r\nCT = ?\r\n\r\nKEY = {key_hex}\r\nIV = {cbc_iv_hex}\r\nPT = {block1}\r\nCT = ?" + ); + let filled = fill_template(&template, &expected, "self-kat-fill"); + assert!(!filled.contains('?'), "템플릿 미기입 잔존"); + assert!(!filled.ends_with('\n'), "후행 개행 규약 불일치"); + let mismatches = verify_sam(&expected, &filled, Kind::KAT, "self-kat-verify"); + assert!(mismatches.is_empty(), "생성 sam 대조 실패 {mismatches:?}"); + + let first_ct = &expected[0][3].1; + let corrupted_char = if first_ct.ends_with('0') { "1" } else { "0" }; + let corrupted_ct = format!("{}{corrupted_char}", &first_ct[..first_ct.len() - 1]); + let corrupted = filled.replacen(first_ct.as_str(), &corrupted_ct, 1); + let mismatches = verify_sam(&expected, &corrupted, Kind::KAT, "self-kat-corrupt"); + assert!(mismatches.len() == 1, "오염 sam 미검출 {mismatches:?}"); + + let mct_expected: Vec = vec![ + vec![ + ("KEY", "AA".to_string()), + ("IV", "BB".to_string()), + ("PT", "CC".to_string()), + ("CT", "DD".to_string()), + ], + vec![ + ("KEY", "EE".to_string()), + ("IV", "FF".to_string()), + ("PT", "11".to_string()), + ("CT", "22".to_string()), + ], + ]; + let mct_template = "COUNT = 0\r\nKEY = aa\r\nIV = BB\r\nPT = CC\r\nCT = ?\r\n\r\nCOUNT = 1\r\nKEY = ?\r\nIV = ?\r\nPT = ?\r\nCT = ?\r\n"; + let mct_filled = fill_template(mct_template, &mct_expected, "self-mct-fill"); + assert!( + mct_filled + == "COUNT = 0\r\nKEY = aa\r\nIV = BB\r\nPT = CC\r\nCT = DD\r\n\r\nCOUNT = 1\r\nKEY = EE\r\nIV = FF\r\nPT = 11\r\nCT = 22\r\n", + "MCT 템플릿 채움 불일치" + ); + let mismatches = verify_sam(&mct_expected, &mct_filled, Kind::MCT, "self-mct-verify"); + assert!(mismatches.is_empty(), "MCT 대조 실패 {mismatches:?}"); + + let synthesized = synthesize(Kind::MCT, &mct_expected, "\r\n"); + let mismatches = verify_sam(&mct_expected, &synthesized, Kind::MCT, "self-mct-synth"); + assert!(mismatches.is_empty(), "MCT 합성 대조 실패 {mismatches:?}"); +} diff --git a/aes/tests/kcmvp_gcm.rs b/aes/tests/kcmvp_gcm.rs new file mode 100644 index 0000000..b9b45f8 --- /dev/null +++ b/aes/tests/kcmvp_gcm.rs @@ -0,0 +1,563 @@ +use aes::{AES256GCM, Error, GCM_MIN_TAG_SIZE, GCM_TAG_SIZE}; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Copy)] +enum Mode { + AE, + AD, +} + +#[derive(Clone, Copy, Default, PartialEq, Eq)] +struct GroupParams { + key_len: usize, + iv_len: usize, + pt_len: usize, + aad_len: usize, + tag_len: usize, +} + +#[derive(Default)] +struct RawBlock { + count: String, + fields: Vec<(String, String)>, + invalid: bool, +} + +impl RawBlock { + fn get(&self, name: &str) -> Option<&str> { + self.fields + .iter() + .find(|(field, _)| field == name) + .map(|(_, value)| value.as_str()) + } +} + +#[derive(Default)] +struct Stats { + counts: usize, + valid: usize, + invalid: usize, +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn hex_encode(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push( + char::from_digit(u32::from(b >> 4), 16) + .unwrap() + .to_ascii_uppercase(), + ); + out.push( + char::from_digit(u32::from(b & 0x0F), 16) + .unwrap() + .to_ascii_uppercase(), + ); + } + out +} + +fn parse_header(line: &str, params: &mut GroupParams, ctx: &str) { + let inner = line + .trim() + .strip_prefix('[') + .and_then(|rest| rest.strip_suffix(']')) + .unwrap_or_else(|| panic!("{ctx}: 잘못된 헤더 형식 {line:?}")); + let (name, value) = inner + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 헤더 형식 {line:?}")); + let value: usize = value + .trim() + .parse() + .unwrap_or_else(|_| panic!("{ctx}: 헤더 값이 정수가 아님 {line:?}")); + match name.trim().to_ascii_lowercase().as_str() { + "keylen" => params.key_len = value, + "ivlen" => params.iv_len = value, + "ptlen" => params.pt_len = value, + "aadlen" => params.aad_len = value, + "taglen" => params.tag_len = value, + _ => panic!("{ctx}: 알 수 없는 헤더 {line:?}"), + } +} + +fn parse_field(line: &str, block: &mut RawBlock, ctx: &str) { + if line.trim().eq_ignore_ascii_case("invalid") { + block.invalid = true; + return; + } + let (name, value) = line + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 필드 형식 {line:?}")); + let name = name.trim(); + let value = value.trim(); + if name == "COUNT" { + block.count = value.to_string(); + } else { + block.fields.push((name.to_string(), value.to_string())); + } +} + +fn parse_file(content: &str, ctx: &str) -> Vec<(GroupParams, RawBlock)> { + let mut params = GroupParams::default(); + let mut block: Option = None; + let mut blocks = Vec::new(); + + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + if let Some(pending) = block.take() { + blocks.push((params, pending)); + } + } else if trimmed.starts_with('[') { + assert!(block.is_none(), "{ctx}: COUNT 블록 도중 헤더 등장 {line:?}"); + parse_header(line, &mut params, ctx); + } else { + parse_field(line, block.get_or_insert_default(), ctx); + } + } + if let Some(pending) = block.take() { + blocks.push((params, pending)); + } + blocks +} + +fn hex_field(block: &RawBlock, name: &str, bits: usize, ctx: &str) -> Vec { + let value = block + .get(name) + .unwrap_or_else(|| panic!("{ctx}: {name} 필드 누락")); + let bytes = hex_decode(value, &format!("{ctx} {name}")); + assert!( + bytes.len() * 8 == bits, + "{ctx}: {name} 길이 {}비트가 선언된 {bits}비트와 다름", + bytes.len() * 8 + ); + bytes +} + +fn compute_answers( + mode: Mode, + params: GroupParams, + block: &RawBlock, + ctx: &str, + stats: &mut Stats, +) -> Vec<(&'static str, String)> { + let ctx = format!("{ctx} COUNT = {}", block.count); + assert!( + params.key_len == 256, + "{ctx}: 지원하지 않는 KeyLen {}", + params.key_len + ); + assert!( + (GCM_MIN_TAG_SIZE * 8..=GCM_TAG_SIZE * 8).contains(¶ms.tag_len) + && params.tag_len.is_multiple_of(8), + "{ctx}: 지원하지 않는 TagLen {}", + params.tag_len + ); + + let key: [u8; 32] = hex_field(block, "Key", params.key_len, &ctx) + .try_into() + .unwrap(); + let iv = hex_field(block, "IV", params.iv_len, &ctx); + let aad = hex_field(block, "Adata", params.aad_len, &ctx); + let gcm = AES256GCM::new(&key); + stats.counts += 1; + + match mode { + Mode::AE => { + let pt = hex_field(block, "PT", params.pt_len, &ctx); + let mut ct = vec![0u8; pt.len()]; + let mut tag = vec![0u8; params.tag_len / 8]; + gcm.encrypt_with_iv(&iv, &aad, &pt, &mut ct, &mut tag) + .unwrap_or_else(|e| panic!("{ctx}: 암호화 실패 {e:?}")); + vec![("C", hex_encode(&ct)), ("T", hex_encode(&tag))] + } + Mode::AD => { + let ct = hex_field(block, "C", params.pt_len, &ctx); + let tag = hex_field(block, "T", params.tag_len, &ctx); + let mut pt = vec![0u8; ct.len()]; + match gcm.decrypt_with_iv(&iv, &aad, &ct, &tag, &mut pt) { + Ok(()) => { + stats.valid += 1; + vec![("PT", hex_encode(&pt))] + } + Err(Error::AuthenticationFailed) => { + stats.invalid += 1; + vec![("Invalid", String::new())] + } + Err(e) => panic!("{ctx}: 복호화 실패 {e:?}"), + } + } + } +} + +fn verify_sam(req_content: &str, sam_content: &str, mode: Mode, ctx: &str) -> (Stats, Vec) { + let req_blocks = parse_file(req_content, ctx); + let sam_blocks = parse_file(sam_content, ctx); + assert!( + req_blocks.len() == sam_blocks.len(), + "{ctx}: req {}블록 vs sam {}블록", + req_blocks.len(), + sam_blocks.len() + ); + + let mut stats = Stats::default(); + let mut mismatches = Vec::new(); + + for ((req_params, req_block), (sam_params, sam_block)) in + req_blocks.iter().zip(sam_blocks.iter()) + { + assert!( + req_block.count == sam_block.count, + "{ctx}: COUNT 순서 불일치 (req {} vs sam {})", + req_block.count, + sam_block.count + ); + assert!( + req_params == sam_params, + "{ctx} COUNT = {}: req/sam 헤더 파라미터 불일치", + req_block.count + ); + assert!( + req_block.get("Key") == sam_block.get("Key"), + "{ctx} COUNT = {}: req/sam Key 불일치", + req_block.count + ); + + for (name, computed) in compute_answers(mode, *req_params, req_block, ctx, &mut stats) { + if name == "Invalid" { + if !sam_block.invalid { + mismatches.push(format!( + "COUNT = {} PT: 계산 Invalid vs sam {}", + req_block.count, + sam_block.get("PT").unwrap_or("(없음)") + )); + } + continue; + } + if name == "PT" && sam_block.invalid { + mismatches.push(format!( + "COUNT = {} PT: 계산 {computed} vs sam Invalid", + req_block.count + )); + continue; + } + let expected = sam_block.get(name).unwrap_or_else(|| { + panic!("{ctx} COUNT = {}: sam에 {name} 필드 없음", req_block.count) + }); + if !expected.eq_ignore_ascii_case(&computed) { + mismatches.push(format!( + "COUNT = {} {name}: 계산 {computed} vs sam {expected}", + req_block.count + )); + } + } + } + (stats, mismatches) +} + +fn fill_req(content: &str, mode: Mode, ctx: &str) -> (String, Stats) { + let newline = if content.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + let mut out: Vec = Vec::new(); + let mut params = GroupParams::default(); + let mut block: Option = None; + let mut stats = Stats::default(); + + let flush = |block: &mut Option, + out: &mut Vec, + stats: &mut Stats, + params: GroupParams| { + if let Some(pending) = block.take() { + for (name, value) in compute_answers(mode, params, &pending, ctx, stats) { + if name == "Invalid" { + out.push("Invalid".to_string()); + } else { + out.push(format!("{name} = {value}")); + } + } + } + }; + + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + flush(&mut block, &mut out, &mut stats, params); + out.push(line.to_string()); + } else if trimmed.starts_with('[') { + assert!(block.is_none(), "{ctx}: COUNT 블록 도중 헤더 등장 {line:?}"); + parse_header(line, &mut params, ctx); + out.push(line.to_string()); + } else { + out.push(line.to_string()); + parse_field(line, block.get_or_insert_default(), ctx); + } + } + flush(&mut block, &mut out, &mut stats, params); + + let mut sam = out.join(newline); + if content.ends_with('\n') { + sam.push_str(newline); + } + (sam, stats) +} + +fn detect_mode(path: &Path, content: &str) -> Mode { + let stem = path + .file_stem() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_ascii_uppercase(); + if stem.ends_with("_AE") { + return Mode::AE; + } + if stem.ends_with("_AD") { + return Mode::AD; + } + if content + .lines() + .any(|line| line.trim_start().starts_with("T =")) + { + Mode::AD + } else { + Mode::AE + } +} + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn collect_gcm_req_files(dir: &Path, found: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_gcm_req_files(&path, found); + } else if let Some(name) = path.file_name().and_then(|n| n.to_str()) + && name.to_ascii_uppercase().starts_with("GCM") + && name.to_ascii_lowercase().ends_with(".req") + { + found.push(path); + } + } +} + +fn describe(mode: Mode, stats: &Stats) -> String { + match mode { + Mode::AE => format!("{} counts", stats.counts), + Mode::AD => format!( + "{} counts, valid {}, invalid {}", + stats.counts, stats.valid, stats.invalid + ), + } +} + +fn flip_last_hex(hex: &str) -> String { + let mut flipped = hex.to_string(); + let last = if flipped.ends_with('0') { "1" } else { "0" }; + flipped.replace_range(flipped.len() - 1.., last); + flipped +} + +fn report_mismatches( + path: &Path, + mismatches: &[String], + summary: &str, + failures: &mut Vec, +) { + eprintln!( + "{} 대조 실패 {}건 (전체 {summary})", + path.display(), + mismatches.len() + ); + for m in mismatches.iter().take(10) { + eprintln!(" {m}"); + } + failures.push(format!("{}: {}건 불일치", path.display(), mismatches.len())); +} + +// cavp/ 트리의 GCM req 를 계산해 rsp 파일로 출력합니다 (sam 은 답이 기입된 경우 대조 기준, 기존 rsp 는 회귀 대조) +#[test] +fn kcmvp_gcm_req_vs_rsp() { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - KCMVP GCM 대조 생략"); + return; + }; + let mut req_files = Vec::new(); + collect_gcm_req_files(&root, &mut req_files); + req_files.sort(); + if req_files.is_empty() { + eprintln!( + "{}: GCM req 파일 없음 - KCMVP GCM 대조 생략", + root.display() + ); + return; + } + + let mut failures = Vec::new(); + for req in &req_files { + let ctx = req.display().to_string(); + let content = fs::read_to_string(req).unwrap_or_else(|e| panic!("{ctx}: 읽기 실패 {e}")); + let mode = detect_mode(req, &content); + let sam_path = req.with_extension("sam"); + let rsp_path = req.with_extension("rsp"); + + let mut sam_ok = true; + if let Ok(sam) = fs::read_to_string(&sam_path) + && !sam.contains("= ?") + { + let (stats, mismatches) = verify_sam(&content, &sam, mode, &ctx); + if mismatches.is_empty() { + println!( + "{} 대조 통과 ({})", + sam_path.display(), + describe(mode, &stats) + ); + } else { + report_mismatches( + &sam_path, + &mismatches, + &describe(mode, &stats), + &mut failures, + ); + sam_ok = false; + } + } + if !sam_ok { + continue; + } + + match fs::read_to_string(&rsp_path) { + Ok(rsp) if !rsp.contains("= ?") => { + let (stats, mismatches) = verify_sam(&content, &rsp, mode, &ctx); + if mismatches.is_empty() { + println!( + "{} 대조 통과 ({})", + rsp_path.display(), + describe(mode, &stats) + ); + } else { + report_mismatches( + &rsp_path, + &mismatches, + &describe(mode, &stats), + &mut failures, + ); + } + } + _ => { + let (rsp, stats) = fill_req(&content, mode, &ctx); + fs::write(&rsp_path, rsp).unwrap_or_else(|e| panic!("{ctx}: rsp 쓰기 실패 {e}")); + println!("{} 생성 ({})", rsp_path.display(), describe(mode, &stats)); + } + } + } + assert!( + failures.is_empty(), + "rsp·sam 대조 실패\n{}", + failures.join("\n") + ); +} + +// 내장 벡터로 req 파싱과 rsp 생성·대조 로직을 자가 검증합니다 +#[test] +fn kcmvp_gcm_self_check() { + let key_hex = "FEFFE9928665731C6D6A8F9467308308FEFFE9928665731C6D6A8F9467308308"; + let iv_hex = "CAFEBABEFACEDBADDECAF888"; + let aad_hex = "FEEDFACEDEADBEEFFEEDFACEDEADBEEFABADDAD2"; + let pt_hex = "D9313225F88406E5A55909C5AFF5269A86A7A9531534F7DA2E4C303D8A318A721C3C0C95956809532FCF0E2449A6B525B16AEDF5AA0DE657BA637B39"; + + let key: [u8; 32] = hex_decode(key_hex, "self").try_into().unwrap(); + let iv = hex_decode(iv_hex, "self"); + let aad = hex_decode(aad_hex, "self"); + let pt = hex_decode(pt_hex, "self"); + let gcm = AES256GCM::new(&key); + let mut ct = vec![0u8; pt.len()]; + let mut tag = vec![0u8; 14]; + gcm.encrypt_with_iv(&iv, &aad, &pt, &mut ct, &mut tag) + .unwrap(); + let ct_hex = hex_encode(&ct); + let tag_hex = hex_encode(&tag); + + let ae_req = format!( + "[KeyLen = 256]\r\n[IVLen = 96]\r\n[PTLen = 480]\r\n[AADLen = 160]\r\n[TagLen = 112]\r\n\r\nCOUNT = 0\r\nKey = {key_hex}\r\nIV = {iv_hex}\r\nPT = {pt_hex}\r\nAdata = {aad_hex}\r\n" + ); + let (ae_sam, ae_stats) = fill_req(&ae_req, Mode::AE, "self-ae"); + let ae_expected = format!("{ae_req}C = {ct_hex}\r\nT = {tag_hex}\r\n"); + assert_eq!(ae_sam, ae_expected, "AE sam 형식 불일치"); + assert!(ae_stats.counts == 1); + + let (_, mismatches) = verify_sam(&ae_req, &ae_sam, Mode::AE, "self-ae-verify"); + assert!(mismatches.is_empty(), "생성 sam 대조 실패 {mismatches:?}"); + + let corrupted_sam = ae_sam.replace( + &format!("T = {tag_hex}"), + &format!("T = {}", flip_last_hex(&tag_hex)), + ); + let (_, mismatches) = verify_sam(&ae_req, &corrupted_sam, Mode::AE, "self-ae-corrupt"); + assert!(mismatches.len() == 1, "오염 sam 미검출 {mismatches:?}"); + + let bad_tag_hex = flip_last_hex(&tag_hex); + let ad_req = format!( + "[KeyLen = 256]\r\n[IVlen = 96]\r\n[PTLen = 480]\r\n[AADLen = 160]\r\n[TagLen = 112]\r\n\r\nCOUNT = 0\r\nKey = {key_hex}\r\nIV = {iv_hex}\r\nAdata = {aad_hex}\r\nC = {ct_hex}\r\nT = {tag_hex}\r\n\r\nCOUNT = 1\r\nKey = {key_hex}\r\nIV = {iv_hex}\r\nAdata = {aad_hex}\r\nC = {ct_hex}\r\nT = {bad_tag_hex}\r\n" + ); + let (ad_sam, ad_stats) = fill_req(&ad_req, Mode::AD, "self-ad"); + assert!( + ad_sam.contains(&format!("T = {tag_hex}\r\nPT = {pt_hex}\r\n")), + "AD 유효 응답 불일치" + ); + assert!( + ad_sam.contains(&format!("T = {bad_tag_hex}\r\nInvalid\r\n")), + "AD Invalid 응답 불일치" + ); + assert!( + !ad_sam.contains("PT = Invalid"), + "AD Invalid 응답에 PT 접두어 잔존" + ); + assert!(ad_stats.counts == 2 && ad_stats.valid == 1 && ad_stats.invalid == 1); + + let (_, mismatches) = verify_sam(&ad_req, &ad_sam, Mode::AD, "self-ad-verify"); + assert!(mismatches.is_empty(), "AD sam 대조 실패 {mismatches:?}"); + + let swapped_sam = ad_sam.replacen("Invalid", &format!("PT = {pt_hex}"), 1); + let (_, mismatches) = verify_sam(&ad_req, &swapped_sam, Mode::AD, "self-ad-swap"); + assert!(mismatches.len() == 1, "Invalid 오염 미검출 {mismatches:?}"); + + let swapped_valid = ad_sam.replacen(&format!("PT = {pt_hex}"), "Invalid", 1); + let (_, mismatches) = verify_sam(&ad_req, &swapped_valid, Mode::AD, "self-ad-swap-valid"); + assert!( + mismatches.len() == 1, + "유효 카운트 Invalid 오염 미검출 {mismatches:?}" + ); + + let empty_req = "[KeyLen = 256]\r\n[IVLen = 96]\r\n[PTLen = 0]\r\n[AADLen = 0]\r\n[TagLen = 128]\r\n\r\nCOUNT = 0\r\nKey = D6D04FFE1C2CF03660E19639B7FB28235E17E307AC7FF534459647B791779A21\r\nIV = 6CD9D58A1F369DD58F2C2736\r\nPT = \r\nAdata = \r\n"; + let (empty_sam, _) = fill_req(empty_req, Mode::AE, "self-empty"); + assert!( + empty_sam.contains("Adata = \r\nC = \r\nT = "), + "빈 PT 응답 형식 불일치" + ); +} From c73b431dc8c1299810989394fbba5b3f91ca1afe Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:37:43 +0900 Subject: [PATCH 42/65] =?UTF-8?q?sha2=20KCMVP=20=ED=95=98=EB=84=A4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sha2/tests/kcmvp_sha2.rs | 571 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 571 insertions(+) create mode 100644 sha2/tests/kcmvp_sha2.rs diff --git a/sha2/tests/kcmvp_sha2.rs b/sha2/tests/kcmvp_sha2.rs new file mode 100644 index 0000000..59b3f67 --- /dev/null +++ b/sha2/tests/kcmvp_sha2.rs @@ -0,0 +1,571 @@ +#![allow(clippy::upper_case_acronyms)] + +use sha2::{SHA2, SHA224, SHA256, SHA384, SHA512}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MCT_COUNTS: usize = 100; +const MCT_INNER: usize = 1000; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Alg { + SHA224, + SHA256, + SHA384, + SHA512, +} + +impl Alg { + fn digest_len(self) -> usize { + match self { + Alg::SHA224 => 28, + Alg::SHA256 => 32, + Alg::SHA384 => 48, + Alg::SHA512 => 64, + } + } + + fn hash(self, msg: &[u8]) -> Vec { + match self { + Alg::SHA224 => one_shot::(msg), + Alg::SHA256 => one_shot::(msg), + Alg::SHA384 => one_shot::(msg), + Alg::SHA512 => one_shot::(msg), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Kind { + LMT, + SMT, + MCT, +} + +#[derive(Default)] +struct RawBlock { + l: String, + len: String, + fields: Vec<(String, String)>, +} + +impl RawBlock { + fn get(&self, name: &str) -> Option<&str> { + let special = match name { + "L" => Some(self.l.as_str()), + "Len" => Some(self.len.as_str()), + _ => None, + }; + if let Some(value) = special { + return (!value.is_empty()).then_some(value); + } + self.fields + .iter() + .find(|(field, _)| field == name) + .map(|(_, value)| value.as_str()) + } +} + +type FieldRow = Vec<(&'static str, String)>; + +fn hex_decode(s: &str, ctx: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn hex_encode(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push( + char::from_digit(u32::from(b >> 4), 16) + .unwrap() + .to_ascii_uppercase(), + ); + out.push( + char::from_digit(u32::from(b & 0x0F), 16) + .unwrap() + .to_ascii_uppercase(), + ); + } + out +} + +fn parse_field(line: &str, block: &mut RawBlock, ctx: &str) { + let (name, value) = line + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 필드 형식 {line:?}")); + let name = name.trim(); + let value = value.trim(); + if name == "L" { + block.l = value.to_string(); + } else if name == "Len" { + block.len = value.to_string(); + } else { + block.fields.push((name.to_string(), value.to_string())); + } +} + +fn parse_file(content: &str, ctx: &str) -> Vec { + let mut block: Option = None; + let mut blocks = Vec::new(); + for line in content.lines() { + if line.trim().is_empty() { + if let Some(pending) = block.take() { + blocks.push(pending); + } + } else { + parse_field(line, block.get_or_insert_default(), ctx); + } + } + if let Some(pending) = block.take() { + blocks.push(pending); + } + blocks +} + +fn hex_field(block: &RawBlock, name: &str, ctx: &str) -> [u8; N] { + let value = block + .get(name) + .unwrap_or_else(|| panic!("{ctx}: {name} 필드 누락")); + hex_decode(value, &format!("{ctx} {name}")) + .try_into() + .unwrap_or_else(|v: Vec| { + panic!("{ctx}: {name} 길이 {}바이트가 {N}바이트와 다름", v.len()) + }) +} + +fn one_shot(msg: &[u8]) -> Vec { + let mut hasher = H::new(); + hasher.update(msg); + hasher.finalize().as_bytes().to_vec() +} + +fn mct_chain(seed: [u8; N]) -> Vec { + let mut seed = seed; + let mut rows = Vec::with_capacity(MCT_COUNTS); + for count in 0..MCT_COUNTS { + let mut md0 = seed; + let mut md1 = seed; + let mut md2 = seed; + for _ in 0..MCT_INNER { + let mut hasher = H::new(); + hasher.update(&md0); + hasher.update(&md1); + hasher.update(&md2); + let digest = hasher.finalize(); + md0 = md1; + md1 = md2; + md2 = digest + .as_bytes() + .try_into() + .unwrap_or_else(|_| panic!("MCT 다이제스트 길이가 {N}바이트와 다름")); + } + rows.push(vec![("COUNT", count.to_string()), ("MD", hex_encode(&md2))]); + seed = md2; + } + rows +} + +fn expected_blocks(alg: Alg, kind: Kind, req_blocks: &[RawBlock], ctx: &str) -> Vec { + assert!(!req_blocks.is_empty(), "{ctx}: req 블록 없음"); + let header = &req_blocks[0]; + assert!( + header.len.is_empty() && header.fields.is_empty(), + "{ctx}: L 헤더 블록에 잉여 필드" + ); + assert!( + header.l == alg.digest_len().to_string(), + "{ctx}: L 값 {}이 기대 {}와 다름", + header.l, + alg.digest_len() + ); + let mut rows = vec![vec![("L", header.l.clone())]]; + match kind { + Kind::LMT | Kind::SMT => { + for (idx, block) in req_blocks[1..].iter().enumerate() { + let bctx = format!("{ctx} 블록 {idx}"); + assert!(!block.len.is_empty(), "{bctx}: Len 필드 누락"); + let bits: usize = block + .len + .parse() + .unwrap_or_else(|_| panic!("{bctx}: Len 파싱 실패")); + assert!( + bits.is_multiple_of(8), + "{bctx}: Len {bits}이 8의 배수가 아님" + ); + let msg_hex = block + .get("Msg") + .unwrap_or_else(|| panic!("{bctx}: Msg 필드 누락")); + let msg = hex_decode(msg_hex, &format!("{bctx} Msg")); + let body = if bits == 0 { + assert!(msg == [0u8], "{bctx}: Len 0 인데 Msg 가 00 이 아님"); + &[][..] + } else { + assert!( + msg.len() * 8 == bits, + "{bctx}: Msg 길이 {}비트가 Len {bits}과 다름", + msg.len() * 8 + ); + &msg[..] + }; + let md = alg.hash(body); + rows.push(vec![ + ("Len", block.len.clone()), + ("Msg", msg_hex.to_string()), + ("MD", hex_encode(&md)), + ]); + } + } + Kind::MCT => { + assert!( + req_blocks.len() == 2, + "{ctx}: MCT req 블록 수 {} (2여야 함)", + req_blocks.len() + ); + let block = &req_blocks[1]; + let seed_hex = block + .get("Seed") + .unwrap_or_else(|| panic!("{ctx}: Seed 필드 누락")); + rows.push(vec![("Seed", seed_hex.to_string())]); + let chain = match alg { + Alg::SHA224 => mct_chain::(hex_field(block, "Seed", ctx)), + Alg::SHA256 => mct_chain::(hex_field(block, "Seed", ctx)), + Alg::SHA384 => mct_chain::(hex_field(block, "Seed", ctx)), + Alg::SHA512 => mct_chain::(hex_field(block, "Seed", ctx)), + }; + rows.extend(chain); + } + } + rows +} + +fn verify_sam(expected: &[FieldRow], sam_content: &str, ctx: &str) -> Vec { + let sam_blocks = parse_file(sam_content, ctx); + assert!( + sam_blocks.len() == expected.len(), + "{ctx}: 기대 {}블록 vs 파일 {}블록", + expected.len(), + sam_blocks.len() + ); + let mut mismatches = Vec::new(); + for (idx, (fields, sam_block)) in expected.iter().zip(sam_blocks.iter()).enumerate() { + for (name, value) in fields { + let sam_value = sam_block + .get(name) + .unwrap_or_else(|| panic!("{ctx} 블록 {idx}: 파일에 {name} 필드 없음")); + if !sam_value.eq_ignore_ascii_case(value) { + mismatches.push(format!( + "블록 {idx} {name}: 계산 {value} vs 파일 {sam_value}" + )); + } + } + } + mismatches +} + +fn fill_template(template: &str, expected: &[FieldRow], ctx: &str) -> String { + let newline = if template.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + let mut out: Vec = Vec::new(); + let mut idx = 0usize; + let mut saw_field = false; + for line in template.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + if saw_field { + idx += 1; + saw_field = false; + } + out.push(line.to_string()); + continue; + } + saw_field = true; + let (name, value) = trimmed + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 템플릿 라인 {line:?}")); + let name = name.trim(); + let value = value.trim(); + assert!( + idx < expected.len(), + "{ctx}: 템플릿 블록 수가 기대 {}블록 초과", + expected.len() + ); + let (_, exp_value) = expected[idx] + .iter() + .find(|(n, _)| *n == name) + .unwrap_or_else(|| panic!("{ctx} 블록 {idx}: 알 수 없는 템플릿 필드 {name}")); + if value == "?" { + out.push(format!("{name} = {exp_value}")); + } else { + assert!( + value.eq_ignore_ascii_case(exp_value), + "{ctx} 블록 {idx}: 템플릿 {name} 값이 req 계산과 불일치" + ); + out.push(line.to_string()); + } + } + let mut rsp = out.join(newline); + if template.ends_with('\n') { + rsp.push_str(newline); + } + rsp +} + +fn synthesize(expected: &[FieldRow], newline: &str) -> String { + let mut out = String::new(); + for (idx, fields) in expected.iter().enumerate() { + if idx > 0 { + out.push_str(newline); + } + for (name, value) in fields { + out.push_str(&format!("{name} = {value}{newline}")); + } + } + out +} + +fn classify(path: &Path) -> Option<(Alg, Kind)> { + let stem = path.file_stem()?.to_str()?.to_ascii_uppercase(); + let alg = if stem.starts_with("SHA2-224") { + Alg::SHA224 + } else if stem.starts_with("SHA2-256") { + Alg::SHA256 + } else if stem.starts_with("SHA2-384") { + Alg::SHA384 + } else if stem.starts_with("SHA2-512") { + Alg::SHA512 + } else { + return None; + }; + let kind = if stem.ends_with("_LMT") { + Kind::LMT + } else if stem.ends_with("_SMT") { + Kind::SMT + } else if stem.ends_with("_MCT") { + Kind::MCT + } else { + return None; + }; + Some((alg, kind)) +} + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn collect_req_files(dir: &Path, found: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_req_files(&path, found); + } else if path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e.eq_ignore_ascii_case("req")) + && classify(&path).is_some() + { + found.push(path); + } + } +} + +fn report_mismatches(path: &Path, mismatches: &[String], total: usize, failures: &mut Vec) { + eprintln!( + "{} 대조 실패 {}건 (전체 {total} 블록)", + path.display(), + mismatches.len() + ); + for m in mismatches.iter().take(10) { + eprintln!(" {m}"); + } + failures.push(format!("{}: {}건 불일치", path.display(), mismatches.len())); +} + +// cavp/ 트리의 SHA2 LMT/SMT/MCT req 를 계산해 rsp 파일로 출력합니다 (sam 은 배포 템플릿 그대로 두고 레이아웃·대조 기준으로만 사용, 기존 rsp 는 회귀 대조) +#[test] +fn kcmvp_sha2_req_vs_rsp() { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - KCMVP SHA2 대조 생략"); + return; + }; + let mut req_files = Vec::new(); + collect_req_files(&root, &mut req_files); + req_files.sort(); + if req_files.is_empty() { + eprintln!( + "{}: SHA2 req 파일 없음 - KCMVP SHA2 대조 생략", + root.display() + ); + return; + } + + let mut failures = Vec::new(); + for req in &req_files { + let ctx = req.display().to_string(); + let (alg, kind) = classify(req).unwrap(); + let content = fs::read_to_string(req).unwrap_or_else(|e| panic!("{ctx}: 읽기 실패 {e}")); + let req_blocks = parse_file(&content, &ctx); + let expected = expected_blocks(alg, kind, &req_blocks, &ctx); + let sam_path = req.with_extension("sam"); + let rsp_path = req.with_extension("rsp"); + + let mut layout = None; + let mut sam_ok = true; + match fs::read_to_string(&sam_path) { + Ok(sam) if sam.contains("= ?") => layout = Some(sam), + Ok(sam) => { + let mismatches = verify_sam(&expected, &sam, &ctx); + if mismatches.is_empty() { + println!("{} 대조 통과 ({} 블록)", sam_path.display(), expected.len()); + layout = Some(sam); + } else { + report_mismatches(&sam_path, &mismatches, expected.len(), &mut failures); + sam_ok = false; + } + } + Err(_) => {} + } + if !sam_ok { + continue; + } + + match fs::read_to_string(&rsp_path) { + Ok(rsp) if !rsp.contains("= ?") => { + let mismatches = verify_sam(&expected, &rsp, &ctx); + if mismatches.is_empty() { + println!("{} 대조 통과 ({} 블록)", rsp_path.display(), expected.len()); + } else { + report_mismatches(&rsp_path, &mismatches, expected.len(), &mut failures); + } + } + _ => { + let rsp = match &layout { + Some(template) => fill_template(template, &expected, &ctx), + None => { + let newline = if content.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + synthesize(&expected, newline) + } + }; + fs::write(&rsp_path, rsp).unwrap_or_else(|e| panic!("{ctx}: rsp 쓰기 실패 {e}")); + println!("{} 생성 ({} 블록)", rsp_path.display(), expected.len()); + } + } + } + assert!( + failures.is_empty(), + "rsp·sam 대조 실패\n{}", + failures.join("\n") + ); +} + +// FIPS 180-4 확정 벡터와 내장 템플릿으로 해시 계산·MCT 체인·템플릿 채움·대조·오염 검출 로직을 자가 검증합니다 +#[test] +fn kcmvp_sha2_self_check() { + let abc = b"abc"; + assert!( + hex_encode(&Alg::SHA224.hash(abc)) + == "23097D223405D8228642A477BDA255B32AADBCE4BDA0B3F7E36C9DA7", + "FIPS 180-4 SHA-224 불일치" + ); + assert!( + hex_encode(&Alg::SHA256.hash(abc)) + == "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD", + "FIPS 180-4 SHA-256 불일치" + ); + assert!( + hex_encode(&Alg::SHA384.hash(abc)) + == "CB00753F45A35E8BB5A03D699AC65007272C32AB0EDED1631A8B605A43FF5BED8086072BA1E7CC2358BAECA134C825A7", + "FIPS 180-4 SHA-384 불일치" + ); + assert!( + hex_encode(&Alg::SHA512.hash(abc)) + == "DDAF35A193617ABACC417349AE20413112E6FA4E89A97EA20A9EEEE64B55D39A2192992A274FC1A836BA3C23A3FEEBBD454D4423643CE80E2A9AC94FA54CA49F", + "FIPS 180-4 SHA-512 불일치" + ); + assert!( + hex_encode(&Alg::SHA256.hash(&[])) + == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "SHA-256 빈 메시지 불일치" + ); + + let req = "L = 32\r\n\r\nLen = 24\r\nMsg = 616263\r\n\r\nLen = 0\r\nMsg = 00\r\n\r\n"; + let req_blocks = parse_file(req, "self-smt"); + assert!(req_blocks.len() == 3, "req 파싱 블록 수 불일치"); + let expected = expected_blocks(Alg::SHA256, Kind::SMT, &req_blocks, "self-smt"); + assert!(expected.len() == 3, "기대 블록 수 불일치"); + assert!( + expected[1][2].1 == "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD", + "SMT abc MD 불일치" + ); + assert!( + expected[2][2].1 == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855", + "SMT Len 0 MD 불일치" + ); + + let template = + "L = 32\r\n\r\nLen = 24\r\nMsg = 616263\r\nMD = ?\r\n\r\nLen = 0\r\nMsg = 00\r\nMD = ?"; + let filled = fill_template(template, &expected, "self-fill"); + assert!(!filled.contains('?'), "템플릿 미기입 잔존"); + assert!(!filled.ends_with('\n'), "후행 개행 규약 불일치"); + let mismatches = verify_sam(&expected, &filled, "self-verify"); + assert!(mismatches.is_empty(), "생성 rsp 대조 실패 {mismatches:?}"); + + let first_md = &expected[1][2].1; + let corrupted_char = if first_md.ends_with('0') { "1" } else { "0" }; + let corrupted_md = format!("{}{corrupted_char}", &first_md[..first_md.len() - 1]); + let corrupted = filled.replacen(first_md.as_str(), &corrupted_md, 1); + let mismatches = verify_sam(&expected, &corrupted, "self-corrupt"); + assert!(mismatches.len() == 1, "오염 rsp 미검출 {mismatches:?}"); + + let synthesized = synthesize(&expected, "\r\n"); + let mismatches = verify_sam(&expected, &synthesized, "self-synth"); + assert!(mismatches.is_empty(), "합성 rsp 대조 실패 {mismatches:?}"); + + let seed: [u8; 32] = Alg::SHA256.hash(abc).try_into().unwrap(); + let rows = mct_chain::(seed); + assert!(rows.len() == MCT_COUNTS, "MCT 행 수 불일치"); + let mut chain_seed = seed.to_vec(); + for (count, row) in rows.iter().take(2).enumerate() { + let mut window = [chain_seed.clone(), chain_seed.clone(), chain_seed.clone()]; + let mut md = Vec::new(); + for _ in 0..MCT_INNER { + let mut msg = Vec::with_capacity(window[0].len() * 3); + msg.extend_from_slice(&window[0]); + msg.extend_from_slice(&window[1]); + msg.extend_from_slice(&window[2]); + md = Alg::SHA256.hash(&msg); + window.swap(0, 1); + window.swap(1, 2); + window[2] = md.clone(); + } + assert!( + row[0].1 == count.to_string() && row[1].1 == hex_encode(&md), + "MCT COUNT {count} 독립 재계산 불일치" + ); + chain_seed = md; + } +} From 7d3a0b8389fcc0a5bcbef22ae70fa380ce420614 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:37:43 +0900 Subject: [PATCH 43/65] =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=A0=95=EB=A6=AC?= =?UTF-8?q?=20-=20cross-confirm=20=EC=97=90=20KCAVP=20AES=20SHA2=20SHA3=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EC=B2=B4=ED=81=AC=20-=20Cargo=20toml=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=84=A0=EC=8A=A4=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 -- cross-confirm.md | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 203981e..b204455 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,6 @@ resolver = "3" version = "1.1.0" edition = "2024" authors = ["Q. T. Felix "] -# 루트 LICENSE-MIT / LICENSE-APACHE 와 동기화. SPDX 식별자로 명시해 다운스트림 -# 공급망 감사 도구 (cargo-deny 등) 가 인식하도록 함. license = "MIT OR Apache-2.0" [profile.dev] diff --git a/cross-confirm.md b/cross-confirm.md index 489edfc..56e74ae 100644 --- a/cross-confirm.md +++ b/cross-confirm.md @@ -31,9 +31,9 @@ ## KCAVP 검증 작업 -- [ ] AES-256 (CBC, CTR, GCM) -- [ ] SHA2 (224, 256, 384, 512) -- [ ] SHA3 (224, 256, 384, 512) +- [X] AES-256 (CBC, CTR, GCM) +- [X] SHA2 (224, 256, 384, 512) +- [X] SHA3 (224, 256, 384, 512) - [ ] RNG (Hash_DRBG) 여기서 언급되는 "검증"이란, 해당 크레이트에 구현된 알고리즘이 "정상적으로 작동한다"는 것을 의미합니다. \ No newline at end of file From 42bf87b835cada9b1948a20c8a3f02b2c002894b Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:31:26 +0900 Subject: [PATCH 44/65] =?UTF-8?q?Docstring=20=EC=97=90=EC=84=9C=20Authors?= =?UTF-8?q?=20=ED=95=AD=EB=AA=A9=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/src/lib.rs | 3 --- ed25519/src/lib.rs | 3 --- ed448/src/lib.rs | 3 --- mldsa/src/lib.rs | 3 --- mlkem/src/lib.rs | 3 --- zeroize/src/barrier.rs | 3 --- zeroize/src/volatile.rs | 3 --- 7 files changed, 21 deletions(-) diff --git a/aes/src/lib.rs b/aes/src/lib.rs index b9ce761..fb8cabd 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -25,9 +25,6 @@ //! //! # Security Note //! 모든 키 및 라운드 키는 Drop 시 강제 소거됩니다. -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] diff --git a/ed25519/src/lib.rs b/ed25519/src/lib.rs index 2f713ae..4d1b086 100644 --- a/ed25519/src/lib.rs +++ b/ed25519/src/lib.rs @@ -22,9 +22,6 @@ //! // 검증 //! assert!(verify(message, &signature, &public).is_ok()); //! ``` -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] diff --git a/ed448/src/lib.rs b/ed448/src/lib.rs index 74c59a4..e3796ee 100644 --- a/ed448/src/lib.rs +++ b/ed448/src/lib.rs @@ -22,9 +22,6 @@ //! // 검증 //! assert!(verify(message, &[], &signature, &public).is_ok()); //! ``` -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] diff --git a/mldsa/src/lib.rs b/mldsa/src/lib.rs index 2d09894..3ae3d31 100644 --- a/mldsa/src/lib.rs +++ b/mldsa/src/lib.rs @@ -32,9 +32,6 @@ //! # Security Note //! - `xi`와 `rnd`는 반드시 암호학적으로 안전한 RNG로 생성해야 합니다. //! - 비밀 키는 사용 후 반드시 소거(zeroize)해야 합니다. -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] diff --git a/mlkem/src/lib.rs b/mlkem/src/lib.rs index 9accfa0..f15793f 100644 --- a/mlkem/src/lib.rs +++ b/mlkem/src/lib.rs @@ -34,9 +34,6 @@ //! - 시드 `d`, `z`, `m`은 반드시 암호학적으로 안전한 난수 생성기(CSPRNG)로 생성해야 합니다. //! - 역캡슐화 키(`dk`)는 `Secret` 래퍼로 보호되며, 스코프 종료 시 자동 소거됩니다. //! - 암호문 변조 시 암묵적 거부(implicit rejection)를 수행합니다. -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] diff --git a/zeroize/src/barrier.rs b/zeroize/src/barrier.rs index db73c4a..41b4bfa 100644 --- a/zeroize/src/barrier.rs +++ b/zeroize/src/barrier.rs @@ -17,9 +17,6 @@ //! // 민감한 메모리 연산 //! memory_barrier(); //! ``` -//! -//! # Authors -//! Q. T. Felix // MIRI 환경에서는 inline asm 미지원이므로 fallback 경로를 사용 // (production 빌드에는 영향 없음 cfg(miri)는 miri 실행 시에만 활성) diff --git a/zeroize/src/volatile.rs b/zeroize/src/volatile.rs index 683f7e3..0034d47 100644 --- a/zeroize/src/volatile.rs +++ b/zeroize/src/volatile.rs @@ -11,9 +11,6 @@ //! # Security Note //! 모든 함수는 컴파일러 배리어와 메모리 배리어를 포함하여 //! 쓰기 연산이 실제로 수행되고 캐시에 반영되도록 보장합니다. -//! -//! # Authors -//! Q. T. Felix use crate::barrier::{atomic_compiler_fence, compiler_barrier, memory_barrier}; use core::ptr; From 8db76804094026f5e691696c76b17e9513408f47 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:32:05 +0900 Subject: [PATCH 45/65] =?UTF-8?q?zeroize=20=EC=97=90=20Zeroable=20?= =?UTF-8?q?=EB=A7=88=EC=BB=A4=20=ED=8A=B8=EB=A0=88=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20-=20all-zero=20=EB=B9=84=ED=8A=B8=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=EC=9D=B4=20=EC=9C=A0=ED=9A=A8=ED=95=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EB=A7=8C=20Secret=20=EA=B3=BC=20zeroize=5Ffl?= =?UTF-8?q?at=20=EC=97=90=20=ED=97=88=EC=9A=A9=20-=20NonZero=20=EB=82=98?= =?UTF-8?q?=20Box=20=EC=B2=98=EB=9F=BC=200=20=EC=9D=B4=20invalid=20value?= =?UTF-8?q?=20=EC=9D=B8=20=ED=83=80=EC=9E=85=20=EB=9E=98=ED=95=91=EC=9D=84?= =?UTF-8?q?=20=EC=BB=B4=ED=8C=8C=EC=9D=BC=ED=83=80=EC=9E=84=20=EC=B0=A8?= =?UTF-8?q?=EB=8B=A8=20-=20black=5Fbox=20=EC=9D=98=20read=5Fvolatile=20?= =?UTF-8?q?=EC=9D=84=20=EC=86=8C=EC=9C=A0=EA=B6=8C=20=EC=9D=B4=EB=8F=99=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20-?= =?UTF-8?q?=20mldsa=20Poly=20PolyVec=20=EA=B3=BC=20elib-k0-ipc=20IsSecret?= =?UTF-8?q?=20=EB=B0=94=EC=9A=B4=EB=93=9C=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- elib-k0-ipc/src/secret.rs | 9 +++---- mldsa/src/poly.rs | 6 ++++- zeroize/src/barrier/aarch64.rs | 10 +++----- zeroize/src/barrier/fallback.rs | 10 +++----- zeroize/src/barrier/x86_64.rs | 10 +++----- zeroize/src/lib.rs | 5 +--- zeroize/src/secret.rs | 27 +++++++++++--------- zeroize/src/zeroize.rs | 42 ++++++++++++++++++++++++------- zeroize/tests/zeroize_readback.rs | 8 +++--- 9 files changed, 73 insertions(+), 54 deletions(-) diff --git a/elib-k0-ipc/src/secret.rs b/elib-k0-ipc/src/secret.rs index 3df9c2d..c361063 100644 --- a/elib-k0-ipc/src/secret.rs +++ b/elib-k0-ipc/src/secret.rs @@ -28,11 +28,8 @@ //! - `Deref` / `DerefMut` / `AsRef` 미구현 — 내부 참조가 typestate 밖으로 escape 되어 //! consume-on-dissolve invariant 를 우회하는 위험 차단 (정책적 결정). //! - `consume_into(dst)` dissolve 경로는 v1 미포함 — Phase 4/5 에서 추가 예정. -//! -//! # Authors -//! Q. T. Felix -use ::zeroize::{Secret, Zeroize}; +use ::zeroize::{Secret, Zeroable, Zeroize}; pub(crate) mod sealed { /// 봉인용 마커 트레이트입니다. `elib-k0-ipc` 외부에서는 구현 불가합니다. @@ -62,8 +59,8 @@ pub(crate) mod sealed { /// ``` pub trait IsSecret: sealed::Sealed {} -impl sealed::Sealed for Secret {} -impl IsSecret for Secret {} +impl sealed::Sealed for Secret {} +impl IsSecret for Secret {} impl sealed::Sealed for MustZeroize {} impl IsSecret for MustZeroize {} diff --git a/mldsa/src/poly.rs b/mldsa/src/poly.rs index 78c80aa..6330a17 100644 --- a/mldsa/src/poly.rs +++ b/mldsa/src/poly.rs @@ -1,6 +1,6 @@ use crate::field::Fq; use crate::ntt::{N, intt, ntt}; -use zeroize::Zeroize; +use zeroize::{Zeroable, Zeroize}; #[derive(Clone, Copy)] pub struct Poly { @@ -14,6 +14,8 @@ impl Zeroize for Poly { } } +unsafe impl Zeroable for Poly {} + impl Poly { pub const fn new_zero() -> Self { Self { coeffs: [Fq(0); N] } @@ -69,6 +71,8 @@ impl Zeroize for PolyVec { } } +unsafe impl Zeroable for PolyVec {} + impl PolyVec { pub const fn new_zero() -> Self { Self { diff --git a/zeroize/src/barrier/aarch64.rs b/zeroize/src/barrier/aarch64.rs index 18dd1d7..814b05c 100644 --- a/zeroize/src/barrier/aarch64.rs +++ b/zeroize/src/barrier/aarch64.rs @@ -2,9 +2,6 @@ //! //! `dsb sy` 명령어와 인라인 어셈블리를 활용하여 //! 메모리 연산의 순서와 가시성을 보장합니다. -//! -//! # Authors -//! Q. T. Felix use core::arch::asm; @@ -41,8 +38,9 @@ pub fn atomic_compiler_fence() { /// 값을 최적화에서 숨깁니다. /// -/// 값의 주소를 레지스터에 강제 로드하고 휘발성 읽기를 수행하여 -/// 컴파일러가 해당 값에 대한 연산을 최적화하지 못하도록 합니다. +/// 값의 주소를 레지스터에 강제 로드하여 컴파일러가 +/// 해당 값에 대한 연산을 최적화하지 못하도록 한 뒤, +/// 값을 소유권 이동으로 그대로 반환합니다. /// /// # Arguments /// - `value`: 최적화에서 숨길 값 @@ -54,6 +52,6 @@ pub fn black_box(value: T) -> T { in("x0") &value, options(nostack, preserves_flags) ); - core::ptr::read_volatile(&value as *const T) } + value } diff --git a/zeroize/src/barrier/fallback.rs b/zeroize/src/barrier/fallback.rs index 7757600..db1aae0 100644 --- a/zeroize/src/barrier/fallback.rs +++ b/zeroize/src/barrier/fallback.rs @@ -2,9 +2,6 @@ //! //! x86_64, aarch64 외의 아키텍처에서 사용되며, //! `core::sync::atomic` 기반의 배리어를 제공합니다. -//! -//! # Authors -//! Q. T. Felix /// CPU 메모리 배리어를 수행합니다. /// @@ -35,13 +32,12 @@ pub fn atomic_compiler_fence() { /// 값을 최적화에서 숨깁니다. /// -/// 휘발성 읽기를 수행하여 컴파일러가 해당 값에 대한 -/// 연산을 최적화하지 못하도록 합니다. +/// `core::hint::black_box` 로 위임하여 컴파일러가 해당 값에 대한 +/// 연산을 최적화하지 못하도록 하며, 값은 소유권 이동으로 그대로 반환됩니다. /// /// # Arguments /// - `value`: 최적화에서 숨길 값 #[inline(never)] pub fn black_box(value: T) -> T { - let ptr = &value as *const T; - unsafe { core::ptr::read_volatile(ptr) } + core::hint::black_box(value) } diff --git a/zeroize/src/barrier/x86_64.rs b/zeroize/src/barrier/x86_64.rs index 07b1d99..0c6252d 100644 --- a/zeroize/src/barrier/x86_64.rs +++ b/zeroize/src/barrier/x86_64.rs @@ -2,9 +2,6 @@ //! //! `mfence` 명령어와 인라인 어셈블리를 활용하여 //! 메모리 연산의 순서와 가시성을 보장합니다. -//! -//! # Authors -//! Q. T. Felix use core::arch::asm; @@ -41,8 +38,9 @@ pub fn atomic_compiler_fence() { /// 값을 최적화에서 숨깁니다. /// -/// 값의 주소를 레지스터에 강제 로드하고 휘발성 읽기를 수행하여 -/// 컴파일러가 해당 값에 대한 연산을 최적화하지 못하도록 합니다. +/// 값의 주소를 레지스터에 강제 로드하여 컴파일러가 +/// 해당 값에 대한 연산을 최적화하지 못하도록 한 뒤, +/// 값을 소유권 이동으로 그대로 반환합니다. /// /// # Arguments /// - `value`: 최적화에서 숨길 값 @@ -54,6 +52,6 @@ pub fn black_box(value: T) -> T { in("rax") &value, options(nostack, preserves_flags) ); - core::ptr::read_volatile(&value as *const T) } + value } diff --git a/zeroize/src/lib.rs b/zeroize/src/lib.rs index ebbf008..bd48741 100644 --- a/zeroize/src/lib.rs +++ b/zeroize/src/lib.rs @@ -27,9 +27,6 @@ //! # Security Note //! 이 크레이트는 소프트웨어 수준에서 달성 가능한 최대 보안을 제공합니다. //! 콜드 부트 공격 등 물리적 공격에 대해서는 하드웨어 수준의 보호가 필요합니다. -//! -//! # Authors -//! Q. T. Felix #![no_std] @@ -39,4 +36,4 @@ pub mod volatile; mod zeroize; pub use secret::Secret; -pub use zeroize::{Zeroize, zeroize_flat}; +pub use zeroize::{Zeroable, Zeroize, zeroize_flat}; diff --git a/zeroize/src/secret.rs b/zeroize/src/secret.rs index 3f22985..f110a92 100644 --- a/zeroize/src/secret.rs +++ b/zeroize/src/secret.rs @@ -25,12 +25,9 @@ //! - `Clone`을 구현하지 않아 암시적 복제를 방지합니다 //! - `into_inner` 호출 시 원본 메모리도 소거됩니다 //! - `Debug` 미구현으로 로깅 시 데이터 노출을 방지합니다 -//! -//! # Authors -//! Q. T. Felix use crate::barrier::{atomic_compiler_fence, black_box, compiler_barrier, memory_barrier}; -use crate::zeroize::Zeroize; +use crate::zeroize::{Zeroable, Zeroize}; use core::ops::{Deref, DerefMut}; use core::{mem, ptr}; @@ -38,11 +35,17 @@ use core::{mem, ptr}; /// /// 내부 데이터는 `Drop` 시 휘발성 쓰기와 배리어를 통해 /// 안전하게 0으로 소거됩니다. -pub struct Secret { +/// +/// # Security Note +/// `T: Zeroable` 바운드가 all-zero 비트 패턴이 invalid value 인 타입 +/// (`NonZeroU32`, `Box` 등) 의 래핑을 컴파일타임에 차단하여, `Drop` 의 +/// 바이트 제로화가 validity invariant 위반이나 drop glue 의 +/// use-after-free 로 이어지는 것을 방지합니다. +pub struct Secret { inner: T, } -impl Secret { +impl Secret { /// 새로운 `Secret`을 생성합니다. /// /// # Arguments @@ -68,7 +71,7 @@ impl Secret { } } -impl Secret { +impl Secret { /// 내부 데이터를 추출하고 원본 메모리를 소거합니다. /// /// 데이터를 복사한 후 원본 메모리를 안전하게 소거하고, @@ -101,7 +104,7 @@ impl Secret { } } -impl Deref for Secret { +impl Deref for Secret { type Target = T; #[inline] @@ -110,14 +113,14 @@ impl Deref for Secret { } } -impl DerefMut for Secret { +impl DerefMut for Secret { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { self.expose_mut() } } -impl Drop for Secret { +impl Drop for Secret { #[inline] fn drop(&mut self) { compiler_barrier(); @@ -139,14 +142,14 @@ impl Drop for Secret { } } -impl Zeroize for Secret { +impl Zeroize for Secret { #[inline] fn zeroize(&mut self) { self.inner.zeroize(); } } -impl Default for Secret { +impl Default for Secret { #[inline] fn default() -> Self { Self::new(T::default()) diff --git a/zeroize/src/zeroize.rs b/zeroize/src/zeroize.rs index 0b46d5d..050449b 100644 --- a/zeroize/src/zeroize.rs +++ b/zeroize/src/zeroize.rs @@ -7,7 +7,8 @@ //! - 기본 정수 타입 (u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize) //! - 고정 크기 배열 `[T; N]` //! - 가변 슬라이스 `&mut [T]` -//! - `zeroize_flat`: 임의 타입의 메모리를 바이트 단위로 소거 +//! - `Zeroable`: all-zero 비트 패턴이 유효한 타입 표시 마커 +//! - `zeroize_flat`: `Zeroable` 타입의 메모리를 바이트 단위로 소거 //! //! # Examples //! ```rust,ignore @@ -17,9 +18,6 @@ //! key.zeroize(); //! assert!(key.iter().all(|&b| b == 0)); //! ``` -//! -//! # Authors -//! Q. T. Felix use crate::barrier::{atomic_compiler_fence, black_box, compiler_barrier, memory_barrier}; use crate::volatile::volatile_write; @@ -144,7 +142,33 @@ impl Zeroize for &mut [T] { } } -/// 임의 타입의 메모리를 바이트 단위로 소거합니다. +/// all-zero 바이트 패턴이 유효한 값인 타입을 표시하는 마커 트레이트입니다. +/// +/// `Secret`의 `Drop`과 `zeroize_flat`은 타입 구조를 무시하고 메모리를 +/// 바이트 단위로 0으로 덮어쓰므로, 이 트레이트가 구현된 타입에만 허용됩니다. +/// +/// # Safety +/// 모든 바이트가 0인 비트 패턴이 해당 타입의 유효한 값이어야 하며, +/// 타입이 소유권 있는 간접 참조(힙 포인터 등)를 포함하지 않아야 합니다. +/// `NonZeroU32`처럼 0이 invalid value인 타입이나 `Box`처럼 널이 될 수 없는 +/// 포인터 타입에 구현하면 소거 시점에 UB가 발생합니다. +pub unsafe trait Zeroable {} + +unsafe impl Zeroable for u8 {} +unsafe impl Zeroable for u16 {} +unsafe impl Zeroable for u32 {} +unsafe impl Zeroable for u64 {} +unsafe impl Zeroable for u128 {} +unsafe impl Zeroable for usize {} +unsafe impl Zeroable for i8 {} +unsafe impl Zeroable for i16 {} +unsafe impl Zeroable for i32 {} +unsafe impl Zeroable for i64 {} +unsafe impl Zeroable for i128 {} +unsafe impl Zeroable for isize {} +unsafe impl Zeroable for [T; N] {} + +/// 타입의 메모리를 바이트 단위로 소거합니다. /// /// 타입의 크기만큼 모든 바이트를 0으로 설정하며, /// 휘발성 쓰기와 배리어를 통해 소거의 완료를 보장합니다. @@ -153,10 +177,10 @@ impl Zeroize for &mut [T] { /// - `value`: 소거할 값의 가변 참조 /// /// # Security Note -/// 이 함수는 `Zeroize` 트레이트를 구현하지 않은 타입에도 -/// 사용할 수 있으나, 내부에 포인터가 있는 타입의 경우 -/// 포인터가 가리키는 데이터는 소거되지 않습니다. -pub fn zeroize_flat(value: &mut T) { +/// `T: Zeroable` 바운드가 all-zero 가 invalid value 인 타입 (`NonZeroU32`, +/// `Box` 등) 의 소거를 컴파일타임에 차단하여 validity invariant 위반과 +/// use-after-free 를 방지합니다. +pub fn zeroize_flat(value: &mut T) { compiler_barrier(); let size = mem::size_of::(); diff --git a/zeroize/tests/zeroize_readback.rs b/zeroize/tests/zeroize_readback.rs index bf9a453..4d21b51 100644 --- a/zeroize/tests/zeroize_readback.rs +++ b/zeroize/tests/zeroize_readback.rs @@ -1,5 +1,5 @@ use core::mem::MaybeUninit; -use zeroize::{Secret, Zeroize, zeroize_flat}; +use zeroize::{Secret, Zeroable, Zeroize, zeroize_flat}; #[cfg_attr(miri, ignore)] #[test] @@ -8,7 +8,7 @@ fn secret_array_zeroized_on_drop() { unsafe { storage.write(Secret::new([0xA5u8; 32])); let ptr = storage.assume_init_ref().expose().as_ptr(); - let byte_len = core::mem::size_of::<[u8; 32]>(); + let byte_len = size_of::<[u8; 32]>(); let pre = core::slice::from_raw_parts(ptr, byte_len); assert!( @@ -58,6 +58,8 @@ fn zeroize_flat_wipes_bytes() { c: u32, } + unsafe impl Zeroable for Blob {} + let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { storage.write(Blob { @@ -66,7 +68,7 @@ fn zeroize_flat_wipes_bytes() { c: 0xCAFE_BABE, }); let ptr = storage.as_ptr() as *const u8; - let byte_len = core::mem::size_of::(); + let byte_len = size_of::(); let pre = core::slice::from_raw_parts(ptr, byte_len); assert!( From e21ee13b8ff1b30871afe2ad5cf1098b836e1598 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:32:33 +0900 Subject: [PATCH 46/65] =?UTF-8?q?constant-time=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EB=B6=84=ED=95=A0=EA=B3=BC=20=ED=8A=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=9C=EB=AA=85=20-=20internal=20=EC=9D=84=20x86?= =?UTF-8?q?=5F64=20aarch64=20fallback=20ct128=20=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=ED=95=A0=20-=20=ED=8A=B8=EB=A0=88=EC=9D=B4=ED=8A=B8=EB=A5=BC?= =?UTF-8?q?=20traits=20=EB=AA=A8=EB=93=88=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20CtGreeter=20=EB=A5=BC=20CtGtOps=20?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=9C=EB=AA=85=20-=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=AA=85=EC=9D=84=20ct=5Feq=20ct=5Fne=20ct=5Fgt=20ct?= =?UTF-8?q?=5Flt=20=EB=A1=9C=20=ED=86=B5=EC=9D=BC=ED=95=B4=20=ED=91=9C?= =?UTF-8?q?=EC=A4=80=20=ED=8A=B8=EB=A0=88=EC=9D=B4=ED=8A=B8=EC=99=80=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=EC=A0=9C=EA=B1=B0=20-=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=EA=B3=BC=20Secret=20=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?CtEqOps=20=EA=B5=AC=ED=98=84=20=EC=B6=94=EA=B0=80=20-=20swap=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=EA=B0=92=20=EC=86=8C=EA=B1=B0=EB=A5=BC=20zer?= =?UTF-8?q?oize=20=ED=81=AC=EB=A0=88=EC=9D=B4=ED=8A=B8=EB=A1=9C=20?= =?UTF-8?q?=EC=9C=84=EC=9E=84=20-=20check=5Fct=5Fasm=20=EC=97=90=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=20ct=5Feq=20=EB=B6=84=EA=B8=B0=20=EC=83=81?= =?UTF-8?q?=ED=95=9C=EA=B3=BC=20memcmp=20=EC=B9=98=ED=99=98=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EC=B6=94=EA=B0=80=20-=20=EC=A0=84=20=ED=81=AC?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=8A=B8=20=ED=98=B8=EC=B6=9C=EB=B6=80?= =?UTF-8?q?=EC=99=80=20README=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/src/block.rs | 3 +- aes/src/gcm.rs | 5 +- blake/src/lib.rs | 25 +- blake/tests/threat_inputs.rs | 3 +- chacha20/src/lib.rs | 5 +- chacha20/src/poly1305.rs | 5 +- constant-time/Cargo.toml | 2 +- constant-time/README.md | 8 +- constant-time/README_EN.md | 8 +- constant-time/examples/ct_asm_probes.rs | 33 +- constant-time/examples/ct_valgrind_probe.rs | 15 +- constant-time/scripts/check_ct_asm.sh | 29 + constant-time/src/internal.rs | 638 +----------------- constant-time/src/internal/aarch64.rs | 158 +++++ constant-time/src/internal/ct128.rs | 56 ++ constant-time/src/internal/fallback.rs | 189 ++++++ constant-time/src/internal/x86_64.rs | 205 ++++++ constant-time/src/lib.rs | 422 +++--------- constant-time/src/traits.rs | 288 ++++++++ constant-time/tests/chan_dual_auth_ct.rs | 13 +- constant-time/tests/chan_length_ct.rs | 6 +- constant-time/tests/dudect.rs | 53 +- constant-time/tests/gap_cap_take_fsm.rs | 8 +- constant-time/tests/hsm_cap_ct.rs | 9 +- constant-time/tests/threat_inputs.rs | 29 +- .../tests/wire_tier2_bad_header_reject.rs | 6 +- mldsa/src/field.rs | 5 +- mldsa/src/keys.rs | 3 +- mldsa/src/pack.rs | 3 +- mlkem/src/kem.rs | 5 +- sha2/src/sha2_256.rs | 7 +- sha2/src/sha2_512.rs | 7 +- sha3/src/keccak.rs | 8 +- x25519/src/field.rs | 3 +- x25519/src/lib.rs | 8 +- x448/src/field.rs | 3 +- x448/src/lib.rs | 8 +- 37 files changed, 1201 insertions(+), 1080 deletions(-) create mode 100644 constant-time/src/internal/aarch64.rs create mode 100644 constant-time/src/internal/ct128.rs create mode 100644 constant-time/src/internal/fallback.rs create mode 100644 constant-time/src/internal/x86_64.rs create mode 100644 constant-time/src/traits.rs diff --git a/aes/src/block.rs b/aes/src/block.rs index c0afd7f..94d8a94 100644 --- a/aes/src/block.rs +++ b/aes/src/block.rs @@ -1,7 +1,8 @@ #![allow(clippy::needless_range_loop)] use crate::sbox::{inv_sub_bytes_block, sub_bytes_block}; -use constant_time::{Choice, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::*; use zeroize::Zeroize; const NB: usize = 4; diff --git a/aes/src/gcm.rs b/aes/src/gcm.rs index efc4171..6ff6c4d 100644 --- a/aes/src/gcm.rs +++ b/aes/src/gcm.rs @@ -1,6 +1,7 @@ use crate::ghash::GHash; use crate::{AES256, Error}; -use constant_time::{Choice, CtEqOps}; +use constant_time::Choice; +use constant_time::traits::CtEqOps; use zeroize::{Secret, Zeroize}; pub const GCM_TAG_SIZE: usize = 16; @@ -205,7 +206,7 @@ impl AES256GCM { let mut eq = Choice::from_u8(1); for (given, expected) in tag.iter().zip(expected_tag.iter()) { - eq &= CtEqOps::eq(given, expected); + eq &= CtEqOps::ct_eq(given, expected); } let authentic = eq.unwrap_u8() == 1; expected_tag.zeroize(); diff --git a/blake/src/lib.rs b/blake/src/lib.rs index 4e52ba6..ddfb566 100644 --- a/blake/src/lib.rs +++ b/blake/src/lib.rs @@ -39,9 +39,6 @@ //! let out = blake2b_long(b"input", 80).unwrap(); //! assert_eq!(out.as_slice().len(), 80); //! ``` -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] @@ -53,7 +50,8 @@ use zeroize::{Secret, Zeroize}; pub use blake2b::Blake2b; pub use blake3::{Blake3, OUT_LEN as BLAKE3_OUT_LEN}; -pub use constant_time::{Choice, CtEqOps}; +pub use constant_time::Choice; +pub use constant_time::traits::CtEqOps; /// 해시 연산 중 발생할 수 있는 에러 타입입니다. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -135,23 +133,30 @@ impl Drop for SecureBuffer { /// 최종 결과가 `Choice(0)`이 되며, 어떤 분기 결과도 호출자에게 직접 누설 /// 되지 않습니다. pub fn ct_eq_slice(a: &[u8], b: &[u8]) -> Choice { - let len_eq = CtEqOps::eq(&a.len(), &b.len()); + let len_eq = CtEqOps::ct_eq(&a.len(), &b.len()); // 두 슬라이스 길이는 공개 가정 // `min` 산출의 비교가 공개 데이터 분기 let min_len = a.len().min(b.len()); let mut result = Choice::from_u8(1); for i in 0..min_len { - result &= CtEqOps::eq(&a[i], &b[i]); + result &= CtEqOps::ct_eq(&a[i], &b[i]); } // 길이가 다르면 len_eq == 0 으로 최종 결과 마스킹 result & len_eq } -impl CtEqOps for SecureBuffer { +impl SecureBuffer { + /// 자신과 `other`의 유효 데이터가 같으면 `Choice(1)`을, 다르면 `Choice(0)`을 + /// 반환하는 함수입니다. + /// + /// # Security Note + /// 비교는 `ct_eq_slice`에 위임되며 동일한 상수시간 보장과 길이 공개 가정을 + /// 따릅니다. + #[must_use] #[inline] - fn eq(&self, other: &Self) -> Choice { + pub fn ct_eq(&self, other: &Self) -> Choice { ct_eq_slice(self.as_slice(), other.as_slice()) } } @@ -510,10 +515,10 @@ mod tests { let mut buf2 = SecureBuffer::new_owned(4).unwrap(); buf2.as_mut_slice().copy_from_slice(&[1, 2, 3, 4]); - assert_eq!(CtEqOps::eq(&buf1, &buf2).unwrap_u8(), 1); + assert_eq!(buf1.ct_eq(&buf2).unwrap_u8(), 1); buf2.as_mut_slice()[3] = 5; - assert_eq!(CtEqOps::eq(&buf1, &buf2).unwrap_u8(), 0); + assert_eq!(buf1.ct_eq(&buf2).unwrap_u8(), 0); } /// Blake2b 인스턴스가 update 후 nonempty 상태에서 Drop 시 diff --git a/blake/tests/threat_inputs.rs b/blake/tests/threat_inputs.rs index 5d68814..d7569df 100644 --- a/blake/tests/threat_inputs.rs +++ b/blake/tests/threat_inputs.rs @@ -9,7 +9,6 @@ //! UB 회귀 가드 역할 수행. use blake::{Blake2b, Blake3, MAX_OUTPUT_LEN, SecureBuffer, blake2b_long, ct_eq_slice}; -use constant_time::CtEqOps; // // ct_eq_slice: 길이 경계와 비대칭 입력 회귀 @@ -97,7 +96,7 @@ fn threat_secure_buffer_ct_eq_different_lengths() { let mut b = SecureBuffer::new_owned(16).unwrap(); a.as_mut_slice().copy_from_slice(&[1; 8]); b.as_mut_slice().copy_from_slice(&[1; 16]); - assert_eq!(CtEqOps::eq(&a, &b).unwrap_u8(), 0); + assert_eq!(a.ct_eq(&b).unwrap_u8(), 0); } // diff --git a/chacha20/src/lib.rs b/chacha20/src/lib.rs index b1f9178..3f10d7c 100644 --- a/chacha20/src/lib.rs +++ b/chacha20/src/lib.rs @@ -5,7 +5,8 @@ mod chacha20; mod poly1305; use chacha20::ChaCha20; -use constant_time::{Choice, CtEqOps}; +use constant_time::Choice; +use constant_time::traits::CtEqOps; use poly1305::{Poly1305, poly1305_verify}; use zeroize::{Secret, Zeroize}; @@ -250,7 +251,7 @@ impl ChaCha20Poly1305 { pub fn verify_tag(computed: &[u8; 16], expected: &[u8; 16]) -> bool { let mut eq = Choice::from_u8(1); for i in 0..16 { - eq &= CtEqOps::eq(&computed[i], &expected[i]); + eq &= CtEqOps::ct_eq(&computed[i], &expected[i]); } eq.unwrap_u8() == 1 } diff --git a/chacha20/src/poly1305.rs b/chacha20/src/poly1305.rs index 53c42cd..de35da8 100644 --- a/chacha20/src/poly1305.rs +++ b/chacha20/src/poly1305.rs @@ -1,4 +1,5 @@ -use constant_time::{Choice, CtEqOps, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtSelOps}; use zeroize::Zeroize; pub struct Poly1305 { @@ -316,7 +317,7 @@ impl Drop for Poly1305 { pub fn poly1305_verify(tag1: &[u8; 16], tag2: &[u8; 16]) -> bool { let mut eq = Choice::from_u8(1); for i in 0..16 { - eq &= CtEqOps::eq(&tag1[i], &tag2[i]); + eq &= CtEqOps::ct_eq(&tag1[i], &tag2[i]); } eq.unwrap_u8() == 1 } diff --git a/constant-time/Cargo.toml b/constant-time/Cargo.toml index c75292d..019204d 100644 --- a/constant-time/Cargo.toml +++ b/constant-time/Cargo.toml @@ -6,10 +6,10 @@ authors.workspace = true license.workspace = true [dependencies] +zeroize = { workspace = true } [dev-dependencies] rng = { workspace = true } -zeroize = { workspace = true } # Phase 4 Plan 02 wire host 단위테스트 9 종 추가 (postcard bitexact + Blake3 cross-validation) blake = { workspace = true } postcard = { version = "1", default-features = false } diff --git a/constant-time/README.md b/constant-time/README.md index e68ad4b..ebcfb27 100644 --- a/constant-time/README.md +++ b/constant-time/README.md @@ -12,9 +12,9 @@ - `Choice`: 항상 0 또는 1 값을 갖는 상수-시간 bool. `&`, `|`, `^`, `!` 비트 연산이 정규화 없이 0/1 불변을 보존하며, 민감 값 유출(CWE-532) 방지를 위해 `Debug`를 의도적으로 파생하지 않습니다. - `CtSelOps`: 조건 선택(`select`), 조건 대입(`assign`), 조건 교환(`swap`). -- `CtEqOps`: 동등 비교(`eq`, `ne`). -- `CtGreeter`: 대소 비교(`gt`). -- `CtLess`: 작음 비교(`lt`). `CtEqOps + CtGreeter`를 만족하는 모든 타입에 `!gt & !eq`로 자동 제공됩니다. +- `CtEqOps`: 동등 비교(`ct_eq`, `ct_ne`). +- `CtGtOps`: 대소 비교(`ct_gt`). +- `CtLess`: 작음 비교(`ct_lt`). `CtEqOps + CtGtOps`를 만족하는 모든 타입에 `!ct_gt & !ct_eq`로 자동 제공됩니다. 네 가지 트레이트는 고정폭 정수 `u8`부터 `i128`까지에 구현되며, `Sealed`로 봉인되어 외부 타입이 끼어들 수 없습니다. 각 구현은 `internal` 모듈의 저수준 프리미티브에 위임합니다. @@ -47,7 +47,7 @@ x86_64와 aarch64에서 각 프리미티브는 인-라인 어셈블리로 직접 ### 3. Choice 0/1 불변 -`Choice`는 `(v | v.wrapping_neg()) >> 7` 마스크로 분기 없이 0/1로 정규화되고, 비트 연산이 이 불변을 보존합니다. 덕분에 `lt = !gt & !eq` 같은 상위 조합도 분기 없이 유지됩니다. +`Choice`는 `(v | v.wrapping_neg()) >> 7` 마스크로 분기 없이 0/1로 정규화되고, 비트 연산이 이 불변을 보존합니다. 덕분에 `lt = !ct_gt & !ct_eq` 같은 상위 조합도 분기 없이 유지됩니다. 추가로, 검증된 인-라인 어셈블리가 없는 아키텍처는 컴파일 게이트(`compile_error!`)로 거부되어 best-effort `black_box` fallback이 고보안 빌드에 섞이지 않습니다(아래 '발견한 문제와 조치' 절 참고). `swap`의 영점화 루프는 종료 조건이 `size_of::()`라는 컴파일타임 상수이므로 그 분기는 비밀에 의존하지 않습니다. diff --git a/constant-time/README_EN.md b/constant-time/README_EN.md index b86ba0f..da6db5d 100644 --- a/constant-time/README_EN.md +++ b/constant-time/README_EN.md @@ -12,9 +12,9 @@ This crate provides integer operations that work without branching on secret val - `Choice`: A constant-time bool that always holds 0 or 1. Bitwise operations `&`, `|`, `^`, `!` preserve the 0/1 invariant without normalization. `Debug` is intentionally not derived to prevent sensitive value leakage (CWE-532). - `CtSelOps`: Conditional select (`select`), conditional assign (`assign`), conditional swap (`swap`). -- `CtEqOps`: Equality comparison (`eq`, `ne`). -- `CtGreeter`: Greater-than comparison (`gt`). -- `CtLess`: Less-than comparison (`lt`). Automatically provided as `!gt & !eq` for any type satisfying `CtEqOps + CtGreeter`. +- `CtEqOps`: Equality comparison (`ct_eq`, `ct_ne`). +- `CtGtOps`: Greater-than comparison (`ct_gt`). +- `CtLess`: Less-than comparison (`ct_lt`). Automatically provided as `!ct_gt & !ct_eq` for any type satisfying `CtEqOps + CtGtOps`. The four traits are implemented for fixed-width integers from `u8` through `i128`, and are sealed so that external types cannot be inserted. Each implementation delegates to low-level primitives in the `internal` module. @@ -47,7 +47,7 @@ All asm blocks take operands exclusively in registers and specify `options(nomem ### 3. Choice 0/1 Invariant -`Choice` is normalized to 0/1 without branching via the mask `(v | v.wrapping_neg()) >> 7`, and bitwise operations preserve this invariant. As a result, higher-level compositions such as `lt = !gt & !eq` also remain branch-free. +`Choice` is normalized to 0/1 without branching via the mask `(v | v.wrapping_neg()) >> 7`, and bitwise operations preserve this invariant. As a result, higher-level compositions such as `lt = !ct_gt & !ct_eq` also remain branch-free. Additionally, architectures without verified inline assembly are rejected at compile time via a compile gate (`compile_error!`), preventing a best-effort `black_box` fallback from silently mixing into high-security builds (see the "Issues Found and Mitigations" section below). The zeroing loop in `swap` has a termination condition equal to the compile-time constant `size_of::()`, so that branch does not depend on any secret. diff --git a/constant-time/examples/ct_asm_probes.rs b/constant-time/examples/ct_asm_probes.rs index 2eff5de..9a56231 100644 --- a/constant-time/examples/ct_asm_probes.rs +++ b/constant-time/examples/ct_asm_probes.rs @@ -7,10 +7,13 @@ //! 강제 방출하여, release 빌드 산출물의 어셈블리에서 조건분기 명령(x86_64 `jcc`, aarch64 //! `b.cc/cbnz/cbz/tbnz/tbz`)부재를 grep 으로 검증 가능하게 합니다. swap probe는 추가로 //! volatile zero store가 살아남는지(CWE-316 회귀 방지)도 함께 검사하도록 설계됩니다. +//! 배열 eq probe는 컴파일타임 상수 종료조건 루프의 back-edge 분기 1건까지만 허용하고 +//! memcmp/bcmp 치환 부재를 함께 검사하도록 설계됩니다. //! //! main 함수는 DCE 방지용 호출만 수행합니다. -use constant_time::{Choice, CtEqOps, CtGreeter, CtLess, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtGtOps, CtLess, CtSelOps}; #[unsafe(no_mangle)] #[inline(never)] @@ -51,55 +54,61 @@ pub extern "C" fn probe_sel_u128(c: u8, a: u128, b: u128) -> u128 { #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_eq_u32(a: u32, b: u32) -> u8 { - CtEqOps::eq(&a, &b).unwrap_u8() + CtEqOps::ct_eq(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_eq_u64(a: u64, b: u64) -> u8 { - CtEqOps::eq(&a, &b).unwrap_u8() + CtEqOps::ct_eq(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_eq_u128(a: u128, b: u128) -> u8 { - CtEqOps::eq(&a, &b).unwrap_u8() + CtEqOps::ct_eq(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_gt_u32(a: u32, b: u32) -> u8 { - CtGreeter::gt(&a, &b).unwrap_u8() + CtGtOps::ct_gt(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_gt_u64(a: u64, b: u64) -> u8 { - CtGreeter::gt(&a, &b).unwrap_u8() + CtGtOps::ct_gt(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_gt_i64(a: i64, b: i64) -> u8 { - CtGreeter::gt(&a, &b).unwrap_u8() + CtGtOps::ct_gt(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_gt_u128(a: u128, b: u128) -> u8 { - CtGreeter::gt(&a, &b).unwrap_u8() + CtGtOps::ct_gt(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_gt_i128(a: i128, b: i128) -> u8 { - CtGreeter::gt(&a, &b).unwrap_u8() + CtGtOps::ct_gt(&a, &b).unwrap_u8() } #[unsafe(no_mangle)] #[inline(never)] pub extern "C" fn probe_lt_u32(a: u32, b: u32) -> u8 { - CtLess::lt(&a, &b).unwrap_u8() + CtLess::ct_lt(&a, &b).unwrap_u8() +} + +#[unsafe(no_mangle)] +#[inline(never)] +pub extern "C" fn probe_eq_arr_u8_32(a: &[u8; 32], b: &[u8; 32]) -> u8 { + CtEqOps::ct_eq(a, b).unwrap_u8() } #[unsafe(no_mangle)] @@ -131,6 +140,10 @@ fn main() { black_box(probe_gt_u128(black_box(2), black_box(1))); black_box(probe_gt_i128(black_box(2), black_box(-1))); black_box(probe_lt_u32(black_box(1), black_box(2))); + black_box(probe_eq_arr_u8_32( + black_box(&[0x5Au8; 32]), + black_box(&[0x5Au8; 32]), + )); let (mut a, mut b) = (1u64, 2u64); probe_swap_u64(&mut a, &mut b, 1); black_box((a, b)); diff --git a/constant-time/examples/ct_valgrind_probe.rs b/constant-time/examples/ct_valgrind_probe.rs index 19ee768..4daa62e 100644 --- a/constant-time/examples/ct_valgrind_probe.rs +++ b/constant-time/examples/ct_valgrind_probe.rs @@ -14,7 +14,8 @@ //! 호스트 한정: Linux x86_64 (또는 macOS/Apple Silicon의 linux/amd64 //! Docker 컨테이너) -use constant_time::{Choice, CtEqOps, CtGreeter, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtGtOps, CtSelOps}; unsafe extern "C" { fn malloc(size: usize) -> *mut u8; @@ -39,42 +40,42 @@ fn main() { // ct_eq32: 양 operand가 uninit let pa: *mut u32 = alloc_secret(); let pb: *mut u32 = alloc_secret(); - let _ = core::hint::black_box(CtEqOps::eq(&*pa, &*pb)); + let _ = core::hint::black_box(CtEqOps::ct_eq(&*pa, &*pb)); drop_secret(pa); drop_secret(pb); // ct_eq64 let pa: *mut u64 = alloc_secret(); let pb: *mut u64 = alloc_secret(); - let _ = core::hint::black_box(CtEqOps::eq(&*pa, &*pb)); + let _ = core::hint::black_box(CtEqOps::ct_eq(&*pa, &*pb)); drop_secret(pa); drop_secret(pb); // ct_eq128 let pa: *mut u128 = alloc_secret(); let pb: *mut u128 = alloc_secret(); - let _ = core::hint::black_box(CtEqOps::eq(&*pa, &*pb)); + let _ = core::hint::black_box(CtEqOps::ct_eq(&*pa, &*pb)); drop_secret(pa); drop_secret(pb); // ct_gt_u64 let pa: *mut u64 = alloc_secret(); let pb: *mut u64 = alloc_secret(); - let _ = core::hint::black_box(CtGreeter::gt(&*pa, &*pb)); + let _ = core::hint::black_box(CtGtOps::ct_gt(&*pa, &*pb)); drop_secret(pa); drop_secret(pb); // ct_gt_i64 let pa: *mut i64 = alloc_secret(); let pb: *mut i64 = alloc_secret(); - let _ = core::hint::black_box(CtGreeter::gt(&*pa, &*pb)); + let _ = core::hint::black_box(CtGtOps::ct_gt(&*pa, &*pb)); drop_secret(pa); drop_secret(pb); // ct_gt_u128 / i128 let pa: *mut u128 = alloc_secret(); let pb: *mut u128 = alloc_secret(); - let _ = core::hint::black_box(CtGreeter::gt(&*pa, &*pb)); + let _ = core::hint::black_box(CtGtOps::ct_gt(&*pa, &*pb)); drop_secret(pa); drop_secret(pb); diff --git a/constant-time/scripts/check_ct_asm.sh b/constant-time/scripts/check_ct_asm.sh index 0f49d86..119ecaa 100755 --- a/constant-time/scripts/check_ct_asm.sh +++ b/constant-time/scripts/check_ct_asm.sh @@ -56,6 +56,7 @@ PROBES_CT=( ) # swap probe 는 별도 — volatile zero store 존재도 함께 확인 PROBES_SWAP=(probe_swap_u64 probe_swap_u128) +PROBES_EQ_ARR=(probe_eq_arr_u8_32) # zeroize probe 는 별도 크레이트(zeroize) 소속 — 별도 빌드/디스어셈블 후 # volatile zero store + 메모리 배리어 fence 잔존을 함께 검증한다. PROBES_ZEROIZE=(probe_secret_drop probe_zeroize_flat) @@ -70,6 +71,7 @@ case "${ARCH}" in ZERO_STORE_RE='\b(mov[bwlq]?[[:space:]]+\$0x?0+,|mov[[:space:]]+(byte|BYTE)[[:space:]]+(ptr|PTR)[^,]*,[[:space:]]*0|xor[bwlq]?[[:space:]]+%[a-z0-9]+,[[:space:]]*%[a-z0-9]+)' # x86_64 메모리 배리어 fence: mfence (zeroize 경로 생존 검증). FENCE_RE='\bmfence\b' + CALL_CMP_RE='\b(call|jmp)\b.*(memcmp|bcmp)' ;; aarch64) # 조건분기: b.cond (b.eq, b.ne, …), cbz, cbnz, tbz, tbnz @@ -79,6 +81,7 @@ case "${ARCH}" in ZERO_STORE_RE='\b(strb?|str|stp)[[:space:]]+(wzr|xzr)\b' # aarch64 메모리 배리어 fence: dmb / dsb (zeroize 경로 생존 검증). FENCE_RE='\b(dmb|dsb)\b' + CALL_CMP_RE='\b(bl|b)[[:space:]].*(memcmp|bcmp)' ;; *) echo "FAIL: 미지원 호스트 아키텍처: ${ARCH}" >&2 @@ -154,6 +157,32 @@ for sym in "${PROBES_SWAP[@]}"; do fi done +echo ">> 배열 ct_eq probe (카운티드 back-edge 분기 1건 허용 + memcmp 치환 부재)" +# 주의 — [T; N] ct_eq 의 fold 는 N 이라는 컴파일타임 상수 종료조건 루프로 +# 컴파일되며 back-edge 조건분기 1건은 카운터(공개 값) 의존이라 CT 보장이 +# 유지된다. 비밀 의존 조기 종료 회귀가 생기면 누산기/데이터 조건분기가 +# 추가되므로 조건분기 허용 상한을 1로 고정해 검출한다. memcmp/bcmp 호출 +# 검사는 컴파일러의 비-CT 라이브러리 치환 회귀를 차단한다. +for sym in "${PROBES_EQ_ARR[@]}"; do + extract_symbol "${sym}" "${TMPDIR}/${sym}.s" + if [[ ! -s "${TMPDIR}/${sym}.s" ]]; then + echo " SKIP ${sym} — 심볼 미발견" + continue + fi + br_cnt=$(grep -v '^#' "${TMPDIR}/${sym}.s" | grep -cE "${BRANCH_RE}" || true) + if [[ "${br_cnt}" -gt 1 ]]; then + echo " FAIL ${sym} — 조건분기 ${br_cnt} 건 > 허용 1 건 (비밀 의존 조기 종료 회귀 의심):" + grep -nE "${BRANCH_RE}" "${TMPDIR}/${sym}.s" | sed 's/^/ /' + fail=1 + elif grep -E "${CALL_CMP_RE}" "${TMPDIR}/${sym}.s" >/dev/null 2>&1; then + echo " FAIL ${sym} — memcmp/bcmp 호출 검출 (비-CT 라이브러리 치환 회귀):" + grep -nE "${CALL_CMP_RE}" "${TMPDIR}/${sym}.s" | sed 's/^/ /' + fail=1 + else + echo " PASS ${sym} — 조건분기 ${br_cnt} 건 (허용 상한 1 = 상수 종료조건 back-edge)" + fi +done + # zeroize probe (별도 크레이트 — 별도 release 빌드 + 별도 바이너리 디스어셈블) # zeroize/examples/zeroize_asm_probes.rs 는 constant-time 이 아닌 zeroize 소속이라 # 별도로 release 빌드하고 별도 바이너리를 디스어셈블한다. zero-fill 루프(컴파일타임 diff --git a/constant-time/src/internal.rs b/constant-time/src/internal.rs index 7cef5f7..2f81f3e 100644 --- a/constant-time/src/internal.rs +++ b/constant-time/src/internal.rs @@ -23,640 +23,32 @@ //! let equal = ct_eq64(7, 7); //! let greater = ct_gt_u32(9, 4); //! ``` -//! -//! # Authors -//! Q. T. Felix - -// -// x86_64 -// - -/// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. -/// -/// # Arguments -/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 -/// - `a`: 조건이 참일 때 반환되는 값입니다 -/// - `b`: 조건이 거짓일 때 반환되는 값입니다 -/// -/// # Safety -/// `test`와 `cmovnz` 명령만 사용하며 `nomem` 옵션으로 메모리에 접근하지 않고 -/// `nostack` 옵션으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 -/// 비밀 의존 분기가 없으므로 상수-시간 성질이 유지됩니다. -#[cfg(all(target_arch = "x86_64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_sel32(cond: u8, a: u32, b: u32) -> u32 { - let result: u32; - unsafe { - core::arch::asm!( - "test {c:e}, {c:e}", - "cmovnz {r:e}, {a:e}", - c = in(reg) cond as u32, - a = in(reg) a, - r = inout(reg) b => result, - options(nomem, nostack), - ); - } - result -} - -/// 조건이 0이 아니면 a, 0이면 b를 64비트 폭으로 상수-시간에 선택하는 함수입니다. -/// -/// # Arguments -/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 -/// - `a`: 조건이 참일 때 반환되는 값입니다 -/// - `b`: 조건이 거짓일 때 반환되는 값입니다 -/// -/// # Safety -/// `test`와 `cmovnz` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 -/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 -/// 의존 분기가 없습니다. -#[cfg(all(target_arch = "x86_64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_sel64(cond: u8, a: u64, b: u64) -> u64 { - let result: u64; - unsafe { - core::arch::asm!( - "test {c:e}, {c:e}", - "cmovnz {r}, {a}", - c = in(reg) cond as u32, - a = in(reg) a, - r = inout(reg) b => result, - options(nomem, nostack), - ); - } - result -} - -/// 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. -/// -/// # Arguments -/// - `a`: 비교 대상 첫 번째 값입니다 -/// - `b`: 비교 대상 두 번째 값입니다 -/// -/// # Safety -/// `cmp` 와 `sete` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 -/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 의존 -/// 분기가 없습니다. -#[cfg(all(target_arch = "x86_64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_eq32(a: u32, b: u32) -> u8 { - let result: u8; - unsafe { - core::arch::asm!( - "cmp {a:e}, {b:e}", - "sete {r}", - a = in(reg) a, - b = in(reg) b, - r = out(reg_byte) result, - options(nomem, nostack), - ); - } - result -} - -/// 두 값이 같으면 1, 다르면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. -/// -/// # Arguments -/// - `a`: 비교 대상 첫 번째 값입니다 -/// - `b`: 비교 대상 두 번째 값입니다 -/// -/// # Safety -/// `cmp` 와 `sete` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 -/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 의존 -/// 분기가 없습니다. -#[cfg(all(target_arch = "x86_64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_eq64(a: u64, b: u64) -> u8 { - let result: u8; - unsafe { - core::arch::asm!( - "cmp {a}, {b}", - "sete {r}", - a = in(reg) a, - b = in(reg) b, - r = out(reg_byte) result, - options(nomem, nostack), - ); - } - result -} - -/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 -/// -/// # Safety -/// `cmp`와 `seta` 명령만 사용합니다. `seta` 는 CF가 0이고 ZF가 0일 때 1을 -/// 기록하므로 a가 b보다 큰 부호 없는 비교 결과를 나타냅니다. `nomem`으로 -/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 -/// 레지스터 전용이고 비밀 의존 분기가 없습니다. -#[cfg(all(target_arch = "x86_64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_gt_u32(a: u32, b: u32) -> u8 { - let result: u8; - unsafe { - core::arch::asm!( - "cmp {a:e}, {b:e}", - "seta {r}", - a = in(reg) a, - b = in(reg) b, - r = out(reg_byte) result, - options(nomem, nostack), - ); - } - result -} - -/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 -/// -/// # Safety -/// `cmp` 와 `seta` 명령만 사용합니다. `seta` 는 CF가 0이고 ZF가 0일 때 1을 -/// 기록하므로 a가 b보다 큰 부호 없는 비교 결과를 나타냅니다. `nomem`으로 -/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 -/// 레지스터 전용이고 비밀 의존 분기가 없습니다. -#[cfg(all(target_arch = "x86_64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_gt_u64(a: u64, b: u64) -> u8 { - let result: u8; - unsafe { - core::arch::asm!( - "cmp {a}, {b}", - "seta {r}", - a = in(reg) a, - b = in(reg) b, - r = out(reg_byte) result, - options(nomem, nostack), - ); - } - result -} - -/// 부호 있는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 더 작은 부호 있는 타입은 호출자가 i64로 부호 확장한 뒤 전달합니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 있는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 있는 값입니다 -/// -/// # Safety -/// `cmp` 와 `setg` 명령만 사용합니다. `setg` 는 ZF가 0이고 SF가 OF와 같을 때 -/// 1을 기록하므로 a가 b보다 큰 부호 있는 비교 결과를 나타냅니다. `nomem` -/// 으로 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 -/// 레지스터 전용이고 비밀 의존 분기가 없습니다. -#[cfg(all(target_arch = "x86_64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_gt_i64(a: i64, b: i64) -> u8 { - let result: u8; - unsafe { - core::arch::asm!( - "cmp {a}, {b}", - "setg {r}", - a = in(reg) a, - b = in(reg) b, - r = out(reg_byte) result, - options(nomem, nostack), - ); - } - result -} - -// -// aarch64 -// 모든 연산을 64비트로 수행하며 32비트 타입은 호출 전에 영 확장 또는 부호 -// 확장합니다. -// - -/// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. -/// -/// 내부적으로 `ct_sel64`에 위임하며 인자를 64비트로 확장한 뒤 결과를 32비트로 줄입니다. -/// -/// # Arguments -/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 -/// - `a`: 조건이 참일 때 반환되는 값입니다 -/// - `b`: 조건이 거짓일 때 반환되는 값입니다 -#[cfg(all(target_arch = "aarch64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_sel32(cond: u8, a: u32, b: u32) -> u32 { - ct_sel64(cond, a as u64, b as u64) as u32 -} - -/// 조건이 0이 아니면 a, 0이면 b를 64비트 폭으로 상수-시간에 선택하는 함수입니다. -/// -/// # Arguments -/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 -/// - `a`: 조건이 참일 때 반환되는 값입니다 -/// - `b`: 조건이 거짓일 때 반환되는 값입니다 -/// -/// # Safety -/// `cmp`와 `csel` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 -/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 -/// 의존 분기가 없으므로 상수-시간 성질이 유지됩니다. -#[cfg(all(target_arch = "aarch64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_sel64(cond: u8, a: u64, b: u64) -> u64 { - let result: u64; - unsafe { - core::arch::asm!( - "cmp {c:w}, #0", - "csel {r}, {a}, {b}, ne", - c = in(reg) cond as u64, - a = in(reg) a, - b = in(reg) b, - r = out(reg) result, - options(nomem, nostack), - ); - } - result -} - -/// 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 내부적으로 `ct_eq64`에 위임하며 인자를 64비트로 확장한 뒤 비교합니다. -/// -/// # Arguments -/// - `a`: 비교 대상 첫 번째 값입니다 -/// - `b`: 비교 대상 두 번째 값입니다 -#[cfg(all(target_arch = "aarch64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_eq32(a: u32, b: u32) -> u8 { - ct_eq64(a as u64, b as u64) -} - -/// 두 값이 같으면 1, 다르면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. -/// -/// # Arguments -/// - `a`: 비교 대상 첫 번째 값입니다 -/// - `b`: 비교 대상 두 번째 값입니다 -/// -/// # Safety -/// `cmp`와 `cset eq` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 -/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 -/// 의존 분기가 없습니다. -#[cfg(all(target_arch = "aarch64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_eq64(a: u64, b: u64) -> u8 { - let result: u64; - unsafe { - core::arch::asm!( - "cmp {a}, {b}", - "cset {r}, eq", - a = in(reg) a, - b = in(reg) b, - r = out(reg) result, - options(nomem, nostack), - ); - } - result as u8 -} - -/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 내부적으로 `ct_gt_u64`에 위임하며 인자를 64비트로 확장한 뒤 비교합니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 -#[cfg(all(target_arch = "aarch64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_gt_u32(a: u32, b: u32) -> u8 { - ct_gt_u64(a as u64, b as u64) -} - -/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 -/// -/// # Safety -/// `cmp`와 `cset hi` 명령만 사용합니다. `cset hi` 는 C가 1이고 Z가 0일 때 1을 -/// 기록하므로 a가 b보다 큰 부호 없는 비교 결과를 나타냅니다. `nomem`으로 -/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 -/// 레지스터 전용이고 비밀 의존 분기가 없습니다. -#[cfg(all(target_arch = "aarch64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_gt_u64(a: u64, b: u64) -> u8 { - let result: u64; - unsafe { - core::arch::asm!( - "cmp {a}, {b}", - "cset {r}, hi", - a = in(reg) a, - b = in(reg) b, - r = out(reg) result, - options(nomem, nostack), - ); - } - result as u8 -} - -/// 부호 있는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 있는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 있는 값입니다 -/// -/// # Safety -/// `cmp`와 `cset gt` 명령만 사용합니다. `cset gt` 는 Z가 0이고 N이 V와 같을 때 -/// 1을 기록하므로 a가 b보다 큰 부호 있는 비교 결과를 나타냅니다. `nomem`으로 -/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 -/// 레지스터 전용이고 비밀 의존 분기가 없습니다. -#[cfg(all(target_arch = "aarch64", not(miri)))] -#[must_use] -#[inline] -pub(crate) fn ct_gt_i64(a: i64, b: i64) -> u8 { - let result: u64; - unsafe { - core::arch::asm!( - "cmp {a}, {b}", - "cset {r}, gt", - a = in(reg) a, - b = in(reg) b, - r = out(reg) result, - options(nomem, nostack), - ); - } - result as u8 -} - -// -// 위에서 처리하지 않은 모든 아키텍처를 위한 일반 fallback입니다. -// -// 보안 게이트: 이 일반 fallback은 best-effort 최적화 배리어인 -// `core::hint::black_box`에 의존하므로 하드웨어 수준 상수-시간을 보장하지 -// 않습니다. 검증된 인-라인 어셈블리 구현이 있는 x86_64와 aarch64만 지원 -// 타겟으로 확정하고, 그 외 아키텍처의 비 miri 빌드는 아래 `compile_error!`로 -// 컴파일 단계에서 거부하여 best-effort 경로가 고보안 빌드에 섞이지 않도록 -// 합니다. miri는 인-라인 어셈블리를 실행하지 못하므로 fallback 로직 검증을 -// 위해 예외로 둡니다. -// #[cfg(all(not(miri), not(any(target_arch = "x86_64", target_arch = "aarch64"))))] compile_error!( - "constant-time 의 검증된 상수-시간 구현은 x86_64 와 aarch64 에서만 제공됩니다. \ - 이 아키텍처의 fallback 은 black_box 기반 best-effort 라 하드웨어 수준 상수-시간을 \ - 보장하지 않으므로 고보안 빌드를 거부합니다. 테스트 목적이면 miri 로 실행하십시오." + "constant-time 의 검증된 상수-시간 구현은 x86_64와 AArch64 아키텍처에서만 제공됩니다. \ + 이 아키텍처의 fallback은 black_box 기반 방법이며, 하드웨어 수준 상수-시간을 \ + 보장하지 않으므로 고보안 빌드를 거부합니다. 테스트 목적이면 miri를 통해 실행하세요." ); -/// 조건 바이트로부터 상수-시간 비트 마스크를 생성하는 함수입니다. -/// -/// 조건이 0이 아니면 모든 비트가 1인 마스크를, 0이면 0 마스크를 반환합니다. -/// -/// # Arguments -/// - `cond`: 마스크 생성의 기준이 되는 조건 바이트입니다 -/// -/// # Security Note -/// 이 fallback 경로는 `core::hint::black_box`에 의존하는 best-effort -/// 구현이므로 상수-시간 성질이 하드웨어 수준으로 보장되지는 않습니다. -#[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[inline(never)] -pub(crate) fn ct_mask(cond: u8) -> u64 { - let c = core::hint::black_box(cond as u64); - core::hint::black_box(((c | c.wrapping_neg()) >> 63).wrapping_neg()) -} - -/// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. -/// -/// 내부적으로 `ct_sel64`에 위임합니다. -/// -/// # Arguments -/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 -/// - `a`: 조건이 참일 때 반환되는 값입니다 -/// - `b`: 조건이 거짓일 때 반환되는 값입니다 -/// -/// # Security Note -/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 -/// 수준으로 보장되지 않습니다. -#[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[must_use] -#[inline] -pub(crate) fn ct_sel32(cond: u8, a: u32, b: u32) -> u32 { - ct_sel64(cond, a as u64, b as u64) as u32 -} - -/// 마스크 연산으로 a 또는 b를 64비트 폭으로 상수-시간에 선택하는 함수입니다. -/// -/// # Arguments -/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 -/// - `a`: 조건이 참일 때 반환되는 값입니다 -/// - `b`: 조건이 거짓일 때 반환되는 값입니다 -/// -/// # Security Note -/// 입력과 중간값을 `core::hint::black_box`로 감싸 최적화를 억제하는 -/// best-effort fallback이므로 상수-시간 성질이 하드웨어 수준으로 보장되지 -/// 않습니다. -#[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[must_use] -#[inline(never)] // 최적화 기회를 줄이기 위해 인라인 방지 -pub(crate) fn ct_sel64(cond: u8, a: u64, b: u64) -> u64 { - let a = core::hint::black_box(a); - let b = core::hint::black_box(b); - let m = ct_mask(cond); - core::hint::black_box((m & a) | ((!m) & b)) -} +#[cfg(all(target_arch = "x86_64", not(miri)))] +mod x86_64; -/// 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 내부적으로 `ct_eq64`에 위임합니다. -/// -/// # Arguments -/// - `a`: 비교 대상 첫 번째 값입니다 -/// - `b`: 비교 대상 두 번째 값입니다 -/// -/// # Security Note -/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 -/// 수준으로 보장되지 않습니다. -#[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[must_use] -#[inline] -pub(crate) fn ct_eq32(a: u32, b: u32) -> u8 { - ct_eq64(a as u64, b as u64) -} +#[cfg(all(target_arch = "aarch64", not(miri)))] +mod aarch64; -/// 두 값이 같으면 1, 다르면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. -/// -/// XOR 결과가 두 값이 같을 때만 0이 되는 성질을 이용합니다. OR 시프트를 -/// 연쇄 적용해 모든 비트를 최하위 비트로 모은 뒤 마스크로 결과를 만듭니다. -/// -/// # Arguments -/// - `a`: 비교 대상 첫 번째 값입니다 -/// - `b`: 비교 대상 두 번째 값입니다 -/// -/// # Security Note -/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 -/// 수준으로 보장되지 않습니다. -#[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[must_use] -#[inline(never)] // 최적화 기회를 줄이기 위해 인라인을 방지합니다 -pub(crate) fn ct_eq64(a: u64, b: u64) -> u8 { - let a = core::hint::black_box(a); - let b = core::hint::black_box(b); - let diff = a ^ b; - let s = core::hint::black_box(diff | diff.wrapping_shr(32)); - let s = core::hint::black_box(s | s.wrapping_shr(16)); - let s = core::hint::black_box(s | s.wrapping_shr(8)); - // `s as u8`은 `diff`에 비트가 하나라도 설정되어 있으면 0이 아님(즉 a가 b와 다름) - let byte = core::hint::black_box(s as u8); - // `ct_mask`는 `byte`가 0이 아니면 0xFF..FF를, 0이면 0을 만듦(a와 b의 동등 여부) - let nonzero = ct_mask(byte); - core::hint::black_box((!nonzero & 1) as u8) -} +mod ct128; -/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// b에서 a를 뺄 때 a가 b보다 크면 언더플로가 발생하며 그 빌림이 64비트로 -/// 확장된 결과의 비트 32로 전파되는 성질을 이용합니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 -/// -/// # Security Note -/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 -/// 수준으로 보장되지 않습니다. #[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[must_use] -#[inline(never)] // 최적화 기회를 줄이기 위해 인라인 방지 -pub(crate) fn ct_gt_u32(a: u32, b: u32) -> u8 { - let a = core::hint::black_box(a); - let b = core::hint::black_box(b); - let diff = (b as u64).wrapping_sub(a as u64); - core::hint::black_box((diff >> 32) as u8 & 1) -} +mod fallback; -/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. -/// -/// 32비트 플랫폼에서 비상수-시간 라이브러리 호출을 피하기 위해 128비트 산술 -/// 대신 반워드 비교로 구성합니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 -/// -/// # Security Note -/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 -/// 수준으로 보장되지 않습니다. -#[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[must_use] -#[inline] -pub(crate) fn ct_gt_u64(a: u64, b: u64) -> u8 { - // 32비트 플랫폼에서 비상수-시간 라이브러리 호출로 컴파일될 수 있는 - // 128비트 산술을 피하고 대신 반워드 비교 사용 - // a가 b보다 큰 조건은 `a_hi`가 `b_hi`보다 크거나 `a_hi`와 `b_hi`가 같고 `a_lo`가 `b_lo`보다 큰 경우임 - let a_hi = (a >> 32) as u32; - let a_lo = a as u32; - let b_hi = (b >> 32) as u32; - let b_lo = b as u32; +#[cfg(all(target_arch = "x86_64", not(miri)))] +pub(crate) use x86_64::*; - let hi_gt = ct_gt_u32(a_hi, b_hi); - let hi_eq = ct_eq32(a_hi, b_hi); - let lo_gt = ct_gt_u32(a_lo, b_lo); +#[cfg(all(target_arch = "aarch64", not(miri)))] +pub(crate) use aarch64::*; - core::hint::black_box(hi_gt | (hi_eq & lo_gt)) -} +pub(crate) use ct128::*; -/// 부호 있는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 부호 비트를 분해하여 분기 없이 계산합니다. 같은 부호일 때는 부호 없는 -/// 대소 비교 결과를 사용하고 a가 음이 아니면서 b가 음수인 경우를 따로 -/// 더합니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 있는 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 있는 값입니다 -/// -/// # Security Note -/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 -/// 수준으로 보장되지 않습니다. #[cfg(any(miri, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] -#[must_use] -#[inline(never)] // 최적화 기회를 줄이기 위해 인라인을 방지합니다 -pub(crate) fn ct_gt_i64(a: i64, b: i64) -> u8 { - let a = core::hint::black_box(a); - let b = core::hint::black_box(b); - let a_u = a as u64; - let b_u = b as u64; - let a_msb = core::hint::black_box((a_u >> 63) as u8); // a가 음수면 1 - let b_msb = core::hint::black_box((b_u >> 63) as u8); // b가 음수면 1 - let u_gt = ct_gt_u64(a_u, b_u); - let same_sign = core::hint::black_box((a_msb ^ b_msb) ^ 1); // 두 부호가 같을 때만 1 - let not_a_msb = core::hint::black_box(a_msb ^ 1); // a가 0 이상일 때만 1 - core::hint::black_box((same_sign & u_gt) | (not_a_msb & b_msb)) -} - -// -// 아키텍처 독립 128비트 프리미티브입니다. -// 위의 아키텍처별 64비트 함수 위에 구성됩니다. -// - -/// 128비트 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 상위 64비트와 하위 64비트를 각각 `ct_eq64`로 비교한 결과를 결합합니다. -/// -/// # Arguments -/// - `a`: 비교 대상 첫 번째 128비트 값입니다 -/// - `b`: 비교 대상 두 번째 128비트 값입니다 -#[must_use] -#[inline] -pub(crate) fn ct_eq128(a: u128, b: u128) -> u8 { - // 상위 절반과 하위 절반이 모두 같아야 합니다 - ct_eq64((a >> 64) as u64, (b >> 64) as u64) & ct_eq64(a as u64, b as u64) -} - -/// 부호 없는 128비트 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 64비트 프리미티브 위에 구성되며 상위 절반과 하위 절반을 단계적으로 비교합니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 없는 128비트 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 없는 128비트 값입니다 -#[must_use] -#[inline] -pub(crate) fn ct_gt_u128(a: u128, b: u128) -> u8 { - // a가 b보다 큰 조건은 상위 절반이 다르고 `a_hi`가 `b_hi`보다 크거나 - // 상위 절반이 같고 `a_lo`가 `b_lo`보다 큰 경우임 - let hi_gt = ct_gt_u64((a >> 64) as u64, (b >> 64) as u64); - let hi_eq = ct_eq64((a >> 64) as u64, (b >> 64) as u64); - let lo_gt = ct_gt_u64(a as u64, b as u64); - hi_gt | (hi_eq & lo_gt) -} - -/// 부호 있는 128비트 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. -/// -/// 64비트 프리미티브 위에 구성되며 부호 비트를 분해해 분기 없이 계산합니다. -/// -/// # Arguments -/// - `a`: 큰지 비교할 첫 번째 부호 있는 128비트 값입니다 -/// - `b`: 기준이 되는 두 번째 부호 있는 128비트 값입니다 -#[must_use] -#[inline] -pub(crate) fn ct_gt_i128(a: i128, b: i128) -> u8 { - let a_u = a as u128; - let b_u = b as u128; - let a_msb = (a_u >> 127) as u8; - let b_msb = (b_u >> 127) as u8; - let u_gt = ct_gt_u128(a_u, b_u); - let same_sign = (a_msb ^ b_msb) ^ 1; - let not_a_msb = a_msb ^ 1; - (same_sign & u_gt) | (not_a_msb & b_msb) -} +pub(crate) use fallback::*; diff --git a/constant-time/src/internal/aarch64.rs b/constant-time/src/internal/aarch64.rs new file mode 100644 index 0000000..76fe7db --- /dev/null +++ b/constant-time/src/internal/aarch64.rs @@ -0,0 +1,158 @@ +// +// AArch64 아키텍처에서 사용되는 내부 함수 모듈입니다. +// +// 모든 연산을 64비트로 수행하며 32비트 타입은 호출 전에 영 확장 또는 부호 +// 확장합니다. +// + +/// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. +/// +/// 내부적으로 `ct_sel64`에 위임하며 인자를 64비트로 확장한 뒤 결과를 32비트로 줄입니다. +/// +/// # Arguments +/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 +/// - `a`: 조건이 참일 때 반환되는 값입니다 +/// - `b`: 조건이 거짓일 때 반환되는 값입니다 +#[must_use] +#[inline] +pub(crate) fn ct_sel32(cond: u8, a: u32, b: u32) -> u32 { + ct_sel64(cond, a as u64, b as u64) as u32 +} + +/// 조건이 0이 아니면 a, 0이면 b를 64비트 폭으로 상수-시간에 선택하는 함수입니다. +/// +/// # Arguments +/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 +/// - `a`: 조건이 참일 때 반환되는 값입니다 +/// - `b`: 조건이 거짓일 때 반환되는 값입니다 +/// +/// # Safety +/// `cmp`와 `csel` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 +/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 +/// 의존 분기가 없으므로 상수-시간 성질이 유지됩니다. +#[must_use] +#[inline] +pub(crate) fn ct_sel64(cond: u8, a: u64, b: u64) -> u64 { + let result: u64; + unsafe { + core::arch::asm!( + "cmp {c:w}, #0", + "csel {r}, {a}, {b}, ne", + c = in(reg) cond as u64, + a = in(reg) a, + b = in(reg) b, + r = out(reg) result, + options(nomem, nostack), + ); + } + result +} + +/// 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 내부적으로 `ct_eq64`에 위임하며 인자를 64비트로 확장한 뒤 비교합니다. +/// +/// # Arguments +/// - `a`: 비교 대상 첫 번째 값입니다 +/// - `b`: 비교 대상 두 번째 값입니다 +#[must_use] +#[inline] +pub(crate) fn ct_eq32(a: u32, b: u32) -> u8 { + ct_eq64(a as u64, b as u64) +} + +/// 두 값이 같으면 1, 다르면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. +/// +/// # Arguments +/// - `a`: 비교 대상 첫 번째 값입니다 +/// - `b`: 비교 대상 두 번째 값입니다 +/// +/// # Safety +/// `cmp`와 `cset eq` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 +/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 +/// 의존 분기가 없습니다. +#[must_use] +#[inline] +pub(crate) fn ct_eq64(a: u64, b: u64) -> u8 { + let result: u64; + unsafe { + core::arch::asm!( + "cmp {a}, {b}", + "cset {r}, eq", + a = in(reg) a, + b = in(reg) b, + r = out(reg) result, + options(nomem, nostack), + ); + } + result as u8 +} + +/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 내부적으로 `ct_gt_u64`에 위임하며 인자를 64비트로 확장한 뒤 비교합니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 +#[must_use] +#[inline] +pub(crate) fn ct_gt_u32(a: u32, b: u32) -> u8 { + ct_gt_u64(a as u64, b as u64) +} + +/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 +/// +/// # Safety +/// `cmp`와 `cset hi` 명령만 사용합니다. `cset hi` 는 C가 1이고 Z가 0일 때 1을 +/// 기록하므로 a가 b보다 큰 부호 없는 비교 결과를 나타냅니다. `nomem`으로 +/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 +/// 레지스터 전용이고 비밀 의존 분기가 없습니다. +#[must_use] +#[inline] +pub(crate) fn ct_gt_u64(a: u64, b: u64) -> u8 { + let result: u64; + unsafe { + core::arch::asm!( + "cmp {a}, {b}", + "cset {r}, hi", + a = in(reg) a, + b = in(reg) b, + r = out(reg) result, + options(nomem, nostack), + ); + } + result as u8 +} + +/// 부호 있는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 있는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 있는 값입니다 +/// +/// # Safety +/// `cmp`와 `cset gt` 명령만 사용합니다. `cset gt` 는 Z가 0이고 N이 V와 같을 때 +/// 1을 기록하므로 a가 b보다 큰 부호 있는 비교 결과를 나타냅니다. `nomem`으로 +/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 +/// 레지스터 전용이고 비밀 의존 분기가 없습니다. +#[must_use] +#[inline] +pub(crate) fn ct_gt_i64(a: i64, b: i64) -> u8 { + let result: u64; + unsafe { + core::arch::asm!( + "cmp {a}, {b}", + "cset {r}, gt", + a = in(reg) a, + b = in(reg) b, + r = out(reg) result, + options(nomem, nostack), + ); + } + result as u8 +} diff --git a/constant-time/src/internal/ct128.rs b/constant-time/src/internal/ct128.rs new file mode 100644 index 0000000..f231164 --- /dev/null +++ b/constant-time/src/internal/ct128.rs @@ -0,0 +1,56 @@ +// +// 아키텍처 독립 128비트 프리미티브를 제공하는 모듈입니다. +// +// 아키텍처별 64비트 함수 위에 구성됩니다. +// + +use crate::internal::{ct_eq64, ct_gt_u64}; + +/// 128비트 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 상위 64비트와 하위 64비트를 각각 `ct_eq64`로 비교한 결과를 결합합니다. +/// +/// # Arguments +/// - `a`: 비교 대상 첫 번째 128비트 값입니다 +/// - `b`: 비교 대상 두 번째 128비트 값입니다 +#[must_use] +#[inline] +pub(crate) fn ct_eq128(a: u128, b: u128) -> u8 { + ct_eq64((a >> 64) as u64, (b >> 64) as u64) & ct_eq64(a as u64, b as u64) +} + +/// 부호 없는 128비트 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 64비트 프리미티브 위에 구성되며 상위 절반과 하위 절반을 단계적으로 비교합니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 없는 128비트 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 없는 128비트 값입니다 +#[must_use] +#[inline] +pub(crate) fn ct_gt_u128(a: u128, b: u128) -> u8 { + let hi_gt = ct_gt_u64((a >> 64) as u64, (b >> 64) as u64); + let hi_eq = ct_eq64((a >> 64) as u64, (b >> 64) as u64); + let lo_gt = ct_gt_u64(a as u64, b as u64); + hi_gt | (hi_eq & lo_gt) +} + +/// 부호 있는 128비트 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 64비트 프리미티브 위에 구성되며 부호 비트를 분해해 분기 없이 계산합니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 있는 128비트 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 있는 128비트 값입니다 +#[must_use] +#[inline] +pub(crate) fn ct_gt_i128(a: i128, b: i128) -> u8 { + let a_u = a as u128; + let b_u = b as u128; + let a_msb = (a_u >> 127) as u8; + let b_msb = (b_u >> 127) as u8; + let u_gt = ct_gt_u128(a_u, b_u); + let same_sign = (a_msb ^ b_msb) ^ 1; + let not_a_msb = a_msb ^ 1; + (same_sign & u_gt) | (not_a_msb & b_msb) +} diff --git a/constant-time/src/internal/fallback.rs b/constant-time/src/internal/fallback.rs new file mode 100644 index 0000000..b4eda81 --- /dev/null +++ b/constant-time/src/internal/fallback.rs @@ -0,0 +1,189 @@ +// +// 다른 아키텍처에 의해 처리되지 않는 모든 아키텍처를 위한 일반 fallback 모듈입니다. 이 모듈은 다음의 보안 게이트를 +// 형성합니다. +// +// 이 일반 fallback은 best-effort 최적화 배리어인 `core::hint::black_box`에 의존하므로 하드웨어 +// 수준 상수-시간을 보장하지 않습니다. 검증된 인-라인 어셈블리 구현이 있는 x86_64와 aarch64만 지원 타겟으로 +// 확정하고, 그 외 아키텍처의 비 miri 빌드는 아래 `compile_error!`로 컴파일 단계에서 거부하여 best- +// effort 경로가 고보안 빌드에 섞이지 않도록 합니다. miri는 인-라인 어셈블리를 실행하지 못하므로 fallback +// 로직 검증을 위해 예외로 둡니다. +// + +/// 조건 바이트로부터 상수-시간 비트 마스크를 생성하는 함수입니다. +/// +/// 조건이 0이 아니면 모든 비트가 1인 마스크를, 0이면 0 마스크를 반환합니다. +/// +/// # Arguments +/// - `cond`: 마스크 생성의 기준이 되는 조건 바이트입니다 +/// +/// # Security Note +/// 이 fallback 경로는 `core::hint::black_box`에 의존하는 best-effort +/// 구현이므로 상수-시간 성질이 하드웨어 수준으로 보장되지는 않습니다. + +#[inline(never)] +pub(crate) fn ct_mask(cond: u8) -> u64 { + let c = core::hint::black_box(cond as u64); + core::hint::black_box(((c | c.wrapping_neg()) >> 63).wrapping_neg()) +} + +/// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. +/// +/// 내부적으로 `ct_sel64`에 위임합니다. +/// +/// # Arguments +/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 +/// - `a`: 조건이 참일 때 반환되는 값입니다 +/// - `b`: 조건이 거짓일 때 반환되는 값입니다 +/// +/// # Security Note +/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 +/// 수준으로 보장되지 않습니다. +#[must_use] +#[inline] +pub(crate) fn ct_sel32(cond: u8, a: u32, b: u32) -> u32 { + ct_sel64(cond, a as u64, b as u64) as u32 +} + +/// 마스크 연산으로 a 또는 b를 64비트 폭으로 상수-시간에 선택하는 함수입니다. +/// +/// # Arguments +/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 +/// - `a`: 조건이 참일 때 반환되는 값입니다 +/// - `b`: 조건이 거짓일 때 반환되는 값입니다 +/// +/// # Security Note +/// 입력과 중간값을 `core::hint::black_box`로 감싸 최적화를 억제하는 +/// best-effort fallback이므로 상수-시간 성질이 하드웨어 수준으로 보장되지 +/// 않습니다. +#[must_use] +#[inline(never)] +pub(crate) fn ct_sel64(cond: u8, a: u64, b: u64) -> u64 { + let a = core::hint::black_box(a); + let b = core::hint::black_box(b); + let m = ct_mask(cond); + core::hint::black_box((m & a) | ((!m) & b)) +} + +/// 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 내부적으로 `ct_eq64`에 위임합니다. +/// +/// # Arguments +/// - `a`: 비교 대상 첫 번째 값입니다 +/// - `b`: 비교 대상 두 번째 값입니다 +/// +/// # Security Note +/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 +/// 수준으로 보장되지 않습니다. +#[must_use] +#[inline] +pub(crate) fn ct_eq32(a: u32, b: u32) -> u8 { + ct_eq64(a as u64, b as u64) +} + +/// 두 값이 같으면 1, 다르면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. +/// +/// XOR 결과가 두 값이 같을 때만 0이 되는 성질을 이용합니다. OR 시프트를 +/// 연쇄 적용해 모든 비트를 최하위 비트로 모은 뒤 마스크로 결과를 만듭니다. +/// +/// # Arguments +/// - `a`: 비교 대상 첫 번째 값입니다 +/// - `b`: 비교 대상 두 번째 값입니다 +/// +/// # Security Note +/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 +/// 수준으로 보장되지 않습니다. +#[must_use] +#[inline(never)] +pub(crate) fn ct_eq64(a: u64, b: u64) -> u8 { + let a = core::hint::black_box(a); + let b = core::hint::black_box(b); + let diff = a ^ b; + let s = core::hint::black_box(diff | diff.wrapping_shr(32)); + let s = core::hint::black_box(s | s.wrapping_shr(16)); + let s = core::hint::black_box(s | s.wrapping_shr(8)); + // `s as u8`은 `diff`에 비트가 하나라도 설정되어 있으면 0이 아님(즉 a가 b와 다름) + let byte = core::hint::black_box(s as u8); + // `ct_mask`는 `byte`가 0이 아니면 0xFF..FF를, 0이면 0을 만듦(a와 b의 동등 여부) + let nonzero = ct_mask(byte); + core::hint::black_box((!nonzero & 1) as u8) +} + +/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// b에서 a를 뺄 때 a가 b보다 크면 언더플로가 발생하며 그 빌림이 64비트로 +/// 확장된 결과의 비트 32로 전파되는 성질을 이용합니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 +/// +/// # Security Note +/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 +/// 수준으로 보장되지 않습니다. +#[must_use] +#[inline(never)] +pub(crate) fn ct_gt_u32(a: u32, b: u32) -> u8 { + let a = core::hint::black_box(a); + let b = core::hint::black_box(b); + let diff = (b as u64).wrapping_sub(a as u64); + core::hint::black_box((diff >> 32) as u8 & 1) +} + +/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. +/// +/// 32비트 플랫폼에서 비상수-시간 라이브러리 호출을 피하기 위해 128비트 산술 +/// 대신 반워드 비교로 구성합니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 +/// +/// # Security Note +/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 +/// 수준으로 보장되지 않습니다. +#[must_use] +#[inline(never)] +pub(crate) fn ct_gt_u64(a: u64, b: u64) -> u8 { + // 32비트 플랫폼에서 비상수-시간 라이브러리 호출로 컴파일될 수 있는 + // 128비트 산술을 피하고 대신 반워드 비교 사용 + // a가 b보다 큰 조건은 `a_hi`가 `b_hi`보다 크거나 `a_hi`와 `b_hi`가 같고 `a_lo`가 `b_lo`보다 큰 경우임 + let a_hi = (a >> 32) as u32; + let a_lo = a as u32; + let b_hi = (b >> 32) as u32; + let b_lo = b as u32; + + let hi_gt = ct_gt_u32(a_hi, b_hi); + let hi_eq = ct_eq32(a_hi, b_hi); + let lo_gt = ct_gt_u32(a_lo, b_lo); + + core::hint::black_box(hi_gt | (hi_eq & lo_gt)) +} + +/// 부호 있는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 부호 비트를 분해하여 분기 없이 계산합니다. 같은 부호일 때는 부호 없는 +/// 대소 비교 결과를 사용하고 a가 음이 아니면서 b가 음수인 경우를 따로 +/// 더합니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 있는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 있는 값입니다 +/// +/// # Security Note +/// `black_box` 기반 best-effort fallback이므로 상수-시간 성질이 하드웨어 +/// 수준으로 보장되지 않습니다. +#[must_use] +#[inline(never)] // 최적화 기회를 줄이기 위해 인라인을 방지합니다 +pub(crate) fn ct_gt_i64(a: i64, b: i64) -> u8 { + let a = core::hint::black_box(a); + let b = core::hint::black_box(b); + let a_u = a as u64; + let b_u = b as u64; + let a_msb = core::hint::black_box((a_u >> 63) as u8); // a가 음수면 1 + let b_msb = core::hint::black_box((b_u >> 63) as u8); // b가 음수면 1 + let u_gt = ct_gt_u64(a_u, b_u); + let same_sign = core::hint::black_box((a_msb ^ b_msb) ^ 1); // 두 부호가 같을 때만 1 + let not_a_msb = core::hint::black_box(a_msb ^ 1); // a가 0 이상일 때만 1 + core::hint::black_box((same_sign & u_gt) | (not_a_msb & b_msb)) +} diff --git a/constant-time/src/internal/x86_64.rs b/constant-time/src/internal/x86_64.rs new file mode 100644 index 0000000..5290459 --- /dev/null +++ b/constant-time/src/internal/x86_64.rs @@ -0,0 +1,205 @@ +// +// x86_64 아키텍처에서 사용되는 내부 함수 모듈입니다. +// + +/// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. +/// +/// # Arguments +/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 +/// - `a`: 조건이 참일 때 반환되는 값입니다 +/// - `b`: 조건이 거짓일 때 반환되는 값입니다 +/// +/// # Safety +/// `test`와 `cmovnz` 명령만 사용하며 `nomem` 옵션으로 메모리에 접근하지 않고 +/// `nostack` 옵션으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 +/// 비밀 의존 분기가 없으므로 상수-시간 성질이 유지됩니다. +#[must_use] +#[inline] +pub(crate) fn ct_sel32(cond: u8, a: u32, b: u32) -> u32 { + let result: u32; + unsafe { + core::arch::asm!( + "test {c:e}, {c:e}", + "cmovnz {r:e}, {a:e}", + c = in(reg) cond as u32, + a = in(reg) a, + r = inout(reg) b => result, + options(nomem, nostack), + ); + } + result +} + +/// 조건이 0이 아니면 a, 0이면 b를 64비트 폭으로 상수-시간에 선택하는 함수입니다. +/// +/// # Arguments +/// - `cond`: 0이 아니면 a를, 0이면 b를 선택하는 조건 바이트입니다 +/// - `a`: 조건이 참일 때 반환되는 값입니다 +/// - `b`: 조건이 거짓일 때 반환되는 값입니다 +/// +/// # Safety +/// `test`와 `cmovnz` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 +/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 +/// 의존 분기가 없습니다. +#[cfg(all(target_arch = "x86_64", not(miri)))] +#[must_use] +#[inline] +pub(crate) fn ct_sel64(cond: u8, a: u64, b: u64) -> u64 { + let result: u64; + unsafe { + core::arch::asm!( + "test {c:e}, {c:e}", + "cmovnz {r}, {a}", + c = in(reg) cond as u32, + a = in(reg) a, + r = inout(reg) b => result, + options(nomem, nostack), + ); + } + result +} + +/// 두 값이 같으면 1, 다르면 0을 상수-시간에 반환하는 함수입니다. +/// +/// # Arguments +/// - `a`: 비교 대상 첫 번째 값입니다 +/// - `b`: 비교 대상 두 번째 값입니다 +/// +/// # Safety +/// `cmp` 와 `sete` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 +/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 의존 +/// 분기가 없습니다. +#[cfg(all(target_arch = "x86_64", not(miri)))] +#[must_use] +#[inline] +pub(crate) fn ct_eq32(a: u32, b: u32) -> u8 { + let result: u8; + unsafe { + core::arch::asm!( + "cmp {a:e}, {b:e}", + "sete {r}", + a = in(reg) a, + b = in(reg) b, + r = out(reg_byte) result, + options(nomem, nostack), + ); + } + result +} + +/// 두 값이 같으면 1, 다르면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. +/// +/// # Arguments +/// - `a`: 비교 대상 첫 번째 값입니다 +/// - `b`: 비교 대상 두 번째 값입니다 +/// +/// # Safety +/// `cmp` 와 `sete` 명령만 사용하며 `nomem`으로 메모리에 접근하지 않고 +/// `nostack`으로 스택을 사용하지 않습니다. 피연산자는 레지스터 전용이고 비밀 의존 +/// 분기가 없습니다. +#[cfg(all(target_arch = "x86_64", not(miri)))] +#[must_use] +#[inline] +pub(crate) fn ct_eq64(a: u64, b: u64) -> u8 { + let result: u8; + unsafe { + core::arch::asm!( + "cmp {a}, {b}", + "sete {r}", + a = in(reg) a, + b = in(reg) b, + r = out(reg_byte) result, + options(nomem, nostack), + ); + } + result +} + +/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 +/// +/// # Safety +/// `cmp`와 `seta` 명령만 사용합니다. `seta` 는 CF가 0이고 ZF가 0일 때 1을 +/// 기록하므로 a가 b보다 큰 부호 없는 비교 결과를 나타냅니다. `nomem`으로 +/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 +/// 레지스터 전용이고 비밀 의존 분기가 없습니다. +#[cfg(all(target_arch = "x86_64", not(miri)))] +#[must_use] +#[inline] +pub(crate) fn ct_gt_u32(a: u32, b: u32) -> u8 { + let result: u8; + unsafe { + core::arch::asm!( + "cmp {a:e}, {b:e}", + "seta {r}", + a = in(reg) a, + b = in(reg) b, + r = out(reg_byte) result, + options(nomem, nostack), + ); + } + result +} + +/// 부호 없는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 64비트 폭으로 상수-시간에 반환하는 함수입니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 없는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 없는 값입니다 +/// +/// # Safety +/// `cmp` 와 `seta` 명령만 사용합니다. `seta` 는 CF가 0이고 ZF가 0일 때 1을 +/// 기록하므로 a가 b보다 큰 부호 없는 비교 결과를 나타냅니다. `nomem`으로 +/// 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 +/// 레지스터 전용이고 비밀 의존 분기가 없습니다. +#[cfg(all(target_arch = "x86_64", not(miri)))] +#[must_use] +#[inline] +pub(crate) fn ct_gt_u64(a: u64, b: u64) -> u8 { + let result: u8; + unsafe { + core::arch::asm!( + "cmp {a}, {b}", + "seta {r}", + a = in(reg) a, + b = in(reg) b, + r = out(reg_byte) result, + options(nomem, nostack), + ); + } + result +} + +/// 부호 있는 두 값에 대해 a가 b보다 크면 1, 아니면 0을 상수-시간에 반환하는 함수입니다. +/// +/// 더 작은 부호 있는 타입은 호출자가 i64로 부호 확장한 뒤 전달합니다. +/// +/// # Arguments +/// - `a`: 큰지 비교할 첫 번째 부호 있는 값입니다 +/// - `b`: 기준이 되는 두 번째 부호 있는 값입니다 +/// +/// # Safety +/// `cmp` 와 `setg` 명령만 사용합니다. `setg` 는 ZF가 0이고 SF가 OF와 같을 때 +/// 1을 기록하므로 a가 b보다 큰 부호 있는 비교 결과를 나타냅니다. `nomem` +/// 으로 메모리에 접근하지 않고 `nostack`으로 스택을 사용하지 않으며 피연산자는 +/// 레지스터 전용이고 비밀 의존 분기가 없습니다. +#[cfg(all(target_arch = "x86_64", not(miri)))] +#[must_use] +#[inline] +pub(crate) fn ct_gt_i64(a: i64, b: i64) -> u8 { + let result: u8; + unsafe { + core::arch::asm!( + "cmp {a}, {b}", + "setg {r}", + a = in(reg) a, + b = in(reg) b, + r = out(reg_byte) result, + options(nomem, nostack), + ); + } + result +} diff --git a/constant-time/src/lib.rs b/constant-time/src/lib.rs index 89f9d22..5d759a1 100644 --- a/constant-time/src/lib.rs +++ b/constant-time/src/lib.rs @@ -2,7 +2,7 @@ //! //! 비밀 값에 의존하는 분기나 데이터 의존 메모리 접근 없이 값 선택과 동등 //! 비교, 대소 비교를 수행하는 `Choice` 타입과 `CtSelOps`, `CtEqOps`, -//! `CtGreeter`, `CtLess` 트레이트를 제공합니다. 저수준 상수-시간 프리미티브는 +//! `CtGtOps`, `CtLess` 트레이트를 제공합니다. 저수준 상수-시간 프리미티브는 //! `internal` 모듈에 있으며 x86_64와 aarch64에서는 인-라인 어셈블리로, 그 외 //! 아키텍처에서는 `black_box` 기반 best-effort fallback으로 동작합니다. //! @@ -10,25 +10,23 @@ //! - `Choice`: 항상 0 또는 1 값을 갖는 상수-시간 bool이며 비트 연산으로 조합됩니다 //! - `CtSelOps`: 조건에 따라 두 값 중 하나를 선택하고 대입과 교환을 파생합니다 //! - `CtEqOps`: 두 값의 동등 여부를 상수-시간에 판정합니다 -//! - `CtGreeter`: 두 값의 대소를 상수-시간에 판정합니다 -//! - `CtLess`: `CtEqOps`와 `CtGreeter`를 만족하는 모든 타입에 자동으로 제공됩니다 +//! - `CtGtOps`: 두 값의 대소를 상수-시간에 판정합니다 +//! - `CtLess`: `CtEqOps`와 `CtGtOps`를 만족하는 모든 타입에 자동으로 제공됩니다 //! //! # Examples //! ```rust,ignore //! let cond = Choice::from_u8(1); //! let selected = u32::select(&10, &20, cond); -//! let equal = 7u32.eq(&7); -//! let greater = 9u32.gt(&4); +//! let equal = 7u32.ct_eq(&7); +//! let greater = 9u32.ct_gt(&4); //! ``` -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] mod internal; +pub mod traits; +use crate::private::Sealed; use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; -use internal::*; // // Choice @@ -68,7 +66,7 @@ impl Choice { } // -// 비트 연산 +// Bits ops - start // // 피연산자가 0 또는 1이므로 `&`, `|`, `^`는 정규화 없이 불변을 보존합니다 // `Not`은 1과의 XOR로 분기 없이 0과 1을 뒤집습니다 @@ -129,125 +127,13 @@ impl Not for Choice { } // -// CtSelOps 트레이트 (select, assign, swap) +// Bits ops - end // -// select(a, b, choice)는 choice가 1이면 *b를, 0이면 *a를 반환하며 -// assign과 swap은 select에서 파생됩니다 + // -// ct_sel(cond, x, y)가 cond가 0이 아닐 때 x를 반환하므로 -// select(a, b, c)는 ct_sel(c.0, *b, *a)와 같습니다 +// Sealed - start // -/// 조건에 따라 두 값 중 하나를 상수-시간에 선택하는 연산을 정의하는 트레이트입니다. -/// -/// `assign`과 `swap`은 `select`에서 파생됩니다. -pub trait CtSelOps: Copy + private::Sealed { - /// `choice`가 1이면 `b`를, 0이면 `a`를 상수-시간에 선택하여 반환하는 함수입니다. - /// - /// # Arguments - /// - `a`: `choice`가 0일 때 선택되는 값입니다 - /// - `b`: `choice`가 1일 때 선택되는 값입니다 - /// - `choice`: 선택 조건입니다 - fn select(a: &Self, b: &Self, choice: Choice) -> Self; - - /// `choice`가 1이면 `other`를 자신에게 상수-시간에 대입하는 함수입니다. - /// - /// # Arguments - /// - `other`: 대입 후보 값입니다 - /// - `choice`: 대입 조건입니다 - #[inline] - fn assign(&mut self, other: &Self, choice: Choice) { - *self = Self::select(self, other, choice); - } - - /// `choice`가 1이면 `a`와 `b`를 상수-시간에 교환하는 함수입니다. - /// - /// # Arguments - /// - `a`: 교환 대상 첫 번째 값입니다 - /// - `b`: 교환 대상 두 번째 값입니다 - /// - `choice`: 교환 조건이며 0이면 두 값을 그대로 둡니다 - /// - /// # Safety - /// 임시 변수 `t`는 `Self: Copy + Sized`이며 함수 로컬 스택 슬롯에만 - /// 존재합니다. 각 바이트의 volatile write는 별칭 없는 유일 포인터로 - /// 수행되므로 경합이 없고 `write_volatile`는 죽은 코드 제거 대상이 - /// 아닙니다. 또한 `CtSelOps`는 `private::Sealed`로 봉인되어 구현자가 - /// 고정폭 정수로 제한되므로 all-zero 비트 패턴이 항상 `Self`의 유효한 - /// 값이며 volatile 0 덮어쓰기가 invalid value를 만들지 않습니다. - /// - /// # Security Note - /// 임시 변수 `t`는 `*a`의 평문 사본을 일시적으로 보유합니다. 함수 종료 시 - /// `size_of::()` 바이트 전 영역을 volatile 0으로 덮어쓴 뒤 - /// `compiler_fence(SeqCst)`로 store 재정렬을 차단하여 CWE-316 (cleartext - /// storage in memory) 잔재를 방지합니다. 추가로 `black_box(&mut t)`가 스택 - /// 슬롯의 escape를 강제하여 최적화기가 임시 변수를 레지스터에만 유지하지 - /// 못하도록 합니다. - #[inline] - fn swap(a: &mut Self, b: &mut Self, choice: Choice) { - let mut t: Self = *a; - let _ = core::hint::black_box(&mut t); - a.assign(b, choice); - b.assign(&t, choice); - // SAFETY: `t`는 함수 로컬 스택 슬롯의 유일 포인터이며 각 바이트의 - // volatile write는 별칭과 경합이 없고 죽은 코드 제거 대상이 - // 아닙니다 - let size = core::mem::size_of::(); - let ptr = core::ptr::from_mut(&mut t).cast::(); - for i in 0..size { - unsafe { - core::ptr::write_volatile(ptr.add(i), 0); - } - } - core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); - let _ = core::hint::black_box(&mut t); - } -} - -macro_rules! impl_sel_via32 { - ($($t:ty),+) => { - $( - impl CtSelOps for $t { - #[inline] - fn select(a: &Self, b: &Self, choice: Choice) -> Self { - ct_sel32(choice.0, *b as u32, *a as u32) as $t - } - } - )+ - }; -} - -macro_rules! impl_sel_via64 { - ($($t:ty),+) => { - $( - impl CtSelOps for $t { - #[inline] - fn select(a: &Self, b: &Self, choice: Choice) -> Self { - ct_sel64(choice.0, *b as u64, *a as u64) as $t - } - } - )+ - }; -} - -impl_sel_via32!(u8, u16, u32, i8, i16, i32); -impl_sel_via64!(u64, i64, usize, isize); - -impl CtSelOps for u128 { - #[inline] - fn select(a: &Self, b: &Self, choice: Choice) -> Self { - let hi = ct_sel64(choice.0, (*b >> 64) as u64, (*a >> 64) as u64) as u128; - let lo = ct_sel64(choice.0, *b as u64, *a as u64) as u128; - (hi << 64) | lo - } -} - -impl CtSelOps for i128 { - #[inline] - fn select(a: &Self, b: &Self, choice: Choice) -> Self { - u128::select(&(*a as u128), &(*b as u128), choice) as i128 - } -} - mod private { pub trait Sealed {} } @@ -264,193 +150,18 @@ impl_sealed!( u8, u16, u32, i8, i16, i32, u64, i64, usize, isize, u128, i128 ); -// -// CtEqOps 트레이트 (eq, ne) -// -// eq는 self와 other가 같으면 Choice(1)을, 아니면 Choice(0)을 반환하며 -// ne는 !eq로 파생됩니다 -// -// 동등성은 부호와 무관하며 두 값의 비트 패턴이 같을 때만 같습니다. 부호 있는 -// 타입은 비교 전에 부호 확장으로 넓히는데 두 피연산자가 같은 확장을 거치므로 -// 결과가 정확합니다 -// +impl Sealed for [T; N] {} -/// 두 값의 동등 여부를 상수-시간에 판정하는 연산을 정의하는 트레이트입니다. -pub trait CtEqOps { - /// 자신과 `other`가 같으면 `Choice(1)`을, 다르면 `Choice(0)`을 반환하는 함수입니다. - /// - /// # Arguments - /// - `other`: 비교 대상 값입니다 - fn eq(&self, other: &Self) -> Choice; - - /// 자신과 `other`가 다르면 `Choice(1)`을, 같으면 `Choice(0)`을 반환하는 함수입니다. - /// - /// # Arguments - /// - `other`: 비교 대상 값입니다 - #[inline] - fn ne(&self, other: &Self) -> Choice { - !self.eq(other) - } -} - -macro_rules! impl_eq_via32 { - ($($t:ty),+) => { - $( - impl CtEqOps for $t { - #[inline] - fn eq(&self, other: &Self) -> Choice { - Choice(ct_eq32(*self as u32, *other as u32)) - } - } - )+ - }; -} - -macro_rules! impl_eq_via64 { - ($($t:ty),+) => { - $( - impl CtEqOps for $t { - #[inline] - fn eq(&self, other: &Self) -> Choice { - Choice(ct_eq64(*self as u64, *other as u64)) - } - } - )+ - }; -} - -impl_eq_via32!(u8, u16, u32, i8, i16, i32); -impl_eq_via64!(u64, i64, usize, isize); - -impl CtEqOps for u128 { - #[inline] - fn eq(&self, other: &Self) -> Choice { - Choice(ct_eq128(*self, *other)) - } -} - -impl CtEqOps for i128 { - #[inline] - fn eq(&self, other: &Self) -> Choice { - Choice(ct_eq128(*self as u128, *other as u128)) - } -} - -// -// CtGreeter 트레이트 (gt) // -// gt는 self가 other보다 크면 Choice(1)을, 아니면 Choice(0)을 반환합니다 +// Sealed - end // -// 부호 없는 타입은 32비트 또는 64비트로 영 확장합니다 -// 부호 있는 타입은 i64로 부호 확장하는데 2의 보수 부호 확장이 각 타입 범위 -// 안에서 단조이므로 순서가 보존됩니다 -// - -/// 두 값의 대소를 상수-시간에 판정하는 연산을 정의하는 트레이트입니다. -pub trait CtGreeter { - /// 자신이 `other`보다 크면 `Choice(1)`을, 아니면 `Choice(0)`을 반환하는 함수입니다. - /// - /// # Arguments - /// - `other`: 비교 대상 값입니다 - fn gt(&self, other: &Self) -> Choice; -} - -macro_rules! impl_gt_unsigned_via32 { - ($($t:ty),+) => { - $( - impl CtGreeter for $t { - #[inline] - fn gt(&self, other: &Self) -> Choice { - Choice(ct_gt_u32(*self as u32, *other as u32)) - } - } - )+ - }; -} - -macro_rules! impl_gt_unsigned_via64 { - ($($t:ty),+) => { - $( - impl CtGreeter for $t { - #[inline] - fn gt(&self, other: &Self) -> Choice { - Choice(ct_gt_u64(*self as u64, *other as u64)) - } - } - )+ - }; -} - -// 부호 있는 타입은 ct_gt_i64 호출 전에 i64로 부호 확장합니다 -// i8 as i64, i16 as i64, i32 as i64는 모두 부호 확장을 수행합니다 -// 64비트 플랫폼에서는 isize가 i64와 같으므로 캐스팅에 손실이 없습니다 -macro_rules! impl_gt_signed_via64 { - ($($t:ty),+) => { - $( - impl CtGreeter for $t { - #[inline] - fn gt(&self, other: &Self) -> Choice { - Choice(ct_gt_i64(*self as i64, *other as i64)) - } - } - )+ - }; -} - -impl_gt_unsigned_via32!(u8, u16, u32); -impl_gt_unsigned_via64!(u64, usize); -impl_gt_signed_via64!(i8, i16, i32, i64, isize); - -impl CtGreeter for u128 { - #[inline] - fn gt(&self, other: &Self) -> Choice { - Choice(ct_gt_u128(*self, *other)) - } -} - -impl CtGreeter for i128 { - #[inline] - fn gt(&self, other: &Self) -> Choice { - Choice(ct_gt_i128(*self, *other)) - } -} -// -// CtLess 트레이트 (lt) -// -// lt는 gt와 eq에서 파생됩니다 -// a가 b보다 작은 것은 a가 b보다 크지 않고 a와 b가 같지도 않은 것과 같으며 -// 결국 a가 b 이상이 아닌 것과 같습니다 -// -// 두 연산 모두 상수-시간이므로 그 조합도 상수-시간입니다 -// - -/// 두 값의 작음 여부를 상수-시간에 판정하는 연산을 정의하는 트레이트입니다. -/// -/// `CtEqOps`와 `CtGreeter`의 결과를 결합하여 기본 구현을 파생합니다. -pub trait CtLess: CtEqOps + CtGreeter { - /// 자신이 `other`보다 작으면 `Choice(1)`을, 아니면 `Choice(0)`을 반환하는 함수입니다. - /// - /// # Arguments - /// - `other`: 비교 대상 값입니다 - #[inline] - fn lt(&self, other: &Self) -> Choice { - !self.gt(other) & !self.eq(other) - } -} - -// 일괄 구현으로 CtEqOps와 CtGreeter를 모두 만족하는 타입은 검증된 상수-시간 -// 기본 구현으로 CtLess를 자동으로 얻습니다. -impl CtLess for T {} - -// -// 계층-1 결정적 값 동등성 테스트 (CTSEC-01, CONTEXT D-03) -// -// 값 동등성만 증명하며 분기 부재는 계층-2 디스어셈블 게이트(check_ct_asm.sh) 소관입니다. -// +/// 결정적 값 동등성 테스트 모듈입니다. #[cfg(test)] mod tests { use super::*; + use crate::traits::*; + use zeroize::Secret; const U32_SAMPLES: [u32; 8] = [ 0, @@ -544,18 +255,79 @@ mod tests { } } + #[test] + fn ct_eq_array_and_secret_delegation() { + let base = [0x5Au8; 32]; + let same = [0x5Au8; 32]; + assert_eq!( + base.ct_eq(&same).unwrap_u8(), + 1, + "[u8; 32] 동일 배열 ct_eq 불일치" + ); + + let mut first_diff = base; + first_diff[0] ^= 0x80; + assert_eq!( + base.ct_eq(&first_diff).unwrap_u8(), + 0, + "[u8; 32] 첫 원소 차이 미검출" + ); + + let mut last_diff = base; + last_diff[31] ^= 1; + assert_eq!( + base.ct_eq(&last_diff).unwrap_u8(), + 0, + "[u8; 32] 마지막 원소 차이 미검출" + ); + + let words = [0xDEAD_BEEFu32; 8]; + assert_eq!( + words.ct_eq(&[0xDEAD_BEEFu32; 8]).unwrap_u8(), + 1, + "[u32; 8] 동일 배열 ct_eq 불일치" + ); + + let s1 = Secret::new(base); + let s2 = Secret::new(same); + let s3 = Secret::new(last_diff); + assert_eq!( + s1.ct_eq(&s2).unwrap_u8(), + 1, + "Secret<[u8; 32]> 동일 키 ct_eq 불일치" + ); + assert_eq!( + s1.ct_eq(&s3).unwrap_u8(), + 0, + "Secret<[u8; 32]> 상이 키 미검출" + ); + assert_eq!( + s1.ct_ne(&s3).unwrap_u8(), + 1, + "Secret<[u8; 32]> ct_ne 파생 불일치" + ); + + let a = Secret::new(0x0123_4567_89AB_CDEFu64); + let b = Secret::new(0x0123_4567_89AB_CDEFu64); + assert_eq!( + a.ct_eq(&b).unwrap_u8(), + 1, + "Secret 동일 값 ct_eq 불일치" + ); + } + #[test] fn ct_eq_value_matches_branchful() { // u8 전수 (256 x 256) for a in 0u8..=255 { for b in 0u8..=255 { assert_eq!( - CtEqOps::eq(&a, &b).unwrap_u8(), + CtEqOps::ct_eq(&a, &b).unwrap_u8(), (a == b) as u8, "CtEqOps::eq u8 불일치: {a} == {b}" ); assert_eq!( - CtEqOps::ne(&a, &b).unwrap_u8(), + CtEqOps::ct_ne(&a, &b).unwrap_u8(), (a != b) as u8, "CtEqOps::ne u8 불일치: {a} != {b}" ); @@ -565,12 +337,12 @@ mod tests { for &a in &U32_SAMPLES { for &b in &U32_SAMPLES { assert_eq!( - CtEqOps::eq(&a, &b).unwrap_u8(), + CtEqOps::ct_eq(&a, &b).unwrap_u8(), (a == b) as u8, "eq u32 불일치" ); assert_eq!( - CtEqOps::ne(&a, &b).unwrap_u8(), + CtEqOps::ct_ne(&a, &b).unwrap_u8(), (a != b) as u8, "ne u32 불일치" ); @@ -579,7 +351,7 @@ mod tests { for &a in &U64_SAMPLES { for &b in &U64_SAMPLES { assert_eq!( - CtEqOps::eq(&a, &b).unwrap_u8(), + CtEqOps::ct_eq(&a, &b).unwrap_u8(), (a == b) as u8, "eq u64 불일치" ); @@ -588,7 +360,7 @@ mod tests { for &a in &U128_SAMPLES { for &b in &U128_SAMPLES { assert_eq!( - CtEqOps::eq(&a, &b).unwrap_u8(), + CtEqOps::ct_eq(&a, &b).unwrap_u8(), (a == b) as u8, "eq u128 불일치" ); @@ -597,7 +369,7 @@ mod tests { for &a in &I64_SAMPLES { for &b in &I64_SAMPLES { assert_eq!( - CtEqOps::eq(&a, &b).unwrap_u8(), + CtEqOps::ct_eq(&a, &b).unwrap_u8(), (a == b) as u8, "eq i64 불일치" ); @@ -606,7 +378,7 @@ mod tests { for &a in &I128_SAMPLES { for &b in &I128_SAMPLES { assert_eq!( - CtEqOps::eq(&a, &b).unwrap_u8(), + CtEqOps::ct_eq(&a, &b).unwrap_u8(), (a == b) as u8, "eq i128 불일치" ); @@ -644,12 +416,12 @@ mod tests { for &a in &U32_SAMPLES { for &b in &U32_SAMPLES { assert_eq!( - CtGreeter::gt(&a, &b).unwrap_u8(), + CtGtOps::ct_gt(&a, &b).unwrap_u8(), (a > b) as u8, "gt u32 불일치: {a} > {b}" ); assert_eq!( - CtLess::lt(&a, &b).unwrap_u8(), + CtLess::ct_lt(&a, &b).unwrap_u8(), (a < b) as u8, "lt u32 불일치: {a} < {b}" ); @@ -658,12 +430,12 @@ mod tests { for &a in &U64_SAMPLES { for &b in &U64_SAMPLES { assert_eq!( - CtGreeter::gt(&a, &b).unwrap_u8(), + CtGtOps::ct_gt(&a, &b).unwrap_u8(), (a > b) as u8, "gt u64 불일치" ); assert_eq!( - CtLess::lt(&a, &b).unwrap_u8(), + CtLess::ct_lt(&a, &b).unwrap_u8(), (a < b) as u8, "lt u64 불일치" ); @@ -672,12 +444,12 @@ mod tests { for &a in &I64_SAMPLES { for &b in &I64_SAMPLES { assert_eq!( - CtGreeter::gt(&a, &b).unwrap_u8(), + CtGtOps::ct_gt(&a, &b).unwrap_u8(), (a > b) as u8, "gt i64 불일치: {a} > {b}" ); assert_eq!( - CtLess::lt(&a, &b).unwrap_u8(), + CtLess::ct_lt(&a, &b).unwrap_u8(), (a < b) as u8, "lt i64 불일치: {a} < {b}" ); @@ -686,12 +458,12 @@ mod tests { for &a in &U128_SAMPLES { for &b in &U128_SAMPLES { assert_eq!( - CtGreeter::gt(&a, &b).unwrap_u8(), + CtGtOps::ct_gt(&a, &b).unwrap_u8(), (a > b) as u8, "gt u128 불일치" ); assert_eq!( - CtLess::lt(&a, &b).unwrap_u8(), + CtLess::ct_lt(&a, &b).unwrap_u8(), (a < b) as u8, "lt u128 불일치" ); @@ -700,12 +472,12 @@ mod tests { for &a in &I128_SAMPLES { for &b in &I128_SAMPLES { assert_eq!( - CtGreeter::gt(&a, &b).unwrap_u8(), + CtGtOps::ct_gt(&a, &b).unwrap_u8(), (a > b) as u8, "gt i128 불일치: {a} > {b}" ); assert_eq!( - CtLess::lt(&a, &b).unwrap_u8(), + CtLess::ct_lt(&a, &b).unwrap_u8(), (a < b) as u8, "lt i128 불일치: {a} < {b}" ); @@ -715,12 +487,12 @@ mod tests { for a in i8::MIN..=i8::MAX { for b in i8::MIN..=i8::MAX { assert_eq!( - CtGreeter::gt(&a, &b).unwrap_u8(), + CtGtOps::ct_gt(&a, &b).unwrap_u8(), (a > b) as u8, "gt i8 전수 불일치" ); assert_eq!( - CtLess::lt(&a, &b).unwrap_u8(), + CtLess::ct_lt(&a, &b).unwrap_u8(), (a < b) as u8, "lt i8 전수 불일치" ); diff --git a/constant-time/src/traits.rs b/constant-time/src/traits.rs new file mode 100644 index 0000000..e80d9b2 --- /dev/null +++ b/constant-time/src/traits.rs @@ -0,0 +1,288 @@ +use crate::Choice; +use crate::internal::*; +use crate::private::Sealed; +use zeroize::{Secret, Zeroable, Zeroize}; + +// +// CtSelOps - start +// + +/// 조건에 따라 두 값 중 하나를 상수-시간에 선택하는 연산을 정의하는 트레이트입니다. +/// +/// `assign`과 `swap`은 `select`에서 파생됩니다. +pub trait CtSelOps: Copy + Zeroize + Sealed { + /// `choice`가 1이면 `b`를, 0이면 `a`를 상수-시간에 선택하여 반환하는 함수입니다. + /// + /// # Arguments + /// - `a`: `choice`가 0일 때 선택되는 값입니다 + /// - `b`: `choice`가 1일 때 선택되는 값입니다 + /// - `choice`: 선택 조건입니다 + fn select(a: &Self, b: &Self, choice: Choice) -> Self; + + /// `choice`가 1이면 `other`를 자신에게 상수-시간에 대입하는 함수입니다. + /// + /// # Arguments + /// - `other`: 대입 후보 값입니다 + /// - `choice`: 대입 조건입니다 + #[inline] + fn assign(&mut self, other: &Self, choice: Choice) { + *self = Self::select(self, other, choice); + } + + /// `choice`가 1이면 `a`와 `b`를 상수-시간에 교환하는 함수입니다. + /// + /// # Arguments + /// - `a`: 교환 대상 첫 번째 값입니다 + /// - `b`: 교환 대상 두 번째 값입니다 + /// - `choice`: 교환 조건이며 0이면 두 값을 그대로 둡니다 + /// + /// # Security Note + /// 안전성에 관한 논증이 [zeroize] 크레이트로 위임됩니다. + #[inline] + fn swap(a: &mut Self, b: &mut Self, choice: Choice) { + let mut t: Self = *a; + let _ = core::hint::black_box(&mut t); + a.assign(b, choice); + b.assign(&t, choice); + t.zeroize(); + core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + let _ = core::hint::black_box(&mut t); + } +} + +macro_rules! impl_sel_via32 { + ($($t:ty),+) => { + $( + impl CtSelOps for $t { + #[inline] + fn select(a: &Self, b: &Self, choice: Choice) -> Self { + ct_sel32(choice.0, *b as u32, *a as u32) as $t + } + } + )+ + }; +} + +macro_rules! impl_sel_via64 { + ($($t:ty),+) => { + $( + impl CtSelOps for $t { + #[inline] + fn select(a: &Self, b: &Self, choice: Choice) -> Self { + ct_sel64(choice.0, *b as u64, *a as u64) as $t + } + } + )+ + }; +} + +impl_sel_via32!(u8, u16, u32, i8, i16, i32); +impl_sel_via64!(u64, i64, usize, isize); + +impl CtSelOps for u128 { + #[inline] + fn select(a: &Self, b: &Self, choice: Choice) -> Self { + let hi = ct_sel64(choice.0, (*b >> 64) as u64, (*a >> 64) as u64) as u128; + let lo = ct_sel64(choice.0, *b as u64, *a as u64) as u128; + (hi << 64) | lo + } +} + +impl CtSelOps for i128 { + #[inline] + fn select(a: &Self, b: &Self, choice: Choice) -> Self { + u128::select(&(*a as u128), &(*b as u128), choice) as i128 + } +} + +// +// CtSelOps - end +// + +// +// CtEqOps - start +// + +/// 두 값의 동등 여부를 상수-시간에 판정하는 연산을 정의하는 트레이트입니다. +pub trait CtEqOps: Sealed { + /// 자신과 `other`가 같으면 `Choice(1)`을, 다르면 `Choice(0)`을 반환하는 함수입니다. + /// + /// # Arguments + /// - `other`: 비교 대상 값입니다 + fn ct_eq(&self, other: &Self) -> Choice; + + /// 자신과 `other`가 다르면 `Choice(1)`을, 같으면 `Choice(0)`을 반환하는 함수입니다. + /// + /// # Arguments + /// - `other`: 비교 대상 값입니다 + #[inline] + fn ct_ne(&self, other: &Self) -> Choice { + !self.ct_eq(other) + } +} + +macro_rules! impl_eq_via32 { + ($($t:ty),+) => { + $( + impl CtEqOps for $t { + #[inline] + fn ct_eq(&self, other: &Self) -> Choice { + Choice(ct_eq32(*self as u32, *other as u32)) + } + } + )+ + }; +} + +macro_rules! impl_eq_via64 { + ($($t:ty),+) => { + $( + impl CtEqOps for $t { + #[inline] + fn ct_eq(&self, other: &Self) -> Choice { + Choice(ct_eq64(*self as u64, *other as u64)) + } + } + )+ + }; +} + +impl_eq_via32!(u8, u16, u32, i8, i16, i32); +impl_eq_via64!(u64, i64, usize, isize); + +impl CtEqOps for u128 { + #[inline] + fn ct_eq(&self, other: &Self) -> Choice { + Choice(ct_eq128(*self, *other)) + } +} + +impl CtEqOps for i128 { + #[inline] + fn ct_eq(&self, other: &Self) -> Choice { + Choice(ct_eq128(*self as u128, *other as u128)) + } +} + +impl Sealed for Secret {} + +impl CtEqOps for [T; N] { + #[inline] + fn ct_eq(&self, other: &Self) -> Choice { + self.iter() + .zip(other.iter()) + .fold(Choice::from_u8(1), |acc, (a, b)| acc & a.ct_eq(b)) + } +} + +impl CtEqOps for Secret { + #[inline] + fn ct_eq(&self, other: &Self) -> Choice { + self.expose().ct_eq(other.expose()) + } +} + +// +// CtEqOps - end +// + +// +// CtGtOps - start +// + +/// 두 값의 대소를 상수-시간에 판정하는 연산을 정의하는 트레이트입니다. +pub trait CtGtOps: Sealed { + /// 자신이 `other`보다 크면 `Choice(1)`을, 아니면 `Choice(0)`을 반환하는 함수입니다. + /// + /// # Arguments + /// - `other`: 비교 대상 값입니다 + fn ct_gt(&self, other: &Self) -> Choice; +} + +macro_rules! impl_gt_unsigned_via32 { + ($($t:ty),+) => { + $( + impl CtGtOps for $t { + #[inline] + fn ct_gt(&self, other: &Self) -> Choice { + Choice(ct_gt_u32(*self as u32, *other as u32)) + } + } + )+ + }; +} + +macro_rules! impl_gt_unsigned_via64 { + ($($t:ty),+) => { + $( + impl CtGtOps for $t { + #[inline] + fn ct_gt(&self, other: &Self) -> Choice { + Choice(ct_gt_u64(*self as u64, *other as u64)) + } + } + )+ + }; +} + +macro_rules! impl_gt_signed_via64 { + ($($t:ty),+) => { + $( + impl CtGtOps for $t { + #[inline] + fn ct_gt(&self, other: &Self) -> Choice { + Choice(ct_gt_i64(*self as i64, *other as i64)) + } + } + )+ + }; +} + +impl_gt_unsigned_via32!(u8, u16, u32); +impl_gt_unsigned_via64!(u64, usize); +impl_gt_signed_via64!(i8, i16, i32, i64, isize); + +impl CtGtOps for u128 { + #[inline] + fn ct_gt(&self, other: &Self) -> Choice { + Choice(ct_gt_u128(*self, *other)) + } +} + +impl CtGtOps for i128 { + #[inline] + fn ct_gt(&self, other: &Self) -> Choice { + Choice(ct_gt_i128(*self, *other)) + } +} + +// +// CtGtOps - end +// + +// +// CtLess - start +// + +/// 두 값의 작음 여부를 상수-시간에 판정하는 연산을 정의하는 트레이트입니다. +/// +/// `CtEqOps`와 `CtGtOps`의 결과를 결합하여 기본 구현을 파생합니다. +pub trait CtLess: CtEqOps + CtGtOps { + /// 자신이 `other`보다 작으면 `Choice(1)`을, 아니면 `Choice(0)`을 반환하는 함수입니다. + /// + /// # Arguments + /// - `other`: 비교 대상 값입니다 + #[inline] + fn ct_lt(&self, other: &Self) -> Choice { + // !self.ct_gt(other) & !self.ct_eq(other) + CtGtOps::ct_gt(other, self) + } +} + +// 일괄 구현으로 CtEqOps와 CtGtOps를 모두 만족하는 타입은 검증된 상수-시간 +// 기본 구현으로 CtLess를 자동으로 얻습니다. +impl CtLess for T {} + +// +// CtLess - end +// diff --git a/constant-time/tests/chan_dual_auth_ct.rs b/constant-time/tests/chan_dual_auth_ct.rs index d0efbab..719359f 100644 --- a/constant-time/tests/chan_dual_auth_ct.rs +++ b/constant-time/tests/chan_dual_auth_ct.rs @@ -9,7 +9,8 @@ #[cfg(test)] mod tests { - use constant_time::{Choice, CtEqOps}; + use constant_time::Choice; + use constant_time::traits::CtEqOps; use std::hint::black_box; // Mock HsmCapability iso-light-k0::hsm_registry::HsmCapability 의 16-byte 레이아웃 미러 @@ -37,13 +38,13 @@ mod tests { // token_nonzero & state_ok & token_eq & stored_rights_ok & cap_rights_ok #[inline(never)] fn authenticate_ct(cap: &MockCap, slot: &MockSlot, required: u16) -> bool { - let token_nonzero: Choice = CtEqOps::ne(&cap.token, &0u64); - let state_ok: Choice = CtEqOps::eq(&slot.state_byte, &1u8); - let token_eq: Choice = CtEqOps::eq(&cap.token, &slot.token); + let token_nonzero: Choice = CtEqOps::ct_ne(&cap.token, &0u64); + let state_ok: Choice = CtEqOps::ct_eq(&slot.state_byte, &1u8); + let token_eq: Choice = CtEqOps::ct_eq(&cap.token, &slot.token); let stored_masked: u16 = slot.rights & required; - let stored_rights_ok: Choice = CtEqOps::eq(&stored_masked, &required); + let stored_rights_ok: Choice = CtEqOps::ct_eq(&stored_masked, &required); let cap_masked: u16 = cap.rights & required; - let cap_rights_ok: Choice = CtEqOps::eq(&cap_masked, &required); + let cap_rights_ok: Choice = CtEqOps::ct_eq(&cap_masked, &required); (token_nonzero & state_ok & token_eq & stored_rights_ok & cap_rights_ok).unwrap_u8() == 1 } diff --git a/constant-time/tests/chan_length_ct.rs b/constant-time/tests/chan_length_ct.rs index caedd50..6f87b2f 100644 --- a/constant-time/tests/chan_length_ct.rs +++ b/constant-time/tests/chan_length_ct.rs @@ -8,7 +8,7 @@ #[cfg(test)] mod tests { - use constant_time::{CtEqOps, CtLess}; + use constant_time::traits::{CtEqOps, CtLess}; use std::hint::black_box; const CHAN_MAX: usize = 4096; @@ -16,8 +16,8 @@ mod tests { // handle_write / handle_relay 의 step (1) CT 범위 검사 미러 #[inline(never)] fn check_byte_len(byte_len: usize) -> bool { - let lt_max: u8 = CtLess::lt(&byte_len, &(CHAN_MAX + 1)).unwrap_u8(); - let nonzero: u8 = CtEqOps::ne(&byte_len, &0usize).unwrap_u8(); + let lt_max: u8 = CtLess::ct_lt(&byte_len, &(CHAN_MAX + 1)).unwrap_u8(); + let nonzero: u8 = CtEqOps::ct_ne(&byte_len, &0usize).unwrap_u8(); (lt_max & nonzero) == 1 } diff --git a/constant-time/tests/dudect.rs b/constant-time/tests/dudect.rs index 300839e..1789f45 100644 --- a/constant-time/tests/dudect.rs +++ b/constant-time/tests/dudect.rs @@ -22,7 +22,8 @@ #[cfg(test)] mod tests { - use constant_time::{Choice, CtEqOps, CtGreeter, CtLess, CtSelOps}; + use constant_time::Choice; + use constant_time::traits::{CtEqOps, CtGtOps, CtLess, CtSelOps}; use std::hint::black_box; // @@ -324,13 +325,13 @@ mod tests { for _ in 0..WARMUP { let a = rnd_i32(&mut s); - black_box(CtEqOps::eq(&a, &a)); + black_box(CtEqOps::ct_eq(&a, &a)); } for i in 0..MEASUREMENTS { let cl = i & 1; let a = black_box(rnd_i32(&mut s)); let b = black_box(if cl == 0 { a } else { a ^ 1 }); - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + stat[cl].push(measure!(CtEqOps::ct_eq(&a, &b))); } if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { return; @@ -349,13 +350,13 @@ mod tests { for _ in 0..WARMUP { let a = rnd_u64(&mut s); - black_box(CtEqOps::eq(&a, &a)); + black_box(CtEqOps::ct_eq(&a, &a)); } for i in 0..MEASUREMENTS { let cl = i & 1; let a = black_box(rnd_u64(&mut s)); let b = black_box(if cl == 0 { a } else { a ^ 1 }); - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + stat[cl].push(measure!(CtEqOps::ct_eq(&a, &b))); } if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { return; @@ -365,7 +366,7 @@ mod tests { } // - // CtGreeter::gt — unsigned 64-bit greater-than. + // CtGtOps::gt — unsigned 64-bit greater-than. // // Class 0: a > b — achieved by setting MSB of `a` and clearing it in `b`. // Class 1: a < b — roles reversed. @@ -383,7 +384,7 @@ mod tests { for _ in 0..WARMUP { let raw = rnd_u64(&mut s); let (a, b) = (raw | MSB, raw & !MSB); - black_box(CtGreeter::gt(&a, &b)); + black_box(CtGtOps::ct_gt(&a, &b)); } for i in 0..MEASUREMENTS { let cl = i & 1; @@ -393,9 +394,9 @@ mod tests { } else { (raw & !MSB, raw | MSB) // a < b }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + stat[cl].push(measure!(CtGtOps::ct_gt(&a, &b))); } - if report("CtGreeter::gt (a>b vs a (a>b vs a — signed 64-bit greater-than. + // CtGtOps::gt — signed 64-bit greater-than. // // Class 0: a > b — a is non-negative (MSB clear), b is negative (MSB set). // Class 1: a < b — roles reversed. @@ -421,7 +422,7 @@ mod tests { for _ in 0..WARMUP { let raw = rnd_u64(&mut s); let (a, b) = ((raw & !SIGN) as i64, (raw | SIGN) as i64); - black_box(CtGreeter::gt(&a, &b)); + black_box(CtGtOps::ct_gt(&a, &b)); } for i in 0..MEASUREMENTS { let cl = i & 1; @@ -431,9 +432,9 @@ mod tests { } else { ((raw | SIGN) as i64, (raw & !SIGN) as i64) // a < 0 ≤ b (signed) }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + stat[cl].push(measure!(CtGtOps::ct_gt(&a, &b))); } - if report("CtGreeter::gt (a>b vs a (a>b vs a — 128-bit unsigned greater-than. + // CtGtOps::gt — 128-bit unsigned greater-than. // // Class 0: a > b — high 64-bit word of `a` is all-ones; `b` is all-zeros. // Class 1: a < b — roles reversed. @@ -457,7 +458,7 @@ mod tests { for _ in 0..WARMUP { let lo = rnd_u64(&mut s) as u128; let (a, b) = ((u64::MAX as u128) << 64 | lo, lo); - black_box(CtGreeter::gt(&a, &b)); + black_box(CtGtOps::ct_gt(&a, &b)); } for i in 0..MEASUREMENTS { let cl = i & 1; @@ -467,9 +468,9 @@ mod tests { } else { (lo, (u64::MAX as u128) << 64 | lo) // a < b }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + stat[cl].push(measure!(CtGtOps::ct_gt(&a, &b))); } - if report("CtGreeter::gt (a>b vs a (a>b vs a b }; - stat[cl].push(measure!(CtLess::lt(&a, &b))); + stat[cl].push(measure!(CtLess::ct_lt(&a, &b))); } if report("CtLess::lt (ab)", &stat[0], &stat[1]) { return; @@ -598,13 +599,13 @@ mod tests { let mut stat = [Stats::default(), Stats::default()]; for _ in 0..WARMUP { let a = rnd_u32(&mut s); - black_box(CtEqOps::eq(&a, &a)); + black_box(CtEqOps::ct_eq(&a, &a)); } for i in 0..MEASUREMENTS { let cl = i & 1; let a = black_box(rnd_u32(&mut s)); let b = black_box(if cl == 0 { a } else { a ^ 1 }); - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + stat[cl].push(measure!(CtEqOps::ct_eq(&a, &b))); } if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { return; @@ -621,13 +622,13 @@ mod tests { let mut stat = [Stats::default(), Stats::default()]; for _ in 0..WARMUP { let a = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; - black_box(CtEqOps::eq(&a, &a)); + black_box(CtEqOps::ct_eq(&a, &a)); } for i in 0..MEASUREMENTS { let cl = i & 1; let a = black_box(((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128); let b = black_box(if cl == 0 { a } else { a ^ 1 }); - stat[cl].push(measure!(CtEqOps::eq(&a, &b))); + stat[cl].push(measure!(CtEqOps::ct_eq(&a, &b))); } if report("CtEqOps::eq (a==a vs a!=a^1)", &stat[0], &stat[1]) { return; @@ -651,7 +652,7 @@ mod tests { for _ in 0..WARMUP { let raw = ((rnd_u64(&mut s) as u128) << 64) | rnd_u64(&mut s) as u128; let (a, b) = ((raw & !SIGN128) as i128, (raw | SIGN128) as i128); - black_box(CtGreeter::gt(&a, &b)); + black_box(CtGtOps::ct_gt(&a, &b)); } for i in 0..MEASUREMENTS { let cl = i & 1; @@ -661,9 +662,9 @@ mod tests { } else { ((raw | SIGN128) as i128, (raw & !SIGN128) as i128) }; - stat[cl].push(measure!(CtGreeter::gt(&a, &b))); + stat[cl].push(measure!(CtGtOps::ct_gt(&a, &b))); } - if report("CtGreeter::gt (a>b vs a (a>b vs a bool { - let t: Choice = CtEqOps::ne(&token, &0u64); - let s: Choice = CtEqOps::eq(&slot_a, &slot_b); + let t: Choice = CtEqOps::ct_ne(&token, &0u64); + let s: Choice = CtEqOps::ct_eq(&slot_a, &slot_b); let masked: u16 = rights & required; - let r: Choice = CtEqOps::eq(&masked, &required); + let r: Choice = CtEqOps::ct_eq(&masked, &required); (t & s & r).unwrap_u8() == 1 } diff --git a/constant-time/tests/threat_inputs.rs b/constant-time/tests/threat_inputs.rs index 818b283..e346e52 100644 --- a/constant-time/tests/threat_inputs.rs +++ b/constant-time/tests/threat_inputs.rs @@ -11,7 +11,8 @@ //! 본 스위트는 timing 검증이 아니므로 release 빌드 / debug 빌드 / MIRI 모두에서 //! 동일한 결과를 산출해야 합니다. -use constant_time::{Choice, CtEqOps, CtGreeter, CtLess, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtGtOps, CtLess, CtSelOps}; // // Choice::from_u8: u8 전 256 값에 대한 정확성 @@ -53,7 +54,7 @@ fn threat_choice_bitops_invariant() { fn threat_eq_boundaries_unsigned() { let cases_u8: &[(u8, u8, u8)] = &[(0, 0, 1), (0xFF, 0xFF, 1), (0, 0xFF, 0), (0x7F, 0x80, 0)]; for &(a, b, exp) in cases_u8 { - assert_eq!(CtEqOps::eq(&a, &b).unwrap_u8(), exp, "u8 {a}=={b}"); + assert_eq!(CtEqOps::ct_eq(&a, &b).unwrap_u8(), exp, "u8 {a}=={b}"); } let cases_u128: &[(u128, u128, u8)] = &[ (0, 0, 1), @@ -64,7 +65,7 @@ fn threat_eq_boundaries_unsigned() { (1u128 << 64, 1, 0), // high half 만 다름 ]; for &(a, b, exp) in cases_u128 { - assert_eq!(CtEqOps::eq(&a, &b).unwrap_u8(), exp, "u128 a/b"); + assert_eq!(CtEqOps::ct_eq(&a, &b).unwrap_u8(), exp, "u128 a/b"); } } @@ -79,12 +80,12 @@ fn threat_eq_boundaries_signed() { (-1, -1, 1), ]; for &(a, b, exp) in cases_i64 { - assert_eq!(CtEqOps::eq(&a, &b).unwrap_u8(), exp, "i64 {a}=={b}"); + assert_eq!(CtEqOps::ct_eq(&a, &b).unwrap_u8(), exp, "i64 {a}=={b}"); } } // -// 경계값: CtGreeter 부호별 ordering 정확성 +// 경계값: CtGtOps 부호별 ordering 정확성 // #[test] @@ -97,7 +98,7 @@ fn threat_gt_boundaries_unsigned() { (1u64 << 63, (1u64 << 63) - 1, 1), // 최대 부호 비트 경계 ]; for &(a, b, exp) in cases_u64 { - assert_eq!(CtGreeter::gt(&a, &b).unwrap_u8(), exp, "u64 {a}>{b}"); + assert_eq!(CtGtOps::ct_gt(&a, &b).unwrap_u8(), exp, "u64 {a}>{b}"); } let cases_u128: &[(u128, u128, u8)] = &[ (0, u128::MAX, 0), @@ -107,7 +108,7 @@ fn threat_gt_boundaries_unsigned() { (1u128 << 64, (1u128 << 64) | 5, 0), ]; for &(a, b, exp) in cases_u128 { - assert_eq!(CtGreeter::gt(&a, &b).unwrap_u8(), exp, "u128 gt"); + assert_eq!(CtGtOps::ct_gt(&a, &b).unwrap_u8(), exp, "u128 gt"); } } @@ -122,7 +123,7 @@ fn threat_gt_boundaries_signed() { (i64::MAX, i64::MAX, 0), ]; for &(a, b, exp) in cases_i64 { - assert_eq!(CtGreeter::gt(&a, &b).unwrap_u8(), exp, "i64 {a}>{b}"); + assert_eq!(CtGtOps::ct_gt(&a, &b).unwrap_u8(), exp, "i64 {a}>{b}"); } let cases_i128: &[(i128, i128, u8)] = &[ (i128::MIN, i128::MAX, 0), @@ -133,7 +134,7 @@ fn threat_gt_boundaries_signed() { (i128::MIN, -1, 0), ]; for &(a, b, exp) in cases_i128 { - assert_eq!(CtGreeter::gt(&a, &b).unwrap_u8(), exp, "i128 gt"); + assert_eq!(CtGtOps::ct_gt(&a, &b).unwrap_u8(), exp, "i128 gt"); } } @@ -146,9 +147,9 @@ fn threat_lt_consistency() { // a, b 모두에 대해 lt + eq + gt 의 합이 exactly 1 (3분할 보장) for a in 0u32..32 { for b in 0u32..32 { - let lt = CtLess::lt(&a, &b).unwrap_u8(); - let eq = CtEqOps::eq(&a, &b).unwrap_u8(); - let gt = CtGreeter::gt(&a, &b).unwrap_u8(); + let lt = CtLess::ct_lt(&a, &b).unwrap_u8(); + let eq = CtEqOps::ct_eq(&a, &b).unwrap_u8(); + let gt = CtGtOps::ct_gt(&a, &b).unwrap_u8(); assert_eq!( lt + eq + gt, 1, @@ -208,10 +209,10 @@ fn threat_extreme_hamming_weight() { ]; for &a in patterns { for &b in patterns { - let eq = CtEqOps::eq(&a, &b).unwrap_u8(); + let eq = CtEqOps::ct_eq(&a, &b).unwrap_u8(); let exp_eq = (a == b) as u8; assert_eq!(eq, exp_eq, "eq HW 위반 a={a:#x} b={b:#x}"); - let gt = CtGreeter::gt(&a, &b).unwrap_u8(); + let gt = CtGtOps::ct_gt(&a, &b).unwrap_u8(); let exp_gt = (a > b) as u8; assert_eq!(gt, exp_gt, "gt HW 위반 a={a:#x} b={b:#x}"); } diff --git a/constant-time/tests/wire_tier2_bad_header_reject.rs b/constant-time/tests/wire_tier2_bad_header_reject.rs index 13644fc..7953dd3 100644 --- a/constant-time/tests/wire_tier2_bad_header_reject.rs +++ b/constant-time/tests/wire_tier2_bad_header_reject.rs @@ -7,7 +7,7 @@ #[cfg(test)] mod tests { - use constant_time::CtEqOps; + use constant_time::traits::CtEqOps; const WIRE_FRAME_MAX: usize = 4096; const WIRE_PAYLOAD_MAX: usize = WIRE_FRAME_MAX - 16; @@ -37,8 +37,8 @@ mod tests { // Tier 2 4 invariant CT-friendly 단일 collapse let magic_u32 = u32::from_le_bytes(magic); let wire_magic_u32 = u32::from_le_bytes(WIRE_MAGIC); - let magic_ok = CtEqOps::eq(&magic_u32, &wire_magic_u32).unwrap_u8() == 1; - let version_ok = CtEqOps::eq(&version, &WIRE_VERSION).unwrap_u8() == 1; + let magic_ok = CtEqOps::ct_eq(&magic_u32, &wire_magic_u32).unwrap_u8() == 1; + let version_ok = CtEqOps::ct_eq(&version, &WIRE_VERSION).unwrap_u8() == 1; let len_ok = (payload_len as usize) + 16 <= data.len() && (payload_len as usize) <= WIRE_PAYLOAD_MAX; let cmd_is_request = (cmd & WIRE_CMD_RESPONSE_BIT) == 0 && cmd != CMD_ERROR; diff --git a/mldsa/src/field.rs b/mldsa/src/field.rs index 2ee1cbd..bf9eedc 100644 --- a/mldsa/src/field.rs +++ b/mldsa/src/field.rs @@ -1,5 +1,6 @@ use crate::{Q, Q_INV}; -use constant_time::{Choice, CtGreeter, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtGtOps, CtSelOps}; use zeroize::Zeroize; #[derive(Clone, Copy, Debug, Default)] @@ -52,7 +53,7 @@ impl Fq { #[inline(always)] pub fn fq_to_signed(v: i32) -> i32 { let half = Q / 2; - let is_greater = CtGreeter::gt(&v, &half); + let is_greater = CtGtOps::ct_gt(&v, &half); i32::select(&v, &(v - Q), is_greater) } diff --git a/mldsa/src/keys.rs b/mldsa/src/keys.rs index 641a7f4..a0ca514 100644 --- a/mldsa/src/keys.rs +++ b/mldsa/src/keys.rs @@ -8,7 +8,8 @@ use crate::pack::{ }; use crate::poly::PolyVec; use crate::sample::{expand_a, expand_s}; -use constant_time::{Choice, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::CtSelOps; use sha3::{SHAKE256, XOF}; use zeroize::{Secret, Zeroize}; diff --git a/mldsa/src/pack.rs b/mldsa/src/pack.rs index 6effeb5..7c7933a 100644 --- a/mldsa/src/pack.rs +++ b/mldsa/src/pack.rs @@ -2,7 +2,8 @@ use crate::Q; use crate::field::{Fq, signed_to_fq}; use crate::ntt::N; use crate::poly::{Poly, PolyVec}; -use constant_time::{Choice, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::CtSelOps; #[inline(always)] pub fn bitlen(n: u32) -> usize { diff --git a/mlkem/src/kem.rs b/mlkem/src/kem.rs index 53daf63..070447a 100644 --- a/mlkem/src/kem.rs +++ b/mlkem/src/kem.rs @@ -3,7 +3,8 @@ use crate::Error; use crate::kpke; use crate::params::{N, Q, SHAREDSECRETBYTES, SYMBYTES}; -use constant_time::{Choice, CtEqOps, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtSelOps}; use sha3::{SHA3, SHA3_256, SHA3_512, SHAKE256, XOF}; use zeroize::Zeroize; @@ -155,7 +156,7 @@ pub fn decaps( let mut eq = Choice::from_u8(1); for i in 0..ct_len { - eq &= CtEqOps::eq(&ct[i], &ct_prime[i]); + eq &= CtEqOps::ct_eq(&ct[i], &ct_prime[i]); } let mut z_result = kdf(&ct[..ct_len], &z); diff --git a/sha2/src/sha2_256.rs b/sha2/src/sha2_256.rs index 832dc15..2598333 100644 --- a/sha2/src/sha2_256.rs +++ b/sha2/src/sha2_256.rs @@ -1,5 +1,6 @@ use crate::{Digest, SHA256State}; -use constant_time::{Choice, CtEqOps, CtGreeter, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtGtOps, CtSelOps}; use zeroize::{Secret, Zeroize}; const SHA_256_K: [u32; 64] = [ @@ -132,7 +133,7 @@ impl SHA256State { // select(a, b, choice) -> choice==1일 때 b, choice==0일 때 a // is_ge==1 (remain >= fill) -> fill (현재 블록 완료) // is_ge==0 (remain < fill) -> remain (남은 것을 가져옴) - let is_ge: Choice = CtGreeter::gt(&remain, &fill) | CtEqOps::eq(&remain, &fill); + let is_ge: Choice = CtGtOps::ct_gt(&remain, &fill) | CtEqOps::ct_eq(&remain, &fill); let chunk_len: usize = usize::select(&remain, &fill, is_ge); self.buffer[self.buffer_len..self.buffer_len + chunk_len] @@ -164,7 +165,7 @@ impl SHA256State { // 이 블록(위치 56-63)에서 // needs_extra == 1 -> buffer_len > 56: 길이는 두 번째 블록으로 가야 함 // needs_extra == 0 -> buffer_len ≤ 56: 길이는 이 블록에 맞음 - let needs_extra: Choice = CtGreeter::gt(&self.buffer_len, &56usize); + let needs_extra: Choice = CtGtOps::ct_gt(&self.buffer_len, &56usize); let not_extra: Choice = !needs_extra; let total_len_bytes = self.total_len.to_be_bytes(); diff --git a/sha2/src/sha2_512.rs b/sha2/src/sha2_512.rs index 1d8b66e..0476792 100644 --- a/sha2/src/sha2_512.rs +++ b/sha2/src/sha2_512.rs @@ -1,5 +1,6 @@ use crate::{Digest, SHA512State}; -use constant_time::{Choice, CtEqOps, CtGreeter, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtGtOps, CtSelOps}; use zeroize::{Secret, Zeroize}; const SHA_512_K: [u64; 80] = [ @@ -224,7 +225,7 @@ impl SHA512State { // select(a, b, choice) -> choice==1일 때 b, choice==0일 때 a // is_ge==1 (remain >= fill) -> fill (현재 블록 완료) // is_ge==0 (remain < fill) -> remain (남은 것을 가져옴) - let is_ge: Choice = CtGreeter::gt(&remain, &fill) | CtEqOps::eq(&remain, &fill); + let is_ge: Choice = CtGtOps::ct_gt(&remain, &fill) | CtEqOps::ct_eq(&remain, &fill); let chunk_len: usize = usize::select(&remain, &fill, is_ge); self.buffer[self.buffer_len..self.buffer_len + chunk_len] @@ -255,7 +256,7 @@ impl SHA512State { // SHA-512 블록 = 128바이트, 메시지 길이는 마지막 16바이트를 차지함 // needs_extra == 1 -> buffer_len > 112: 길이는 두 번째 블록으로 가야 함 // needs_extra == 0 -> buffer_len ≤ 112: 길이는 이 블록에 맞음 - let needs_extra: Choice = CtGreeter::gt(&self.buffer_len, &112usize); + let needs_extra: Choice = CtGtOps::ct_gt(&self.buffer_len, &112usize); let not_extra: Choice = !needs_extra; // SHA-512는 128비트 메시지 길이 필드를 사용함 (FIPS 180-4 §5.1.2) diff --git a/sha3/src/keccak.rs b/sha3/src/keccak.rs index a768791..3fa9b7c 100644 --- a/sha3/src/keccak.rs +++ b/sha3/src/keccak.rs @@ -1,5 +1,6 @@ use crate::{Digest, KeccakState, MAX_RATE_BYTES}; -use constant_time::{Choice, CtEqOps, CtGreeter, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::{CtEqOps, CtGtOps, CtSelOps}; use zeroize::barrier::{atomic_compiler_fence, memory_barrier}; use zeroize::volatile::volatile_write; use zeroize::{Secret, Zeroize}; @@ -132,7 +133,7 @@ impl KeccakState { // select(a, b, choice) → choice==1일 때 b, choice==0일 때 a // is_ge==1 (remain >= fill) → fill (현재 블록 완료) // is_ge==0 (remain < fill) → remain (남은 것을 가져옴) - let is_ge: Choice = CtGreeter::gt(&remain, &fill) | CtEqOps::eq(&remain, &fill); + let is_ge: Choice = CtGtOps::ct_gt(&remain, &fill) | CtEqOps::ct_eq(&remain, &fill); let chunk_len: usize = usize::select(&remain, &fill, is_ge); self.buffer[self.buffer_len..self.buffer_len + chunk_len] @@ -210,7 +211,8 @@ impl KeccakState { word_bytes = self.state[word_idx].to_le_bytes(); let remain: usize = output_len - pos; // CT take = min(remain, 8) - let is_ge: Choice = CtGreeter::gt(&remain, &8usize) | CtEqOps::eq(&remain, &8usize); + let is_ge: Choice = + CtGtOps::ct_gt(&remain, &8usize) | CtEqOps::ct_eq(&remain, &8usize); let take: usize = usize::select(&remain, &8usize, is_ge); out[pos..pos + take].copy_from_slice(&word_bytes[..take]); pos += take; diff --git a/x25519/src/field.rs b/x25519/src/field.rs index a45172a..18f0fed 100644 --- a/x25519/src/field.rs +++ b/x25519/src/field.rs @@ -4,7 +4,8 @@ clippy::needless_range_loop )] -use constant_time::{Choice, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::CtSelOps; use core::ops::{Add, Mul, Sub}; use zeroize::Zeroize; diff --git a/x25519/src/lib.rs b/x25519/src/lib.rs index 6a72126..a626983 100644 --- a/x25519/src/lib.rs +++ b/x25519/src/lib.rs @@ -26,15 +26,13 @@ //! //! assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); //! ``` -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] mod field; -use constant_time::{Choice, CtEqOps}; +use constant_time::Choice; +use constant_time::traits::CtEqOps; use field::FieldElement; use zeroize::{Secret, Zeroize}; @@ -259,7 +257,7 @@ pub fn is_contributory(shared: &SharedSecret) -> Choice { for b in shared.0.expose().iter() { acc |= *b; } - CtEqOps::ne(&acc, &0) + CtEqOps::ct_ne(&acc, &0) } #[cfg(test)] diff --git a/x448/src/field.rs b/x448/src/field.rs index 4bf2bbd..3753a1f 100644 --- a/x448/src/field.rs +++ b/x448/src/field.rs @@ -4,7 +4,8 @@ clippy::needless_range_loop )] -use constant_time::{Choice, CtSelOps}; +use constant_time::Choice; +use constant_time::traits::CtSelOps; use core::ops::{Add, Mul, Sub}; use zeroize::Zeroize; diff --git a/x448/src/lib.rs b/x448/src/lib.rs index 3a28bce..d39e240 100644 --- a/x448/src/lib.rs +++ b/x448/src/lib.rs @@ -27,15 +27,13 @@ //! //! assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); //! ``` -//! -//! # Authors -//! Q. T. Felix #![cfg_attr(not(test), no_std)] mod field; -use constant_time::{Choice, CtEqOps}; +use constant_time::Choice; +use constant_time::traits::CtEqOps; use field::FieldElement; use zeroize::{Secret, Zeroize}; @@ -281,7 +279,7 @@ pub fn is_contributory(shared: &SharedSecret) -> Choice { for b in shared.0.expose().iter() { acc |= *b; } - CtEqOps::ne(&acc, &0) + CtEqOps::ct_ne(&acc, &0) } #[cfg(test)] From cecb428c27bcbaee6c5ca2ba171db831e50366d4 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:32:47 +0900 Subject: [PATCH 47/65] =?UTF-8?q?rng=20=EC=97=90=20SHA-3=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20Hash=5FDRBG=20=EC=B6=94=EA=B0=80=20-=20HashDRBGSHA3?= =?UTF-8?q?=5F224=20256=20384=20512=204=EC=A2=85=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EA=B3=BC=20=EC=9E=AC=EB=85=B8=EC=B6=9C=20-=20rng=20=EC=97=90?= =?UTF-8?q?=20sha3=20=EC=9B=8C=ED=81=AC=EC=8A=A4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=9D=98=EC=A1=B4=20=EC=B6=94=EA=B0=80=20-=20rng/t?= =?UTF-8?q?ests=20=EC=97=90=20KCMVP=20Hash=5FDRBG=20=ED=95=98=EB=84=A4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rng/Cargo.toml | 1 + rng/src/hash_drbg.rs | 19 +- rng/src/lib.rs | 5 +- rng/tests/kcmvp_hash_drbg.rs | 1013 ++++++++++++++++++++++++++++++++++ 4 files changed, 1029 insertions(+), 9 deletions(-) create mode 100644 rng/tests/kcmvp_hash_drbg.rs diff --git a/rng/Cargo.toml b/rng/Cargo.toml index 7aa159e..88aebb4 100644 --- a/rng/Cargo.toml +++ b/rng/Cargo.toml @@ -7,4 +7,5 @@ license.workspace = true [dependencies] sha2.workspace = true +sha3.workspace = true zeroize.workspace = true diff --git a/rng/src/hash_drbg.rs b/rng/src/hash_drbg.rs index ad80beb..6063f27 100644 --- a/rng/src/hash_drbg.rs +++ b/rng/src/hash_drbg.rs @@ -4,11 +4,11 @@ //! //! # 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) +//! - **다양한 해시 함수 지원**: `SHA-2` / `SHA-3` 계열을 기반으로 하는 DRBG 인스턴스를 제공합니다. +//! - [`HashDRBGSHA224`] / [`HashDRBGSHA3_224`] (Security Strength: 112 bits) +//! - [`HashDRBGSHA256`] / [`HashDRBGSHA3_256`] (Security Strength: 128 bits) +//! - [`HashDRBGSHA384`] / [`HashDRBGSHA3_384`] (Security Strength: 192 bits) +//! - [`HashDRBGSHA512`] / [`HashDRBGSHA3_512`] (Security Strength: 256 bits) //! - **메모리 보안**: 내부 상태 `V`와 `C`를 [`SecureBuffer`]를 사용하여 관리합니다. 이를 통해 OS 레벨의 메모리 잠금(`mlock`)과 Drop 시점의 자동 소거를 보장하여, 메모리 덤프나 콜드 부트 공격으로부터 내부 상태를 보호합니다. //! - **Reseed 강제**: 표준에 따라 최대 reseed 간격(`RESEED_INTERVAL`)을 초과하면 [`generate`] 함수가 [`ReseedRequired`] 에러를 반환하여 주기적인 엔트로피 갱신을 강제합니다. //! - **유연한 입력 처리**: `instantiate`, `reseed`, `generate` 함수에서 `additional_input`과 `personalization_string`을 지원합니다. @@ -41,13 +41,11 @@ //! - `impl_hash_drbg!` 매크로를 사용하여 각 해시 함수에 대한 DRBG 구조체와 구현을 생성합니다. 이는 코드 중복을 최소화하고 일관성을 유지합니다. //! - 내부 상태 덧셈 연산(`add_mod`, `add_u64_mod`)은 Big-endian 모듈러 덧셈으로 구현되어 표준을 정확히 따릅니다. //! - 중간 계산값이나 스택에 복사된 민감한 데이터는 [`zeroize::Secret`] 으로 감싸 모든 종료 경로(정상/`?`/패닉) 에서 휘발성 쓰기 + 컴파일러·메모리 배리어로 자동 소거합니다. -//! -//! # Authors -//! Q. T. Felix use crate::{DrbgError, SecureBuffer}; use core::cmp::min; use sha2::{SHA2, SHA224, SHA256, SHA384, SHA512}; +use sha3::{SHA3, SHA3_224, SHA3_256, SHA3_384, SHA3_512}; use zeroize::{Secret, Zeroize}; /// 최대 reseed 간격 @@ -85,6 +83,7 @@ macro_rules! impl_hash_drbg { /// /// 내부 상태 V, C는 [`SecureBuffer`]로 관리되어 OS 레벨 메모리 잠금(lock)과 /// [Drop] 시점의 강제 소거([`Zeroize`])가 보장됩니다. + #[allow(non_camel_case_types)] pub struct $struct_name { /// 내부 상태 V — seedlen bytes v: SecureBuffer, @@ -524,6 +523,10 @@ impl_hash_drbg!(HashDRBGSHA224, SHA224, 28, 55, 14); // security_strength=112 bi 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! +impl_hash_drbg!(HashDRBGSHA3_224, SHA3_224, 28, 55, 14); // security_strength=112 bits +impl_hash_drbg!(HashDRBGSHA3_256, SHA3_256, 32, 55, 16); // security_strength=128 bits +impl_hash_drbg!(HashDRBGSHA3_384, SHA3_384, 48, 111, 24); // security_strength=192 bits +impl_hash_drbg!(HashDRBGSHA3_512, SHA3_512, 64, 111, 32); // security_strength=256 bits #[cfg(test)] mod tests { diff --git a/rng/src/lib.rs b/rng/src/lib.rs index 69d5c9f..c4f6643 100644 --- a/rng/src/lib.rs +++ b/rng/src/lib.rs @@ -38,7 +38,10 @@ pub enum DrbgError { OsEntropyFailed, } -pub use hash_drbg::{HashDRBGSHA224, HashDRBGSHA256, HashDRBGSHA384, HashDRBGSHA512}; +pub use hash_drbg::{ + HashDRBGSHA3_224, HashDRBGSHA3_256, HashDRBGSHA3_384, HashDRBGSHA3_512, HashDRBGSHA224, + HashDRBGSHA256, HashDRBGSHA384, HashDRBGSHA512, +}; /// DRBG 내부 상태를 위한 최대 버퍼 크기입니다. /// SHA-512 기반 Hash_DRBG의 seedlen = 111바이트 diff --git a/rng/tests/kcmvp_hash_drbg.rs b/rng/tests/kcmvp_hash_drbg.rs new file mode 100644 index 0000000..051ed32 --- /dev/null +++ b/rng/tests/kcmvp_hash_drbg.rs @@ -0,0 +1,1013 @@ +#![allow(clippy::upper_case_acronyms)] +#![allow(non_camel_case_types)] + +use rng::{ + HashDRBGSHA3_224, HashDRBGSHA3_256, HashDRBGSHA3_384, HashDRBGSHA3_512, HashDRBGSHA224, + HashDRBGSHA256, HashDRBGSHA384, HashDRBGSHA512, +}; +use sha2::{SHA2, SHA224, SHA256, SHA384, SHA512}; +use sha3::{SHA3, SHA3_224, SHA3_256, SHA3_384, SHA3_512}; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Alg { + SHA224, + SHA256, + SHA384, + SHA512, + SHA3_224, + SHA3_256, + SHA3_384, + SHA3_512, +} + +const ALL_ALGS: [Alg; 8] = [ + Alg::SHA224, + Alg::SHA256, + Alg::SHA384, + Alg::SHA512, + Alg::SHA3_224, + Alg::SHA3_256, + Alg::SHA3_384, + Alg::SHA3_512, +]; + +impl Alg { + fn label(self) -> &'static str { + match self { + Alg::SHA224 => "SHA-224", + Alg::SHA256 => "SHA-256", + Alg::SHA384 => "SHA-384", + Alg::SHA512 => "SHA-512", + Alg::SHA3_224 => "SHA3-224", + Alg::SHA3_256 => "SHA3-256", + Alg::SHA3_384 => "SHA3-384", + Alg::SHA3_512 => "SHA3-512", + } + } + + fn seed_len(self) -> usize { + match self { + Alg::SHA224 | Alg::SHA3_224 | Alg::SHA256 | Alg::SHA3_256 => 55, + Alg::SHA384 | Alg::SHA3_384 | Alg::SHA512 | Alg::SHA3_512 => 111, + } + } + + fn min_entropy(self) -> usize { + match self { + Alg::SHA224 | Alg::SHA3_224 => 14, + Alg::SHA256 | Alg::SHA3_256 => 16, + Alg::SHA384 | Alg::SHA3_384 => 24, + Alg::SHA512 | Alg::SHA3_512 => 32, + } + } + + fn hash_fn(self) -> HashFn { + match self { + Alg::SHA224 => one_shot_sha2::, + Alg::SHA256 => one_shot_sha2::, + Alg::SHA384 => one_shot_sha2::, + Alg::SHA512 => one_shot_sha2::, + Alg::SHA3_224 => one_shot_sha3::, + Alg::SHA3_256 => one_shot_sha3::, + Alg::SHA3_384 => one_shot_sha3::, + Alg::SHA3_512 => one_shot_sha3::, + } + } + + fn run(self, case: &Case, out_len: usize) -> Vec { + match self { + Alg::SHA224 => run_case::(case, out_len), + Alg::SHA256 => run_case::(case, out_len), + Alg::SHA384 => run_case::(case, out_len), + Alg::SHA512 => run_case::(case, out_len), + Alg::SHA3_224 => run_case::(case, out_len), + Alg::SHA3_256 => run_case::(case, out_len), + Alg::SHA3_384 => run_case::(case, out_len), + Alg::SHA3_512 => run_case::(case, out_len), + } + } +} + +fn one_shot_sha2(msg: &[u8]) -> Vec { + let mut hasher = H::new(); + hasher.update(msg); + hasher.finalize().as_bytes().to_vec() +} + +fn one_shot_sha3(msg: &[u8]) -> Vec { + let mut hasher = H::new(); + hasher.update(msg); + hasher.finalize().as_bytes().to_vec() +} + +type HashFn = fn(&[u8]) -> Vec; + +fn opt(bytes: &[u8]) -> Option<&[u8]> { + (!bytes.is_empty()).then_some(bytes) +} + +// +// 라이브러리 경로 (rng 크레이트의 Hash_DRBG) +// + +trait DrbgOps: Sized { + fn kat_instantiate(entropy: &[u8], nonce: &[u8], ps: Option<&[u8]>) -> Self; + fn kat_reseed(&mut self, entropy: &[u8], ai: Option<&[u8]>); + fn kat_generate(&mut self, out: &mut [u8], ai: Option<&[u8]>); +} + +macro_rules! impl_drbg_ops { + ($($t:ty),+) => {$( + impl DrbgOps for $t { + fn kat_instantiate(entropy: &[u8], nonce: &[u8], ps: Option<&[u8]>) -> Self { + unsafe { <$t>::new_from_entropy(entropy, nonce, ps) } + .expect("KAT instantiate 실패") + } + fn kat_reseed(&mut self, entropy: &[u8], ai: Option<&[u8]>) { + <$t>::reseed(self, entropy, ai).expect("KAT reseed 실패"); + } + fn kat_generate(&mut self, out: &mut [u8], ai: Option<&[u8]>) { + <$t>::generate(self, out, ai).expect("KAT generate 실패"); + } + } + )+}; +} + +impl_drbg_ops!( + HashDRBGSHA224, + HashDRBGSHA256, + HashDRBGSHA384, + HashDRBGSHA512, + HashDRBGSHA3_224, + HashDRBGSHA3_256, + HashDRBGSHA3_384, + HashDRBGSHA3_512 +); + +struct Case { + entropy: Vec, + nonce: Vec, + ps: Vec, + flow: Flow, +} + +enum Flow { + NoPr { + reseed_entropy: Vec, + reseed_ai: Vec, + ai: [Vec; 2], + }, + UsePr { + pr: [(Vec, Vec); 2], + }, +} + +fn run_case(case: &Case, out_len: usize) -> Vec { + let mut out = vec![0u8; out_len]; + let mut drbg = T::kat_instantiate(&case.entropy, &case.nonce, opt(&case.ps)); + match &case.flow { + Flow::NoPr { + reseed_entropy, + reseed_ai, + ai, + } => { + drbg.kat_reseed(reseed_entropy, opt(reseed_ai)); + drbg.kat_generate(&mut out, opt(&ai[0])); + drbg.kat_generate(&mut out, opt(&ai[1])); + } + Flow::UsePr { pr } => { + for (entropy, ai) in pr { + drbg.kat_reseed(entropy, opt(ai)); + drbg.kat_generate(&mut out, None); + } + } + } + out +} + +// +// 독립 참조 구현 (자가 검증 차분용 — rng 크레이트 코드와 공유 없음) +// + +struct RefDrbg { + hash: HashFn, + seed_len: usize, + v: Vec, + c: Vec, + reseed_counter: u64, +} + +fn concat(parts: &[&[u8]]) -> Vec { + let mut out = Vec::new(); + for p in parts { + out.extend_from_slice(p); + } + out +} + +fn be_add(dst: &mut [u8], src: &[u8]) { + let mut carry = 0u16; + let mut src_iter = src.iter().rev(); + for d in dst.iter_mut().rev() { + let s = u16::from(src_iter.next().copied().unwrap_or(0)); + let sum = u16::from(*d) + s + carry; + *d = sum as u8; + carry = sum >> 8; + } +} + +fn ref_hash_df(hash: HashFn, parts: &[&[u8]], out_bytes: usize) -> Vec { + let bits_be = ((out_bytes as u32) * 8).to_be_bytes(); + let body = concat(parts); + let mut out = Vec::new(); + let mut counter = 1u8; + while out.len() < out_bytes { + out.extend_from_slice(&hash(&concat(&[&[counter], &bits_be, &body]))); + counter += 1; + } + out.truncate(out_bytes); + out +} + +impl RefDrbg { + fn instantiate(alg: Alg, entropy: &[u8], nonce: &[u8], ps: Option<&[u8]>) -> Self { + let hash = alg.hash_fn(); + let seed_len = alg.seed_len(); + let seed = concat(&[entropy, nonce, ps.unwrap_or(&[])]); + let v = ref_hash_df(hash, &[&seed], seed_len); + let c = ref_hash_df(hash, &[&[0x00], &v], seed_len); + RefDrbg { + hash, + seed_len, + v, + c, + reseed_counter: 1, + } + } + + fn reseed(&mut self, entropy: &[u8], ai: Option<&[u8]>) { + let new_v = ref_hash_df( + self.hash, + &[&[0x01], &self.v, entropy, ai.unwrap_or(&[])], + self.seed_len, + ); + self.v = new_v; + self.c = ref_hash_df(self.hash, &[&[0x00], &self.v], self.seed_len); + self.reseed_counter = 1; + } + + fn generate(&mut self, out_len: usize, ai: Option<&[u8]>) -> Vec { + if let Some(ai) = ai { + let w = (self.hash)(&concat(&[&[0x02], &self.v, ai])); + be_add(&mut self.v, &w); + } + let mut data = self.v.clone(); + let mut out = Vec::new(); + while out.len() < out_len { + out.extend_from_slice(&(self.hash)(&data)); + be_add(&mut data, &[1]); + } + out.truncate(out_len); + let h = (self.hash)(&concat(&[&[0x03], &self.v])); + be_add(&mut self.v, &h); + let c = self.c.clone(); + be_add(&mut self.v, &c); + be_add(&mut self.v, &self.reseed_counter.to_be_bytes()); + self.reseed_counter += 1; + out + } +} + +fn run_case_ref(alg: Alg, case: &Case, out_len: usize) -> Vec { + let mut drbg = RefDrbg::instantiate(alg, &case.entropy, &case.nonce, opt(&case.ps)); + match &case.flow { + Flow::NoPr { + reseed_entropy, + reseed_ai, + ai, + } => { + drbg.reseed(reseed_entropy, opt(reseed_ai)); + drbg.generate(out_len, opt(&ai[0])); + drbg.generate(out_len, opt(&ai[1])) + } + Flow::UsePr { pr } => { + let mut out = Vec::new(); + for (entropy, ai) in pr { + drbg.reseed(entropy, opt(ai)); + out = drbg.generate(out_len, None); + } + out + } + } +} + +// +// req / sam / rsp 파싱과 대조 +// + +fn hex_decode(s: &str, ctx: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn hex_encode(bytes: &[u8]) -> String { + let mut out = String::with_capacity(bytes.len() * 2); + for b in bytes { + out.push( + char::from_digit(u32::from(b >> 4), 16) + .unwrap() + .to_ascii_uppercase(), + ); + out.push( + char::from_digit(u32::from(b & 0x0F), 16) + .unwrap() + .to_ascii_uppercase(), + ); + } + out +} + +struct Section { + header: Vec, + blocks: Vec>, +} + +fn parse_sections(content: &str, ctx: &str) -> Vec
{ + let mut sections: Vec
= Vec::new(); + let mut block: Option> = None; + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + if let Some(pending) = block.take() { + sections + .last_mut() + .unwrap_or_else(|| panic!("{ctx}: 헤더 없이 블록 등장")) + .blocks + .push(pending); + } + } else if trimmed.starts_with('[') { + assert!(block.is_none(), "{ctx}: 헤더 라인이 블록 내부에 등장"); + match sections.last_mut() { + Some(last) if last.blocks.is_empty() => last.header.push(trimmed.to_string()), + _ => sections.push(Section { + header: vec![trimmed.to_string()], + blocks: Vec::new(), + }), + } + } else { + let (name, value) = trimmed + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 필드 형식 {line:?}")); + assert!(!sections.is_empty(), "{ctx}: 헤더 없이 필드 등장"); + block + .get_or_insert_default() + .push((name.trim().to_string(), value.trim().to_string())); + } + } + if let Some(pending) = block.take() { + sections + .last_mut() + .unwrap_or_else(|| panic!("{ctx}: 헤더 없이 블록 등장")) + .blocks + .push(pending); + } + sections +} + +fn header_value<'a>(header: &'a [String], name: &str) -> Option<&'a str> { + header.iter().find_map(|line| { + let inner = line.strip_prefix('[')?.strip_suffix(']')?; + let (key, value) = inner.split_once('=')?; + (key.trim() == name).then_some(value.trim()) + }) +} + +struct Header { + lines: Vec, + entropy_len: usize, + nonce_len: usize, + ps_len: usize, + ai_len: usize, + out_len: usize, +} + +fn header_byte_len(header: &[String], name: &str, ctx: &str) -> usize { + let bits: usize = header_value(header, name) + .unwrap_or_else(|| panic!("{ctx}: 헤더 {name} 누락")) + .parse() + .unwrap_or_else(|_| panic!("{ctx}: 헤더 {name} 파싱 실패")); + assert!( + bits.is_multiple_of(8), + "{ctx}: {name} {bits}이 8의 배수가 아님" + ); + bits / 8 +} + +fn parse_header(section: &Section, alg: Alg, pr: bool, ctx: &str) -> Header { + assert!( + section.header.first().map(String::as_str) == Some(&format!("[{}]", alg.label())[..]), + "{ctx}: 헤더 알고리즘이 파일명 {}과 다름", + alg.label() + ); + let pr_value = header_value(§ion.header, "PredictionResistance") + .unwrap_or_else(|| panic!("{ctx}: PredictionResistance 헤더 누락")); + let header_pr = match pr_value { + "True" => true, + "False" => false, + other => panic!("{ctx}: PredictionResistance 값 {other:?} 해석 불가"), + }; + assert!(header_pr == pr, "{ctx}: PR 헤더가 파일명과 다름"); + let header = Header { + lines: section.header.clone(), + entropy_len: header_byte_len(§ion.header, "EntropyInputLen", ctx), + nonce_len: header_byte_len(§ion.header, "NonceLen", ctx), + ps_len: header_byte_len(§ion.header, "PersonalizationStringLen", ctx), + ai_len: header_byte_len(§ion.header, "AdditionalInputLen", ctx), + out_len: header_byte_len(§ion.header, "ReturnedBitsLen", ctx), + }; + assert!( + header.entropy_len >= alg.min_entropy(), + "{ctx}: EntropyInputLen 이 최소 엔트로피 미달" + ); + header +} + +const NO_PR_FIELDS: [&str; 8] = [ + "COUNT", + "EntropyInput", + "Nonce", + "PersonalizationString", + "EntropyInputReseed", + "AdditionalInputReseed", + "AdditionalInput", + "AdditionalInput", +]; + +const USE_PR_FIELDS: [&str; 8] = [ + "COUNT", + "EntropyInput", + "Nonce", + "PersonalizationString", + "EntropyInputPR", + "AdditionalInput", + "EntropyInputPR", + "AdditionalInput", +]; + +fn checked_hex(value: &str, expected_len: usize, ctx: &str) -> Vec { + let bytes = hex_decode(value, ctx); + assert!( + bytes.len() == expected_len, + "{ctx}: 길이 {}바이트가 헤더 기대 {expected_len}바이트와 다름", + bytes.len() + ); + bytes +} + +fn parse_case(block: &[(String, String)], pr: bool, header: &Header, ctx: &str) -> Case { + let expected_names: &[&str] = if pr { &USE_PR_FIELDS } else { &NO_PR_FIELDS }; + assert!( + block.len() == expected_names.len(), + "{ctx}: 필드 수 {} (기대 {})", + block.len(), + expected_names.len() + ); + for ((name, _), expected) in block.iter().zip(expected_names) { + assert!(name == expected, "{ctx}: 필드 {name:?} (기대 {expected:?})"); + } + let value = |idx: usize| block[idx].1.as_str(); + let entropy = checked_hex(value(1), header.entropy_len, &format!("{ctx} EntropyInput")); + let nonce = checked_hex(value(2), header.nonce_len, &format!("{ctx} Nonce")); + let ps = checked_hex( + value(3), + header.ps_len, + &format!("{ctx} PersonalizationString"), + ); + let flow = if pr { + Flow::UsePr { + pr: [ + ( + checked_hex( + value(4), + header.entropy_len, + &format!("{ctx} EntropyInputPR 1"), + ), + checked_hex(value(5), header.ai_len, &format!("{ctx} AdditionalInput 1")), + ), + ( + checked_hex( + value(6), + header.entropy_len, + &format!("{ctx} EntropyInputPR 2"), + ), + checked_hex(value(7), header.ai_len, &format!("{ctx} AdditionalInput 2")), + ), + ], + } + } else { + Flow::NoPr { + reseed_entropy: checked_hex( + value(4), + header.entropy_len, + &format!("{ctx} EntropyInputReseed"), + ), + reseed_ai: checked_hex( + value(5), + header.ai_len, + &format!("{ctx} AdditionalInputReseed"), + ), + ai: [ + checked_hex(value(6), header.ai_len, &format!("{ctx} AdditionalInput 1")), + checked_hex(value(7), header.ai_len, &format!("{ctx} AdditionalInput 2")), + ], + } + }; + Case { + entropy, + nonce, + ps, + flow, + } +} + +type FieldRow = Vec<(String, String)>; + +struct ExpSection { + header: Header, + rows: Vec, +} + +fn expected_sections(alg: Alg, pr: bool, sections: &[Section], ctx: &str) -> Vec { + assert!(!sections.is_empty(), "{ctx}: 섹션 없음"); + sections + .iter() + .enumerate() + .map(|(sec_idx, section)| { + let sctx = format!("{ctx} 섹션 {sec_idx}"); + let header = parse_header(section, alg, pr, &sctx); + assert!(!section.blocks.is_empty(), "{sctx}: COUNT 블록 없음"); + let rows = section + .blocks + .iter() + .enumerate() + .map(|(idx, block)| { + let bctx = format!("{sctx} 블록 {idx}"); + let case = parse_case(block, pr, &header, &bctx); + assert!( + block[0].1 == idx.to_string(), + "{bctx}: COUNT {} (기대 {idx})", + block[0].1 + ); + let returned = alg.run(&case, header.out_len); + let mut row = block.clone(); + row.push(("ReturnedBits".to_string(), hex_encode(&returned))); + row + }) + .collect(); + ExpSection { header, rows } + }) + .collect() +} + +fn total_blocks(expected: &[ExpSection]) -> usize { + expected.iter().map(|s| s.rows.len()).sum() +} + +fn compare_sections(expected: &[ExpSection], parsed: &[Section], ctx: &str) -> Vec { + assert!( + parsed.len() == expected.len(), + "{ctx}: 기대 {}섹션 vs 파일 {}섹션", + expected.len(), + parsed.len() + ); + let mut mismatches = Vec::new(); + for (sec_idx, (exp, section)) in expected.iter().zip(parsed.iter()).enumerate() { + assert!( + section.header == exp.header.lines, + "{ctx} 섹션 {sec_idx}: 헤더가 req 와 다름" + ); + assert!( + section.blocks.len() == exp.rows.len(), + "{ctx} 섹션 {sec_idx}: 기대 {}블록 vs 파일 {}블록", + exp.rows.len(), + section.blocks.len() + ); + for (idx, (exp_fields, blk)) in exp.rows.iter().zip(section.blocks.iter()).enumerate() { + assert!( + blk.len() == exp_fields.len(), + "{ctx} 섹션 {sec_idx} 블록 {idx}: 필드 수 {} (기대 {})", + blk.len(), + exp_fields.len() + ); + for ((name, value), (exp_name, exp_value)) in blk.iter().zip(exp_fields.iter()) { + assert!( + name == exp_name, + "{ctx} 섹션 {sec_idx} 블록 {idx}: 필드 {name:?} (기대 {exp_name:?})" + ); + if !value.eq_ignore_ascii_case(exp_value) { + mismatches.push(format!( + "섹션 {sec_idx} 블록 {idx} {name}: 계산 {exp_value} vs 파일 {value}" + )); + } + } + } + } + mismatches +} + +fn fill_template(template: &str, expected: &[ExpSection], ctx: &str) -> String { + let newline = if template.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + let mut out: Vec = Vec::new(); + let mut sec_idx = 0usize; + let mut header_idx = 0usize; + let mut block_idx = 0usize; + let mut field_idx = 0usize; + let mut saw_field = false; + for line in template.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + if saw_field { + block_idx += 1; + field_idx = 0; + saw_field = false; + } + out.push(line.to_string()); + continue; + } + if trimmed.starts_with('[') { + assert!(sec_idx < expected.len(), "{ctx}: 템플릿 섹션 수 초과"); + if header_idx == expected[sec_idx].header.lines.len() { + assert!( + block_idx == expected[sec_idx].rows.len() && !saw_field, + "{ctx} 섹션 {sec_idx}: 블록 수 {block_idx} (기대 {}) 상태에서 다음 섹션 시작", + expected[sec_idx].rows.len() + ); + sec_idx += 1; + header_idx = 0; + block_idx = 0; + field_idx = 0; + assert!(sec_idx < expected.len(), "{ctx}: 템플릿 섹션 수 초과"); + } + let exp_lines = &expected[sec_idx].header.lines; + assert!( + header_idx < exp_lines.len() && trimmed == exp_lines[header_idx], + "{ctx} 섹션 {sec_idx}: 템플릿 헤더 {trimmed:?} 이 req 헤더와 다름" + ); + header_idx += 1; + out.push(line.to_string()); + continue; + } + saw_field = true; + let (name, value) = trimmed + .split_once('=') + .unwrap_or_else(|| panic!("{ctx}: 잘못된 템플릿 라인 {line:?}")); + let name = name.trim(); + let value = value.trim(); + assert!( + sec_idx < expected.len() && block_idx < expected[sec_idx].rows.len(), + "{ctx}: 템플릿 블록 수가 기대 초과 (섹션 {sec_idx} 블록 {block_idx})" + ); + let row = &expected[sec_idx].rows[block_idx]; + assert!( + field_idx < row.len(), + "{ctx} 섹션 {sec_idx} 블록 {block_idx}: 템플릿 필드 수가 기대 {}필드 초과", + row.len() + ); + let (exp_name, exp_value) = &row[field_idx]; + field_idx += 1; + assert!( + name == exp_name, + "{ctx} 섹션 {sec_idx} 블록 {block_idx}: 템플릿 필드 {name:?} (기대 {exp_name:?})" + ); + if value == "?" { + out.push(format!("{name} = {exp_value}")); + } else { + assert!( + value.eq_ignore_ascii_case(exp_value), + "{ctx} 섹션 {sec_idx} 블록 {block_idx}: 템플릿 {name} 값이 req 계산과 불일치" + ); + out.push(line.to_string()); + } + } + assert!( + sec_idx == expected.len() - 1 + && header_idx == expected[sec_idx].header.lines.len() + && block_idx + usize::from(saw_field) == expected[sec_idx].rows.len(), + "{ctx}: 템플릿이 req 보다 짧음 (섹션 {sec_idx} 블록 {block_idx})" + ); + let mut rsp = out.join(newline); + if template.ends_with('\n') { + rsp.push_str(newline); + } + rsp +} + +fn synthesize(expected: &[ExpSection], newline: &str) -> String { + let mut out = String::new(); + for (sec_idx, section) in expected.iter().enumerate() { + if sec_idx > 0 { + out.push_str(newline); + } + for line in §ion.header.lines { + out.push_str(line); + out.push_str(newline); + } + for fields in §ion.rows { + out.push_str(newline); + for (name, value) in fields { + out.push_str(&format!("{name} = {value}{newline}")); + } + } + } + out +} + +// +// 파일 수집 +// + +fn classify(path: &Path) -> Option<(Alg, bool)> { + let stem = path.file_stem()?.to_str()?.to_ascii_uppercase(); + if !stem.starts_with("HASH_DRBG") { + return None; + } + let pr = if stem.contains("(USE PR)") { + true + } else if stem.contains("(NO PR)") { + false + } else { + return None; + }; + let alg = if stem.contains("SHA3-224") { + Alg::SHA3_224 + } else if stem.contains("SHA3-256") { + Alg::SHA3_256 + } else if stem.contains("SHA3-384") { + Alg::SHA3_384 + } else if stem.contains("SHA3-512") { + Alg::SHA3_512 + } else if stem.contains("SHA-224") { + Alg::SHA224 + } else if stem.contains("SHA-256") { + Alg::SHA256 + } else if stem.contains("SHA-384") { + Alg::SHA384 + } else if stem.contains("SHA-512") { + Alg::SHA512 + } else { + return None; + }; + Some((alg, pr)) +} + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn collect_req_files(dir: &Path, found: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_req_files(&path, found); + } else if path + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e.eq_ignore_ascii_case("req")) + && classify(&path).is_some() + { + found.push(path); + } + } +} + +fn report_mismatches(path: &Path, mismatches: &[String], total: usize, failures: &mut Vec) { + eprintln!( + "{} 대조 실패 {}건 (전체 {total} 블록)", + path.display(), + mismatches.len() + ); + for m in mismatches.iter().take(10) { + eprintln!(" {m}"); + } + failures.push(format!("{}: {}건 불일치", path.display(), mismatches.len())); +} + +// cavp/ 트리의 Hash_DRBG no PR·use PR req 16종(파일당 4섹션 × 15 COUNT)을 계산해 rsp 파일로 출력합니다 (sam 은 배포 템플릿 그대로 두고 레이아웃·대조 기준으로만 사용, 기존 rsp 는 회귀 대조) +#[test] +fn kcmvp_hash_drbg_req_vs_rsp() { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - KCMVP Hash_DRBG 대조 생략"); + return; + }; + let mut req_files = Vec::new(); + collect_req_files(&root, &mut req_files); + req_files.sort(); + if req_files.is_empty() { + eprintln!( + "{}: Hash_DRBG req 파일 없음 - KCMVP Hash_DRBG 대조 생략", + root.display() + ); + return; + } + + let mut failures = Vec::new(); + for req in &req_files { + let ctx = req.display().to_string(); + let (alg, pr) = classify(req).unwrap(); + let content = fs::read_to_string(req).unwrap_or_else(|e| panic!("{ctx}: 읽기 실패 {e}")); + let sections = parse_sections(&content, &ctx); + let expected = expected_sections(alg, pr, §ions, &ctx); + let total = total_blocks(&expected); + let sam_path = req.with_extension("sam"); + let rsp_path = req.with_extension("rsp"); + + let mut layout = None; + let mut sam_ok = true; + match fs::read_to_string(&sam_path) { + Ok(sam) if sam.contains("= ?") => layout = Some(sam), + Ok(sam) => { + let sam_sections = parse_sections(&sam, &ctx); + let mismatches = compare_sections(&expected, &sam_sections, &ctx); + if mismatches.is_empty() { + println!("{} 대조 통과 ({total} 블록)", sam_path.display()); + layout = Some(sam); + } else { + report_mismatches(&sam_path, &mismatches, total, &mut failures); + sam_ok = false; + } + } + Err(_) => {} + } + if !sam_ok { + continue; + } + + match fs::read_to_string(&rsp_path) { + Ok(rsp) if !rsp.contains("= ?") => { + let rsp_sections = parse_sections(&rsp, &ctx); + let mismatches = compare_sections(&expected, &rsp_sections, &ctx); + if mismatches.is_empty() { + println!("{} 대조 통과 ({total} 블록)", rsp_path.display()); + } else { + report_mismatches(&rsp_path, &mismatches, total, &mut failures); + } + } + _ => { + let rsp = match &layout { + Some(template) => fill_template(template, &expected, &ctx), + None => { + let newline = if content.contains("\r\n") { + "\r\n" + } else { + "\n" + }; + synthesize(&expected, newline) + } + }; + fs::write(&rsp_path, rsp).unwrap_or_else(|e| panic!("{ctx}: rsp 쓰기 실패 {e}")); + println!("{} 생성 ({total} 블록)", rsp_path.display()); + } + } + } + assert!( + failures.is_empty(), + "rsp·sam 대조 실패\n{}", + failures.join("\n") + ); +} + +// SP 800-90A 를 그대로 옮긴 독립 참조 구현과의 전수 차분(8 알고리즘 × PR × ps·ai 유무)으로 라이브러리 경로와 하네스 흐름을 자가 검증하고, 템플릿 채움·오염 검출 로직을 점검합니다 +#[test] +fn kcmvp_hash_drbg_self_check() { + for (alg_idx, alg) in ALL_ALGS.into_iter().enumerate() { + for pr in [false, true] { + for with_ps in [false, true] { + for with_ai in [false, true] { + let seed_byte = 0x11 + (alg_idx as u8) * 0x10; + let entropy = vec![seed_byte; alg.min_entropy().max(24)]; + let nonce = vec![seed_byte ^ 0xFF; 16]; + let ps = if with_ps { + vec![0xA5u8; 20] + } else { + Vec::new() + }; + let ai_body = if with_ai { + vec![0x3Cu8; 16] + } else { + Vec::new() + }; + let flow = if pr { + Flow::UsePr { + pr: [ + (vec![0xB0 ^ seed_byte; entropy.len()], ai_body.clone()), + (vec![0xC1 ^ seed_byte; entropy.len()], ai_body.clone()), + ], + } + } else { + Flow::NoPr { + reseed_entropy: vec![0xD2 ^ seed_byte; entropy.len()], + reseed_ai: ai_body.clone(), + ai: [ai_body.clone(), ai_body.clone()], + } + }; + let case = Case { + entropy, + nonce, + ps, + flow, + }; + let out_len = 100; + let lib = alg.run(&case, out_len); + let reference = run_case_ref(alg, &case, out_len); + assert!( + lib == reference, + "{} pr={pr} ps={with_ps} ai={with_ai}: 라이브러리·참조 구현 차분 불일치", + alg.label() + ); + assert!(lib.iter().any(|&b| b != 0), "{} 출력이 전부 0", alg.label()); + } + } + } + } + + let header = Header { + lines: vec![ + "[SHA-256]".to_string(), + "[PredictionResistance = False]".to_string(), + ], + entropy_len: 32, + nonce_len: 16, + ps_len: 0, + ai_len: 0, + out_len: 32, + }; + let entropy_hex = "11".repeat(32); + let nonce_hex = "22".repeat(16); + let reseed_hex = "33".repeat(32); + let case = Case { + entropy: hex_decode(&entropy_hex, "self"), + nonce: hex_decode(&nonce_hex, "self"), + ps: Vec::new(), + flow: Flow::NoPr { + reseed_entropy: hex_decode(&reseed_hex, "self"), + reseed_ai: Vec::new(), + ai: [Vec::new(), Vec::new()], + }, + }; + let returned = hex_encode(&Alg::SHA256.run(&case, header.out_len)); + let rows: Vec = vec![vec![ + ("COUNT".to_string(), "0".to_string()), + ("EntropyInput".to_string(), entropy_hex.clone()), + ("Nonce".to_string(), nonce_hex.clone()), + ("PersonalizationString".to_string(), String::new()), + ("EntropyInputReseed".to_string(), reseed_hex.clone()), + ("AdditionalInputReseed".to_string(), String::new()), + ("AdditionalInput".to_string(), String::new()), + ("AdditionalInput".to_string(), String::new()), + ("ReturnedBits".to_string(), returned.clone()), + ]]; + let expected = vec![ExpSection { header, rows }]; + let template = format!( + "[SHA-256]\r\n[PredictionResistance = False]\r\n\r\nCOUNT = 0\r\nEntropyInput = {entropy_hex}\r\nNonce = {nonce_hex}\r\nPersonalizationString = \r\nEntropyInputReseed = {reseed_hex}\r\nAdditionalInputReseed = \r\nAdditionalInput = \r\nAdditionalInput = \r\nReturnedBits = ?\r\n" + ); + let filled = fill_template(&template, &expected, "self-fill"); + assert!(!filled.contains('?'), "템플릿 미기입 잔존"); + let filled_sections = parse_sections(&filled, "self-verify"); + let mismatches = compare_sections(&expected, &filled_sections, "self-verify"); + assert!(mismatches.is_empty(), "생성 rsp 대조 실패 {mismatches:?}"); + + let corrupted_char = if returned.ends_with('0') { "1" } else { "0" }; + let corrupted_hex = format!("{}{corrupted_char}", &returned[..returned.len() - 1]); + let corrupted = filled.replacen(returned.as_str(), &corrupted_hex, 1); + let corrupted_sections = parse_sections(&corrupted, "self-corrupt"); + let mismatches = compare_sections(&expected, &corrupted_sections, "self-corrupt"); + assert!(mismatches.len() == 1, "오염 rsp 미검출 {mismatches:?}"); + + let synthesized = synthesize(&expected, "\r\n"); + let synthesized_sections = parse_sections(&synthesized, "self-synth"); + let mismatches = compare_sections(&expected, &synthesized_sections, "self-synth"); + assert!(mismatches.is_empty(), "합성 rsp 대조 실패 {mismatches:?}"); +} From bf88e25e22b8cd1a0f5354e826546a915cb29463 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:33:02 +0900 Subject: [PATCH 48/65] =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EA=B0=B1=EC=8B=A0?= =?UTF-8?q?=20-=20README=20=EC=A0=9C=EB=AA=A9=EC=9D=84=20ELIB-K0-NT=20?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20entlib-nativ?= =?UTF-8?q?e=20=EB=B9=84=EA=B5=90=20=EB=AC=B8=EB=8B=A8=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20-=20INTRODUCTION=20=EC=9D=84=20K0=20=EC=BB=A4?= =?UTF-8?q?=EB=84=90=20=EB=AA=A9=EC=A0=81=EA=B3=BC=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=91=9C=20=EC=A4=91=EC=8B=AC=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9E=AC=EC=9E=91=EC=84=B1=20-=20AI=5FSCOPE=20?= =?UTF-8?q?=EC=97=90=20=ED=81=AC=EB=A0=88=EC=9D=B4=ED=8A=B8=EB=B3=84=20AI?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20=EB=B2=94=EC=9C=84=20=EB=AA=A9=EC=B0=A8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20CONTRIBUTING=20=EC=97=90=20?= =?UTF-8?q?=EA=B2=80=ED=86=A0=20=EC=9A=94=EC=B2=AD=20=EB=B2=94=EC=9C=84?= =?UTF-8?q?=EC=99=80=20AI=20=EC=97=90=EC=9D=B4=EC=A0=84=ED=8A=B8=20?= =?UTF-8?q?=EA=B8=B0=EC=97=AC=20=EA=B7=9C=EC=B9=99=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?-=20cross-confirm=20=EC=9D=84=20release1.1.0-confirm=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?-=20gitignore=20=EC=97=90=20claude=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- AI_SCOPE.md | 90 ++++++++++-- AI_SCOPE_EN.md | 92 ++++++++++-- CONTRIBUTING.md | 150 +++++++++++--------- CONTRIBUTING_EN.md | 131 +++++++++++++++-- INTRODUCTION.md | 49 +++++-- INTRODUCTION_EN.md | 51 +++++-- README.md | 16 +-- README_EN.md | 22 ++- cross-confirm.md => release1.1.0-confirm.md | 0 10 files changed, 456 insertions(+), 148 deletions(-) rename cross-confirm.md => release1.1.0-confirm.md (100%) diff --git a/.gitignore b/.gitignore index 289b024..66cd9ff 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ dist/ Cargo.lock CLAUDE.md .planning/ -cavp/ \ No newline at end of file +cavp/ +.claude/ \ No newline at end of file diff --git a/AI_SCOPE.md b/AI_SCOPE.md index 1873cb8..cd7192e 100644 --- a/AI_SCOPE.md +++ b/AI_SCOPE.md @@ -2,22 +2,94 @@ [![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](AI_SCOPE_EN.md) -저는 이 암호 모듈의 다양한 암호 기능과 기술적인 작동 방식을 상세화한 명세나 모듈과 함수 등의 기능 작동 사항에 대한 다이렉트 주석을 제가 직접 작성하려고 최대한 노력합니다만, 작성된 결과물을 '이해하기 쉽게' 만드는 데(Mermaid 다이어그램을 생성하거나, 명세의 가독성 향상을 위한 맥락, 설명체 등의 간결화 작업과 영문 번역 등) [Claude Code](http://claude.ai/)의 Sonnet 4.6 모델을 적극적으로 사용합니다. +`1.1.0` 릴리즈를 작업하면서 느낀 바로는, AI 에이전트가 사용된 범위와 사람이 직접 검증할 예정이거나 이미 진행된 부분에 대한 체크리스트 노트가 존재하면 좋다고 생각했습니다. 보안 기능 개발에서 이 문서는 아주 중요하게 여겨질거라 생각됩니다. 여러분의 냉정하고 현실적인 피드백에 대해 전적으로 동의하겠습니다. 저희는 이러한 개발 방식을 채택했지만, 여러분의 소중한 피드백은 사람이 직접 보고 판단하겠다고 명심하겠습니다. -추가적으로, [ISO-LIGHT-K0](https://github.com/Quant-Off/iso-light-k0) 마이크로커널과의 통합을 위해 기능적으로 어떤 부분이 필요한지 점검하고, 누락됐거나 개선이 필요한 부분에 대해 피드백(리뷰) 받고, 암호 알고리즘이 국제적 표준을 준수하는지 확인하기 위해 [NIST FIPS 140-2 또는 3](https://csrc.nist.gov/projects/cryptographic-module-validation-program/fips-140-3-standards)의 명세를 파악하기 위해 Claude Code의 Opus 4.8, Fable 5, [Gemini 3.1 Pro](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-1-pro?hl=ko), [Qwen 3.7 Max](https://qwen.ai/blog?id=qwen3.7) 모델을 사용합니다. 모든 작업을 이 분야의 전문가를 통해 교차 검증하는 것을 절대 잊지 않습니다. +> [!IMPORTANT] +> 이 문서의 내용은 AI를 활용하여 다양한 자동화 작업이나, 개발, 보안 감사를 진행하는 사람들을 비판하기 위해 작성된 것이 아닙니다. 발전하는 AI 시대에 맞게 AI 사용 범위를 사람을 위해 명시했을 뿐입니다. 우선, 이 문서는 사람이 직접 작성했습니다. -이 프로젝트는 1인 개발임에 따라 '문서 작업', '주석 작업'에 에이전트를 적극 활용하고 있습니다. 에이전트가 이 프로젝트의 파일을 수정하는 부분은 `.md` 파일과 작성된 모듈, 함수의 주석 추가를 위한 `.rs` 파일입니다. **민감한 기능**(암호 알고리즘의 구현 등)에 대해서는 절대적으로 접근할 수 없도록 제한하고 있습니다. 저는 바이브 코더가 아니며, 다소 구시대적인 개발자일 뿐입니다. 이 명시는 AI를 활용하여 다양한 자동화 작업이나, 개발, 보안 감사를 진행하는 사람들에 대해 비아냥거리기 위해 작성된 것이 아닙니다. 발전하는 AI 시대에 맞게 AI 사용 범위를 인간을 위해 명시했을 뿐입니다. +슬래쉬를 통해 나뉘어진 여러 분류는 'OR'을 의미합니다. -## 지침과 사용 프롬프트에 관해 +## Table of Content + +- [공통적](#공통적) +- [Crates](#crates) + - [AES](#aes) + - [BLAKE](#blake) + - [ChaCha20](#chacha20) + - [Constant-Time](#constant-time) + - [Ed448](#ed448) + - [Ed25519](#ed25519) + - [ML-DSA](#ml-dsa) + - [ML-KEM](#ml-kem) + - [SHA2](#sha2) + - [SHA3](#sha3) + - [X448](#x448) + - [X25519](#x25519) + - [Zeroize](#zeroize) +- [지침과 사용 프롬프트에 관해](#지침과-사용-프롬프트에-관해) +- [추가적으로](#추가적으로) + +## 공통적 + +- 암호 알고리즘에 대한 기술 명세를 크레이트 루트 `README.md`에 작성 (Opus 5 / Fable 5) +- 일부 `//`, `//!`, `///` 등 주석을 통한 흐름 및 함/변수, 모듈 설명 (Opus 4.8 / Sonnet 5) + - AI가 생성한 주석의 경우를 아래 크레이트별 AI 사용 범위 설명에서 포함하겠습니다. +- 특정 문서의 영문 번역 (Sonnet 5) + - 모든 문서는 기본적으로 **한국어**로 작성하고 있습니다. +- 개별 암호 알고리즘 표준안에 따른 보고서 작성 (Sonnet 5 / Opus 5 / Fable 5) + - 해당 보고서는 저장소에 배포되지 않습니다. + - 보고서 작성에 100% 비율만큼 AI를 사용하진 않고 있습니다. + - (1차) 개발자를 통해 분석, (2차) AI가 교차 검증, (3차) 대학 교수, 대학원생, 중소기업 보안 개발 종사자(포괄; 전문가)의 교차 검증을 거치며, 참여자들의 의견에 따라 추 후 프로필을 게시할 예정입니다. +- 로컬 테스트 환경으로 스니펫을 제공하는, 또는 그런 일관된 행위 (Qwen 3.5) +- 엣지 케이스 분석, 전체적 코드 감사, 일부 테스트 코드 작성 (Fable 5) + - 코드 감사 작업의 경우 아키텍처별 다른 작동 가능성, K0 커널 통합에 따른 IPC/Ring 3 격리 모델에 위반되는 사항, 암호 알고리즘 표준안에 명시된 (복잡성을 가진) 특정 알고리즘의 구현 방법에 관한 논의 등이 이루어집니다. + +## Crates + +### AES + +### BLAKE + +### ChaCha20 + +### Constant-Time + +- 각 파일에서 스플릿 형식 `//` 주석 (Sonnet 4.5) +- [src/lib.rs의 ct_sel_swap_value_roundtrip 테스트 함수](https://github.com/Quant-Off/elib-k0-nt/blob/7d3a0b8389fcc0a5bcbef22ae70fa380ce420614/constant-time/src/lib.rs#L732) (Opus 4.8) + +### Ed448 -CLAUDE.md 지침 문서엔 주로 주석 작성 방법, 결과물 동작 및 구현 검증을 위한 로컬 샌드박스 작동 환경, 생태계 이해를 위한 설명이 작성되어 있습니다. +### Ed25519 -알고리즘 구현 1차적 검증 작업은 [GSD(Git-Ship-Done) Core](https://github.com/open-gsd/gsd-core)를, 폐쇄형 샌드박스에서 작동하는 에이전트가 프로젝트를 원활히 학습할 수 있도록 정형 문서화 작업 및 제2의 뇌 구축 작업엔 [Graphify](https://github.com/safishamsi/graphify)를 사용하고 있습니다. `CLAUDE.md` 파일, `.planning` 디렉토리에는 환경에 대한 설명과 실제 경로 등이 포함되어 있어 보안을 위해 이그노어링됩니다. +### ML-DSA -Claude Code의 경우, 문서화와 주석 작성을 위해 "@path/to/module 에 Docstring을 추가해.", "해당 모듈은 ~역할을 맡는 모듈로서, ... 주석을 작성 및 수정해."와 같은 톤과 맥락을 사용합니다. 모듈이 기술적으로 동작하는 데에는 "해당 모듈에 작성된 주석을 바탕으로 현재 모듈의 기술적 작동 방식을 검증하고 결과를 @path/to/target.md 에 작성해."나 "@path/to/pdf FIPS 202 명세에 따른 SHA3 구현 중, 이 모듈의 ~기능이 상태 배열에 대한 레이블링 규칙을 준수하는지 확인해."와 같은 톤을 유지합니다. **이 과정에서 Claude Code가 문서를 "신규 작성"할 뿐, 구현을 직접 수정하지 않습니다.** +### ML-KEM -커밋은 단순히 "현재 변경 사항을 n개로 나눠 커밋해." 를 사용합니다. +### SHA2 + +### SHA3 + +### X448 + +### X25519 + +### Zeroize + +- 테스트 코드 `zeroize_readback.rs` 작성 (Opus 5) +- 사람에 의해 발견된 문제에 관한 해결책을 찾음 (Fable 5) + - `Secret` 구조체의 Drop이 임의 T를 비트 제로화하는 문제, `zeroize_flat`이 safe 함수처리되어 있지만 invalid value를 생성하는 문제를 해결하기 위한 해결책으로, `Zeroable` 마커 트레이트에 관한 아이디어를 얻었으며 뼈대 코드, 주석을 작성 + - 단, 다른 모듈 및 크레이트에 적용하고 응용하는 방식은 **사람이 직접 작성** +- `Secret` 구조체에 상수-시간 프리미티브 적용 아이디어 (Fable 5) + +## 지침과 사용 프롬프트에 관해 + +CLAUDE.md 지침 문서엔 주로 주석 작성 방법, 결과물 동작 및 구현 검증을 위한 로컬 샌드박스 작동 환경, 생태계 이해를 위한 설명이 작성되어 있습니다. `CLAUDE.md` 파일, [GSD(Git-Ship-Done) Core](https://github.com/open-gsd/gsd-core)으로 인해 생성된 `.planning` 디렉토리에는 개발 환경에 대한 구체적 설명(실제 경로 등)이 포함되어 있어 보안을 위해 이그노어링했습니다. + +문서화와 주석 작성을 위해 "@path/to/module 에 Docstring을 추가해.", "해당 모듈은 ~역할을 맡는 모듈로서, ... 주석을 작성 및 수정해."와 같은 톤과 맥락을 사용합니다. 모듈이 기술적으로 동작하는 데에는 "해당 모듈에 작성된 주석을 바탕으로 현재 모듈의 기술적 작동 방식을 검증하고 결과를 @path/to/target.md 에 작성해."나 "@path/to/pdf FIPS 202 명세에 따른 SHA3 구현 중, 이 모듈의 ~기능이 상태 배열에 대한 레이블링 규칙을 준수하는지 확인해."와 같은 톤을 유지합니다. ## 추가적으로 -이 프로젝트에 적용되는 AI에 대해 여러분은 언제든 자신의 의견을 내어 주실 수 있습니다. 이메일 또는 [저장소 논의](https://github.com/Quant-Off/elib-k0-nt/discussions)에서 "AI Scope" 카테고리를 적극적으로 사용해주세요. +이 문서에 대해 의견이 있다면, 다음 방식 중 편한 하나로 언제든 알려주세요. + +- 이메일: 또는 +- "AI Scope" 카테고리를 사용한 [저장소 논의](https://github.com/Quant-Off/elib-k0-nt/discussions) \ No newline at end of file diff --git a/AI_SCOPE_EN.md b/AI_SCOPE_EN.md index cc1ac2c..404a05e 100644 --- a/AI_SCOPE_EN.md +++ b/AI_SCOPE_EN.md @@ -2,22 +2,94 @@ [![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](AI_SCOPE.md) -I personally write detailed specifications covering the cryptographic features and technical workings of this module, along with inline comments on the behavior of individual modules and functions. That said, I actively use the [Claude Code](http://claude.ai/) Sonnet 4.6 model to make the produced artifacts **easier to understand** — generating Mermaid diagrams, condensing context and explanations for improved readability in specifications, and handling English translation. +While working on the `1.1.0` release, we felt it would be good to keep a checklist note of the scope in which AI agents were used, and of the parts scheduled for (or already given) direct human verification. We believe this kind of document matters a great deal in security feature development. We fully agree with any cold, realistic feedback you may have. We've adopted this development approach, but we'll keep in mind that your valuable feedback is something a human will review and judge directly. -In addition, I use Opus 4.8 / Fable 5 (via Claude Code), [Gemini 3.1 Pro](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/gemini/3-1-pro?hl=en), and [Qwen 3.7 Max](https://qwen.ai/blog?id=qwen3.7) to review which functional pieces are needed for integration with the [ISO-LIGHT-K0](https://github.com/Quant-Off/iso-light-k0) microkernel, receive feedback on missing or improvable areas, and cross-check whether the cryptographic algorithms comply with international standards such as [NIST FIPS 140-2 / 140-3](https://csrc.nist.gov/projects/cryptographic-module-validation-program/fips-140-3-standards). All results are cross-validated by human domain experts. +> [!IMPORTANT] +> This document is not written to criticize people who use AI for various automation tasks, development, or security audits. It simply spells out, for people's benefit, the scope of AI usage in keeping with the advancing AI era. First and foremost, this document itself was written directly by a human. -Because this is a solo project, I rely heavily on AI agents for **documentation and comment writing**. Agents are permitted to modify only `.md` files and `.rs` files, and solely for the purpose of adding comments to existing modules and functions. Access to **sensitive logic** — such as cryptographic algorithm implementations — is strictly restricted. I am not a vibe coder; I am simply a developer who still believes in doing things the old-fashioned way. This statement is not meant to ridicule those who use AI for automation, development, or security auditing. It is simply a disclosure of where AI assistance ends and human judgment begins, for the benefit of anyone reading this project. +Where multiple categories are separated by a slash, that means 'OR'. -## Guidelines and Prompt Conventions +## Table of Content -The `CLAUDE.md` instruction document primarily covers: comment-writing conventions, the local sandbox environment used for verifying outputs and implementations, and contextual explanations of the ecosystem. +- [Common](#common) +- [Crates](#crates) + - [AES](#aes) + - [BLAKE](#blake) + - [ChaCha20](#chacha20) + - [Constant-Time](#constant-time) + - [Ed448](#ed448) + - [Ed25519](#ed25519) + - [ML-DSA](#ml-dsa) + - [ML-KEM](#ml-kem) + - [SHA2](#sha2) + - [SHA3](#sha3) + - [X448](#x448) + - [X25519](#x25519) + - [Zeroize](#zeroize) +- [On Guidelines and Prompts Used](#on-guidelines-and-prompts-used) +- [Additionally](#additionally) -For first-pass algorithm implementation verification, I use [GSD (Git-Ship-Done) Core](https://github.com/open-gsd/gsd-core). For structured documentation and second-brain construction — so that agents running inside a closed sandbox can learn the project efficiently — I use [Graphify](https://github.com/safishamsi/graphify). The `CLAUDE.md` file and the `.planning` directory contain environment descriptions and real local paths, so they are gitignored for security. +## Common -For documentation and comment work in Claude Code, prompts such as the following are used: *"Add a Docstring to @path/to/module."* or *"This module serves the role of ~; write and revise its comments accordingly."* For technical verification of how a module operates, prompts follow the pattern: *"Based on the comments in this module, verify the technical behavior and write the result to @path/to/target.md."* or *"Check whether the [feature] in this SHA3 implementation conforms to the state-array labeling rules defined in the FIPS 202 spec at @path/to/pdf."* **In this process, Claude Code only creates new documentation — it never directly modifies implementations.** +- Writing the technical specification for a cryptographic algorithm in the crate root's `README.md` (Opus 5 / Fable 5) +- Some flow, function/variable, and module explanations via `//`, `//!`, `///` comments (Opus 4.8 / Sonnet 5) + - Cases of AI-generated comments are included in the per-crate AI usage scope descriptions below. +- English translation of certain documents (Sonnet 5) + - All documents are written in **Korean** by default. +- Writing reports based on individual cryptographic algorithm standards (Sonnet 5 / Opus 5 / Fable 5) + - These reports are not distributed with the repository. + - We do not use AI for 100% of report writing. + - The process goes through (1st) analysis by a developer, (2nd) cross-verification by AI, and (3rd) cross-verification by university professors, graduate students, and security developers at small/medium enterprises (collectively, "experts"). We plan to publish a profile later based on participants' input. +- Providing snippets for a local test environment, or similarly consistent actions (Qwen 3.5) +- Edge-case analysis, overall code audits, and writing some test code (Fable 5) + - Code audit work involves discussions of differing behavior across architectures, violations of the IPC/Ring 3 isolation model arising from K0 kernel integration, and the implementation approach for specific (inherently complex) algorithms specified in cryptographic standards. -Commits are initiated simply with: *"Split the current changes into n commits."* +## Crates -## Additional Notes +### AES -You are welcome to share your thoughts on the AI usage in this project at any time. Please use the email or the [repository discussions](https://github.com/Quant-Off/elib-k0-nt/discussions) under the "AI Scope" category. +### BLAKE + +### ChaCha20 + +### Constant-Time + +- Split-style `//` comments in individual files (Sonnet 4.5) +- [The `ct_sel_swap_value_roundtrip` test function in src/lib.rs](https://github.com/Quant-Off/elib-k0-nt/blob/7d3a0b8389fcc0a5bcbef22ae70fa380ce420614/constant-time/src/lib.rs#L732) (Opus 4.8) + +### Ed448 + +### Ed25519 + +### ML-DSA + +### ML-KEM + +### SHA2 + +### SHA3 + +### X448 + +### X25519 + +### Zeroize + +- Writing the `zeroize_readback.rs` test code (Opus 5) +- Finding a solution to a problem discovered by a human (Fable 5) + - As a solution to the problem where `Drop` on the `Secret` struct bit-zeroizes an arbitrary `T`, and the problem where `zeroize_flat` is treated as a safe function while producing an invalid value, we drew the idea of a `Zeroable` marker trait from it, and wrote the skeleton code and comments for that. + - However, how it is applied and adapted to other modules and crates was **written directly by a human**. +- The idea of applying constant-time primitives to the `Secret` struct (Fable 5) + +## On Guidelines and Prompts Used + +The `CLAUDE.md` guideline document mainly contains comment-writing conventions, the local sandbox environment used to verify build behavior and implementation, and explanations for understanding the ecosystem. The `.planning` directory generated by the `CLAUDE.md` file and by [GSD (Git-Ship-Done) Core](https://github.com/open-gsd/gsd-core) contains specific details about the development environment (such as actual paths), so it has been ignored for security reasons. + +For documentation and comment writing, we use a tone and context like "Add a docstring to @path/to/module." or "This module is responsible for ~; write and revise its comments...". For verifying that a module technically works, we keep a tone like "Based on the comments written in this module, verify how the module technically operates and write the result to @path/to/target.md." or "In this SHA3 implementation per the @path/to/pdf FIPS 202 specification, check whether this module's ~ function complies with the state-array labeling rules." + +## Additionally + +If you have any thoughts on this document, feel free to let us know through whichever of the following is convenient. + +- Email: , or +- [Repository Discussions](https://github.com/Quant-Off/elib-k0-nt/discussions) under the "AI Scope" category diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71b9024..7598aff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,116 +4,132 @@ *가장 먼저, 기여해주시는 모든 여러분께 감사 인사를 드립니다. 여러분의 기여는 아주 큰 힘이 됩니다.* -기여에 앞서 몇 가지 주의 사항을 알려드리고자 합니다. 불편하시겠지만, 엄중한 보안성을 위해 철저히 지켜주시길 바랍니다. 아시다시피 저희는 크게 다음 두 가지 보안 원칙을 준수합니다. +포괄적인 감사(audit)를 요청하는 건 아닙니다. 각 모듈은 독립적인 크레이트이므로, 관심 있는 알고리즘 하나만 골라 다음 중 하나에 집중해 주셔도 충분합니다. + +- 특정 구현이 참조 사양(FIPS/RFC/관련 논문)과 정확히 일치하는지 +- 분기(branch)나 배열 인덱싱이 비밀 데이터에 의존해 타이밍 차이를 만드는지 +- `zeroize` 호출이 실제로 스코프를 벗어나기 전에 메모리를 (정상적으로) 지우는지, 컴파일러 최적화로 제거되지 않는지 +- API가 안전하지 않은 사용(예를 들어, nonce 재사용 등)을 컴파일 타임 또는 런타임에 막고 있는지 + +발견 사항은 이슈로 등록해 주시면 되고, 코드 수정이 필요 없는 "이 부분이 의심스럽다"는 지적만으로도 충분한 기여입니다. + +이어서 기여에 앞서 몇 가지 주의 사항을 알려드리고자 합니다. 아시다시피 저희는 크게 다음 두 가지 보안 원칙을 준수합니다. - 제로 트러스트 (Zero-Trust) - 폐쇄 동작 (Air-Gapped) -이는 외부로부터 들여오는 모든 소스에 대해 기본적으로 신뢰하지 않으며, 모든 기능은 Sandbox-like 으로 바이너리 독립적으로 동작 가능해야 함을 의미합니다. 여러분이 저희 프로젝트에 특별한 소스를 첨가하기 위해선 위 원칙은 매우 중요합니다. +이는 외부로부터 들여오는 모든 소스에 대해 기본적으로 신뢰하지 않으며, 모든 기능은 Sandbox-like으로 바이너리 독립적으로 동작 가능해야 함을 의미합니다. -또는 단순히 **문서의 오타 수정**, **Docstring 작성 또는 수정**, **구체적 아이디어 제공**, **보안 소스 제공** 등과 같은 부문에 대해서 기여하실 수 있습니다. 사소한 변경이라도 "좋은 설명"을 첨부해주시길 바랍니다. 이는 저희가 여러분의 기여를 빠르게 이해하는 데 도움이 됩니다. +여러분은 **문서의 오타 수정**, **Docstring 작성 또는 수정**, **구체적 아이디어 제공**, **보안 소스 제공** 등과 같은 비교적 간단한 부문에 대해 기여하실 수 있습니다. 사소한 변경이라도 "좋은 설명"을 첨부해주시면 감사하겠습니다. 이는 저희가 여러분의 기여를 빠르게 이해하는 데 도움이 됩니다. -## 보안 취약점 보고 +## AI 에이전트 사용 -프로젝트 내에 잔존하는 **잠재적 위협**, 실제 릴리즈(또는 스냅샷)에서 발생하는 **치명적이거나 다소 복합적인 문제**, **암호 기능의 잘못된 구현**, **하드웨어적 발견** 등과 일관된 종류인 기여에 대해선 에게 연락주시길 바랍니다. 양식은 신경쓰지 않으나, 관련 부분(문제)에 대한 명확한 브레이크 포인트 및 구체적 설명(발생 경위, 근거 등)을 첨가해주세요. 원하시는 경우 기술, 정형적 문서 제공도 가능합니다. +저희는 여러분이 이 프로젝트 기여에 AI 에이전트를 사용하는 것에 아무런 불만을 가지고 있지 않습니다. [AI_SCOPE.md](AI_SCOPE.md)문서는 저희가 어떤 부분에 AI를 사용했는지 명시되어 있습니다. 이는 여러분의 기여에서 규칙으로 작용하기도 합니다. **만약 여러분이 AI를 통해 기여하신다면 이 문서에 적용된 범위(스코프)를 명시해주시길 부탁드립니다.** (필요하신 경우, `Co-authored-by`와 같은 트레일러를 커밋에 추가하실 수도 있습니다.) 해당 문서에 직접 명시하신다면, 다음 형식을 따라주시길 바랍니다. -## 검토 +``` +### <변경 크레이트명> -모든 기여는 직접 확인한 후 심각성, 규모 등을 고려하고 현재 버전에 배포하겠습니다. 그리고 원하시는 경우 기여자 목록에 추가하겠습니다(선택적으로 익명도 가능하고, 실명 또는 이메일을 게시해도 됩니다). +- <변경 내용> (<사용 에이전트 또는 모델명>) <변경자 Github 태그> + - [필요한 경우 추가 설명] +``` + +깃허브 태그가 생략된 경우는 저장소 메인테이너에 의해 작성된 것임을 인지해주시면 됩니다. + +그리고 **AI가 다음 항목에 대한 이슈 또는 PR을 작성하는 경우는 철저히 금지**시켜 주시길 바랍니다. + +- 잠재적 위협 또는 실제 발견된 보안 취약점에 대한 이슈 또는 PR +- CI 문제에 대한 이슈 또는 PR + +이 규칙으로 번거롭게 해 죄송스러운 마음입니다. --- -## 개발 환경 / Development Environment +## 개발 환경 + +[K0 마이크로커널](https://github.com/Quant-Off/iso-light-k0)에서 Ring 3(사용자 공간) 데몬 라이브러리로써, 해당 커널의 암호학적 기능 제공 목적으로 시작된 프로젝트인 점을 이해해주시길 바랍니다. -본 저장소는 ISO-LIGHT-K0 마이크로커널 위 Ring-3 사용자 공간 데몬 라이브러리입니다. -커널 친화 빌드 프로파일이 기본이므로, 호스트에서 테스트를 실행하려면 명시적인 -타깃 오버라이드가 필요합니다. +커널 친화 빌드 프로파일이 기본이므로, 호스트에서 테스트를 실행하려면 명시적인 타겟 오버라이드가 필요합니다. -### 빌드 / Build +### 빌드 -워크스페이스 기본 빌드 타깃은 **`x86_64-unknown-none`** (bare-metal) 입니다 -(`/.cargo/config.toml` 으로 고정). 따라서 워크스페이스 루트에서 `cargo build` -를 실행하면 자동으로 베어메탈 타깃으로 컴파일됩니다. +워크스페이스 기본 빌드 타겟은 **`x86_64-unknown-none`** (bare-metal) 입니다(`/.cargo/config.toml` 으로 고정). 따라서 워크스페이스 루트에서 `cargo build`를 실행하면 자동으로 베어메탈 타겟으로 컴파일됩니다. ```bash # 베어메탈 (기본) -cargo build --workspace +$ cargo build --workspace # AArch64 베어메탈 -cargo build --workspace --target aarch64-unknown-none +$ cargo build --workspace --target aarch64-unknown-none ``` -### 테스트 / Test +### 테스트 -`cargo test` 는 호스트 OS 의 표준 하네스 (std) 가 필요하므로, 호스트 타깃을 -명시적으로 지정해야 합니다. +`cargo test`는 호스트 OS의 표준 하네스(std)가 필요하므로, 호스트 타겟을 명시적으로 지정해야 합니다. ```bash # macOS Intel 호스트 -cargo test --workspace --target x86_64-apple-darwin +$ cargo test --workspace --target x86_64-apple-darwin # Apple Silicon -cargo test --workspace --target aarch64-apple-darwin +$ cargo test --workspace --target aarch64-apple-darwin # Linux x86_64 -cargo test --workspace --target x86_64-unknown-linux-gnu +$ cargo test --workspace --target x86_64-unknown-linux-gnu ``` -`elib-k0d-core` 와 `constant-time` 는 각자 `.cargo/config.toml` 에서 호스트 -타깃 오버라이드가 이미 설정되어 있어, 단일 크레이트 테스트 실행 시 `--target` -생략이 가능합니다 (예: `cargo test -p elib-k0d-core`). +`elib-k0d-core`와 `constant-time`는 각자 `.cargo/config.toml` 에서 호스트 타겟 오버라이드가 이미 설정(macOS로)되어 있어, 단일 크레이트 테스트 실행 시 `--target` 생략이 가능합니다(예: `cargo test -p elib-k0d-core`). -### 린트 / Lint +### 린트 -모든 커밋에서 다음이 무경고로 통과해야 합니다 (CLAUDE.md 헌장 §"로직 구현"): +모든 커밋에서 다음 명령이 경고 없이 통과해야 합니다. ```bash -cargo fmt --check -cargo clippy --workspace --all-targets --all-features --target x86_64-apple-darwin -- -D warnings -cargo clippy --workspace --target x86_64-unknown-none -- -D warnings +$ cargo fmt --check +$ cargo clippy --workspace --all-targets --all-features --target x86_64-apple-darwin -- -D warnings +$ cargo clippy --workspace --target x86_64-unknown-none -- -D warnings ``` -새로운 `#![allow(clippy::*)]` 추가는 원칙적으로 금지입니다. 클리피 경고는 코드를 -바꿔서 해결하세요. +새로운 `#![allow(clippy::*)]` 추가는 원칙적으로 금지됩니다. clippy 경고는 코드를 변경하여 해결해야 합니다. -### 코멘트 / Comment Convention +### 코멘트 -CLAUDE.md §"주석" 정책 요약: +저희는 CLAUDE.md와 같은 파일에 주석에 관한 지침을 다음과 같이 추가해두었습니다(요약). - **기본:** 코멘트 없음. 코드는 자기 설명적으로. -- `#[test]` 위에는 한 줄 한국어 의도 코멘트 (왜 이 테스트가 존재하는지). -- Docstring 은 사용자 요청 시에만. 형식은 `CLAUDE.md` 참조. -- 표준 (FIPS / RFC / NIST SP) 인용이나 매직 상수 주석은 영어 가능. - -### 보안 / Security - -본 저장소의 모든 변경은 다음 헌장 위에서 이루어집니다: - -- **Zero-Trust:** 외부 crate 의존성 0 개. `Cargo.toml [dependencies]` 는 - workspace path-only. -- **`alloc` 금지:** 동적 할당 없음. 모든 버퍼는 고정 크기 스택 배열. -- **`zeroize` 의무:** 비밀 데이터는 사용 후 즉시 `zeroize::Secret` 또는 - 명시적 `.zeroize()` 호출로 소거. -- **Constant-time:** 비밀 의존 분기 금지. `constant-time` 크레이트의 - `Choice` / `CtSelOps` / `CtEqOps` 사용. -- **`panic = "abort"`:** 패닉 = 데몬 종료. 모든 패닉 가능 경로는 `Result` - 로 변환. - -### 신규 크레이트 추가 / Adding a New Crate - -1. 워크스페이스 루트 `/` 디렉토리 생성, `Cargo.toml` + `src/lib.rs`. -2. `Cargo.toml [package]` 메타데이터: `version.workspace = true`, - `edition.workspace = true`, `authors.workspace = true`, - `license.workspace = true`. -3. `[dependencies]` 는 path-only `workspace = true` 만 사용 — 외부 crate - 추가는 절대 금지 (Zero-Trust 헌장). -4. 워크스페이스 루트 `Cargo.toml [workspace] members` 와 - `[workspace.dependencies]` 양쪽에 추가: +- `#[test]` 위에는 한 줄 한국어 의도 코멘트(왜 이 테스트가 존재하는지). +- Docstring은 사용자 요청 시에만. 형식은 `CLAUDE.md` 참조. +- 표준안(FIPS / RFC / SP) 인용이나 매직 상수 주석은 영어 가능. + +### 보안 + +본 저장소의 모든 변경은 다음 규칙 하에 이루어져야 합니다. + +- **제로 트러스트:** 외부 크레이트의 의존성 미사용, `Cargo.toml`의 `[dependencies]`는 워크스페이스 경로만 가능 +- **`alloc` 금지:** 동적 할당 미사용, 모든 버퍼는 고정 크기의 스택 배열 +- **`zeroize` 의무:** 비밀 데이터는 사용 후 즉시 `zeroize::Secret` 또는 명시적 `.zeroize()` 함수 호출로 소거 +- **상수-시간:** 비밀 의존 분기 금지, `constant-time` 크레이트의 `Choice` / `CtSelOps` / `CtEqOps` 등 프리미티브 사용 +- **`panic = "abort"`:** 패닉 = 데몬 종료, 모든 패닉 가능 경로는 `Result`로 변환. + +### 신규 크레이트 추가 시 + +1. 워크스페이스 루트 `/` 디렉토리 생성 후 `Cargo.toml` 및 `src/lib.rs` 등 초기 소스코드 작성 +2. `Cargo.toml`의 `[package]` 메타데이터는 `version.workspace = true`, `edition.workspace = true`, `authors.workspace = true`, `license.workspace = true`으로 설정 +3. `[dependencies]`는 `workspace = true`(경로만)를 적용하며 외부 크레이트 의존 금지 +4. 워크스페이스 루트 `Cargo.toml`에 다음과 같이 멤버 명시: ```toml [workspace.dependencies] = { path = "", version = "1.0.0" } ``` -5. 비밀 데이터를 다루면 `zeroize.workspace = true`, 상수시간 비교가 - 필요하면 `constant-time.workspace = true`. -6. `lib.rs` 첫 줄: `#![cfg_attr(not(test), no_std)]` (또는 `#![no_std]` - for foundation crates). +5. 비밀 데이터를 다루는 경우 `zeroize` 사용, 상수-시간 비교가 필요한 경우 `constant-time` 사용 +6. `lib.rs` 첫 줄은 `#![cfg_attr(not(test), no_std)]`(또는 `#![no_std]`) 명시 + +> [!IMPORTANT] +> `elib` 으로 시작하는 크레이트는 K0 커널을 위해 준비된 크레이트임을 의미합니다. + +## 치명적 문제 보고 + +프로젝트 내에 잔존하는 **잠재적 위협**, 실제 릴리즈(또는 스냅샷)에서 발생하는 **치명적이거나 다소 복합적인 문제**, **암호 기능의 잘못된 구현**, **하드웨어적 발견** 등과 일관된 종류인 기여에 대해선 에게 연락주시길 바랍니다. 양식은 신경쓰지 않으나, 관련 부분(문제)에 대한 명확한 브레이크 포인트 및 구체적 설명(발생 경위, 근거 등)을 첨가해주세요. 필요하신 경우 기술, 정형적 문서(명세) 제공도 가능하십니다. + +## 검토 + +이메일을 통해 보고해주신 문제를 포함해 이슈 및 PR은 저희가 직접 확인한 후 심각성, 규모 등을 고려하고 (수정 후) 현재 버전에 배포(병합)하겠습니다. 그리고 원하시는 경우 기여자 목록에 추가하겠습니다(선택적으로 익명도 가능하고, 실명 또는 이메일을 게시해도 됩니다). \ No newline at end of file diff --git a/CONTRIBUTING_EN.md b/CONTRIBUTING_EN.md index 0ad214b..10d168d 100644 --- a/CONTRIBUTING_EN.md +++ b/CONTRIBUTING_EN.md @@ -2,21 +2,134 @@ [![Language](https://img.shields.io/badge/CONTRIBUTING-Korean_Ver-blue?style=for-the-badge)](CONTRIBUTING.md) -*First and foremost, we would like to express our gratitude to all contributors. Your contributions are a tremendous help to us.* +*First of all, thank you to everyone contributing. Your contributions are a huge help.* -Before you contribute, there are a few important guidelines we would like to bring to your attention. While it may be inconvenient, we strictly enforce these rules to maintain the highest level of security. As you may know, we primarily adhere to the following two security principles: +We're not asking for a comprehensive audit. Since each module is an independent crate, it's more than enough to pick a single algorithm you're interested in and focus on one of the following. + +- Whether a specific implementation exactly matches the reference specification (FIPS/RFC/related papers) +- Whether a branch or array indexing depends on secret data and creates a timing difference +- Whether a `zeroize` call actually wipes memory (properly) before it goes out of scope, and isn't eliminated by compiler optimization +- Whether the API prevents unsafe usage (e.g., nonce reuse) at compile time or runtime + +Please file any findings as an issue. Even a "this part looks suspicious" observation that needs no code fix is a sufficient contribution. + +Before you contribute, we'd like to flag a few things. As you may know, we broadly follow two security principles. - Zero-Trust -- Air-Gapped Operation +- Air-Gapped operation + +This means we do not trust any source brought in from outside by default, and every feature must be able to operate Sandbox-like, independent of any binary. + +You're welcome to contribute to relatively simple areas such as **fixing typos in documentation**, **writing or revising docstrings**, **providing concrete ideas**, and **providing security sources**. Even for a trivial change, we'd appreciate a "good explanation" attached, since it helps us understand your contribution quickly. + +## Using AI Agents + +We have no objection to your using AI agents to contribute to this project. The [AI_SCOPE.md](AI_SCOPE.md) document specifies which parts we've used AI for, and it also serves as a rule for your contributions. **If you contribute through AI, please specify the scope applied, per this document.** (If you'd like, you may also add a trailer such as `Co-authored-by` to your commit.) If you specify it directly in that document, please follow this format. + +``` +### + +- () + - [Additional explanation if needed] +``` + +If the GitHub handle is omitted, you can take it to mean the change was made by a repository maintainer. + +And please **strictly refrain from having AI write issues or PRs** for the following. + +- Issues or PRs about potential threats, or about security vulnerabilities that were actually discovered +- Issues or PRs about CI problems + +Sorry for the inconvenience this rule causes. + +--- + +## Development Environment + +Please keep in mind that this project started as a Ring 3 (user space) daemon library on the [K0 microkernel](https://github.com/Quant-Off/iso-light-k0), for the purpose of providing that kernel's cryptographic functionality. + +Since the kernel-friendly build profile is the default, running tests on the host requires an explicit target override. + +### Build + +The workspace's default build target is **`x86_64-unknown-none`** (bare-metal), fixed via `/.cargo/config.toml`. So running `cargo build` at the workspace root automatically compiles for the bare-metal target. + +```bash +# Bare-metal (default) +$ cargo build --workspace + +# AArch64 bare-metal +$ cargo build --workspace --target aarch64-unknown-none +``` + +### Testing + +`cargo test` requires the host OS's standard harness (std), so you must explicitly specify the host target. + +```bash +# macOS Intel host +$ cargo test --workspace --target x86_64-apple-darwin + +# Apple Silicon +$ cargo test --workspace --target aarch64-apple-darwin + +# Linux x86_64 +$ cargo test --workspace --target x86_64-unknown-linux-gnu +``` + +`elib-k0d-core` and `constant-time` each already have a host target override set (to macOS) in their own `.cargo/config.toml`, so `--target` can be omitted when running a single crate's tests (e.g., `cargo test -p elib-k0d-core`). + +### Lint + +The following commands must pass with no warnings on every commit. + +```bash +$ cargo fmt --check +$ cargo clippy --workspace --all-targets --all-features --target x86_64-apple-darwin -- -D warnings +$ cargo clippy --workspace --target x86_64-unknown-none -- -D warnings +``` + +Adding a new `#![allow(clippy::*)]` is prohibited in principle. Clippy warnings must be resolved by changing the code. + +### Comments + +We've added guidance on comments to files like CLAUDE.md, summarized as follows. + +- **Default:** No comments. Code should be self-explanatory. +- Above `#[test]`: a one-line Korean intent comment (why this test exists). +- Docstrings only when the user requests them. See `CLAUDE.md` for the format. +- Comments quoting a standard (FIPS / RFC / SP), or annotating a magic constant, may be in English. + +### Security + +Every change to this repository must follow these rules. + +- **Zero-Trust:** No dependency on external crates; `[dependencies]` in `Cargo.toml` may only use workspace paths +- **No `alloc`:** No dynamic allocation; every buffer is a fixed-size stack array +- **`zeroize` is mandatory:** Secret data must be wiped immediately after use, via `zeroize::Secret` or an explicit `.zeroize()` call +- **Constant-time:** No branching on secret-dependent values; use the `constant-time` crate's `Choice` / `CtSelOps` / `CtEqOps` and other primitives +- **`panic = "abort"`:** A panic means the daemon terminates, so every panic-capable path must be converted into a `Result`. + +### When Adding a New Crate -This means that we fundamentally do not trust any external sources by default, and all features must be capable of operating independently as binaries in a sandbox-like environment. Adhering to these principles is crucial if you wish to add specialized source code to our project. +1. Create the `/` directory at the workspace root, then write the initial source, including `Cargo.toml` and `src/lib.rs`. +2. Set the `[package]` metadata in `Cargo.toml` to `version.workspace = true`, `edition.workspace = true`, `authors.workspace = true`, `license.workspace = true`. +3. Apply `workspace = true` (path-only) to `[dependencies]`; no external crate dependencies allowed. +4. Declare the member in the workspace root `Cargo.toml` as follows: + ```toml + [workspace.dependencies] + = { path = "", version = "1.0.0" } + ``` +5. Use `zeroize` when handling secret data, and `constant-time` when constant-time comparison is needed. +6. The first line of `lib.rs` must declare `#![cfg_attr(not(test), no_std)]` (or `#![no_std]`). -Alternatively, you are more than welcome to contribute in areas such as **fixing typos in the documentation**, **writing or correcting docstrings**, **providing concrete ideas**, or **supplying secure source code**. Even for minor changes, please include a "good explanation." This greatly helps us quickly understand the intent of your contribution. +> [!IMPORTANT] +> A crate whose name begins with `elib` is a crate prepared for the K0 kernel. -## Reporting Security Vulnerabilities +## Reporting Critical Issues -For contributions related to **potential threats** remaining in the project, **critical or somewhat complex issues** occurring in actual releases (or snapshots), **incorrect implementations of cryptographic features**, **hardware-related discoveries**, and similar findings, please contact us at . There is no required format, but please provide a clear breakpoint regarding the relevant section (the issue) and a specific explanation (how it occurs, evidence, etc.). If you prefer, you may also provide technical or formal documentation. +For contributions of a kind consistent with **potential threats** remaining in the project, **critical or somewhat complex issues** arising in an actual release (or snapshot), **incorrect implementations of cryptographic functionality**, **hardware-level findings**, and the like, please contact . We don't mind the format, but please include a clear break point for the relevant issue and a concrete explanation (how it occurred, its basis, etc.). If needed, you're also welcome to provide technical or formal documentation (specifications). -## Review Process +## Review -We will personally review all contributions and deploy them to the current version, taking into account their severity and scope. If you wish, we will also add you to the contributors list (you may opt to remain anonymous, or choose to publish your real name or email address). \ No newline at end of file +For issues and PRs, including problems reported by email, we'll review them ourselves, weigh severity and scope, and (after a fix) deploy (merge) them into the current version. If you'd like, we'll also add you to the contributor list (you may optionally stay anonymous, or publish your real name or email). diff --git a/INTRODUCTION.md b/INTRODUCTION.md index c50b237..6619dea 100644 --- a/INTRODUCTION.md +++ b/INTRODUCTION.md @@ -1,30 +1,53 @@ -# 이 프로젝트 +# 이 프로젝트는 무엇인가요? [![Language](https://img.shields.io/badge/INTRODUCTION-English_Ver-blue?style=for-the-badge)](INTRODUCTION_EN.md) -이전에 개발된 암호모듈 [얽힘 라이브러리 네이티브](https://github.com/Quant-Off/entlib-native)는 아키텍처 타겟팅이 모호했고, 각 암호 알고리즘 내에서 구현되지 않은 시스템이 있었었으며, HSM 하드웨어 연결 가능성에 대한 대비책이 적었고 Java와 연결을 고려하여 FFI(Foreign Function Interface)를 제공했습니다. 예를 들어, AES-256-GCM은 SHA-NI/AES-NI을 지원하지 않고, Java 측에서 FFM(Foreign Function & Memory) API를 통해 호출되는 방식이므로 호출자와 피호출자를 나눠 데이터에 대한 메모리 할당자를 차별했습니다. +> [!TIP] +> 프로젝트명 ELIB-K0-NT에서 "ELIB"은 [entlib-native](https://github.com/Quant-Off/entlib-native) 프로젝트의 줄임 표현을 의미합니다. 해당 프로젝트와는 무관합니다. -궁극적으로 Java 기반의 폐쇄형 인프라 관리 애플리케이션은 통신 구현에 있어 약간의 편리함을 느꼈지만, 몇 가지 보안에 관한 제약은 찝찝한 상태였습니다. Java는 플랫폼 독립성이 막강하여 범용 환경에서는 더할 나위 없이 훌륭한 선택지이지만, JVM(Java Virtual Machine) 런타임 자체가 고보안 마이크로커널 환경에는 어울리지 않습니다. 결정적으로, JVM의 가비지 컬렉터(Garbage Collector, GC)는 메모리 해제 시점을 자체적으로 결정하기 때문에 키 물질(key material)과 같은 민감한 데이터가 스코프를 벗어난 이후에도 힙 메모리에 잔류할 수 있습니다. 이는 메모리 덤프(memory dump) 공격에 고스란히 노출되는 결함이며, 고보안 환경에서는 용납되기 어렵습니다. +이 프로젝트(ELIB-K0-NT)는 [K0(ISO-LIGHT-K0) 마이크로커널](https://github.com/Quant-Off/iso-light-k0)에서 **암호 알고리즘 기능 제공을 위한 목적**으로 만들어졌습니다. K0는 간단히 고보안 엣지 게이트웨이, 항공 및 군수 임베디드 단말, 폐쇄망 데이터 다이오드를 타겟으로 하는 초경량 `no_std` 보안 마이크로커널입니다. 따라서 일반 암호 모듈과는 다음과 같은 차별점을 가집니다. -결국 Java와의 연결 고리를 완전히 끊기로 했습니다. FFI 바운더리도, JVM도, `std` 런타임도 없이, 격리형 초경량 마이크로커널 K0(Isolation Lightweight Microkernel K0, ISO-LIGHT-K0)를 위한 전용 암호 모듈을 100% Rust로 새롭게 설계했습니다. 이 모듈은 `no_std` 기반의 Ring 3 사용자 공간 데몬(daemon)으로 구동되며, IPC를 통해 커널과 통신합니다. 이것이 `elib-k0-nt`입니다. +- Ring 3(사용자 공간) 데몬(daemon)과 IPC(Inter-Process Communication)를 통한 통신 (K0 커널 특유의 아키텍처 제약) +- `no_std` 환경에서의 경량성 -# 설계 지향점 및 철학 +# 이 프로젝트는 새로운 암호 알고리즘이 아닙니다 -이 프로젝트에서 저희는 개량된 [얽힘 라이브러리 네이티브](https://github.com/Quant-Off/entlib-native) CLI 바이너리를 마이크로커널의 Ring 3 사용자 공간(user space) 서비스로 분리하고 IPC로 통신하는 설계를 지향합니다. 이는 마이크로커널의 핵심 철학(권한 분리와 장애 격리)에 부합하기 때문입니다. 이러한 설계는 암호화 모듈에서 패닉이나 결함이 발생하더라도 Ring 0 커널 공간(kernel space) / EL1 은 안전하게 보호되며, 높은 보안 등급의 기본 요건(규격)인 '보안 기능의 물리적/논리적 격리'를 충족할 수 있습니다. +이 사실은 중요합니다. ELIB-K0-NT는 새로운 암호학적 설계(scheme)를 제안하지 않습니다. AES, ChaCha20, SHA-2/3, BLAKE, Ed25519/448, X25519/448, ML-KEM, ML-DSA 등은 모두 기존 표준(FIPS, RFC 등)을 따르는 **구현체**이며, 이 프로젝트가 하는 일은 이들을 K0 커널의 제약 조건(`no_std`, Ring 3 격리, IPC 기반 통신) 위에서 다시 구현하는 것입니다. 알고리즘 자체의 안전성은 이미 검증된 표준에 기대고 있으며, 이 프로젝트가 새로 검증받아야 하는 부분은 기술적으로 오직 **알고리즘 구현의 정확성과 사이드 채널(side-channel) 저항성**입니다. -그리고 여전히 Zero-Trust, Air-Gapped Ready 원칙을 절대적으로 준수합니다. 개발의 측면에서 "안전함"을 위해선 편의를 다소 버려야 할 때가 있다고 생각됩니다. 각 암호 알고리즘을 구현하는 데 있어 중간 협의점(예를 들어, `core`, `base` 라는 이름을 가진 코어 의존성)에서 편리한 도구를 제공할 수도 있습니다. 중복도가 상대적으로 높은 `trait`의 구현은 이 코어 크레이트에서 끌어다 사용하는 방식의 설계가 적절할 예입니다. 물론, 이런 방식은 훌륭합니다. 오히려 권장되는 설계라고 할 수 있겠지만 인증 및 규정 사항(컴플라이언스)이나 개별 단위 크레이트에서 감수해야 할 중요한 원칙을 생각하면 다소 공격적인 프로그래밍 방식이라고 생각되기도 합니다. 우리는 이를 통틀어 단순하게 "편리함"이라고 말합니다. 이를 버리고 각 크레이트가 독립적으로 구현되도록 설계하고 있습니다. +이 구분이 중요한 이유는, 검토자 여러분에게 요구하는것이 "이 새로운 방식이 안전한가"가 아니라 "이 표준 구현에 문제가 있는가"이기 때문입니다. 전자는 암호학 연구이고, 후자는 소프트웨어 엔지니어링입니다. 이 프로젝트가 필요로 하는 건 후자입니다. -그렇다면 "의존성"이 필수적으로 필요한 경우는 어떨까요? 예를 들어, `Ed448` 서명 알고리즘에서 컨텍스트(context) 서명을 구현하기 위해 SHAKE 해시 알고리즘을 사용하는 경우입니다. 컴플라이언스 차원에서 이러한 사용은 일반적입니다. "편리함"을 버리기 위해 이 구현은 "폐쇄 및 방어적 프로그래밍"으로 성립되어야 합니다. 즉, Ed448 전자 서명 알고리즘을 구현하기 위해선 [FIPS 202](https://csrc.nist.gov/pubs/fips/202/final)에 따른 `SHAKE128` 및 `SHAKE256` 알고리즘 구현이 필수적인 것입니다. 이 프로젝트 내에서 체계적으로 사용되는 `zeroize`도 동일합니다. [RustCrypto](https://github.com/RustCrypto)는 소거를 위한 완벽한 크레이트를 제공하지만 우리는 이를 곧이곧대로 사용하지 않습니다. 그리고 순수히 '저희만의 방법'으로 구현합니다. (RustCrypto는 엄청난 Rust 보안 팀입니다!!!!) +# 개발 방식과 현재 검증 상태 -저희 팀이 말하고자 하는 것은 '저희만의 방법'입니다. 설계에 앞서 '이 기능이 시중에 존재하는가?'에 대해 의문을 가지지 않습니다. 이러한 바보같아 보이는 철학은 저희 팀의 코드 설계의 주력으로 작용하고, 정보보안 관점에선 꽤나 유용할 수 있다는 것을 말씀드리고 싶습니다. 완전히 독립된 개별 단위의 크레이트 코드베이스에서 발생한 문제나 몇 데이터 흐름 자체가 외부로 흐르지 않은 것이 가장 커 보입니다. +이 프로젝트의 많은 부분은 AI 에이전트를 통해 작성되었습니다(이 문서는 아닙니다). 구체적인 사용 범위와 프롬프트는 [AI_SCOPE.md](AI_SCOPE.md)에 기록되어 있습니다. AI를 사용했다는 사실 자체는 코드 품질에 대한 보증도, 결함의 증거도 아닙니다. 다만 **사람에 의한 암호학적 검증이 아직 이루어지지 않은 부분이 많다**는 뜻이며, 이 문서는 그 공백을 숨기지 않고 명시하는 것을 목표로 합니다. -그럼에도 불구하고, 여러분이 여전히 저희의 설계 방식에 대해 정말 멍청해 보인다고 하시면... 딱히 저희는 반박할 수 없습니다. 다만 저희는 개개인의 코드 설계 철학을 존중합니다. 설령 그것이 "1000% Spaghetti-Code"라고 해도요. +현재까지 확인된 것과 확인되지 않은 것은 다음과 같습니다(모듈별 상세는 각 크레이트의 README.md 문서를 참고해주세요). -# 포함된 기능 +| 검증 항목 | 상태 | +|-----------------------------------------------|-----------------| +| 표준 테스트 벡터(NIST/KCMVP KAT, RFC test vector) 통과 | 진행 중, 일부 모듈만 완료 | +| 상수-시간(constant-time) 검증 (dudect 등 도구 기반) | 미완료 | +| 사이드 채널 저항성 (타이밍, 캐시 등) | 미검토 | +| 메모리 소거(zeroize) 정확성 | 미검토 | +| 퍼징(fuzzing) 커버리지 | 미완료 | +| API 설계의 오용 저항성(misuse-resistance) | 미검토 | + +이 표는 진행 상황에 따라 갱신됩니다. "미검토"로 표시된 항목은 현재 이 프로젝트가 프로덕션에 적합하다고 주장하지 않는 근거이기도 합니다. + +# 설계 지향점 + +이 프로젝트는 CLI 바이너리를 마이크로커널의 Ring 3 사용자 공간 서비스로 분리하고 IPC로 통신하는 구조를 취합니다. 이는 마이크로커널의 핵심 철학인 권한 분리와 장애 격리에 부합하기 때문입니다. 암호화 모듈에서 패닉이나 결함이 발생하더라도 Ring 0(커널 공간; EL1)은 영향을 받지 않으며, 이는 높은 보안 등급에서 요구하는 '보안 기능의 물리적/논리적 격리'를 만족시키기 위한 요구사항입니다. + +## 크레이트 간 공유 의존성을 두지 않는 이유 -이 바이너리엔 여러 아키텍처 플랫폼을 전적으로 커버하는 소거 기능이 탑재되어 있습니다. 거의 모든 암호학적(또는 정보보안) 기능에 내에서는 임시 생성된 민감 데이터를 (스코프를 벗어나는) 즉시 소거하는 로직과 휘발성 연산이 포함되며, 이 기능은 `zeroize` 크레이트에서 제공됩니다. [이 곳](zeroize/README.md)에서 소거에 관한 몇 가지 메커니즘을 찾아보실 수 있습니다. +일반적인 Rust 프로젝트라면 여러 크레이트에서 반복되는 트레이트(trait) 구현을 `core`나 `base` 같은 공유 크레이트로 추출하는 것이 합리적인 설계입니다. 하지만 이 프로젝트는 그렇게 하지 않습니다. [CONTRIBUTING.md](CONTRIBUTING.md)에 명시된 **제로 트러스트 원칙**에 따라 각 크레이트의 `[dependencies]`는 워크스페이스 경로 참조만 허용되며, 이는 편의성보다 장애/결함의 전파 범위를 크레이트 단위로 제한하는 것을 우선하기 때문입니다. 공유 코어에 결함이 있으면 이를 사용하는 모든 크레이트가 동시에 영향을 받지만, 독립 구현에서는 결함이 해당 크레이트에 국한됩니다. 코드 중복이라는 비용을 감수하는 대신 얻는 트레이드-오프(trade-off)입니다. -강력한 소거 로직을 기반으로 다음의 암호학적 기능을 제공합니다. +이 원칙에도 예외는 있습니다. 예를 들어, Ed448의 컨텍스트 서명은 FIPS 202에 정의된 SHAKE128/256을 요구하므로, `ed448` 크레이트는 `sha3` 크레이트를 워크스페이스 내부 의존성으로 사용합니다. 이는 사양이 요구하는 의존성이지, 편의를 위한 것이 아닙니다. + +## RustCrypto의 zeroize 대신 자체 구현을 사용하는 이유 + +`zeroize`는 [RustCrypto](https://github.com/RustCrypto/)가 제공하는 검증되었으며 안정적인 크레이트입니다. ELIB-K0-NT가 이를 사용하지 않는 이유는 품질 문제가 아니라 위에서 설명한 제로 트러스트 원칙(외부 크레이트 의존 금지) 때문입니다. 다만 이는 곧 이 프로젝트의 소거 로직이 RustCrypto만큼의 검증을 거치지 않았다는 뜻이기도 합니다. `zeroize` 구현은 이 프로젝트에서 가장 미묘한 버그(컴파일러 최적화에 의한 소거 코드 제거, `volatile` 쓰기의 정확성 등)가 발생하기 쉬운 지점 중 하나이며, 위 [검증 상태 표](#개발-방식과-현재-검증-상태)에서 "미검토"로 표시된 이유이기도 합니다. **이 부분에 대한 검토를 특히 환영**합니다. + +# 포함된 기능 - [Constant-time Ops](./constant-time) - Hash([SHA2](./sha2), [SHA3](./sha3), [SHAKE](./sha3), [BLAKE2](./blake)) diff --git a/INTRODUCTION_EN.md b/INTRODUCTION_EN.md index e33760b..83787a3 100644 --- a/INTRODUCTION_EN.md +++ b/INTRODUCTION_EN.md @@ -1,30 +1,53 @@ -# This Project +# What Is This Project? [![Language](https://img.shields.io/badge/INTRODUCTION-Korean_Ver-blue?style=for-the-badge)](INTRODUCTION.md) -The previously developed cryptographic module, [Entlib-Native](https://github.com/Quant-Off/entlib-native), had ambiguous architecture targeting, contained unimplemented systems within certain cryptographic algorithms, lacked sufficient preparation for HSM hardware connectivity, and provided an FFI (Foreign Function Interface) to accommodate Java integration. For example, AES-256-GCM did not support SHA-NI/AES-NI, and since it was invoked from the Java side via the FFM (Foreign Function & Memory) API, the caller and callee were separated — resulting in differentiated memory allocators for the data. +> [!TIP] +> In the project name ELIB-K0-NT, "ELIB" is a shorthand for the [entlib-native](https://github.com/Quant-Off/entlib-native) project. It is unrelated to that project. -Ultimately, the Java-based closed-infrastructure management application found a degree of convenience in its communication implementation, but several security constraints remained unsettling. Java's platform independence makes it an excellent choice in general-purpose environments, but the JVM (Java Virtual Machine) runtime itself is ill-suited for high-security microkernel environments. Most critically, the JVM's Garbage Collector (GC) determines memory deallocation timing on its own, meaning sensitive data such as key material can persist in heap memory even after leaving its scope. This is a clear vulnerability that exposes the system to memory dump attacks, and is difficult to tolerate in high-security environments. +This project (ELIB-K0-NT) was created **for the purpose of providing cryptographic algorithm functionality** on the [K0 (ISO-LIGHT-K0) microkernel](https://github.com/Quant-Off/iso-light-k0). In short, K0 is an ultra-lightweight `no_std` security microkernel targeting high-security edge gateways, aviation and defense embedded terminals, and air-gapped data diodes. Accordingly, it differs from a general cryptographic module in the following ways. -In the end, we decided to sever all ties with Java. Without FFI boundaries, JVM, or the `std` runtime, we designed a dedicated cryptographic module for the Isolation Lightweight Microkernel K0 (ISO-LIGHT-K0) from the ground up in 100% Rust. This module runs as a `no_std`-based Ring 3 user space daemon, communicating with the kernel via IPC. This is `elib-k0-nt`. +- Communication via a Ring 3 (user space) daemon and IPC (Inter-Process Communication) (an architectural constraint unique to the K0 kernel) +- Lightweightness in a `no_std` environment -# Design Goals and Philosophy +# This Project Is Not a New Cryptographic Algorithm -In this project, we aim for a design that decouples the enhanced [Entlib-Native](https://github.com/Quant-Off/entlib-native) CLI binary into a Microkernel Ring 3 user space service, communicating via IPC. This aligns with the core philosophy of microkernels: privilege separation and fault isolation. Such a design ensures that even if a panic or fault occurs within the cryptographic module, the Ring 0 kernel space / EL1 remains protected. Furthermore, it satisfies the "physical/logical isolation of security functions," a fundamental requirement for high-security standards. +This fact matters. ELIB-K0-NT does not propose a new cryptographic scheme. AES, ChaCha20, SHA-2/3, BLAKE, Ed25519/448, X25519/448, ML-KEM, and ML-DSA are all **implementations** that follow existing standards (FIPS, RFC, etc.), and what this project does is re-implement them under the constraints of the K0 kernel (`no_std`, Ring 3 isolation, IPC-based communication). The safety of the algorithms themselves rests on already-verified standards, and the only thing this project technically needs to have newly verified is the **correctness of the algorithm implementations and their resistance to side-channel attacks**. -We strictly adhere to the principles of Zero-Trust and Air-Gapped Ready. From a development perspective, we believe that "convenience" must sometimes be sacrificed for "security." When implementing cryptographic algorithms, one might provide convenient tools through a middle ground (e.g., core dependencies named `core` or `base`). Implementing highly redundant `traits` by pulling them from such a core crate is a typical example of this. While this approach is excellent and often recommended, we view it as a somewhat aggressive programming style when considering compliance requirements or the critical principles each individual unit crate must uphold. We simply refer to this as "convenience." We choose to discard this convenience, designing each crate to be implemented independently. +This distinction matters because what we're asking of reviewers is not "is this new approach secure?" but "does this standard implementation have a problem?" The former is cryptographic research; the latter is software engineering. What this project needs is the latter. -What if a "dependency" is absolutely necessary? For instance, using the SHAKE hash algorithm to implement context signatures in the `Ed448` signature algorithm. Such usage is common for compliance. To discard "convenience," this implementation must be established through "closed and defensive programming." In other words, to implement the Ed448 digital signature algorithm, the implementation of `SHAKE128` and `SHAKE256` according to [FIPS 202](https://csrc.nist.gov/pubs/fips/202/final) becomes mandatory within that scope. The same applies to `zeroize`, which is used systematically throughout this project. While [RustCrypto](https://github.com/RustCrypto) provides an excellent crate for zeroing memory, we do not use it as-is. Instead, we implement it purely in "our own way." (RustCrypto is an amazing Rust security team!!!!) +# Development Approach and Current Verification Status -What our team wants to emphasize is "our own way." Before designing, we do not ask, "Does this feature already exist in the market?" This seemingly foolish philosophy serves as the backbone of our code design. From an information security perspective, this can be quite useful. The most significant advantage is that issues or data flows within a completely independent unit crate's codebase do not leak externally. +Much of this project was written using AI agents. The specific scope of use and the prompts are recorded in [AI_SCOPE.md](AI_SCOPE.md). The mere fact that AI was used is neither a guarantee of code quality nor evidence of a defect. It does mean, however, that **there are still many parts that have not yet undergone human cryptographic verification**, and this document aims to state that gap explicitly rather than hide it. -Nevertheless, if you still think our design approach looks truly stupid... we have no particular rebuttal. We simply respect individual code design philosophies—even if it's "1000% Spaghetti-Code." +What has and has not been confirmed so far is as follows (for per-module detail, please refer to each crate's README.md). -# Features Included +| Verification Item | Status | +|------------------------------------------------------------------|-----------------------------------------| +| Passing standard test vectors (NIST/KCMVP KAT, RFC test vectors) | In progress, only some modules complete | +| Constant-time verification (tool-based, e.g. dudect) | Not complete | +| Side-channel resistance (timing, cache, etc.) | Not reviewed | +| Memory zeroization (zeroize) correctness | Not reviewed | +| Fuzzing coverage | Not complete | +| Misuse-resistance of the API design | Not reviewed | -This binary is equipped with memory-zeroing capabilities that fully cover multiple architectural platforms. Within almost all cryptographic (or information security) functions, logic and volatile operations are included to immediately erase sensitive data as soon as it leaves its scope. This functionality is provided by our internal `zeroize` implementation. You can find several mechanisms regarding erasure [here](zeroize/README.md). +This table is updated as progress is made. Items marked "Not reviewed" are also the basis for why this project does not currently claim to be production-ready. -Based on robust zeroing logic, we provide the following cryptographic features: +# Design Direction + +This project separates the CLI binary out into a Ring 3 user-space service of the microkernel and communicates with it over IPC. This is because it aligns with the microkernel's core philosophy of privilege separation and fault isolation. Even if a panic or defect occurs in the cryptographic module, Ring 0 (kernel space; EL1) is unaffected, which is a requirement for satisfying the "physical/logical isolation of security functions" demanded at high security levels. + +## Why We Avoid Shared Dependencies Between Crates + +In a typical Rust project, it would be a reasonable design to extract trait implementations that repeat across several crates into a shared crate like `core` or `base`. This project, however, does not do that. Under the **Zero-Trust principle** specified in [CONTRIBUTING.md](CONTRIBUTING.md), each crate's `[dependencies]` only permits workspace path references, because this prioritizes limiting the propagation of faults/defects to a per-crate scope over convenience. If a shared core has a defect, every crate that uses it is affected at once, whereas in independent implementations a defect stays confined to that one crate. It's a trade-off we accept, paying the cost of code duplication in exchange. + +There are exceptions to this principle. For example, Ed448's context signatures require SHAKE128/256 as defined in FIPS 202, so the `ed448` crate uses the `sha3` crate as an internal workspace dependency. This is a dependency the specification requires, not one adopted for convenience. + +## Why We Use Our Own Implementation Instead of RustCrypto's zeroize + +`zeroize` is a validated, stable crate provided by [RustCrypto](https://github.com/RustCrypto/). The reason ELIB-K0-NT does not use it is not a quality issue but the Zero-Trust principle described above (no external crate dependencies). This does, however, also mean that this project's zeroization logic has not undergone as much verification as RustCrypto's. The `zeroize` implementation is one of the spots in this project most prone to the subtlest bugs (compiler-optimization removal of zeroization code, correctness of `volatile` writes, etc.), and it's also why it's marked "Not reviewed" in the [verification status table above](#development-approach-and-current-verification-status). **We especially welcome review of this part.** + +# Included Features - [Constant-time Ops](./constant-time) - Hash([SHA2](./sha2), [SHA3](./sha3), [SHAKE](./sha3), [BLAKE2](./blake)) @@ -32,4 +55,4 @@ Based on robust zeroing logic, we provide the following cryptographic features: - Digital Signature([Ed25519](./ed25519), [Ed448](./ed448)) - Key Establishment Protocol([X25519](./x25519), [X448](./x448)) - AEAD, BlockCipher([AES](./aes), [ChaCha20-Poly1305](./chacha20)) -- Post-Quantum Cryptography([ML-DSA](./mldsa), [ML-KEM](./mlkem)) \ No newline at end of file +- Post-Quantum Cryptography([ML-DSA](./mldsa), [ML-KEM](./mlkem)) diff --git a/README.md b/README.md index e26a3ad..0098e46 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# None-Triple EntanglementLib Crypto Module +# ELIB-K0-NT [![Language](https://img.shields.io/badge/README-English_Ver-blue?style=for-the-badge)](README_EN.md) [![Qu4nt-Space-Discord](https://img.shields.io/badge/Qu4nt_Space-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/9utg4hp3m8) @@ -7,21 +7,15 @@ > 정식 CMVP(Cryptographic Module Validation Program)를 거치지 않은 암호 모듈입니다. 연구 목적으로 사용해주세요. > 1.1.0 릴리즈 버전에선 각 암호 알고리즘의 구현과 검증, 정형 문서 작성이 이루어집니다. -[Rust 기반 얽힘 라이브러리 네이티브 프로젝트](https://github.com/Quant-Off/entlib-native)는 가장 많이 사용되는 아키텍처에 대해 `std`(및 `no_std`)를 지원하며, 고보안 규격(국제적 규제 등 컴플라이언스)을 준수하는 데 초점을 맞춥니다. 이 모듈은 그런 부분에서 합리적입니다. +이 모듈은 격리형 초경량 마이크로커널 K0(Isolation Lightweight Microkernel K0, ISO-LIGHT-K0)에서 Ring 3 사용자 공간(user space) 내에 데몬(daemon) 형식으로 구동되며 TUI와 IPC 메시지를 통해 암호화 통신합니다. 데몬은 Ring 0(커널 공간) 속 IPC 엔드포인트 라우터로 데이터를 전송하는 방식으로 동작합니다. -이 모듈은 격리형 초경량 마이크로커널 K0(Isolation Lightweight Microkernel K0, ISO-LIGHT-K0)에서 Ring 3 사용자 공간(user space) 내에 데몬(daemon) 형식으로 구동되며 TUI와 IPC 메시지를 통해 암호화 통신합니다. 데몬은 Ring 0 커널 공간(kernel space) 속 IPC 엔드포인트 라우터로 데이터를 전송하는 방식으로 동작합니다. - -`entlib-native` 암호 모듈의 NT을 타게팅한 만큼 100% Rust 언어로 작성되며, 가벼워졌음에도 여전히 강한 보안성을 보입니다. - -여러분은 언제나 프로젝트를 소개한 문서인 [INTRODUCTION.md](INTRODUCTION.md) 파일을 참고할 수 있습니다. - -또한, 이 프로젝트에서 AI 에이전트가 어떤 범위에 맞춰 사용되고, 실제로 수정하는 부분과 사용되는 스킬 및 프롬프트는 무엇인지에 관해 [AI_SCOPE.md](AI_SCOPE.md) 문서에 작성했습니다. +프로젝트 소개 문서인 [INTRODUCTION.md](INTRODUCTION.md) 파일을 참고해주세요. 또한, 이 프로젝트에서 AI 에이전트가 어떤 범위에 맞춰 사용되고, 실제로 수정하는 부분과 사용되는 스킬 및 프롬프트는 무엇인지에 관해 [AI_SCOPE.md](AI_SCOPE.md) 문서에 작성했습니다. # 릴리즈 `1.1.0` -과거 `1.0.0` 릴리즈에서 미처 발견하지 못 했던 몇 가지 문제와 컴플라이언스 측면에서의 구현 오류를 바로잡았습니다. 이 작업은 세밀하고 체계적인 교차 검증 작업을 기반으로 수행되며, 그 기록은 [이 문서](cross-confirm.md)에서 확인하실 수 있습니다. 해당 문서는 `1.1.0` 릴리즈가 공개되면 제거됩니다. +과거 `1.0.0` 릴리즈에서 미처 발견하지 못 했던 몇 가지 문제와 컴플라이언스 측면에서의 구현 오류를 바로잡았습니다. 좀 더 세부적인 기록은 [이 문서](release1.1.0-confirm.md)에서 확인하실 수 있습니다. 해당 문서는 `1.1.0` 릴리즈가 공개되면 제거됩니다. -이 릴리즈에선 개별 암호 알고리즘의 개발자 - Fable 5 - 전문가의 교차 검증 작업과 정형 문서 작성을 주력으로 검토했습니다. 이 과정에서 상당수의 보안 문제를 해결했습니다. 모든 구현된 암호 알고리즘이 정상적으로 구현되어 작동하는지 확인하기 위해 KCMVP의 CAVP(Cryptographic Algorithm Validation Program) 검증을 진행합니다. +이 릴리즈에선 개별 암호 알고리즘의 문제 해결, 교차 검증과 정형 문서 작성을 주력으로 작업했습니다. 이 과정에서 상당수의 보안 문제를 해결했습니다. 모든 구현된 암호 알고리즘이 정상적으로 구현되어 작동하는지 확인하기 위해 KCMVP의 CAVP(Cryptographic Algorithm Validation Program) 검증 작업을 스케쥴에 추가했고, 모두 완료했습니다. # 라이선스 diff --git a/README_EN.md b/README_EN.md index b2c0870..d3b5379 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,28 +1,22 @@ -# None-Triple EntanglementLib Crypto Module +# ELIB-K0-NT [![Language](https://img.shields.io/badge/README-Korean_Ver-blue?style=for-the-badge)](README.md) [![Qu4nt-Space-Discord](https://img.shields.io/badge/Qu4nt_Space-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.com/invite/9utg4hp3m8) > [!IMPORTANT] -> This cryptographic module has not gone through the official CMVP (Cryptographic Module Validation Program). Please use it for research purposes. -> The `1.1.0` release covers the implementation and validation of each cryptographic algorithm, along with the writing of formal documentation. +> This cryptographic module has not gone through a formal CMVP (Cryptographic Module Validation Program). Please use it for research purposes only. +> The 1.1.0 release focuses on implementing and verifying each cryptographic algorithm, along with writing formal documentation. -The [Rust-based Entanglement Library native project](https://github.com/Quant-Off/entlib-native) supports `std` (and `no_std`) for the most widely used architectures and focuses on complying with high-security standards (such as international regulations and compliance). This module is reasonable in that regard. +This module runs as a daemon in Ring 3 user space on the Isolation Lightweight Microkernel K0 (ISO-LIGHT-K0), communicating cryptographically through a TUI and IPC messages. The daemon works by forwarding data to the IPC endpoint router inside Ring 0 (kernel space). -This module runs as a daemon in the Ring 3 user space on an Isolation Lightweight Microkernel K0 (ISO-LIGHT-K0) and communicates for encryption via TUI and IPC messages. The daemon operates by sending data to an IPC endpoint router in the Ring 0 kernel space. - -Targeting the NT of the `entlib-native` crypto module, it is written 100% in Rust, and despite being lightweight, it still offers strong security. - -You can always refer to the [INTRODUCTION_EN.md](INTRODUCTION_EN.md) file for a detailed project introduction. - -Additionally, information about what scope AI agents are used for in this project, what parts they actually modify, and the skills and prompts used is documented in [AI_SCOPE_EN.md](AI_SCOPE_EN.md). +Please refer to [INTRODUCTION.md](INTRODUCTION.md) for the project introduction. We've also documented the scope in which AI agents are used in this project, including what they actually modify and which skills and prompts are used, in [AI_SCOPE.md](AI_SCOPE.md). # Release `1.1.0` -We have corrected several issues and compliance-related implementation errors that went unnoticed in the previous `1.0.0` release. This work is performed based on meticulous and systematic cross-validation, and the records can be found in [this document (Korean)](cross-confirm.md). That document will be removed once the `1.1.0` release is published. +We've fixed a number of issues that went unnoticed in the previous `1.0.0` release, along with implementation errors on the compliance side. A more detailed record can be found in [this document](release1.1.0-confirm.md). That document will be removed once the `1.1.0` release is published. -For this release, we focused on cross-validation between the developer, Fable 5, and outside experts for each cryptographic algorithm, along with the writing of formal documentation. A significant number of security issues were resolved in that process. To confirm that every implemented cryptographic algorithm is correctly implemented and works as intended, we are proceeding with CAVP (Cryptographic Algorithm Validation Program) validation under KCMVP. +This release focused mainly on resolving issues in individual cryptographic algorithms, cross-verification, and writing formal documentation. In the process, we resolved a significant number of security issues. To confirm that every implemented cryptographic algorithm is correctly implemented and functioning, we scheduled KCMVP's CAVP (Cryptographic Algorithm Validation Program) verification work, and have now completed all of it. # License -This project is dual-licensed under either the [MIT LICENSE](LICENSE-MIT) or the [Apache License 2.0](LICENSE-APACHE), at your option. +This project is dual-licensed under [MIT LICENSE](LICENSE-MIT) or [Apache License 2.0](LICENSE-APACHE), at your option. diff --git a/cross-confirm.md b/release1.1.0-confirm.md similarity index 100% rename from cross-confirm.md rename to release1.1.0-confirm.md From 7da9873abe25d60535ade41ea1d1af2f9c9f2370 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:53:06 +0900 Subject: [PATCH 49/65] =?UTF-8?q?aes=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=EB=B3=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20KAT=20=EB=B0=B0=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20tests/common/mod.rs=20=EB=A5=BC=20core?= =?UTF-8?q?=20=EC=A0=84=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=ED=95=B4=20no=5Fstd=20=EB=B2=A0=EC=96=B4=EB=A9=94=ED=83=88?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20=ED=8F=AC=ED=95=A8=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EA=B2=8C=20=ED=95=A8=20-=20FIPS=20197=20C3?= =?UTF-8?q?=20=EC=99=80=20SP=20800-38A=20F2.5=20F5.5=20SP=20800-38D=20case?= =?UTF-8?q?=2014=2015=2016=20=EB=B2=A1=ED=84=B0=20=EC=88=98=EB=A1=9D=20-?= =?UTF-8?q?=20GCM=20=ED=83=9C=EA=B7=B8=20=ED=9B=BC=EC=86=90=20=EC=8B=9C=20?= =?UTF-8?q?AuthenticationFailed=20=EB=B0=98=ED=99=98=EA=B3=BC=20AES256=20D?= =?UTF-8?q?rop=20=ED=9B=84=20=EC=A0=84=20=EB=B0=94=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EC=86=8C=EA=B1=B0=EB=A5=BC=20readback=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20-=20=EC=A7=84=EC=9E=85=EC=A0=90=EC=9D=84?= =?UTF-8?q?=20run=5Fall=20=ED=95=98=EB=82=98=EB=A1=9C=20=EB=91=90=EA=B3=A0?= =?UTF-8?q?=20=EC=8B=A4=ED=8C=A8=20=EC=A7=80=EC=A0=90=EB=A7=88=EB=8B=A4=20?= =?UTF-8?q?=EA=B3=A0=EC=9C=A0=20=ED=83=9C=EA=B7=B8=EB=A5=BC=20=EB=B0=98?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/tests/common/mod.rs | 341 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 aes/tests/common/mod.rs diff --git a/aes/tests/common/mod.rs b/aes/tests/common/mod.rs new file mode 100644 index 0000000..91c0b63 --- /dev/null +++ b/aes/tests/common/mod.rs @@ -0,0 +1,341 @@ +use aes::{ + AES256, AES256CBC, AES256CTR, AES256GCM, BLOCK_SIZE, CBC_IV_SIZE, CTR_IV_SIZE, Error, + GCM_NONCE_SIZE, GCM_TAG_SIZE, KEY_SIZE, +}; +use core::mem::MaybeUninit; + +const GCM16_KEY: [u8; KEY_SIZE] = [ + 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, +]; + +const GCM16_NONCE: [u8; GCM_NONCE_SIZE] = [ + 0xca, 0xfe, 0xba, 0xbe, 0xfa, 0xce, 0xdb, 0xad, 0xde, 0xca, 0xf8, 0x88, +]; + +const GCM16_PLAINTEXT: [u8; 64] = [ + 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, 0x39, 0x1a, 0xaf, 0xd2, 0x55, +]; + +const GCM16_CIPHERTEXT: [u8; 64] = [ + 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, 0x62, 0x89, 0x80, 0x15, 0xad, +]; + +const GCM16_TAG: [u8; GCM_TAG_SIZE] = [ + 0xb0, 0x94, 0xda, 0xc5, 0xd9, 0x34, 0x71, 0xbd, 0xec, 0x1a, 0x50, 0x22, 0x70, 0xe3, 0xcc, 0x6c, +]; + +pub fn run_all() -> Result<(), &'static str> { + fips197_c3()?; + cbc_sp800_38a_f2_5()?; + ctr_sp800_38a_f5_5()?; + gcm_case_14()?; + gcm_case_15()?; + gcm_case_16()?; + gcm_auth_failure()?; + aes256_drop_readback()?; + Ok(()) +} + +fn fips197_c3() -> Result<(), &'static str> { + let key: [u8; KEY_SIZE] = [ + 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 plaintext: [u8; BLOCK_SIZE] = [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, + 0xff, + ]; + let expected_ciphertext: [u8; BLOCK_SIZE] = [ + 0x8e, 0xa2, 0xb7, 0xca, 0x51, 0x67, 0x45, 0xbf, 0xea, 0xfc, 0x49, 0x90, 0x4b, 0x49, 0x60, + 0x89, + ]; + + let cipher = AES256::new(&key); + + let ciphertext = cipher.encrypt(&plaintext); + if ciphertext != expected_ciphertext { + return Err("fips197_c3_encrypt"); + } + + let decrypted = cipher.decrypt(&ciphertext); + if decrypted != plaintext { + return Err("fips197_c3_decrypt"); + } + + Ok(()) +} + +fn cbc_sp800_38a_f2_5() -> Result<(), &'static str> { + let key: [u8; KEY_SIZE] = [ + 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 iv: [u8; CBC_IV_SIZE] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, + ]; + let plaintext: [u8; 64] = [ + 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, 0xef, 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, + 0xe6, 0x6c, 0x37, 0x10, + ]; + let expected_ciphertext: [u8; 64] = [ + 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, 0xb2, 0xeb, 0x05, 0xe2, 0xc3, 0x9b, 0xe9, 0xfc, 0xda, 0x6c, 0x19, 0x07, + 0x8c, 0x6a, 0x9d, 0x1b, + ]; + + let cbc = AES256CBC::new(&key); + + let mut ciphertext = [0u8; 64]; + if cbc.encrypt(&iv, &plaintext, &mut ciphertext).is_err() { + return Err("cbc_f2_5_encrypt_call"); + } + if ciphertext != expected_ciphertext { + return Err("cbc_f2_5_ciphertext"); + } + + let mut decrypted = [0u8; 64]; + if cbc.decrypt(&iv, &ciphertext, &mut decrypted).is_err() { + return Err("cbc_f2_5_decrypt_call"); + } + if decrypted != plaintext { + return Err("cbc_f2_5_plaintext"); + } + + Ok(()) +} + +fn ctr_sp800_38a_f5_5() -> Result<(), &'static str> { + let key: [u8; KEY_SIZE] = [ + 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 iv: [u8; CTR_IV_SIZE] = [ + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, + 0xff, + ]; + let plaintext: [u8; 64] = [ + 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, 0xef, 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, + 0xe6, 0x6c, 0x37, 0x10, + ]; + let expected_ciphertext: [u8; 64] = [ + 0x60, 0x1e, 0xc3, 0x13, 0x77, 0x57, 0x89, 0xa5, 0xb7, 0xa7, 0xf5, 0x04, 0xbb, 0xf3, 0xd2, + 0x28, 0xf4, 0x43, 0xe3, 0xca, 0x4d, 0x62, 0xb5, 0x9a, 0xca, 0x84, 0xe9, 0x90, 0xca, 0xca, + 0xf5, 0xc5, 0x2b, 0x09, 0x30, 0xda, 0xa2, 0x3d, 0xe9, 0x4c, 0xe8, 0x70, 0x17, 0xba, 0x2d, + 0x84, 0x98, 0x8d, 0xdf, 0xc9, 0xc5, 0x8d, 0xb6, 0x7a, 0xad, 0xa6, 0x13, 0xc2, 0xdd, 0x08, + 0x45, 0x79, 0x41, 0xa6, + ]; + + let ctr = AES256CTR::new(&key); + + let mut ciphertext = [0u8; 64]; + if ctr.apply_iv(&iv, &plaintext, &mut ciphertext).is_err() { + return Err("ctr_f5_5_encrypt_call"); + } + if ciphertext != expected_ciphertext { + return Err("ctr_f5_5_ciphertext"); + } + + let mut decrypted = [0u8; 64]; + if ctr.apply_iv(&iv, &ciphertext, &mut decrypted).is_err() { + return Err("ctr_f5_5_decrypt_call"); + } + if decrypted != plaintext { + return Err("ctr_f5_5_plaintext"); + } + + Ok(()) +} + +fn gcm_case_14() -> Result<(), &'static str> { + let key = [0u8; KEY_SIZE]; + let nonce = [0u8; GCM_NONCE_SIZE]; + let plaintext: [u8; 0] = []; + let aad: [u8; 0] = []; + let expected_tag: [u8; GCM_TAG_SIZE] = [ + 0x53, 0x0f, 0x8a, 0xfb, 0xc7, 0x45, 0x36, 0xb9, 0xa9, 0x63, 0xb4, 0xf1, 0xc4, 0xcb, 0x73, + 0x8b, + ]; + + let gcm = AES256GCM::new(&key); + + let mut ciphertext = [0u8; 0]; + let mut tag = [0u8; GCM_TAG_SIZE]; + if gcm + .encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) + .is_err() + { + return Err("gcm_case_14_encrypt_call"); + } + if tag != expected_tag { + return Err("gcm_case_14_tag"); + } + + let mut decrypted = [0u8; 0]; + if gcm + .decrypt(&nonce, &aad, &ciphertext, &tag, &mut decrypted) + .is_err() + { + return Err("gcm_case_14_decrypt_call"); + } + + Ok(()) +} + +fn gcm_case_15() -> Result<(), &'static str> { + let key = [0u8; KEY_SIZE]; + let nonce = [0u8; GCM_NONCE_SIZE]; + let plaintext = [0u8; BLOCK_SIZE]; + let aad: [u8; 0] = []; + let expected_ciphertext: [u8; BLOCK_SIZE] = [ + 0xce, 0xa7, 0x40, 0x3d, 0x4d, 0x60, 0x6b, 0x6e, 0x07, 0x4e, 0xc5, 0xd3, 0xba, 0xf3, 0x9d, + 0x18, + ]; + let expected_tag: [u8; GCM_TAG_SIZE] = [ + 0xd0, 0xd1, 0xc8, 0xa7, 0x99, 0x99, 0x6b, 0xf0, 0x26, 0x5b, 0x98, 0xb5, 0xd4, 0x8a, 0xb9, + 0x19, + ]; + + let gcm = AES256GCM::new(&key); + + let mut ciphertext = [0u8; BLOCK_SIZE]; + let mut tag = [0u8; GCM_TAG_SIZE]; + if gcm + .encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) + .is_err() + { + return Err("gcm_case_15_encrypt_call"); + } + if ciphertext != expected_ciphertext { + return Err("gcm_case_15_ciphertext"); + } + if tag != expected_tag { + return Err("gcm_case_15_tag"); + } + + let mut decrypted = [0u8; BLOCK_SIZE]; + if gcm + .decrypt(&nonce, &aad, &ciphertext, &tag, &mut decrypted) + .is_err() + { + return Err("gcm_case_15_decrypt_call"); + } + if decrypted != plaintext { + return Err("gcm_case_15_plaintext"); + } + + Ok(()) +} + +fn gcm_case_16() -> Result<(), &'static str> { + let aad: [u8; 0] = []; + + let gcm = AES256GCM::new(&GCM16_KEY); + + let mut ciphertext = [0u8; 64]; + let mut tag = [0u8; GCM_TAG_SIZE]; + if gcm + .encrypt( + &GCM16_NONCE, + &aad, + &GCM16_PLAINTEXT, + &mut ciphertext, + &mut tag, + ) + .is_err() + { + return Err("gcm_case_16_encrypt_call"); + } + if ciphertext != GCM16_CIPHERTEXT { + return Err("gcm_case_16_ciphertext"); + } + if tag != GCM16_TAG { + return Err("gcm_case_16_tag"); + } + + let mut decrypted = [0u8; 64]; + if gcm + .decrypt(&GCM16_NONCE, &aad, &ciphertext, &tag, &mut decrypted) + .is_err() + { + return Err("gcm_case_16_decrypt_call"); + } + if decrypted != GCM16_PLAINTEXT { + return Err("gcm_case_16_plaintext"); + } + + Ok(()) +} + +fn gcm_auth_failure() -> Result<(), &'static str> { + let aad: [u8; 0] = []; + + let gcm = AES256GCM::new(&GCM16_KEY); + + let mut ciphertext = [0u8; 64]; + let mut tag = [0u8; GCM_TAG_SIZE]; + if gcm + .encrypt( + &GCM16_NONCE, + &aad, + &GCM16_PLAINTEXT, + &mut ciphertext, + &mut tag, + ) + .is_err() + { + return Err("gcm_auth_failure_encrypt_call"); + } + + tag[0] ^= 0x01; + + let mut decrypted = [0u8; 64]; + match gcm.decrypt(&GCM16_NONCE, &aad, &ciphertext, &tag, &mut decrypted) { + Err(Error::AuthenticationFailed) => Ok(()), + Ok(()) => Err("gcm_auth_failure_accepted"), + Err(_) => Err("gcm_auth_failure_wrong_error"), + } +} + +fn aes256_drop_readback() -> Result<(), &'static str> { + let key = [0xA5u8; KEY_SIZE]; + let byte_len = size_of::(); + let mut storage: MaybeUninit = MaybeUninit::uninit(); + + unsafe { + storage.write(AES256::new(&key)); + let ptr = storage.as_ptr().cast::(); + + let pre = core::slice::from_raw_parts(ptr, byte_len); + if pre.iter().all(|&b| b == 0) { + return Err("aes256_drop_readback_precondition"); + } + + storage.assume_init_drop(); + + let post = core::slice::from_raw_parts(ptr, byte_len); + if post.iter().any(|&b| b != 0) { + return Err("aes256_drop_readback_residue"); + } + } + + Ok(()) +} From 492ea98f97f8591a0076da73acd810b6bf65bb22 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:53:48 +0900 Subject: [PATCH 50/65] =?UTF-8?q?aes=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=EB=B3=84=20std=20=ED=95=98=EB=84=A4=EC=8A=A4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=202=EC=A2=85=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20-=20arch=5Fx86=5F64.rs=20=EC=99=80=20arch=5Faarch64.rs=20?= =?UTF-8?q?=EA=B0=80=20=EB=8F=99=EC=9D=BC=20KAT=20=EB=B0=B0=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EA=B0=81=20target=5Farch=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=8B=A4=ED=96=89=20-=20not(target=5Fos=20=3D=20no?= =?UTF-8?q?ne)=20=EC=88=A0=EC=96=B4=EB=A1=9C=20=EB=B2=A0=EC=96=B4=EB=A9=94?= =?UTF-8?q?=ED=83=88=20=EB=B9=8C=EB=93=9C=EC=97=90=EC=84=9C=20std=20?= =?UTF-8?q?=ED=95=98=EB=84=A4=EC=8A=A4=20=EC=9A=94=EA=B5=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20-=20target=5Farch=20guard=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A1=9C=20cfg=20=EA=B2=8C=EC=9D=B4=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EC=9D=98=EB=8F=84=EB=8C=80=EB=A1=9C=20=EA=B1=B8?= =?UTF-8?q?=EB=A0=B8=EC=9D=8C=EC=9D=84=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/tests/arch_aarch64.rs | 15 +++++++++++++++ aes/tests/arch_x86_64.rs | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 aes/tests/arch_aarch64.rs create mode 100644 aes/tests/arch_x86_64.rs diff --git a/aes/tests/arch_aarch64.rs b/aes/tests/arch_aarch64.rs new file mode 100644 index 0000000..8b67a68 --- /dev/null +++ b/aes/tests/arch_aarch64.rs @@ -0,0 +1,15 @@ +#![cfg(all(target_arch = "aarch64", not(target_os = "none")))] + +mod common; + +#[test] +fn aarch64_kat_battery() { + if let Err(tag) = common::run_all() { + panic!("aarch64 KAT 실패: {tag}"); + } +} + +#[test] +fn aarch64_target_arch_guard() { + assert!(cfg!(target_arch = "aarch64")); +} diff --git a/aes/tests/arch_x86_64.rs b/aes/tests/arch_x86_64.rs new file mode 100644 index 0000000..7b5230a --- /dev/null +++ b/aes/tests/arch_x86_64.rs @@ -0,0 +1,15 @@ +#![cfg(all(target_arch = "x86_64", not(target_os = "none")))] + +mod common; + +#[test] +fn x86_64_kat_battery() { + if let Err(tag) = common::run_all() { + panic!("x86_64 KAT 실패: {tag}"); + } +} + +#[test] +fn x86_64_target_arch_guard() { + assert!(cfg!(target_arch = "x86_64")); +} From 889348ae6365634cc5f74c8e3c7343c8f14d8cea Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:56:09 +0900 Subject: [PATCH 51/65] =?UTF-8?q?aes=20=EB=B2=A0=EC=96=B4=EB=A9=94?= =?UTF-8?q?=ED=83=88=20KAT=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20tests/baremetal.rs=20=EB=A5=BC=20no=5Fstd=20no?= =?UTF-8?q?=5Fmain=20=EC=9D=B4=EC=A4=91=20=EB=AA=A8=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=B4=20=ED=98=B8=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8A=94=20main=20=EB=B2=A0=EC=96=B4?= =?UTF-8?q?=EB=A9=94=ED=83=88=EC=97=90=EC=84=9C=EB=8A=94=20=5Fstart=20?= =?UTF-8?q?=EB=A1=9C=20=EC=8B=A4=ED=96=89=20-=20Cargo.toml=20=EC=97=90=20h?= =?UTF-8?q?arness=20=3D=20false=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=83=80?= =?UTF-8?q?=EA=B2=9F=20=EC=84=A0=EC=96=B8=20-=20black=5Fbox=20=EB=A1=9C=20?= =?UTF-8?q?KAT=20=ED=98=B8=EC=B6=9C=EC=9D=B4=20lto=20opt-level=20z=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20-=20arch=20guard=20=EB=A5=BC=20const=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D=EC=9C=BC=EB=A1=9C=20=EC=98=AE=EA=B2=A8=20clippy=20ass?= =?UTF-8?q?ertions=5Fon=5Fconstants=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/Cargo.toml | 4 ++++ aes/tests/arch_aarch64.rs | 4 +++- aes/tests/arch_x86_64.rs | 4 +++- aes/tests/baremetal.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 aes/tests/baremetal.rs diff --git a/aes/Cargo.toml b/aes/Cargo.toml index 7435428..251de09 100644 --- a/aes/Cargo.toml +++ b/aes/Cargo.toml @@ -8,3 +8,7 @@ license.workspace = true [dependencies] constant-time.workspace = true zeroize.workspace = true + +[[test]] +name = "baremetal" +harness = false diff --git a/aes/tests/arch_aarch64.rs b/aes/tests/arch_aarch64.rs index 8b67a68..3ac2e4f 100644 --- a/aes/tests/arch_aarch64.rs +++ b/aes/tests/arch_aarch64.rs @@ -11,5 +11,7 @@ fn aarch64_kat_battery() { #[test] fn aarch64_target_arch_guard() { - assert!(cfg!(target_arch = "aarch64")); + const { + assert!(cfg!(target_arch = "aarch64")); + } } diff --git a/aes/tests/arch_x86_64.rs b/aes/tests/arch_x86_64.rs index 7b5230a..fff73ba 100644 --- a/aes/tests/arch_x86_64.rs +++ b/aes/tests/arch_x86_64.rs @@ -11,5 +11,7 @@ fn x86_64_kat_battery() { #[test] fn x86_64_target_arch_guard() { - assert!(cfg!(target_arch = "x86_64")); + const { + assert!(cfg!(target_arch = "x86_64")); + } } diff --git a/aes/tests/baremetal.rs b/aes/tests/baremetal.rs new file mode 100644 index 0000000..c6222da --- /dev/null +++ b/aes/tests/baremetal.rs @@ -0,0 +1,31 @@ +#![cfg_attr(target_os = "none", no_std)] +#![cfg_attr(target_os = "none", no_main)] + +mod common; + +#[cfg(target_os = "none")] +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + let result = core::hint::black_box(common::run_all()); + if result.is_err() { + panic!(); + } + loop { + core::hint::spin_loop(); + } +} + +#[cfg(target_os = "none")] +#[panic_handler] +fn panic(_: &core::panic::PanicInfo) -> ! { + loop { + core::hint::spin_loop(); + } +} + +#[cfg(not(target_os = "none"))] +fn main() { + if let Err(tag) = common::run_all() { + panic!("베어메탈 KAT 배터리 실패: {tag}"); + } +} From 9b928d23faaa3c3997a716fdca1b024f24f532df Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:39:56 +0900 Subject: [PATCH 52/65] =?UTF-8?q?constant-time=20=EC=97=90=20aarch64=20DIT?= =?UTF-8?q?=20=EA=B0=80=EB=93=9C=20=EC=B6=94=EA=B0=80=20-=20`dit.rs`=20?= =?UTF-8?q?=EC=8B=A0=EC=84=A4=EB=A1=9C=20=EC=A7=84=EC=9E=85=20=EC=8B=9C=20?= =?UTF-8?q?PSTATE.DIT=20=EC=84=A4=EC=A0=95=ED=95=98=EA=B3=A0=20Drop=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B3=B5=EC=9B=90=ED=95=98=EB=8A=94=20`DitGuard`?= =?UTF-8?q?=20=EC=A0=9C=EA=B3=B5=20-=20=EC=8B=A4=EC=A0=9C=20asm=20?= =?UTF-8?q?=EC=9D=80=20target=5Ffeature=20dit=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=A7=8C=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EA=B7=B8=20=EC=99=B8=EB=8A=94=20no-op=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=91=90=EC=96=B4=20=EA=B5=AC=ED=98=95=20=EC=BD=94?= =?UTF-8?q?=EC=96=B4=20UNDEFINED=20=ED=8A=B8=EB=9E=A9=20=ED=9A=8C=ED=94=BC?= =?UTF-8?q?=20-=20=ED=95=98=EB=93=9C=EC=9B=A8=EC=96=B4=20=EB=B3=B4?= =?UTF-8?q?=EC=9E=A5=20=EC=97=AC=EB=B6=80=EB=A5=BC=20`DIT=5FHW=5FBACKED`?= =?UTF-8?q?=20=EC=83=81=EC=88=98=EB=A1=9C=20=EB=85=B8=EC=B6=9C=20-=20READM?= =?UTF-8?q?E=20=ED=95=9C=EC=98=81=20=EC=96=91=EC=AA=BD=EC=97=90=20?= =?UTF-8?q?=EA=B5=AC=ED=98=95=20=EC=BD=94=EC=96=B4=20=EC=9E=94=EC=97=AC=20?= =?UTF-8?q?=EC=9C=84=ED=97=98=EA=B3=BC=20x86=20DOITM=20=EC=BB=A4=EB=84=90?= =?UTF-8?q?=20=EC=B1=85=EC=9E=84=20=EA=B8=B0=EC=88=A0=20-=20AI=5FSCOPE=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AI_SCOPE.md | 9 +- constant-time/README.md | 19 +++- constant-time/README_EN.md | 19 +++- constant-time/src/dit.rs | 177 +++++++++++++++++++++++++++++++++++++ constant-time/src/lib.rs | 14 +-- 5 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 constant-time/src/dit.rs diff --git a/AI_SCOPE.md b/AI_SCOPE.md index cd7192e..23f0845 100644 --- a/AI_SCOPE.md +++ b/AI_SCOPE.md @@ -48,14 +48,21 @@ ### AES +- [lib.rs](aes/src/lib.rs), [ghash.rs](aes/src/ghash.rs), [sbox.rs](aes/src/sbox.rs), [cbc.rs](aes/src/cbc.rs), [ctr.rs](aes/src/ctr.rs), [gcm.rs](aes/src/gcm.rs) 내에 테스트 모듈 작성 (Opus 4.8) +- `ghash.rs` 내 `bitsliced_sbox` 함수 (Opus 4.8) +- KCMVP 간편 테스트기 [kcmvp_cbc_ctr.rs](aes/tests/kcmvp_cbc_ctr.rs), [kcmvp_gcm.rs](aes/tests/kcmvp_gcm.rs) 모듈 작성 (Fable 5) +- [baremetal.rs](aes/tests/baremetal.rs) 테스트 모듈 작성 (Fable 5) + ### BLAKE ### ChaCha20 ### Constant-Time -- 각 파일에서 스플릿 형식 `//` 주석 (Sonnet 4.5) +- [lib.rs](constant-time/src/lib.rs) 파일 내 스플릿 형식 `//` 주석 (Sonnet 4.5) - [src/lib.rs의 ct_sel_swap_value_roundtrip 테스트 함수](https://github.com/Quant-Off/elib-k0-nt/blob/7d3a0b8389fcc0a5bcbef22ae70fa380ce420614/constant-time/src/lib.rs#L732) (Opus 4.8) +- [dit.rs](constant-time/src/dit.rs) 내 imp 모듈 아이디어 및 `//` 주석 (Fable 5) +- [README.md](constant-time/README.md) 내 "aarch64 DIT 가드" 섹션 내용 (Opus 5) ### Ed448 diff --git a/constant-time/README.md b/constant-time/README.md index ebcfb27..fb513bd 100644 --- a/constant-time/README.md +++ b/constant-time/README.md @@ -119,7 +119,24 @@ compile_error!("constant-time 의 검증된 상수-시간 구현은 x86_64와 aa 이로써 기존 `#[deprecated]` 경고(soft)를 하드 게이트로 승격했고, best-effort `black_box` fallback이 고보안 빌드에 조용히 섞일 일이 없어졌습니다. `miri`는 인-라인 어셈블리를 실행하지 못하므로 fallback 로직 검증을 위해 예외로 둡니다. 실제로 `riscv64gc-unknown-none-elf` 빌드가 이 게이트 메시지로 거부되는 것을 확인했습니다. -남은 과제로, x86_64/aarch64의 `cmov`/`csel`이 ISA 차원에서 데이터 독립 시간을 보장받으려면 aarch64 DIT 비트와 x86 DOITM을 켜는 하드닝이 별도로 필요하며 이는 향후 다룰 예정입니다. +x86_64/aarch64의 `cmov`/`csel`이 ISA 차원에서 데이터 독립 시간을 보장받으려면 aarch64 DIT 비트와 x86 DOITM을 켜는 하드닝이 별도로 필요합니다. 이 중 aarch64 쪽은 [이슈 #11](https://github.com/Quant-Off/elib-k0-nt/issues/11) 대응으로 옵트인 `DitGuard` API 를 도입해 해결했습니다. + +### aarch64 DIT 가드 (이슈 #11 대응) + +ARM은 FEAT_DIT(ARMv8.4-A 이상)가 구현되고 PSTATE.DIT=1인 경우에만 `cmp`/`csel`/`cset` 등 DIT 목록 명령의 데이터 독립 시간을 아키텍처 차원에서 보장합니다. 이를 위해 [dit.rs](src/dit.rs)에 RAII 가드를 두었습니다. 암호 연산 진입 시 `DitGuard::enter()`로 이전 PSTATE.DIT 를 저장하고 1로 설정하며, 스코프 이탈(Drop) 시 이전 값을 복원합니다. 중첩 생성도 안전합니다. + +```rust +let _dit = constant_time::DitGuard::enter(); +// 이 스코프의 상수-시간 연산은 DIT 활성 상태에서 수행됨 +``` + +설계 결정 세 가지가 중요합니다. + +1. **컴파일 타임 옵트인**: FEAT_DIT 미구현 코어에서 DIT 레지스터 접근은 UNDEFINED 트랩입니다. EL0 순수 라이브러리는 커널 협조 없이 런타임 기능 감지(`ID_AA64PFR0_EL1` 읽기)가 불가능하므로, 실제 asm 은 `target_feature = "dit"`가 켜진 빌드에서만 생성됩니다(`aarch64-apple-darwin`은 기본 활성, `aarch64-unknown-none`은 `-C target-feature=+dit` 필요). 그 외 빌드는 no-op이며 `DIT_HW_BACKED` 상수로 하드웨어 보장 여부를 구분합니다. +2. **인코딩 직접 표기**: 어셈블러 기능 게이트를 피하기 위해 `DIT` 별칭 대신 시스템 레지스터 인코딩 `S3_3_C4_C2_5`를 직접 사용합니다(BoringSSL과 동일한 기법). +3. **구형 코어 잔여 위험**: ARMv8.4 이전 코어(A53/A57/A72 등)에는 DIT 기능 자체가 없어 코드로 켤 방법이 없습니다. 다만 알려진 모든 구현에서 범용 레지스터 대상 `cmp`/`csel`/`cset`은 실측상 데이터 독립이며, DIT는 이를 아키텍처 보증으로 격상하는 장치입니다. 즉 구형 코어에서 이 크레이트의 상수-시간 성질은 실측 근거에 의존하며, 아키텍처 보증이 필요한 배포는 v8.4 이상 코어 + `+dit` 빌드를 사용해야 합니다. + +x86쪽 DOITM(IA32_UARCH_MISC_CTL)은 Ring 0 전용 MSR 이라 Ring 3 데몬이 설정할 수 없습니다. x86_64 배포에서는 K0 커널이 부트 시 DOITM을 설정해야 하며 **이는 통합 측 책임**으로 남습니다. --- diff --git a/constant-time/README_EN.md b/constant-time/README_EN.md index da6db5d..e607716 100644 --- a/constant-time/README_EN.md +++ b/constant-time/README_EN.md @@ -119,7 +119,24 @@ compile_error!("constant-time verified constant-time implementation is only avai This promotes the former `#[deprecated]` warning (soft) to a hard gate, ensuring that best-effort `black_box` fallbacks can never silently mix into high-security builds. `miri` cannot execute inline assembly, so it is exempted to allow validation of fallback logic. The gate message was confirmed in practice when an `riscv64gc-unknown-none-elf` build was rejected. -As a remaining task, hardening to explicitly enable the aarch64 DIT bit and x86 DOITM — which are required for ISA-level data-independent timing guarantees on `cmov`/`csel` — is left for future work. +For `cmov`/`csel` on x86_64/aarch64 to receive ISA-level data-independent timing guarantees, separate hardening is required: the aarch64 DIT bit and x86 DOITM. The aarch64 side has been addressed via [issue #11](https://github.com/Quant-Off/elib-k0-nt/issues/11) with an opt-in `DitGuard` API. + +### aarch64 DIT guard (issue #11) + +ARM architecturally guarantees data-independent timing for DIT-listed instructions such as `cmp`/`csel`/`cset` only when FEAT_DIT (ARMv8.4-A and later) is implemented and PSTATE.DIT is set to 1. [dit.rs](src/dit.rs) provides an RAII guard for this: `DitGuard::enter()` saves the previous PSTATE.DIT and sets it to 1 on entry into cryptographic operations, and restores the previous value on scope exit (Drop). Nested guards are safe. + +```rust +let _dit = constant_time::DitGuard::enter(); +// constant-time operations in this scope run with DIT active +``` + +Three design decisions matter here. + +1. **Compile-time opt-in**: On cores without FEAT_DIT, accessing the DIT register is an UNDEFINED trap. A pure EL0 library cannot perform runtime feature detection (reading `ID_AA64PFR0_EL1`) without kernel cooperation, so the real asm is only generated in builds with `target_feature = "dit"` enabled (`aarch64-apple-darwin` enables it by default; `aarch64-unknown-none` requires `-C target-feature=+dit`). All other builds get a no-op, and the `DIT_HW_BACKED` constant distinguishes whether hardware backing is present. +2. **Direct encoding notation**: To avoid assembler feature gating, the system register encoding `S3_3_C4_C2_5` is used directly instead of the `DIT` alias (the same technique as BoringSSL). +3. **Residual risk on older cores**: Cores before ARMv8.4 (A53/A57/A72, etc.) have no DIT feature at all, so there is no way to enable it in code. That said, on all known implementations, `cmp`/`csel`/`cset` on general-purpose registers are empirically data-independent; DIT is the mechanism that elevates this to an architectural guarantee. In other words, on older cores the constant-time property of this crate rests on empirical evidence, and deployments requiring an architectural guarantee must use v8.4+ cores with a `+dit` build. + +On the x86 side, DOITM (IA32_UARCH_MISC_CTL) is a Ring-0-only MSR that a Ring-3 daemon cannot set. For x86_64 deployments, the kernel (ISO-LIGHT-K0) must configure DOITM at boot; this remains the integrator's responsibility. --- diff --git a/constant-time/src/dit.rs b/constant-time/src/dit.rs new file mode 100644 index 0000000..1698475 --- /dev/null +++ b/constant-time/src/dit.rs @@ -0,0 +1,177 @@ +//! AArch64 PSTATE.DIT 비트를 트랜잭션 단위로 설정하고 복원하는 RAII 가드가 +//! 구현된 모듈입니다. +//! +//! ARM은 FEAT_DIT(ARMv8.4-A 이상)가 구현되고 PSTATE.DIT가 1인 경우에만 +//! `cmp`/`csel`/`cset` 등 DIT 목록 명령의 데이터 독립 시간을 아키텍처 +//! 차원에서 보장합니다. 이 모듈은 암호 연산 진입 시 DIT를 켜고 이탈 시 이전 +//! 상태로 복원하는 옵트인 가드를 제공합니다. +//! +//! FEAT_DIT 미구현 코어에서 DIT 레지스터 접근은 UNDEFINED 트랩이므로 실제 +//! 접근 코드는 `target_feature = "dit"`가 컴파일 타임에 활성화된 경우에만 +//! 생성됩니다. 런타임 기능 감지는 EL0 순수 라이브러리에서 커널 협조 없이 +//! 불가능하므로 제공하지 않습니다. 그 외 모든 빌드(x86_64 포함)에서 가드는 +//! no-op이며, 하드웨어 보장 여부는 [`DIT_HW_BACKED`]로 구분합니다. +//! +//! # Features +//! - `DitGuard`: 생성 시 이전 PSTATE.DIT 저장 후 1로 설정, Drop 시 복원 +//! - `DIT_HW_BACKED`: 현재 빌드에서 가드가 실제 하드웨어 보장을 제공하는지 여부 +//! +//! # Security Note +//! x86_64의 대응 기능인 DOITM(IA32_UARCH_MISC_CTL)은 Ring 0 전용 MSR 이므로 +//! 이 크레이트가 Ring 3에서 설정할 수 없습니다. x86_64 배포에서는 커널이 +//! 부트 시 DOITM을 설정해야 하며 이는 통합 측 책임입니다. +//! +//! # Examples +//! ```rust,ignore +//! let _dit = DitGuard::enter(); +//! // 이 스코프의 상수-시간 연산은 DIT 활성 상태에서 수행됨 +//! ``` + +/// 현재 빌드에서 `DitGuard`가 실제 하드웨어 DIT 설정을 수행하는지 여부입니다. +/// +/// `target_arch = "aarch64"` 이면서 `target_feature = "dit"`가 활성화된 +/// 빌드에서만 `true` 입니다. `false`인 빌드에서 가드는 no-op 입니다. +pub const DIT_HW_BACKED: bool = cfg!(all(target_arch = "aarch64", target_feature = "dit")); + +#[cfg(all(target_arch = "aarch64", target_feature = "dit"))] +mod imp { + // DIT 특수 레지스터의 PSTATE.DIT 비트 위치 + const DIT_BIT: u64 = 1 << 24; + + /// 생성 시 PSTATE.DIT를 1로 설정하고 Drop 시 이전 값으로 복원하는 + /// RAII 가드 구조체입니다. + /// + /// 중첩 생성이 안전합니다. 각 가드는 자신이 관측한 이전 값만 복원하므로 + /// 안쪽 가드가 해제되어도 바깥 가드의 활성 상태가 유지됩니다. + /// + /// # Security Note + /// PSTATE.DIT는 스레드별 상태입니다. 가드가 살아 있는 동안 수행되는 + /// DIT 목록 명령만 아키텍처 차원의 데이터 독립 시간을 보장받습니다. + #[must_use = "가드가 즉시 해제되면 DIT 보장이 적용되지 않음"] + pub struct DitGuard { + prev: u64, + } + + impl DitGuard { + /// 이전 PSTATE.DIT 값을 저장한 뒤 DIT를 1로 설정하는 함수입니다. + /// + /// # Safety + /// `mrs`/`msr`로 DIT 특수 레지스터(인코딩 값: S3_3_C4_C2_5)만 접근합니다. + /// 이 코드는 `target_feature = "dit"` 빌드에서만 생성되므로 UNDEFINED + /// 트랩이 발생하지 않습니다. `nomem`을 지정하지 않아 컴파일러가 + /// 메모리 연산을 가드 경계 밖으로 재배치하지 못하며 `nostack`, + /// `preserves_flags`로 스택과 조건 플래그를 건드리지 않습니다. + #[inline] + pub fn enter() -> Self { + let prev: u64; + unsafe { + core::arch::asm!( + "mrs {p}, S3_3_C4_C2_5", + "msr S3_3_C4_C2_5, {v}", + p = out(reg) prev, + v = in(reg) DIT_BIT, + options(nostack, preserves_flags), + ); + } + Self { prev } + } + } + + impl Drop for DitGuard { + /// 생성 시 저장한 이전 PSTATE.DIT 값을 복원하는 함수입니다. + /// + /// # Safety + /// `enter`와 동일한 근거로 안전합니다. 기록 값은 DIT 비트만 유효하고 + /// 나머지 비트는 무시되므로 저장해 둔 원시 값을 그대로 되씁니다. + #[inline] + fn drop(&mut self) { + unsafe { + core::arch::asm!( + "msr S3_3_C4_C2_5, {v}", + v = in(reg) self.prev, + options(nostack, preserves_flags), + ); + } + } + } +} + +#[cfg(not(all(target_arch = "aarch64", target_feature = "dit")))] +mod imp { + /// DIT 미지원 빌드에서 동일한 API를 제공하는 no-op 가드 구조체입니다. + /// + /// # Security Note + /// 이 변형은 어떤 하드웨어 상태도 변경하지 않습니다. 아키텍처 차원의 + /// DIT 보장이 필요한 배포는 [`super::DIT_HW_BACKED`]로 빌드 구성을 + /// 검증해야 합니다. + #[must_use = "가드가 즉시 해제되면 DIT 보장이 적용되지 않음"] + pub struct DitGuard { + _priv: (), + } + + impl DitGuard { + /// 아무 상태도 변경하지 않고 가드를 생성하는 함수입니다. + #[inline] + pub fn enter() -> Self { + Self { _priv: () } + } + } +} + +pub use imp::DitGuard; + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(all(target_arch = "aarch64", target_feature = "dit"))] + fn read_dit_bit() -> u64 { + let raw: u64; + unsafe { + core::arch::asm!( + "mrs {r}, S3_3_C4_C2_5", + r = out(reg) raw, + options(nostack, preserves_flags), + ); + } + (raw >> 24) & 1 + } + + // 빌드 구성과 상수가 일치해야 배포 측이 하드웨어 보장 여부를 신뢰할 수 있음 + #[test] + fn dit_hw_backed_matches_build_config() { + assert_eq!( + DIT_HW_BACKED, + cfg!(all(target_arch = "aarch64", target_feature = "dit")), + "DIT_HW_BACKED 가 빌드 구성과 불일치" + ); + } + + // 가드 생존 중 DIT=1, 해제 후 이전 값 복원이 RAII의 핵심 + #[cfg(all(target_arch = "aarch64", target_feature = "dit"))] + #[test] + fn dit_guard_sets_and_restores() { + let before = read_dit_bit(); + { + let _g = DitGuard::enter(); + assert_eq!(read_dit_bit(), 1, "가드 생존 중 DIT 미설정"); + } + assert_eq!(read_dit_bit(), before, "가드 해제 후 DIT 미복원"); + } + + // 중첩 가드에서 안쪽 해제가 바깥 가드의 활성 상태를 깨면 안 됨 + #[cfg(all(target_arch = "aarch64", target_feature = "dit"))] + #[test] + fn dit_guard_nesting_preserves_outer() { + let before = read_dit_bit(); + { + let _outer = DitGuard::enter(); + { + let _inner = DitGuard::enter(); + assert_eq!(read_dit_bit(), 1, "중첩 가드 생존 중 DIT 미설정"); + } + assert_eq!(read_dit_bit(), 1, "안쪽 가드 해제가 바깥 가드 상태를 훼손"); + } + assert_eq!(read_dit_bit(), before, "전체 해제 후 DIT 미복원"); + } +} diff --git a/constant-time/src/lib.rs b/constant-time/src/lib.rs index 5d759a1..6608965 100644 --- a/constant-time/src/lib.rs +++ b/constant-time/src/lib.rs @@ -7,11 +7,12 @@ //! 아키텍처에서는 `black_box` 기반 best-effort fallback으로 동작합니다. //! //! # Features -//! - `Choice`: 항상 0 또는 1 값을 갖는 상수-시간 bool이며 비트 연산으로 조합됩니다 -//! - `CtSelOps`: 조건에 따라 두 값 중 하나를 선택하고 대입과 교환을 파생합니다 -//! - `CtEqOps`: 두 값의 동등 여부를 상수-시간에 판정합니다 -//! - `CtGtOps`: 두 값의 대소를 상수-시간에 판정합니다 -//! - `CtLess`: `CtEqOps`와 `CtGtOps`를 만족하는 모든 타입에 자동으로 제공됩니다 +//! - `Choice`: 항상 0 또는 1 값을 갖는 상수-시간 bool이며 비트 연산으로 조합됩니다. +//! - `CtSelOps`: 조건에 따라 두 값 중 하나를 선택하고 대입과 교환을 파생합니다. +//! - `CtEqOps`: 두 값의 동등 여부를 상수-시간에 판정합니다. +//! - `CtGtOps`: 두 값의 대소를 상수-시간에 판정합니다. +//! - `CtLess`: `CtEqOps`와 `CtGtOps`를 만족하는 모든 타입에 자동으로 제공됩니다. +//! - `DitGuard`: AArch64 PSTATE.DIT를 암호 연산 구간 동안 설정하는 옵트-인(opt-in) [RAII](https://doc.rust-lang.org/rust-by-example/scope/raii.html) 가드입니다. //! //! # Examples //! ```rust,ignore @@ -22,9 +23,12 @@ //! ``` #![cfg_attr(not(test), no_std)] +mod dit; mod internal; pub mod traits; +pub use dit::{DIT_HW_BACKED, DitGuard}; + use crate::private::Sealed; use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; From db05693b5389bcbc9fb975f34150b233850970a3 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:36:37 +0900 Subject: [PATCH 53/65] =?UTF-8?q?=EC=9D=BC=EB=B0=98=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=EC=9D=84=20=EB=AA=A8=EB=93=88=20Docstring=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- constant-time/src/internal/aarch64.rs | 10 ++++------ constant-time/src/internal/ct128.rs | 8 +++----- constant-time/src/internal/fallback.rs | 19 ++++++++----------- constant-time/src/internal/x86_64.rs | 4 +--- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/constant-time/src/internal/aarch64.rs b/constant-time/src/internal/aarch64.rs index 76fe7db..31c7120 100644 --- a/constant-time/src/internal/aarch64.rs +++ b/constant-time/src/internal/aarch64.rs @@ -1,9 +1,7 @@ -// -// AArch64 아키텍처에서 사용되는 내부 함수 모듈입니다. -// -// 모든 연산을 64비트로 수행하며 32비트 타입은 호출 전에 영 확장 또는 부호 -// 확장합니다. -// +//! AArch64 아키텍처에서 사용되는 내부 함수 모듈입니다. +//! +//! 모든 연산을 64비트로 수행하며 32비트 타입은 호출 전에 영 확장 또는 부호 +//! 확장합니다. /// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. /// diff --git a/constant-time/src/internal/ct128.rs b/constant-time/src/internal/ct128.rs index f231164..5de98fe 100644 --- a/constant-time/src/internal/ct128.rs +++ b/constant-time/src/internal/ct128.rs @@ -1,8 +1,6 @@ -// -// 아키텍처 독립 128비트 프리미티브를 제공하는 모듈입니다. -// -// 아키텍처별 64비트 함수 위에 구성됩니다. -// +//! 아키텍처 독립 128비트 프리미티브를 제공하는 모듈입니다. +//! +//! 아키텍처별 64비트 함수 위에 구성됩니다. use crate::internal::{ct_eq64, ct_gt_u64}; diff --git a/constant-time/src/internal/fallback.rs b/constant-time/src/internal/fallback.rs index b4eda81..9b02389 100644 --- a/constant-time/src/internal/fallback.rs +++ b/constant-time/src/internal/fallback.rs @@ -1,13 +1,11 @@ -// -// 다른 아키텍처에 의해 처리되지 않는 모든 아키텍처를 위한 일반 fallback 모듈입니다. 이 모듈은 다음의 보안 게이트를 -// 형성합니다. -// -// 이 일반 fallback은 best-effort 최적화 배리어인 `core::hint::black_box`에 의존하므로 하드웨어 -// 수준 상수-시간을 보장하지 않습니다. 검증된 인-라인 어셈블리 구현이 있는 x86_64와 aarch64만 지원 타겟으로 -// 확정하고, 그 외 아키텍처의 비 miri 빌드는 아래 `compile_error!`로 컴파일 단계에서 거부하여 best- -// effort 경로가 고보안 빌드에 섞이지 않도록 합니다. miri는 인-라인 어셈블리를 실행하지 못하므로 fallback -// 로직 검증을 위해 예외로 둡니다. -// +//! 다른 아키텍처에 의해 처리되지 않는 모든 아키텍처를 위한 일반 fallback 모듈입니다. 이 모듈은 다음의 보안 게이트를 +//! 형성합니다. +//! +//! 이 일반 fallback은 best-effort 최적화 배리어인 `core::hint::black_box`에 의존하므로 하드웨어 +//! 수준 상수-시간을 보장하지 않습니다. 검증된 인-라인 어셈블리 구현이 있는 x86_64와 aarch64만 지원 타겟으로 +//! 확정하고, 그 외 아키텍처의 비 miri 빌드는 아래 `compile_error!`로 컴파일 단계에서 거부하여 best- +//! effort 경로가 고보안 빌드에 섞이지 않도록 합니다. miri는 인-라인 어셈블리를 실행하지 못하므로 fallback +//! 로직 검증을 위해 예외로 둡니다. /// 조건 바이트로부터 상수-시간 비트 마스크를 생성하는 함수입니다. /// @@ -19,7 +17,6 @@ /// # Security Note /// 이 fallback 경로는 `core::hint::black_box`에 의존하는 best-effort /// 구현이므로 상수-시간 성질이 하드웨어 수준으로 보장되지는 않습니다. - #[inline(never)] pub(crate) fn ct_mask(cond: u8) -> u64 { let c = core::hint::black_box(cond as u64); diff --git a/constant-time/src/internal/x86_64.rs b/constant-time/src/internal/x86_64.rs index 5290459..5812b78 100644 --- a/constant-time/src/internal/x86_64.rs +++ b/constant-time/src/internal/x86_64.rs @@ -1,6 +1,4 @@ -// -// x86_64 아키텍처에서 사용되는 내부 함수 모듈입니다. -// +//! x86_64 아키텍처에서 사용되는 내부 함수 모듈입니다. /// 조건이 0이 아니면 a, 0이면 b를 상수-시간에 선택하는 함수입니다. /// From 4d47381773677f45d4492b04c08e625b82a160a9 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:35:47 +0900 Subject: [PATCH 54/65] =?UTF-8?q?Secret=20=EC=97=90=20=EC=A0=9C=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EC=B4=88=EA=B8=B0=ED=99=94=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20`init=5Fwith`=20=EB=A1=9C=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20=EB=82=B4=EC=9A=A9=20=EC=86=8C=EA=B1=B0=20=ED=9B=84=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20=EC=9C=84=EC=B9=98=EC=97=90=20=EC=A7=81?= =?UTF-8?q?=EC=A0=91=20=EA=B8=B0=EB=A1=9D=ED=95=B4=20move=20=EC=9E=94?= =?UTF-8?q?=EB=A5=98=20=EC=B0=A8=EB=8B=A8=20-=20Drop=20=EA=B3=BC=20into=5F?= =?UTF-8?q?inner=20=EC=9D=98=20=EC=A4=91=EB=B3=B5=20=EC=86=8C=EA=B1=B0=20?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=EC=9D=84=20`wipe`=20=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20-=20`zeroize=5Freadback.rs`=20=EC=97=90=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zeroize/src/secret.rs | 65 ++++++++++++++++++------------- zeroize/tests/zeroize_readback.rs | 25 ++++++++++++ 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/zeroize/src/secret.rs b/zeroize/src/secret.rs index f110a92..8cb2f86 100644 --- a/zeroize/src/secret.rs +++ b/zeroize/src/secret.rs @@ -69,36 +69,60 @@ impl Secret { pub fn expose_mut(&mut self) -> &mut T { &mut self.inner } -} -impl Secret { - /// 내부 데이터를 추출하고 원본 메모리를 소거합니다. + /// 기존 내용을 소거한 뒤 내부 데이터를 제자리에서 초기화합니다. /// - /// 데이터를 복사한 후 원본 메모리를 안전하게 소거하고, - /// `Drop`을 우회하여 복사본을 반환합니다. + /// 비밀 값을 by-value로 옮기지 않고 최종 저장 위치에 직접 기록하므로 + /// move 로 인한 스택 사본 잔류가 발생하지 않습니다. + /// + /// # Arguments + /// - `f`: 소거된 내부 데이터를 받아 새 값을 기록하는 클로저 /// /// # Security Note - /// 반환된 값은 더 이상 `Secret`의 보호를 받지 않습니다. - /// 사용 후 직접 소거해야 합니다. - /// `T: Copy` 바운드로 비-`Copy` 타입의 논리적 double-drop window 와 - /// 내부 포인터 데이터 미소거 가능성을 컴파일타임에 차단합니다. + /// `Secret::new(value)`는 `value`가 이동하며 원본 스택 슬롯에 + /// 사본을 남깁니다. 비밀이 이미 실체화된 값은 이 API로 기록하고, + /// `new`는 0 등 비밀이 아닌 초기값 생성에만 사용하세요. #[inline] - pub fn into_inner(mut self) -> T { - let inner = self.inner; + pub fn init_with(&mut self, f: F) { + self.wipe(); + f(&mut self.inner); + } + #[inline] + fn wipe(&mut self) { compiler_barrier(); - let size = mem::size_of::(); + + let size = size_of::(); let p = &mut self.inner as *mut T as *mut u8; for i in 0..size { unsafe { ptr::write_volatile(p.add(i), 0); } } + compiler_barrier(); atomic_compiler_fence(); memory_barrier(); + black_box(p); + } +} +impl Secret { + /// 내부 데이터를 추출하고 원본 메모리를 소거합니다. + /// + /// 데이터를 복사한 후 원본 메모리를 안전하게 소거하고, + /// `Drop`을 우회하여 복사본을 반환합니다. + /// + /// # Security Note + /// 반환된 값은 더 이상 `Secret`의 보호를 받지 않습니다. + /// 사용 후 직접 소거해야 합니다. + /// `T: Copy` 바운드로 비-`Copy` 타입의 논리적 double-drop window 와 + /// 내부 포인터 데이터 미소거 가능성을 컴파일타임에 차단합니다. + #[inline] + pub fn into_inner(mut self) -> T { + let inner = self.inner; + self.wipe(); mem::forget(self); inner } @@ -123,22 +147,7 @@ impl DerefMut for Secret { impl Drop for Secret { #[inline] fn drop(&mut self) { - compiler_barrier(); - - let size = size_of::(); - let p = &mut self.inner as *mut T as *mut u8; - - for i in 0..size { - unsafe { - ptr::write_volatile(p.add(i), 0); - } - } - - compiler_barrier(); - atomic_compiler_fence(); - memory_barrier(); - - black_box(p); + self.wipe(); } } diff --git a/zeroize/tests/zeroize_readback.rs b/zeroize/tests/zeroize_readback.rs index 4d21b51..9cd7688 100644 --- a/zeroize/tests/zeroize_readback.rs +++ b/zeroize/tests/zeroize_readback.rs @@ -48,6 +48,31 @@ fn secret_into_inner_extracts_value() { ); } +// init_with 는 기존 내용을 먼저 소거한 뒤 클로저가 제자리에 기록한다. +// 빈 클로저로 소거-선행 계약을, 기록 클로저로 제자리 초기화 계약을 검증한다. +#[cfg_attr(miri, ignore)] +#[test] +fn secret_init_with_wipes_then_writes_in_place() { + let mut s = Secret::new([0xEEu8; 32]); + let addr = s.expose().as_ptr(); + + s.init_with(|_| {}); + assert!( + s.expose().iter().all(|&b| b == 0), + "init_with 가 기존 내용을 소거하지 않음" + ); + + s.init_with(|v| v.copy_from_slice(&[0x42u8; 32])); + assert!( + s.expose().iter().all(|&b| b == 0x42), + "init_with 가 새 값을 기록하지 않음" + ); + assert!( + core::ptr::eq(addr, s.expose().as_ptr()), + "init_with 후 내부 데이터 주소가 이동함 — 제자리 계약 위반" + ); +} + #[cfg_attr(miri, ignore)] #[test] fn zeroize_flat_wipes_bytes() { From 56d5357e3cb260753a0d885da1a7848546baa7a5 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:01 +0900 Subject: [PATCH 55/65] =?UTF-8?q?Blake3=20=EC=A0=95=EB=A0=AC=20=ED=8C=A8?= =?UTF-8?q?=EB=94=A9=20=EB=AF=B8=EC=86=8C=EA=B1=B0=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20=ED=95=84=EB=93=9C=EB=B3=84=20=EC=86=8C=EA=B1=B0=EA=B0=80?= =?UTF-8?q?=20=EB=8B=BF=EC=A7=80=20=EC=95=8A=EB=8D=98=20Blake3=204?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=ED=8A=B8=20ChunkState=203=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=ED=8C=A8=EB=94=A9=EC=9D=B4=20=EC=9E=94=EB=A5=98=20?= =?UTF-8?q?-=20`zeroize=5Fflat`=20=EA=B8=B0=EB=B0=98=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=B4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EC=B2=B4=20=EC=A0=84=EC=B2=B4=20=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=86=8C=EA=B1=B0=20-=20=EB=91=90=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EC=B2=B4=EC=97=90=20`Zeroable`=20=EB=A7=88?= =?UTF-8?q?=EC=BB=A4=20=EB=B6=80=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blake/src/blake3.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/blake/src/blake3.rs b/blake/src/blake3.rs index f6a60c6..35e46d4 100644 --- a/blake/src/blake3.rs +++ b/blake/src/blake3.rs @@ -1,7 +1,7 @@ //! BLAKE3 코어 구현 모듈입니다. //! BLAKE3 명세(https://github.com/BLAKE3-team/BLAKE3-specs)를 준수합니다. -use zeroize::{Secret, Zeroize}; +use zeroize::{Secret, Zeroable, Zeroize, zeroize_flat}; use crate::{HashError, SecureBuffer}; @@ -143,14 +143,21 @@ impl Default for Blake3 { } } +// # Safety +// Blake3 의 모든 필드(Secret 래핑 정수 배열, usize, u32, ChunkState)는 +// all-zero 비트 패턴이 유효한 값이다 +unsafe impl Zeroable for Blake3 {} + impl Zeroize for Blake3 { + /// 필드 사이 패딩까지 포함해 구조체 전체 바이트를 소거합니다. + /// + /// # Security Note + /// 필드별 소거는 정렬 패딩(Blake3 4바이트, ChunkState 3바이트)을 남깁니다. + /// 구조체 대입 시 패딩에 이전 비밀 잔재가 복사될 수 있으므로 + /// byte-extent 전체를 휘발성 쓰기로 덮습니다. #[inline] fn zeroize(&mut self) { - self.key_words.zeroize(); - self.cv_stack.zeroize(); - self.chunk_state.zeroize(); - self.flags.zeroize(); - self.cv_stack_len.zeroize(); + zeroize_flat(self); } } @@ -243,15 +250,15 @@ impl ChunkState { } } +// # Safety +// ChunkState 의 모든 필드는 all-zero 비트 패턴이 유효한 값이다 +unsafe impl Zeroable for ChunkState {} + impl Zeroize for ChunkState { + /// 필드 사이 패딩까지 포함해 구조체 전체 바이트를 소거합니다. #[inline] fn zeroize(&mut self) { - self.chaining_value.zeroize(); - self.buf.zeroize(); - self.chunk_counter.zeroize(); - self.buf_len.zeroize(); - self.blocks_compressed.zeroize(); - self.flags.zeroize(); + zeroize_flat(self); } } From 9b8ee0e3919e7bd1bf082c924293556b255ae793 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:01 +0900 Subject: [PATCH 56/65] =?UTF-8?q?aes=20=EB=A5=BC=20Default=20=EC=99=80=20i?= =?UTF-8?q?nit=202=EB=8B=A8=EA=B3=84=20=EC=83=9D=EC=84=B1=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98=20-=20AES256=20CBC=20CTR=20GCM?= =?UTF-8?q?=20GHash=20=EC=9D=98=20by-value=20=EC=83=9D=EC=84=B1=EC=9E=90?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=B4=20=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=20=ED=82=A4=20=EC=9D=B4=EB=8F=99=20=EC=9E=94=EB=A5=98?= =?UTF-8?q?=20=EC=B0=A8=EB=8B=A8=20-=20`GHash::finalize`=20=EB=A5=BC=20`&m?= =?UTF-8?q?ut=20self`=20=EB=A1=9C=20=EB=B0=94=EA=BF=94=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=86=8C=EB=B9=84=20=EC=A0=9C=EA=B1=B0=20-=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=86=B5=ED=95=A9=20KCMVP=20=EB=B2=A0?= =?UTF-8?q?=EC=96=B4=EB=A9=94=ED=83=88=20=ED=95=98=EB=84=A4=EC=8A=A4=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=EC=B2=98=20=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/src/cbc.rs | 24 ++++++++++------- aes/src/ctr.rs | 21 +++++++++------ aes/src/gcm.rs | 54 ++++++++++++++++++++++++++------------ aes/src/ghash.rs | 32 +++++++++++++++------- aes/src/lib.rs | 33 ++++++++++++++++------- aes/tests/common/mod.rs | 24 +++++++++++------ aes/tests/kcmvp_cbc_ctr.rs | 13 ++++++--- aes/tests/kcmvp_gcm.rs | 6 +++-- 8 files changed, 139 insertions(+), 68 deletions(-) diff --git a/aes/src/cbc.rs b/aes/src/cbc.rs index 93e4e84..380d5f9 100644 --- a/aes/src/cbc.rs +++ b/aes/src/cbc.rs @@ -3,16 +3,18 @@ use zeroize::Zeroize; pub const CBC_IV_SIZE: usize = 16; +#[derive(Default)] pub struct AES256CBC { cipher: AES256, } impl AES256CBC { - #[must_use] - pub fn new(key: &[u8; 32]) -> Self { - Self { - cipher: AES256::new(key), - } + /// 256비트 키를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `key`: 32바이트 암호화 키 + pub fn init(&mut self, key: &[u8; 32]) { + self.cipher.init(key); } pub fn encrypt( @@ -111,7 +113,8 @@ mod tests { 0xda, 0x6c, 0x19, 0x07, 0x8c, 0x6a, 0x9d, 0x1b, ]; - let cbc = AES256CBC::new(&key); + let mut cbc = AES256CBC::default(); + cbc.init(&key); let mut ciphertext = [0u8; 64]; cbc.encrypt(&iv, &plaintext, &mut ciphertext).unwrap(); assert_eq!(ciphertext, expected_ciphertext); @@ -138,7 +141,8 @@ mod tests { 0x73, 0x74, 0x21, 0x00, ]; - let cbc = AES256CBC::new(&key); + let mut cbc = AES256CBC::default(); + cbc.init(&key); let mut ciphertext = [0u8; 32]; cbc.encrypt(&iv, &plaintext, &mut ciphertext).unwrap(); @@ -153,7 +157,8 @@ mod tests { let iv = [0x22u8; 16]; let plaintext = [0x33u8; 17]; - let cbc = AES256CBC::new(&key); + let mut cbc = AES256CBC::default(); + cbc.init(&key); let mut ciphertext = [0u8; 32]; let result = cbc.encrypt(&iv, &plaintext, &mut ciphertext); assert_eq!(result, Err(Error::InvalidLength)); @@ -165,7 +170,8 @@ mod tests { let iv = [0x22u8; 16]; let plaintext = [0x33u8; 32]; - let cbc = AES256CBC::new(&key); + let mut cbc = AES256CBC::default(); + cbc.init(&key); let mut ciphertext = [0u8; 16]; let result = cbc.encrypt(&iv, &plaintext, &mut ciphertext); assert_eq!(result, Err(Error::BufferTooSmall)); diff --git a/aes/src/ctr.rs b/aes/src/ctr.rs index b3af584..98a22a4 100644 --- a/aes/src/ctr.rs +++ b/aes/src/ctr.rs @@ -15,16 +15,18 @@ fn inc32(block: &mut [u8; 16]) { } } +#[derive(Default)] pub struct AES256CTR { cipher: AES256, } impl AES256CTR { - #[must_use] - pub fn new(key: &[u8; 32]) -> Self { - Self { - cipher: AES256::new(key), - } + /// 256비트 키를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `key`: 32바이트 암호화 키 + pub fn init(&mut self, key: &[u8; 32]) { + self.cipher.init(key); } fn apply_internal(&self, counter: &mut [u8; 16], input: &[u8], output: &mut [u8]) { @@ -140,7 +142,8 @@ mod tests { 0x13, 0xc2, 0xdd, 0x08, 0x45, 0x79, 0x41, 0xa6, ]; - let ctr = AES256CTR::new(&key); + let mut ctr = AES256CTR::default(); + ctr.init(&key); let mut ciphertext = [0u8; 64]; ctr.apply_iv(&iv, &plaintext, &mut ciphertext).unwrap(); assert_eq!(ciphertext, expected_ciphertext); @@ -156,7 +159,8 @@ mod tests { let nonce: [u8; 12] = [0x01u8; 12]; let plaintext: [u8; 20] = [0xAAu8; 20]; - let ctr = AES256CTR::new(&key); + let mut ctr = AES256CTR::default(); + ctr.init(&key); let mut ciphertext = [0u8; 20]; ctr.encrypt(&nonce, &plaintext, &mut ciphertext).unwrap(); @@ -171,7 +175,8 @@ mod tests { let nonce = [0x22u8; 12]; let input = [0x33u8; 32]; - let ctr = AES256CTR::new(&key); + let mut ctr = AES256CTR::default(); + ctr.init(&key); let mut output = [0u8; 16]; let result = ctr.apply(&nonce, &input, &mut output); assert_eq!(result, Err(Error::BufferTooSmall)); diff --git a/aes/src/gcm.rs b/aes/src/gcm.rs index 6ff6c4d..e1488fa 100644 --- a/aes/src/gcm.rs +++ b/aes/src/gcm.rs @@ -21,17 +21,25 @@ fn inc32(block: &mut [u8; 16]) { } } +#[derive(Default)] pub struct AES256GCM { cipher: AES256, h: Secret<[u8; 16]>, } impl AES256GCM { - #[must_use] - pub fn new(key: &[u8; 32]) -> Self { - let cipher = AES256::new(key); - let h = Secret::new(cipher.encrypt(&[0u8; 16])); - Self { cipher, h } + /// 256비트 키와 해시 서브키를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `key`: 32바이트 암호화 키 + /// + /// # Security Note + /// `Default` 로 만든 인스턴스를 최종 위치에 둔 뒤 호출하면 + /// 라운드 키와 해시 서브키의 move 스택 잔류가 없습니다. + pub fn init(&mut self, key: &[u8; 32]) { + self.cipher.init(key); + self.h + .init_with(|dst| *dst = self.cipher.encrypt(&[0u8; 16])); } fn gctr(&self, icb: &[u8; 16], input: &[u8], output: &mut [u8]) { @@ -70,7 +78,8 @@ impl AES256GCM { return self.compute_j0(nonce); } - let mut ghash = GHash::new(self.h.expose()); + let mut ghash = GHash::default(); + ghash.init(self.h.expose()); ghash.update_padded(iv); let mut len_block = [0u8; 16]; @@ -84,7 +93,8 @@ impl AES256GCM { } fn compute_tag(&self, aad: &[u8], ciphertext: &[u8], j0: &[u8; 16]) -> [u8; 16] { - let mut ghash = GHash::new(self.h.expose()); + let mut ghash = GHash::default(); + ghash.init(self.h.expose()); ghash.update_padded(aad); ghash.update_padded(ciphertext); @@ -240,7 +250,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(AES256GCM::new(&key)); + storage.write(AES256GCM::default()); + storage.assume_init_mut().init(&key); let h_ptr = storage.assume_init_ref().h.expose().as_ptr(); let rk_ptr = storage .assume_init_ref() @@ -279,7 +290,8 @@ mod tests { 0x73, 0x8b, ]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 0]; let mut tag = [0u8; 16]; gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) @@ -302,7 +314,8 @@ mod tests { 0xb9, 0x19, ]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 16]; let mut tag = [0u8; 16]; gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) @@ -346,7 +359,8 @@ mod tests { 0xcc, 0x6c, ]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 64]; let mut tag = [0u8; 16]; gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) @@ -394,7 +408,8 @@ mod tests { 0x55, 0x1b, ]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 60]; let mut tag = [0u8; 16]; gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) @@ -422,7 +437,8 @@ mod tests { let plaintext: [u8; 16] = [0u8; 16]; let aad: [u8; 0] = []; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 16]; let mut tag = [0u8; 16]; gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag) @@ -442,7 +458,8 @@ mod tests { let aad: [u8; 0] = []; let plaintext = [0x33u8; 32]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 16]; let mut tag = [0u8; 16]; let result = gcm.encrypt(&nonce, &aad, &plaintext, &mut ciphertext, &mut tag); @@ -475,7 +492,8 @@ mod tests { ); let expected_tag: [u8; 16] = hex_arr("3a337dbf46a792c45e454913fe2ea8f2"); - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 60]; let mut tag = [0u8; 16]; gcm.encrypt_with_iv(&iv, &aad, &plaintext, &mut ciphertext, &mut tag) @@ -506,7 +524,8 @@ mod tests { ); let expected_tag: [u8; 16] = hex_arr("a44a8266ee1c8eb0c8b5d4cf5ae9f19a"); - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 60]; let mut tag = [0u8; 16]; gcm.encrypt_with_iv(&iv, &aad, &plaintext, &mut ciphertext, &mut tag) @@ -528,7 +547,8 @@ mod tests { let aad: [u8; 0] = []; let plaintext = [0x33u8; 24]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 24]; let mut full_tag = [0u8; 16]; gcm.encrypt(&iv, &aad, &plaintext, &mut ciphertext, &mut full_tag) diff --git a/aes/src/ghash.rs b/aes/src/ghash.rs index 0242c8e..8bcdb1c 100644 --- a/aes/src/ghash.rs +++ b/aes/src/ghash.rs @@ -82,18 +82,21 @@ fn gf128_mul(x: u128, y: u128) -> u128 { reduce_natural(lo, hi).reverse_bits() } +#[derive(Default)] pub struct GHash { h_n: Secret, state_n: Secret, } impl GHash { - #[must_use] - pub fn new(h: &[u8; 16]) -> Self { - Self { - h_n: Secret::new(u128::from_be_bytes(*h).reverse_bits()), - state_n: Secret::new(0), - } + /// 인증 서브키를 제자리에서 설정하고 상태를 초기화합니다. + /// + /// # Arguments + /// - `h`: 16바이트 인증 서브키 + pub fn init(&mut self, h: &[u8; 16]) { + self.h_n + .init_with(|v| *v = u128::from_be_bytes(*h).reverse_bits()); + self.state_n.zeroize(); } pub fn update(&mut self, block: &[u8; 16]) { @@ -118,9 +121,16 @@ impl GHash { } } + /// 누적 상태를 태그로 출력하고 상태를 제자리에서 소거합니다. + /// + /// # Security Note + /// `self` 를 이동 소비하지 않으므로 원본 슬롯에 사본이 남지 않으며, + /// 서브키 소거는 스코프 종료 시 `Drop` 이 제자리에서 수행합니다. #[must_use] - pub fn finalize(self) -> [u8; 16] { - self.state_n.expose().reverse_bits().to_be_bytes() + pub fn finalize(&mut self) -> [u8; 16] { + let tag = self.state_n.expose().reverse_bits().to_be_bytes(); + self.state_n.zeroize(); + tag } pub fn reset(&mut self) { @@ -141,7 +151,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(GHash::new(&h)); + storage.write(GHash::default()); + (*storage.as_mut_ptr()).init(&h); (*storage.as_mut_ptr()).update(&[0xAAu8; 16]); let h_ptr = (*storage.as_ptr()).h_n.expose() as *const u128 as *const u8; @@ -176,7 +187,8 @@ mod tests { 0xde, 0xb7, ]; - let mut ghash = GHash::new(&h); + let mut ghash = GHash::default(); + ghash.init(&h); ghash.update(&data); let result = ghash.finalize(); assert_eq!(result, expected); diff --git a/aes/src/lib.rs b/aes/src/lib.rs index fb8cabd..134109a 100644 --- a/aes/src/lib.rs +++ b/aes/src/lib.rs @@ -17,7 +17,8 @@ //! let nonce = [0u8; GCM_NONCE_SIZE]; //! let plaintext = b"Hello, World!!!!"; //! -//! let gcm = AES256GCM::new(&key); +//! let mut gcm = AES256GCM::default(); +//! gcm.init(&key); //! let mut ciphertext = [0u8; 16]; //! let mut tag = [0u8; GCM_TAG_SIZE]; //! gcm.encrypt(&nonce, &[], plaintext, &mut ciphertext, &mut tag).unwrap(); @@ -67,16 +68,25 @@ pub struct AES256 { round_keys: Secret<[u32; NB * (NR + 1)]>, } +impl Default for AES256 { + fn default() -> Self { + Self { + round_keys: Secret::new([0u32; NB * (NR + 1)]), + } + } +} + impl AES256 { - /// 256비트 키로 AES-256 인스턴스를 생성합니다. + /// 256비트 키를 제자리에서 확장해 라운드 키를 설정합니다. /// /// # Arguments /// - `key`: 32바이트 암호화 키 - #[must_use] - pub fn new(key: &[u8; KEY_SIZE]) -> Self { - let mut round_keys = Secret::new([0u32; NB * (NR + 1)]); - expand_key(key, round_keys.expose_mut()); - Self { round_keys } + /// + /// # Security Note + /// by-value 생성자 대신 `Default` 로 만든 인스턴스를 최종 위치에 둔 뒤 + /// 이 함수로 초기화하면 move 로 인한 라운드 키 스택 잔류가 없습니다. + pub fn init(&mut self, key: &[u8; KEY_SIZE]) { + self.round_keys.init_with(|w| expand_key(key, w)); } /// 16바이트 블록을 암호화합니다. @@ -111,7 +121,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(AES256::new(&key)); + storage.write(AES256::default()); + storage.assume_init_mut().init(&key); let ptr = storage.assume_init_ref().round_keys.expose().as_ptr() as *const u8; let byte_len = size_of::<[u32; NB * (NR + 1)]>(); @@ -147,7 +158,8 @@ mod tests { 0x60, 0x89, ]; - let cipher = AES256::new(&key); + let mut cipher = AES256::default(); + cipher.init(&key); let ciphertext = cipher.encrypt(&plaintext); assert_eq!(ciphertext, expected_ciphertext); @@ -167,7 +179,8 @@ mod tests { 0x00, 0x00, ]; - let cipher = AES256::new(&key); + let mut cipher = AES256::default(); + cipher.init(&key); let ciphertext = cipher.encrypt(&plaintext); let decrypted = cipher.decrypt(&ciphertext); assert_eq!(decrypted, plaintext); diff --git a/aes/tests/common/mod.rs b/aes/tests/common/mod.rs index 91c0b63..1a0ab31 100644 --- a/aes/tests/common/mod.rs +++ b/aes/tests/common/mod.rs @@ -58,7 +58,8 @@ fn fips197_c3() -> Result<(), &'static str> { 0x89, ]; - let cipher = AES256::new(&key); + let mut cipher = AES256::default(); + cipher.init(&key); let ciphertext = cipher.encrypt(&plaintext); if ciphertext != expected_ciphertext { @@ -98,7 +99,8 @@ fn cbc_sp800_38a_f2_5() -> Result<(), &'static str> { 0x8c, 0x6a, 0x9d, 0x1b, ]; - let cbc = AES256CBC::new(&key); + let mut cbc = AES256CBC::default(); + cbc.init(&key); let mut ciphertext = [0u8; 64]; if cbc.encrypt(&iv, &plaintext, &mut ciphertext).is_err() { @@ -144,7 +146,8 @@ fn ctr_sp800_38a_f5_5() -> Result<(), &'static str> { 0x45, 0x79, 0x41, 0xa6, ]; - let ctr = AES256CTR::new(&key); + let mut ctr = AES256CTR::default(); + ctr.init(&key); let mut ciphertext = [0u8; 64]; if ctr.apply_iv(&iv, &plaintext, &mut ciphertext).is_err() { @@ -175,7 +178,8 @@ fn gcm_case_14() -> Result<(), &'static str> { 0x8b, ]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; 0]; let mut tag = [0u8; GCM_TAG_SIZE]; @@ -214,7 +218,8 @@ fn gcm_case_15() -> Result<(), &'static str> { 0x19, ]; - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ciphertext = [0u8; BLOCK_SIZE]; let mut tag = [0u8; GCM_TAG_SIZE]; @@ -248,7 +253,8 @@ fn gcm_case_15() -> Result<(), &'static str> { fn gcm_case_16() -> Result<(), &'static str> { let aad: [u8; 0] = []; - let gcm = AES256GCM::new(&GCM16_KEY); + let mut gcm = AES256GCM::default(); + gcm.init(&GCM16_KEY); let mut ciphertext = [0u8; 64]; let mut tag = [0u8; GCM_TAG_SIZE]; @@ -288,7 +294,8 @@ fn gcm_case_16() -> Result<(), &'static str> { fn gcm_auth_failure() -> Result<(), &'static str> { let aad: [u8; 0] = []; - let gcm = AES256GCM::new(&GCM16_KEY); + let mut gcm = AES256GCM::default(); + gcm.init(&GCM16_KEY); let mut ciphertext = [0u8; 64]; let mut tag = [0u8; GCM_TAG_SIZE]; @@ -321,7 +328,8 @@ fn aes256_drop_readback() -> Result<(), &'static str> { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(AES256::new(&key)); + storage.write(AES256::default()); + storage.assume_init_mut().init(&key); let ptr = storage.as_ptr().cast::(); let pre = core::slice::from_raw_parts(ptr, byte_len); diff --git a/aes/tests/kcmvp_cbc_ctr.rs b/aes/tests/kcmvp_cbc_ctr.rs index 189fe94..fbfcd32 100644 --- a/aes/tests/kcmvp_cbc_ctr.rs +++ b/aes/tests/kcmvp_cbc_ctr.rs @@ -124,12 +124,15 @@ fn one_shot(mode: Mode, key: &[u8; 32], iv: &[u8; 16], pt: &[u8], ctx: &str) -> let mut ct = vec![0u8; pt.len()]; match mode { Mode::CNC => { - AES256CBC::new(key) + let mut cipher = AES256CBC::default(); + cipher.init(key); + cipher .encrypt(iv, pt, &mut ct) .unwrap_or_else(|e| panic!("{ctx}: CBC 암호화 실패 {e:?}")); } Mode::CTR => { - let cipher = AES256CTR::new(key); + let mut cipher = AES256CTR::default(); + cipher.init(key); let mut counter = u128::from_be_bytes(*iv); for (pt_chunk, ct_chunk) in pt.chunks(16).zip(ct.chunks_mut(16)) { cipher @@ -154,7 +157,8 @@ fn mct_chain(mode: Mode, mut key: [u8; 32], mut iv: [u8; 16], mut pt: [u8; 16]) let mut ct = [0u8; 16]; match mode { Mode::CNC => { - let cipher = AES256CBC::new(&key); + let mut cipher = AES256CBC::default(); + cipher.init(&key); let mut chain = iv; let mut block = pt; for _ in 0..MCT_INNER { @@ -169,7 +173,8 @@ fn mct_chain(mode: Mode, mut key: [u8; 32], mut iv: [u8; 16], mut pt: [u8; 16]) pt = ct_prev; } Mode::CTR => { - let cipher = AES256CTR::new(&key); + let mut cipher = AES256CTR::default(); + cipher.init(&key); let mut counter = u128::from_be_bytes(iv); let mut block = pt; for _ in 0..MCT_INNER { diff --git a/aes/tests/kcmvp_gcm.rs b/aes/tests/kcmvp_gcm.rs index b9b45f8..9e4cea2 100644 --- a/aes/tests/kcmvp_gcm.rs +++ b/aes/tests/kcmvp_gcm.rs @@ -174,7 +174,8 @@ fn compute_answers( .unwrap(); let iv = hex_field(block, "IV", params.iv_len, &ctx); let aad = hex_field(block, "Adata", params.aad_len, &ctx); - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); stats.counts += 1; match mode { @@ -495,7 +496,8 @@ fn kcmvp_gcm_self_check() { let iv = hex_decode(iv_hex, "self"); let aad = hex_decode(aad_hex, "self"); let pt = hex_decode(pt_hex, "self"); - let gcm = AES256GCM::new(&key); + let mut gcm = AES256GCM::default(); + gcm.init(&key); let mut ct = vec![0u8; pt.len()]; let mut tag = vec![0u8; 14]; gcm.encrypt_with_iv(&iv, &aad, &pt, &mut ct, &mut tag) From 1c8dbca41178cccb1f5397b463317b2d2e170307 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:01 +0900 Subject: [PATCH 57/65] =?UTF-8?q?chacha20=20=EC=9D=84=20Default=20?= =?UTF-8?q?=EC=99=80=20init=202=EB=8B=A8=EA=B3=84=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98=20-=20ChaCha20Poly1305?= =?UTF-8?q?=20ChaCha20=20Poly1305=20=EC=9D=98=20by-value=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9E=90=20=EC=A0=9C=EA=B1=B0=20-=20`Poly1305::finali?= =?UTF-8?q?ze`=20=EB=A5=BC=20`&mut=20self`=20=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20KAT=20=ED=95=98=EB=84=A4=EC=8A=A4=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=EC=B2=98=20=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chacha20/src/chacha20.rs | 47 ++++++++++++++++++------------- chacha20/src/lib.rs | 61 +++++++++++++++++++++++++++------------- chacha20/src/poly1305.rs | 38 +++++++++++++++---------- chacha20/tests/kat.rs | 21 +++++++++----- 4 files changed, 106 insertions(+), 61 deletions(-) diff --git a/chacha20/src/chacha20.rs b/chacha20/src/chacha20.rs index 75c5e5e..ea72998 100644 --- a/chacha20/src/chacha20.rs +++ b/chacha20/src/chacha20.rs @@ -83,6 +83,7 @@ fn chacha20_block(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u8; 64] { output } +#[derive(Default)] pub struct ChaCha20 { key: Secret<[u8; 32]>, nonce: [u8; 12], @@ -90,22 +91,25 @@ pub struct ChaCha20 { } impl ChaCha20 { - #[must_use] - pub fn new(key: &[u8; 32], nonce: &[u8; 12]) -> Self { - Self { - key: Secret::new(*key), - nonce: *nonce, - counter: 0, - } + /// 키와 논스를 제자리에서 설정하고 카운터를 0 으로 초기화합니다. + /// + /// # Arguments + /// - `key`: 32바이트 암호화 키 + /// - `nonce`: 12바이트 논스 + pub fn init(&mut self, key: &[u8; 32], nonce: &[u8; 12]) { + self.init_with_counter(key, nonce, 0); } - #[must_use] - pub fn new_with_counter(key: &[u8; 32], nonce: &[u8; 12], counter: u32) -> Self { - Self { - key: Secret::new(*key), - nonce: *nonce, - counter: u64::from(counter), - } + /// 키·논스·초기 카운터를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `key`: 32바이트 암호화 키 + /// - `nonce`: 12바이트 논스 + /// - `counter`: 초기 블록 카운터 + pub fn init_with_counter(&mut self, key: &[u8; 32], nonce: &[u8; 12], counter: u32) { + self.key.init_with(|k| *k = *key); + self.nonce = *nonce; + self.counter = u64::from(counter); } #[must_use] @@ -177,7 +181,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(ChaCha20::new_with_counter(&key, &nonce, 42)); + storage.write(ChaCha20::default()); + (*storage.as_mut_ptr()).init_with_counter(&key, &nonce, 42); let key_ptr = storage.assume_init_ref().key.expose().as_ptr(); let nonce_ptr = storage.assume_init_ref().nonce.as_ptr(); let ctr_ptr = &raw const (*storage.as_ptr()).counter; @@ -205,7 +210,8 @@ mod tests { #[test] #[should_panic(expected = "카운터 소진")] fn test_counter_exhaustion_keystream_block() { - let mut chacha = ChaCha20::new_with_counter(&[0u8; 32], &[0u8; 12], u32::MAX); + let mut chacha = ChaCha20::default(); + chacha.init_with_counter(&[0u8; 32], &[0u8; 12], u32::MAX); let _ = chacha.keystream_block(); let _ = chacha.keystream_block(); } @@ -214,7 +220,8 @@ mod tests { #[test] #[should_panic(expected = "카운터 소진")] fn test_counter_exhaustion_apply_keystream() { - let mut chacha = ChaCha20::new_with_counter(&[0u8; 32], &[0u8; 12], u32::MAX); + let mut chacha = ChaCha20::default(); + chacha.init_with_counter(&[0u8; 32], &[0u8; 12], u32::MAX); let mut data = [0u8; 128]; chacha.apply_keystream(&mut data); } @@ -223,7 +230,8 @@ mod tests { #[test] #[should_panic(expected = "블록 카운터 0")] fn test_poly1305_keygen_requires_counter_zero() { - let mut chacha = ChaCha20::new_with_counter(&[0u8; 32], &[0u8; 12], 1); + let mut chacha = ChaCha20::default(); + chacha.init_with_counter(&[0u8; 32], &[0u8; 12], 1); let _ = chacha.generate_poly1305_key(); } @@ -277,7 +285,8 @@ mod tests { 0x87, 0x4d, ]; - let mut chacha = ChaCha20::new_with_counter(&key, &nonce, 1); + let mut chacha = ChaCha20::default(); + chacha.init_with_counter(&key, &nonce, 1); let mut output = [0u8; 114]; output.copy_from_slice(plaintext); chacha.apply_keystream(&mut output); diff --git a/chacha20/src/lib.rs b/chacha20/src/lib.rs index 3f10d7c..9371e7c 100644 --- a/chacha20/src/lib.rs +++ b/chacha20/src/lib.rs @@ -24,16 +24,22 @@ pub enum Error { BufferTooSmall, } +#[derive(Default)] pub struct ChaCha20Poly1305 { key: Secret<[u8; 32]>, } impl ChaCha20Poly1305 { - #[must_use] - pub fn new(key: &[u8; 32]) -> Self { - Self { - key: Secret::new(*key), - } + /// 256비트 키를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `key`: 32바이트 암호화 키 + /// + /// # Security Note + /// by-value 생성자 대신 `Default` 인스턴스를 최종 위치에 둔 뒤 호출하면 + /// move 로 인한 키 스택 잔류가 없습니다. + pub fn init(&mut self, key: &[u8; 32]) { + self.key.init_with(|k| *k = *key); } pub fn encrypt( @@ -52,13 +58,15 @@ impl ChaCha20Poly1305 { "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" ); - let mut chacha = ChaCha20::new(self.key.expose(), nonce); + let mut chacha = ChaCha20::default(); + chacha.init(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); ciphertext[..plaintext.len()].copy_from_slice(plaintext); chacha.apply_keystream(&mut ciphertext[..plaintext.len()]); - let mut poly = Poly1305::new(&poly_key); + let mut poly = Poly1305::default(); + poly.init(&poly_key); poly.update(aad); if aad.len() % 16 != 0 { @@ -102,10 +110,12 @@ impl ChaCha20Poly1305 { "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" ); - let mut chacha = ChaCha20::new(self.key.expose(), nonce); + let mut chacha = ChaCha20::default(); + chacha.init(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); - let mut poly = Poly1305::new(&poly_key); + let mut poly = Poly1305::default(); + poly.init(&poly_key); poly.update(aad); if aad.len() % 16 != 0 { @@ -158,12 +168,14 @@ impl ChaCha20Poly1305 { "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" ); - let mut chacha = ChaCha20::new(self.key.expose(), nonce); + let mut chacha = ChaCha20::default(); + chacha.init(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); chacha.apply_keystream(&mut buffer[..plaintext_len]); - let mut poly = Poly1305::new(&poly_key); + let mut poly = Poly1305::default(); + poly.init(&poly_key); poly.update(aad); if aad.len() % 16 != 0 { @@ -206,10 +218,12 @@ impl ChaCha20Poly1305 { "RFC 8439 입력 길이 한계(2^32-1 블록) 초과로 카운터 재사용 발생" ); - let mut chacha = ChaCha20::new(self.key.expose(), nonce); + let mut chacha = ChaCha20::default(); + chacha.init(self.key.expose(), nonce); let mut poly_key = chacha.generate_poly1305_key(); - let mut poly = Poly1305::new(&poly_key); + let mut poly = Poly1305::default(); + poly.init(&poly_key); poly.update(aad); if aad.len() % 16 != 0 { @@ -268,7 +282,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(ChaCha20Poly1305::new(&key)); + storage.write(ChaCha20Poly1305::default()); + storage.assume_init_mut().init(&key); let key_ptr = storage.assume_init_ref().key.expose().as_ptr(); let pre = core::slice::from_raw_parts(key_ptr, 32); @@ -319,7 +334,8 @@ mod tests { 0x06, 0x91, ]; - let aead = ChaCha20Poly1305::new(&key); + let mut aead = ChaCha20Poly1305::default(); + aead.init(&key); let mut ciphertext = [0u8; 114]; let mut tag = [0u8; 16]; @@ -343,7 +359,8 @@ mod tests { let aad = b"additional data"; let plaintext = b"Hello, World!"; - let aead = ChaCha20Poly1305::new(&key); + let mut aead = ChaCha20Poly1305::default(); + aead.init(&key); let mut ciphertext = [0u8; 13]; let mut tag = [0u8; 16]; @@ -364,7 +381,8 @@ mod tests { let aad = b"additional data"; let plaintext = b"Hello, World!"; - let aead = ChaCha20Poly1305::new(&key); + let mut aead = ChaCha20Poly1305::default(); + aead.init(&key); let mut ciphertext = [0u8; 13]; let mut tag = [0u8; 16]; @@ -385,7 +403,8 @@ mod tests { let aad = b"additional data"; let plaintext = b"Hello, World!"; - let aead = ChaCha20Poly1305::new(&key); + let mut aead = ChaCha20Poly1305::default(); + aead.init(&key); let mut buffer = [0u8; 13]; buffer.copy_from_slice(plaintext); @@ -407,7 +426,8 @@ mod tests { let aad = b"only aad, no plaintext"; let plaintext: &[u8] = b""; - let aead = ChaCha20Poly1305::new(&key); + let mut aead = ChaCha20Poly1305::default(); + aead.init(&key); let mut ciphertext = [0u8; 0]; let mut tag = [0u8; 16]; @@ -426,7 +446,8 @@ mod tests { let aad: &[u8] = b""; let plaintext = b"plaintext only"; - let aead = ChaCha20Poly1305::new(&key); + let mut aead = ChaCha20Poly1305::default(); + aead.init(&key); let mut ciphertext = [0u8; 14]; let mut tag = [0u8; 16]; diff --git a/chacha20/src/poly1305.rs b/chacha20/src/poly1305.rs index de35da8..a3f7a6f 100644 --- a/chacha20/src/poly1305.rs +++ b/chacha20/src/poly1305.rs @@ -2,6 +2,7 @@ use constant_time::Choice; use constant_time::traits::{CtEqOps, CtSelOps}; use zeroize::Zeroize; +#[derive(Default)] pub struct Poly1305 { r: [u32; 5], h: [u32; 5], @@ -11,8 +12,11 @@ pub struct Poly1305 { } impl Poly1305 { - #[must_use] - pub fn new(key: &[u8; 32]) -> Self { + /// Poly1305 키를 제자리에서 설정하고 상태를 초기화합니다. + /// + /// # Arguments + /// - `key`: 32바이트 일회용 Poly1305 키 + pub fn init(&mut self, key: &[u8; 32]) { let mut clamped = [0u8; 16]; clamped.copy_from_slice(&key[..16]); @@ -42,13 +46,11 @@ impl Poly1305 { let mut pad2 = u32::from_le_bytes([key[24], key[25], key[26], key[27]]); let mut pad3 = u32::from_le_bytes([key[28], key[29], key[30], key[31]]); - let state = Self { - r: [r0, r1, r2, r3, r4], - h: [0, 0, 0, 0, 0], - pad: [pad0, pad1, pad2, pad3], - buffer: [0u8; 16], - buffer_len: 0, - }; + self.r = [r0, r1, r2, r3, r4]; + self.h = [0, 0, 0, 0, 0]; + self.pad = [pad0, pad1, pad2, pad3]; + self.buffer = [0u8; 16]; + self.buffer_len = 0; t0.zeroize(); t1.zeroize(); @@ -63,8 +65,6 @@ impl Poly1305 { pad1.zeroize(); pad2.zeroize(); pad3.zeroize(); - - state } fn block(&mut self, m: &[u8], hibit: u32) { @@ -212,8 +212,12 @@ impl Poly1305 { } } + /// 태그를 출력하고 모든 내부 상태를 제자리에서 소거합니다. + /// + /// # Security Note + /// `self` 를 이동 소비하지 않으므로 원본 슬롯에 사본이 남지 않습니다. #[must_use] - pub fn finalize(mut self) -> [u8; 16] { + pub fn finalize(&mut self) -> [u8; 16] { if self.buffer_len > 0 { self.buffer[self.buffer_len] = 0x01; for i in (self.buffer_len + 1)..16 { @@ -282,6 +286,7 @@ impl Poly1305 { self.r.zeroize(); self.pad.zeroize(); self.buffer.zeroize(); + self.buffer_len = 0; h0.zeroize(); h1.zeroize(); @@ -335,7 +340,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(Poly1305::new(&key)); + storage.write(Poly1305::default()); + (*storage.as_mut_ptr()).init(&key); // update 로 h 를 0 이 아닌 값으로 진행시킴 (*storage.as_mut_ptr()).update(&[0x33u8; 16]); @@ -379,7 +385,8 @@ mod tests { 0x27, 0xa9, ]; - let mut poly = Poly1305::new(&key); + let mut poly = Poly1305::default(); + poly.init(&key); poly.update(msg); let tag = poly.finalize(); @@ -394,7 +401,8 @@ mod tests { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x10, ]); - let poly = Poly1305::new(&key); + let mut poly = Poly1305::default(); + poly.init(&key); let tag = poly.finalize(); assert_eq!(&tag[..], &key[16..], "빈 메시지 태그가 s 와 다름"); } diff --git a/chacha20/tests/kat.rs b/chacha20/tests/kat.rs index 70afad9..091f6f1 100644 --- a/chacha20/tests/kat.rs +++ b/chacha20/tests/kat.rs @@ -71,7 +71,8 @@ fn kat_a1_chacha20_block() { for (key, nonce, counter, keystream) in cases { let key: [u8; 32] = unhex(key).try_into().unwrap(); let nonce: [u8; 12] = unhex(nonce).try_into().unwrap(); - let mut chacha = ChaCha20Core::new_with_counter(&key, &nonce, *counter); + let mut chacha = ChaCha20Core::default(); + chacha.init_with_counter(&key, &nonce, *counter); let block = chacha.keystream_block(); assert_eq!(&block[..], &unhex(keystream)[..], "키스트림 불일치"); } @@ -107,7 +108,8 @@ fn kat_a2_chacha20_encrypt() { let key: [u8; 32] = unhex(key).try_into().unwrap(); let nonce: [u8; 12] = unhex(nonce).try_into().unwrap(); let mut buf = unhex(plaintext); - let mut chacha = ChaCha20Core::new_with_counter(&key, &nonce, *counter); + let mut chacha = ChaCha20Core::default(); + chacha.init_with_counter(&key, &nonce, *counter); chacha.apply_keystream(&mut buf); assert_eq!(&buf[..], &unhex(ciphertext)[..], "암호문 불일치"); } @@ -175,7 +177,8 @@ fn kat_a3_poly1305() { ]; for (otk, text, tag) in cases { let otk: [u8; 32] = unhex(otk).try_into().unwrap(); - let mut poly = Poly1305Core::new(&otk); + let mut poly = Poly1305Core::default(); + poly.init(&otk); poly.update(&unhex(text)); let computed = poly.finalize(); assert_eq!(&computed[..], &unhex(tag)[..], "태그 불일치"); @@ -205,7 +208,8 @@ fn kat_a4_poly1305_keygen() { for (key, nonce, otk) in cases { let key: [u8; 32] = unhex(key).try_into().unwrap(); let nonce: [u8; 12] = unhex(nonce).try_into().unwrap(); - let mut chacha = ChaCha20Core::new(&key, &nonce); + let mut chacha = ChaCha20Core::default(); + chacha.init(&key, &nonce); let poly_key = chacha.generate_poly1305_key(); assert_eq!(&poly_key[..], &unhex(otk)[..], "Poly1305 일회용 키 불일치"); } @@ -228,7 +232,8 @@ fn kat_a5_aead_decrypt() { "496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d617920626520757064617465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d656e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f2075736520496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f7220746f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67726573732e2fe2809d", ); - let aead = ChaCha20Poly1305::new(&key); + let mut aead = ChaCha20Poly1305::default(); + aead.init(&key); let mut plaintext = vec![0u8; ciphertext.len()]; aead.decrypt(&nonce, &aad, &ciphertext, &tag, &mut plaintext) @@ -263,7 +268,8 @@ fn kat_a3_poly1305_chunked_update() { ); let expected_tag = unhex("36e5f6b5c5e06070f0efca96227a863e"); - let mut poly = Poly1305Core::new(&otk); + let mut poly = Poly1305Core::default(); + poly.init(&otk); let mut offset = 0; for chunk in [1usize, 15, 17, 100, 3, 64] { poly.update(&text[offset..offset + chunk]); @@ -285,7 +291,8 @@ fn kat_a2_chacha20_chunked_keystream() { "416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f", ); - let mut chacha = ChaCha20Core::new_with_counter(&key, &nonce, 1); + let mut chacha = ChaCha20Core::default(); + chacha.init_with_counter(&key, &nonce, 1); let (head, tail) = buf.split_at_mut(320); chacha.apply_keystream(&mut head[..64]); chacha.apply_keystream(&mut head[64..]); From 0b7ff9dd6e570dc9e3cda208dd618fb64ca88c6d Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:12 +0900 Subject: [PATCH 58/65] =?UTF-8?q?x25519=20x448=20=EA=B3=B5=EC=9C=A0?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EC=9D=84=20=EC=A0=9C=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98=20-?= =?UTF-8?q?=20`diffie=5Fhellman=5Finto`=20=EB=A1=9C=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EB=91=94=20=EB=B2=84=ED=8D=BC=EC=97=90=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20=EA=B8=B0=EB=A1=9D=20-=20=EB=AA=BD?= =?UTF-8?q?=EA=B3=A0=EB=A9=94=EB=A6=AC=20=EC=82=AC=EB=8B=A4=EB=A6=AC?= =?UTF-8?q?=EC=99=80=20`to=5Fbytes=5Finto`=20=EB=A5=BC=20out=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-=20`generate=5Fkeypair`=20?= =?UTF-8?q?=EB=8F=84=20=EB=B9=84=EB=B0=80=ED=82=A4=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- x25519/src/field.rs | 12 ++-- x25519/src/lib.rs | 133 ++++++++++++++++++++++++++++-------------- x448/src/field.rs | 9 ++- x448/src/lib.rs | 139 +++++++++++++++++++++++++++++++------------- 4 files changed, 198 insertions(+), 95 deletions(-) diff --git a/x25519/src/field.rs b/x25519/src/field.rs index 18f0fed..dfb2488 100644 --- a/x25519/src/field.rs +++ b/x25519/src/field.rs @@ -46,16 +46,15 @@ impl FieldElement { FieldElement(limbs) } - pub fn to_bytes(&self) -> [u8; 32] { - let t = self.reduce(); - let mut bytes = [0u8; 32]; + pub fn to_bytes_into(&self, out: &mut [u8; 32]) { + let mut t = self.reduce(); let mut acc: u128 = t.0[0] as u128; acc |= (t.0[1] as u128) << 51; acc |= (t.0[2] as u128) << 102; for i in 0..16 { - bytes[i] = (acc >> (i * 8)) as u8; + out[i] = (acc >> (i * 8)) as u8; } acc = (t.0[2] >> 26) as u128; @@ -63,10 +62,11 @@ impl FieldElement { acc |= (t.0[4] as u128) << 76; for i in 0..16 { - bytes[16 + i] = (acc >> (i * 8)) as u8; + out[16 + i] = (acc >> (i * 8)) as u8; } - bytes + t.zeroize(); + acc.zeroize(); } #[inline] diff --git a/x25519/src/lib.rs b/x25519/src/lib.rs index a626983..a5b3134 100644 --- a/x25519/src/lib.rs +++ b/x25519/src/lib.rs @@ -12,17 +12,21 @@ //! ```rust,ignore //! use x25519::{SecretKey, PublicKey}; //! -//! // Alice 키쌍 생성 -//! let alice_secret = SecretKey::from_bytes(alice_random_bytes); +//! // Alice 키쌍 생성 (제자리 초기화로 move 잔류 방지) +//! let mut alice_secret = SecretKey::default(); +//! alice_secret.init(&alice_random_bytes); //! let alice_public = alice_secret.public_key(); //! //! // Bob 키쌍 생성 -//! let bob_secret = SecretKey::from_bytes(bob_random_bytes); +//! let mut bob_secret = SecretKey::default(); +//! bob_secret.init(&bob_random_bytes); //! let bob_public = bob_secret.public_key(); //! -//! // 공유 비밀 계산 -//! let alice_shared = alice_secret.diffie_hellman(&bob_public); -//! let bob_shared = bob_secret.diffie_hellman(&alice_public); +//! // 공유 비밀 계산 (호출자가 둔 최종 위치에 직접 기록) +//! let mut alice_shared = SharedSecret::default(); +//! let mut bob_shared = SharedSecret::default(); +//! alice_secret.diffie_hellman_into(&bob_public, &mut alice_shared).unwrap(); +//! bob_secret.diffie_hellman_into(&alice_public, &mut bob_shared).unwrap(); //! //! assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); //! ``` @@ -45,11 +49,21 @@ pub enum X25519Error { LowOrderPoint, } +#[derive(Default)] pub struct SecretKey(Secret<[u8; 32]>); impl SecretKey { - pub fn from_bytes(bytes: [u8; 32]) -> Self { - SecretKey(Secret::new(bytes)) + /// 비밀키 바이트를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `bytes`: 32바이트 비밀키 재료 + /// + /// # Security Note + /// by-value 생성자 대신 `Default` 인스턴스를 최종 위치에 둔 뒤 호출하면 + /// move 로 인한 비밀키 스택 잔류가 없습니다. 입력 버퍼는 호출자가 + /// 사용 후 직접 소거해야 합니다. + pub fn init(&mut self, bytes: &[u8; 32]) { + self.0.init_with(|k| *k = *bytes); } pub fn as_bytes(&self) -> &[u8; 32] { @@ -61,14 +75,26 @@ impl SecretKey { PublicKey(public_bytes) } - pub fn diffie_hellman(&self, their_public: &PublicKey) -> Result { - let mut shared = x25519(self.0.expose(), &their_public.0); - let result = SharedSecret(Secret::new(shared)); - shared.zeroize(); - if result.is_zero() { + /// 공유 비밀을 호출자가 둔 `out` 에 제자리에서 계산합니다. + /// + /// # Arguments + /// - `their_public`: 상대 공개키 + /// - `out`: 공유 비밀이 기록될 최종 위치 + /// + /// # Errors + /// - `X25519Error::LowOrderPoint`: 결과가 all-zero 인 저차수 점. `out` 은 + /// 0 으로 남습니다. + pub fn diffie_hellman_into( + &self, + their_public: &PublicKey, + out: &mut SharedSecret, + ) -> Result<(), X25519Error> { + out.0 + .init_with(|s| x25519_into(self.0.expose(), &their_public.0, s)); + if out.is_zero() { Err(X25519Error::LowOrderPoint) } else { - Ok(result) + Ok(()) } } } @@ -86,6 +112,7 @@ impl PublicKey { } } +#[derive(Default)] pub struct SharedSecret(Secret<[u8; 32]>); impl SharedSecret { @@ -113,13 +140,19 @@ fn x25519_base(k: &[u8; 32]) -> [u8; 32] { } fn x25519(k: &[u8; 32], u: &[u8; 32]) -> [u8; 32] { - let mut scalar = Secret::new(*k); - clamp_scalar(scalar.expose_mut()); - montgomery_ladder(scalar.expose(), u) - // scalar 는 스코프 종료 시 Secret::Drop 으로 자동 소거 + let mut out = [0u8; 32]; + x25519_into(k, u, &mut out); + out } -fn montgomery_ladder(k: &[u8; 32], u: &[u8; 32]) -> [u8; 32] { +fn x25519_into(k: &[u8; 32], u: &[u8; 32], out: &mut [u8; 32]) { + let mut scalar = *k; + clamp_scalar(&mut scalar); + montgomery_ladder_into(&scalar, u, out); + scalar.zeroize(); +} + +fn montgomery_ladder_into(k: &[u8; 32], u: &[u8; 32], out: &mut [u8; 32]) { let mut u_coord = FieldElement::from_bytes(u); let mut x_1 = u_coord; @@ -178,7 +211,7 @@ fn montgomery_ladder(k: &[u8; 32], u: &[u8; 32]) -> [u8; 32] { let mut z_2_inv = z_2.invert(); let mut result = x_2 * z_2_inv; - let bytes = result.to_bytes(); + result.to_bytes_into(out); // 민감 중간값 명시적 소거 u_coord.zeroize(); @@ -202,8 +235,6 @@ fn montgomery_ladder(k: &[u8; 32], u: &[u8; 32]) -> [u8; 32] { sum.zeroize(); diff.zeroize(); a24_e.zeroize(); - - bytes } fn mul_by_a24(e: FieldElement) -> FieldElement { @@ -243,13 +274,14 @@ fn mul_by_a24(e: FieldElement) -> FieldElement { FieldElement([c0 as u64, c1 as u64, c2 as u64, c3 as u64, c4 as u64]) } -pub fn generate_keypair(mut rng: R) -> (SecretKey, PublicKey) { - let mut secret_bytes = [0u8; 32]; - rng(&mut secret_bytes); - let secret = SecretKey::from_bytes(secret_bytes); - secret_bytes.zeroize(); - let public = secret.public_key(); - (secret, public) +/// 호출자가 둔 `secret` 에 비밀키를 제자리에서 생성하고 공개키를 반환합니다. +/// +/// # Arguments +/// - `rng`: 32바이트를 채우는 엔트로피 공급 클로저 +/// - `secret`: 비밀키가 기록될 최종 위치 +pub fn generate_keypair(mut rng: R, secret: &mut SecretKey) -> PublicKey { + secret.0.init_with(|k| rng(k)); + secret.public_key() } pub fn is_contributory(shared: &SharedSecret) -> Choice { @@ -272,7 +304,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(SecretKey::from_bytes([pattern; 32])); + storage.write(SecretKey::default()); + storage.assume_init_mut().init(&[pattern; 32]); let ptr = storage.assume_init_ref().as_bytes().as_ptr(); let pre = core::slice::from_raw_parts(ptr, 32); @@ -292,18 +325,19 @@ mod tests { /// SharedSecret 의 Drop 으로 내부 32바이트가 0 으로 소거되는지 검증. #[test] fn test_shared_secret_zeroize_on_drop() { - let alice_secret = SecretKey::from_bytes([0x11; 32]); - let bob_secret = SecretKey::from_bytes([0x22; 32]); + let mut alice_secret = SecretKey::default(); + alice_secret.init(&[0x11; 32]); + let mut bob_secret = SecretKey::default(); + bob_secret.init(&[0x22; 32]); let bob_pk = bob_secret.public_key(); let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write( - alice_secret - .diffie_hellman(&bob_pk) - .expect("정상 공개키는 Ok 여야 함"), - ); + storage.write(SharedSecret::default()); + alice_secret + .diffie_hellman_into(&bob_pk, storage.assume_init_mut()) + .expect("정상 공개키는 Ok 여야 함"); let ptr = storage.assume_init_ref().as_bytes().as_ptr(); let pre = core::slice::from_raw_parts(ptr, 32); @@ -343,7 +377,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(SecretKey::from_bytes(input)); + storage.write(SecretKey::default()); + storage.assume_init_mut().init(&input); let internal_ptr = storage.assume_init_ref().as_bytes().as_ptr(); assert_ne!( internal_ptr, @@ -433,14 +468,22 @@ mod tests { 0xff, 0x88, 0xe0, 0xeb, ]; - let alice_sk = SecretKey::from_bytes(alice_secret); - let bob_sk = SecretKey::from_bytes(bob_secret); + let mut alice_sk = SecretKey::default(); + alice_sk.init(&alice_secret); + let mut bob_sk = SecretKey::default(); + bob_sk.init(&bob_secret); let alice_pk = alice_sk.public_key(); let bob_pk = bob_sk.public_key(); - let alice_shared = alice_sk.diffie_hellman(&bob_pk).unwrap(); - let bob_shared = bob_sk.diffie_hellman(&alice_pk).unwrap(); + let mut alice_shared = SharedSecret::default(); + let mut bob_shared = SharedSecret::default(); + alice_sk + .diffie_hellman_into(&bob_pk, &mut alice_shared) + .unwrap(); + bob_sk + .diffie_hellman_into(&alice_pk, &mut bob_shared) + .unwrap(); assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); @@ -492,7 +535,8 @@ mod tests { #[test] fn test_low_order_point_rejection() { - let secret = SecretKey::from_bytes([1u8; 32]); + let mut secret = SecretKey::default(); + secret.init(&[1u8; 32]); let low_order_points: [[u8; 32]; 5] = [ [0u8; 32], [ @@ -518,7 +562,8 @@ mod tests { for low_order in &low_order_points { let public = PublicKey::from_bytes(*low_order); - let result = secret.diffie_hellman(&public); + let mut shared = SharedSecret::default(); + let result = secret.diffie_hellman_into(&public, &mut shared); assert!( matches!(result, Err(X25519Error::LowOrderPoint)), "low-order point should be rejected" diff --git a/x448/src/field.rs b/x448/src/field.rs index 3753a1f..54ea3c1 100644 --- a/x448/src/field.rs +++ b/x448/src/field.rs @@ -51,16 +51,15 @@ impl FieldElement { FieldElement(limbs) } - pub fn to_bytes(&self) -> [u8; 56] { - let t = self.reduce(); - let mut bytes = [0u8; 56]; + pub fn to_bytes_into(&self, out: &mut [u8; 56]) { + let mut t = self.reduce(); for i in 0..LIMBS { let offset = i * 7; for j in 0..7 { - bytes[offset + j] = ((t.0[i] >> (j * 8)) & 0xff) as u8; + out[offset + j] = ((t.0[i] >> (j * 8)) & 0xff) as u8; } } - bytes + t.zeroize(); } fn weak_reduce(&self) -> Self { diff --git a/x448/src/lib.rs b/x448/src/lib.rs index d39e240..5bd050a 100644 --- a/x448/src/lib.rs +++ b/x448/src/lib.rs @@ -13,17 +13,21 @@ //! ```rust,ignore //! use x448::{SecretKey, PublicKey}; //! -//! // Alice 키쌍 생성 -//! let alice_secret = SecretKey::from_bytes(alice_random_bytes); +//! // Alice 키쌍 생성 (제자리 초기화로 move 잔류 방지) +//! let mut alice_secret = SecretKey::default(); +//! alice_secret.init(&alice_random_bytes); //! let alice_public = alice_secret.public_key(); //! //! // Bob 키쌍 생성 -//! let bob_secret = SecretKey::from_bytes(bob_random_bytes); +//! let mut bob_secret = SecretKey::default(); +//! bob_secret.init(&bob_random_bytes); //! let bob_public = bob_secret.public_key(); //! -//! // 공유 비밀 계산 -//! let alice_shared = alice_secret.diffie_hellman(&bob_public).unwrap(); -//! let bob_shared = bob_secret.diffie_hellman(&alice_public).unwrap(); +//! // 공유 비밀 계산 (호출자가 둔 최종 위치에 직접 기록) +//! let mut alice_shared = SharedSecret::default(); +//! let mut bob_shared = SharedSecret::default(); +//! alice_secret.diffie_hellman_into(&bob_public, &mut alice_shared).unwrap(); +//! bob_secret.diffie_hellman_into(&alice_public, &mut bob_shared).unwrap(); //! //! assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); //! ``` @@ -49,9 +53,24 @@ pub enum X448Error { pub struct SecretKey(Secret<[u8; 56]>); +impl Default for SecretKey { + fn default() -> Self { + SecretKey(Secret::new([0u8; 56])) + } +} + impl SecretKey { - pub fn from_bytes(bytes: [u8; 56]) -> Self { - SecretKey(Secret::new(bytes)) + /// 비밀키 바이트를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `bytes`: 56바이트 비밀키 재료 + /// + /// # Security Note + /// by-value 생성자 대신 `Default` 인스턴스를 최종 위치에 둔 뒤 호출하면 + /// move 로 인한 비밀키 스택 잔류가 없습니다. 입력 버퍼는 호출자가 + /// 사용 후 직접 소거해야 합니다. + pub fn init(&mut self, bytes: &[u8; 56]) { + self.0.init_with(|k| *k = *bytes); } pub fn as_bytes(&self) -> &[u8; 56] { @@ -63,14 +82,26 @@ impl SecretKey { PublicKey(public_bytes) } - pub fn diffie_hellman(&self, their_public: &PublicKey) -> Result { - let mut shared = x448(self.0.expose(), &their_public.0); - let result = SharedSecret(Secret::new(shared)); - shared.zeroize(); - if result.is_zero() { + /// 공유 비밀을 호출자가 둔 `out` 에 제자리에서 계산합니다. + /// + /// # Arguments + /// - `their_public`: 상대 공개키 + /// - `out`: 공유 비밀이 기록될 최종 위치 + /// + /// # Errors + /// - `X448Error::LowOrderPoint`: 결과가 all-zero 인 저차수 점. `out` 은 + /// 0 으로 남습니다. + pub fn diffie_hellman_into( + &self, + their_public: &PublicKey, + out: &mut SharedSecret, + ) -> Result<(), X448Error> { + out.0 + .init_with(|s| x448_into(self.0.expose(), &their_public.0, s)); + if out.is_zero() { Err(X448Error::LowOrderPoint) } else { - Ok(result) + Ok(()) } } } @@ -89,6 +120,12 @@ impl PublicKey { pub struct SharedSecret(Secret<[u8; 56]>); +impl Default for SharedSecret { + fn default() -> Self { + SharedSecret(Secret::new([0u8; 56])) + } +} + impl SharedSecret { pub fn as_bytes(&self) -> &[u8; 56] { self.0.expose() @@ -113,13 +150,19 @@ fn x448_base(k: &[u8; 56]) -> [u8; 56] { } fn x448(k: &[u8; 56], u: &[u8; 56]) -> [u8; 56] { - let mut scalar = Secret::new(*k); - clamp_scalar(scalar.expose_mut()); - montgomery_ladder(scalar.expose(), u) - // scalar 는 스코프 종료 시 Secret::Drop 으로 자동 소거 + let mut out = [0u8; 56]; + x448_into(k, u, &mut out); + out } -fn montgomery_ladder(k: &[u8; 56], u: &[u8; 56]) -> [u8; 56] { +fn x448_into(k: &[u8; 56], u: &[u8; 56], out: &mut [u8; 56]) { + let mut scalar = *k; + clamp_scalar(&mut scalar); + montgomery_ladder_into(&scalar, u, out); + scalar.zeroize(); +} + +fn montgomery_ladder_into(k: &[u8; 56], u: &[u8; 56], out: &mut [u8; 56]) { let mut u_coord = FieldElement::from_bytes(u); let mut x_1 = u_coord; @@ -178,7 +221,7 @@ fn montgomery_ladder(k: &[u8; 56], u: &[u8; 56]) -> [u8; 56] { let mut z_2_inv = z_2.invert(); let mut result = x_2 * z_2_inv; - let bytes = result.to_bytes(); + result.to_bytes_into(out); // 민감 중간값 명시적 소거 u_coord.zeroize(); @@ -202,8 +245,6 @@ fn montgomery_ladder(k: &[u8; 56], u: &[u8; 56]) -> [u8; 56] { sum.zeroize(); diff.zeroize(); a24_e.zeroize(); - - bytes } fn mul_by_a24(e: FieldElement) -> FieldElement { @@ -265,13 +306,14 @@ fn mul_by_a24(e: FieldElement) -> FieldElement { out } -pub fn generate_keypair(mut rng: R) -> (SecretKey, PublicKey) { - let mut secret_bytes = [0u8; 56]; - rng(&mut secret_bytes); - let secret = SecretKey::from_bytes(secret_bytes); - secret_bytes.zeroize(); - let public = secret.public_key(); - (secret, public) +/// 호출자가 둔 `secret` 에 비밀키를 제자리에서 생성하고 공개키를 반환합니다. +/// +/// # Arguments +/// - `rng`: 56바이트를 채우는 엔트로피 공급 클로저 +/// - `secret`: 비밀키가 기록될 최종 위치 +pub fn generate_keypair(mut rng: R, secret: &mut SecretKey) -> PublicKey { + secret.0.init_with(|k| rng(k)); + secret.public_key() } pub fn is_contributory(shared: &SharedSecret) -> Choice { @@ -294,7 +336,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(SecretKey::from_bytes([pattern; 56])); + storage.write(SecretKey::default()); + storage.assume_init_mut().init(&[pattern; 56]); let ptr = storage.assume_init_ref().as_bytes().as_ptr(); let pre = core::slice::from_raw_parts(ptr, 56); @@ -314,14 +357,19 @@ mod tests { /// SharedSecret 의 Drop 으로 내부 56바이트가 0 으로 소거되는지 검증. #[test] fn test_shared_secret_zeroize_on_drop() { - let alice_secret = SecretKey::from_bytes([0x11; 56]); - let bob_secret = SecretKey::from_bytes([0x22; 56]); + let mut alice_secret = SecretKey::default(); + alice_secret.init(&[0x11; 56]); + let mut bob_secret = SecretKey::default(); + bob_secret.init(&[0x22; 56]); let bob_pk = bob_secret.public_key(); let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(alice_secret.diffie_hellman(&bob_pk).unwrap()); + storage.write(SharedSecret::default()); + alice_secret + .diffie_hellman_into(&bob_pk, storage.assume_init_mut()) + .unwrap(); let ptr = storage.assume_init_ref().as_bytes().as_ptr(); let pre = core::slice::from_raw_parts(ptr, 56); @@ -360,7 +408,8 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - storage.write(SecretKey::from_bytes(input)); + storage.write(SecretKey::default()); + storage.assume_init_mut().init(&input); let internal_ptr = storage.assume_init_ref().as_bytes().as_ptr(); assert_ne!( internal_ptr, @@ -459,14 +508,22 @@ mod tests { 0x9c, 0x92, 0x1b, 0x09, 0xd1, 0xb0, 0x36, 0x6f, 0x10, 0xb6, 0x51, 0x73, 0x99, 0x2d, ]; - let alice_sk = SecretKey::from_bytes(alice_secret); - let bob_sk = SecretKey::from_bytes(bob_secret); + let mut alice_sk = SecretKey::default(); + alice_sk.init(&alice_secret); + let mut bob_sk = SecretKey::default(); + bob_sk.init(&bob_secret); let alice_pk = alice_sk.public_key(); let bob_pk = bob_sk.public_key(); - let alice_shared = alice_sk.diffie_hellman(&bob_pk).unwrap(); - let bob_shared = bob_sk.diffie_hellman(&alice_pk).unwrap(); + let mut alice_shared = SharedSecret::default(); + let mut bob_shared = SharedSecret::default(); + alice_sk + .diffie_hellman_into(&bob_pk, &mut alice_shared) + .unwrap(); + bob_sk + .diffie_hellman_into(&alice_pk, &mut bob_shared) + .unwrap(); assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes()); @@ -521,7 +578,8 @@ mod tests { #[test] fn test_low_order_point_rejection() { - let secret = SecretKey::from_bytes([1u8; 56]); + let mut secret = SecretKey::default(); + secret.init(&[1u8; 56]); let mut u_one = [0u8; 56]; u_one[0] = 1; @@ -538,7 +596,8 @@ mod tests { for low_order in &low_order_points { let public = PublicKey::from_bytes(*low_order); - let result = secret.diffie_hellman(&public); + let mut shared = SharedSecret::default(); + let result = secret.diffie_hellman_into(&public, &mut shared); assert!( matches!(result, Err(X448Error::LowOrderPoint)), "low-order point should be rejected" From 60be8c101c10743b59aad7bfd0b6569cf10dbde6 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:12 +0900 Subject: [PATCH 59/65] =?UTF-8?q?ed25519=20ed448=20=ED=82=A4=EC=8C=8D?= =?UTF-8?q?=EC=9D=84=20=EC=A0=9C=EC=9E=90=EB=A6=AC=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=EB=A1=9C=20=EC=A0=84=ED=99=98=20-=20SecretKey=20?= =?UTF-8?q?=EC=99=80=20Keypair=20=EC=97=90=20`init`=20=EB=8F=84=EC=9E=85?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20by-value=20=EC=83=9D=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20-=20`expand`=20=EB=A5=BC=20`expand=5Finto`?= =?UTF-8?q?=20=EB=A1=9C=20=EB=B0=94=EA=BF=94=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=ED=82=A4=20=EC=9D=B4=EB=8F=99=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20-=20=EB=B9=84=EB=B0=80=20=EC=82=AC=EB=B3=B8?= =?UTF-8?q?=EC=9D=84=20=EB=A7=8C=EB=93=9C=EB=8A=94=20`Clone=20for=20Secret?= =?UTF-8?q?Key`=20=EC=82=AD=EC=A0=9C=20-=20=EB=8D=B0=EB=AA=AC=20dispatcher?= =?UTF-8?q?=20=ED=98=B8=EC=B6=9C=EC=B2=98=20=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ed25519/src/lib.rs | 89 +++++++++++++++++----------- ed448/src/lib.rs | 101 ++++++++++++++++++++++---------- elib-k0d-core/src/dispatcher.rs | 10 +++- 3 files changed, 132 insertions(+), 68 deletions(-) diff --git a/ed25519/src/lib.rs b/ed25519/src/lib.rs index 4d1b086..534ec35 100644 --- a/ed25519/src/lib.rs +++ b/ed25519/src/lib.rs @@ -11,8 +11,9 @@ //! ```rust,ignore //! use ed25519::{SecretKey, PublicKey, sign, verify}; //! -//! // 키 생성 -//! let secret = SecretKey::from_bytes(&seed); +//! // 키 생성 (제자리 초기화로 move 잔류 방지) +//! let mut secret = SecretKey::default(); +//! secret.init(&seed); //! let public = PublicKey::from(&secret); //! //! // 서명 @@ -59,13 +60,22 @@ pub enum Ed25519Error { /// Ed25519 비밀키 (32바이트 시드)입니다. /// /// Drop 시 내부 데이터가 `Secret` 의 Drop 으로 자동 소거됩니다. +#[derive(Default)] pub struct SecretKey(Secret<[u8; SECRET_KEY_LENGTH]>); impl SecretKey { - /// 32바이트 시드에서 비밀키를 생성합니다. + /// 32바이트 시드를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `bytes`: 32바이트 시드 + /// + /// # Security Note + /// by-value 생성자 대신 `Default` 인스턴스를 최종 위치에 둔 뒤 호출하면 + /// move 로 인한 시드 스택 잔류가 없습니다. 입력 버퍼는 호출자가 + /// 사용 후 직접 소거해야 합니다. #[inline] - pub fn from_bytes(bytes: &[u8; SECRET_KEY_LENGTH]) -> Self { - SecretKey(Secret::new(*bytes)) + pub fn init(&mut self, bytes: &[u8; SECRET_KEY_LENGTH]) { + self.0.init_with(|k| *k = *bytes); } /// 바이트 배열 참조를 반환합니다. @@ -74,17 +84,17 @@ impl SecretKey { self.0.expose() } - /// 확장 비밀키를 계산합니다. - fn expand(&self) -> ExpandedSecretKey { + /// 확장 비밀키를 호출자가 둔 `out` 에 제자리에서 계산합니다. + fn expand_into(&self, out: &mut ExpandedSecretKey) { let mut h = SHA512::new(); h.update(self.0.expose()); let hash = h.finalize(); let hash_bytes = hash.as_bytes(); let mut lower = Secret::new([0u8; 32]); - let mut upper = Secret::new([0u8; 32]); lower.expose_mut().copy_from_slice(&hash_bytes[..32]); - upper.expose_mut().copy_from_slice(&hash_bytes[32..]); + out.nonce + .init_with(|n| n.copy_from_slice(&hash_bytes[32..])); // RFC 8032: 비트 클램핑 { @@ -94,18 +104,8 @@ impl SecretKey { lo[31] |= 64; // 254번째 비트 설정 } - let scalar = Scalar::from_bytes(*lower.expose()); + out.scalar = Scalar::from_bytes(*lower.expose()); // lower 는 스코프 종료 시 Secret::Drop 으로 자동 소거 - ExpandedSecretKey { - scalar, - nonce: upper, - } - } -} - -impl Clone for SecretKey { - fn clone(&self) -> Self { - SecretKey(Secret::new(*self.0.expose())) } } @@ -115,6 +115,15 @@ struct ExpandedSecretKey { nonce: Secret<[u8; 32]>, } +impl Default for ExpandedSecretKey { + fn default() -> Self { + Self { + scalar: Scalar::zero(), + nonce: Secret::new([0u8; 32]), + } + } +} + impl Drop for ExpandedSecretKey { fn drop(&mut self) { // scalar 는 Copy 타입이므로 명시적 zeroize 호출 @@ -124,7 +133,7 @@ impl Drop for ExpandedSecretKey { } /// Ed25519 공개키 (32바이트)입니다. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, Default)] pub struct PublicKey([u8; PUBLIC_KEY_LENGTH]); impl PublicKey { @@ -149,7 +158,8 @@ impl PublicKey { impl From<&SecretKey> for PublicKey { /// 비밀키에서 공개키를 유도합니다. fn from(secret: &SecretKey) -> Self { - let expanded = secret.expand(); + let mut expanded = ExpandedSecretKey::default(); + secret.expand_into(&mut expanded); let point = EdwardsPoint::basepoint_mul(&expanded.scalar); PublicKey(point.to_bytes()) } @@ -195,7 +205,8 @@ impl Signature { /// 비밀 nonce r, 비밀 스칼라 a, 그리고 모든 중간 해시 출력은 함수 종료 전 /// `Secret` Drop 또는 명시적 `zeroize()` 호출로 소거됩니다. pub fn sign(message: &[u8], secret_key: &SecretKey) -> Signature { - let expanded = secret_key.expand(); + let mut expanded = ExpandedSecretKey::default(); + secret_key.expand_into(&mut expanded); // 공개키 A 는 이미 계산한 확장 스칼라에서 직접 유도 (expand 중복 호출 제거) let public_key = PublicKey(EdwardsPoint::basepoint_mul(&expanded.scalar).to_bytes()); @@ -292,17 +303,24 @@ pub fn verify( } /// 키페어 (비밀키 + 공개키)입니다. +#[derive(Default)] pub struct Keypair { pub secret: SecretKey, pub public: PublicKey, } impl Keypair { - /// 32바이트 시드에서 키페어를 생성합니다. - pub fn from_seed(seed: &[u8; 32]) -> Self { - let secret = SecretKey::from_bytes(seed); - let public = PublicKey::from(&secret); - Keypair { secret, public } + /// 32바이트 시드로 키페어를 제자리에서 초기화합니다. + /// + /// # Arguments + /// - `seed`: 32바이트 시드 + /// + /// # Security Note + /// `Default` 인스턴스를 최종 위치에 둔 뒤 호출하면 move 로 인한 + /// 비밀키 스택 잔류가 없습니다. + pub fn init(&mut self, seed: &[u8; 32]) { + self.secret.init(seed); + self.public = PublicKey::from(&self.secret); } /// 메시지에 서명합니다. @@ -451,7 +469,8 @@ mod tests { #[test] fn test_sign_verify_roundtrip() { let seed = [42u8; 32]; - let keypair = Keypair::from_seed(&seed); + let mut keypair = Keypair::default(); + keypair.init(&seed); let message = b"test message"; let signature = keypair.sign(message); @@ -462,7 +481,8 @@ mod tests { #[test] fn test_wrong_message() { let seed = [42u8; 32]; - let keypair = Keypair::from_seed(&seed); + let mut keypair = Keypair::default(); + keypair.init(&seed); let message = b"test message"; let wrong_message = b"wrong message"; @@ -474,8 +494,10 @@ mod tests { fn test_wrong_key() { let seed1 = [1u8; 32]; let seed2 = [2u8; 32]; - let keypair1 = Keypair::from_seed(&seed1); - let keypair2 = Keypair::from_seed(&seed2); + let mut keypair1 = Keypair::default(); + keypair1.init(&seed1); + let mut keypair2 = Keypair::default(); + keypair2.init(&seed2); let message = b"test message"; let signature = keypair1.sign(message); @@ -498,7 +520,8 @@ mod tests { let exp_pk = decode_hex::<32>(pk_h); let exp_sig = decode_hex::<64>(sig_h); - let sk = SecretKey::from_bytes(&seed); + let mut sk = SecretKey::default(); + sk.init(&seed); let pk = PublicKey::from(&sk); assert_eq!(pk.as_bytes(), &exp_pk, "공개키 KAT 불일치"); diff --git a/ed448/src/lib.rs b/ed448/src/lib.rs index e3796ee..c3f3182 100644 --- a/ed448/src/lib.rs +++ b/ed448/src/lib.rs @@ -11,8 +11,9 @@ //! ```rust,ignore //! use ed448::{SecretKey, PublicKey, sign, verify}; //! -//! // 키 생성 -//! let secret = SecretKey::from_bytes(&seed); +//! // 키 생성 (제자리 초기화로 move 잔류 방지) +//! let mut secret = SecretKey::default(); +//! secret.init(&seed); //! let public = PublicKey::from(&secret); //! //! // 서명 @@ -53,10 +54,25 @@ pub enum Ed448Error { /// Drop 시 내부 데이터가 `Secret` 의 Drop 으로 자동 소거됩니다. pub struct SecretKey(Secret<[u8; SECRET_KEY_LENGTH]>); +impl Default for SecretKey { + fn default() -> Self { + SecretKey(Secret::new([0u8; SECRET_KEY_LENGTH])) + } +} + impl SecretKey { + /// 57바이트 시드를 제자리에서 설정합니다. + /// + /// # Arguments + /// - `bytes`: 57바이트 시드 + /// + /// # Security Note + /// by-value 생성자 대신 `Default` 인스턴스를 최종 위치에 둔 뒤 호출하면 + /// move 로 인한 시드 스택 잔류가 없습니다. 입력 버퍼는 호출자가 + /// 사용 후 직접 소거해야 합니다. #[inline] - pub fn from_bytes(bytes: &[u8; SECRET_KEY_LENGTH]) -> Self { - SecretKey(Secret::new(*bytes)) + pub fn init(&mut self, bytes: &[u8; SECRET_KEY_LENGTH]) { + self.0.init_with(|k| *k = *bytes); } #[inline] @@ -64,16 +80,16 @@ impl SecretKey { self.0.expose() } - fn expand(&self) -> ExpandedSecretKey { + fn expand_into(&self, out: &mut ExpandedSecretKey) { let mut h = SHAKE256::new(); h.update(self.0.expose()); let mut hash = Secret::new([0u8; 114]); h.finalize_into(hash.expose_mut()); let mut lower = Secret::new([0u8; 57]); - let mut upper = Secret::new([0u8; 57]); lower.expose_mut().copy_from_slice(&hash.expose()[..57]); - upper.expose_mut().copy_from_slice(&hash.expose()[57..]); + out.nonce + .init_with(|n| n.copy_from_slice(&hash.expose()[57..])); { let lo = lower.expose_mut(); @@ -84,20 +100,9 @@ impl SecretKey { // 클램핑된 스칼라는 L 이상일 수 있으므로 mod L 리듀스 let mut clamped = Scalar::from_bytes(*lower.expose()); - let scalar = sc_muladd(&Scalar::one(), &clamped, &Scalar::zero()); + out.scalar = sc_muladd(&Scalar::one(), &clamped, &Scalar::zero()); clamped.zeroize(); // hash, lower 는 스코프 종료 시 Secret::Drop 으로 자동 소거 - - ExpandedSecretKey { - scalar, - nonce: upper, - } - } -} - -impl Clone for SecretKey { - fn clone(&self) -> Self { - SecretKey(Secret::new(*self.0.expose())) } } @@ -106,6 +111,15 @@ struct ExpandedSecretKey { nonce: Secret<[u8; 57]>, } +impl Default for ExpandedSecretKey { + fn default() -> Self { + Self { + scalar: Scalar::zero(), + nonce: Secret::new([0u8; 57]), + } + } +} + impl Drop for ExpandedSecretKey { fn drop(&mut self) { // scalar 는 Copy 타입이므로 명시적 zeroize 호출 @@ -117,6 +131,12 @@ impl Drop for ExpandedSecretKey { #[derive(Clone, Copy, PartialEq, Eq)] pub struct PublicKey([u8; PUBLIC_KEY_LENGTH]); +impl Default for PublicKey { + fn default() -> Self { + PublicKey([0u8; PUBLIC_KEY_LENGTH]) + } +} + impl PublicKey { #[inline] pub fn from_bytes(bytes: &[u8; PUBLIC_KEY_LENGTH]) -> Self { @@ -135,7 +155,8 @@ impl PublicKey { impl From<&SecretKey> for PublicKey { fn from(secret: &SecretKey) -> Self { - let expanded = secret.expand(); + let mut expanded = ExpandedSecretKey::default(); + secret.expand_into(&mut expanded); let point = EdwardsPoint::basepoint_mul(&expanded.scalar); PublicKey(point.to_bytes()) } @@ -186,7 +207,8 @@ pub fn sign(message: &[u8], secret_key: &SecretKey) -> Signature { } pub fn sign_with_context(message: &[u8], secret_key: &SecretKey, context: &[u8]) -> Signature { - let expanded = secret_key.expand(); + let mut expanded = ExpandedSecretKey::default(); + secret_key.expand_into(&mut expanded); let public_key = PublicKey::from(secret_key); let (dom, dom_len) = dom4(context); @@ -284,16 +306,24 @@ pub fn verify_with_context( } } +#[derive(Default)] pub struct Keypair { pub secret: SecretKey, pub public: PublicKey, } impl Keypair { - pub fn from_seed(seed: &[u8; 57]) -> Self { - let secret = SecretKey::from_bytes(seed); - let public = PublicKey::from(&secret); - Keypair { secret, public } + /// 57바이트 시드로 키페어를 제자리에서 초기화합니다. + /// + /// # Arguments + /// - `seed`: 57바이트 시드 + /// + /// # Security Note + /// `Default` 인스턴스를 최종 위치에 둔 뒤 호출하면 move 로 인한 + /// 비밀키 스택 잔류가 없습니다. + pub fn init(&mut self, seed: &[u8; 57]) { + self.secret.init(seed); + self.public = PublicKey::from(&self.secret); } pub fn sign(&self, message: &[u8]) -> Signature { @@ -325,18 +355,21 @@ mod tests { #[test] fn test_keypair_generation() { let seed = [42u8; 57]; - let keypair = Keypair::from_seed(&seed); + let mut keypair = Keypair::default(); + keypair.init(&seed); assert_ne!(keypair.public.as_bytes(), &[0u8; 57]); } #[test] fn test_sign_verify_roundtrip() { let seed = [42u8; 57]; - let keypair = Keypair::from_seed(&seed); + let mut keypair = Keypair::default(); + keypair.init(&seed); let message = b"test message"; // Verify expanded scalar is now canonical - let expanded = keypair.secret.expand(); + let mut expanded = ExpandedSecretKey::default(); + keypair.secret.expand_into(&mut expanded); let a_scalar = expanded.scalar; let a_point = keypair.public.as_point().unwrap(); let (dom, dom_len) = dom4(&[]); @@ -527,7 +560,8 @@ mod tests { #[test] fn test_wrong_message() { let seed = [42u8; 57]; - let keypair = Keypair::from_seed(&seed); + let mut keypair = Keypair::default(); + keypair.init(&seed); let message = b"test message"; let wrong_message = b"wrong message"; @@ -539,8 +573,10 @@ mod tests { fn test_wrong_key() { let seed1 = [1u8; 57]; let seed2 = [2u8; 57]; - let keypair1 = Keypair::from_seed(&seed1); - let keypair2 = Keypair::from_seed(&seed2); + let mut keypair1 = Keypair::default(); + keypair1.init(&seed1); + let mut keypair2 = Keypair::default(); + keypair2.init(&seed2); let message = b"test message"; let signature = keypair1.sign(message); @@ -550,7 +586,8 @@ mod tests { #[test] fn test_context_signature() { let seed = [42u8; 57]; - let keypair = Keypair::from_seed(&seed); + let mut keypair = Keypair::default(); + keypair.init(&seed); let message = b"test message"; let context = b"test context"; diff --git a/elib-k0d-core/src/dispatcher.rs b/elib-k0d-core/src/dispatcher.rs index 23f05cb..f8c12cd 100644 --- a/elib-k0d-core/src/dispatcher.rs +++ b/elib-k0d-core/src/dispatcher.rs @@ -255,7 +255,8 @@ pub(crate) fn dispatch_ed25519_keygen( None => return encode_error(wire_out.as_mut_slice(), IpcError::MalformedRequest), }; seed.copy_from_slice(seed_src); - let keypair = ed25519::Keypair::from_seed(&seed); + let mut keypair = ed25519::Keypair::default(); + keypair.init(&seed); let pk_bytes: [u8; 32] = *keypair.public.as_bytes(); // payload = pk(32) || sk(32) = 64 @@ -306,7 +307,8 @@ pub(crate) fn dispatch_ed25519_sign( None => return encode_error(wire_out.as_mut_slice(), IpcError::MalformedRequest), }; seed.copy_from_slice(seed_src); - let keypair = ed25519::Keypair::from_seed(&seed); + let mut keypair = ed25519::Keypair::default(); + keypair.init(&seed); let msg_len = arena.msg_len as usize; let msg = match arena.msg.get(..msg_len) { Some(m) => m, @@ -605,7 +607,9 @@ mod tests { // sk 는 seed 그대로 assert_eq!(&wire[HEADER_LEN + 32..HEADER_LEN + 64], &seed); // pk 는 ed25519 reference 와 일치 (as_bytes — to_bytes 아님 per W-05) - let expected_pk: [u8; 32] = *ed25519::Keypair::from_seed(&seed).public.as_bytes(); + let mut expected_kp = ed25519::Keypair::default(); + expected_kp.init(&seed); + let expected_pk: [u8; 32] = *expected_kp.public.as_bytes(); assert_eq!(&wire[HEADER_LEN..HEADER_LEN + 32], &expected_pk); } From 77b655c0bcb92e227c938e906b6c839ec7627257 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:12 +0900 Subject: [PATCH 60/65] =?UTF-8?q?rng=20DRBG=20=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=EC=B4=88=EA=B8=B0=ED=99=94=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20-=20HashDRBG=208=EC=A2=85=EC=97=90=20`init?= =?UTF-8?q?=5Ffrom=5Fos`=20=EC=99=80=20`init=5Ffrom=5Fentropy`=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=ED=95=B4=20by-value=20=EB=B0=98=ED=99=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20-=20`SecureBuffer::init`=20=EB=A1=9C=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EC=83=81=ED=83=9C=20=EC=86=8C=EA=B1=B0=20=ED=9B=84?= =?UTF-8?q?=20=EA=B8=B8=EC=9D=B4=20=EC=9E=AC=EC=84=A4=EC=A0=95=20-=20KCMVP?= =?UTF-8?q?=20=ED=95=98=EB=84=A4=EC=8A=A4=EC=99=80=20capability=20?= =?UTF-8?q?=ED=9A=8C=EA=B7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=EC=B2=98=20=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/capability_token_regression.rs | 8 +- rng/src/hash_drbg.rs | 123 +++++++++++++----- rng/src/lib.rs | 27 ++++ rng/tests/kcmvp_hash_drbg.rs | 6 +- 4 files changed, 123 insertions(+), 41 deletions(-) diff --git a/constant-time/tests/capability_token_regression.rs b/constant-time/tests/capability_token_regression.rs index 22d150d..5e5f9cb 100644 --- a/constant-time/tests/capability_token_regression.rs +++ b/constant-time/tests/capability_token_regression.rs @@ -9,11 +9,11 @@ mod tests { const NONCE: [u8; 16] = [0x37u8; 16]; fn fresh_drbg() -> HashDRBGSHA256 { + let mut drbg = HashDRBGSHA256::default(); // SAFETY: 테스트 전용 결정론적 시드 (보안 강도 무관, 회귀 비교만 수행) - unsafe { - HashDRBGSHA256::new_from_entropy(&ENTROPY, &NONCE, None) - .expect("instantiate HashDRBGSHA256") - } + unsafe { drbg.init_from_entropy(&ENTROPY, &NONCE, None) } + .expect("instantiate HashDRBGSHA256"); + drbg } fn gen_token_u64_local(drbg: &mut HashDRBGSHA256) -> u64 { diff --git a/rng/src/hash_drbg.rs b/rng/src/hash_drbg.rs index 6063f27..1c9df0e 100644 --- a/rng/src/hash_drbg.rs +++ b/rng/src/hash_drbg.rs @@ -20,7 +20,8 @@ //! fn main() -> Result<(), DrbgError> { //! // 1. 초기화 — OS 엔트로피 소스 사용 (임의 엔트로피 주입 불가) //! let personalization = Some(b"my-app-specific-string" as &[u8]); -//! let mut drbg = HashDRBGSHA256::new_from_os(personalization)?; +//! let mut drbg = HashDRBGSHA256::default(); +//! drbg.init_from_os(personalization)?; //! //! // 2. 난수 생성 (Generate) //! let mut random_bytes = [0u8; 128]; @@ -93,6 +94,16 @@ macro_rules! impl_hash_drbg { reseed_counter: u64, } + impl Default for $struct_name { + fn default() -> Self { + Self { + v: SecureBuffer::default(), + c: SecureBuffer::default(), + reseed_counter: 0, + } + } + } + impl $struct_name { /// NIST SP 800-90A Rev. 1의 Section 10.3.1의 Hash_df /// @@ -258,7 +269,13 @@ macro_rules! impl_hash_drbg { /// /// # Errors /// - `DrbgError::OsEntropyFailed`: OS 엔트로피 소스 접근 실패 - pub fn new_from_os(personalization_string: Option<&[u8]>) -> Result { + /// # Security Note + /// 내부 상태는 호출자가 둔 `self` 위치에 제자리 기록되므로 by-value + /// 반환에 따른 move 스택 잔류가 없습니다. + pub fn init_from_os( + &mut self, + personalization_string: Option<&[u8]>, + ) -> Result<(), DrbgError> { // entropy_input: 2 × security_strength 바이트 (별개 호출로 독립성 보장) let entropy = crate::os_entropy::extract_os_entropy($min_entropy * 2) .map_err(|_| DrbgError::OsEntropyFailed)?; @@ -268,7 +285,7 @@ macro_rules! impl_hash_drbg { .map_err(|_| DrbgError::OsEntropyFailed)?; // SecureBuffer는 Drop 시 자동 소거 — 별도 write_volatile 루프 불필요 - Self::instantiate(entropy.as_slice(), nonce.as_slice(), personalization_string) + self.instantiate(entropy.as_slice(), nonce.as_slice(), personalization_string) } /// 호출자가 수집한 하드웨어 엔트로피로 DRBG를 초기화합니다. @@ -295,12 +312,13 @@ macro_rules! impl_hash_drbg { /// 상위 계층(키 생성, Capability 토큰, IV)의 보안이 붕괴됩니다. /// 가능한 경우 [`new_from_os`] 를 우선 사용하고, 이 함수는 OS 엔트로피 /// 소스를 사용할 수 없는 경우에만 사용하세요. - pub unsafe fn new_from_entropy( + pub unsafe fn init_from_entropy( + &mut self, entropy_input: &[u8], nonce: &[u8], personalization_string: Option<&[u8]>, - ) -> Result { - Self::instantiate(entropy_input, nonce, personalization_string) + ) -> Result<(), DrbgError> { + self.instantiate(entropy_input, nonce, personalization_string) } /// NIST SP 800-90A Rev. 1, Section 10.1.1.2: Hash_DRBG_Instantiate_algorithm @@ -317,10 +335,11 @@ macro_rules! impl_hash_drbg { /// DRBG 출력의 무작위성이 공격자에 의해 제어될 수 있습니다. /// 외부 코드는 반드시 [`new_from_os`]를 통해 OS 엔트로피로 초기화하세요. pub(crate) fn instantiate( + &mut self, entropy_input: &[u8], nonce: &[u8], personalization_string: Option<&[u8]>, - ) -> Result { + ) -> Result<(), DrbgError> { // NIST SP 800-90A Rev. 1, Section 8.6.7 검증 if entropy_input.len() < $min_entropy { return Err(DrbgError::EntropyTooShort); @@ -342,24 +361,26 @@ macro_rules! impl_hash_drbg { } // 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())?; + // 이전 상태를 소거한 뒤 최종 위치에 직접 기록 + self.v + .init($seedlen) + .map_err(|_| DrbgError::AllocationFailed)?; + Self::hash_df(&[entropy_input, nonce, ps], $seedlen, self.v.as_mut_slice())?; // C = Hash_df(0x00 || V, seedlen) - let mut c_buf = - SecureBuffer::new_owned($seedlen).map_err(|_| DrbgError::AllocationFailed)?; + self.c + .init($seedlen) + .map_err(|_| DrbgError::AllocationFailed)?; + let mut c_tmp = Secret::new([0u8; $seedlen]); Self::hash_df( - &[&[0x00u8], v_buf.as_slice()], + &[&[0x00u8], self.v.as_slice()], $seedlen, - c_buf.as_mut_slice(), + c_tmp.expose_mut(), )?; + self.c.as_mut_slice().copy_from_slice(c_tmp.expose()); - Ok(Self { - v: v_buf, - c: c_buf, - reseed_counter: 1, - }) + self.reseed_counter = 1; + Ok(()) } /// NIST SP 800-90A Rev. 1, Section 10.1.1.3: Hash_DRBG_Reseed_algorithm @@ -543,8 +564,12 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - let drbg = - HashDRBGSHA256::new_from_entropy(&entropy, &nonce, None).expect("instantiate"); + let drbg = { + let mut d = HashDRBGSHA256::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate"); + d + }; storage.write(drbg); let v_data_ptr = (&raw const (*storage.as_ptr()).v.data) as *const u8; @@ -595,8 +620,12 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - let drbg = - HashDRBGSHA512::new_from_entropy(&entropy, &nonce, None).expect("instantiate"); + let drbg = { + let mut d = HashDRBGSHA512::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate"); + d + }; storage.write(drbg); let v_data_ptr = (&raw const (*storage.as_ptr()).v.data) as *const u8; @@ -625,8 +654,12 @@ mod tests { let mut storage: MaybeUninit = MaybeUninit::uninit(); unsafe { - let mut drbg = - HashDRBGSHA256::new_from_entropy(&entropy, &nonce, None).expect("instantiate"); + let mut drbg = { + let mut d = HashDRBGSHA256::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate"); + d + }; let mut out = [0u8; 64]; drbg.generate(&mut out, Some(b"ai-1")).expect("generate-1"); @@ -663,10 +696,18 @@ mod tests { let nonce = [0x22u8; 16]; unsafe { - let mut a = - HashDRBGSHA256::new_from_entropy(&entropy, &nonce, None).expect("instantiate-a"); - let mut b = - HashDRBGSHA256::new_from_entropy(&entropy, &nonce, None).expect("instantiate-b"); + let mut a = { + let mut d = HashDRBGSHA256::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate-a"); + d + }; + let mut b = { + let mut d = HashDRBGSHA256::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate-b"); + d + }; let mut out_a = [0u8; 128]; let mut out_b = [0u8; 128]; @@ -684,10 +725,18 @@ mod tests { let nonce = [0x22u8; 16]; unsafe { - let mut a = - HashDRBGSHA256::new_from_entropy(&entropy, &nonce, None).expect("instantiate-a"); - let mut b = - HashDRBGSHA256::new_from_entropy(&entropy, &nonce, None).expect("instantiate-b"); + let mut a = { + let mut d = HashDRBGSHA256::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate-a"); + d + }; + let mut b = { + let mut d = HashDRBGSHA256::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate-b"); + d + }; let mut out_empty = [0u8; 64]; let mut out_none = [0u8; 64]; @@ -709,8 +758,12 @@ mod tests { let nonce = [0x44u8; 16]; unsafe { - let mut drbg = - HashDRBGSHA256::new_from_entropy(&entropy, &nonce, None).expect("instantiate"); + let mut drbg = { + let mut d = HashDRBGSHA256::default(); + d.init_from_entropy(&entropy, &nonce, None) + .expect("instantiate"); + d + }; let mut out = [0u8; 32]; drbg.generate(&mut out, None).expect("generate-1"); // counter -> 2 diff --git a/rng/src/lib.rs b/rng/src/lib.rs index c4f6643..e01ad81 100644 --- a/rng/src/lib.rs +++ b/rng/src/lib.rs @@ -57,6 +57,16 @@ pub(crate) struct SecureBuffer { len: usize, } +impl Default for SecureBuffer { + #[inline] + fn default() -> Self { + Self { + data: [0u8; MAX_SECURE_BUFFER_LEN], + len: 0, + } + } +} + impl SecureBuffer { #[inline] pub fn new_owned(len: usize) -> Result { @@ -69,6 +79,23 @@ impl SecureBuffer { }) } + /// 기존 내용을 소거한 뒤 활성 길이를 제자리에서 재설정합니다. + /// + /// # Arguments + /// - `len`: 새 활성 길이 + /// + /// # Errors + /// - `DrbgError::InvalidArgument`: `len` 이 backing storage 크기를 초과 + #[inline] + pub fn init(&mut self, len: usize) -> Result<(), DrbgError> { + if len > MAX_SECURE_BUFFER_LEN { + return Err(DrbgError::InvalidArgument); + } + self.zeroize(); + self.len = len; + Ok(()) + } + #[inline] pub fn as_slice(&self) -> &[u8] { &self.data[..self.len] diff --git a/rng/tests/kcmvp_hash_drbg.rs b/rng/tests/kcmvp_hash_drbg.rs index 051ed32..33675d4 100644 --- a/rng/tests/kcmvp_hash_drbg.rs +++ b/rng/tests/kcmvp_hash_drbg.rs @@ -122,8 +122,10 @@ macro_rules! impl_drbg_ops { ($($t:ty),+) => {$( impl DrbgOps for $t { fn kat_instantiate(entropy: &[u8], nonce: &[u8], ps: Option<&[u8]>) -> Self { - unsafe { <$t>::new_from_entropy(entropy, nonce, ps) } - .expect("KAT instantiate 실패") + let mut drbg = <$t>::default(); + unsafe { drbg.init_from_entropy(entropy, nonce, ps) } + .expect("KAT instantiate 실패"); + drbg } fn kat_reseed(&mut self, entropy: &[u8], ai: Option<&[u8]>) { <$t>::reseed(self, entropy, ai).expect("KAT reseed 실패"); From e5b827270d37ad41f8a916dfad02574ad3f565a2 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:23 +0900 Subject: [PATCH 61/65] =?UTF-8?q?mlkem=20=EB=82=98=EB=88=97=EC=85=88=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=EC=99=80=20=EC=A0=9C=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EC=A0=84=ED=99=98=20-=20`poly=5Ftomsg`=20?= =?UTF-8?q?=EC=99=80=20`compress`=20=EB=A5=BC=20=EA=B3=B1=EC=85=88=20?= =?UTF-8?q?=EC=8B=9C=ED=94=84=ED=8A=B8=EB=A1=9C=20=EB=B0=94=EA=BF=94=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=20=EC=9D=98=EC=A1=B4=20udiv=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20-=20keygen=20encaps=20decaps=209=EA=B0=9C=EB=A5=BC?= =?UTF-8?q?=20out=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=ED=95=B4=20=EA=B3=B5=EC=9C=A0=EB=B9=84?= =?UTF-8?q?=EB=B0=80=20=EC=9D=B4=EB=8F=99=20=EC=9E=94=EB=A5=98=20=EC=B0=A8?= =?UTF-8?q?=EB=8B=A8=20-=20KeyPair=203=EC=A2=85=EC=97=90=20Default=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0=20KAT=20=ED=95=98=EB=84=A4?= =?UTF-8?q?=EC=8A=A4=20=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Fable 5 --- mlkem/src/encode.rs | 2 +- mlkem/src/lib.rs | 281 ++++++++++++++++++++++++-------------------- mlkem/src/poly.rs | 2 +- mlkem/tests/kat.rs | 34 ++++-- 4 files changed, 180 insertions(+), 139 deletions(-) diff --git a/mlkem/src/encode.rs b/mlkem/src/encode.rs index 7e1b076..0875d59 100644 --- a/mlkem/src/encode.rs +++ b/mlkem/src/encode.rs @@ -47,7 +47,7 @@ pub fn poly_tomsg(r: &mut [u8; 32], a: &Poly) { *byte = 0; for j in 0..8 { let t = freeze(a.coeffs[8 * i + j]) as u16; - let d = ((((t as u32) << 1) + Q as u32 / 2) / Q as u32) & 1; + let d = ((((t as u32) << 1) + 1665).wrapping_mul(80635) >> 28) & 1; *byte |= (d as u8) << j; } } diff --git a/mlkem/src/lib.rs b/mlkem/src/lib.rs index f15793f..5e2d268 100644 --- a/mlkem/src/lib.rs +++ b/mlkem/src/lib.rs @@ -13,19 +13,23 @@ //! //! # Examples //! ```rust,ignore -//! use mlkem::{mlkem768_keygen, mlkem768_encaps, mlkem768_decaps}; +//! use mlkem::{MLKEM768KeyPair, mlkem768_keygen, mlkem768_encaps, mlkem768_decaps}; +//! use zeroize::Secret; //! -//! // 1. 키 쌍 생성 (d, z는 32바이트 난수 시드) +//! // 1. 키 쌍 생성 (d, z는 32바이트 난수 시드, 키는 호출자가 둔 위치에 제자리 기록) //! let d = [0u8; 32]; // CSPRNG로 생성 필요 //! let z = [0u8; 32]; // CSPRNG로 생성 필요 -//! let keypair = mlkem768_keygen(&d, &z); +//! let mut keypair = MLKEM768KeyPair::default(); +//! mlkem768_keygen(&d, &z, &mut keypair); //! //! // 2. 캡슐화 (송신자: 공유 비밀 + 암호문 생성) //! let m = [0u8; 32]; // CSPRNG로 생성 필요 -//! let (ciphertext, shared_secret_enc) = mlkem768_encaps(&keypair.ek, &m).unwrap(); +//! let mut shared_secret_enc = Secret::new([0u8; 32]); +//! let ciphertext = mlkem768_encaps(&keypair.ek, &m, &mut shared_secret_enc).unwrap(); //! //! // 3. 역캡슐화 (수신자: 공유 비밀 복원) -//! let shared_secret_dec = mlkem768_decaps(&ciphertext, keypair.dk.expose()); +//! let mut shared_secret_dec = Secret::new([0u8; 32]); +//! mlkem768_decaps(&ciphertext, keypair.dk.expose(), &mut shared_secret_dec); //! //! assert_eq!(shared_secret_enc.expose(), shared_secret_dec.expose()); //! ``` @@ -92,6 +96,15 @@ pub struct MLKEM512KeyPair { pub dk: Secret<[u8; MLKEM512_DK_BYTES]>, } +impl Default for MLKEM512KeyPair { + fn default() -> Self { + Self { + ek: [0u8; MLKEM512_EK_BYTES], + dk: Secret::new([0u8; MLKEM512_DK_BYTES]), + } + } +} + /// ML-KEM-768 키 쌍 구조체 /// /// # Security Note @@ -103,6 +116,15 @@ pub struct MLKEM768KeyPair { pub dk: Secret<[u8; MLKEM768_DK_BYTES]>, } +impl Default for MLKEM768KeyPair { + fn default() -> Self { + Self { + ek: [0u8; MLKEM768_EK_BYTES], + dk: Secret::new([0u8; MLKEM768_DK_BYTES]), + } + } +} + /// ML-KEM-1024 키 쌍 구조체 /// /// # Security Note @@ -114,6 +136,15 @@ pub struct MLKEM1024KeyPair { pub dk: Secret<[u8; MLKEM1024_DK_BYTES]>, } +impl Default for MLKEM1024KeyPair { + fn default() -> Self { + Self { + ek: [0u8; MLKEM1024_EK_BYTES], + dk: Secret::new([0u8; MLKEM1024_DK_BYTES]), + } + } +} + /// ML-KEM-512 키 쌍을 생성합니다. /// /// # Arguments @@ -122,14 +153,9 @@ pub struct MLKEM1024KeyPair { /// /// # Security Note /// `d`와 `z`는 반드시 암호학적으로 안전한 난수 생성기로 생성해야 합니다. -pub fn mlkem512_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM512KeyPair { - let mut ek = [0u8; MLKEM512_EK_BYTES]; - let mut dk = [0u8; MLKEM512_DK_BYTES]; - kem::keygen::<2>(&mut ek, &mut dk, d, z, MLKEM512.eta1); - MLKEM512KeyPair { - ek, - dk: Secret::new(dk), - } +pub fn mlkem512_keygen(d: &[u8; 32], z: &[u8; 32], out: &mut MLKEM512KeyPair) { + out.dk + .init_with(|dk| kem::keygen::<2>(&mut out.ek, dk, d, z, MLKEM512.eta1)); } /// ML-KEM-512 캡슐화를 수행합니다. @@ -139,27 +165,30 @@ pub fn mlkem512_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM512KeyPair { /// - `m`: 32바이트 난수 (CSPRNG로 생성 필요) /// /// # Returns -/// - 암호문과 공유 비밀 키의 튜플 +/// - 암호문 (공유 비밀은 호출자가 둔 `ss` 에 제자리 기록) /// /// # Errors /// - `Error::InvalidEncapsulationKey`: `ek` 계수가 비정규(q 이상)일 때 (FIPS 203 7.2 모듈러스 검사) pub fn mlkem512_encaps( ek: &[u8; MLKEM512_EK_BYTES], m: &[u8; 32], -) -> Result<([u8; MLKEM512_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>), Error> { + ss: &mut Secret<[u8; SHAREDSECRETBYTES]>, +) -> Result<[u8; MLKEM512_CT_BYTES], Error> { let mut ct = [0u8; MLKEM512_CT_BYTES]; - let mut ss = [0u8; SHAREDSECRETBYTES]; - kem::encaps::<2>( - &mut ct, - &mut ss, - ek, - m, - MLKEM512.eta1, - MLKEM512.eta2, - MLKEM512.du, - MLKEM512.dv, - )?; - Ok((ct, Secret::new(ss))) + let mut result = Ok(()); + ss.init_with(|s| { + result = kem::encaps::<2>( + &mut ct, + s, + ek, + m, + MLKEM512.eta1, + MLKEM512.eta2, + MLKEM512.du, + MLKEM512.dv, + ); + }); + result.map(|()| ct) } /// ML-KEM-512 역캡슐화를 수행합니다. @@ -168,26 +197,25 @@ pub fn mlkem512_encaps( /// - `ct`: 암호문 /// - `dk`: 역캡슐화 키 (비밀 키) /// -/// # Returns -/// - 32바이트 공유 비밀 키 -/// /// # Security Note +/// 공유 비밀은 호출자가 둔 `ss` 에 제자리 기록됩니다. /// 암호문 변조 시 암묵적 거부(implicit rejection)를 수행합니다. pub fn mlkem512_decaps( ct: &[u8; MLKEM512_CT_BYTES], dk: &[u8; MLKEM512_DK_BYTES], -) -> Secret<[u8; SHAREDSECRETBYTES]> { - let mut ss = [0u8; SHAREDSECRETBYTES]; - kem::decaps::<2>( - &mut ss, - ct, - dk, - MLKEM512.eta1, - MLKEM512.eta2, - MLKEM512.du, - MLKEM512.dv, - ); - Secret::new(ss) + ss: &mut Secret<[u8; SHAREDSECRETBYTES]>, +) { + ss.init_with(|s| { + kem::decaps::<2>( + s, + ct, + dk, + MLKEM512.eta1, + MLKEM512.eta2, + MLKEM512.du, + MLKEM512.dv, + ); + }); } /// ML-KEM-768 키 쌍을 생성합니다. @@ -198,14 +226,9 @@ pub fn mlkem512_decaps( /// /// # Security Note /// `d`와 `z`는 반드시 암호학적으로 안전한 난수 생성기로 생성해야 합니다. -pub fn mlkem768_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM768KeyPair { - let mut ek = [0u8; MLKEM768_EK_BYTES]; - let mut dk = [0u8; MLKEM768_DK_BYTES]; - kem::keygen::<3>(&mut ek, &mut dk, d, z, MLKEM768.eta1); - MLKEM768KeyPair { - ek, - dk: Secret::new(dk), - } +pub fn mlkem768_keygen(d: &[u8; 32], z: &[u8; 32], out: &mut MLKEM768KeyPair) { + out.dk + .init_with(|dk| kem::keygen::<3>(&mut out.ek, dk, d, z, MLKEM768.eta1)); } /// ML-KEM-768 캡슐화를 수행합니다. @@ -215,27 +238,30 @@ pub fn mlkem768_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM768KeyPair { /// - `m`: 32바이트 난수 (CSPRNG로 생성 필요) /// /// # Returns -/// - 암호문과 공유 비밀 키의 튜플 +/// - 암호문 (공유 비밀은 호출자가 둔 `ss` 에 제자리 기록) /// /// # Errors /// - `Error::InvalidEncapsulationKey`: `ek` 계수가 비정규(q 이상)일 때 (FIPS 203 7.2 모듈러스 검사) pub fn mlkem768_encaps( ek: &[u8; MLKEM768_EK_BYTES], m: &[u8; 32], -) -> Result<([u8; MLKEM768_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>), Error> { + ss: &mut Secret<[u8; SHAREDSECRETBYTES]>, +) -> Result<[u8; MLKEM768_CT_BYTES], Error> { let mut ct = [0u8; MLKEM768_CT_BYTES]; - let mut ss = [0u8; SHAREDSECRETBYTES]; - kem::encaps::<3>( - &mut ct, - &mut ss, - ek, - m, - MLKEM768.eta1, - MLKEM768.eta2, - MLKEM768.du, - MLKEM768.dv, - )?; - Ok((ct, Secret::new(ss))) + let mut result = Ok(()); + ss.init_with(|s| { + result = kem::encaps::<3>( + &mut ct, + s, + ek, + m, + MLKEM768.eta1, + MLKEM768.eta2, + MLKEM768.du, + MLKEM768.dv, + ); + }); + result.map(|()| ct) } /// ML-KEM-768 역캡슐화를 수행합니다. @@ -244,26 +270,25 @@ pub fn mlkem768_encaps( /// - `ct`: 암호문 /// - `dk`: 역캡슐화 키 (비밀 키) /// -/// # Returns -/// - 32바이트 공유 비밀 키 -/// /// # Security Note +/// 공유 비밀은 호출자가 둔 `ss` 에 제자리 기록됩니다. /// 암호문 변조 시 암묵적 거부(implicit rejection)를 수행합니다. pub fn mlkem768_decaps( ct: &[u8; MLKEM768_CT_BYTES], dk: &[u8; MLKEM768_DK_BYTES], -) -> Secret<[u8; SHAREDSECRETBYTES]> { - let mut ss = [0u8; SHAREDSECRETBYTES]; - kem::decaps::<3>( - &mut ss, - ct, - dk, - MLKEM768.eta1, - MLKEM768.eta2, - MLKEM768.du, - MLKEM768.dv, - ); - Secret::new(ss) + ss: &mut Secret<[u8; SHAREDSECRETBYTES]>, +) { + ss.init_with(|s| { + kem::decaps::<3>( + s, + ct, + dk, + MLKEM768.eta1, + MLKEM768.eta2, + MLKEM768.du, + MLKEM768.dv, + ); + }); } /// ML-KEM-1024 키 쌍을 생성합니다. @@ -274,14 +299,9 @@ pub fn mlkem768_decaps( /// /// # Security Note /// `d`와 `z`는 반드시 암호학적으로 안전한 난수 생성기로 생성해야 합니다. -pub fn mlkem1024_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM1024KeyPair { - let mut ek = [0u8; MLKEM1024_EK_BYTES]; - let mut dk = [0u8; MLKEM1024_DK_BYTES]; - kem::keygen::<4>(&mut ek, &mut dk, d, z, MLKEM1024.eta1); - MLKEM1024KeyPair { - ek, - dk: Secret::new(dk), - } +pub fn mlkem1024_keygen(d: &[u8; 32], z: &[u8; 32], out: &mut MLKEM1024KeyPair) { + out.dk + .init_with(|dk| kem::keygen::<4>(&mut out.ek, dk, d, z, MLKEM1024.eta1)); } /// ML-KEM-1024 캡슐화를 수행합니다. @@ -291,27 +311,30 @@ pub fn mlkem1024_keygen(d: &[u8; 32], z: &[u8; 32]) -> MLKEM1024KeyPair { /// - `m`: 32바이트 난수 (CSPRNG로 생성 필요) /// /// # Returns -/// - 암호문과 공유 비밀 키의 튜플 +/// - 암호문 (공유 비밀은 호출자가 둔 `ss` 에 제자리 기록) /// /// # Errors /// - `Error::InvalidEncapsulationKey`: `ek` 계수가 비정규(q 이상)일 때 (FIPS 203 7.2 모듈러스 검사) pub fn mlkem1024_encaps( ek: &[u8; MLKEM1024_EK_BYTES], m: &[u8; 32], -) -> Result<([u8; MLKEM1024_CT_BYTES], Secret<[u8; SHAREDSECRETBYTES]>), Error> { + ss: &mut Secret<[u8; SHAREDSECRETBYTES]>, +) -> Result<[u8; MLKEM1024_CT_BYTES], Error> { let mut ct = [0u8; MLKEM1024_CT_BYTES]; - let mut ss = [0u8; SHAREDSECRETBYTES]; - kem::encaps::<4>( - &mut ct, - &mut ss, - ek, - m, - MLKEM1024.eta1, - MLKEM1024.eta2, - MLKEM1024.du, - MLKEM1024.dv, - )?; - Ok((ct, Secret::new(ss))) + let mut result = Ok(()); + ss.init_with(|s| { + result = kem::encaps::<4>( + &mut ct, + s, + ek, + m, + MLKEM1024.eta1, + MLKEM1024.eta2, + MLKEM1024.du, + MLKEM1024.dv, + ); + }); + result.map(|()| ct) } /// ML-KEM-1024 역캡슐화를 수행합니다. @@ -320,26 +343,25 @@ pub fn mlkem1024_encaps( /// - `ct`: 암호문 /// - `dk`: 역캡슐화 키 (비밀 키) /// -/// # Returns -/// - 32바이트 공유 비밀 키 -/// /// # Security Note +/// 공유 비밀은 호출자가 둔 `ss` 에 제자리 기록됩니다. /// 암호문 변조 시 암묵적 거부(implicit rejection)를 수행합니다. pub fn mlkem1024_decaps( ct: &[u8; MLKEM1024_CT_BYTES], dk: &[u8; MLKEM1024_DK_BYTES], -) -> Secret<[u8; SHAREDSECRETBYTES]> { - let mut ss = [0u8; SHAREDSECRETBYTES]; - kem::decaps::<4>( - &mut ss, - ct, - dk, - MLKEM1024.eta1, - MLKEM1024.eta2, - MLKEM1024.du, - MLKEM1024.dv, - ); - Secret::new(ss) + ss: &mut Secret<[u8; SHAREDSECRETBYTES]>, +) { + ss.init_with(|s| { + kem::decaps::<4>( + s, + ct, + dk, + MLKEM1024.eta1, + MLKEM1024.eta2, + MLKEM1024.du, + MLKEM1024.dv, + ); + }); } #[cfg(test)] @@ -581,9 +603,12 @@ mod tests { let z = [2u8; 32]; let m = [3u8; 32]; - let keypair = mlkem512_keygen(&d, &z); - let (ct, ss_enc) = mlkem512_encaps(&keypair.ek, &m).unwrap(); - let ss_dec = mlkem512_decaps(&ct, keypair.dk.expose()); + let mut keypair = MLKEM512KeyPair::default(); + mlkem512_keygen(&d, &z, &mut keypair); + let mut ss_enc = Secret::new([0u8; 32]); + let ct = mlkem512_encaps(&keypair.ek, &m, &mut ss_enc).unwrap(); + let mut ss_dec = Secret::new([0u8; 32]); + mlkem512_decaps(&ct, keypair.dk.expose(), &mut ss_dec); assert_eq!(ss_enc.expose(), ss_dec.expose()); } @@ -594,9 +619,12 @@ mod tests { let z = [5u8; 32]; let m = [6u8; 32]; - let keypair = mlkem768_keygen(&d, &z); - let (ct, ss_enc) = mlkem768_encaps(&keypair.ek, &m).unwrap(); - let ss_dec = mlkem768_decaps(&ct, keypair.dk.expose()); + let mut keypair = MLKEM768KeyPair::default(); + mlkem768_keygen(&d, &z, &mut keypair); + let mut ss_enc = Secret::new([0u8; 32]); + let ct = mlkem768_encaps(&keypair.ek, &m, &mut ss_enc).unwrap(); + let mut ss_dec = Secret::new([0u8; 32]); + mlkem768_decaps(&ct, keypair.dk.expose(), &mut ss_dec); assert_eq!(ss_enc.expose(), ss_dec.expose()); } @@ -607,9 +635,12 @@ mod tests { let z = [8u8; 32]; let m = [9u8; 32]; - let keypair = mlkem1024_keygen(&d, &z); - let (ct, ss_enc) = mlkem1024_encaps(&keypair.ek, &m).unwrap(); - let ss_dec = mlkem1024_decaps(&ct, keypair.dk.expose()); + let mut keypair = MLKEM1024KeyPair::default(); + mlkem1024_keygen(&d, &z, &mut keypair); + let mut ss_enc = Secret::new([0u8; 32]); + let ct = mlkem1024_encaps(&keypair.ek, &m, &mut ss_enc).unwrap(); + let mut ss_dec = Secret::new([0u8; 32]); + mlkem1024_decaps(&ct, keypair.dk.expose(), &mut ss_dec); assert_eq!(ss_enc.expose(), ss_dec.expose()); } diff --git a/mlkem/src/poly.rs b/mlkem/src/poly.rs index 1f3eaa7..6457307 100644 --- a/mlkem/src/poly.rs +++ b/mlkem/src/poly.rs @@ -142,7 +142,7 @@ impl PolyVec { } pub fn compress(a: u16, d: usize) -> u16 { - let t = (((a as u32) << d) + (Q as u32 / 2)) / (Q as u32); + let t = (((a as u64) << d) + 1664).wrapping_mul(330_282_857) >> 40; (t & ((1 << d) - 1)) as u16 } diff --git a/mlkem/tests/kat.rs b/mlkem/tests/kat.rs index 1749494..41e902e 100644 --- a/mlkem/tests/kat.rs +++ b/mlkem/tests/kat.rs @@ -10,6 +10,7 @@ //! - decaps: `(dk, c)` 로부터 공유 비밀 `k` 검증 (정상 복호화 및 변조 암호문 암묵적 거부 포함) use mlkem::*; +use zeroize::Secret; fn unhex(s: &str) -> Vec { let b = s.as_bytes(); @@ -69,7 +70,8 @@ fn kat_mlkem512_keygen() { for (d, z, ek, dk) in cases { let d: [u8; 32] = unhex(d).try_into().unwrap(); let z: [u8; 32] = unhex(z).try_into().unwrap(); - let kp = mlkem512_keygen(&d, &z); + let mut kp = MLKEM512KeyPair::default(); + mlkem512_keygen(&d, &z, &mut kp); assert_eq!(&kp.ek[..], &unhex(ek)[..], "ek 불일치"); assert_eq!(&kp.dk.expose()[..], &unhex(dk)[..], "dk 불일치"); } @@ -113,7 +115,8 @@ fn kat_mlkem512_encaps() { for (ek, m, c, k) in cases { let ek: [u8; 800] = unhex(ek).try_into().unwrap(); let m: [u8; 32] = unhex(m).try_into().unwrap(); - let (ct, ss) = mlkem512_encaps(&ek, &m).unwrap(); + let mut ss = Secret::new([0u8; 32]); + let ct = mlkem512_encaps(&ek, &m, &mut ss).unwrap(); assert_eq!(&ct[..], &unhex(c)[..], "암호문 c 불일치"); assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); } @@ -187,7 +190,8 @@ fn kat_mlkem512_decaps() { for (dk, c, k) in cases { let dk: [u8; 1632] = unhex(dk).try_into().unwrap(); let c: [u8; 768] = unhex(c).try_into().unwrap(); - let ss = mlkem512_decaps(&c, &dk); + let mut ss = Secret::new([0u8; 32]); + mlkem512_decaps(&c, &dk, &mut ss); assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); } } @@ -200,7 +204,7 @@ fn kat_mlkem512_rejects_malformed_ek() { let m = [0u8; 32]; assert!( matches!( - mlkem512_encaps(&ek, &m), + mlkem512_encaps(&ek, &m, &mut Secret::new([0u8; 32])), Err(Error::InvalidEncapsulationKey) ), "비정규 ek 미거부" @@ -245,7 +249,8 @@ fn kat_mlkem768_keygen() { for (d, z, ek, dk) in cases { let d: [u8; 32] = unhex(d).try_into().unwrap(); let z: [u8; 32] = unhex(z).try_into().unwrap(); - let kp = mlkem768_keygen(&d, &z); + let mut kp = MLKEM768KeyPair::default(); + mlkem768_keygen(&d, &z, &mut kp); assert_eq!(&kp.ek[..], &unhex(ek)[..], "ek 불일치"); assert_eq!(&kp.dk.expose()[..], &unhex(dk)[..], "dk 불일치"); } @@ -289,7 +294,8 @@ fn kat_mlkem768_encaps() { for (ek, m, c, k) in cases { let ek: [u8; 1184] = unhex(ek).try_into().unwrap(); let m: [u8; 32] = unhex(m).try_into().unwrap(); - let (ct, ss) = mlkem768_encaps(&ek, &m).unwrap(); + let mut ss = Secret::new([0u8; 32]); + let ct = mlkem768_encaps(&ek, &m, &mut ss).unwrap(); assert_eq!(&ct[..], &unhex(c)[..], "암호문 c 불일치"); assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); } @@ -363,7 +369,8 @@ fn kat_mlkem768_decaps() { for (dk, c, k) in cases { let dk: [u8; 2400] = unhex(dk).try_into().unwrap(); let c: [u8; 1088] = unhex(c).try_into().unwrap(); - let ss = mlkem768_decaps(&c, &dk); + let mut ss = Secret::new([0u8; 32]); + mlkem768_decaps(&c, &dk, &mut ss); assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); } } @@ -376,7 +383,7 @@ fn kat_mlkem768_rejects_malformed_ek() { let m = [0u8; 32]; assert!( matches!( - mlkem768_encaps(&ek, &m), + mlkem768_encaps(&ek, &m, &mut Secret::new([0u8; 32])), Err(Error::InvalidEncapsulationKey) ), "비정규 ek 미거부" @@ -421,7 +428,8 @@ fn kat_mlkem1024_keygen() { for (d, z, ek, dk) in cases { let d: [u8; 32] = unhex(d).try_into().unwrap(); let z: [u8; 32] = unhex(z).try_into().unwrap(); - let kp = mlkem1024_keygen(&d, &z); + let mut kp = MLKEM1024KeyPair::default(); + mlkem1024_keygen(&d, &z, &mut kp); assert_eq!(&kp.ek[..], &unhex(ek)[..], "ek 불일치"); assert_eq!(&kp.dk.expose()[..], &unhex(dk)[..], "dk 불일치"); } @@ -465,7 +473,8 @@ fn kat_mlkem1024_encaps() { for (ek, m, c, k) in cases { let ek: [u8; 1568] = unhex(ek).try_into().unwrap(); let m: [u8; 32] = unhex(m).try_into().unwrap(); - let (ct, ss) = mlkem1024_encaps(&ek, &m).unwrap(); + let mut ss = Secret::new([0u8; 32]); + let ct = mlkem1024_encaps(&ek, &m, &mut ss).unwrap(); assert_eq!(&ct[..], &unhex(c)[..], "암호문 c 불일치"); assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); } @@ -539,7 +548,8 @@ fn kat_mlkem1024_decaps() { for (dk, c, k) in cases { let dk: [u8; 3168] = unhex(dk).try_into().unwrap(); let c: [u8; 1568] = unhex(c).try_into().unwrap(); - let ss = mlkem1024_decaps(&c, &dk); + let mut ss = Secret::new([0u8; 32]); + mlkem1024_decaps(&c, &dk, &mut ss); assert_eq!(&ss.expose()[..], &unhex(k)[..], "공유 비밀 k 불일치"); } } @@ -552,7 +562,7 @@ fn kat_mlkem1024_rejects_malformed_ek() { let m = [0u8; 32]; assert!( matches!( - mlkem1024_encaps(&ek, &m), + mlkem1024_encaps(&ek, &m, &mut Secret::new([0u8; 32])), Err(Error::InvalidEncapsulationKey) ), "비정규 ek 미거부" From 50397a00cad34b47926423303402996e85b2356b Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:23 +0900 Subject: [PATCH 62/65] =?UTF-8?q?mldsa=20=ED=82=A4=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EC=9E=90=EB=A6=AC=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=EB=A1=9C=20=EC=A0=84=ED=99=98=20-=20`keygen=5Finterna?= =?UTF-8?q?l`=20=EA=B3=BC=20`sk=5Fdecode`=20=EB=A5=BC=20out=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20-?= =?UTF-8?q?=20PrivateKey=20PublicKey=20=EC=97=90=20Default=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20Secret=20=ED=95=84=EB=93=9C=EB=A5=BC=20`init=5F?= =?UTF-8?q?with`=20=EB=A1=9C=20=EA=B8=B0=EB=A1=9D=ED=95=B4=20move=20?= =?UTF-8?q?=EC=9E=94=EB=A5=98=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Fable 5 --- mldsa/src/keys.rs | 80 ++++++++++++++++++++++++++++------------------- mldsa/src/lib.rs | 14 ++++++--- mldsa/src/sign.rs | 3 +- 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/mldsa/src/keys.rs b/mldsa/src/keys.rs index a0ca514..080b5f6 100644 --- a/mldsa/src/keys.rs +++ b/mldsa/src/keys.rs @@ -27,6 +27,28 @@ pub struct PrivateKey { pub t0: Secret>, } +impl Default for PrivateKey { + fn default() -> Self { + Self { + rho: [0u8; 32], + k_seed: Secret::new([0u8; 32]), + tr: [0u8; 64], + s1: Secret::new(PolyVec::::new_zero()), + s2: Secret::new(PolyVec::::new_zero()), + t0: Secret::new(PolyVec::::new_zero()), + } + } +} + +impl Default for PublicKey { + fn default() -> Self { + Self { + rho: [0u8; 32], + t1: PolyVec::::new_zero(), + } + } +} + #[inline(always)] fn is_negative_ct(v: i32) -> Choice { Choice::from_u8(((v >> 31) & 1) as u8) @@ -55,7 +77,9 @@ fn power2round_vec(t: &PolyVec) -> (PolyVec, PolyVec) { pub fn keygen_internal( xi: &[u8; 32], -) -> Result<(PublicKey, PrivateKey), Error> { + pk: &mut PublicKey, + sk: &mut PrivateKey, +) -> Result<(), Error> { let mut seed_input = [0u8; 34]; seed_input[..32].copy_from_slice(xi); seed_input[32] = K as u8; @@ -75,17 +99,17 @@ pub fn keygen_internal( let a_hat = expand_a::(&rho)?; let (mut s1, mut s2pv) = expand_s::(&rho_prime)?; - let s2 = Secret::new(s2pv); + sk.s2.init_with(|dst| *dst = s2pv); s2pv.zeroize(); - let s1_original = Secret::new(s1); + sk.s1.init_with(|dst| *dst = s1); s1.ntt(); let mut t = a_hat.multiply_vector(&s1); t.intt(); - t = t.add(&s2); + t = t.add(sk.s2.expose()); let (t1, mut t0pv) = power2round_vec(&t); - let t0 = Secret::new(t0pv); + sk.t0.init_with(|dst| *dst = t0pv); t0pv.zeroize(); let mut shake_tr = SHAKE256::new(); @@ -98,15 +122,11 @@ pub fn keygen_internal( let mut tr = [0u8; 64]; shake_tr.finalize_into(&mut tr); - let pk = PublicKey { rho, t1 }; - let sk = PrivateKey { - rho, - k_seed: Secret::new(k_seed), - tr, - s1: s1_original, - s2, - t0, - }; + pk.rho = rho; + pk.t1 = t1; + sk.rho = rho; + sk.k_seed.init_with(|dst| *dst = k_seed); + sk.tr = tr; k_seed.zeroize(); expanded.zeroize(); @@ -114,7 +134,7 @@ pub fn keygen_internal( s1.zeroize(); t.zeroize(); - Ok((pk, sk)) + Ok(()) } pub fn pk_encode(pk: &PublicKey) -> [u8; PK_LEN] { @@ -171,7 +191,8 @@ pub fn sk_encode( sk_bytes: &[u8; SK_LEN], -) -> PrivateKey { + out: &mut PrivateKey, +) { let eta_bw = bitlen((2 * ETA) as u32); let s1_len = L * 32 * eta_bw; let s2_len = K * 32 * eta_bw; @@ -183,32 +204,25 @@ pub fn sk_decode> = - Secret::new(polyvec_bit_unpack_eta(&sk_bytes[off..off + s1_len], ETA)); + out.s1 + .init_with(|dst| *dst = polyvec_bit_unpack_eta(&sk_bytes[off..off + s1_len], ETA)); off += s1_len; - let s2: Secret> = - Secret::new(polyvec_bit_unpack_eta(&sk_bytes[off..off + s2_len], ETA)); + out.s2 + .init_with(|dst| *dst = polyvec_bit_unpack_eta(&sk_bytes[off..off + s2_len], ETA)); off += s2_len; - let t0: Secret> = Secret::new(polyvec_bit_unpack_t0(&sk_bytes[off..off + t0_len])); + out.t0 + .init_with(|dst| *dst = polyvec_bit_unpack_t0(&sk_bytes[off..off + t0_len])); - PrivateKey { - rho, - k_seed, - tr, - s1, - s2, - t0, - } + out.rho = rho; + out.tr = tr; } diff --git a/mldsa/src/lib.rs b/mldsa/src/lib.rs index 3ae3d31..c509ec5 100644 --- a/mldsa/src/lib.rs +++ b/mldsa/src/lib.rs @@ -92,7 +92,7 @@ mod params { pub const SIG_LEN_87: usize = 4627; } -use keys::{keygen_internal, pk_encode, sk_encode}; +use keys::{PrivateKey, PublicKey, keygen_internal, pk_encode, sk_encode}; use params::*; use sign::{sign_internal, verify_internal}; @@ -121,7 +121,9 @@ impl MLDSA44 { /// # Errors /// 내부 연산 실패 시 `Error::InternalError` 반환 pub fn keygen(xi: &[u8; 32]) -> Result<([u8; PK_LEN_44], [u8; SK_LEN_44]), Error> { - let (pk, sk) = keygen_internal::(xi)?; + let mut pk = PublicKey::::default(); + let mut sk = PrivateKey::::default(); + keygen_internal::(xi, &mut pk, &mut sk)?; let pk_bytes = pk_encode::(&pk); let sk_bytes = sk_encode::(&sk); Ok((pk_bytes, sk_bytes)) @@ -236,7 +238,9 @@ impl MLDSA65 { /// # Errors /// 내부 연산 실패 시 `Error::InternalError` 반환 pub fn keygen(xi: &[u8; 32]) -> Result<([u8; PK_LEN_65], [u8; SK_LEN_65]), Error> { - let (pk, sk) = keygen_internal::(xi)?; + let mut pk = PublicKey::::default(); + let mut sk = PrivateKey::::default(); + keygen_internal::(xi, &mut pk, &mut sk)?; let pk_bytes = pk_encode::(&pk); let sk_bytes = sk_encode::(&sk); Ok((pk_bytes, sk_bytes)) @@ -351,7 +355,9 @@ impl MLDSA87 { /// # Errors /// 내부 연산 실패 시 `Error::InternalError` 반환 pub fn keygen(xi: &[u8; 32]) -> Result<([u8; PK_LEN_87], [u8; SK_LEN_87]), Error> { - let (pk, sk) = keygen_internal::(xi)?; + let mut pk = PublicKey::::default(); + let mut sk = PrivateKey::::default(); + keygen_internal::(xi, &mut pk, &mut sk)?; let pk_bytes = pk_encode::(&pk); let sk_bytes = sk_encode::(&sk); Ok((pk_bytes, sk_bytes)) diff --git a/mldsa/src/sign.rs b/mldsa/src/sign.rs index d555046..f0b79e1 100644 --- a/mldsa/src/sign.rs +++ b/mldsa/src/sign.rs @@ -240,7 +240,8 @@ pub fn sign_internal< m_prime: &[u8], rnd: &[u8; 32], ) -> Result<[u8; SIG_LEN], Error> { - let sk: PrivateKey = sk_decode::(sk_bytes); + let mut sk: PrivateKey = PrivateKey::default(); + sk_decode::(sk_bytes, &mut sk); let mut s1_hat = sk.s1; s1_hat.ntt(); From 9a04b1a5653dc6705fa3d6e188957275f2008354 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:36:30 +0900 Subject: [PATCH 63/65] =?UTF-8?q?=EB=AC=B8=EC=84=9C=EC=99=80=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B0=B1=EC=8B=A0=20-=20AI=5FSCOPE=20=ED=95=9C?= =?UTF-8?q?=EA=B5=AD=EC=96=B4=ED=8C=90=20=EC=8B=A0=EA=B7=9C=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=209=EA=B1=B4=EC=9D=84=20=EC=98=81=EB=AC=B8=ED=8C=90?= =?UTF-8?q?=EC=97=90=20=EB=B2=88=EC=97=AD=20=EB=B0=98=EC=98=81=20-=20?= =?UTF-8?q?=EC=A7=80=EC=B9=A8=20=EB=AC=B8=EB=8B=A8=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=85=B8=EC=96=B4=20=EB=8C=80=EC=83=81=20?= =?UTF-8?q?=EC=98=A4=EC=97=AD=20=EC=88=98=EC=A0=95=20-=20=EB=A6=B4?= =?UTF-8?q?=EB=A6=AC=EC=A6=88=20=ED=94=84=EB=A1=9C=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EB=B3=B4=EA=B0=95=EA=B3=BC=20INTERNAL=5F*?= =?UTF-8?q?=20=EC=9D=B4=EA=B7=B8=EB=85=B8=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- AI_SCOPE.md | 7 +++++++ AI_SCOPE_EN.md | 18 ++++++++++++++++-- Cargo.toml | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 66cd9ff..b67f341 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ Cargo.lock CLAUDE.md .planning/ cavp/ -.claude/ \ No newline at end of file +.claude/ +INTERNAL_* \ No newline at end of file diff --git a/AI_SCOPE.md b/AI_SCOPE.md index 23f0845..97fdd07 100644 --- a/AI_SCOPE.md +++ b/AI_SCOPE.md @@ -70,8 +70,12 @@ ### ML-DSA +- 비밀키 언패킹 관련 문제 발견 (Fable 5) + ### ML-KEM +- 복호화 과정에서, 내부적인 타이밍 오라클은 평문 검사 오라클이므로 CCA 안전성이 무너지는 문제 발견 (Fable 5) + ### SHA2 ### SHA3 @@ -80,6 +84,8 @@ ### X25519 +- 공유비밀 반환값이 호출자로 전혀 나가지 않도록 1바이트만 반환하게 좁혀도 스택에 전체 공유비밀 2벌이 남는 문제 발견 (Fable 5) + ### Zeroize - 테스트 코드 `zeroize_readback.rs` 작성 (Opus 5) @@ -87,6 +93,7 @@ - `Secret` 구조체의 Drop이 임의 T를 비트 제로화하는 문제, `zeroize_flat`이 safe 함수처리되어 있지만 invalid value를 생성하는 문제를 해결하기 위한 해결책으로, `Zeroable` 마커 트레이트에 관한 아이디어를 얻었으며 뼈대 코드, 주석을 작성 - 단, 다른 모듈 및 크레이트에 적용하고 응용하는 방식은 **사람이 직접 작성** - `Secret` 구조체에 상수-시간 프리미티브 적용 아이디어 (Fable 5) +- `Secret` 가 이동을 건너뛰면서 Drop 보장을 잃는 문제 발견 (Fable 5) ## 지침과 사용 프롬프트에 관해 diff --git a/AI_SCOPE_EN.md b/AI_SCOPE_EN.md index 404a05e..81149f1 100644 --- a/AI_SCOPE_EN.md +++ b/AI_SCOPE_EN.md @@ -48,14 +48,21 @@ Where multiple categories are separated by a slash, that means 'OR'. ### AES +- Writing the test modules in [lib.rs](aes/src/lib.rs), [ghash.rs](aes/src/ghash.rs), [sbox.rs](aes/src/sbox.rs), [cbc.rs](aes/src/cbc.rs), [ctr.rs](aes/src/ctr.rs), and [gcm.rs](aes/src/gcm.rs) (Opus 4.8) +- The `bitsliced_sbox` function in `ghash.rs` (Opus 4.8) +- Writing the KCMVP quick-test harness modules [kcmvp_cbc_ctr.rs](aes/tests/kcmvp_cbc_ctr.rs) and [kcmvp_gcm.rs](aes/tests/kcmvp_gcm.rs) (Fable 5) +- Writing the [baremetal.rs](aes/tests/baremetal.rs) test module (Fable 5) + ### BLAKE ### ChaCha20 ### Constant-Time -- Split-style `//` comments in individual files (Sonnet 4.5) +- Split-style `//` comments in [lib.rs](constant-time/src/lib.rs) (Sonnet 4.5) - [The `ct_sel_swap_value_roundtrip` test function in src/lib.rs](https://github.com/Quant-Off/elib-k0-nt/blob/7d3a0b8389fcc0a5bcbef22ae70fa380ce420614/constant-time/src/lib.rs#L732) (Opus 4.8) +- The idea for the `imp` module in [dit.rs](constant-time/src/dit.rs) and its `//` comments (Fable 5) +- The content of the "aarch64 DIT guard" section in [README.md](constant-time/README.md) (Opus 5) ### Ed448 @@ -63,8 +70,12 @@ Where multiple categories are separated by a slash, that means 'OR'. ### ML-DSA +- Discovery of an issue related to private key unpacking (Fable 5) + ### ML-KEM +- Discovery that, during decapsulation, an internal timing oracle amounts to a plaintext-checking oracle and therefore breaks CCA security (Fable 5) + ### SHA2 ### SHA3 @@ -73,6 +84,8 @@ Where multiple categories are separated by a slash, that means 'OR'. ### X25519 +- Discovery that two full copies of the shared secret remain on the stack even after narrowing the function to return only a single byte so that the shared secret never escapes to the caller (Fable 5) + ### Zeroize - Writing the `zeroize_readback.rs` test code (Opus 5) @@ -80,10 +93,11 @@ Where multiple categories are separated by a slash, that means 'OR'. - As a solution to the problem where `Drop` on the `Secret` struct bit-zeroizes an arbitrary `T`, and the problem where `zeroize_flat` is treated as a safe function while producing an invalid value, we drew the idea of a `Zeroable` marker trait from it, and wrote the skeleton code and comments for that. - However, how it is applied and adapted to other modules and crates was **written directly by a human**. - The idea of applying constant-time primitives to the `Secret` struct (Fable 5) +- Discovery of the problem where `Secret` skips over moves and thereby loses its `Drop` guarantee (Fable 5) ## On Guidelines and Prompts Used -The `CLAUDE.md` guideline document mainly contains comment-writing conventions, the local sandbox environment used to verify build behavior and implementation, and explanations for understanding the ecosystem. The `.planning` directory generated by the `CLAUDE.md` file and by [GSD (Git-Ship-Done) Core](https://github.com/open-gsd/gsd-core) contains specific details about the development environment (such as actual paths), so it has been ignored for security reasons. +The `CLAUDE.md` guideline document mainly contains comment-writing conventions, the local sandbox environment used to verify build behavior and implementation, and explanations for understanding the ecosystem. The `CLAUDE.md` file and the `.planning` directory generated by [GSD (Git-Ship-Done) Core](https://github.com/open-gsd/gsd-core) contain specific details about the development environment (such as actual paths), so they have been ignored for security reasons. For documentation and comment writing, we use a tone and context like "Add a docstring to @path/to/module." or "This module is responsible for ~; write and revise its comments...". For verifying that a module technically works, we keep a tone like "Based on the comments written in this module, verify how the module technically operates and write the result to @path/to/target.md." or "In this SHA3 implementation per the @path/to/pdf FIPS 202 specification, check whether this module's ~ function complies with the state-array labeling rules." diff --git a/Cargo.toml b/Cargo.toml index b204455..0a46434 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ panic = "abort" [profile.release] panic = "abort" lto = true # Link Time Optimization을 통한 바이너리 최소화 -opt-level = "z" # 크기 최적화 +opt-level = "z" # K0 커널로 인한 크기 최적화 필요 [workspace.dependencies] zeroize = { path = "zeroize", version = "1.1.0" } From 8cb48b9de2b62ab5f2e2d5b25b6d107174a69d06 Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:20:14 +0900 Subject: [PATCH 64/65] =?UTF-8?q?aes=20=ED=91=9C=EC=A4=80=20=EB=B2=A1?= =?UTF-8?q?=ED=84=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=203=EC=A2=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20ACVP=20AFT/MCT=20=ED=95=98=EB=84=A4?= =?UTF-8?q?=EC=8A=A4=20=EC=8B=A0=EC=84=A4=20-=20NIST=20CAVP=20GCM=20rsp=20?= =?UTF-8?q?=ED=95=98=EB=84=A4=EC=8A=A4=20=EC=8B=A0=EC=84=A4=20-=20Wychepro?= =?UTF-8?q?of=20GCM=20CBC-PKCS5=20=ED=95=98=EB=84=A4=EC=8A=A4=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aes/tests/acvp_aes.rs | 332 ++++++++++++++++++++++++++++++++++++ aes/tests/nist_gcm.rs | 178 +++++++++++++++++++ aes/tests/wycheproof_aes.rs | 189 ++++++++++++++++++++ 3 files changed, 699 insertions(+) create mode 100644 aes/tests/acvp_aes.rs create mode 100644 aes/tests/nist_gcm.rs create mode 100644 aes/tests/wycheproof_aes.rs diff --git a/aes/tests/acvp_aes.rs b/aes/tests/acvp_aes.rs new file mode 100644 index 0000000..b06e57a --- /dev/null +++ b/aes/tests/acvp_aes.rs @@ -0,0 +1,332 @@ +use aes::{AES256, AES256CBC, AES256CTR, BLOCK_SIZE, KEY_SIZE}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MCT_OUTER: usize = 100; +const MCT_INNER: usize = 1000; + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + if s == "-" { + return Vec::new(); + } + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn arr(v: &[u8], ctx: &str) -> [u8; N] { + v.try_into() + .unwrap_or_else(|_| panic!("{ctx}: 길이 {} (기대 {N})", v.len())) +} + +fn load_records(path: &Path) -> Option>> { + let text = fs::read_to_string(path).ok()?; + let records = text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.split_whitespace().map(str::to_string).collect()) + .collect::>>(); + (!records.is_empty()).then_some(records) +} + +fn vectors(name: &str) -> Option>> { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - ACVP {name} 생략"); + return None; + }; + let path = root.join("ACVP-AES").join(name); + match load_records(&path) { + Some(records) => Some(records), + None => { + eprintln!("{}: 벡터 없음 - ACVP {name} 생략", path.display()); + None + } + } +} + +fn report(label: &str, total: usize, mismatches: &[String]) { + assert!( + mismatches.is_empty(), + "{label} 불일치 {}건 / 전체 {total}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!("{label} {total}건 통과"); +} + +#[test] +fn acvp_ecb_aft() { + let Some(records) = vectors("ECB_AFT.txt") else { + return; + }; + let mut mismatches = Vec::new(); + for r in &records { + let ctx = format!("ECB AFT tcId {}", r[0]); + let key: [u8; KEY_SIZE] = arr(&hex_decode(&r[2], &ctx), &ctx); + let pt = hex_decode(&r[3], &ctx); + let ct = hex_decode(&r[4], &ctx); + let mut cipher = AES256::default(); + cipher.init(&key); + let encrypt = r[1] == "encrypt"; + let (input, expected) = if encrypt { (&pt, &ct) } else { (&ct, &pt) }; + assert!(input.len().is_multiple_of(BLOCK_SIZE), "{ctx}: 블록 정렬"); + let mut out = Vec::with_capacity(input.len()); + for chunk in input.chunks_exact(BLOCK_SIZE) { + let block: [u8; BLOCK_SIZE] = arr(chunk, &ctx); + let processed = if encrypt { + cipher.encrypt(&block) + } else { + cipher.decrypt(&block) + }; + out.extend_from_slice(&processed); + } + if out != *expected { + mismatches.push(ctx); + } + } + report("ACVP ECB AFT", records.len(), &mismatches); +} + +#[test] +fn acvp_cbc_aft() { + let Some(records) = vectors("CBC_AFT.txt") else { + return; + }; + let mut mismatches = Vec::new(); + for r in &records { + let ctx = format!("CBC AFT tcId {}", r[0]); + let key: [u8; KEY_SIZE] = arr(&hex_decode(&r[2], &ctx), &ctx); + let iv: [u8; BLOCK_SIZE] = arr(&hex_decode(&r[3], &ctx), &ctx); + let pt = hex_decode(&r[4], &ctx); + let ct = hex_decode(&r[5], &ctx); + let mut cbc = AES256CBC::default(); + cbc.init(&key); + let encrypt = r[1] == "encrypt"; + let (input, expected) = if encrypt { (&pt, &ct) } else { (&ct, &pt) }; + let mut out = vec![0u8; input.len()]; + let res = if encrypt { + cbc.encrypt(&iv, input, &mut out) + } else { + cbc.decrypt(&iv, input, &mut out) + }; + if res.is_err() || out != *expected { + mismatches.push(format!("{ctx} ({res:?})")); + } + } + report("ACVP CBC AFT", records.len(), &mismatches); +} + +#[test] +fn acvp_ctr_aft() { + let Some(records) = vectors("CTR_AFT.txt") else { + return; + }; + let mut mismatches = Vec::new(); + for r in &records { + let ctx = format!("CTR AFT tcId {}", r[0]); + let bits: usize = r[2] + .parse() + .unwrap_or_else(|_| panic!("{ctx}: payloadLen 파싱 실패")); + let key: [u8; KEY_SIZE] = arr(&hex_decode(&r[3], &ctx), &ctx); + let iv: [u8; BLOCK_SIZE] = arr(&hex_decode(&r[4], &ctx), &ctx); + let pt = hex_decode(&r[5], &ctx); + let ct = hex_decode(&r[6], &ctx); + let encrypt = r[1] == "encrypt"; + let (input, expected) = if encrypt { (&pt, &ct) } else { (&ct, &pt) }; + assert_eq!(input.len(), bits.div_ceil(8), "{ctx}: payload 길이"); + let mut ctr = AES256CTR::default(); + ctr.init(&key); + let mut out = vec![0u8; input.len()]; + ctr.apply_iv(&iv, input, &mut out) + .unwrap_or_else(|e| panic!("{ctx}: {e:?}")); + let rem = bits % 8; + if rem != 0 + && let Some(last) = out.last_mut() + { + *last &= 0xFFu8 << (8 - rem); + } + if out != *expected { + mismatches.push(ctx); + } + } + report("ACVP CTR AFT", records.len(), &mismatches); +} + +struct MctEntry { + key: Vec, + iv: Vec, + pt: Vec, + ct: Vec, +} + +fn load_mct(records: &[Vec], dir: &str, has_iv: bool) -> Vec { + let mut entries: Vec<(usize, MctEntry)> = records + .iter() + .filter(|r| r[0] == dir) + .map(|r| { + let ctx = format!("MCT {dir} idx {}", r[1]); + let idx: usize = r[1] + .parse() + .unwrap_or_else(|_| panic!("{ctx}: 인덱스 파싱 실패")); + let key = hex_decode(&r[2], &ctx); + let iv = if has_iv { + hex_decode(&r[3], &ctx) + } else { + Vec::new() + }; + let off = usize::from(has_iv); + let pt = hex_decode(&r[3 + off], &ctx); + let ct = hex_decode(&r[4 + off], &ctx); + (idx, MctEntry { key, iv, pt, ct }) + }) + .collect(); + entries.sort_by_key(|(idx, _)| *idx); + entries.into_iter().map(|(_, entry)| entry).collect() +} + +fn run_ecb_mct_chain(entries: &[MctEntry], encrypt: bool, failures: &mut Vec) { + let dir = if encrypt { "encrypt" } else { "decrypt" }; + assert_eq!(entries.len(), MCT_OUTER, "ECB MCT {dir} 체크포인트 수"); + let mut key: [u8; KEY_SIZE] = arr(&entries[0].key, "ECB MCT 시드 키"); + let seed_src = if encrypt { + &entries[0].pt + } else { + &entries[0].ct + }; + let mut seed: [u8; BLOCK_SIZE] = arr(seed_src, "ECB MCT 시드 블록"); + for (i, entry) in entries.iter().enumerate() { + let input = if encrypt { &entry.pt } else { &entry.ct }; + if entry.key != key || *input != seed { + failures.push(format!("ECB MCT {dir} {i}: 체인 입력 불일치")); + return; + } + let mut cipher = AES256::default(); + cipher.init(&key); + let mut block = seed; + let (mut prev, mut last) = ([0u8; BLOCK_SIZE], [0u8; BLOCK_SIZE]); + for _ in 0..MCT_INNER { + prev = last; + last = if encrypt { + cipher.encrypt(&block) + } else { + cipher.decrypt(&block) + }; + block = last; + } + let expected = if encrypt { &entry.ct } else { &entry.pt }; + if *expected != last { + failures.push(format!("ECB MCT {dir} {i}: 출력 불일치")); + return; + } + for (dst, src) in key[..BLOCK_SIZE].iter_mut().zip(prev.iter()) { + *dst ^= src; + } + for (dst, src) in key[BLOCK_SIZE..].iter_mut().zip(last.iter()) { + *dst ^= src; + } + seed = last; + } +} + +fn run_cbc_mct_chain(entries: &[MctEntry], encrypt: bool, failures: &mut Vec) { + let dir = if encrypt { "encrypt" } else { "decrypt" }; + assert_eq!(entries.len(), MCT_OUTER, "CBC MCT {dir} 체크포인트 수"); + let mut key: [u8; KEY_SIZE] = arr(&entries[0].key, "CBC MCT 시드 키"); + let mut iv: [u8; BLOCK_SIZE] = arr(&entries[0].iv, "CBC MCT 시드 IV"); + let seed_src = if encrypt { + &entries[0].pt + } else { + &entries[0].ct + }; + let mut seed: [u8; BLOCK_SIZE] = arr(seed_src, "CBC MCT 시드 블록"); + for (i, entry) in entries.iter().enumerate() { + let input = if encrypt { &entry.pt } else { &entry.ct }; + if entry.key != key || entry.iv != iv || *input != seed { + failures.push(format!("CBC MCT {dir} {i}: 체인 입력 불일치")); + return; + } + let mut cbc = AES256CBC::default(); + cbc.init(&key); + let (mut prev, mut last) = ([0u8; BLOCK_SIZE], [0u8; BLOCK_SIZE]); + if encrypt { + let mut cv = iv; + let mut cur = seed; + for _ in 0..MCT_INNER { + let mut out = [0u8; BLOCK_SIZE]; + cbc.encrypt(&cv, &cur, &mut out) + .unwrap_or_else(|e| panic!("CBC MCT {dir} {i}: {e:?}")); + prev = last; + last = out; + cur = cv; + cv = out; + } + } else { + let mut cv = iv; + let mut cur = seed; + for j in 0..MCT_INNER { + let mut out = [0u8; BLOCK_SIZE]; + cbc.decrypt(&cv, &cur, &mut out) + .unwrap_or_else(|e| panic!("CBC MCT {dir} {i}: {e:?}")); + prev = last; + last = out; + let next_ct = if j == 0 { iv } else { prev }; + cv = cur; + cur = next_ct; + } + } + let expected = if encrypt { &entry.ct } else { &entry.pt }; + if *expected != last { + failures.push(format!("CBC MCT {dir} {i}: 출력 불일치")); + return; + } + for (dst, src) in key[..BLOCK_SIZE].iter_mut().zip(prev.iter()) { + *dst ^= src; + } + for (dst, src) in key[BLOCK_SIZE..].iter_mut().zip(last.iter()) { + *dst ^= src; + } + iv = last; + seed = prev; + } +} + +#[test] +fn acvp_ecb_mct() { + let Some(records) = vectors("ECB_MCT.txt") else { + return; + }; + let mut failures = Vec::new(); + run_ecb_mct_chain(&load_mct(&records, "encrypt", false), true, &mut failures); + run_ecb_mct_chain(&load_mct(&records, "decrypt", false), false, &mut failures); + assert!(failures.is_empty(), "ACVP ECB MCT 실패: {failures:?}"); + println!("ACVP ECB MCT 2체인 x {MCT_OUTER}회 통과"); +} + +#[test] +fn acvp_cbc_mct() { + let Some(records) = vectors("CBC_MCT.txt") else { + return; + }; + let mut failures = Vec::new(); + run_cbc_mct_chain(&load_mct(&records, "encrypt", true), true, &mut failures); + run_cbc_mct_chain(&load_mct(&records, "decrypt", true), false, &mut failures); + assert!(failures.is_empty(), "ACVP CBC MCT 실패: {failures:?}"); + println!("ACVP CBC MCT 2체인 x {MCT_OUTER}회 통과"); +} diff --git a/aes/tests/nist_gcm.rs b/aes/tests/nist_gcm.rs new file mode 100644 index 0000000..c90725d --- /dev/null +++ b/aes/tests/nist_gcm.rs @@ -0,0 +1,178 @@ +use aes::{AES256GCM, Error, GCM_MIN_TAG_SIZE, KEY_SIZE}; +use std::fs; +use std::path::{Path, PathBuf}; + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn arr(v: &[u8], ctx: &str) -> [u8; N] { + v.try_into() + .unwrap_or_else(|_| panic!("{ctx}: 길이 {} (기대 {N})", v.len())) +} + +#[derive(Default)] +struct Case { + count: String, + key: Vec, + iv: Vec, + pt: Vec, + aad: Vec, + ct: Vec, + tag: Vec, + fail: bool, + started: bool, +} + +fn parse_rsp(text: &str, ctx: &str) -> Vec { + let mut cases = Vec::new(); + let mut cur = Case::default(); + for raw in text.lines() { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') || line.starts_with('[') { + continue; + } + if line == "FAIL" { + cur.fail = true; + continue; + } + let Some((name, value)) = line.split_once('=') else { + panic!("{ctx}: 해석 불가 행 {line:?}"); + }; + let (name, value) = (name.trim(), value.trim()); + if name == "Count" { + if cur.started { + cases.push(std::mem::take(&mut cur)); + } + cur.started = true; + cur.count = value.to_string(); + continue; + } + let field_ctx = format!("{ctx} Count {} {name}", cur.count); + let bytes = hex_decode(value, &field_ctx); + match name { + "Key" => cur.key = bytes, + "IV" => cur.iv = bytes, + "PT" => cur.pt = bytes, + "AAD" => cur.aad = bytes, + "CT" => cur.ct = bytes, + "Tag" => cur.tag = bytes, + other => panic!("{ctx}: 알 수 없는 필드 {other}"), + } + } + if cur.started { + cases.push(cur); + } + cases +} + +fn load_cases(name: &str) -> Option> { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - NIST GCM {name} 생략"); + return None; + }; + let path = root.join("NIST-GCM").join(name); + let Ok(text) = fs::read_to_string(&path) else { + eprintln!("{}: 벡터 없음 - NIST GCM {name} 생략", path.display()); + return None; + }; + let cases = parse_rsp(&text, name); + (!cases.is_empty()).then_some(cases) +} + +fn run_cases(name: &str, cases: &[Case], decrypt: bool) { + let (mut ran, mut skipped, mut failed_expected) = (0usize, 0usize, 0usize); + let mut mismatches = Vec::new(); + for case in cases { + if case.tag.len() < GCM_MIN_TAG_SIZE { + skipped += 1; + continue; + } + ran += 1; + let ctx = format!( + "{name} Count {} (iv {}B tag {}B pt {}B aad {}B)", + case.count, + case.iv.len(), + case.tag.len(), + if decrypt { + case.ct.len() + } else { + case.pt.len() + }, + case.aad.len() + ); + let key: [u8; KEY_SIZE] = arr(&case.key, &ctx); + let mut gcm = AES256GCM::default(); + gcm.init(&key); + if decrypt { + let mut pt_buf = vec![0u8; case.ct.len()]; + let res = gcm.decrypt_with_iv(&case.iv, &case.aad, &case.ct, &case.tag, &mut pt_buf); + let ok = match (&res, case.fail) { + (Err(Error::AuthenticationFailed), true) => { + failed_expected += 1; + true + } + (Ok(()), false) => pt_buf == case.pt, + _ => false, + }; + if !ok { + mismatches.push(format!("{ctx}: {res:?} (fail 기대 {})", case.fail)); + } + } else { + let mut ct_buf = vec![0u8; case.pt.len()]; + let mut tag_buf = vec![0u8; case.tag.len()]; + let res = gcm.encrypt_with_iv(&case.iv, &case.aad, &case.pt, &mut ct_buf, &mut tag_buf); + if !(res.is_ok() && ct_buf == case.ct && tag_buf == case.tag) { + mismatches.push(format!("{ctx}: 암호화 출력 불일치 ({res:?})")); + } + } + } + assert!( + mismatches.is_empty(), + "{name} 불일치 {}건 / 실행 {ran}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + if decrypt { + println!( + "{name} {ran}건 통과 (인증 실패 판정 {failed_expected}건 포함, 태그 96비트 미만 {skipped}건 스킵)" + ); + } else { + println!("{name} {ran}건 통과 (태그 96비트 미만 {skipped}건 스킵)"); + } +} + +#[test] +fn nist_cavs_gcm_encrypt_256() { + let Some(cases) = load_cases("gcmEncryptExtIV256.rsp") else { + return; + }; + run_cases("gcmEncryptExtIV256", &cases, false); +} + +#[test] +fn nist_cavs_gcm_decrypt_256() { + let Some(cases) = load_cases("gcmDecrypt256.rsp") else { + return; + }; + run_cases("gcmDecrypt256", &cases, true); +} diff --git a/aes/tests/wycheproof_aes.rs b/aes/tests/wycheproof_aes.rs new file mode 100644 index 0000000..40c3017 --- /dev/null +++ b/aes/tests/wycheproof_aes.rs @@ -0,0 +1,189 @@ +use aes::{AES256CBC, AES256GCM, BLOCK_SIZE, KEY_SIZE}; +use std::fs; +use std::path::{Path, PathBuf}; + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + if s == "-" { + return Vec::new(); + } + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn arr(v: &[u8], ctx: &str) -> [u8; N] { + v.try_into() + .unwrap_or_else(|_| panic!("{ctx}: 길이 {} (기대 {N})", v.len())) +} + +fn vectors(name: &str) -> Option>> { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - Wycheproof {name} 생략"); + return None; + }; + let path = root.join("Wycheproof-AES").join(name); + let Ok(text) = fs::read_to_string(&path) else { + eprintln!("{}: 벡터 없음 - Wycheproof {name} 생략", path.display()); + return None; + }; + let records = text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.split_whitespace().map(str::to_string).collect()) + .collect::>>(); + (!records.is_empty()).then_some(records) +} + +fn pkcs7_pad(msg: &[u8]) -> Vec { + let pad = BLOCK_SIZE - msg.len() % BLOCK_SIZE; + let mut out = msg.to_vec(); + out.resize(msg.len() + pad, pad as u8); + out +} + +fn pkcs7_unpad(buf: &[u8]) -> Option<&[u8]> { + let pad = *buf.last()? as usize; + if pad == 0 || pad > BLOCK_SIZE || pad > buf.len() { + return None; + } + buf[buf.len() - pad..] + .iter() + .all(|&b| b as usize == pad) + .then_some(&buf[..buf.len() - pad]) +} + +#[test] +fn wycheproof_gcm() { + let Some(records) = vectors("GCM.txt") else { + return; + }; + let (mut valid, mut invalid, mut acceptable) = (0usize, 0usize, 0usize); + let mut mismatches = Vec::new(); + for r in &records { + let ctx = format!("Wycheproof GCM tcId {} [{}]", r[0], r[8]); + let key: [u8; KEY_SIZE] = arr(&hex_decode(&r[2], &ctx), &ctx); + let iv = hex_decode(&r[3], &ctx); + let aad = hex_decode(&r[4], &ctx); + let msg = hex_decode(&r[5], &ctx); + let ct = hex_decode(&r[6], &ctx); + let tag = hex_decode(&r[7], &ctx); + let mut gcm = AES256GCM::default(); + gcm.init(&key); + let mut pt_buf = vec![0u8; ct.len()]; + let dec = gcm.decrypt_with_iv(&iv, &aad, &ct, &tag, &mut pt_buf); + match r[1].as_str() { + "valid" => { + valid += 1; + if !(dec.is_ok() && pt_buf == msg) { + mismatches.push(format!("{ctx}: 정상 입력 복호 실패 ({dec:?})")); + continue; + } + let mut ct_buf = vec![0u8; msg.len()]; + let mut tag_buf = vec![0u8; tag.len()]; + let enc = gcm.encrypt_with_iv(&iv, &aad, &msg, &mut ct_buf, &mut tag_buf); + if !(enc.is_ok() && ct_buf == ct && tag_buf == tag) { + mismatches.push(format!("{ctx}: 암호화 출력 불일치 ({enc:?})")); + } + } + "invalid" => { + invalid += 1; + if dec.is_ok() { + mismatches.push(format!("{ctx}: 위조 입력 수락")); + } + } + "acceptable" => { + acceptable += 1; + if dec.is_ok() && pt_buf != msg { + mismatches.push(format!("{ctx}: 수락 후 평문 불일치")); + } + } + other => panic!("{ctx}: 알 수 없는 판정 {other}"), + } + } + assert!( + mismatches.is_empty(), + "Wycheproof GCM 불일치 {}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!( + "Wycheproof GCM 256비트 {}건 통과 (valid {valid} / invalid {invalid} / acceptable {acceptable})", + records.len() + ); +} + +#[test] +fn wycheproof_cbc_pkcs5() { + let Some(records) = vectors("CBC_PKCS5.txt") else { + return; + }; + let (mut valid, mut invalid) = (0usize, 0usize); + let mut mismatches = Vec::new(); + for r in &records { + let ctx = format!("Wycheproof CBC tcId {} [{}]", r[0], r[6]); + let key: [u8; KEY_SIZE] = arr(&hex_decode(&r[2], &ctx), &ctx); + let iv: [u8; BLOCK_SIZE] = arr(&hex_decode(&r[3], &ctx), &ctx); + let msg = hex_decode(&r[4], &ctx); + let ct = hex_decode(&r[5], &ctx); + let mut cbc = AES256CBC::default(); + cbc.init(&key); + match r[1].as_str() { + "valid" => { + valid += 1; + let padded = pkcs7_pad(&msg); + let mut ct_buf = vec![0u8; padded.len()]; + let enc = cbc.encrypt(&iv, &padded, &mut ct_buf); + if !(enc.is_ok() && ct_buf == ct) { + mismatches.push(format!("{ctx}: 암호화 출력 불일치 ({enc:?})")); + continue; + } + let mut pt_buf = vec![0u8; ct.len()]; + let dec = cbc.decrypt(&iv, &ct, &mut pt_buf); + let stripped = dec.is_ok().then(|| pkcs7_unpad(&pt_buf)).flatten(); + if stripped != Some(&msg[..]) { + mismatches.push(format!("{ctx}: 복호 또는 패딩 제거 실패 ({dec:?})")); + } + } + "invalid" => { + invalid += 1; + let mut pt_buf = vec![0u8; ct.len()]; + match cbc.decrypt(&iv, &ct, &mut pt_buf) { + Err(_) => {} + Ok(()) => { + if pkcs7_unpad(&pt_buf).is_some() { + mismatches.push(format!("{ctx}: 잘못된 패딩 수락")); + } + } + } + } + other => panic!("{ctx}: 알 수 없는 판정 {other}"), + } + } + assert!( + mismatches.is_empty(), + "Wycheproof CBC-PKCS5 불일치 {}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!( + "Wycheproof CBC-PKCS5 256비트 {}건 통과 (valid {valid} / invalid {invalid})", + records.len() + ); +} From 44477a5b2d3c8be3ab9ab6b38c087aa63d4a99aa Mon Sep 17 00:00:00 2001 From: Quant-TheodoreFelix <53819958+Quant-TheodoreFelix@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:20:15 +0900 Subject: [PATCH 65/65] =?UTF-8?q?sha2=20sha3=20=ED=91=9C=EC=A4=80=20?= =?UTF-8?q?=EB=B2=A1=ED=84=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20ACVP=20AFT=20MCT=20LDT=20=ED=95=98=EB=84=A4?= =?UTF-8?q?=EC=8A=A4=20=EC=8B=A0=EC=84=A4=20-=20Wycheproof=20HMAC=20?= =?UTF-8?q?=ED=95=98=EB=84=A4=EC=8A=A4=20=EC=8B=A0=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sha2/tests/acvp_sha2.rs | 164 +++++++++++++++++++++++ sha2/tests/wycheproof_hmac.rs | 122 +++++++++++++++++ sha3/tests/acvp_sha3.rs | 240 ++++++++++++++++++++++++++++++++++ sha3/tests/wycheproof_hmac.rs | 124 ++++++++++++++++++ 4 files changed, 650 insertions(+) create mode 100644 sha2/tests/acvp_sha2.rs create mode 100644 sha2/tests/wycheproof_hmac.rs create mode 100644 sha3/tests/acvp_sha3.rs create mode 100644 sha3/tests/wycheproof_hmac.rs diff --git a/sha2/tests/acvp_sha2.rs b/sha2/tests/acvp_sha2.rs new file mode 100644 index 0000000..9f08a5b --- /dev/null +++ b/sha2/tests/acvp_sha2.rs @@ -0,0 +1,164 @@ +use sha2::{SHA2, SHA224, SHA256, SHA384, SHA512}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MCT_COUNTS: usize = 100; +const MCT_INNER: usize = 1000; + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + if s == "-" { + return Vec::new(); + } + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn load_lines(name: &str) -> Option> { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - ACVP {name} 생략"); + return None; + }; + let path = root.join("ACVP-SHA2").join(name); + let Ok(text) = fs::read_to_string(&path) else { + eprintln!("{}: 벡터 없음 - ACVP {name} 생략", path.display()); + return None; + }; + let lines = text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(str::to_string) + .collect::>(); + (!lines.is_empty()).then_some(lines) +} + +fn one_shot(msg: &[u8]) -> Vec { + let mut h = H::new(); + h.update(msg); + h.finalize().as_bytes().to_vec() +} + +fn run_aft(alg: &str) { + let Some(lines) = load_lines(&format!("{alg}_AFT.txt")) else { + return; + }; + let mut mismatches = Vec::new(); + for line in &lines { + let f = line.split_whitespace().collect::>(); + let ctx = format!("ACVP {alg} AFT tcId {}", f[0]); + let msg = hex_decode(f[1], &ctx); + let md = hex_decode(f[2], &ctx); + if one_shot::(&msg) != md { + mismatches.push(ctx); + } + } + assert!( + mismatches.is_empty(), + "{alg} AFT 불일치 {}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!("ACVP {alg} AFT {}건 통과", lines.len()); +} + +fn run_mct(alg: &str) { + let Some(lines) = load_lines(&format!("{alg}_MCT.txt")) else { + return; + }; + assert_eq!(lines.len(), 2 + MCT_COUNTS, "{alg} MCT 형식 오류"); + let version = lines[0].as_str(); + let mut seed = hex_decode(&lines[1], &format!("{alg} MCT seed")); + let init_len = seed.len(); + for (i, expected_hex) in lines[2..].iter().enumerate() { + let ctx = format!("ACVP {alg} MCT({version}) 체크포인트 {i}"); + let expected = hex_decode(expected_hex, &ctx); + let (mut a, mut b, mut c) = (seed.clone(), seed.clone(), seed); + let mut md = Vec::new(); + for _ in 0..MCT_INNER { + let mut msg = a.clone(); + msg.extend_from_slice(&b); + msg.extend_from_slice(&c); + match version { + "standard" => {} + "alternate" => msg.resize(init_len, 0), + other => panic!("{ctx}: 알 수 없는 MCT 버전 {other}"), + } + md = one_shot::(&msg); + a = b; + b = c; + c = md.clone(); + } + assert_eq!(md, expected, "{ctx} 불일치"); + seed = md; + } + println!("ACVP {alg} MCT({version}) 체크포인트 {MCT_COUNTS}개 통과"); +} + +fn run_ldt(alg: &str) { + let Some(lines) = load_lines(&format!("{alg}_LDT.txt")) else { + return; + }; + for line in &lines { + let f = line.split_whitespace().collect::>(); + let ctx = format!("ACVP {alg} LDT tcId {}", f[0]); + let content = hex_decode(f[1], &ctx); + let full_bits = f[2] + .parse::() + .unwrap_or_else(|_| panic!("{ctx}: 길이 해석 불가")); + let md = hex_decode(f[3], &ctx); + assert!(full_bits.is_multiple_of(8), "{ctx}: 비트 단위 길이"); + let total = full_bits / 8; + let chunk = content.repeat(65536 / content.len()); + let mut h = H::new(); + let mut remaining = total; + while remaining > 0 { + let n = remaining.min(chunk.len() as u64) as usize; + h.update(&chunk[..n]); + remaining -= n as u64; + } + assert_eq!(h.finalize().as_bytes(), md, "{ctx} 불일치"); + println!("ACVP {alg} LDT tcId {} ({} MiB) 통과", f[0], total >> 20); + } +} + +#[test] +fn acvp_sha2_aft() { + run_aft::("SHA2-224"); + run_aft::("SHA2-256"); + run_aft::("SHA2-384"); + run_aft::("SHA2-512"); +} + +#[test] +fn acvp_sha2_mct() { + run_mct::("SHA2-224"); + run_mct::("SHA2-256"); + run_mct::("SHA2-384"); + run_mct::("SHA2-512"); +} + +#[test] +#[ignore = "대용량 (알고리즘당 15GiB) - cargo test --release -- --ignored 로 실행"] +fn acvp_sha2_ldt() { + run_ldt::("SHA2-224"); + run_ldt::("SHA2-256"); + run_ldt::("SHA2-384"); + run_ldt::("SHA2-512"); +} diff --git a/sha2/tests/wycheproof_hmac.rs b/sha2/tests/wycheproof_hmac.rs new file mode 100644 index 0000000..3d0dd6a --- /dev/null +++ b/sha2/tests/wycheproof_hmac.rs @@ -0,0 +1,122 @@ +use sha2::{SHA2, SHA224, SHA256, SHA384, SHA512}; +use std::fs; +use std::path::{Path, PathBuf}; + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + if s == "-" { + return Vec::new(); + } + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn vectors(name: &str) -> Option>> { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - Wycheproof {name} 생략"); + return None; + }; + let path = root.join("Wycheproof-HMAC").join(name); + let Ok(text) = fs::read_to_string(&path) else { + eprintln!("{}: 벡터 없음 - Wycheproof {name} 생략", path.display()); + return None; + }; + let records = text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.split_whitespace().map(str::to_string).collect()) + .collect::>>(); + (!records.is_empty()).then_some(records) +} + +fn one_shot(msg: &[u8]) -> Vec { + let mut h = H::new(); + h.update(msg); + h.finalize().as_bytes().to_vec() +} + +fn hmac(block: usize, key: &[u8], msg: &[u8]) -> Vec { + let mut k0 = if key.len() > block { + one_shot::(key) + } else { + key.to_vec() + }; + k0.resize(block, 0); + let mut inner = H::new(); + inner.update(&k0.iter().map(|b| b ^ 0x36).collect::>()); + inner.update(msg); + let inner_md = inner.finalize(); + let mut outer = H::new(); + outer.update(&k0.iter().map(|b| b ^ 0x5C).collect::>()); + outer.update(inner_md.as_bytes()); + outer.finalize().as_bytes().to_vec() +} + +fn run_hmac(name: &str, block: usize) { + let Some(records) = vectors(&format!("{name}.txt")) else { + return; + }; + let (mut valid, mut invalid) = (0usize, 0usize); + let mut mismatches = Vec::new(); + for r in &records { + let ctx = format!("Wycheproof {name} tcId {} [{}]", r[0], r[6]); + let tag_bits = r[2] + .parse::() + .unwrap_or_else(|_| panic!("{ctx}: tagSize 해석 불가")); + let key = hex_decode(&r[3], &ctx); + let msg = hex_decode(&r[4], &ctx); + let tag = hex_decode(&r[5], &ctx); + let computed = hmac::(block, &key, &msg); + let matches = computed[..tag_bits / 8] == tag[..]; + match r[1].as_str() { + "valid" => { + valid += 1; + if !matches { + mismatches.push(format!("{ctx}: 정상 태그 불일치")); + } + } + "invalid" => { + invalid += 1; + if matches { + mismatches.push(format!("{ctx}: 위조 태그 수락")); + } + } + other => panic!("{ctx}: 알 수 없는 판정 {other}"), + } + } + assert!( + mismatches.is_empty(), + "{name} 불일치 {}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!( + "Wycheproof {name} {}건 통과 (valid {valid} / invalid {invalid})", + records.len() + ); +} + +#[test] +fn wycheproof_hmac_sha2() { + run_hmac::("HMAC_SHA224", 64); + run_hmac::("HMAC_SHA256", 64); + run_hmac::("HMAC_SHA384", 128); + run_hmac::("HMAC_SHA512", 128); +} diff --git a/sha3/tests/acvp_sha3.rs b/sha3/tests/acvp_sha3.rs new file mode 100644 index 0000000..67aa4d8 --- /dev/null +++ b/sha3/tests/acvp_sha3.rs @@ -0,0 +1,240 @@ +#![allow(clippy::upper_case_acronyms)] + +use sha3::{SHA3, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, XOF}; +use std::fs; +use std::path::{Path, PathBuf}; + +const MCT_COUNTS: usize = 100; +const MCT_INNER: usize = 1000; + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + if s == "-" { + return Vec::new(); + } + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn load_lines(name: &str) -> Option> { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - ACVP {name} 생략"); + return None; + }; + let path = root.join("ACVP-SHA3").join(name); + let Ok(text) = fs::read_to_string(&path) else { + eprintln!("{}: 벡터 없음 - ACVP {name} 생략", path.display()); + return None; + }; + let lines = text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(str::to_string) + .collect::>(); + (!lines.is_empty()).then_some(lines) +} + +fn one_shot(msg: &[u8]) -> Vec { + let mut h = H::new(); + h.update(msg); + h.finalize().as_bytes().to_vec() +} + +fn xof_bits(msg: &[u8], out_bits: usize, msb_align: bool) -> Vec { + let mut h = H::new(); + h.update(msg); + let mut out = vec![0u8; out_bits.div_ceil(8)]; + h.finalize_into(&mut out); + let rem = out_bits % 8; + if rem != 0 + && let Some(last) = out.last_mut() + { + let kept = *last & ((1u8 << rem) - 1); + *last = if msb_align { kept << (8 - rem) } else { kept }; + } + out +} + +fn run_aft(alg: &str) { + let Some(lines) = load_lines(&format!("{alg}_AFT.txt")) else { + return; + }; + let mut mismatches = Vec::new(); + for line in &lines { + let f = line.split_whitespace().collect::>(); + let ctx = format!("ACVP {alg} AFT tcId {}", f[0]); + let msg = hex_decode(f[1], &ctx); + let md = hex_decode(f[2], &ctx); + if one_shot::(&msg) != md { + mismatches.push(ctx); + } + } + assert!( + mismatches.is_empty(), + "{alg} AFT 불일치 {}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!("ACVP {alg} AFT {}건 통과", lines.len()); +} + +fn run_mct(alg: &str) { + let Some(lines) = load_lines(&format!("{alg}_MCT.txt")) else { + return; + }; + assert_eq!(lines.len(), 2 + MCT_COUNTS, "{alg} MCT 형식 오류"); + assert_eq!(lines[0], "standard", "{alg} MCT 버전 미지원"); + let mut md = hex_decode(&lines[1], &format!("{alg} MCT seed")); + for (i, expected_hex) in lines[2..].iter().enumerate() { + let ctx = format!("ACVP {alg} MCT 체크포인트 {i}"); + let expected = hex_decode(expected_hex, &ctx); + for _ in 0..MCT_INNER { + md = one_shot::(&md); + } + assert_eq!(md, expected, "{ctx} 불일치"); + } + println!("ACVP {alg} MCT 체크포인트 {MCT_COUNTS}개 통과"); +} + +fn run_shake(alg: &str, kind: &str) { + let Some(lines) = load_lines(&format!("{alg}_{kind}.txt")) else { + return; + }; + let mut mismatches = Vec::new(); + for line in &lines { + let f = line.split_whitespace().collect::>(); + let ctx = format!("ACVP {alg} {kind} tcId {}", f[0]); + let msg = hex_decode(f[1], &ctx); + let out_bits = f[2] + .parse::() + .unwrap_or_else(|_| panic!("{ctx}: outLen 해석 불가")); + let md = hex_decode(f[3], &ctx); + if xof_bits::(&msg, out_bits, kind == "FIPS202") != md { + mismatches.push(format!("{ctx} (outLen {out_bits})")); + } + } + assert!( + mismatches.is_empty(), + "{alg} {kind} 불일치 {}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!("ACVP {alg} {kind} {}건 통과", lines.len()); +} + +fn run_shake_mct(alg: &str) { + let Some(lines) = load_lines(&format!("{alg}_MCT.txt")) else { + return; + }; + assert_eq!(lines.len(), 2 + MCT_COUNTS, "{alg} MCT 형식 오류"); + let bounds = lines[0].split_whitespace().collect::>(); + let min_bytes = bounds[0].parse::().unwrap() / 8; + let max_bytes = bounds[1].parse::().unwrap() / 8; + let range = max_bytes - min_bytes + 1; + let mut out = hex_decode(&lines[1], &format!("{alg} MCT seed")); + let mut out_len = max_bytes; + for (i, line) in lines[2..].iter().enumerate() { + let f = line.split_whitespace().collect::>(); + let ctx = format!("ACVP {alg} MCT 체크포인트 {i}"); + let expected_bits = f[0] + .parse::() + .unwrap_or_else(|_| panic!("{ctx}: outLen 해석 불가")); + let expected = hex_decode(f[1], &ctx); + for _ in 0..MCT_INNER { + let mut msg = [0u8; 16]; + let n = out.len().min(16); + msg[..n].copy_from_slice(&out[..n]); + out = xof_bits::(&msg, out_len * 8, false); + let rm = u16::from_be_bytes([out[out.len() - 2], out[out.len() - 1]]) as usize; + out_len = min_bytes + rm % range; + } + assert_eq!(out.len() * 8, expected_bits, "{ctx} 출력 길이 불일치"); + assert_eq!(out, expected, "{ctx} 불일치"); + } + println!("ACVP {alg} MCT 체크포인트 {MCT_COUNTS}개 통과"); +} + +fn run_ldt(alg: &str) { + let Some(lines) = load_lines(&format!("{alg}_LDT.txt")) else { + return; + }; + for line in &lines { + let f = line.split_whitespace().collect::>(); + let ctx = format!("ACVP {alg} LDT tcId {}", f[0]); + let content = hex_decode(f[1], &ctx); + let full_bits = f[2] + .parse::() + .unwrap_or_else(|_| panic!("{ctx}: 길이 해석 불가")); + let md = hex_decode(f[3], &ctx); + assert!(full_bits.is_multiple_of(8), "{ctx}: 비트 단위 길이"); + let total = full_bits / 8; + let chunk = content.repeat(65536 / content.len()); + let mut h = H::new(); + let mut remaining = total; + while remaining > 0 { + let n = remaining.min(chunk.len() as u64) as usize; + h.update(&chunk[..n]); + remaining -= n as u64; + } + assert_eq!(h.finalize().as_bytes(), md, "{ctx} 불일치"); + println!("ACVP {alg} LDT tcId {} ({} MiB) 통과", f[0], total >> 20); + } +} + +#[test] +fn acvp_sha3_aft() { + run_aft::("SHA3-224"); + run_aft::("SHA3-256"); + run_aft::("SHA3-384"); + run_aft::("SHA3-512"); +} + +#[test] +fn acvp_sha3_mct() { + run_mct::("SHA3-224"); + run_mct::("SHA3-256"); + run_mct::("SHA3-384"); + run_mct::("SHA3-512"); +} + +#[test] +fn acvp_shake_aft() { + run_shake::("SHAKE-128", "AFT"); + run_shake::("SHAKE-128", "VOT"); + run_shake::("SHAKE-128", "FIPS202"); + run_shake::("SHAKE-256", "AFT"); + run_shake::("SHAKE-256", "VOT"); + run_shake::("SHAKE-256", "FIPS202"); +} + +#[test] +fn acvp_shake_mct() { + run_shake_mct::("SHAKE-128"); + run_shake_mct::("SHAKE-256"); +} + +#[test] +#[ignore = "대용량 (알고리즘당 15GiB) - cargo test --release -- --ignored 로 실행"] +fn acvp_sha3_ldt() { + run_ldt::("SHA3-224"); + run_ldt::("SHA3-256"); + run_ldt::("SHA3-384"); + run_ldt::("SHA3-512"); +} diff --git a/sha3/tests/wycheproof_hmac.rs b/sha3/tests/wycheproof_hmac.rs new file mode 100644 index 0000000..cc37241 --- /dev/null +++ b/sha3/tests/wycheproof_hmac.rs @@ -0,0 +1,124 @@ +#![allow(clippy::upper_case_acronyms)] + +use sha3::{SHA3, SHA3_224, SHA3_256, SHA3_384, SHA3_512}; +use std::fs; +use std::path::{Path, PathBuf}; + +fn cavp_root() -> Option { + if let Ok(dir) = std::env::var("KCMVP_CAVP_DIR") { + return Some(PathBuf::from(dir)); + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent()?.join("cavp"); + root.is_dir().then_some(root) +} + +fn hex_decode(s: &str, ctx: &str) -> Vec { + if s == "-" { + return Vec::new(); + } + assert!(s.len().is_multiple_of(2), "{ctx}: 홀수 길이 16진 문자열"); + s.as_bytes() + .chunks_exact(2) + .map(|pair| { + let hi = char::from(pair[0]).to_digit(16); + let lo = char::from(pair[1]).to_digit(16); + match (hi, lo) { + (Some(hi), Some(lo)) => ((hi << 4) | lo) as u8, + _ => panic!("{ctx}: 16진수가 아닌 문자"), + } + }) + .collect() +} + +fn vectors(name: &str) -> Option>> { + let Some(root) = cavp_root() else { + eprintln!("cavp 디렉터리 없음 - Wycheproof {name} 생략"); + return None; + }; + let path = root.join("Wycheproof-HMAC").join(name); + let Ok(text) = fs::read_to_string(&path) else { + eprintln!("{}: 벡터 없음 - Wycheproof {name} 생략", path.display()); + return None; + }; + let records = text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.split_whitespace().map(str::to_string).collect()) + .collect::>>(); + (!records.is_empty()).then_some(records) +} + +fn one_shot(msg: &[u8]) -> Vec { + let mut h = H::new(); + h.update(msg); + h.finalize().as_bytes().to_vec() +} + +fn hmac(block: usize, key: &[u8], msg: &[u8]) -> Vec { + let mut k0 = if key.len() > block { + one_shot::(key) + } else { + key.to_vec() + }; + k0.resize(block, 0); + let mut inner = H::new(); + inner.update(&k0.iter().map(|b| b ^ 0x36).collect::>()); + inner.update(msg); + let inner_md = inner.finalize(); + let mut outer = H::new(); + outer.update(&k0.iter().map(|b| b ^ 0x5C).collect::>()); + outer.update(inner_md.as_bytes()); + outer.finalize().as_bytes().to_vec() +} + +fn run_hmac(name: &str, block: usize) { + let Some(records) = vectors(&format!("{name}.txt")) else { + return; + }; + let (mut valid, mut invalid) = (0usize, 0usize); + let mut mismatches = Vec::new(); + for r in &records { + let ctx = format!("Wycheproof {name} tcId {} [{}]", r[0], r[6]); + let tag_bits = r[2] + .parse::() + .unwrap_or_else(|_| panic!("{ctx}: tagSize 해석 불가")); + let key = hex_decode(&r[3], &ctx); + let msg = hex_decode(&r[4], &ctx); + let tag = hex_decode(&r[5], &ctx); + let computed = hmac::(block, &key, &msg); + let matches = computed[..tag_bits / 8] == tag[..]; + match r[1].as_str() { + "valid" => { + valid += 1; + if !matches { + mismatches.push(format!("{ctx}: 정상 태그 불일치")); + } + } + "invalid" => { + invalid += 1; + if matches { + mismatches.push(format!("{ctx}: 위조 태그 수락")); + } + } + other => panic!("{ctx}: 알 수 없는 판정 {other}"), + } + } + assert!( + mismatches.is_empty(), + "{name} 불일치 {}건: {:?}", + mismatches.len(), + &mismatches[..mismatches.len().min(10)] + ); + println!( + "Wycheproof {name} {}건 통과 (valid {valid} / invalid {invalid})", + records.len() + ); +} + +#[test] +fn wycheproof_hmac_sha3() { + run_hmac::("HMAC_SHA3-224", 144); + run_hmac::("HMAC_SHA3-256", 136); + run_hmac::("HMAC_SHA3-384", 104); + run_hmac::("HMAC_SHA3-512", 72); +}