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

Python 개선 제안 한국어 번역

PEP 748 – Python을 위한 통합 TLS API

Author:
Joop van de Pol <joop.vandepol at trailofbits.com>, William Woodruff <william at yossarian.net>
Sponsor:
Alyssa Coghlan <ncoghlan at gmail.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Created:
27-Jun-2024
Python-Version:
3.14
Post-History:
17-Apr-2024
Replaces:
543

Table of Contents

번역·라이선스 안내

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

초록

이 PEP는 프로토콜 클래스 모음의 형태로 표준 TLS 인터페이스를 정의합니다. 이 인터페이스를 통해 Python 구현과 서드 파티 라이브러리는 OpenSSL 이외의 TLS 라이브러리에 대한 바인딩을 제공할 수 있습니다.

이러한 바인딩은 Python 표준 라이브러리가 제공하는 인터페이스를 기대하는 도구에서 사용할 수 있으며, Python 생태계의 OpenSSL 의존도를 줄이는 것을 목표로 합니다.

근거

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

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

안타깝게도 ssl 모듈의 우월성은 여러 문제를 초래하여 Python 생태계 전체를 OpenSSL에 긴밀하게 묶어 두었습니다. 이로 인해 Python 사용자는 대체 TLS 구현보다 사용자 경험이 나쁠 수 있는 상황에서도 OpenSSL을 사용해야 했으며, 이는 인지적 부담을 초래하고 “플랫폼 네이티브” 경험을 제공하기 어렵게 만듭니다.

문제점

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

  • 새로운 OpenSSL을 사용하기 위해 Python을 다시 컴파일하지 않고서는 새롭고 더 높은 보안 수준의 TLS를 활용하기 어렵습니다. OpenSSL에 대한 서드 파티 바인딩(예: pyOpenSSL)이 존재하지만, 이러한 바인딩은 표준 라이브러리가 이해하는 형식으로 심을 삽입해야 하므로, 이를 사용하려는 프로젝트는 상당한 호환성 계층을 유지해야 합니다.
  • Windows용 Python 배포판은 OpenSSL 사본과 함께 제공되어야 합니다. 이로 인해 CPython 개발 팀은 OpenSSL 재배포자가 되며, OpenSSL 취약점이 공개될 때 Windows용 Python 배포판에 보안 업데이트를 제공해야 할 가능성이 생깁니다.
  • macOS용 Python 배포판은 OpenSSL 사본과 함께 제공되거나 시스템 OpenSSL 라이브러리에 링크되어야 합니다. Apple은 시스템 OpenSSL 라이브러리에 링크하는 것을 공식적으로 더 이상 사용하지 않도록 지정했으며, 그렇지 않았더라도 작성 시점 기준으로 해당 라이브러리 버전은 업스트림에서 거의 1년 동안 지원되지 않았습니다. CPython 개발 팀은 python.org에서 제공하는 Python에 더 최신 버전의 OpenSSL을 함께 제공하기 시작했지만, 이는 Windows와 동일한 문제를 안고 있습니다.
  • 사용자는 시스템이 제공하는 구현과 비교한 유지 관리 부담이나, OpenSSL이 해당 플랫폼에 사용하기에는 단순히 너무 크고 다루기 어렵다는 이유(예: 임베디드 Python) 등으로 OpenSSL 이외의 TLS 라이브러리와 통합하기를 원할 수 있습니다. 이러한 사용자는 선호하는 TLS 라이브러리와 상호 작용할 수 있는 서드파티 네트워킹 라이브러리를 사용하거나, 선호하는 라이브러리를 OpenSSL 전용 ssl 모듈 API에 맞게 심어야 합니다.

또한 현재 구현된 ssl 모듈은 대체 TLS 구현에 대한 지원을 추가하거나 OpenSSL 지원을 완전히 제거하는 CPython 자체의 능력을 제한하며, 이러한 작업 중 하나가 필요하거나 유용해지는 경우에도 그러합니다. ssl 모듈은 OpenSSL 전용 함수 호출과 기능을 너무 많이 노출하므로 대체 TLS 구현에 쉽게 매핑하기 어렵습니다.

제안

이 PEP는 OpenSSL에 그다지 강하게 결합되지 않은 TLS 기능을 제공하기 위해 Python 3.14에 몇 가지 새로운 프로토콜 클래스를 도입할 것을 제안합니다. 또한 가능한 경우 표준 라이브러리 모듈이 이러한 프로토콜 클래스가 노출하는 인터페이스만 사용하도록 업데이트할 것을 제안합니다. 여기에는 세 가지 목표가 있습니다.

  1. 핵심 개발자와 서드 파티 개발자 모두가 자신의 TLS 구현을 대상으로 삼을 수 있는 공통 API 표면을 제공하는 것입니다. 이를 통해 TLS 개발자는 대부분의 Python 코드에서 사용할 수 있는 인터페이스를 제공할 수 있고, 네트워크 개발자는 광범위한 TLS 구현에서 작동하는 것을 대상으로 삼을 수 있는 인터페이스를 사용할 수 있습니다.
  2. OpenSSL 전용 개념이 거의 또는 전혀 새어 나오지 않는 API를 제공하기 위해서입니다. 현재 ssl 모듈에는 OpenSSL 개념이 API로 새어 나와 발생하는 여러 문제점이 있으며, 새로운 프로토콜 클래스는 이러한 특정 개념을 제거합니다.
  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는 인터페이스 (3)과 (4)를 제거하고, 소켓을 통한 모든 통신이 TLS로 보호되도록 보장하는 소켓을 반환하는 더 간단한 인터페이스로 대체할 것을 제안합니다. 다시 말해, 이 인터페이스는 소켓 초기화, TLS 핸드셰이크, 서버 이름 표시(SNI) 등을 클라이언트 또는 서버 연결 생성의 원자적 부분으로 취급합니다. 그러나 메모리 내 버퍼는 비동기 통신에 유용하므로 계속 지원합니다.

