노트북 07 — 실제로 동작할까?#

스트레스 테스트를 돌리고, FIPS 203 크기 표를 확인하며, 벤치마크를 수행합니다.

import time
import os
import numpy as np
import matplotlib.pyplot as plt
from pqc_edu.params import ML_KEM_512, ML_KEM_768, ML_KEM_1024, ALL
from pqc_edu.ml_kem import ml_kem_keygen, ml_kem_encaps, ml_kem_decaps, _ml_kem_keygen_from_seeds

왕복(roundtrip) 스트레스 테스트 — 파라미터 집합당 200회 반복#

복호화 실패는 0이어야 합니다.

iters = 200
for p in ALL:
    fails = 0
    for _ in range(iters):
        ek, dk = ml_kem_keygen(p)
        K1, ct = ml_kem_encaps(p, ek)
        K2 = ml_kem_decaps(p, dk, ct)
        if K1 != K2:
            fails += 1
    print(f"{p.name}: {iters - fails}/{iters} matched, {fails} failure(s)")
ML-KEM-512: 200/200 matched, 0 failure(s)
ML-KEM-768: 200/200 matched, 0 failure(s)
ML-KEM-1024: 200/200 matched, 0 failure(s)

크기 표 vs FIPS 203 Table 3#

print(f"{'name':13} {'ek':>6} {'ct':>6} {'K':>3}   FIPS 203 Table 3")
print('-' * 52)
for p in ALL:
    ek, dk = ml_kem_keygen(p)
    _, ct = ml_kem_encaps(p, ek)
    print(f"{p.name:13} {len(ek):>6} {len(ct):>6} {32:>3}   matches={len(ek)==p.ek_bytes and len(ct)==p.ct_bytes}")
name              ek     ct   K   FIPS 203 Table 3
----------------------------------------------------
ML-KEM-512       800    768  32   matches=True
ML-KEM-768      1184   1088  32   matches=True
ML-KEM-1024     1568   1568  32   matches=True

결정성(determinism): 같은 시드는 반드시 같은 키를 생성해야 합니다#

d = b'\xaa' * 32
z = b'\xbb' * 32
ek1, dk1 = _ml_kem_keygen_from_seeds(ML_KEM_768, d, z)
ek2, dk2 = _ml_kem_keygen_from_seeds(ML_KEM_768, d, z)
print('identical ek:', ek1 == ek2)
print('identical dk:', dk1 == dk2)
identical ek: True
identical dk: True

벤치마크 — 파라미터 집합당, 연산당 30회 반복#

iters = 30
ops = ['keygen', 'encaps', 'decaps']
results = {p.name: {} for p in ALL}
for p in ALL:
    # keygen
    t0 = time.time()
    eks_dks = [ml_kem_keygen(p) for _ in range(iters)]
    results[p.name]['keygen'] = (time.time() - t0) / iters
    # encaps
    ek_sample, dk_sample = eks_dks[0]
    t0 = time.time()
    pairs = [ml_kem_encaps(p, ek_sample) for _ in range(iters)]
    results[p.name]['encaps'] = (time.time() - t0) / iters
    # decaps
    K_sample, ct_sample = pairs[0]
    t0 = time.time()
    for _ in range(iters):
        ml_kem_decaps(p, dk_sample, ct_sample)
    results[p.name]['decaps'] = (time.time() - t0) / iters

# Plot
names = [p.name for p in ALL]
x = np.arange(len(names))
width = 0.25
fig, ax = plt.subplots(figsize=(8, 4))
for i, op in enumerate(ops):
    vals = [results[n][op] * 1000 for n in names]
    ax.bar(x + (i - 1) * width, vals, width, label=op)
ax.set_xticks(x); ax.set_xticklabels(names)
ax.set_ylabel('ms per operation')
ax.set_title(f'ML-KEM operation timing ({iters} iters each)')
ax.legend(); plt.tight_layout(); plt.show()

print('\ntiming (ms):')
for n in names:
    print(f"  {n}: " + ', '.join(f"{op}={results[n][op]*1000:.1f}" for op in ops))
../_images/9731b325d740ff0196d1aeb4c98afc9b4c573fc94b3b6fb3ed88d0e44af7fcb5.png
timing (ms):
  ML-KEM-512: keygen=7.0, encaps=8.8, decaps=12.3
  ML-KEM-768: keygen=11.0, encaps=13.1, decaps=18.1
  ML-KEM-1024: keygen=15.5, encaps=18.0, decaps=24.7

주의: 이것은 순수 Python 교육용 구현입니다. 상수 인수(최적화된 C 대비 약 1000배 느림)가 k=2/3/4의 알고리즘적 차이를 압도합니다. 그러나 전체 패턴(FO 재암호화 때문에 decaps ≈ keygen + encaps)은 여전히 보여야 합니다.