노트북 06 — ML-KEM (FIPS 203)#
목표: 표준 문서를 동작하는 구현과 나란히 읽습니다. 세 가지 파라미터 집합을 보여주고, K-PKE와 FO-변환 래퍼를 따라가며, 한 번의 완전한 encapsulation/decapsulation을 실행합니다.
from pqc_edu.params import ALL
header = f"{'name':12} {'k':>3} {'eta1':>5} {'eta2':>5} {'du':>3} {'dv':>3} {'ek_bytes':>9} {'ct_bytes':>9}"
print(header)
print('-' * len(header))
for p in ALL:
print(f"{p.name:12} {p.k:>3} {p.eta1:>5} {p.eta2:>5} {p.du:>3} {p.dv:>3} {p.ek_bytes:>9} {p.ct_bytes:>9}")
name k eta1 eta2 du dv ek_bytes ct_bytes
--------------------------------------------------------
ML-KEM-512 2 3 2 10 4 800 768
ML-KEM-768 3 2 2 10 4 1184 1088
ML-KEM-1024 4 2 2 11 5 1568 1568
K-PKE.KeyGen (FIPS 203 Algorithm 13)#
Input: d in {0,1}^256
(rho, sigma) <- G(d)
A_hat[i][j] <- SampleNTT(rho, j, i) for i,j in 0..k-1
s[i] <- CBD_eta1(PRF(sigma, N)) for i in 0..k-1
e[i] <- CBD_eta1(PRF(sigma, N+k+i)) for i in 0..k-1
s_hat <- NTT(s)
e_hat <- NTT(e)
t_hat <- A_hat . s_hat + e_hat
ek <- ByteEncode(t_hat, 12) || rho
dk <- ByteEncode(s_hat, 12)
from pqc_edu.params import ML_KEM_512
from pqc_edu.ml_kem import k_pke_keygen
keys = k_pke_keygen(ML_KEM_512, b'\x00' * 32)
print('ek bytes:', len(keys.ek), '(expected 800 for ML-KEM-512)')
print('dk bytes:', len(keys.dk), '(expected 768)')
ek bytes: 800 (expected 800 for ML-KEM-512)
dk bytes: 768 (expected 768)
K-PKE.Encrypt (Algorithm 14) — compression 단계가 암호문 크기를 줄이는 핵심이지만, 동시에 복호화 실패 가능성을 도입하는 원인이기도 합니다.#
from pqc_edu.ml_kem import k_pke_encrypt, k_pke_decrypt, compress, decompress
from pqc_edu.ntt import Q
import numpy as np
import os
m = os.urandom(32)
ct = k_pke_encrypt(ML_KEM_512, keys.ek, m, os.urandom(32))
m2 = k_pke_decrypt(ML_KEM_512, keys.dk, ct)
print('K-PKE match:', m == m2)
# Show the compression effect on a random coefficient vector.
from pqc_edu.polyring import Poly
rng = np.random.default_rng(0)
p = Poly(rng.integers(0, Q, 256), Q)
p_round = decompress(compress(p, 10), 10)
# distortion in Z_q, wrapped into [-q/2, q/2]
diff = (p.coeffs - p_round.coeffs) % Q
diff = np.where(diff > Q // 2, diff - Q, diff)
print(f'compress(d=10) round-trip: max |distortion| = {int(np.max(np.abs(diff)))} out of q/2 = {Q//2}')
K-PKE match: True
compress(d=10) round-trip: max |distortion| = 2 out of q/2 = 1664