물론 (5)에는 프로토콜 클래스가 필요하지 않습니다. 대신 서로 다른 구현에서 지원하는 암호 스위트로 쉽게 업데이트할 수 있는, 지원되는 암호 스위트를 구성하기 위한 더 풍부한 API가 필요합니다.

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

(10) 또한 문제가 됩니다. 서로 다른 TLS 구현은 매우 크게 달라집니다 사용자가 트러스트 저장소를 선택하도록 허용하는 방식에서. 일부 구현에는 해당 구현에서만 사용할 수 있는 특정 트러스트 저장소 형식이 있습니다(c_rehash가 생성하는 OpenSSL CA 디렉터리 형식 등). 다른 구현에서는 기본 트러스트 저장소를 포함하지 않는 트러스트 저장소를 지정하지 못하게 할 수도 있습니다. 반면 대부분의 구현은 사용자 지정 DER 또는 PEM 인코딩 인증서를 어떤 형태로든 로드하는 기능을 지원합니다.

이러한 이유로, 다른 구현과의 타입 호환성을 유지하면서도 트러스트 스토어가 취하는 형식에 대해 거의 가정하지 않는 모델을 제공해야 합니다. 아래의 “Certificate”, “Private Keys”, “Trust Store” 섹션에서 이를 달성하는 방법을 더 자세히 설명합니다.

마지막으로, 이 API는 현재 SSLContext 객체가 담당하는 책임을 둘로 나눕니다. 구체적으로는 구성을 보유하고 관리하는 책임과, 해당 구성을 사용하여 버퍼나 소켓을 구축하는 책임으로 나눕니다.

이는 주로 서버 이름 표시(Server Name Indication, SNI)와 같은 기능을 지원하기 위해 필요합니다. OpenSSL(따라서 ssl 모듈)에서는 클라이언트가 도달하려는 호스트 이름을 서버에 알리는 것에 응답하여 서버가 TLS 구성을 변경할 수 있습니다. 이는 주어진 호스트 이름에 올바른 TLS 인증서 체인을 제시할 수 있도록 인증서 체인을 변경하는 데 주로 사용됩니다. 이를 수행하는 구체적인 메커니즘은 사용자가 제공한 SNI 콜백 함수의 일부로 적절한 구성을 갖춘 새로운 SSLContext 객체를 반환하는 것입니다.

이는 다른 TLS 구현에 잘 적용되지 않는 모델이며, 사용자가 콜백 함수를 작성해야 하는 부담을 줍니다. 대신 관련 인증서를 받은 후 구체적인 구현이 모든 사용자에 대해 SNI를 투명하게 처리하도록 제안합니다.

이러한 이유로 SSLContext의 책임을 두 개의 별도 객체로 나누고, 각 객체를 서버용과 클라이언트용 버전으로 다시 나눕니다. TLSServerConfigurationTLSClientConfiguration 객체는 TLS 구성을 담는 컨테이너로 동작합니다. ClientContext 및 ServerContext 객체는 각각 TLSClientConfigurationTLSServerConfiguration 객체로 인스턴스화되며, 버퍼나 소켓을 생성하는 데 사용됩니다. 네 객체 모두 변경할 수 없습니다.

Note

다음 API 선언은 읽기 쉽도록 일관되게 타입 힌트를 사용합니다.

구성

TLSServerConfigurationTLSClientConfiguration 구체 클래스는 TLS 구성을 보유하고 관리할 수 있는 객체를 정의합니다. 이러한 클래스의 목표는 다음과 같습니다.

  1. 입력 오류의 위험을 줄이는 방식으로 TLS 구성을 지정하는 방법을 제공합니다(단순한 딕셔너리 사용은 제외합니다).
  2. SNI 콜백에서 사용할 수 있도록 TLS 구성의 변경 사항을 감지하기 위해 다른 구성 객체와 안전하게 비교할 수 있는 객체를 제공합니다.

이러한 클래스는 주로 구현별 동작을 가질 것으로 예상되지 않으므로 프로토콜 클래스가 아닙니다. TLSServerConfiguration객체 또는 TLSClientConfiguration객체를 특정 TLS 구현에 유용한 구성 집합으로 변환하는 책임은 아래에서 설명하는 Context 객체에 속합니다.

이러한 클래스의 또 다른 주목할 만한 특성은 변경할 수 없다는 점입니다. 이는 몇 가지 이유로 바람직한 특성입니다. 가장 중요한 이유는 기본적으로 변경 불가능하게 만드는 것이 훌륭한 엔지니어링 관행이기 때문입니다. 부수적인 이점으로, 이러한 객체를 딕셔너리 키로 사용할 수 있으며, 이는 특정 TLS 구현과 해당 SNI 구성에 잠재적으로 유용합니다. 또한 구현은 구성 객체가 자신도 모르는 사이에 변경되는 것을 걱정할 필요가 없으므로, 구체적인 데이터 구조와 구성 객체 사이의 변경 사항을 신중하게 동기화할 필요를 피할 수 있습니다.

이러한 객체는 확장 가능합니다. 즉, 향후 Python 릴리스에서는 유용해지는 구성 필드를 이러한 객체에 추가할 수 있습니다. 하위 호환성을 위해 새 필드는 이러한 객체의 끝에만 추가됩니다. 기존 필드는 제거되거나 이름이 변경되거나 순서가 바뀌는 일이 없습니다. API의 혼란을 최소화하기 위해 클라이언트용과 서버용으로 나눕니다.

TLSClientConfiguration 클래스는 다음 코드로 정의됩니다.

