dev.to

Caddy 2.11's default post-quantum key exchange sends six times more handshake bytes

Caddy 2.11의 기본 양자내성 키 교환, TLS 핸드셰이크 바이트를 6배 늘립니다

Caddy 2.11.4는 기본값으로 X25519와 ML-KEM-768을 결합한 하이브리드 키 교환을 사용합니다. 암호 연산 시간은 거의 눈에 띄지 않았지만, 키 교환 핸드셰이크 크기는 440바이트에서 2,704바이트로 6.1배 증가했으며 Caddy 로그에는 실제 협상 그룹이 기록되지 않습니다.

AI 요약

Caddy는 2.10부터 양자내성 키 교환(Post-Quantum Key Exchange)을 기본 활성화했으며, 이 글은 Caddy 2.11.4와 최신 TLS 클라이언트를 직접 연결해 실제 비용을 측정합니다. 핵심은 “양자내성 암호는 느리다”는 막연한 인상이 아니라, CPU 연산 비용보다 네트워크를 오가는 핸드셰이크 메시지 크기 증가가 더 중요한 운영상의 비용일 수 있다는 점입니다.

■ X25519와 ML-KEM-768을 함께 사용하는 이유

현재 TLS에서 널리 쓰이는 X25519는 고전적인 타원곡선 기반 Diffie-Hellman 키 교환입니다. 충분히 강력한 양자 컴퓨터가 등장하면 이 수학적 기반이 깨질 수 있습니다. NIST가 2024년에 표준화한 ML-KEM은 이 공격에 대응하기 위한 키 캡슐화 메커니즘(KEM)입니다. Caddy의 기본 그룹인 X25519MLKEM768은 X25519와 ML-KEM-768을 동시에 실행하고 두 결과를 결합하는 하이브리드 방식입니다. 따라서 두 알고리즘 중 하나가 나중에 깨지더라도 다른 하나가 안전하다면 연결의 보안성을 유지하도록 설계되어 있습니다.

이 방식을 지금 도입하는 배경에는 ‘지금 수집하고 나중에 복호화하는(harvest now, decrypt later)’ 공격이 있습니다. 공격자가 현재의 암호화 트래픽을 저장해 두었다가 미래에 양자 컴퓨터를 확보한 뒤 고전적 키 교환을 역산할 수 있다는 시나리오입니다. 하이브리드 키 교환은 전환 이후 새로 기록되는 트래픽에는 대응하지만, 순수한 고전적 핸드셰이크로 이미 수집된 트래픽을 보호해 주지는 않습니다. 따라서 프록시를 업그레이드했다는 사실만으로 실제 클라이언트 트래픽이 양자내성 그룹을 사용한다고 간주해서는 안 됩니다.

■ 기본 협상 확인과 핸드셰이크 크기 측정

작성자는 Docker에서 자체 서명 인증서를 사용하는 Caddy 2.11.4를 실행하고, 네이티브 ML-KEM 지원이 처음 들어간 안정 버전인 OpenSSL 3.5.7 클라이언트로 접속했습니다. `openssl s_client`의 협상 결과는 다음과 같습니다.

`Negotiated TLS1.3 group: X25519MLKEM768` `New, TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256`

별도 설정 없이도 지원하는 클라이언트와 Caddy가 X25519MLKEM768을 협상했으며, 글에서는 이를 Caddy의 기본 동작과 일치하는 결과로 설명합니다. 이후 `openssl s_client -msg`를 사용해 키 교환을 담는 TLS 1.3 `ClientHello`와 `ServerHello`의 레코드 크기를 각각 측정했습니다.

고전적 X25519를 강제했을 때 `ClientHello`는 318바이트, `ServerHello`는 122바이트로 합계 440바이트였습니다. 반면 기본 하이브리드 그룹인 X25519MLKEM768에서는 `ClientHello`가 1,494바이트, `ServerHello`가 1,210바이트로 늘어나 합계 2,704바이트가 됐습니다. 전체 증가율은 6.1배이며, 개별 메시지 기준으로는 각각 4.7배와 9.9배입니다. ML-KEM-768의 공개 키와 암호문이 X25519의 32바이트 포인트보다 훨씬 크기 때문에 발생하는 증가입니다.

■ CPU 비용보다 네트워크 비용이 더 눈에 띕니다

작성자는 네트워크 시험만으로 암호 연산 비용을 추정하지 않고 `openssl speed -seconds 2`로 원시 KEM 연산을 별도로 측정했습니다. ML-KEM-768의 결과는 키 생성 0.000063초, 캡슐화 0.000038초, 디캡슐화 0.000059초였고, 초당 처리량은 각각 15,789.5회, 26,176.0회, 16,915.0회였습니다. X25519는 키 생성 0.000046초, 캡슐화 0.000095초, 디캡슐화 0.000043초였으며 초당 21,709.5회, 10,526.5회, 23,497.0회를 처리했습니다.

