Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 543 – Python을 위한 통합 TLS API

Author:
Cory Benfield <cory at lukasa.co.uk>, Christian Heimes <christian at python.org>
Status:
Withdrawn
Type:
Standards Track
Created:
17-Oct-2016
Python-Version:
3.7
Post-History:
11-Jan-2017, 19-Jan-2017, 02-Feb-2017, 09-Feb-2017
Superseded-By:
748

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 추상 베이스 클래스 모음 형태로 표준 TLS 인터페이스를 정의하고자 합니다. 이 인터페이스를 통해 Python 구현과 서드파티 라이브러리는 OpenSSL 이외의 TLS 라이브러리에 대한 바인딩을 제공할 수 있으며, 이 바인딩은 Python 표준 라이브러리가 제공하는 인터페이스를 기대하는 도구에서 사용할 수 있습니다. 이는 Python 생태계의 OpenSSL 의존도를 줄이는 것을 목표로 합니다.

결정

2020-06-25: 한 저자의 현재 동의와 다른 저자의 과거 동의에 따라, 기반 운영 체제의 API가 변경되었기 때문에 이 PEP는 철회되었습니다.

근거

21세기에 들어 견고하고 사용자 친화적인 TLS 지원이 널리 쓰이는 모든 프로그래밍 언어 생태계에서 매우 중요한 부분이라는 점이 점점 더 분명해졌습니다. Python 생태계에서 이 역할은 그 역사의 대부분 동안 주로 ssl module이 담당해 왔으며, 이 모듈은 OpenSSL library에 대한 Python API를 제공합니다.

ssl 모듈은 Python 표준 라이브러리와 함께 배포되므로, Python에서 TLS를 처리하는 방법 중 압도적으로 가장 널리 쓰이는 방법이 되었습니다. 표준 라이브러리와 Python 패키지 색인에 있는 Python 라이브러리의 압도적 다수가 TLS 연결을 위해 ssl 모듈에 의존합니다.

안타깝게도 ssl 모듈의 우위는 예상치 못한 여러 부작용을 낳았으며, 그 결과 Python 생태계 전체가 OpenSSL에 긴밀하게 묶이게 되었습니다. 이 때문에 Python 사용자는 OpenSSL이 다른 TLS 구현보다 더 나쁜 사용자 경험을 제공할 수 있는 상황에서도 OpenSSL을 사용할 수밖에 없었으며, 이는 인지적 부담을 주고 “플랫폼 네이티브” 경험을 제공하기 어렵게 만듭니다.

문제점

ssl 모듈이 표준 라이브러리에 내장되어 있다는 사실은 표준 라이브러리의 모든 Python 네트워킹 라이브러리가 해당 Python 구현이 링크된 OpenSSL에 전적으로 의존한다는 것을 의미합니다. 이로 인해 다음과 같은 문제가 발생합니다:

  • 새 OpenSSL을 사용하기 위해 Python을 다시 컴파일하지 않고서는 보안성이 더 높은 새 TLS를 활용하기 어렵습니다. OpenSSL에 대한 서드파티 바인딩(예: pyOpenSSL)이 있기는 하지만, 이를 표준 라이브러리가 이해하는 형식에 맞추는 심(shim)이 필요하므로 이를 사용하려는 프로젝트는 상당한 규모의 호환성 계층을 유지해야 합니다.
  • Windows용 Python 배포판은 OpenSSL 사본과 함께 배포되어야 합니다. 이로 인해 CPython 개발 팀은 OpenSSL 재배포자의 위치에 놓이게 되며, OpenSSL 취약점이 공개되면 Windows Python 배포판에 보안 업데이트를 배포해야 할 수도 있습니다.
  • macOS용 Python 배포판은 OpenSSL 사본과 함께 배포되거나 시스템 OpenSSL 라이브러리에 링크되어야 합니다. Apple은 시스템 OpenSSL 라이브러리에 링크하는 것을 공식적으로 폐지했으며, 설령 그렇지 않았더라도 이 글을 쓰는 시점을 기준으로 해당 라이브러리 버전은 거의 1년 동안 업스트림의 지원을 받지 못했습니다. CPython 개발 팀은 python.org에서 제공하는 Python과 함께 더 새로운 OpenSSL을 배포하기 시작했지만, 이는 Windows와 같은 문제를 안고 있습니다.
  • Windows와 macOS를 포함하되 이에 국한되지 않는 많은 시스템은 시스템 인증서 저장소를 OpenSSL에서 사용할 수 있도록 제공하지 않습니다. 이 때문에 사용자는 신뢰 루트를 다른 곳(예: certifi)에서 얻거나, 시스템 신뢰 저장소를 어떤 형태로든 내보내려고 시도해야 합니다.

    대부분의 시스템 관리자는 보안상 중요한 소프트웨어 업데이트를 PyPI에서 받으리라고 예상하지 않으므로, certifi에 의존하는 것은 바람직하지 않습니다. 또한 certifi 신뢰 번들을 확장하여 사용자 정의 루트를 포함하거나, certifi 모델을 사용하여 신뢰를 중앙에서 관리하기가 쉽지 않습니다.

    시스템 인증서 저장소를 어떤 형태로든 OpenSSL에서 사용할 수 있게 한 상황에서도, OpenSSL은 플랫폼 네이티브 TLS 구현과 다른 검증 검사를 수행하므로 사용 경험은 여전히 기대에 미치지 못합니다. 이로 인해 사용자는 브라우저나 기타 플랫폼 네이티브 도구에서 Python에서와 다른 동작을 겪을 수 있으며, 이 문제를 해결할 수단이 거의 또는 전혀 없습니다.

  • 사용자는 OpenSSL에 없는 기능(예: TLS 1.3 지원)이나, OpenSSL이 해당 플랫폼에서 쓰기에 너무 크고 다루기 어렵다는 점(예: 임베디드 Python의 경우) 등 여러 다른 이유로 OpenSSL 이외의 TLS 라이브러리와 통합하기를 원할 수 있습니다. 이러한 사용자는 선호하는 TLS 라이브러리와 상호 작용할 수 있는 서드파티 네트워킹 라이브러리를 사용하거나, 선호하는 라이브러리를 심(shim)을 통해 OpenSSL 전용 ssl 모듈 API에 맞춰야 하는 처지에 놓입니다.

또한 현재 구현된 ssl 모듈은 대체 TLS 백엔드 지원 추가나 OpenSSL 지원의 완전한 제거가 필요해지거나 유용해질 경우, CPython 자체가 이를 수행할 수 있는 능력을 제한합니다. ssl 모듈은 OpenSSL에 특화된 함수 호출과 기능을 너무 많이 노출하고 있어서 대체 TLS 백엔드에 쉽게 대응시킬 수 없습니다.

제안

이 PEP는 OpenSSL에 그다지 강하게 묶여 있지 않은 TLS 기능을 제공하기 위해 Python 3.7에 몇 가지 새로운 추상 베이스 클래스를 도입할 것을 제안합니다. 또한 표준 라이브러리 모듈이 가능한 한 이 추상 베이스 클래스들이 노출하는 인터페이스만 사용하도록 갱신할 것을 제안합니다. 여기에는 세 가지 목표가 있습니다.

  1. 코어 개발자와 서드파티 개발자 모두가 자신의 TLS 구현이 맞춰야 할 대상으로 삼을 수 있는 공통 API 표면을 제공하는 것입니다. 이를 통해 TLS 개발자는 대부분의 Python 코드에서 사용할 수 있는 인터페이스를 제공할 수 있고, 네트워크 개발자는 광범위한 TLS 구현과 함께 동작하는 인터페이스를 대상으로 삼을 수 있습니다.
  2. OpenSSL 고유의 개념이 거의 또는 전혀 새어 나오지 않는 API를 제공하는 것입니다. 현재 ssl 모듈에는 OpenSSL 개념이 API로 새어 나와서 생긴 여러 결함이 있습니다. 새로운 ABC는 그러한 고유 개념을 제거할 것입니다.
  3. Python이 TLS를 지원하기 위해 시스템에 OpenSSL이 반드시 있어야 하도록 요구하는 대신, 코어 개발 팀이 OpenSSL을 여러 가능한 TLS 백엔드 중 하나로 만들 수 있는 길을 마련하는 것입니다.

제안하는 인터페이스는 아래에 제시되어 있습니다.

인터페이스