class TLSClientConfiguration:
    __slots__ = (
        "_certificate_chain",
        "_ciphers",
        "_inner_protocols",
        "_lowest_supported_version",
        "_highest_supported_version",
        "_trust_store",
    )

    def __init__(
        self,
        certificate_chain: SigningChain | None = None,
        ciphers: Sequence[CipherSuite] | None = None,
        inner_protocols: Sequence[NextProtocol | bytes] | None = None,
        lowest_supported_version: TLSVersion | None = None,
        highest_supported_version: TLSVersion | None = None,
        trust_store: TrustStore | None = None,
    ) -> None:
        if inner_protocols is None:
            inner_protocols = []

        self._certificate_chain = certificate_chain
        self._ciphers = ciphers
        self._inner_protocols = inner_protocols
        self._lowest_supported_version = lowest_supported_version
        self._highest_supported_version = highest_supported_version
        self._trust_store = trust_store

    @property
    def certificate_chain(self) -> SigningChain | None:
        return self._certificate_chain

    @property
    def ciphers(self) -> Sequence[CipherSuite | int] | None:
        return self._ciphers

    @property
    def inner_protocols(self) -> Sequence[NextProtocol | bytes]:
        return self._inner_protocols

    @property
    def lowest_supported_version(self) -> TLSVersion | None:
        return self._lowest_supported_version

    @property
    def highest_supported_version(self) -> TLSVersion | None:
        return self._highest_supported_version

    @property
    def trust_store(self) -> TrustStore | None:
        return self._trust_store

TLSServerConfiguration 객체는 클라이언트 객체와 유사하지만, certificate_chain 매개변수로 Sequence[SigningChain]을 받는다는 점이 다릅니다.

컨텍스트

두 개의 컨텍스트 프로토콜 클래스를 정의합니다. 이러한 프로토콜 클래스는 특정 연결에 TLS 구성을 적용할 수 있도록 하는 객체를 정의합니다. 이러한 객체는 TLSSocketTLSBuffer 객체를 생성하는 팩토리로 간주할 수 있습니다.

현재의 ssl 모듈과 달리, 하나가 아닌 두 개의 컨텍스트 클래스를 제공합니다. 구체적으로 ClientContextServerContext 클래스를 제공합니다. 이렇게 하면 API가 단순해지고(예를 들어 서버가 wrap_socket()server_hostname 매개변수를 제공하는 것은 의미가 없지만, 컨텍스트 클래스가 하나뿐이므로 해당 매개변수를 여전히 사용할 수 있습니다), 구현에서 TLS 연결의 어느 쪽을 담당하게 될지 최대한 일찍 알 수 있습니다. 또한 구현에서 연결의 한쪽 또는 양쪽을 선택적으로 지원하지 않을 수 있습니다.

가능한 한 구현자는 이러한 클래스를 불변으로 만드는 것을 목표로 해야 합니다. 즉, 사용자가 내부 상태를 직접 변경하도록 허용하기보다는 새로운 TLSConfiguration 객체에서 새로운 컨텍스트를 생성하는 방식을 선호해야 합니다. 물론 프로토콜 클래스는 이러한 제약을 강제할 수 없으므로, 이를 시도하지도 않습니다.

ClientContext 프로토콜 클래스의 클래스 정의는 다음과 같습니다.

class ClientContext(Protocol):
    @abstractmethod
    def __init__(self, configuration: TLSClientConfiguration) -> None:
        """Create a new client context object from a given TLS client configuration."""
        ...

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

    @abstractmethod
    def connect(self, address: tuple[str | None, int]) -> TLSSocket:
        """Creates a TLSSocket that behaves like a socket.socket, and
        contains information about the TLS exchange
        (cipher, negotiated_protocol, negotiated_tls_version, etc.).
        """
        ...

    @abstractmethod
    def create_buffer(self, server_hostname: str) -> TLSBuffer:
        """Creates a TLSBuffer that acts as an in-memory channel,
        and contains information about the TLS exchange
        (cipher, negotiated_protocol, negotiated_tls_version, etc.)."""
        ...

ServerContext도 이와 유사하지만 대신 TLSServerConfiguration을 받습니다.

소켓

컨텍스트를 사용하여 소켓을 생성할 수 있으며, 이러한 소켓은 TLSSocket 프로토콜 클래스의 사양을 따라야 합니다. 구체적으로 구현에서는 다음을 구현해야 합니다.

  • recvsend
  • listenaccept
  • close
  • getsockname
  • getpeername

또한 TLS 연결에 관한 정보를 제공하는 다음과 같은 일부 인터페이스도 구현해야 합니다.

  • 이 소켓을 생성하는 데 사용된 기본 컨텍스트 객체
  • 협상된 암호
  • 협상된 “다음” 프로토콜
  • 협상된 TLS 버전

다음 코드는 이러한 함수를 더 자세히 설명합니다.

class TLSSocket(Protocol):
    """This class implements a socket.socket-like object that creates an OS
    socket, wraps it in an SSL context, and provides read and write methods
    over that channel."""

    @abstractmethod
    def __init__(self, *args: tuple, **kwargs: tuple) -> None:
        """TLSSockets should not be constructed by the user.
        The implementation should implement a method to construct a TLSSocket
        object and call it in ClientContext.connect() and
        ServerContext.connect()."""
        ...

    @abstractmethod
    def recv(self, bufsize: int) -> bytes:
        """Receive data from the socket. The return value is a bytes object
        representing the data received. Should not work before the handshake
        is completed."""
        ...

    @abstractmethod
    def send(self, bytes: bytes) -> int:
        """Send data to the socket. The socket must be connected to a remote socket."""
        ...

    @abstractmethod
    def close(self, force: bool = False) -> None:
        """Shuts down the connection and mark the socket closed.
        If force is True, this method should send the close_notify alert and shut down
        the socket without waiting for the other side.
        If force is False, this method should send the close_notify alert and raise
        the WantReadError exception until a corresponding close_notify alert has been
        received from the other side.
        In either case, this method should return WantWriteError if sending the
        close_notify alert currently fails."""
        ...

    @abstractmethod
    def listen(self, backlog: int) -> None:
        """Enable a server to accept connections. If backlog is specified, it
        specifies the number of unaccepted connections that the system will allow
        before refusing new connections."""
        ...

    @abstractmethod
    def accept(self) -> tuple[TLSSocket, tuple[str | None, int]]:
        """Accept a connection. The socket must be bound to an address and listening
        for connections. The return value is a pair (conn, address) where conn is a
        new TLSSocket object usable to send and receive data on the connection, and
        address is the address bound to the socket on the other end of the connection."""
        ...

    @abstractmethod
    def getsockname(self) -> tuple[str | None, int]:
        """Return the local address to which the socket is connected."""
        ...

    @abstractmethod
    def getpeercert(self) -> bytes | None:
        """
        Return the raw DER bytes of the certificate provided by the peer
        during the handshake, if applicable.
        """
        ...

    @abstractmethod
    def getpeername(self) -> tuple[str | None, int]:
        """Return the remote address to which the socket is connected."""
        ...

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

    @abstractmethod
    def cipher(self) -> CipherSuite | int | None:
        """
        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) -> NextProtocol | bytes | None:
        """
        Returns the protocol that was selected during the TLS handshake.

        This selection may have been made using ALPN 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 negotiated_tls_version(self) -> TLSVersion | None:
        """The version of TLS that has been negotiated on this connection."""
        ...

