- filippo.io/mlkem768은 NIST 표준화가 진행 중인 ML-KEM-768을 순수 Go로 구현해, Go 생태계에서 양자 내성 키 교환을 검토할 수 있게 함
- 약 500줄 코드, 200줄 주석, 650줄 테스트로 구성되며
golang.org/x/crypto/sha3외 의존성이 없어 Go 표준 라이브러리 내부 패키지로 올리기 쉬운 형태임 - pq-crystals 참조 구현을 포팅하지 않고 FIPS 203 명세를 직접 따라 작성해, 명세만으로 상호운용 구현이 가능한지 검증함
- 가장 까다로운 영역은 압축·해제와 상수 시간 연산이며, Barrett reduction을 사용해 참조 구현 계열에서 생길 수 있던 가변 시간 DIV 명령 위험을 피함
- 성능 최적화가 1차 목표는 아니지만 Bob 경로는 Go의 X25519·P-256과 비슷하고 Alice 경로도 2배 미만 수준이라, 단순한 구현으로도 실사용 가능한 속도를 보임
ML-KEM-768 순수 Go 구현
- filippo.io/mlkem768은 ML-KEM-768의 순수 Go 구현이며, 정확성과 가독성을 우선함
- ML-KEM은 이전에 Kyber로 알려졌고, NIST 표준화 과정에 있는 양자 내성 키 교환 메커니즘임
- 패키지는 약 500줄 코드, 200줄 주석, 650줄 테스트로 구성됨
- 의존성은
golang.org/x/crypto/sha3뿐임 - Go 표준 라이브러리에 upstream하는 것이 목표이며, 초기에는 opt-in
crypto/tls실험에서 쓰이는 내부 전용 패키지로 계획됨
FIPS 203을 그대로 따라간 구현 방식
- 이 구현은 pq-crystals 참조 라이브러리를 포팅하지 않고, 다른 코드베이스를 자세히 읽지 않은 상태에서 처음부터 작성됨
- 핵심 목표는 명세만으로 상호운용 가능한 구현을 만들 수 있는지 확인하는 것이었음
- FIPS 203 문서는 상세한 의사코드, 완전한 정의, 일관된 타입 정보를 제공해 구현 가이드로 적합했음
- 함수명, 변수명, 연산 순서는 리뷰와 학습을 쉽게 하기 위해 FIPS 명세를 최대한 반영함
- ML-KEM 구현에 필요한 수학 배경은 Enough Polynomials and Linear Algebra to Implement Kyber에서 별도로 정리됨
압축·해제와 상수 시간 구현
- 남은 핵심 구현 과제는 세 가지였음
- 소수 3329에 대한 모듈러 산술 구현
[0, 3329)값을[0, 2ᵈ)로 매핑하고 되돌리는 압축·해제 함수 구현- 상수 시간 연산 보장
- 모듈러 산술은 RSA와 타원곡선 구현 경험이 축적된 덕분에 비교적 쉬웠고, 작은 소수는 구현을 단순하게 만들었음
- 압축과 해제가 가장 어려운 부분이었음
- 명세는 분수와 반올림 규칙으로 추상적으로 정의함
- 실제 구현은 상수 시간 산술과 비트 연산으로 처리해야 함
- 참조 구현과 그 포팅 구현 다수는 컴파일러 최적화와 플랫폼에 따라 가변 시간 DIV 명령이 될 수 있는 나눗셈을 사용했음
- 이 패키지는 처음부터 Barrett reduction을 사용해 영향을 받지 않았고, BoringSSL도 같은 접근을 사용함
ML-KEM-768만 대상으로 삼은 이유
- 구현은 ML-KEM의 세 보안 수준
-512,-768,-1024중 ML-KEM-768만 대상으로 함 - Kyber 팀은 새로운 암호해석에 대한 더 보수적인 보안 여유를 위해
-512보다-768사용을 권장함 -1024는 256비트 보안 수준과 같은 이유, 즉 규정 준수와 강도 맞춤(strength matching)을 위한 선택지로 설명됨- 실험 또는 표준화 중인 대부분의 프로토콜이 ML-KEM-768에 모였기 때문에 단일 수준 대상화가 비용을 거의 늘리지 않음
- 단일 대상화는 움직이는 부분을 줄여 가독성, 보안, 성능에 유리함
- 예를 들어 1·4·10·12비트 정수 직렬화를 범용 인코더 하나로 처리하지 않고 전용 인코더·디코더로 나눔
- ML-KEM-768만 대상으로 했기 때문에 5비트와 11비트 인코딩을 구현할 필요가 없었음
테스트 전략과 공개 테스트 벡터
- 테스트는 이 패키지의 보안 보증 전략에서 가독성 다음으로 중요한 축임
- 기본 테스트는 키 생성, 캡슐화, 디캡슐화 라운드트립과 95% 이상 테스트 커버리지를 포함함
- 추가 테스트 범위는 다음을 포함함
- NIST 및 다른 구현에서 얻은 테스트 벡터와의 상호운용성 확인
- 3329 모듈러 덧셈·뺄셈·곱셈의 모든 입력 조합을 변수 시간 방식으로 계산한 기대값과 비교
- 압축·해제를
math/big.Rat기준으로 전수 테스트 - 사전 계산 상수가 정의와 일치하는지 확인
- 모든 함수 입력에서 길이가 너무 길거나 짧을 때 적절한 오류가 나는지 확인
- Sophie Schmieg가 제공했고 향후 Wycheproof에 포함될 테스트 벡터 실행
- 자체 테스트 벡터는 다른 구현에서도 재사용할 수 있도록 CCTV 프로젝트의 일부로 공개됨
- CCTV 벡터는 각 중간 단계와 부분 알고리듬을 테스트·디버깅할 수 있는 중간값을 포함함
특수 테스트 벡터가 잡아내는 오류
- Negative test vectors는 계수가 3329보다 큰 잘못된 캡슐화 키를 제공함
- Kyber와 NIST 팀의 벡터는 정상 입력 중심이어서 이런 벡터가 자주 요청됨
- 3329부터
2¹²-1까지의 모든 값과 모든 계수 위치를 개별 테스트함 - 나머지 계수를 공유해 1–3MiB 데이터를 12–28KiB로 압축함
- “Unlucky” vectors는 XOF 읽기가 비정상적으로 많이 필요한 경우를 테스트함
SampleNTT에서 SHAKE-128 XOF로부터 575바이트 이상 읽어야 하는 공개 키이며, 일반적으로는 확률2⁻³⁸로 발생함- Sophie의 벡터는 더 브루트포스되어 최대 591바이트가 필요함
- strcmp vectors는
ML-KEM.Decaps에서strcmp()를 쓰는 구현을 실패하게 만듦- 디캡슐화에서 ciphertext와
K-PKE.Encrypt출력을 비교할 때 0바이트가 있으면strcmp()가 비교를 조기 종료할 수 있음
- 디캡슐화에서 ciphertext와
- Accumulated vectors는 참조 pq-crystals 구현에서 파생됨
- 300MB 랜덤 벡터 출력을 저장하지 않고, 결정적 RNG로 테스트 중 재생성한 뒤 해시를 기대값과 비교함
- 참조 구현의 10k개를 넘어 100만 개 랜덤 테스트 해시도 만들 수 있음
- 완료 후 추가된 여러 테스트에서도
filippo.io/mlkem768문제는 발견되지 않았고, negative vector가 주요 구현의 결함을 찾은 보고 사례는 최소 1건 있음
성능 결과
- 성능은 이 패키지나 Go 암호화 패키지의 1차 목표가 아니지만, 유용할 만큼 충분히 빨라야 함
- ML-KEM은 충분히 빠르며, 이 단순 구현도 어셈블리 최적화된 Go의 P-256 및 X25519 구현과 경쟁 가능한 수준임
- 비교는 키 설정에서 각 측이 수행해야 하는 전체 작업을 기준으로 해야 함
- ECDH는 고정 베이스포인트 1회를 포함해 스칼라 곱셈 2회를 수행함
- KEM은 한쪽에서 키 생성과 디캡슐화를, 다른 쪽에서 캡슐화를 수행함
- ECDH는 대칭적이지만 ML-KEM 키 설정은 비대칭적임
- 벤치마크에서 “Alice”는 키 생성과 디캡슐화를 수행하고, “Bob”은 캡슐화를 수행함
- 디캡슐화에는 입력 ciphertext와 결과가 맞는지 확인하기 위한 전체 암호화가 포함됨
- Alice는 암호화, 복호화, 키 생성을 수행해 Bob보다 오래 걸림
- 결과적으로 Bob은 X25519 또는 P-256만큼 빠르고, Alice는 그 2배 미만임
- BoringSSL과 libcrux 같은 빠른 ML-KEM 구현과 비교하면 이 패키지는 대략 2배 시간이 걸림
벤치마크 수치와 최적화 여지
- 측정 수치는 다음과 같음
- macOS arm64에서
ECDH/P256-8은 49.43µs,ECDH/X25519-8은 77.46µs - 같은 환경에서
RoundTrip/Alice-8은 109.4µs,RoundTrip/Bob-8은 56.19µs - Linux amd64에서
ECDH/P256-4는 78.88µs,ECDH/X25519-4는 115.6µs - 같은 환경에서
RoundTrip/Alice-4는 223.8µs,RoundTrip/Bob-4는 114.7µs
- macOS arm64에서
- 구현은 힙 할당을 줄이는 등 고성능 Go 패턴을 따름
x/crypto/sha3를 힙 할당 없이 사용할 수 있도록 재작업했지만, Apple M2에서 부정적 효과가 있어 아직 병합하지 않았고 위 벤치마크에도 포함되지 않음- 남은 최적화 여지는 명확함
- 키 생성과 디캡슐화가 같은 값에서 행렬을 샘플링하므로, Alice 측에서 두 작업이 연속 수행될 때 행렬을 저장하면 약 10% 시간 절약 가능
sha3읽기 경로에서 복사를 줄일 가능성이 있음- 이후에는 필드 구현 최적화가 필요함
ML-KEM 구현으로 Kyber v3 지원하기
- NIST는 Kyber Round 3 제출본에 몇 가지 작은 변경을 했고, FIPS 초안 1.3절에 요약됨
- Kyber v3 또는 “draft00” 기준 실험 프로토콜이 몇 가지 있으며, 주요 배포 PQ TLS 키 교환도 여기에 포함됨
- 별도 패키지 없이 ML-KEM 구현으로 Kyber v3를 지원할 수 있음
- 변경 중 하나는 공개 키의 비정규 계수 인코딩이라는 예외 사례에 검증을 추가함
- 정상 구현은 그런 키를 만들지 않으므로 FIPS 초안대로 거부할 수 있음
- 이 동작은 Kyber-on-ML-KEM 구현을 식별 가능하게 만들지만, 그 외에는 해롭지 않음
- 다른 변경은 CSPRNG 입력에 적용되던 해싱 단계를 제거한 것임
- 입력 바이트가 랜덤이므로 어떤 당사자도 차이를 구분할 수 없음
- 가장 큰 변경은 공유 비밀에 ciphertext를 해시하던 동작임
- 이 차이는 상호운용을 막을 수 있음
- ML-KEM으로 공유 비밀
K를 만든 뒤SHAKE-256(K || SHA3-256(c))[:32]를 적용하면 Kyber 공유 비밀을 만들 수 있음 - ML-KEM 추상화를 깨지 않아도 됨
- Kyber와 ML-KEM은 모두 디캡슐화에서 implicit rejection을 위해 비밀과 ciphertext를 해시함
- ML-KEM 위에 위 키 파생을 적용하면 implicit rejection에서 ciphertext를 두 번 해시함
- implicit rejection 출력은 설계상 예측 불가능하고 상호운용 대상이 아니므로 문제가 되지 않음