이 측정에서 ML-KEM-768의 키 생성은 X25519보다 약 37% 느렸지만, 캡슐화는 두 배 이상 빨랐습니다. 두 알고리즘의 모든 개별 연산은 65마이크로초 이내에 끝났습니다. 글은 이 시간이 밀리초 단위의 일반적인 네트워크 왕복 시간보다 대략 세 자릿수 작기 때문에, 보통의 연결에서는 CPU 연산 차이가 사용자가 체감할 핵심 요소가 아니라고 설명합니다.

실제로 Docker의 루프백 속도 네트워크에서 각 그룹으로 50회 연속 핸드셰이크를 수행하고 3회씩 시험한 결과, X25519와 X25519MLKEM768 모두 가장 빠른 측정값이 1.038초, 연결당 20.8밀리초로 동일했습니다. 다만 이 결과를 두 방식의 비용이 완전히 같다는 뜻으로 해석하지는 않았습니다. 프로세스 생성과 TCP 연결 비용이 100마이크로초 이하인 암호 연산을 압도했기 때문입니다. 글에서 이 실험의 정직한 결론은 새 기본값의 실제 벽시계 비용이 이 방식의 연결별 하니스로는 측정하기 어려울 정도로 작다는 것이며, 기능에 직접 귀속할 수 있는 수치는 핸드셰이크 바이트 증가라는 점입니다.

■ 제한된 네트워크에서 추가 왕복이 문제가 될 수 있습니다

6배 커진 메시지는 연결이 실제로 사용되기 전부터 전송됩니다. 모바일, 위성, 혼잡한 네트워크처럼 느리거나 손실이 있는 링크에서는 더 커진 `ClientHello`가 초기 TCP 혼잡 윈도우에 들어가지 않을 수 있고, 그 결과 이전에는 없던 추가 왕복이 발생할 수 있습니다. 일반적인 광대역 연결에서는 큰 문제가 아닐 가능성이 있지만, 모바일 사용자가 많은 서비스라면 CPU 사용률보다 실제 `ClientHello` 크기와 지연 시간을 확인해야 한다고 글은 설명합니다.

■ 구형 클라이언트는 자동으로 고전적 그룹으로 폴백합니다

ML-KEM을 전혀 지원하지 않는 클라이언트의 동작을 확인하기 위해 2020년 3월 출시된 OpenSSL 1.1.1f로 같은 Caddy 서버에 접속한 결과, TLS 1.3 핸드셰이크가 정상적으로 완료됐습니다. 인증서 검증 오류는 자체 서명 CA를 신뢰하지 않아서 발생한 별개의 문제였습니다. 클라이언트는 ML-KEM을 이해하지 못했지만 Caddy는 오류나 경고, 추가 설정 없이 해당 클라이언트가 지원하는 그룹으로 폴백했습니다. 즉 PQC 전용 그룹이 아니라 하이브리드 그룹을 기본값으로 선택했기 때문에 기존 클라이언트와의 호환성이 유지됩니다.

■ Caddy 액세스 로그만으로는 전환 비율을 알 수 없습니다

Caddy의 구조화된 JSON 액세스 로그에는 TLS 버전, 암호 스위트, 서버 이름, ECH(Encrypted Client Hello) 사용 여부가 기록됩니다. 그러나 실제로 어떤 키 교환 그룹을 협상했는지는 기본 로그 형식과 JSON 형식 모두에 포함되지 않습니다. 작성자가 같은 요청을 X25519와 X25519MLKEM768으로 각각 강제한 뒤 로그를 비교했을 때도 타임스탬프와 소스 포트를 제외하면 두 줄이 동일했습니다.

따라서 운영 환경에서 실제 트래픽 중 어느 비율이 양자내성 그룹을 사용하고 어느 비율이 고전적 그룹으로 폴백했는지 확인하려면 Caddy 액세스 로그만으로는 부족합니다. 글은 패킷 캡처 도구인 `tshark`로 `ClientHello`의 `key_share` 확장을 관찰하거나, 프록시 앞단의 클라이언트에서 협상 결과를 확인해야 한다고 안내합니다.

■ 재현 시 주의할 점

재현 환경은 Docker 네트워크와 `tls internal`, `respond "hello"`를 포함한 간단한 Caddyfile입니다. Debian trixie에는 ML-KEM을 지원하는 OpenSSL 3.5 계열이 들어 있지만, Ubuntu 24.04의 기본 OpenSSL 3.0.13은 ML-KEM을 알지 못해 이를 조용히 협상하지 않습니다. 또한 `openssl s_client`에 HTTP 요청 없이 표준 입력의 EOF만 전달하면 서버가 연결을 닫을 때까지 대기할 수 있습니다. 작성자는 이 상태에서 50회 연결이 2분 넘게 걸려 PQC 관련 지연으로 오해했지만, `GET / HTTP/1.1`과 `Connection: close`를 보내도록 수정한 뒤 실제 핸드셰이크 비용이 약 20밀리초임을 확인했습니다.

원문: dev.to / 번역·요약: Trawling