버퍼

컨텍스트를 사용하여 버퍼를 생성할 수도 있으며, 이러한 버퍼는 TLSBuffer 프로토콜 클래스의 사양을 따라야 합니다. 구체적으로 구현에서는 다음을 구현해야 합니다.

  • readwrite
  • do_handshake
  • shutdown
  • process_incomingprocess_outgoing
  • incoming_bytes_bufferedoutgoing_bytes_buffered
  • getpeercert

소켓의 경우와 마찬가지로, TLS 연결에 관한 정보를 제공하는 일부 인터페이스도 구현해야 합니다. 예를 들면 다음과 같습니다:

  • 이 버퍼를 만드는 데 사용된 기반 컨텍스트 객체
  • 협상된 암호 스위트
  • 협상된 “다음” 프로토콜
  • 협상된 TLS 버전

다음 코드는 이러한 함수를 더 자세히 설명합니다:

class TLSBuffer(Protocol):
    """This class implements an in memory-channel that creates two buffers,
    wraps them in an SSL context, and provides read and write methods over
    that channel."""

    @abstractmethod
    def read(self, amt: int, buffer: Buffer | None) -> bytes | int:
        """
        Read up to ``amt`` bytes of data from the input buffer and return
        the result as a ``bytes`` instance. If an optional buffer is
        provided, the result is written into the buffer and the number of
        bytes is returned instead.

        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 write(self, buf: Buffer) -> 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 implementations 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) -> CipherSuite | int | None:
        """
        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) -> NextProtocol | bytes | None:
        """
        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) -> ClientContext | ServerContext:
        """
        The ``Context`` object this buffer is tied to.
        """
        ...

    @property
    @abstractmethod
    def negotiated_tls_version(self) -> TLSVersion | None:
        """
        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 process_incoming(self, data_from_network: bytes) -> None:
        """
        Receives some TLS data from the network and stores it in an
        internal buffer.

        If the internal buffer is overfull, this method will raise
        ``WantReadError`` and store no data. At this point, the user must
        call ``read`` to remove some data from the internal buffer
        before repeating this call.
        """
        ...

    @abstractmethod
    def incoming_bytes_buffered(self) -> int:
        """
        Returns how many bytes are in the incoming buffer waiting to be processed.
        """
        ...

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

    @abstractmethod
    def outgoing_bytes_buffered(self) -> int:
        """
        Returns how many bytes are in the outgoing buffer waiting to be sent.
        """
        ...

    @abstractmethod
    def getpeercert(self) -> bytes | None:
        """
        Return the raw DER bytes of the certificate provided by the peer
        during the handshake, if applicable.
        """
        ...

암호 스위트

특정 라이브러리에 종속되지 않는 방식으로 암호 스위트를 지원하는 일은 매우 어려운 작업입니다. 서로 다른 TLS 구현은 암호 스위트를 지정하는 API가 근본적으로 다른 경우가 많지만, 더 문제가 되는 점은 이러한 API가 형식뿐만 아니라 기능 면에서도 자주 다르다는 것입니다.

다음은 서로 다른 암호 스위트 선택 API의 예입니다. 이러한 예는 각 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 항목은 타원 곡선 Diffie-Hellman 키 교환과 Galois Counter Mode의 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 암호 문자열로 변환할 수 있어야 한다는 의미입니다.

네트워크 프레임워크

Network Framework은 macOS(10.15 이상)의 시스템 TLS 라이브러리입니다. 이 라이브러리는 사용자가 훨씬 더 제한적인 부류에 속하므로 여러 측면에서 OpenSSL보다 상당히 제한적입니다. 이러한 상당한 제한 중 하나는 지원되는 암호 스위트를 제어하는 기능과 관련됩니다.

Network Framework에서 암호는 Objective-C uint16_t 열거형으로 표현됩니다. 이 열거형에는 암호 스위트마다 하나의 항목만 있고 집계 항목은 없으므로, 각 열거형 멤버가 속하는 범주를 직접 코드로 작성하지 않고서는 “ECDH+AESGCM”와 같은 OpenSSL 암호 문자열의 의미를 재현할 수 없습니다.

그러나 대부분의 열거형 멤버 이름은 암호 스위트의 공식 이름과 일치합니다. 즉, OpenSSL에서 “ECDHE-ECDSA-AES256-GCM-SHA384”라고 부르는 암호 스위트는 Network Framework에서 “tls_ciphersuite_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384”라고 불립니다.

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

void sec_protocol_options_append_tls_ciphersuite(sec_protocol_options_t options, tls_ciphersuite_t ciphersuite);
SChannel

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

SChannel은 사용 가능한 TLS 암호 스위트를 제어하는 기능이 극도로 제한적이며, TLS 암호 스위트가 지원되는 방식을 표현하는 세 번째 방법도 채택합니다.

구체적으로 SChannel은 ALG_ID 상수(C 부호 없는 정수)의 집합을 정의합니다. 이러한 각 상수는 전체 암호 스위트를 가리키는 것이 아니라 개별 알고리즘을 가리킵니다. 예를 들어 CALG_3DESCALG_AES_256은 암호 스위트에서 사용되는 대량 암호화 알고리즘을 가리키고, CALG_ECDH_EPHEMCALG_RSA_KEYX는 암호 스위트에서 사용되는 키 교환 알고리즘의 일부를 가리키며, CALG_SHA_256CALG_SHA_384는 암호 스위트에서 사용되는 메시지 인증 코드를 가리키고, CALG_ECDSACALG_RSA_SIGN은 키 교환 알고리즘의 서명 부분을 가리킵니다.