표준화가 필요한 인터페이스가 몇 가지 있습니다. 그 인터페이스는 다음과 같습니다.

  1. TLS 구성. 현재는 ssl 모듈의 SSLContext 클래스로 구현되어 있습니다.
  2. 실제 I/O 없이 메모리 내에서 암호화나 복호화를 수행하기 위한 메모리 내 버퍼 제공(비동기 I/O 모델에 필요합니다). 현재는 ssl 모듈의 SSLObject 클래스로 구현되어 있습니다.
  3. 소켓 객체 래핑. 현재는 ssl 모듈의 SSLSocket 클래스로 구현되어 있습니다.
  4. (2)와 (3)의 래핑 객체에 TLS 구성 적용. 현재는 이것도 ssl 모듈의 SSLContext 클래스로 구현되어 있습니다.
  5. TLS 암호 스위트 지정. 현재 표준 라이브러리에는 이를 위한 코드가 없습니다. 대신 표준 라이브러리는 OpenSSL 암호 스위트 문자열을 사용합니다.
  6. TLS 핸드셰이크 중에 협상할 수 있는 애플리케이션 계층 프로토콜 지정.
  7. TLS 버전 지정.
  8. 호출자에게 오류 보고. 현재는 ssl 모듈의 SSLError 클래스로 구현되어 있습니다.
  9. 클라이언트 인증서나 서버 인증서로 로드할 인증서 지정.
  10. 원격 피어가 제시한 인증서를 검증하는 데 사용할 신뢰 데이터베이스 지정.
  11. 런타임에 이러한 인터페이스를 얻는 방법 마련.

단순성을 위해 이 PEP는 (2)와 (3)(즉, 버퍼와 소켓)에 통합된 접근 방식을 취할 것을 제안합니다. Python 소켓 API는 상당히 방대하며, 일반 Python 소켓과 똑같이 동작하는 래핑된 소켓을 구현하는 것은 미묘하고 까다로운 일입니다. 그러나 래핑된 버퍼를 바탕으로 범용 래핑된 소켓을 구현하는 것은 충분히 가능합니다. 즉, (2)를 제공하는 모든 구현에서 동작하는 래핑된 소켓 (3)을 작성할 수 있습니다. 이러한 이유로 이 PEP는 래핑된 버퍼 (2)에 대해서는 ABC를 제공하되, 래핑된 소켓 (3)에 대해서는 구체 클래스를 제공할 것을 제안합니다.

이 결정으로 인해 소수의 TLS 라이브러리는 이 ABC에 바인딩할 수 없게 됩니다. 해당 TLS 라이브러리는 래핑된 버퍼 구현을 제공할 수 없기 때문입니다. 현재 이 중 가장 대표적인 것은 Amazon의 s2n으로 보이며, 이 라이브러리는 현재 I/O 추상화 계층을 제공하지 않습니다. 그러나 이 라이브러리조차도 이를 누락된 기능으로 여기고 있으며, 이를 추가하기 위한 작업을 진행하고 있습니다(working to add it). 이러한 이유로, (2)를 기반으로 한 (3)의 구체적인 구현은 노력을 상당히 절약해 주는 수단이자 정확성을 위한 훌륭한 도구가 될 것이라고 가정해도 무방합니다. 따라서 이 PEP는 바로 그렇게 할 것을 제안합니다.

당연히 (5)에는 추상 베이스 클래스가 필요하지 않습니다. 그 대신 지원되는 암호 스위트를 구성하기 위한 더 풍부한 API가 필요하며, 이 API는 여러 구현에서 지원하는 암호 스위트에 맞추어 쉽게 갱신할 수 있어야 합니다.

(9)는 까다로운 문제입니다. 이상적인 환경이라면 이러한 인증서에 연결된 개인 키가 Python 프로세스의 메모리에 올라가는 일이 절대 없어야 하기 때문입니다(즉, TLS 라이브러리가 하드웨어 보안 모듈(HSM)과 협력하여 프로세스 메모리에서 추출할 수 없는 방식으로 개인 키를 제공해야 합니다). 따라서 구체적인 구현이 이러한 더 높은 수준의 보안을 제공할 수 있게 하면서도, 그렇게 할 수 없는 구현에는 더 낮은 기준을 허용하는, 확장 가능한 인증서 제공 모델을 마련해야 합니다. 이 낮은 기준은 현재 상태와 동일합니다. 즉, 인증서는 메모리 내 버퍼나 디스크의 파일에서 로드될 수 있습니다.

(10) 또한 문제가 됩니다. TLS 구현마다 사용자가 신뢰 저장소를 선택하도록 허용하는 방식이 크게 다르기 때문입니다. 일부 구현에는 해당 구현만 사용할 수 있는 고유한 신뢰 저장소 형식이 있으며(예를 들어 c_rehash로 생성되는 OpenSSL CA 디렉터리 형식), 다른 구현은 자체 기본 신뢰 저장소를 포함하지 않는 신뢰 저장소를 지정하는 것을 허용하지 않을 수도 있습니다.

이러한 이유로, 신뢰 저장소의 형태에 대해 최소한만 가정하는 모델을 제공해야 합니다. 이를 어떻게 달성하는지는 아래의 “신뢰 저장소(Trust Store)” 절에서 더 자세히 설명합니다.

마지막으로, 이 API는 현재 SSLContext 객체가 맡고 있는 책임, 구체적으로는 구성을 보관하고 관리하는 책임과 그 구성을 사용하여 래퍼 객체를 생성하는 책임을 분리합니다.

이는 주로 서버 이름 표시(SNI)와 같은 기능을 지원하기 위해 필요합니다. OpenSSL에서는(따라서 ssl 모듈에서도) 클라이언트가 접속하려는 호스트 이름을 서버에 알려 주면, 서버가 그에 따라 TLS 구성을 수정할 수 있습니다. 이는 주로 인증서 체인을 변경하여 해당 호스트 이름에 맞는 올바른 TLS 인증서 체인을 제시하는 데 사용됩니다. 이를 수행하는 구체적인 메커니즘은 적절한 구성을 가진 새 SSLContext 객체를 반환하는 것입니다.

이는 다른 TLS 구현에 잘 대응되는 모델이 아닙니다. 그 대신, 어떤 구성 변경을 적용해야 하는지 나타내는 데 사용할 수 있는 반환값을 SNI 콜백에서 제공할 수 있도록 해야 합니다. 이는 TLS 구성을 보관할 수 있는 객체를 제공해야 함을 의미합니다. 이 객체는 특정 TLSWrappedBuffer 및 TLSWrappedSocket 객체에 적용되어야 합니다.

이러한 이유로, SSLContext의 책임을 두 개의 별도 객체로 분리합니다. TLSConfiguration 객체는 TLS 구성을 담는 컨테이너 역할을 하는 객체이며, ClientContext 및 ServerContext 객체는 TLSConfiguration 객체로 인스턴스화되는 객체입니다. 세 객체는 모두 불변입니다.

Note

이하의 API 선언은 가독성을 돕기 위해 일관되게 타입 힌트를 사용합니다. 이러한 타입 힌트 중 일부는 순환 참조이기 때문에 실제로는 사용할 수 없습니다. 이를 모듈의 최종 코드를 반영한 것이라기보다는 지침으로 여기십시오.

구성

TLSConfiguration 구체 클래스는 TLS 설정을 보관하고 관리할 수 있는 객체를 정의합니다. 이 클래스의 목표는 다음과 같습니다:

  1. 오타로 인한 오류의 위험을 피할 수 있는 TLS 설정 지정 방법을 제공하는 것입니다(따라서 단순한 딕셔너리는 사용하지 않습니다).
  2. SNI 콜백에서 사용할 수 있도록, 다른 설정 객체와 안전하게 비교하여 TLS 설정의 변경을 감지할 수 있는 객체를 제공하는 것입니다.

이 클래스는 ABC가 아닌데, 주된 이유는 구현별 동작이 필요하지 않을 것으로 예상되기 때문입니다. TLSConfiguration 객체를 특정 TLS 구현에 유용한 설정 집합으로 변환하는 책임은 아래에서 다루는 Context 객체에 있습니다.

이 클래스에는 주목할 만한 특성이 하나 더 있습니다: 불변이라는 점입니다. 이는 몇 가지 이유로 바람직한 특성입니다. 가장 중요한 이유는 이 객체를 딕셔너리 키로 사용할 수 있다는 점이며, 이는 특정 TLS 백엔드와 그 SNI 설정에 매우 큰 가치가 있을 수 있습니다. 또한 구현은 설정 객체가 자신도 모르는 사이에 변경될까 걱정할 필요가 없으므로, 구체적인 자료 구조와 설정 객체 사이의 변경 사항을 신중하게 동기화해야 하는 부담을 피할 수 있습니다.

이 객체는 확장 가능합니다: 즉, 향후 Python 릴리스에서 유용해지는 설정 필드를 이 객체에 추가할 수 있습니다. 하위 호환성을 위해 새 필드는 이 객체의 끝에만 추가됩니다. 기존 필드는 절대 제거되거나, 이름이 바뀌거나, 순서가 바뀌지 않습니다.

TLSConfiguration 객체는 다음 코드로 정의됩니다:

ServerNameCallback = Callable[[TLSBufferObject, Optional[str], TLSConfiguration], Any]


_configuration_fields = [
    'validate_certificates',
    'certificate_chain',
    'ciphers',
    'inner_protocols',
    'lowest_supported_version',
    'highest_supported_version',
    'trust_store',
    'sni_callback',
]


_DEFAULT_VALUE = object()


