pyca/cryptography 유지관리자들이 OpenSSL 3 이후의 성능 저하, 복잡도 증가, 테스트·검증 부족, 메모리 안전성 문제를 지적하고 OpenSSL 의존도를 줄이기 위한 향후 방향을 설명한다.
발행일: 2026년 1월 14일
지난 12년 동안 저희(폴 케러(Paul Kehrer)와 알렉스 게이너(Alex Gaynor))는 Python cryptography 라이브러리(또는 pyca/cryptography, cryptography.io로도 알려져 있음)를 유지관리해 왔습니다. 그 전체 기간 동안 핵심 암호 알고리즘 제공을 위해 OpenSSL에 의존해 왔습니다. 지난 10월, 우리는 OpenSSL 컨퍼런스에서 발표를 통해 우리의 경험을 공유했습니다. 이 발표는 OpenSSL의 방향성에 대해 우리가 점점 더 크게 느끼는 문제들에 초점을 맞춥니다. OpenSSL 개발에서 우리가 목격하는 실수들은 이제 매우 중대해져서, OpenSSL 자체에든 또는 우리가 OpenSSL에 의존하는 방식에든 상당한 변화가 필요하다고 믿게 되었습니다.
근본적으로 OpenSSL의 흐름은 3막으로 구성된 연극으로 이해할 수 있습니다:
Heartbleed 이전 시대(2014년 이전): OpenSSL은 유지보수가 부족했고 침체되어 있었으며, 기대 수준에 크게 못 미쳤습니다.
Heartbleed 직후 시대: OpenSSL의 유지보수가 다시 활기를 띠었고 상당한 진전과 개선이 있었습니다. 제대로 된 코드 리뷰 프로세스를 갖추었고, CI에서 테스트를 돌리기 시작했으며, 퍼즈 테스트를 도입했고, 릴리스 프로세스도 성숙해졌습니다.
마지막으로 2021년 OpenSSL 3가 출시되었습니다. OpenSSL 3는 새로운 API를 도입하고 내부적으로 대규모 리팩터링을 수행했습니다. 이전 OpenSSL 버전들과 비교해, OpenSSL 3는 성능, 복잡성, API 사용성(API ergonomics)에서 큰 퇴보가 있었고, 테스트, 검증, 메모리 안전성과 같은 영역에서 필요한 개선도 이루지 못했습니다. 같은 기간 동안 OpenSSL의 포크들은 이 영역들에서 모두 진전을 이뤄냈습니다. 이 시기의 OpenSSL 방향성에 대한 우리의 많은 우려는 HAProxy가 지적한 내용과도 상당히 겹칩니다.
이 글의 나머지 부분에서는 OpenSSL에 대해 우리가 겪는 문제들을 더 자세히 설명하고, 이에 대응하여 우리가 자체 정책을 어떻게 바꾸는지로 마무리합니다. 결론을 뒤로 미루지 않자면, 우리는 OpenSSL에 대한 의존도를 줄이기 위해 여러 접근을 추진할 계획입니다.
OpenSSL 1.1.1과 비교할 때 OpenSSL 3는 파싱 및 키 로딩과 같은 영역에서 상당한 성능 퇴보가 있습니다.
몇 년 전 우리는 타원 곡선 공개키 로딩이 OpenSSL 1.1.1과 3.0.7 사이에서 5~8배 느려졌다는 버그를 보고했습니다. 이를 알아차린 이유는, 성능이 너무 나빠져서 테스트 스위트 실행 시간에서 체감될 정도였기 때문입니다. 이후 OpenSSL은 성능을 개선해 예전보다 “단지” 3배 느린 수준으로 만들었습니다. 하지만 더 중요한 것은, 이 이슈에 대한 반응이 “OpenSSL 3에서는 퇴보가 예상된 것이며, 약간의 최적화 여지는 있겠지만 1.1.1 수준으로 돌아갈 것이라 기대해선 안 된다”는 것이었다는 점입니다. 성능 퇴보는 라이브러리의 다른 영역을 개선한다면 수용 가능할 수도, 심지어 적절할 수도 있습니다. 그러나 뒤에서 설명하겠지만, 이러한 퇴보의 원인은 다른 실수들이었고, 그에 상응하는 개선으로 상쇄되지 않았습니다.
이런 종류의 퇴보의 결과로, pyca/cryptography가 X.509 인증서 파싱을 OpenSSL에서 우리 자체 Rust 코드로 옮겼을 때 OpenSSL 3 대비 10배 성능 향상을 얻었습니다(참고: 이 향상의 일부는 우리 코드 자체의 장점에서도 오지만, 상당 부분은 OpenSSL 3의 퇴보로 설명됩니다). 이후 공개키 파싱을 우리 Rust 코드로 옮기자 종단 간(end-to-end) X.509 경로 검증이 60% 더 빨라졌습니다. 즉 키 로딩만 개선해도 전체적으로 60% 개선이 된 셈인데, 이것이 OpenSSL의 키 파싱 오버헤드가 얼마나 극심했는지를 보여줍니다.
우리가 자체 파싱으로 더 나은 성능을 달성할 수 있다는 사실은 “더 잘하는 것”이 충분히 실용적임을 분명히 보여줍니다. 실제로 우리의 성능은 영리한 SIMD 미시 최적화의 결과가 아니라, 단순하지만 효과적인 일을 한 결과입니다. 우리는 복사, 할당, 해시 테이블, 간접 호출, 락을 피합니다. 기본적인 DER 구조를 파싱하는 데 이런 것들은 필요하지 않아야 합니다.
OpenSSL 3는 API를 대폭 바꾸는 과정을 시작했습니다. OSSL_PARAM을 도입했고, 이후 모든 새로운 API 표면(양자내성(post-quantum) 알고리즘용 API 포함)에서 이를 사용하고 있습니다. 요컨대 OSSL_PARAM은 일반적인 인자 전달 대신, 함수에 키-값 쌍의 배열을 전달하는 방식으로 동작합니다. 이는 성능을 떨어뜨리고, 컴파일 타임 검증을 약화시키며, 장황함을 늘리고, 코드 가독성을 해칩니다. 이를 옹호할 만한 논거가 있다면, 서로 다른 매개변수를 갖는 다양한 알고리즘에 대해 동일한 API(및 ABI)를 사용할 수 있게 해 준다는 점이라고 우리는 추정합니다. 그 결과 OpenSSL에 새 알고리즘이 추가되더라도 업데이트할 필요가 없는 일반적인 설정 파서로 설정 파일에서 알고리즘 파라미터를 읽어오는 등의 일이 가능해집니다.
장황함을 구체적으로 비교해 보면, OpenSSL로 ML-KEM 캡슐화(encapsulation)를 수행하려면 실패 가능한 함수 호출 6개를 포함해 37줄이 필요합니다. BoringSSL에서는 실패 가능한 함수 호출 3개를 포함해 19줄이면 됩니다.
공개 API를 더 답답하고 오류가 나기 쉬운 형태로 만드는 것 외에도, OpenSSL 내부도 더 복잡해졌습니다. 예를 들어 OSSL_PARAM 배열을 그나마 다루기 쉽게 만들기 위해, OpenSSL의 많은 소스 파일은 이제 단순한 C 파일이 아니라 C 코드에 대한 커스텀 Perl 전처리기를 사용하는 형태가 되었습니다.
OpenSSL 3는 또한(이전 ENGINE API를 구식으로 만들었지만 대체하지는 않은) “프로바이더(provider)” 개념을 도입했습니다. 이는 외부 알고리즘 구현(OpenSSL 자체가 제공하는 알고리즘 포함)을 허용합니다. 그런데 이 기능은 잘못 설계된 API 때문에 셀 수 없이 많은 성능 퇴보를 유발했습니다. 특히 OpenSSL은 프로그램 실행 중 어느 시점에서든 어떤 알고리즘이든 교체할 수 있도록 허용했는데, 이 때문에 거의 모든 연산에 수많은 할당과 락을 추가해야 했습니다. 이를 완화하기 위해 OpenSSL은 더 많은 캐시를 추가했고, 결국 RCU(Read-Copy-Update)까지 도입했습니다. RCU는 복잡한 메모리 관리 전략으로, 진단하기 어려운 버그들을 동반했습니다.
우리 관점에서 이것은 나쁜 결정들이 누적되는 악순환입니다. 프로바이더 API는 잘못 설계되었습니다(프로그램 실행 중 임의의 시점에서 SHA-256을 재정의할 수 있어야 할 필요는 없습니다). 그 결과 성능이 퇴보했습니다. 이를 완화하기 위해 캐싱과 RCU라는 형태로 복잡도를 추가했고, 그 복잡도는 다시 더 많은 버그로 이어졌습니다. 그리고 그 모든 과정을 거쳤는데도 성능은 처음보다 여전히 나빴습니다.
마지막으로, OpenSSL 공개 API를 잡고 실제 구현이 어떻게 되어 있는지 추적해 들어가는 일은 자기 채찍질에 가깝게 되었습니다. 소스를 읽어 동작 방식을 이해할 수 있는 것은 소프트웨어 엔지니어링에서의 자기 발전의 일부로서도 중요하지만, 숙련된 소비자(consumer)로서 문서화되지 않은 구현상의 사실들이 필연적으로 존재하기 때문에 “정답(ground truth)”을 얻기 위해서도 중요합니다. 그런데 간접 호출, 선택적 경로, #ifdef, 그리고 이해를 방해하는 다른 장애물의 수가 놀라울 정도입니다. OpenSSL 소스 코드를 읽는 일이 얼마나 끔찍해졌는지 우리는 과장하지 않습니다. 이는 과거에는 그렇지 않았고, LibreSSL, BoringSSL, AWS-LC에서도 그렇지 않습니다.
우리는 “Python Cryptographic Authority는 우연히 암호 라이브러리를 만들어내는 CI 엔지니어링 프로젝트다”라고 농담하곤 합니다. 이 농담에는 테스트와 자동화에 대한 투자가 개발 속도와 정확성에서 파레토 개선을 가능하게 한다는 우리의 진심이 담겨 있습니다. 그 수준이 너무 커서, 다른 작업들이 하찮아 보일 정도입니다.
OpenSSL 프로젝트는 테스트를 충분히 우선순위에 두고 있지 않습니다. Heartbleed 이전 시대에 비해 OpenSSL의 테스트는 크게 개선되었지만, 여전히 상당한 공백이 존재합니다. OpenSSL 3.0 개발 사이클 동안 이 공백은 특히 뚜렷하게 드러났습니다. 프로젝트는 16개월에 걸친(19개의 사전 릴리스를 포함하는) 긴 알파/베타 기간 동안, 실제 환경에서 발생한 회귀(regression)를 커뮤니티가 보고해 주는 것에 극도로 의존했습니다. 왜냐하면 자체 테스트만으로는 의도치 않은 실사용 환경 파손(breakage)을 잡아내기에 충분하지 않았기 때문입니다. OpenSSL의 테스트 커버리지 공백이 잘 알려져 있음에도, 회귀 테스트 없이 버그 수정이 머지되는 일은 여전히 흔합니다.
OpenSSL의 CI는 유난히 불안정(flaky)하며, OpenSSL 프로젝트는 이 불안정을 어느 정도 용인하는 문화가 되었고, 그로 인해 심각한 버그가 가려집니다. OpenSSL 3.0.4에는 AVX-512를 지원하는 CPU에서 RSA 구현에 치명적인 버퍼 오버플로가 있었습니다. 이 버그는 실제로 CI가 잡아냈습니다. 하지만 크래시가 CI 러너가 우연히 AVX-512 CPU일 때만 발생했기 때문에(모든 러너가 그런 것은 아닙니다), 실패가 단순한 불안정성으로 치부된 것으로 보입니다. 3년이 지난 지금도 프로젝트는 실패하는 테스트가 있는 상태로 코드를 머지합니다. 우리가 컨퍼런스 슬라이드를 준비한 날 기준으로 최근 커밋 10개 중 5개가 CI 체크에 실패했고, 발표 전날에는 모든 커밋이 크로스 컴파일 빌드에 실패하고 있었습니다.
이 사건은 Intel SDE 같은 도구를 도입하는 가치도 보여줍니다. Intel SDE를 사용하면 x86-64 확장 명령어의 서로 다른 부분집합을 가진 CPU를 대상으로 제어된 테스트를 수행할 수 있습니다. AVX-512가 있는 경우와 없는 경우를 각각 전용 테스트 잡으로 돌렸다면, 실패의 성격이 즉시 명확해지고 재현 가능해졌을 것입니다.
OpenSSL은 형식 검증(formal verification) 분야의 최신 수준을 따라가지 못하고 있습니다. 형식 기법은 학계의 신기한 장난감에서 벗어나, 암호 코드의 의미 있는 부분들에 대해 실용적 현실이 되었습니다. BoringSSL과 AWS-LC는 형식적으로 검증된 구현을 도입했고, 자동 추론(automated reasoning)을 활용해 보증 수준을 높이고 있습니다.
OpenSSL이 만들어졌을 당시에는 성능, 내장 가능성(embeddability), 메모리 안전성을 의미 있게 제공하는 프로그래밍 언어가 없었습니다. 메모리 안전한 언어를 원한다면 성능을 포기하고 가비지 컬렉터를 추가해야 했습니다.
세상은 바뀌었습니다. 거의 5년 전 pyca/cryptography는 Rust 코드를 포함한 첫 릴리스를 발표했고, 그 이후 우리는 거의 모든 기능을 Rust로 옮겨왔습니다. 파싱과 X.509 연산 전부는 순수 Rust로 처리하고, 암호 알고리즘 제공은 OpenSSL을 사용하는 혼합 방식으로 진행했습니다. 그 결과 성능 향상을 얻었고, 여러 OpenSSL CVE도 피할 수 있었습니다. 우리는 이러한 전환이 가능하다는 것을 알고 있습니다.
보안에 헌신하는 라이브러리라면, 메모리 안전한 프로그래밍 언어로의 마이그레이션에 장기적으로 헌신해야 합니다. OpenSSL은 이 문제에 대해 어떤 주도적 움직임도 보여주지 않았습니다.
오픈 소스 프로젝트의 문제가 제기될 때면, 많은 사람들이 이를 자금 부족이나 공유지의 비극 문제로 돌리려 합니다. 그러나 이는 적절하지 않습니다. 지난 10년, 즉 Heartbleed 이후 OpenSSL은 상당한 자금을 지원받았고, 현재 시점에서 OpenSSL Corporation과 Foundation은 BoringSSL이나 LibreSSL에서 풀타임으로 일하는 인원보다 더 많은 소프트웨어 엔지니어를 고용하고 있습니다. 우리가 설명한 문제들은 자금 부족으로 인해 발생한 것이 아닙니다.
우리는 여기서 설명한 공개 API와 내부 복잡도로 이어진 동기를 완전히 이해하지 못합니다. 우리는 “무엇이 이런 선택을 하도록 동기부여할까?”라고 질문하며 최선을 다해 역추적해 보았지만, 종종 답을 찾지 못했습니다. 다른 어떤 OpenSSL 포크도 동일한 설계 선택을 하지 않았다는 사실은 “이것이 정말 필요했는가?”라는 질문에 대해 시사하는 바가 큽니다.
OpenSSL에 대한 우리의 경험은 수년간 부정적인 방향으로 흘러왔습니다. 이러한 문제들로 인해, 우리는 (사실 문서화되어 있지 않았던) 자체 정책을 다음과 같이 변경하고 있습니다.
첫째, 더 이상 새로운 기능에 대해 OpenSSL 구현을 요구하지 않겠습니다. 필요하다고 판단되는 경우, LibreSSL/BoringSSL/AWS-LC에서만 제공되는 새로운 API를 추가할 것입니다. 구체적으로, OpenSSL에서는 제공되지 않고 LibreSSL/BoringSSL/AWS-LC에서만 사용 가능한 ML-KEM 및 ML-DSA API를 추가할 것으로 예상합니다.
둘째, 우리는 현재 휠(wheel, 바이너리 아티팩트)에 OpenSSL 사본을 정적 링크하고 있습니다. 우리는 휠이 OpenSSL 포크 중 하나에 링크하도록 바꾸기 위해 무엇이 필요한지 검토하는 과정을 시작하고 있습니다.
만약 바이너리 휠을 OpenSSL의 포크 중 하나로 성공적으로 전환할 수 있다면, 어떤 상황에서 OpenSSL 지원을 완전히 중단할지를 검토하기 시작할 것입니다.
마지막으로, 장기적으로는 Graviola와 같은 비-OpenSSL 계열 암호 라이브러리도 잠재적 대안으로 적극적으로 추적하고 있습니다.
우리는 암호 구현을 제공하는 데 어떤 라이브러리를 사용하는지의 변화가 사용자—특히 재배포자(redistributor)—에게 큰 영향을 준다는 점을 알고 있습니다. 우리는 이런 조치를 가볍게 여기지 않으며, 성급히 진행할 생각도 없습니다. 그러나 우려의 중대성 때문에 행동해야 한다고 느낍니다. 만약 여러분이 pyca/cryptography의 OpenSSL 지원에 의존하고 있다면, 여기서 검토한 가장 극단적인 조치를 피하는 최선의 방법은 OpenSSL 프로젝트에 참여하여 이 글에서 언급한 축들(성능, 복잡도, 테스트·검증, 메모리 안전성 등)에서의 개선에 기여하는 것입니다.