SChannel API의 이전 버전에서는 사용 가능한 알고리즘을 정의하는 데 이러한 상수를 사용했습니다. 그러나 최신 버전에서는 이러한 상수를 사용하여 사용할 수 없는 알고리즘을 지정합니다.

이는 Network Framework에 없는 OpenSSL 기능의 절반으로 볼 수 있습니다. Network Framework는 정확한 암호 스위트와 제한된 수의 사전 정의된 암호 스위트 그룹만 지정할 수 있는 반면, SChannel은 암호 스위트의 일부만 지정할 수 있고 OpenSSL은 두 가지 모두를 허용합니다.

특정 연결에서 허용되는 암호 스위트를 확인하려면 이러한 ALG_ID 상수의 배열을 가리키는 포인터를 제공해야 합니다. 따라서 적절한 API는 Python 코드가 제공해야 하는 ALG_ID 상수를 결정할 수 있도록 해야 합니다.

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비트 정수입니다. 예를 들어 0x1301TLS_AES_128_GCM_SHA256입니다. OpenSSL과 달리 암호의 우선순위 순서는 고정되어 있으며 런타임에 수정할 수 없습니다.

Network Framework와 마찬가지로 NSS에는 집계 항목을 위한 API가 없습니다. NSS 사용자의 일부는 OpenSSL 암호 이름 및 규칙에서 NSS 암호로 변환하는 사용자 지정 매핑을 구현했습니다. 예를 들어 mod_nss가 있습니다.

제안된 인터페이스

새 모듈에 제안된 인터페이스는 앞서 설명한 구현의 한계가 결합된 집합의 영향을 받습니다. 구체적으로 OpenSSL을 제외한 모든 구현은 각 개별 암호를 제공해야 하므로, 가장 낮은 공통 분모 방식으로 제공하는 것 외에는 선택의 여지가 없습니다.

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

현재 IANA cipher suite registry에는 320개가 넘는 암호 스위트가 포함되어 있습니다. 암호 스위트의 상당 부분은 네트워크 서비스에 대한 TLS 연결과 관련이 없습니다. 다른 스위트는 구현의 최신 버전에서 더 이상 제공되지 않는, 사용 중단되었거나 안전하지 않은 알고리즘을 지정합니다. 이 열거형에는 TLS v1.3에 정의된 5개의 고정 암호 스위트가 포함되어 있습니다. TLS v1.2의 경우에는 TLS v1.3 암호 스위트에 해당하면서, 완전 순방향 비밀성을 위한 ECDHE 키 교환과 ECDSA 또는 RSA 서명을 사용하는 추가 10개의 암호 스위트만 포함합니다.

이 열거형과 함께 인터페이스는 TLS v1.2의 기본 암호 스위트 목록을 정의하며, 여기에는 AES-GCM 또는 ChaCha20-Poly1305를 기반으로 정의된 암호 스위트만 포함됩니다. TLS v1.3의 기본 암호 스위트 목록은 사양에 정의된 5개의 암호 스위트로 구성됩니다.

현재 열거형은 순방향 비밀성을 제공하는 암호 스위트만 포함하므로 상당히 제한적입니다. 열거형에는 정의된 모든 암호가 포함되어 있지 않으며 미래 지향적인 애플리케이션도 지원해야 하므로, CipherSuite 객체를 허용하는 이 API의 모든 부분은 원시 16비트 정수도 직접 허용합니다.

class CipherSuite(IntEnum):
    """
    Known cipher suites.

    See: <https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml>
    """

    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_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B
    TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C
    TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F
    TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xC030
    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

Network Framework에서는 이러한 열거형 멤버가 암호 스위트 상수의 값을 직접 참조합니다. 예를 들어 Network Framework는 암호 스위트 열거형 멤버 tls_ciphersuite_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384를 값 0xC02C로 정의합니다. 우연의 일치가 아니게도 그 값은 앞서 설명한 열거형에서의 값과 동일합니다. 따라서 Network Framework와 앞서 설명한 열거형 간의 매핑은 매우 쉽습니다.

SChannel의 경우 SChannel은 암호 스위트가 아니라 암호를 구성하므로 직접 매핑하기가 쉽지 않습니다. 이는 다른 TLS 구현과 비교할 때 특정한 방식으로 구성하기가 매우 어렵다는 SChannel의 지속적인 문제를 나타냅니다.

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

마지막으로 대부분의 사용자에게는 안전한 기본값으로 충분할 것으로 예상합니다. 암호 목록을 지정하지 않으면 구현은 안전한 기본값을 사용해야 합니다(시스템 권장 설정에서 파생될 수도 있습니다).

프로토콜 협상

ALPN은 HTTP/2 핸드셰이크의 일부로 프로토콜 협상을 가능하게 합니다. ALPN은 근본적으로 바이트 문자열을 기반으로 하지만, 문자열 기반 API는 입력 오류를 허용하여 이를 감지하기 어려운 경우가 많으므로 자주 문제가 됩니다.

이러한 이유로 이 모듈은 프로토콜 협상 구현이 전달하고 전달받을 수 있는 타입을 정의합니다. 이 타입은 잘 알려진 프로토콜의 별칭을 허용하도록 바이트 문자열을 래핑합니다. 이를 통해 잘 알려진 프로토콜에서 발생하는 오타에 내재된 문제를 피하면서도, 필요한 경우 사용자가 바이트 문자열을 직접 전달하여 프로토콜 협상 계층의 완전한 확장성을 활용할 수 있습니다.