class TLSConfiguration(namedtuple('TLSConfiguration', _configuration_fields)):
    """
    An immutable TLS Configuration object. This object has the following
    properties:

    :param validate_certificates bool: Whether to validate the TLS
        certificates. This switch operates at a very broad scope: either
        validation is enabled, in which case all forms of validation are
        performed including hostname validation if possible, or validation
        is disabled, in which case no validation is performed.

        Not all backends support having their certificate validation
        disabled. If a backend does not support having their certificate
        validation disabled, attempting to set this property to ``False``
        will throw a ``TLSError`` when this object is passed into a
        context object.

    :param certificate_chain Tuple[Tuple[Certificate],PrivateKey]: The
        certificate, intermediate certificate, and the corresponding
        private key for the leaf certificate. These certificates will be
        offered to the remote peer during the handshake if required.

        The first Certificate in the list must be the leaf certificate. All
        subsequent certificates will be offered as intermediate additional
        certificates.

    :param ciphers Tuple[Union[CipherSuite, int]]:
        The available ciphers for TLS connections created with this
        configuration, in priority order.

    :param inner_protocols Tuple[Union[NextProtocol, bytes]]:
        Protocols that connections created with this configuration should
        advertise as supported during the TLS handshake. These may be
        advertised using either or both of ALPN or NPN. This list of
        protocols should be ordered by preference.

    :param lowest_supported_version TLSVersion:
        The minimum version of TLS that should be allowed on TLS
        connections using this configuration.

    :param highest_supported_version TLSVersion:
        The maximum version of TLS that should be allowed on TLS
        connections using this configuration.

    :param trust_store TrustStore:
        The trust store that connections using this configuration will use
        to validate certificates.

    :param sni_callback Optional[ServerNameCallback]:
        A callback function that will be called after the TLS Client Hello
        handshake message has been received by the TLS server when the TLS
        client specifies a server name indication.

        Only one callback can be set per ``TLSConfiguration``. If the
        ``sni_callback`` is ``None`` then the callback is disabled. If the
        ``TLSConfiguration`` is used for a ``ClientContext`` then this
        setting will be ignored.

        The ``callback`` function will be called with three arguments: the
        first will be the ``TLSBufferObject`` for the connection; the
        second will be a string that represents the server name that the
        client is intending to communicate (or ``None`` if the TLS Client
        Hello does not contain a server name); and the third argument will
        be the original ``TLSConfiguration`` that configured the
        connection. The server name argument will be the IDNA *decoded*
        server name.

        The ``callback`` must return a ``TLSConfiguration`` to allow
        negotiation to continue. Other return values signal errors.
        Attempting to control what error is signaled by the underlying TLS
        implementation is not specified in this API, but is up to the
        concrete implementation to handle.

        The Context will do its best to apply the ``TLSConfiguration``
        changes from its original configuration to the incoming connection.
        This will usually include changing the certificate chain, but may
        also include changes to allowable ciphers or any other
        configuration settings.
    """
    __slots__ = ()

    def __new__(cls, validate_certificates: Optional[bool] = None,
                     certificate_chain: Optional[Tuple[Tuple[Certificate], PrivateKey]] = None,
                     ciphers: Optional[Tuple[Union[CipherSuite, int]]] = None,
                     inner_protocols: Optional[Tuple[Union[NextProtocol, bytes]]] = None,
                     lowest_supported_version: Optional[TLSVersion] = None,
                     highest_supported_version: Optional[TLSVersion] = None,
                     trust_store: Optional[TrustStore] = None,
                     sni_callback: Optional[ServerNameCallback] = None):

        if validate_certificates is None:
            validate_certificates = True

        if ciphers is None:
            ciphers = DEFAULT_CIPHER_LIST

        if inner_protocols is None:
            inner_protocols = []

        if lowest_supported_version is None:
            lowest_supported_version = TLSVersion.TLSv1

        if highest_supported_version is None:
            highest_supported_version = TLSVersion.MAXIMUM_SUPPORTED

        return super().__new__(
            cls, validate_certificates, certificate_chain, ciphers,
            inner_protocols, lowest_supported_version,
            highest_supported_version, trust_store, sni_callback
        )

    def update(self, validate_certificates=_DEFAULT_VALUE,
                     certificate_chain=_DEFAULT_VALUE,
                     ciphers=_DEFAULT_VALUE,
                     inner_protocols=_DEFAULT_VALUE,
                     lowest_supported_version=_DEFAULT_VALUE,
                     highest_supported_version=_DEFAULT_VALUE,
                     trust_store=_DEFAULT_VALUE,
                     sni_callback=_DEFAULT_VALUE):
        """
        Create a new ``TLSConfiguration``, overriding some of the settings
        on the original configuration with the new settings.
        """
        if validate_certificates is _DEFAULT_VALUE:
            validate_certificates = self.validate_certificates

        if certificate_chain is _DEFAULT_VALUE:
            certificate_chain = self.certificate_chain

        if ciphers is _DEFAULT_VALUE:
            ciphers = self.ciphers

        if inner_protocols is _DEFAULT_VALUE:
            inner_protocols = self.inner_protocols

        if lowest_supported_version is _DEFAULT_VALUE:
            lowest_supported_version = self.lowest_supported_version

        if highest_supported_version is _DEFAULT_VALUE:
            highest_supported_version = self.highest_supported_version

        if trust_store is _DEFAULT_VALUE:
            trust_store = self.trust_store

        if sni_callback is _DEFAULT_VALUE:
            sni_callback = self.sni_callback

        return self.__class__(
            validate_certificates, certificate_chain, ciphers,
            inner_protocols, lowest_supported_version,
            highest_supported_version, trust_store, sni_callback
        )

Context

두 개의 Context 추상 베이스 클래스를 정의합니다. 이 ABC들은 TLS 설정을 특정 연결에 적용할 수 있게 하는 객체를 정의합니다. 이들은 TLSWrappedSocket 및 TLSWrappedBuffer 객체의 팩토리로 생각할 수 있습니다.

현재의 ssl 모듈과 달리, 하나가 아닌 두 개의 컨텍스트 클래스를 제공합니다. 구체적으로는 ClientContext 및 ServerContext 클래스를 제공합니다. 이렇게 하면 API가 단순해지고(예를 들어, 서버가 ssl.SSLContext.wrap_socket에 server_hostname 매개변수를 제공하는 것은 의미가 없지만, 컨텍스트 클래스가 하나뿐이므로 그 매개변수를 여전히 사용할 수 있습니다), 구현이 TLS 연결의 어느 쪽을 담당할지 가능한 한 일찍 알 수 있습니다. 또한 구현이 연결의 한쪽 또는 양쪽을 지원하지 않기로 선택할 수 있습니다. 예를 들어, macOS의 SecureTransport는 사실상 서버용으로 의도된 것이 아니며, 서버 측 사용에 필요한 기능이 매우 많이 빠져 있습니다. 이렇게 하면 SecureTransport 구현은 ServerContext의 구체 서브클래스를 정의하지 않는 것만으로 지원하지 않음을 알릴 수 있습니다.

현재의 ssl 모듈과의 또 다른 주요 차이점은 여러 플래그와 옵션이 제거되었다는 점입니다. 대부분은 자명하지만, wrap_socket에서 auto_handshake가 제거되었다는 점은 짚고 넘어갈 만합니다. 이것을 제거한 이유는, 사용자와 구현자 모두에게 복잡성을 늘리는 대가로 아주 적은 수고만 덜어 주는, 근본적으로 기이한 설계상의 흠이기 때문입니다. 이 PEP는 모든 사용자가 연결 후 do_handshake를 명시적으로 호출할 것을 요구합니다.

구현자는 가능한 한 이 클래스들을 불변으로 만드는 것을 목표로 해야 합니다: 즉, 사용자가 내부 상태를 직접 변경하도록 허용하지 말고, 대신 새 TLSConfiguration 객체로부터 새 컨텍스트를 생성하는 방식을 택해야 합니다. 당연히 ABC는 이 제약을 강제할 수 없으므로, 강제하려고 시도하지도 않습니다.

Context 추상 베이스 클래스의 클래스 정의는 다음과 같습니다:

TLSBufferObject = Union[TLSWrappedSocket, TLSWrappedBuffer]


class _BaseContext(metaclass=ABCMeta):
    @abstractmethod
    def __init__(self, configuration: TLSConfiguration):
        """
        Create a new context object from a given TLS configuration.
        """

    @property
    @abstractmethod
    def configuration(self) -> TLSConfiguration:
        """
        Returns the TLS configuration that was used to create the context.
        """


class ClientContext(_BaseContext):
    def wrap_socket(self,
                    socket: socket.socket,
                    server_hostname: Optional[str]) -> TLSWrappedSocket:
        """
        Wrap an existing Python socket object ``socket`` and return a
        ``TLSWrappedSocket`` object. ``socket`` must be a ``SOCK_STREAM``
        socket: all other socket types are unsupported.

        The returned SSL socket is tied to the context, its settings and
        certificates. The socket object originally passed to this method
        should not be used again: attempting to use it in any way will lead
        to undefined behaviour, especially across different TLS
        implementations. To get the original socket object back once it has
        been wrapped in TLS, see the ``unwrap`` method of the
        TLSWrappedSocket.

        The parameter ``server_hostname`` specifies the hostname of the
        service which we are connecting to. This allows a single server to
        host multiple SSL-based services with distinct certificates, quite
        similarly to HTTP virtual hosts. This is also used to validate the
        TLS certificate for the given hostname. If hostname validation is
        not desired, then pass ``None`` for this parameter. This parameter
        has no default value because opting-out of hostname validation is
        dangerous, and should not be the default behaviour.
        """
        buffer = self.wrap_buffers(server_hostname)
        return TLSWrappedSocket(socket, buffer)

    @abstractmethod
    def wrap_buffers(self, server_hostname: Optional[str]) -> TLSWrappedBuffer:
        """
        Create an in-memory stream for TLS, using memory buffers to store
        incoming and outgoing ciphertext. The TLS routines will read
        received TLS data from one buffer, and write TLS data that needs to
        be emitted to another buffer.

        The implementation details of how this buffering works are up to
        the individual TLS implementation. This allows TLS libraries that
        have their own specialised support to continue to do so, while
        allowing those without to use whatever Python objects they see fit.

        The ``server_hostname`` parameter has the same meaning as in
        ``wrap_socket``.
        """


class ServerContext(_BaseContext):
    def wrap_socket(self, socket: socket.socket) -> TLSWrappedSocket:
        """
        Wrap an existing Python socket object ``socket`` and return a
        ``TLSWrappedSocket`` object. ``socket`` must be a ``SOCK_STREAM``
        socket: all other socket types are unsupported.

        The returned SSL socket is tied to the context, its settings and
        certificates. The socket object originally passed to this method
        should not be used again: attempting to use it in any way will lead
        to undefined behaviour, especially across different TLS
        implementations. To get the original socket object back once it has
        been wrapped in TLS, see the ``unwrap`` method of the
        TLSWrappedSocket.
        """
        buffer = self.wrap_buffers()
        return TLSWrappedSocket(socket, buffer)

    @abstractmethod
    def wrap_buffers(self) -> TLSWrappedBuffer:
        """
        Create an in-memory stream for TLS, using memory buffers to store
        incoming and outgoing ciphertext. The TLS routines will read
        received TLS data from one buffer, and write TLS data that needs to
        be emitted to another buffer.

        The implementation details of how this buffering works are up to
        the individual TLS implementation. This allows TLS libraries that
        have their own specialised support to continue to do so, while
        allowing those without to use whatever Python objects they see fit.
        """

버퍼

버퍼 래퍼 ABC는 TLSWrappedBuffer ABC로 정의되며, 그 정의는 다음과 같습니다.:

class TLSWrappedBuffer(metaclass=ABCMeta):
    @abstractmethod
    def read(self, amt: int) -> bytes:
        """
        Read up to ``amt`` bytes of data from the input buffer and return
        the result as a ``bytes`` instance.

        Once EOF is reached, all further calls to this method return the
        empty byte string ``b''``.

        May read "short": that is, fewer bytes may be returned than were
        requested.

        Raise ``WantReadError`` or ``WantWriteError`` if there is
        insufficient data in either the input or output buffer and the
        operation would have caused data to be written or read.

        May raise ``RaggedEOF`` if the connection has been closed without a
        graceful TLS shutdown. Whether this is an exception that should be
        ignored or not is up to the specific application.

        As at any time a re-negotiation is possible, a call to ``read()``
        can also cause write operations.
        """

    @abstractmethod
    def readinto(self, buffer: Any, amt: int) -> int:
        """
        Read up to ``amt`` bytes of data from the input buffer into
        ``buffer``, which must be an object that implements the buffer
        protocol. Returns the number of bytes read.

        Once EOF is reached, all further calls to this method return the
        empty byte string ``b''``.

        Raises ``WantReadError`` or ``WantWriteError`` if there is
        insufficient data in either the input or output buffer and the
        operation would have caused data to be written or read.

        May read "short": that is, fewer bytes may be read than were
        requested.

        May raise ``RaggedEOF`` if the connection has been closed without a
        graceful TLS shutdown. Whether this is an exception that should be
        ignored or not is up to the specific application.

        As at any time a re-negotiation is possible, a call to
        ``readinto()`` can also cause write operations.
        """

    @abstractmethod
    def write(self, buf: Any) -> int:
        """
        Write ``buf`` in encrypted form to the output buffer and return the
        number of bytes written. The ``buf`` argument must be an object
        supporting the buffer interface.

        Raise ``WantReadError`` or ``WantWriteError`` if there is
        insufficient data in either the input or output buffer and the
        operation would have caused data to be written or read. In either
        case, users should endeavour to resolve that situation and then
        re-call this method. When re-calling this method users *should*
        re-use the exact same ``buf`` object, as some backends require that
        the exact same buffer be used.

        This operation may write "short": that is, fewer bytes may be
        written than were in the buffer.

        As at any time a re-negotiation is possible, a call to ``write()``
        can also cause read operations.
        """

    @abstractmethod
    def do_handshake(self) -> None:
        """
        Performs the TLS handshake. Also performs certificate validation
        and hostname verification.
        """

    @abstractmethod
    def cipher(self) -> Optional[Union[CipherSuite, int]]:
        """
        Returns the CipherSuite entry for the cipher that has been
        negotiated on the connection. If no connection has been negotiated,
        returns ``None``. If the cipher negotiated is not defined in
        CipherSuite, returns the 16-bit integer representing that cipher
        directly.
        """

    @abstractmethod
    def negotiated_protocol(self) -> Optional[Union[NextProtocol, bytes]]:
        """
        Returns the protocol that was selected during the TLS handshake.
        This selection may have been made using ALPN, NPN, or some future
        negotiation mechanism.

        If the negotiated protocol is one of the protocols defined in the
        ``NextProtocol`` enum, the value from that enum will be returned.
        Otherwise, the raw bytestring of the negotiated protocol will be
        returned.

        If ``Context.set_inner_protocols()`` was not called, if the other
        party does not support protocol negotiation, if this socket does
        not support any of the peer's proposed protocols, or if the
        handshake has not happened yet, ``None`` is returned.
        """

    @property
    @abstractmethod
    def context(self) -> Context:
        """
        The ``Context`` object this buffer is tied to.
        """

    @abstractproperty
    def negotiated_tls_version(self) -> Optional[TLSVersion]:
        """
        The version of TLS that has been negotiated on this connection.
        """

    @abstractmethod
    def shutdown(self) -> None:
        """
        Performs a clean TLS shut down. This should generally be used
        whenever possible to signal to the remote peer that the content is
        finished.
        """

    @abstractmethod
    def receive_from_network(self, data):
        """
        Receives some TLS data from the network and stores it in an
        internal buffer.
        """

    @abstractmethod
    def peek_outgoing(self, amt):
        """
        Returns the next ``amt`` bytes of data that should be written to
        the network from the outgoing data buffer, without removing it from
        the internal buffer.
        """

    @abstractmethod
    def consume_outgoing(self, amt):
        """
        Discard the next ``amt`` bytes from the outgoing data buffer. This
        should be used when ``amt`` bytes have been sent on the network, to
        signal that the data no longer needs to be buffered.
        """

소켓

소켓 래퍼 클래스는 구체 클래스이며, 생성자에서 일반 소켓 객체와 TLSWrappedBuffer 객체의 두 가지를 받습니다. 이 객체는 너무 커서 이 PEP에 옮겨 싣지 않지만, 모듈 구축 작업의 일부로 제출될 것입니다.

래핑된 소켓은 소켓 API 전체를 구현합니다. 다만 SOCK_STREAM 이외의 타입을 가진 소켓에서만 동작하는 메서드(예: sendto/recvfrom)는 스텁으로만 구현합니다. 이 제한은 이 모듈에 DTLS 지원이 추가될 때 해제할 수 있습니다.

또한 소켓 클래스에는 일반 소켓 메서드 외에 다음과 같은 추가 메서드가 포함됩니다.:

class TLSWrappedSocket:
    def do_handshake(self) -> None:
        """
        Performs the TLS handshake. Also performs certificate validation
        and hostname verification. This must be called after the socket has
        connected (either via ``connect`` or ``accept``), before any other
        operation is performed on the socket.
        """

    def cipher(self) -> Optional[Union[CipherSuite, int]]:
        """
        Returns the CipherSuite entry for the cipher that has been
        negotiated on the connection. If no connection has been negotiated,
        returns ``None``. If the cipher negotiated is not defined in
        CipherSuite, returns the 16-bit integer representing that cipher
        directly.
        """

    def negotiated_protocol(self) -> Optional[Union[NextProtocol, bytes]]:
        """
        Returns the protocol that was selected during the TLS handshake.
        This selection may have been made using ALPN, NPN, or some future
        negotiation mechanism.

        If the negotiated protocol is one of the protocols defined in the
        ``NextProtocol`` enum, the value from that enum will be returned.
        Otherwise, the raw bytestring of the negotiated protocol will be
        returned.

        If ``Context.set_inner_protocols()`` was not called, if the other
        party does not support protocol negotiation, if this socket does
        not support any of the peer's proposed protocols, or if the
        handshake has not happened yet, ``None`` is returned.
        """

    @property
    def context(self) -> Context:
        """
        The ``Context`` object this socket is tied to.
        """

    def negotiated_tls_version(self) -> Optional[TLSVersion]:
        """
        The version of TLS that has been negotiated on this connection.
        """

    def unwrap(self) -> socket.socket:
        """
        Cleanly terminate the TLS connection on this wrapped socket. Once
        called, this ``TLSWrappedSocket`` can no longer be used to transmit
        data. Returns the socket that was wrapped with TLS.
        """