class NextProtocol(Enum):
    """The underlying negotiated ("next") protocol."""

    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):
    """
    TLS versions.

    The `MINIMUM_SUPPORTED` and `MAXIMUM_SUPPORTED` variants are "open ended",
    and refer to the "lowest mutually supported" and "highest mutually supported"
    TLS versions, respectively.
    """

    MINIMUM_SUPPORTED = "MINIMUM_SUPPORTED"
    TLSv1_2 = "TLSv1.2"
    TLSv1_3 = "TLSv1.3"
    MAXIMUM_SUPPORTED = "MAXIMUM_SUPPORTED"

오류

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

일반적으로 구체적인 구현은 이러한 예외를 직접 발생시키기보다는 서브클래싱해야 합니다. 이렇게 하면 예상하지 못한 오류를 디버깅하는 동안 어떤 구체적인 TLS 구현이 사용 중인지 어느 정도 더 쉽게 확인할 수 있습니다. 그러나 이는 필수 사항이 아닙니다.

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

class TLSError(Exception):
    """
    The base exception for all TLS related errors from any implementation.

    Catching this error should be sufficient to catch *all* TLS errors,
    regardless of what implementation 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.
    """


class ConfigurationError(TLSError):
    """An special exception that implementations can use when the provided
    configuration uses features not supported by that implementation."""

인증서

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

이러한 이유로 이 인증서 클래스는 구상된 세 가지 생성자에 대응하는 세 개의 속성을 정의합니다. 즉, 파일의 인증서, 메모리의 인증서 또는 임의 식별자의 인증서입니다. 구현이 이러한 생성자를 모두 지원하지 않을 수도 있으며, 아래 “Runtime” 섹션에 설명된 대로 사용자에게 이를 알릴 수 있습니다. 특히 임의 식별자의 인증서는 HSM, TPM, SSM 및 이와 유사한 시스템을 기반으로 통합 기능을 구축하려는 사용자에게 주로 유용할 것으로 예상합니다.

구체적으로 이 클래스는 제공된 입력이 올바른 인증서인지 검증하기 위해 입력을 구문 분석하지 않으며, 특정 인증서의 인트로스펙션 기능도 어떤 형태로든 제공하지 않습니다. TLS 구현 역시 그러한 인트로스펙션 기능을 제공할 필요가 없습니다. 핸드셰이크 중 수신되는 피어 인증서는 원시 DER 바이트로 제공됩니다.

class Certificate:
    """Object representing a certificate used in TLS."""

    __slots__ = (
        "_buffer",
        "_path",
        "_id",
    )

    def __init__(
        self, buffer: bytes | None = None, path: os.PathLike[str] | None = None, id: bytes | None = None
    ):
        """
        Creates a Certificate object from a path, buffer, or ID.

        If none of these is given, an exception is raised.
        """

        if buffer is None and path is None and id is None:
            raise ValueError("Certificate cannot be empty.")

        self._buffer = buffer
        self._path = path
        self._id = id

    @classmethod
    def from_buffer(cls, buffer: bytes) -> Certificate:
        """
        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.
        """
        return cls(buffer=buffer)

    @classmethod
    def from_file(cls, path: os.PathLike[str]) -> Certificate:
        """
        Creates a Certificate object from a file on disk. The file on disk
        should contain a series of bytes corresponding to a certificate that
        may be either PEM-encoded or DER-encoded. If the bytes are 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.
        """
        return cls(path=path)

    @classmethod
    def from_id(cls, id: bytes) -> Certificate:
        """
        Creates a Certificate object from an arbitrary identifier. This may
        be useful for implementations that rely on system certificate stores.
        """
        return cls(id=id)

개인 키

이 모듈은 구체적인 개인 키 클래스를 정의합니다. Certificate 클래스와 마찬가지로 이 클래스는 세 가지 생성자에 대응하는 세 개의 속성을 가지며, Certificate 클래스의 모든 주의 사항도 그대로 적용됩니다.

class PrivateKey:
    """Object representing a private key corresponding to a public key
    for a certificate used in TLS."""

    __slots__ = (
        "_buffer",
        "_path",
        "_id",
    )

    def __init__(
        self, buffer: bytes | None = None, path: os.PathLike | None = None, id: bytes | None = None
    ):
        """
        Creates a PrivateKey object from a path, buffer, or ID.

        If none of these is given, an exception is raised.
        """

        if buffer is None and path is None and id is None:
            raise ValueError("PrivateKey cannot be empty.")

        self._buffer = buffer
        self._path = path
        self._id = id

    @classmethod
    def from_buffer(cls, buffer: bytes) -> PrivateKey:
        """
        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 private key is DER-encoded
        instead.
        """
        return cls(buffer=buffer)

    @classmethod
    def from_file(cls, path: os.PathLike) -> PrivateKey:
        """
        Creates a PrivateKey object from a file on disk. The file on disk
        should contain a series of bytes corresponding to a certificate that
        may be either PEM-encoded or DER-encoded. If the bytes are 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.
        """
        return cls(path=path)

    @classmethod
    def from_id(cls, id: bytes) -> PrivateKey:
        """
        Creates a PrivateKey object from an arbitrary identifier. This may
        be useful for implementations that rely on system private key stores.
        """
        return cls(id=id)

서명 체인

TLS 참여자는 자신을 인증하기 위해 상대방이 신뢰하는 일부 루트 인증서까지 이어지는 체인을 포함한 리프 인증서를 제공해야 합니다. 서버는 항상 클라이언트에 자신을 인증해야 하지만, 클라이언트 인증 중에는 클라이언트도 서버에 자신을 인증할 수 있습니다. 또한 리프 인증서에는 개인 키가 함께 있어야 하며, 개인 키는 별도의 객체에 저장하거나 리프 인증서 자체와 함께 저장할 수 있습니다. 이 모듈은 아래에 자세히 설명된 대로 이러한 객체의 모음을 SigningChain으로 정의합니다.

class SigningChain:
    """Object representing a certificate chain used in TLS."""

    leaf: tuple[Certificate, PrivateKey | None]
    chain: list[Certificate]

    def __init__(
        self,
        leaf: tuple[Certificate, PrivateKey | None],
        chain: Sequence[Certificate] | None = None,
    ):
        """Initializes a SigningChain object."""
        self.leaf = leaf
        if chain is None:
            chain = []
        self.chain = list(chain)

위의 구성 클래스에 표시된 것처럼 클라이언트 인증의 경우 클라이언트는 하나의 서명 체인을 가질 수 있으며, 그렇지 않은 경우에는 가질 수 없습니다. 서버는 여러 도메인을 담당할 때 유용한 서명 체인의 시퀀스를 가질 수 있습니다.

신뢰 저장소

위에서 설명했듯이 신뢰 저장소를 로드하는 것은 문제가 됩니다. TLS 구현마다 사용자가 신뢰 저장소를 선택하도록 허용하는 방식이 크게 다르기 때문입니다. 이러한 이유로 신뢰 저장소가 취할 수 있는 형식에 대해 거의 가정하지 않는 모델을 제공해야 합니다.

이 문제는 CertificatePrivateKey유형이 해결해야 하는 문제와 동일합니다. 이러한 이유로 사용자가 신뢰 저장소를 정의할 수 있는 다양한 방식을 담는 구체 클래스를 생성하여 정확히 동일한 모델을 사용합니다.

특정 TLS 구현이 가능한 모든 신뢰 저장소를 처리할 필요는 없습니다. 그러나 가능한 경우 특정 TLS 구현이 system생성자를 처리하는 것이 강력히 권장됩니다. 이는 가장 일반적으로 사용되는 검증 신뢰 저장소이기 때문입니다. TLS 구현은 아래의 “런타임” 섹션에 설명된 대로 지원되지 않는 옵션을 알릴 수 있습니다.

class TrustStore:
    """
    The trust store that is used to verify certificate validity.
    """

    __slots__ = (
        "_buffer",
        "_path",
        "_id",
    )

    def __init__(
        self, buffer: bytes | None = None, path: os.PathLike | None = None, id: bytes | None = None
    ):
        """
        Creates a TrustStore object from a path, buffer, or ID.

        If none of these is given, the default system trust store is used.
        """

        self._buffer = buffer
        self._path = path
        self._id = id

    @classmethod
    def system(cls) -> TrustStore:
        """
        Returns a TrustStore object that represents the system trust
        database.
        """
        return cls()

    @classmethod
    def from_buffer(cls, buffer: bytes) -> TrustStore:
        """
        Initializes a trust store from a buffer of PEM-encoded certificates.
        """
        return cls(buffer=buffer)

    @classmethod
    def from_file(cls, path: os.PathLike) -> TrustStore:
        """
        Initializes a trust store from a single file containing PEMs.
        """
        return cls(path=path)

    @classmethod
    def from_id(cls, id: bytes) -> TrustStore:
        """
        Initializes a trust store from an arbitrary identifier.
        """
        return cls(id=id)

런타임 액세스

라이브러리 사용자가 TLS 연결의 실제 세부 사항은 라이브러리가 구성하도록 하면서 사용할 TLS 구현을 지정하려는 경우는 드물지 않습니다. 예를 들어 requests사용자는 Windows와 macOS에서 OpenSSL과 플랫폼 네이티브 솔루션 중 하나를 선택하거나, 일부 Linux 플랫폼에서 OpenSSL과 NSS 중 하나를 선택할 수 있기를 원할 수 있습니다. 그러나 이러한 사용자는 TLS 구성이 정확히 어떻게 이루어지는지에는 관심이 없을 수 있습니다.

이로 인해 두 가지 문제가 발생합니다. 임의의 구체 구현이 주어졌을 때 라이브러리는 다음을 어떻게 수행할 수 있습니까:

  • 구현이 인증서 또는 신뢰 저장소에 대한 특정 생성자를 지원하는지(예: 임의의 식별자에서 생성하는 생성자를 지원하는지) 알아냅니까?
  • 두 컨텍스트 클래스에 대한 올바른 유형을 얻습니까?

인증서 및 신뢰 저장소 객체는 구현 외부에서도 구성할 수 있어야 합니다. 따라서 구현은 사용자가 구성한 인증서 및 신뢰 저장소와 해당 구현이 호환되는지 확인할 방법을 제공해야 합니다. 따라서 각 구현은 TLSClientConfiguration또는 TLSServerConfiguration객체를 인수로 받고, 지원되지 않는 생성자가 사용된 경우 예외를 발생시키는 validate_config메서드를 구현해야 합니다.

유형에 대해서는 두 가지 선택지가 있습니다. 모든 구체 구현이 특정 명명 체계에 맞도록 요구하거나, 이러한 객체를 가져올 수 있게 하는 API를 제공하는 것입니다.

이 PEP는 두 번째 접근 방식을 사용할 것을 제안합니다. 이를 통해 구체 구현은 적절하다고 판단하는 방식으로 코드를 구성할 수 있는 최대한의 자유를 얻으며, 적절한 속성이 설정된 단일 객체만 제공하면 됩니다. 그러면 사용자는 이 구현 객체를 이를 지원하는 라이브러리에 전달할 수 있고, 해당 라이브러리는 구체 구현의 구성과 사용을 처리할 수 있습니다.

모든 구체 구현은 TLSImplementation객체를 얻는 메서드를 제공해야 합니다. TLSImplementation객체는 전역 싱글턴일 수 있으며, 그렇게 하는 데 이점이 있다면 호출 가능 객체를 통해 생성할 수도 있습니다.

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

class TLSImplementation(Generic[_ClientContext, _ServerContext]):
    __slots__ = (
        "_client_context",
        "_server_context",
        "_validate_config",
    )

    def __init__(
        self,
        client_context: type[_ClientContext],
        server_context: type[_ServerContext],
        validate_config: Callable[[TLSClientConfiguration | TLSServerConfiguration], None],
    ) -> None:
        self._client_context = client_context
        self._server_context = server_context
        self._validate_config = validate_config

처음 두 프로퍼티는 관련 프로토콜 클래스의 구체적인 구현을 제공해야 합니다. 예를 들어, 클라이언트 컨텍스트의 경우 다음과 같습니다.