암호 스위트

특정 라이브러리에 전혀 의존하지 않는 방식으로 암호 스위트를 지원하는 것은 매우 어려운 일입니다. TLS 구현마다 암호 스위트를 지정하는 API가 근본적으로 다른 경우가 많습니다. 더 큰 문제는 이러한 API가 스타일뿐 아니라 기능 면에서도 자주 다르다는 점입니다. 아래에 몇 가지 예를 들었습니다.

OpenSSL

OpenSSL은 잘 알려진 암호 문자열 형식을 사용합니다. Python을 비롯해 OpenSSL을 사용하는 대부분의 제품이 이 형식을 설정 언어로 채택했습니다. 이 형식은 비교적 읽기 쉽지만 몇 가지 단점이 있습니다. 문자열이므로 잘못된 입력을 넣기가 매우 쉽습니다. 상세한 검증이 거의 없어서 OpenSSL이 어떤 암호도 협상할 수 없게 설정될 수도 있습니다. 또한 암호 스위트를 여러 가지 방식으로 지정할 수 있어 파싱하기가 까다롭습니다. 이 형식의 가장 큰 문제는 공식 명세가 없다는 점입니다. 따라서 주어진 문자열을 OpenSSL과 똑같이 파싱하려면 OpenSSL에 파싱을 맡기는 수밖에 없습니다.

OpenSSL의 암호 문자열은 다음과 같은 모습일 수 있습니다.:

'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5'

이 문자열은 OpenSSL 형식이 얼마나 복잡한지 일부 보여 줍니다. 예를 들어 하나의 항목이 여러 암호 스위트를 지정할 수 있습니다. ECDH+AESGCM 항목은 “타원 곡선 디피-헬만 키 교환과 갈루아 카운터 모드의 AES를 모두 포함하는 모든 암호 스위트”를 의미합니다. 좀 더 명시적으로 풀면 이 항목은 네 개의 암호 스위트로 확장됩니다.:

"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"

이 때문에 완전한 OpenSSL 암호 문자열을 파싱하기는 매우 까다롭습니다. 여기에 다른 메타 문자들까지 더하면 파싱하기가 극도로 복잡한 형식이 됩니다. “!”는 이 기준에 일치하는 암호 스위트를 원래는 포함될 것이더라도 모두 제외합니다. 예를 들어 “!MD5”는 MD5 해시 알고리즘을 사용하는 암호 스위트를 하나도 포함하지 않는다는 뜻입니다. “-“는 일치하는 암호가 이미 포함되어 있으면 제외하지만, 나중에 다시 포함되면 재추가를 허용합니다. “+”는 일치하는 암호를 포함하되 목록의 끝에 배치합니다. 이러한 복잡성에 더해, 실제 결과가 OpenSSL 버전에 따라 달라진다는 점에도 유의해야 합니다. OpenSSL 암호 문자열은 OpenSSL이 인식하는 암호를 하나 이상 포함하기만 하면 유효하기 때문입니다.

또한 OpenSSL은 관련 명세에 나오는 이름과 다른 이름을 암호에 붙입니다. 자세한 내용은 ciphers(1) 매뉴얼 페이지를 참조하십시오.

암호 문자열을 다루는 OpenSSL 내부의 실제 API는 간단합니다.:

char *cipher_list = <some cipher list>;
int rc = SSL_CTX_set_cipher_list(context, cipher_list);

따라서 이 모듈이 사용하는 형식은 OpenSSL에서 쓸 수 있도록 무엇이든 OpenSSL 암호 문자열로 변환할 수 있어야 합니다.

SecureTransport

SecureTransport는 macOS의 시스템 TLS 라이브러리입니다. 이 라이브러리는 사용자층이 훨씬 더 좁기 때문에 여러 면에서 OpenSSL보다 제약이 상당히 많습니다. 이러한 큰 제약 중 하나는 지원되는 암호 스위트를 제어하는 부분에 있습니다.

SecureTransport에서 암호는 C enum으로 표현됩니다. 이 enum에는 암호 스위트마다 항목이 하나씩 있고 집계 항목은 없습니다. 따라서 각 enum 멤버가 어떤 범주에 속하는지를 일일이 코딩하지 않고서는 “ECDH+AESGCM”과 같은 OpenSSL 암호 문자열의 의미를 재현할 수 없습니다.

그러나 대부분의 enum 멤버 이름은 암호 스위트의 공식 이름과 일치합니다. 즉, OpenSSL이 “ECDHE-ECDSA-AES256-GCM-SHA384”라고 부르는 암호 스위트를 SecureTransport에서는 “TLS_ECDHE_ECDHSA_WITH_AES_256_GCM_SHA384”라고 부릅니다.

SecureTransport 내부에서 암호 스위트를 구성하는 API는 간단합니다:

SSLCipherSuite ciphers[] = {TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, ...};
OSStatus status = SSLSetEnabledCiphers(context, ciphers, sizeof(ciphers));
SChannel

SChannel은 Windows 시스템 TLS 라이브러리입니다.

SChannel은 사용 가능한 TLS 암호 스위트를 제어하는 기능을 극히 제한적으로 지원하며, 어떤 TLS 암호 스위트가 지원되는지를 표현하는 세 번째 방식을 추가로 채택하고 있습니다.

구체적으로 SChannel은 ALG_ID 상수(C unsigned int) 집합을 정의합니다. 이 상수들은 각각 암호 스위트 전체가 아니라 개별 알고리즘을 가리킵니다. 몇 가지 예를 들면, 암호 스위트에서 사용되는 대량 암호화 알고리즘을 가리키는 CALG_3DES와 CALG_AES_256, 암호 스위트에서 사용되는 키 교환 알고리즘의 일부를 가리키는 CALG_DH_EPHEM과 CALG_RSA_KEYX, 암호 스위트에서 사용되는 메시지 인증 코드를 가리키는 CALG_SHA1과 CALG_MD5, 그리고 키 교환 알고리즘의 서명 부분을 가리키는 CALG_ECDSA와 CALG_RSA_SIGN이 있습니다.

이는 SecureTransport에 없는 OpenSSL 기능의 나머지 절반이라고 생각할 수 있습니다. SecureTransport는 정확한 암호 스위트만 지정할 수 있고 SChannel은 암호 스위트의 일부만 지정할 수 있는 반면, OpenSSL은 둘 다 허용합니다.

특정 연결에서 어떤 암호 스위트를 허용할지는 이러한 ALG_ID 상수 배열에 대한 포인터를 제공하여 결정합니다. 따라서 적합한 API라면 어떤 ALG_ID 상수를 제공해야 하는지를 Python 코드가 결정할 수 있도록 해야 합니다.

Network Security Services (NSS)

NSS는 Mozilla의 암호화 및 TLS 라이브러리입니다. NSS는 Firefox와 Thunderbird에서 사용되며, curl 등 여러 라이브러리에서 OpenSSL의 대안으로 사용됩니다.

NSS는 기본적으로 허용 암호에 대한 안전한 구성을 갖추고 있습니다. Fedora와 같은 일부 플랫폼에서는 활성화된 암호 목록이 시스템 정책을 통해 전역적으로 구성됩니다. 일반적으로 애플리케이션은 특별한 이유가 없는 한 암호 스위트를 수정해서는 안 됩니다.

NSS에는 암호 스위트에 대한 프로세스 전역 설정과 연결별 설정이 모두 있습니다. NSS에는 OpenSSL과 같은 SSLContext 개념이 없습니다. SSLContext와 유사한 동작은 쉽게 에뮬레이션할 수 있습니다. 구체적으로, 암호는 SSL_CipherPrefSetDefault(PRInt32 cipher, PRBool enabled)로 전역적으로 활성화하거나 비활성화할 수 있고, 연결 단위로는 SSL_CipherPrefSet(PRFileDesc *fd, PRInt32 cipher, PRBool enabled)로 활성화하거나 비활성화할 수 있습니다. 암호 PRInt32 번호는 등록된 IANA ID에 직접 대응하는 부호 있는 32비트 정수입니다. 예를 들어 0x1301은 TLS_AES_128_GCM_SHA256입니다. OpenSSL과 달리 암호의 선호 순서는 고정되어 있으며 런타임에 수정할 수 없습니다.