@property
def client_context(self) -> type[_ClientContext]:
    """The concrete implementation of the PEP 543 Client Context object,
    if this TLS implementation supports being the client on a TLS connection.
    """
    return self._client_context

이렇게 하면 다음과 같은 코드가 모든 구현에서 작동합니다.

client_config = TLSClientConfiguration()
client_context = implementation.client_context(client_config)

세 번째 프로퍼티는 주어진 TLS 구성이 구현과 호환되는 인증서, 개인 키 및 트러스트 스토어를 포함하는지 확인하는 함수를 제공해야 합니다.

@property
def validate_config(self) -> Callable[[TLSClientConfiguration | TLSServerConfiguration], None]:
    """A function that reveals whether this TLS implementation supports a
    particular TLS configuration.
    """
    return self._validate_config

이 함수는 인증서, 개인 키 및 트러스트 스토어에 지원되는 생성자가 사용되었는지만 확인하면 된다는 점에 유의하십시오. 객체를 파싱하거나 가져와 추가로 검증할 필요는 없습니다.

안전하지 않은 사용

위의 모든 내용은 사용자가 모듈을 안전한 방식으로 사용하려 한다는 것을 전제로 합니다. 때로는 사용자가 테스트 목적으로 인증서 검증을 비활성화하는 것과 같은 신중하지 못한 작업을 하려는 경우가 있습니다. 이를 위해 사용자가 이러한 작업을 수행할 수 있도록 별도의 insecure 모듈을 제안합니다. 이 모듈에는 구성, 컨텍스트 및 구현 객체의 안전하지 않은 변형이 포함되어 있으며, 인증서 검증과 서버 호스트 이름 검사를 비활성화할 수 있습니다.

이 기능은 정상적인 사용자가 실수로 안전하지 않은 기능을 사용하는 것을 최대한 어렵게 만들기 위해 별도의 모듈에 배치됩니다. 또한 SecurityWarning이라는 새 경고를 정의하고, 안전하지 않은 연결을 만들려고 할 때 모든 단계에서 큰 경고를 표시합니다.

이 모듈은 오직 테스트 목적으로만 사용됩니다. 자체 서명된 인증서만 가진 일부 IoT 장치에 연결하려는 실제 상황에서는, 인증서 검증을 비활성화하기 위해 안전하지 않은 모듈을 사용하는 대신 이 인증서를 사용자 지정 트러스트 스토어에 추가하는 것이 강력히 권장됩니다.

표준 라이브러리 변경 사항

TLS와 상호 작용하는 표준 라이브러리의 부분을 이러한 프로토콜 클래스들을 사용하도록 수정해야 합니다. 이렇게 하면 다른 TLS 구현에서도 작동할 수 있습니다. 여기에는 다음 모듈이 포함됩니다.

ssl 모듈의 마이그레이션

당연히 ssl 모듈 자체를 확장하여 이러한 프로토콜 클래스들을 준수하도록 해야 합니다. 이 확장은 새로운 클래스의 형태를 취하며, 완전히 새로운 모듈에 포함될 수도 있습니다. 이렇게 하면 현재 ssl 모듈을 활용하는 애플리케이션이 계속 이를 사용할 수 있으며, 새로운 API를 사용하려는 애플리케이션과 라이브러리에는 새로운 API를 사용할 수 있게 됩니다.

일반적으로 ssl 모듈에서 새로운 프로토콜 클래스로 마이그레이션하는 작업은 일대일 방식으로 이루어지지 않을 것으로 예상합니다. 이는 일반적으로 허용할 수 있습니다. ssl 모듈을 사용하는 대부분의 도구는 해당 모듈을 사용자에게 숨기므로, 새로운 모듈을 사용하도록 리팩터링해도 사용자에게는 보이지 않아야 합니다.

그러나 정의된 API의 일부로 또는 (쉽게 발생할 수 있는) 우연한 이유로 ssl 모듈의 예외를 누출하는 라이브러리나 애플리케이션에서는 특정 문제가 발생합니다. 이러한 도구의 사용자는 ssl 모듈에서 예외가 발생해도 이를 허용하고 처리하는 코드를 작성했을 수 있습니다. 여기서 제시하는 프로토콜 클래스로 마이그레이션하면 대신 위에서 정의한 예외가 발생할 수 있으며, 기존 except 블록은 이를 포착하지 못합니다.

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

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

이를 수행하는 정확한 메커니즘은 현재 ssl 예외가 C 코드에 정의되어 있다는 사실 때문에 더 복잡해지므로 이 PEP의 범위를 벗어나지만, 자세한 내용은 Christian Heimes가 Security-SIG에 보낸 이메일에서 확인할 수 있습니다.

향후

향후의 주요 TLS 기능을 지원하려면 이러한 프로토콜 클래스를 개정해야 할 수 있습니다. 이러한 개정은 신중하게 이루어져야 합니다. 많은 구현은 신속하게 전진하지 못할 수 있으며, 이러한 프로토콜 클래스의 변경으로 무효화될 수 있습니다. 이는 허용할 수 있지만, 가능한 경우 개별 구현에 특화된 기능을 프로토콜 클래스에 추가해서는 안 됩니다. 프로토콜 클래스는 IETF가 지정한 기능을 고수준으로 설명하는 데 그쳐야 합니다.

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

기여자

이 PEP는 2020년에 철회된 PEP 543에서 상당 부분을 개작한 것입니다. PEP 543는 Cory Benfield와 Christian Heimes가 작성했으며, 이를 실질적으로 형성하는 데 크게 기여한 여러 커뮤니티 구성원으로부터 광범위한 검토를 받았습니다. 이 PEP와 PEP 543에 대한 상세한 검토는 다음 사람들이 제공했습니다:

  • 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
  • 스티븐 팩클러
  • 웨스 터너
  • 윌 본드
  • 코리 벤필드
  • 마르크앙드레 렘부르크
  • 세스 M. 라슨
  • 빅터 스틴너
  • 로널드 우서렌

Security-SIG 및 python-ideas 메일링 리스트에서 PEP 543에 대한 추가 검토가 이루어졌습니다.