SecureTransport와 마찬가지로 NSS에는 집계 항목을 위한 API가 없습니다. 일부 NSS 사용자는 OpenSSL 암호 이름과 규칙을 NSS 암호로 변환하는 자체 매핑을 구현했습니다. 예로 mod_nss가 있습니다.

제안하는 인터페이스

새 모듈에 제안하는 인터페이스는 위 구현들의 제약 사항을 모두 합친 것의 영향을 받습니다. 구체적으로, OpenSSL을 제외한 모든 구현이 개별 암호를 하나하나 제공하도록 요구하므로, 그러한 최소 공통분모 방식을 제공할 수밖에 없습니다.

가장 간단한 방법은 TLS에 정의된 암호 스위트 중 상당 부분을 포함하는 열거형을 제공하는 것입니다. 열거형 멤버의 값은 TLS 핸드셰이크에서 사용되는 2옥텟 암호 식별자이며, 16비트 정수로 저장됩니다. 열거형 멤버의 이름은 IANA에 등록된 암호 스위트 이름입니다.

현재 IANA cipher suite registry에는 320개가 넘는 암호 스위트가 등록되어 있습니다. 이 암호 스위트 중 상당 부분은 네트워크 서비스에 대한 TLS 연결과 관련이 없습니다. 그 밖의 스위트는 폐기되었거나 안전하지 않은 알고리즘을 지정하며, 이러한 알고리즘은 최신 버전의 구현에서 더 이상 제공되지 않습니다. 이 열거형에는 다음을 사용하는 암호가 포함되지 않습니다:

  • 키 교환: NULL, Kerberos (KRB5), 사전 공유 키(PSK), 보안 원격 전송(TLS-SRP)
  • 인증: NULL, 익명, 수출 등급, Kerberos (KRB5), 사전 공유 키(PSK), 보안 원격 전송(TLS-SRP), DSA 인증서(DSS)
  • 암호화: NULL, ARIA, DES, RC2, 수출 등급 40비트
  • PRF: MD5
  • SCSV 암호 스위트

3DES, RC4, SEED, IDEA는 레거시 애플리케이션을 위해 포함됩니다. 또한 TLS 1.3 초안(draft-ietf-tls-tls13-18)의 추가 암호 스위트 다섯 개도 포함됩니다. TLS 1.3은 TLS 1.2 이하 버전과 암호 스위트를 하나도 공유하지 않습니다. 그 결과 열거형에는 약 110개의 스위트가 포함됩니다.

이러한 제약 때문에, 그리고 열거형이 정의된 모든 암호를 포함하지는 않기 때문에, 또한 향후의 애플리케이션도 지원할 수 있도록, 이 API에서 CipherSuite 객체를 받는 모든 부분은 16비트 정수 원시값도 직접 받습니다.

이 열거형을 수작업으로 채우는 대신, Christian Heimes의 tlsdb JSON file (주의: 큰 파일)과 IANA cipher suite registry로부터 열거형을 생성하는 TLS enum script를 마련해 두었습니다. 또한 TLSDB를 이용하면, 그러한 기능이 유용하거나 필요하다고 판단될 경우 어떤 TLS 버전이 어떤 암호를 지원하는지 판별하는 것과 같은 추가 조회 함수로 API를 확장할 수도 있습니다.

사용자가 이 방식을 번거롭다고 느낀다면, 향후 이 API의 확장에서 OpenSSL의 집계 기능을 다시 도입하는 헬퍼를 제공할 수 있습니다.

class CipherSuite(IntEnum):
    TLS_RSA_WITH_RC4_128_SHA = 0x0005
    TLS_RSA_WITH_IDEA_CBC_SHA = 0x0007
    TLS_RSA_WITH_3DES_EDE_CBC_SHA = 0x000a
    TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA = 0x0010
    TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA = 0x0016
    TLS_RSA_WITH_AES_128_CBC_SHA = 0x002f
    TLS_DH_RSA_WITH_AES_128_CBC_SHA = 0x0031
    TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x0033
    TLS_RSA_WITH_AES_256_CBC_SHA = 0x0035
    TLS_DH_RSA_WITH_AES_256_CBC_SHA = 0x0037
    TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x0039
    TLS_RSA_WITH_AES_128_CBC_SHA256 = 0x003c
    TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x003d
    TLS_DH_RSA_WITH_AES_128_CBC_SHA256 = 0x003f
    TLS_RSA_WITH_CAMELLIA_128_CBC_SHA = 0x0041
    TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA = 0x0043
    TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA = 0x0045
    TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 0x0067
    TLS_DH_RSA_WITH_AES_256_CBC_SHA256 = 0x0069
    TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 0x006b
    TLS_RSA_WITH_CAMELLIA_256_CBC_SHA = 0x0084
    TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA = 0x0086
    TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA = 0x0088
    TLS_RSA_WITH_SEED_CBC_SHA = 0x0096
    TLS_DH_RSA_WITH_SEED_CBC_SHA = 0x0098
    TLS_DHE_RSA_WITH_SEED_CBC_SHA = 0x009a
    TLS_RSA_WITH_AES_128_GCM_SHA256 = 0x009c
    TLS_RSA_WITH_AES_256_GCM_SHA384 = 0x009d
    TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = 0x009e
    TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = 0x009f
    TLS_DH_RSA_WITH_AES_128_GCM_SHA256 = 0x00a0
    TLS_DH_RSA_WITH_AES_256_GCM_SHA384 = 0x00a1
    TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0x00ba
    TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0x00bc
    TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0x00be
    TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 0x00c0
    TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 0x00c2
    TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 0x00c4
    TLS_AES_128_GCM_SHA256 = 0x1301
    TLS_AES_256_GCM_SHA384 = 0x1302
    TLS_CHACHA20_POLY1305_SHA256 = 0x1303
    TLS_AES_128_CCM_SHA256 = 0x1304
    TLS_AES_128_CCM_8_SHA256 = 0x1305
    TLS_ECDH_ECDSA_WITH_RC4_128_SHA = 0xc002
    TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA = 0xc003
    TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA = 0xc004
    TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA = 0xc005
    TLS_ECDHE_ECDSA_WITH_RC4_128_SHA = 0xc007
    TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA = 0xc008
    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xc009
    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xc00a
    TLS_ECDH_RSA_WITH_RC4_128_SHA = 0xc00c
    TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA = 0xc00d
    TLS_ECDH_RSA_WITH_AES_128_CBC_SHA = 0xc00e
    TLS_ECDH_RSA_WITH_AES_256_CBC_SHA = 0xc00f
    TLS_ECDHE_RSA_WITH_RC4_128_SHA = 0xc011
    TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA = 0xc012
    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xc013
    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 0xc014
    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xc023
    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc024
    TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 = 0xc025
    TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc026
    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xc027
    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xc028
    TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 = 0xc029
    TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 = 0xc02a
    TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xc02b
    TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xc02c
    TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 = 0xc02d
    TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 = 0xc02e
    TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xc02f
    TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xc030
    TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256 = 0xc031
    TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384 = 0xc032
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc072
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc073
    TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc074
    TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc075
    TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc076
    TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc077
    TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc078
    TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc079
    TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc07a
    TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc07b
    TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc07c
    TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc07d
    TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc07e
    TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc07f
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc086
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc087
    TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc088
    TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc089
    TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc08a
    TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc08b
    TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc08c
    TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc08d
    TLS_RSA_WITH_AES_128_CCM = 0xc09c
    TLS_RSA_WITH_AES_256_CCM = 0xc09d
    TLS_DHE_RSA_WITH_AES_128_CCM = 0xc09e
    TLS_DHE_RSA_WITH_AES_256_CCM = 0xc09f
    TLS_RSA_WITH_AES_128_CCM_8 = 0xc0a0
    TLS_RSA_WITH_AES_256_CCM_8 = 0xc0a1
    TLS_DHE_RSA_WITH_AES_128_CCM_8 = 0xc0a2
    TLS_DHE_RSA_WITH_AES_256_CCM_8 = 0xc0a3
    TLS_ECDHE_ECDSA_WITH_AES_128_CCM = 0xc0ac
    TLS_ECDHE_ECDSA_WITH_AES_256_CCM = 0xc0ad
    TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 = 0xc0ae
    TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 = 0xc0af
    TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xcca8
    TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xcca9
    TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xccaa

열거형 멤버는 OpenSSL 암호 이름에 매핑할 수 있습니다:

>>> import ssl
>>> ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
>>> ctx.set_ciphers('ALL:COMPLEMENTOFALL')
>>> ciphers = {c['id'] & 0xffff: c['name'] for c in ctx.get_ciphers()}
>>> ciphers[CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
'ECDHE-RSA-AES128-GCM-SHA256'

SecureTransport의 경우, 이 열거형 멤버는 암호 스위트 상수의 값을 직접 가리킵니다. 예를 들어, SecureTransport는 암호 스위트 열거형 멤버 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384의 값을 0xC02C로 정의합니다. 우연이 아니게도, 이 값은 위 열거형에서의 값과 동일합니다. 따라서 SecureTransport와 위 열거형 사이의 매핑은 매우 쉽습니다.

SChannel의 경우, SChannel이 암호 스위트가 아니라 암호를 구성하기 때문에 쉽고 직접적인 매핑이 없습니다. 이는 SChannel에 대해 계속 제기되는 우려 사항으로, 다른 TLS 구현에 비해 특정한 방식으로 구성하기가 매우 어렵다는 점입니다.

이 PEP의 목적상, 모든 SChannel 구현은 열거형 멤버를 바탕으로 어떤 암호를 선택할지 결정해야 합니다. 구현의 선택에 따라, 이는 실제 암호 스위트 목록이 허용하려는 것보다 더 개방적일 수도 있고 더 제한적일 수도 있습니다. 이 PEP는 더 제한적인 쪽을 권장하지만, 물론 이를 강제할 수는 없습니다.

프로토콜 협상

NPN과 ALPN은 모두 HTTP/2 핸드셰이크 과정에서 프로토콜을 협상할 수 있게 합니다. NPN과 ALPN은 근본적으로 바이트 문자열을 기반으로 하지만, 문자열 기반 API는 발견하기 어려운 오타를 허용하므로 문제가 되는 경우가 많습니다.

이러한 이유로 이 모듈은 프로토콜 협상 구현이 전달하고 전달받을 수 있는 타입을 정의합니다. 이 타입은 바이트 문자열을 감싸서 잘 알려진 프로토콜에 별칭을 사용할 수 있게 합니다. 이를 통해 잘 알려진 프로토콜의 이름을 잘못 입력할 때 생기는 문제를 피할 수 있습니다. 또한 필요한 경우 사용자가 바이트 문자열을 직접 전달할 수 있으므로 프로토콜 협상 계층의 확장성도 온전히 유지됩니다.

class NextProtocol(Enum):
    H2 = b'h2'
    H2C = b'h2c'
    HTTP1 = b'http/1.1'
    WEBRTC = b'webrtc'
    C_WEBRTC = b'c-webrtc'
    FTP = b'ftp'
    STUN = b'stun.nat-discovery'
    TURN = b'stun.turn'

TLS 버전

지원할 TLS 버전을 제한할 수 있으면 유용한 경우가 많습니다. 오래된 TLS 버전의 사용을 거부하면 보안상 많은 이점이 있으며, 일부 오작동하는 서버는 더 새로운 버전을 지원한다고 알리는 TLS 클라이언트를 잘못 처리합니다.

다음 열거형 타입을 사용하여 TLS 버전을 제한할 수 있습니다. 미래를 내다보는 애플리케이션은 꼭 필요한 경우가 아니라면 최대 TLS 버전을 거의 설정하지 않아야 합니다. 이를 사용하는 Python보다 새로운 TLS 백엔드는 이 열거형 타입에 없는 TLS 버전을 지원할 수 있기 때문입니다.

또한 이 열거형 타입은 구현이 지원하는 가장 낮은 TLS 버전이나 가장 높은 TLS 버전을 요청할 때 언제나 사용할 수 있는 추가 플래그 두 개를 정의합니다.

class TLSVersion(Enum):
    MINIMUM_SUPPORTED = auto()
    SSLv2 = auto()
    SSLv3 = auto()
    TLSv1 = auto()
    TLSv1_1 = auto()
    TLSv1_2 = auto()
    TLSv1_3 = auto()
    MAXIMUM_SUPPORTED = auto()

오류

이 모듈은 오류 처리에 사용할 베이스 클래스 네 개를 정의합니다. 여기에서 정의하는 다른 많은 클래스와 달리, 이 클래스들은 동작이 없으므로 추상 클래스가 아닙니다. 이 클래스들은 단지 특정한 공통 동작을 알리기 위해 존재합니다. 백엔드는 자체 패키지에서 이 예외들의 서브클래스를 정의해야 하지만, 그 서브클래스에 어떤 동작도 정의할 필요는 없습니다.

일반적으로 구체적인 구현은 이 예외들을 직접 발생시키기보다는 이 예외들의 서브클래스를 만들어 사용해야 합니다. 이렇게 하면 예상치 못한 오류를 디버깅할 때 어떤 구체적인 TLS 구현이 사용되고 있는지 파악하기가 어느 정도 쉬워집니다. 그러나 이는 필수 사항이 아닙니다.

오류의 정의는 다음과 같습니다:

class TLSError(Exception):
    """
    The base exception for all TLS related errors from any backend.
    Catching this error should be sufficient to catch *all* TLS errors,
    regardless of what backend is used.
    """

class WantWriteError(TLSError):
    """
    A special signaling exception used only when non-blocking or
    buffer-only I/O is used. This error signals that the requested
    operation cannot complete until more data is written to the network,
    or until the output buffer is drained.

    This error is should only be raised when it is completely impossible
    to write any data. If a partial write is achievable then this should
    not be raised.
    """

class WantReadError(TLSError):
    """
    A special signaling exception used only when non-blocking or
    buffer-only I/O is used. This error signals that the requested
    operation cannot complete until more data is read from the network, or
    until more data is available in the input buffer.

    This error should only be raised when it is completely impossible to
    write any data. If a partial write is achievable then this should not
    be raised.
    """

class RaggedEOF(TLSError):
    """
    A special signaling exception used when a TLS connection has been
    closed gracelessly: that is, when a TLS CloseNotify was not received
    from the peer before the underlying TCP socket reached EOF. This is a
    so-called "ragged EOF".

    This exception is not guaranteed to be raised in the face of a ragged
    EOF: some implementations may not be able to detect or report the
    ragged EOF.

    This exception is not always a problem. Ragged EOFs are a concern only
    when protocols are vulnerable to length truncation attacks. Any
    protocol that can detect length truncation attacks at the application
    layer (e.g. HTTP/1.1 and HTTP/2) is not vulnerable to this kind of
    attack and so can ignore this exception.
    """

인증서

이 모듈은 추상 X509 인증서 클래스를 정의합니다. 이 클래스에는 동작이 거의 없습니다. 이 모듈의 목표가 X509 인증서로 제공할 수 있는 관련 암호화 기능을 모두 제공하는 것이 아니기 때문입니다. 그 대신 구체적인 구현에 인증서의 출처를 알려 줄 수 있는 기능만 있으면 됩니다.

이러한 이유로 이 인증서 구현은 생성자만 정의합니다. 본질적으로 이 모듈의 인증서 객체는 특정 인증서를 찾는 데 쓰는 핸들 정도로 추상적이어도 됩니다.

구체적인 구현은 HSM에서 인증서를 불러오는 생성자와 같은 대체 생성자를 제공할 수 있습니다. 이를 위한 공통 인터페이스가 등장하면, 이 사용 사례를 위한 표준 생성자도 제공하도록 이 모듈을 갱신할 수 있습니다.

구체적인 구현은 가능한 한 Certificate(인증서) 객체가 해시 가능하도록 만드는 것을 목표로 해야 합니다. 이렇게 하면 개별 구체적인 구현과 함께 사용하는 TLSConfiguration 객체도 해시 가능하도록 보장하는 데 도움이 됩니다.

class Certificate(metaclass=ABCMeta):
    @abstractclassmethod
    def from_buffer(cls, buffer: bytes):
        """
        Creates a Certificate object from a byte buffer. This byte buffer
        may be either PEM-encoded or DER-encoded. If the buffer is PEM
        encoded it *must* begin with the standard PEM preamble (a series of
        dashes followed by the ASCII bytes "BEGIN CERTIFICATE" and another
        series of dashes). In the absence of that preamble, the
        implementation may assume that the certificate is DER-encoded
        instead.
        """

    @abstractclassmethod
    def from_file(cls, path: Union[pathlib.Path, AnyStr]):
        """
        Creates a Certificate object from a file on disk. This method may
        be a convenience method that wraps ``open`` and ``from_buffer``,
        but some TLS implementations may be able to provide more-secure or
        faster methods of loading certificates that do not involve Python
        code.
        """

개인 키

이 모듈은 추상 개인 키 클래스를 정의하게 됩니다. Certificate(인증서) 클래스와 마찬가지로, 이 클래스는 구체적인 구현이 키를 신중하게 다룰 수 있도록 최대한 많은 자유를 주기 위해 동작을 거의 갖지 않습니다.

이 클래스에는 Certificate 클래스의 모든 주의 사항이 동일하게 적용됩니다.

class PrivateKey(metaclass=ABCMeta):
    @abstractclassmethod
    def from_buffer(cls,
                    buffer: bytes,
                    password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None):
        """
        Creates a PrivateKey object from a byte buffer. This byte buffer
        may be either PEM-encoded or DER-encoded. If the buffer is PEM
        encoded it *must* begin with the standard PEM preamble (a series of
        dashes followed by the ASCII bytes "BEGIN", the key type, and
        another series of dashes). In the absence of that preamble, the
        implementation may assume that the certificate is DER-encoded
        instead.

        The key may additionally be encrypted. If it is, the ``password``
        argument can be used to decrypt the key. The ``password`` argument
        may be a function to call to get the password for decrypting the
        private key. It will only be called if the private key is encrypted
        and a password is necessary. It will be called with no arguments,
        and it should return either bytes or bytearray containing the
        password. Alternatively a bytes, or bytearray value may be supplied
        directly as the password argument. It will be ignored if the
        private key is not encrypted and no password is needed.
        """

    @abstractclassmethod
    def from_file(cls,
                  path: Union[pathlib.Path, bytes, str],
                  password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None):
        """
        Creates a PrivateKey object from a file on disk. This method may
        be a convenience method that wraps ``open`` and ``from_buffer``,
        but some TLS implementations may be able to provide more-secure or
        faster methods of loading certificates that do not involve Python
        code.

        The ``password`` parameter behaves exactly as the equivalent
        parameter on ``from_buffer``.
        """

신뢰 저장소

앞서 논의했듯이, 신뢰 저장소를 불러오는 일은 문제가 됩니다. TLS 구현마다 사용자가 신뢰 저장소를 선택하도록 허용하는 방식이 크게 다르기 때문입니다. 이러한 이유로, 신뢰 저장소가 어떤 형태를 취하는지에 대해 최소한의 가정만 하는 모델을 제공해야 합니다.

이 문제는 Certificate(인증서) 타입과 PrivateKey 타입이 해결해야 하는 문제와 같습니다. 이러한 이유로, TLS 백엔드가 신뢰 저장소를 열 수 있는 다양한 방법을 캡슐화할 수 있는 불투명 타입을 만들어 완전히 같은 모델을 사용합니다.

특정 TLS 구현이 모든 생성자를 구현해야 하는 것은 아닙니다. 그러나 시스템 신뢰 저장소는 가장 흔히 사용되는 검증용 신뢰 저장소이므로, 각 TLS 구현은 가능한 한 system 생성자를 제공할 것을 강력히 권장합니다. 구체 구현은 자체 생성자를 추가할 수도 있습니다.

구체 구현은 가능한 한 TrustStore 객체가 해시 가능하도록 하는 것을 목표로 해야 합니다. 이렇게 하면 개별 구체 구현과 함께 사용되는 TLSConfiguration 객체도 해시 가능하도록 보장하는 데 도움이 됩니다.

class TrustStore(metaclass=ABCMeta):
    @abstractclassmethod
    def system(cls) -> TrustStore:
        """
        Returns a TrustStore object that represents the system trust
        database.
        """

    @abstractclassmethod
    def from_pem_file(cls, path: Union[pathlib.Path, bytes, str]) -> TrustStore:
        """
        Initializes a trust store from a single file full of PEMs.
        """

런타임 접근

라이브러리 사용자에게 드물지 않은 사용 사례는 TLS 구성은 라이브러리가 제어하도록 맡기되, 사용할 백엔드는 직접 선택하고 싶어 하는 경우입니다. 예를 들어 Requests 사용자는 Windows와 macOS에서 OpenSSL과 플랫폼 네이티브 솔루션 중 하나를, 일부 Linux 플랫폼에서는 OpenSSL과 NSS 중 하나를 선택할 수 있기를 원할 수 있습니다. 그러나 이런 사용자는 TLS 구성이 정확히 어떻게 이루어지는지에는 관심이 없을 수 있습니다.

여기서 문제가 생깁니다. 임의의 구체 구현이 주어졌을 때, 라이브러리는 신뢰 저장소에 인증서를 불러오는 방법을 어떻게 알아낼 수 있습니까? 선택지는 두 가지입니다. 모든 구체 구현이 특정 명명 체계를 따르도록 요구하거나, 이러한 객체를 가져올 수 있게 해 주는 API를 제공하는 것입니다.

이 PEP는 두 번째 방식을 사용할 것을 제안합니다. 이 방식은 적절한 속성을 갖춘 단일 객체 하나만 제공하도록 요구하므로, 구체 구현이 원하는 대로 코드를 구성할 수 있는 가장 큰 자유를 줍니다. 그러면 사용자는 이 “백엔드” 객체를 이를 지원하는 라이브러리에 전달할 수 있고, 해당 라이브러리가 구체 구현의 구성과 사용을 처리할 수 있습니다.

모든 구체 구현은 Backend 객체를 얻는 방법을 제공해야 합니다. Backend 객체는 전역 싱글턴일 수도 있고, 그렇게 하는 데 이점이 있다면 호출 가능 객체로 생성될 수도 있습니다.

Backend 객체의 정의는 다음과 같습니다:

Backend = namedtuple(
    'Backend',
    ['client_context', 'server_context',
     'certificate', 'private_key', 'trust_store']
)

각 속성은 해당 ABC의 구체 구현을 제공해야 합니다. 이렇게 하면 다음과 같은 코드가 어떤 백엔드에서든 동작하도록 보장됩니다:

trust_store = backend.trust_store.system()

표준 라이브러리 변경 사항

표준 라이브러리 중 TLS와 상호 작용하는 부분은 이러한 ABC를 사용하도록 수정해야 합니다. 이를 통해 이 모듈들의 함수가 다른 TLS 백엔드에서도 동작할 수 있게 됩니다. 여기에는 다음 모듈이 포함됩니다:

  • asyncio
  • ftplib
  • http
  • imaplib
  • nntplib
  • poplib
  • smtplib
  • urllib

ssl 모듈의 마이그레이션

당연히 ssl 모듈 자체도 이 ABC들을 따르도록 확장해야 합니다. 이 확장은 새 클래스의 형태를 띠며, 완전히 새로운 모듈에 들어갈 수도 있습니다. 이렇게 하면 현재 ssl 모듈을 활용하는 애플리케이션은 계속 그렇게 할 수 있고, 새 API를 사용하려는 애플리케이션과 라이브러리는 새 API를 사용할 수 있습니다.

일반적으로 ssl 모듈에서 새 ABC로의 마이그레이션이 일대일로 대응하지는 않을 것으로 예상됩니다. 이는 보통 문제가 되지 않습니다. ssl 모듈을 사용하는 도구 대부분은 이를 사용자에게 숨기므로, 새 모듈을 사용하도록 리팩터링하더라도 사용자에게는 드러나지 않을 것입니다.

그러나 정의된 API의 일부로서든, (쉽게 일어나는 일이지만) 실수로든 ssl 모듈의 예외를 외부로 노출하는 라이브러리나 애플리케이션에서는 특정한 문제가 발생합니다. 그러한 도구의 사용자는 ssl 모듈의 예외가 발생하는 것을 허용하고 처리하는 코드를 작성했을 수 있습니다. 여기서 제시하는 ABC로 마이그레이션하면 그 대신 위에서 정의한 예외가 발생할 수 있으며, 기존 except 블록은 이를 잡지 못합니다.

이러한 이유로 ssl 모듈 마이그레이션의 일환으로 ssl 모듈의 예외가 위에서 정의한 예외의 별칭이 되도록 해야 합니다. 즉, 다음 문이 모두 성공하도록 해야 합니다:

assert ssl.SSLError is tls.TLSError
assert ssl.SSLWantReadError is tls.WantReadError
assert ssl.SSLWantWriteError is tls.WantWriteError

이를 정확히 어떤 방식으로 수행할지는 이 PEP의 범위를 벗어납니다. 현재 ssl 예외가 C 코드로 정의되어 있어 더 복잡해지기 때문입니다. 자세한 내용은 an email sent to the Security-SIG by Christian Heimes에서 확인할 수 있습니다.

향후 계획

향후 주요 TLS 기능을 지원하려면 이 ABC들을 개정해야 할 수도 있습니다. 이러한 개정은 신중하게 이루어져야 합니다. 많은 백엔드가 빠르게 따라가지 못할 수 있으며, 이 ABC들이 변경되면 해당 백엔드는 무효화되기 때문입니다. 이는 감수할 만하지만, 가능한 한 개별 구현에 특화된 기능은 ABC에 추가하지 않아야 합니다. ABC는 IETF가 명세한 기능을 고수준에서 기술하는 데 그쳐야 합니다.

그러나 충분한 근거가 있는 이 API의 확장은 반드시 이루어져야 합니다. 이 API의 초점은 Python 커뮤니티에 통일된 최소 공통분모 구성 옵션을 제공하는 것입니다. TLS는 고정된 대상이 아니며, TLS가 발전함에 따라 이 API도 발전해야 합니다.

감사의 말

이 문서는 커뮤니티의 여러 사람에게서 폭넓은 검토를 받았으며, 이들은 문서의 형태를 잡는 데 크게 기여했습니다. 상세한 검토를 해 주신 분들은 다음과 같습니다:

  • Alex Chan
  • Alex Gaynor
  • Antoine Pitrou
  • Ashwini Oruganti
  • Donald Stufft
  • Ethan Furman
  • Glyph
  • Hynek Schlawack
  • Jim J Jewett
  • Nathaniel J. Smith
  • Alyssa Coghlan
  • Paul Kehrer
  • Steve Dower
  • Steven Fackler
  • Wes Turner
  • Will Bond

Security-SIG 및 python-ideas 메일링 리스트에서도 추가로 검토해 주었습니다.