rfc-ko

RFC 9846: TLS 프로토콜 버전 1.3 (한국어 번역)

역주: 용어는 glossary.md, 번역 방식은 번역 가이드를 따른다. 핸드셰이크 메시지와 구조체·필드 이름(ClientHello, legacy_version 등)은 코드 서식으로, 원문이 따옴표로 쓴 확장·경고 이름(“supported_versions”, “close_notify” 등)은 따옴표 그대로 둔다. 구조체 정의 코드 블록은 원문 그대로 두고 코드 안의 주석만 옮긴다.


요약

이 문서는 TLS(Transport Layer Security) 프로토콜의 버전 1.3을 규정한다. TLS를 사용하면 클라이언트/서버 애플리케이션이 도청(eavesdropping), 변조(tampering), 메시지 위조(message forgery)를 막도록 설계된 방식으로 인터넷에서 통신할 수 있다.

이 문서는 TLS 1.3을 규정했던 RFC 8446을 대체한다. 또한 이 문서는 RFC 5246(TLS 1.2 규정)과, 모두 TLS 1.2 이하에 관한 RFC 5077, 6961, 7627, 8422를 대체하고, RFC 5705와 6066을 갱신한다. 이 문서는 TLS 1.2 구현에 대한 새 요구 사항도 규정한다.

이 메모의 상태

이 문서는 인터넷 표준화 트랙(Standards Track) 문서다.

이 문서는 IETF(Internet Engineering Task Force)의 결과물이며 IETF 커뮤니티의 합의를 나타낸다. 이 문서는 공개 검토를 거쳤고 IESG(Internet Engineering Steering Group)의 발행 승인을 받았다. 인터넷 표준에 대한 자세한 정보는 RFC 7841의 2절에 있다.

이 문서의 현재 상태, 정오표, 의견을 보내는 방법은 https://www.rfc-editor.org/info/rfc9846 에서 확인할 수 있다.

저작권 고지

Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.

This document is subject to BCP 78 and the IETF Trust’s Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.

This document may contain material from IETF Documents or IETF Contributions published or made publicly available before November 10, 2008. The person(s) controlling the copyright in some of this material may not have granted the IETF Trust the right to allow modifications of such material outside the IETF Standards Process. Without obtaining an adequate license from the person(s) controlling the copyright in such materials, this document may not be modified outside the IETF Standards Process, and derivative works of it may not be created outside the IETF Standards Process, except to format it for publication as an RFC or to translate it into languages other than English.

Copyright (c) 2026 IETF Trust 및 문서 저자로 명시된 사람들. 모든 권리 보유.

이 문서에는 BCP 78과, 이 문서의 발행일에 유효한 IETF 문서 관련 IETF Trust 법적 조항(https://trustee.ietf.org/license-info)이 적용된다. 이 문서들은 이 문서에 대한 독자의 권리와 제한을 설명하므로 주의 깊게 검토하기 바란다. 이 문서에서 추출한 코드 구성 요소(Code Components)에는 Trust 법적 조항 4.e절에 설명된 Revised BSD License 문구를 포함해야 하며, 이 코드 구성 요소는 Revised BSD License에 설명된 대로 보증 없이 제공된다.

이 문서에는 2008년 11월 10일 이전에 발행되었거나 공개된 IETF 문서 또는 IETF 기고(Contribution)의 내용이 포함되어 있을 수 있다. 그 내용 일부의 저작권을 가진 사람은 IETF 표준화 절차 밖에서 그 내용을 수정하도록 허락할 권리를 IETF Trust에 부여하지 않았을 수 있다. 그런 내용의 저작권자에게서 적절한 허락을 받지 않는 한, 이 문서는 IETF 표준화 절차 밖에서 수정될 수 없고 IETF 표준화 절차 밖에서 이 문서의 2차 저작물을 만들 수 없다. 단, RFC로 발행하기 위해 서식을 맞추는 것과 영어 이외의 언어로 번역하는 것은 예외다.

역주: 원문이 따르는 IETF Trust 법적 조항에 따라 원문의 Copyright Notice를 그대로 두고 그 아래에 한국어 번역을 덧붙였으며, 법적 효력은 원문에 있다. 마지막 문단에 따라 이 문서의 번역은 허용된다.

목차


1. 소개

TLS의 주된 목표는 통신하는 두 피어 사이에 보안 채널(secure channel)을 제공하는 것이다. 기반 전송 계층에 요구하는 것은 신뢰할 수 있고 순서가 보장되는 데이터 스트림뿐이다. 구체적으로, 보안 채널은 다음 속성을 제공해야 한다.

이 속성들은 [RFC3552]에 설명된 것처럼 네트워크를 완전히 장악한 공격자 앞에서도 성립해야 한다. 관련 보안 속성을 더 완전하게 서술한 내용은 부록 F를 참고한다.

TLS는 두 가지 주요 구성 요소로 이루어진다.

TLS는 애플리케이션 프로토콜과 독립적이다. 상위 프로토콜은 TLS 위에 투명하게 올라갈 수 있다. 그러나 TLS 표준은 프로토콜이 TLS로 보안을 더하는 방법을 규정하지 않는다. TLS 핸드셰이크를 시작하는 방법과 교환한 인증용 인증서를 해석하는 방법은 TLS 위에서 동작하는 프로토콜의 설계자와 구현자의 판단에 맡긴다. TLS를 사용하는 애플리케이션 프로토콜은 핸드셰이크가 언제 어떻게 일어나는지, 신원 검증을 어떻게 하는지를 포함해, TLS가 그 애플리케이션 프로토콜과 어떻게 함께 동작하는지를 반드시 규정해야 한다(MUST). TLS를 애플리케이션 프로토콜과 통합하는 데 유용한 지침은 [RFC9525]에 있다.

이 문서는 TLS 버전 1.3을 정의한다. TLS 1.3은 이전 버전과 직접 호환되지 않지만, 모든 TLS 버전에는 버전 관리 메커니즘이 들어 있다. 이 메커니즘으로 클라이언트와 서버는 두 피어가 모두 지원하는 공통 버전이 있으면 그 버전을 상호 운용 가능하게 협상할 수 있다.

이 문서는 버전 1.2 [RFC5246], 확장 마스터 비밀값(extended master secret) 확장 [RFC7627], TLS 1.2용 타원 곡선 정의 [RFC8422]를 비롯한 이전 버전의 TLS를 대체한다. 또한 [RFC5077]에 정의된 TLS 티켓 메커니즘을 대체하고 2.2절에 정의된 메커니즘으로 바꾼다. TLS 1.3은 키를 유도하는 방식을 바꾸므로, 7.5절에 설명된 대로 [RFC5705]를 갱신한다. 익스포터(exporter)는 더 이상 컨텍스트가 없는 경우와 빈 컨텍스트인 경우를 구분하지 않는다. 또한 이 문서는 OCSP(Online Certificate Status Protocol) 메시지를 전달하는 방식을 바꾸므로, 4.5.1.1절에 설명된 대로 [RFC6066]을 갱신하고 [RFC6961]을 대체한다.

1.1. 표기 규칙과 용어

이 문서의 키워드 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, “OPTIONAL”은 여기에 보인 것처럼 모두 대문자로 쓰였을 때, 그리고 그때에만 BCP 14 [RFC2119] [RFC8174]에 설명된 대로 해석한다.

이 문서에서는 다음 용어를 쓴다.

1.2. RFC 8446과의 관계

TLS 1.3은 처음에 [RFC8446]에서 규정되었다. 이 문서는 같은 버전 번호를 유지하고 하위 호환되는, TLS 1.3의 작은 갱신이다. 이 문서는 일부 요구 사항을 강화하고, 불명확하다고 밝혀진 부분의 문구를 고쳤으며, 그 밖의 편집상 개선을 담았다. 또한 비밀값(secret)에 붙이던 용어 “master”를 없애고, 그 대신 용어 “main”을 쓰거나 용어가 필요 없는 곳에서는 더 짧은 이름을 쓴다. 이 문서의 구체적인 기술적 변경은 다음과 같다.

이 밖에도 보안 고려 사항, 특히 프라이버시에 관한 부분이 일부 개선되었다.

1.3. TLS 1.2와의 주요 차이

다음은 TLS 1.2와 TLS 1.3 사이의 주요 기능상 차이를 나열한 것이다. 모든 차이를 다 담으려는 목록은 아니며, 작은 차이는 이 밖에도 많다.

1.4. TLS 1.2에 영향을 주는 갱신

이 문서는 TLS 1.2 구현에 선택적으로 영향을 주는 몇 가지 변경을 정의한다. TLS 1.3을 지원하지 않는 TLS 1.2 구현도 여기에 포함된다.

이와 함께 이 문서는 이전 TLS 버전에 대한 일부 준수 요구 사항을 명확히 한다. 9.3절을 참고한다.

2. 프로토콜 개요

보안 채널이 사용하는 암호 매개 변수는 TLS 핸드셰이크 프로토콜이 만들어 낸다. 클라이언트와 서버는 서로 처음 통신할 때 TLS의 이 하위 프로토콜을 사용한다. 핸드셰이크 프로토콜로 피어들은 프로토콜 버전을 협상하고, 암호 알고리즘을 선택하고, 서로를 인증하고(클라이언트 인증은 선택 사항), 공유 비밀 키 재료를 수립할 수 있다. 핸드셰이크가 끝나면 피어들은 수립한 키를 사용해 애플리케이션 계층 트래픽을 보호한다.

핸드셰이크가 실패하거나 그 밖의 프로토콜 오류가 일어나면 연결이 종료된다. 종료 전에 경고 메시지(6장)를 보낼 수도 있다.

TLS는 세 가지 기본 키 교환 모드를 지원한다.

이 문서에서 비대칭 키 교환에는 유한체 위의 Diffie-Hellman과 타원 곡선 위의 Diffie-Hellman이 모두 포함된다. 키 캡슐화 방식(key-encapsulation mechanism, KEM)의 사용은 다른 확장이 정의한다.

아래 그림 1은 기본적인 전체 TLS 핸드셰이크(full handshake)를 보여 준다.

       Client                                         Server

Key  ^ ClientHello
Exch | + key_share*
     | + signature_algorithms*
     | + psk_key_exchange_modes*
     v + pre_shared_key*       -------->
                                                  ServerHello  ^ Key
                                                 + key_share*  | Exch
                                            + pre_shared_key*  v
                                        {EncryptedExtensions}  ^  Server
                                        {CertificateRequest*}  v  Params
                                               {Certificate*}  ^
                                         {CertificateVerify*}  | Auth
                                                   {Finished}  v
                               <--------  [Application Data*]
     ^ {Certificate*}
Auth | {CertificateVerify*}
     v {Finished}              -------->
       [Application Data]      <------->   [Application Data]

              +  Indicates noteworthy extensions sent in the
                 previously noted message.

              *  Indicates optional or situation-dependent
                 messages/extensions that are not always sent.

              {} Indicates messages protected using keys
                 derived from a [sender]_handshake_traffic_secret.

              [] Indicates messages protected using keys
                 derived from [sender]_application_traffic_secret_N.

그림 1: 전체 TLS 핸드셰이크의 메시지 흐름

역주: 그림의 기호는 다음 뜻이다. +는 바로 앞에 적힌 메시지에 담아 보내는 주목할 만한 확장, *는 항상 보내지는 않는 선택적이거나 상황에 따른 메시지·확장, {}는 [sender]_handshake_traffic_secret에서 유도한 키로 보호하는 메시지, []는 [sender]_application_traffic_secret_N에서 유도한 키로 보호하는 메시지다. [sender]는 보내는 쪽에 따라 client 또는 server가 된다. 그림 양옆의 ^·|·v 괄호와 Key Exch(키 교환), Server Params(서버 매개 변수), Auth(인증)는 아래에서 설명하는 핸드셰이크의 세 단계를 나타낸다.

핸드셰이크는 세 단계로 이루어진다고 볼 수 있다(위 그림에 표시됨).

키 교환 단계에서 클라이언트는 ClientHello(4.2.2절) 메시지를 보낸다. 이 메시지에는 무작위 논스(nonce)인 ClientHello.random, 클라이언트가 제시하는 프로토콜 버전, 대칭 암호/해시 쌍의 목록이 들어 있다. 또한 비대칭 키 교환 공유값(share)의 목록(“key_share” 확장(4.3.8절)에 담김)과 사전 공유 키 레이블의 목록(“pre_shared_key” 확장(4.3.11절)에 담김) 가운데 하나 또는 둘 다가 들어 있고, 추가 확장이 더 들어갈 수도 있다. 미들박스 호환성을 위해 필드나 메시지가 더 있을 수도 있다.

서버는 ClientHello를 처리해 연결에 알맞은 암호 매개 변수를 정한다. 그런 다음 서버는 협상된 연결 매개 변수를 나타내는 자신의 ServerHello(4.2.3절)로 응답한다. ClientHello와 ServerHello를 조합해 공유 키가 정해진다. 비대칭 키 수립을 사용하면, ServerHello에는 서버의 임시(ephemeral) 비대칭 키 교환 공유값을 담은 “key_share” 확장이 들어간다. 서버의 공유값은 반드시 클라이언트 공유값 중 하나와 같은 그룹에 속해야 한다(MUST). PSK 키 수립을 사용하면, ServerHello에는 클라이언트가 제시한 PSK 중 어느 것을 선택했는지 나타내는 “pre_shared_key” 확장이 들어간다. 구현은 비대칭 키 교환과 PSK를 함께 쓸 수 있으며, 그 경우 두 확장이 모두 들어간다는 점에 유의하라.

이어서 서버는 서버 매개 변수를 정하기 위해 메시지 두 개를 보낸다.

마지막으로 클라이언트와 서버는 인증 메시지를 교환한다. TLS는 인증서 기반 인증이 필요할 때마다 같은 메시지 묶음을 사용한다. (PSK 기반 인증은 키 교환의 부수 효과로 이루어진다.) 구체적으로는 다음과 같다.

서버의 메시지를 받으면 클라이언트는 자신의 인증 메시지, 즉 Certificate와 CertificateVerify(요청받은 경우), 그리고 Finished로 응답한다.

이 시점에서 핸드셰이크가 끝나고, 클라이언트와 서버는 인증된 암호화(authenticated encryption)로 보호된 애플리케이션 계층 데이터를 레코드 계층이 교환하는 데 필요한 키 재료를 유도한다. 2.3절에 명시된 경우를 제외하면, 애플리케이션 데이터는 Finished 메시지를 보내기 전에 보내서는 안 된다(MUST NOT). 서버는 클라이언트의 인증 메시지를 받기 전에 애플리케이션 데이터를 보낼 수 있지만, 그 시점에 보낸 데이터는 물론 인증되지 않은 피어에게 보내는 것이라는 점에 유의하라.

2.1. 잘못된 DHE 공유값

클라이언트가 충분한 “key_share” 확장을 제공하지 않았다면(예: 서버가 받아들이지 않거나 지원하지 않는 DHE 또는 ECDHE 그룹만 들어 있는 경우), 서버는 HelloRetryRequest로 불일치를 바로잡고, 클라이언트는 그림 2처럼 알맞은 “key_share” 확장으로 핸드셰이크를 다시 시작할 필요가 있다. 공통 암호 매개 변수를 하나도 협상할 수 없으면, 서버는 반드시 알맞은 경고로 핸드셰이크를 중단해야 한다(MUST).

         Client                                               Server

         ClientHello
         + key_share             -------->
                                                   HelloRetryRequest
                                 <--------               + key_share
         ClientHello
         + key_share             -------->
                                                         ServerHello
                                                         + key_share
                                               {EncryptedExtensions}
                                               {CertificateRequest*}
                                                      {Certificate*}
                                                {CertificateVerify*}
                                                          {Finished}
                                 <--------       [Application Data*]
         {Certificate*}
         {CertificateVerify*}
         {Finished}              -------->
         [Application Data]      <------->        [Application Data]

그림 2: 매개 변수가 맞지 않는 전체 핸드셰이크의 메시지 흐름

역주: 그림의 기호는 그림 1과 같은 뜻이다.

참고: 핸드셰이크 트랜스크립트(handshake transcript)에는 처음의 ClientHello/HelloRetryRequest 교환이 포함된다. 새 ClientHello를 보낸다고 해서 트랜스크립트가 초기화되지 않는다.

TLS에서는 기본 핸드셰이크를 최적화한 변형도 몇 가지 쓸 수 있다. 다음 절들에서 이를 설명한다.

2.2. 재개와 사전 공유 키(PSK)

TLS PSK는 외부에서 수립할 수도 있지만, 이전 연결에서 수립해 두었다가 새 연결을 수립하는 데 쓸 수도 있다(PSK를 사용한 “세션 재개(session resumption)” 또는 “재개(resuming)”). 핸드셰이크가 끝나면 서버는 처음 핸드셰이크에서 유도한 고유한 키에 대응하는 PSK 식별자(PSK identity)를 클라이언트에게 보낼 수 있다(4.7.1절 참고). 그러면 클라이언트는 이후 핸드셰이크에서 그 PSK 식별자를 사용해 연관된 PSK의 사용을 협상할 수 있다. 서버가 그 PSK를 받아들이면, 새 연결의 보안 컨텍스트는 원래 연결에 암호학적으로 묶이며, 전체 핸드셰이크를 하는 대신 처음 핸드셰이크에서 유도한 키로 암호 상태를 세운다. TLS 1.2 이하에서는 이 기능을 “세션 ID(session ID)”와 “세션 티켓(session ticket)” [RFC5077]이 제공했다. TLS 1.3에서는 두 메커니즘 모두 폐기되었다.

PSK는 비대칭 키 교환과 함께 사용해 공유 키와 결합함으로써 순방향 비밀성을 제공할 수도 있고, 애플리케이션 데이터의 순방향 비밀성을 잃는 대가로 단독으로 사용할 수도 있다.

그림 3은 핸드셰이크 한 쌍을 보여 준다. 첫 핸드셰이크에서 PSK를 수립하고, 두 번째 핸드셰이크에서 그 PSK를 사용한다.

          Client                                               Server

   Initial Handshake:
          ClientHello
          + key_share               -------->
                                                          ServerHello
                                                          + key_share
                                                {EncryptedExtensions}
                                                {CertificateRequest*}
                                                       {Certificate*}
                                                 {CertificateVerify*}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Certificate*}
          {CertificateVerify*}
          {Finished}                -------->
                                    <--------      [NewSessionTicket]
          [Application Data]        <------->      [Application Data]


   Subsequent Handshake:
          ClientHello
          + key_share*
          + psk_key_exchange_modes
          + pre_shared_key          -------->
                                                          ServerHello
                                                     + pre_shared_key
                                                         + key_share*
                                                {EncryptedExtensions}
                                                           {Finished}
                                    <--------     [Application Data*]
          {Finished}                -------->
          [Application Data]        <------->      [Application Data]

그림 3: 재개와 PSK의 메시지 흐름

역주: 그림의 기호는 그림 1과 같은 뜻이다. “Initial Handshake”는 처음 핸드셰이크, “Subsequent Handshake”는 이후 핸드셰이크다.

서버는 PSK로 인증하므로 Certificate나 CertificateVerify 메시지를 보내지 않는다. 클라이언트가 PSK를 통한 재개를 제시할 때는, 필요하면 서버가 재개를 거절하고 전체 핸드셰이크로 돌아갈 수 있도록 “key_share” 확장도 서버에 보내야 한다(SHOULD). 서버는 PSK 키 수립의 사용을 협상하기 위해 “pre_shared_key” 확장으로 응답하며, (여기에 보인 것처럼) 비대칭 키 수립을 하기 위해 “key_share” 확장으로 응답해 순방향 비밀성을 제공할 수 있다.

PSK를 외부에서 프로비저닝(provisioning)하는 경우, PSK 식별자와 그 PSK에 쓸 KDF 해시 알고리즘도 반드시 함께 프로비저닝해야 한다(MUST).

참고: 외부에서 프로비저닝한 사전 공유 비밀값을 쓸 때는, [RFC4086]에서 논의한 대로 키를 생성할 때 충분한 엔트로피를 쓰는 것이 매우 중요하다. 비밀번호나 그 밖의 엔트로피가 낮은 출처에서 공유 비밀값을 유도하는 것은 안전하지 않다. 엔트로피가 낮은 비밀값이나 비밀번호는 PSK 바인더(PSK binder)를 이용한 사전 공격(dictionary attack)에 취약하다. 여기에 규정한 PSK 인증은 비대칭 키 수립과 함께 쓰더라도 강력한 비밀번호 기반 인증 키 교환이 아니다. 구체적으로, 핸드셰이크를 관찰할 수 있는 공격자가 비밀번호/사전 공유 키에 무차별 대입 공격(brute-force attack)을 하는 것을 막지 못한다.

2.3. 0-RTT 데이터

클라이언트와 서버가 PSK를 공유하면(외부에서 얻었든 이전 핸드셰이크로 얻었든), TLS 1.3에서는 클라이언트가 첫 번째 플라이트(flight)에 데이터를 보낼 수 있다(“조기 데이터(early data)”). 클라이언트는 PSK를 사용해 서버를 인증하고 조기 데이터를 암호화한다.

그림 4처럼 0-RTT 데이터는 첫 번째 플라이트에서 1-RTT 핸드셰이크에 더해질 뿐이다. 핸드셰이크의 나머지 부분은 PSK 재개를 사용하는 1-RTT 핸드셰이크와 같은 메시지를 쓴다.

         Client                                               Server

         ClientHello
         + early_data
         + key_share*
         + psk_key_exchange_modes
         + pre_shared_key
         (Application Data*)     -------->
                                                         ServerHello
                                                    + pre_shared_key
                                                        + key_share*
                                               {EncryptedExtensions}
                                                       + early_data*
                                                          {Finished}
                                 <--------       [Application Data*]
         (EndOfEarlyData*)
         {Finished}              -------->
         [Application Data]      <------->        [Application Data]

               +  Indicates noteworthy extensions sent in the
                  previously noted message.

               *  Indicates optional or situation-dependent
                  messages/extensions that are not always sent.

               () Indicates messages protected using keys
                  derived from a client_early_traffic_secret.

               {} Indicates messages protected using keys
                  derived from a [sender]_handshake_traffic_secret.

               [] Indicates messages protected using keys
                  derived from [sender]_application_traffic_secret_N.

그림 4: 0-RTT 핸드셰이크의 메시지 흐름

역주: 기호 +, *, {}, []의 뜻은 그림 1과 같다. ()는 client_early_traffic_secret에서 유도한 키로 보호하는 메시지다.

중요: 0-RTT 데이터의 보안 속성은 다른 종류의 TLS 데이터보다 약하다. 구체적으로는 다음과 같다.

  1. 프로토콜은 이 데이터에 대해 어떤 순방향 비밀성도 보장하지 않는다. 순방향 비밀성 보장이 적용되는지, 적용된다면 어떤 것인지는 서버의 동작에 따라 정해진다(8.1절 참고). 이 동작은 프로토콜의 일부로 클라이언트에 전달되지 않는다. 따라서 서버의 동작을 대역 외(out-of-band)에서 알지 못하는 한, 클라이언트는 이 데이터가 순방향 비밀성을 갖지 않는다고 가정해야 한다.

  2. 연결 사이에 재전송이 없다는 보장이 없다. 일반적인 TLS 1.3 1-RTT 데이터는 서버의 Random 값으로 재전송을 막지만, 0-RTT 데이터는 ServerHello에 의존하지 않으므로 보장이 더 약하다. 이 점은 데이터를 TLS 클라이언트 인증으로 인증하든 애플리케이션 프로토콜 안에서 인증하든, 데이터가 인증되는 경우에 특히 중요하다. early_exporter_secret을 쓰는 모든 경우에도 같은 경고가 적용된다.

0-RTT 데이터는 한 연결 안에서 중복될 수 없다(즉, 서버는 같은 연결에서 같은 데이터를 두 번 처리하지 않는다). 또한 공격자는 0-RTT 데이터를 1-RTT 데이터처럼 보이게 만들 수 없다(서로 다른 키로 보호되기 때문이다). 부록 F.5에 가능한 공격이 설명되어 있고, 8장에서는 서버가 재전송의 영향을 제한하는 데 쓸 수 있는 메커니즘을 설명한다.

3. 표현 언어

이 문서는 데이터를 외부 표현(external representation)으로 나타내는 형식을 다룬다. 이 문서에서는 다음과 같은, 아주 기본적이고 다소 느슨하게 정의된 표현 문법(presentation syntax)을 쓴다.

아래 정의에서 이 문법의 선택적 구성 요소는 [[ ]](이중 대괄호)로 둘러싸 나타낸다.

3.1. 기본 블록 크기

모든 데이터 항목의 표현은 명시적으로 규정된다. 기본 데이터 블록 크기는 1바이트(즉, 8비트)다. 멀티바이트(multibyte) 데이터 항목은 바이트를 왼쪽에서 오른쪽으로, 위에서 아래로 이어 붙인 것이다. 바이트 스트림에서 멀티바이트 항목(다음 예에서는 숫자)은 (C 표기법으로) 다음과 같이 만들어진다.

      value = (byte[0] << 8*(n-1)) | (byte[1] << 8*(n-2)) |
              ... | byte[n-1];

멀티바이트 값의 이 바이트 순서는 흔히 쓰는 네트워크 바이트 순서(network byte order), 곧 빅 엔디언(big-endian) 형식이다.

3.2. 기타

주석은 /*로 시작해 */로 끝난다.

해석하지 않는 데이터를 담는 1바이트 단위의 개체는 opaque 타입(type)이다.

기존 타입 T에 대한 타입 별칭 T'은 다음과 같이 정의한다.

      T T';

3.3. 숫자

기본 숫자 데이터 타입은 부호 없는 바이트(uint8)다. 그보다 큰 숫자 데이터 타입은 모두 3.1절에 설명한 대로 이어 붙인 고정 길이의 바이트 열로 구성되며, 역시 부호가 없다. 다음 숫자 타입이 미리 정의되어 있다.

      uint8 uint16[2];
      uint8 uint24[3];
      uint8 uint32[4];
      uint8 uint64[8];

여기서든 명세의 다른 곳에서든 모든 값은 네트워크 바이트(빅 엔디언) 순서로 전송된다. 16진수 바이트 01 02 03 04로 표현되는 uint32는 10진수 값 16909060과 같다.

3.4. 벡터

벡터(vector, 1차원 배열)는 동질적인 데이터 요소의 스트림이다. 이 명세는 설명의 편의상 벡터를 목록(list)이라고 부른다. 벡터의 크기는 문서를 작성할 때 정해 둘 수도 있고, 실행 시점까지 정하지 않고 둘 수도 있다. 어느 경우든 길이는 벡터에 든 요소의 수가 아니라 바이트 수를 나타낸다. 타입 T의 고정 길이 벡터인 새 타입 T'을 규정하는 문법은 다음과 같다.

      T T'[n];

여기서 T'은 데이터 스트림에서 n바이트를 차지하며, n은 T 크기의 배수다. 벡터의 길이는 인코딩된 스트림에 들어가지 않는다.

다음 예에서 Datum은 프로토콜이 해석하지 않는 연속된 3바이트로 정의되고, Data는 연속된 Datum 3개로 모두 9바이트를 차지한다.

      opaque Datum[3];      /* 해석하지 않는 3바이트 */
      Datum Data[9];        /* 연속된 3바이트 벡터 3개 */

가변 길이 벡터는 허용되는 길이의 하위 범위를 <floor..ceiling> 표기법으로 지정해 정의한다(양 끝값 포함). 가변 길이 벡터를 인코딩하면 바이트 스트림에서 실제 길이가 벡터 내용 앞에 온다. 이 길이는 벡터에 지정된 최대(ceiling) 길이를 담는 데 필요한 만큼의 바이트를 차지하는 수의 형태다. 실제 길이 필드가 0인 가변 길이 벡터를 빈 벡터(empty vector)라고 한다.

      T T'<floor..ceiling>;

다음 예에서 “mandatory”는 opaque 타입의 바이트를 300~400개 담아야 하는 벡터다. 이 벡터는 결코 비어 있을 수 없다. 실제 길이 필드는 값 400을 나타내기에 충분한 2바이트, 즉 uint16을 차지한다(3.3절 참고). 마찬가지로 “longer”는 최대 800바이트의 데이터, 곧 uint16 요소 400개까지 나타낼 수 있고, 비어 있을 수도 있다. 이 벡터의 인코딩에는 벡터 앞에 붙는 2바이트 실제 길이 필드가 들어간다. 인코딩된 벡터의 길이는 요소 하나의 길이의 정확한 배수여야 한다(예: uint16 요소로 된 17바이트 벡터는 허용되지 않는다).

      opaque mandatory<300..400>;
            /* 길이 필드는 2바이트, 비어 있을 수 없음 */
      uint16 longer<0..800>;
            /* 16비트 부호 없는 정수 0~400개 */

3.5. 열거형

“enum” 또는 “enumerated”라고 부르는, 값이 드문드문한 데이터 타입인 열거형이 하나 더 있다. 열거형은 정의마다 서로 다른 타입이다. 같은 타입의 열거형끼리만 대입하거나 비교할 수 있다. 다음 예에서 보듯이 열거형의 모든 요소에는 값을 할당해야 한다. 열거형의 요소에는 순서가 없으므로, 요소에는 고유하기만 하면 어떤 값이든 어떤 순서로든 할당할 수 있다.

      enum { e1(v1), e2(v2), ... , en(vn) [[, (n)]] } Te;

앞으로 프로토콜의 확장이나 추가 사항에서 새 값을 정의할 수 있다. 필드 정의에서 달리 말하지 않는 한, 구현은 알 수 없는 값을 파싱해서 무시할 수 있을 필요가 있다.

열거형은 바이트 스트림에서, 정의된 서수 값 중 가장 큰 값을 나타내는 데 필요한 만큼의 공간을 차지한다. 다음 정의에서는 Color 타입의 필드를 담는 데 1바이트를 쓰게 된다.

      enum { red(3), blue(5), white(7) } Color;

쓸데없는 요소를 정의하지 않고 폭(width)을 정하기 위해, 연관 태그가 없는 값을 선택적으로 지정할 수 있다.

다음 예에서 Taste는 데이터 스트림에서 2바이트를 차지하지만, 현재 버전의 프로토콜에서는 값 1, 2, 4만 가질 수 있다.

      enum { sweet(1), sour(2), bitter(4), (32000) } Taste;

열거형 요소의 이름은 정의된 타입 안에서 유효 범위를 갖는다. 첫 번째 예에서 열거형의 두 번째 요소를 완전히 한정해 참조하면 Color.blue가 된다. 대입 대상이 잘 규정되어 있으면 이런 한정은 필요 없다.

      Color color = Color.blue;     /* 과하게 지정했지만 허용됨 */
      Color color = blue;           /* 올바름, 타입은 암묵적 */

열거형에 할당하는 이름은 고유하지 않아도 된다. 숫자 값으로 같은 이름이 적용되는 범위를 나타낼 수 있다. 이 값은 범위의 최솟값과 최댓값(둘 다 포함)을 마침표 두 개로 구분해 적는다. 이는 주로 값 공간의 영역을 예약하는 데 유용하다.

      enum { sad(0), meh(1..254), happy(255) } Mood;

3.6. 구성 타입

편의를 위해 원시 타입(primitive type)으로 구조체 타입을 구성할 수 있다. 구조체를 규정할 때마다 고유한 새 타입이 선언된다. 정의에 쓰는 문법은 C의 문법과 매우 비슷하다.

      struct {
          T1 f1;
          T2 f2;
          ...
          Tn fn;
      } T;

고정 길이와 가변 길이 목록(벡터) 필드는 표준 목록 문법으로 쓸 수 있다. 변형 예(3.8절)의 구조체 V1과 V2가 이를 보여 준다.

구조체 안의 필드는 열거형에서 쓸 수 있는 것과 매우 비슷한 문법으로, 타입 이름으로 한정할 수 있다. 예를 들어 T.f2는 앞 선언의 두 번째 필드를 가리킨다.

3.7. 상수

필드와 변수에는 다음처럼 =를 사용해 고정 값을 할당할 수 있다.

      struct {
          T1 f1 = 8;  /* T.f1은 항상 8이어야 함 */
          T2 f2;
      } T;

3.8. 변형

정의된 구조체는 환경 안에서 알 수 있는 정보에 따라 변형(variant)을 가질 수 있다. 선택자(selector)는 구조체가 정의하는 가능한 변형을 정의하는 열거형 타입이어야 한다. 아래 select의 각 갈래(arm)는 그 변형의 필드 타입과 선택적인 필드 레이블을 지정한다. 실행 시점에 변형을 선택하는 메커니즘은 표현 언어가 규정하지 않는다.

      struct {
          T1 f1;
          T2 f2;
          ....
          Tn fn;
          select (E) {
              case e1: Te1 [[fe1]];
              case e2: Te2 [[fe2]];
              ....
              case en: Ten [[fen]];
          };
      } Tv;

예를 들면 다음과 같다.

      enum { apple(0), orange(1) } VariantTag;

      struct {
          uint16 number;
          opaque string<0..10>; /* 가변 길이 */
      } V1;

      struct {
          uint32 number;
          opaque string[10];    /* 고정 길이 */
      } V2;

      struct {
          VariantTag type;
          select (VariantRecord.type) {
              case apple:  V1;
              case orange: V2;
          };
      } VariantRecord;

4. 핸드셰이크 프로토콜

핸드셰이크 프로토콜은 연결의 보안 매개 변수를 협상하는 데 쓰인다. 핸드셰이크 메시지는 TLS 레코드 계층에 넘겨지고, 레코드 계층에서 하나 이상의 TLSPlaintext 또는 TLSCiphertext 구조체에 캡슐화된다. 이 구조체는 현재 활성 연결 상태(connection state)가 규정하는 대로 처리되어 전송된다.

      enum {
          client_hello(1),
          server_hello(2),
          new_session_ticket(4),
          end_of_early_data(5),
          encrypted_extensions(8),
          certificate(11),
          certificate_request(13),
          certificate_verify(15),
          finished(20),
          key_update(24),
          message_hash(254),
          (255)
      } HandshakeType;

      struct {
          HandshakeType msg_type;    /* 핸드셰이크 유형 */
          uint24 length;             /* 메시지의 남은 바이트 수 */
          select (Handshake.msg_type) {
              case client_hello:          ClientHello;
              case server_hello:          ServerHello;
              case end_of_early_data:     EndOfEarlyData;
              case encrypted_extensions:  EncryptedExtensions;
              case certificate_request:   CertificateRequest;
              case certificate:           Certificate;
              case certificate_verify:    CertificateVerify;
              case finished:              Finished;
              case new_session_ticket:    NewSessionTicket;
              case key_update:            KeyUpdate;
          };
      } Handshake;

TLS 확장으로 바뀌지 않는 한, 프로토콜 메시지는 반드시 4.1절에 정의되고 2장의 그림에 보인 순서대로 보내야 한다(MUST). 예상하지 않은 순서로 핸드셰이크 메시지를 받은 피어는 반드시 “unexpected_message” 경고로 핸드셰이크를 중단해야 한다(MUST).

새 핸드셰이크 메시지 유형은 11장에 설명된 대로 IANA가 할당한다.

4.1. 트랜스크립트 해시

TLS의 암호 계산 중 상당수는 트랜스크립트 해시를 사용한다. 이 값은 포함 대상인 각 핸드셰이크 메시지를 이어 붙인 것을 해시해 계산한다. 이때 핸드셰이크 메시지 유형과 길이 필드를 담은 핸드셰이크 메시지 헤더는 포함하지만, 레코드 계층 헤더는 포함하지 않는다. 즉, 다음과 같다.

    Transcript-Hash(M1, M2, ... Mn) = Hash(M1 || M2 || ... || Mn)

이 일반 규칙의 예외로, 서버가 ClientHello에 HelloRetryRequest로 응답하면 ClientHello1의 값을, 핸드셰이크 유형이 “message_hash”(254)이고 Hash(ClientHello1)을 담은 특수한 합성(synthetic) 핸드셰이크 메시지로 바꾼다. 즉, 다음과 같다.

  Transcript-Hash(ClientHello1, HelloRetryRequest, ... Mn) =
      Hash(message_hash ||        /* 핸드셰이크 유형 */
           00 00 Hash.length  ||  /* 핸드셰이크 메시지 길이(바이트) */
           Hash(ClientHello1) ||  /* ClientHello1의 해시 */
           HelloRetryRequest  || ... || Mn)

이렇게 구성하는 이유는, 서버가 중간 해시 상태 전체를 내보내지 않고도 ClientHello1의 해시만 쿠키에 저장해 무상태(stateless) HelloRetryRequest를 할 수 있게 하기 위해서다(4.3.2절 참고).

메시지 범위를 “…“로 지정하면, 트랜스크립트 해시는 주 핸드셰이크 동안 보내거나 받은 핸드셰이크 메시지 열 가운데 지정된 메시지에서 시작해 지정된 메시지에서 끝나는 부분으로 계산한다. 이 문서에서는 다음 핸드셰이크 메시지 열에서 계산한다(일부는 생략될 수도 있다). ClientHello, HelloRetryRequest, ClientHello, ServerHello, EncryptedExtensions, 서버 CertificateRequest, 서버 Certificate, 서버 CertificateVerify, 서버 Finished, EndOfEarlyData, 클라이언트 Certificate, 클라이언트 CertificateVerify, 클라이언트 Finished. TLS 확장은 메시지를 더하거나 뺄 수 있으며, 그 경우 트랜스크립트 해시는 바뀐 메시지 열을 반영한다.

일반적으로 구현은 ServerHello에서 해시가 선택되고 나면, 협상된 해시를 바탕으로 누적 트랜스크립트 해시 값을 유지하는 방식으로 트랜스크립트를 구현할 수 있다.

다만 이후의 핸드셰이크 후 인증들은 서로를 포함하지 않고, 주 핸드셰이크가 끝날 때까지의 메시지만 포함한다는 점에 유의하라.

4.2. 키 교환 메시지

키 교환 메시지는 클라이언트와 서버가 지원하는 보안 기능을 파악하고, 핸드셰이크의 나머지 부분과 데이터를 보호하는 데 쓰는 트래픽 키를 비롯한 공유 비밀값을 수립하는 데 쓰인다.

4.2.1. 암호 협상

TLS에서 암호 협상은 클라이언트가 ClientHello에 다음 네 가지 옵션 묶음을 제시하는 것으로 진행된다.

서버가 PSK를 선택하지 않으면, 이 옵션 가운데 앞의 세 가지는 서로 완전히 독립적이다. 서버는 암호 스위트, 키 수립에 쓸 비대칭 키 교환 그룹과 키 공유값, 클라이언트에게 자신을 인증할 서명 알고리즘/인증서 쌍을 각각 독립적으로 선택한다. 받은 “supported_groups”와 서버가 지원하는 그룹 사이에 겹치는 것이 없으면, 서버는 반드시 “handshake_failure” 또는 “insufficient_security” 경고로 핸드셰이크를 중단해야 한다(MUST).

서버가 PSK를 선택하면, 서버는 클라이언트의 “psk_key_exchange_modes” 확장이 나타낸 목록에서(현재는 PSK 단독, 또는 PSK와 비대칭 키 교환의 결합) 키 수립 모드도 반드시 선택해야 한다(MUST). PSK를 비대칭 키 교환 없이 쓸 수 있다면, “supported_groups” 매개 변수가 겹치지 않는 것이 치명적일 필요는 없다는 점에 유의하라. 앞 문단에서 다룬 PSK가 아닌 경우에는 치명적이다.

서버가 비대칭 키 교환 그룹을 선택했는데 클라이언트가 처음 ClientHello에서 호환되는 “key_share” 확장을 제시하지 않았다면, 서버는 반드시 HelloRetryRequest(4.2.4절) 메시지로 응답해야 한다(MUST).

서버가 매개 변수를 성공적으로 선택했고 HelloRetryRequest가 필요 없다면, 서버는 선택한 매개 변수를 ServerHello에 다음과 같이 나타낸다.

서버가 지원하는 매개 변수 묶음을 협상할 수 없으면(즉, 클라이언트와 서버의 매개 변수 사이에 겹치는 것이 없으면), 서버는 반드시 “handshake_failure” 또는 “insufficient_security” 치명적(fatal) 경고로 핸드셰이크를 중단해야 한다(MUST). 6장을 참고한다.

4.2.2. Client Hello

클라이언트가 서버에 처음 연결할 때는 첫 TLS 메시지로 반드시 ClientHello를 보내야 한다(REQUIRED). 서버가 클라이언트의 ClientHello에 HelloRetryRequest로 응답한 경우에도 클라이언트는 ClientHello를 보낸다. 이 경우 클라이언트는 다음을 제외하고는 반드시 같은 ClientHello를 수정 없이 보내야 한다(MUST).

TLS 1.3은 재협상을 금지하므로, TLS 1.3을 협상한 서버가 그 밖의 시점에 ClientHello를 받으면 반드시 “unexpected_message” 경고로 연결을 종료해야 한다(MUST).

이전 버전의 TLS로 TLS 연결을 수립한 서버가 재협상에서 TLS 1.3 ClientHello를 받으면, 서버는 반드시 이전 프로토콜 버전을 유지해야 한다(MUST). 특히 TLS 1.3을 협상해서는 안 된다(MUST NOT).

이 메시지의 구조는 다음과 같다.

      uint16 ProtocolVersion;
      opaque Random[32];

      uint8 CipherSuite[2];    /* 암호 스위트 선택자 */

      struct {
          ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
          Random random;
          opaque legacy_session_id<0..32>;
          CipherSuite cipher_suites<2..2^16-2>;
          opaque legacy_compression_methods<1..2^8-1>;
          Extension extensions<7..2^16-1>;
      } ClientHello;

모든 버전의 TLS에서는 compression_methods 필드 뒤에 확장 필드가 선택적으로 올 수 있다. TLS 1.3 ClientHello 메시지에는 항상 확장이 들어 있다(최소한 “supported_versions”가 있어야 하며, 없으면 TLS 1.2 ClientHello 메시지로 해석된다). 그러나 TLS 1.3 서버는 이전 버전 TLS에서 온, 확장 필드가 없는 ClientHello 메시지를 받을 수도 있다. 확장이 있는지는 ClientHello 끝의 compression_methods 필드 뒤에 바이트가 있는지로 알아낼 수 있다. 선택적 데이터를 알아내는 이 방법은 가변 길이 필드를 두는 일반적인 TLS 방법과 다르지만, 확장이 정의되기 전의 TLS와 호환되기 위해 쓴다는 점에 유의하라. TLS 1.3 서버는 이 검사를 먼저 수행하고, “supported_versions” 확장이 있을 때만 TLS 1.3 협상을 시도할 필요가 있다. TLS 1.3 이전 버전을 협상한다면, 서버는 메시지에 legacy_compression_methods 뒤의 데이터가 없는지, 또는 유효한 확장 블록이 있고 그 뒤에 데이터가 없는지 반드시 확인해야 한다(MUST). 그렇지 않으면 서버는 반드시 “decode_error” 경고로 핸드셰이크를 중단해야 한다(MUST).

클라이언트가 확장으로 추가 기능을 요청했는데 서버가 그 기능을 제공하지 않으면, 클라이언트는 핸드셰이크를 중단할 수 있다(MAY).

ClientHello 메시지를 보낸 뒤 클라이언트는 ServerHello 또는 HelloRetryRequest 메시지를 기다린다. 조기 데이터를 사용 중이면, 클라이언트는 다음 핸드셰이크 메시지를 기다리는 동안 조기 애플리케이션 데이터(2.3절)를 전송할 수 있다.

4.2.3. Server Hello

서버는 ClientHello를 바탕으로 받아들일 수 있는 핸드셰이크 매개 변수 묶음을 협상할 수 있으면, 핸드셰이크를 진행하기 위해 ClientHello 메시지에 대한 응답으로 이 메시지를 보낸다.

이 메시지의 구조는 다음과 같다.

      struct {
          ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
          Random random;
          opaque legacy_session_id_echo<0..32>;
          CipherSuite cipher_suite;
          uint8 legacy_compression_method = 0;
          Extension extensions<6..2^16-1>;
      } ServerHello;

미들박스와의 하위 호환성 때문에(부록 E.4 참고), HelloRetryRequest 메시지는 ServerHello와 같은 구조를 쓰되 Random을 “HelloRetryRequest”의 SHA-256이라는 특수한 값으로 설정한다.

     CF 21 AD 74 E5 9A 61 11 BE 1D 8C 02 1E 65 B8 91
     C2 A2 11 16 7A BB 8C 5E 07 9E 09 E2 C8 A8 33 9C

server_hello 유형의 메시지를 받으면, 구현은 반드시 먼저 Random 값을 살펴보고, 이 값과 일치하면 4.2.4절에 설명된 대로 처리해야 한다(MUST).

TLS 1.3에는 서버의 무작위 값에 심어 둔 다운그레이드 방지 메커니즘이 있다. ClientHello에 대한 응답으로 TLS 1.2 이하를 협상하는 TLS 1.3 서버는 반드시 ServerHello에서 Random 값의 마지막 8바이트를 특별하게 설정해야 한다(MUST).

TLS 1.2를 협상한다면, TLS 1.3 서버는 반드시 Random 값의 마지막 8바이트를 다음 바이트로 설정해야 한다(MUST).

     44 4F 57 4E 47 52 44 01

[RFC8996]과 부록 E.5는 1.2보다 낮은 TLS 버전의 협상을 금지한다. 그러나 이 지침을 따르지 않는 서버 구현은 반드시 ServerHello.random 값의 마지막 8바이트를 다음 바이트로 설정해야 한다(MUST).

     44 4F 57 4E 47 52 44 00

TLS 1.2 이하를 나타내는 ServerHello를 받은 TLS 1.3 클라이언트는 반드시 마지막 8바이트가 이 두 값 중 어느 것과도 같지 않은지 확인해야 한다(MUST). TLS 1.2 클라이언트도 ServerHello가 TLS 1.1 이하를 나타내면 마지막 8바이트가 두 번째 값과 같지 않은지 확인해야 한다(SHOULD). 일치하는 값이 있으면 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST). 이 메커니즘은 Finished 교환이 제공하는 것 이상으로 다운그레이드 공격을 제한적으로 막아 준다. TLS 1.2 이하에 있는 메시지인 ServerKeyExchange에는 두 무작위 값 모두에 대한 서명이 들어 있으므로, 임시 암호를 사용하는 한 능동 공격자가 탐지되지 않은 채로 무작위 값을 수정할 수 없기 때문이다. 정적 RSA를 사용할 때는 다운그레이드 방지를 제공하지 않는다.

참고: 이는 [RFC5246]에서 바뀐 것이므로, 실제로는 많은 TLS 1.2 클라이언트와 서버가 위에 규정한 대로 동작하지 않을 것이다.

TLS 1.2 이하로 재협상을 하던 레거시 TLS 클라이언트가 재협상 중에 TLS 1.3 ServerHello를 받으면, 반드시 “protocol_version” 경고로 핸드셰이크를 중단해야 한다(MUST). TLS 1.3을 협상한 뒤에는 재협상을 할 수 없다는 점에 유의하라.

4.2.4. Hello Retry Request

서버는 받아들일 수 있는 매개 변수 묶음을 찾을 수 있지만 ClientHello에 핸드셰이크를 진행하기에 충분한 정보가 없으면, ClientHello 메시지에 대한 응답으로 이 메시지를 보낸다. 4.2.3절에서 논의했듯이 HelloRetryRequest는 ServerHello 메시지와 형식이 같으며, legacy_version, legacy_session_id_echo, cipher_suite, legacy_compression_method 필드의 의미도 같다. 그러나 편의상 이 문서 전체에서는 “HelloRetryRequest”를 별개의 메시지인 것처럼 다룬다.

서버의 확장에는 반드시 “supported_versions”가 들어 있어야 한다(MUST). 또한 서버의 확장에는 클라이언트가 올바른 ClientHello 쌍을 생성하는 데 필요한 최소한의 확장 묶음이 들어 있어야 한다(SHOULD). HelloRetryRequest에는 클라이언트가 자신의 ClientHello에서 먼저 제시하지 않은 확장을 넣어서는 안 된다(MUST NOT). 단, “cookie”(4.3.2절 참고) 확장은 선택적으로 넣을 수 있는 예외다.

HelloRetryRequest를 받으면, 클라이언트는 반드시 4.2.3절에 규정된 대로 legacy_version, legacy_session_id_echo, cipher_suite, legacy_compression_method를 확인한 다음, “supported_versions”로 버전을 정하는 것부터 시작해 확장을 처리해야 한다(MUST). HelloRetryRequest가 ClientHello에 아무 변경도 일으키지 않는다면, 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST). 같은 연결에서 두 번째 HelloRetryRequest를 받으면(즉, ClientHello 자체가 HelloRetryRequest에 대한 응답이었던 경우), 클라이언트는 반드시 “unexpected_message” 경고로 핸드셰이크를 중단해야 한다(MUST).

그렇지 않으면 클라이언트는 반드시 HelloRetryRequest의 모든 확장을 처리하고, 갱신한 두 번째 ClientHello를 보내야 한다(MUST). 이 명세에 정의된 HelloRetryRequest 확장은 다음과 같다.

자신이 제시하지 않은 암호 스위트를 받은 클라이언트는 반드시 핸드셰이크를 중단해야 한다(MUST). 서버는 규정을 따르는 갱신한 ClientHello를 받았을 때 반드시 같은 암호 스위트를 협상해야 한다(MUST). 서버가 협상의 첫 단계로 암호 스위트를 선택한다면 이는 자동으로 이루어진다. ServerHello를 받으면, 클라이언트는 반드시 ServerHello에 담긴 암호 스위트가 HelloRetryRequest의 것과 같은지 확인하고, 같지 않으면 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

또한 클라이언트는 갱신한 ClientHello에서, 선택된 암호 스위트의 해시가 아닌 다른 해시에 연관된 사전 공유 키를 제시하지 않아야 한다(SHOULD NOT). 이렇게 하면 클라이언트는 두 번째 ClientHello에서 여러 해시에 대해 부분 해시 트랜스크립트를 계산하지 않아도 된다.

HelloRetryRequest의 “supported_versions” 확장에 담긴 selected_version 값은 반드시 ServerHello에서도 유지되어야 하며(MUST), 이 값이 바뀌면 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

4.3. 확장

여러 TLS 메시지에는 태그-길이-값(tag-length-value)으로 인코딩된 확장 구조체가 들어 있다.

    struct {
        ExtensionType extension_type;
        opaque extension_data<0..2^16-1>;
    } Extension;

    enum {
        server_name(0),                             /* RFC 6066, 9261 */
        status_request(5),                          /* RFC 6066, 9846 */
        supported_groups(10),                       /* RFC 7919, 9846 */
        signature_algorithms(13),                   /* RFC 9846 */
        use_srtp(14),                               /* RFC 5764 */
        heartbeat(15),                              /* RFC 6520 */
        application_layer_protocol_negotiation(16), /* RFC 7301 */
        client_certificate_type(19),                /* RFC 7250 */
        server_certificate_type(20),                /* RFC 7250 */
        padding(21),                                /* RFC 7685 */
        compress_certificate(27),                   /* RFC 8879 */
        record_size_limit(28),                      /* RFC 8449 */
        delegated_credential(34),                   /* RFC 9345 */
        supported_ekt_ciphers(39),                  /* RFC 8870 */
        pre_shared_key(41),                         /* RFC 9846 */
        early_data(42),                             /* RFC 9846 */
        supported_versions(43),                     /* RFC 9846 */
        cookie(44),                                 /* RFC 9846 */
        psk_key_exchange_modes(45),                 /* RFC 9846 */
        certificate_authorities(47),                /* RFC 9846 */
        oid_filters(48),                            /* RFC 9846 */
        post_handshake_auth(49),                    /* RFC 9846 */
        signature_algorithms_cert(50),              /* RFC 9846 */
        key_share(51),                              /* RFC 9846 */
        transparency_info(52),                      /* RFC 9162 */
        external_id_hash(55),                       /* RFC 8844 */
        external_session_id(56),                    /* RFC 8844 */
        quic_transport_parameters(57),              /* RFC 9001 */
        ticket_request(58),                         /* RFC 9149 */
        ech_outer_extensions(64768),                /* RFC 9849 */
        encrypted_client_hello(65037),              /* RFC 9849 */
        (65535)
    } ExtensionType;

참고: 이 목록에는 이 문서를 쓰는 시점에 “Recommended”로 표시되고 TLS 1.3에서 허용된 확장만 들어 있다.

여기서 각 필드는 다음과 같다.

“extension_data” 필드의 내용은 보통 TLS 표현 언어로 정의한 확장별 구조체로 정의된다. 달리 규정하지 않는 한, 뒤에 붙는 데이터(trailing data)는 금지된다. 즉, 송신자는 “extension_data” 필드에서 구조체 뒤에 데이터를 넣어서는 안 된다(MUST NOT). 확장을 처리할 때, 구조체를 파싱한 뒤에 남는 데이터가 있으면 수신자는 반드시 “decode_error” 경고로 핸드셰이크를 중단해야 한다(MUST). 수신자가 그 확장을 구현하지 않았거나 무시하도록 구성된 경우에는 이 규칙이 적용되지 않는다.

확장 유형 목록은 11장에 설명된 대로 IANA가 관리한다.

확장은 일반적으로 요청/응답 방식으로 구성되지만, 대응하는 응답이 없는 요청(즉, 표시(indication))뿐인 확장도 있다. 클라이언트는 확장 요청을 ClientHello 메시지에 담아 보내고, 서버는 확장 응답을 ServerHello, EncryptedExtensions, HelloRetryRequest, Certificate 메시지에 담아 보낸다. 서버는 확장 요청을 CertificateRequest 메시지에 담아 보내며, 클라이언트는 이에 Certificate 메시지로 응답할 수 있다(MAY). 서버는 NewSessionTicket에 요청받지 않은 확장을 담아 보낼 수도 있지만(MAY), 클라이언트는 이 확장에 직접 응답하지 않는다.

원격 엔드포인트가 대응하는 확장 요청을 보내지 않았다면, 구현은 확장 응답을(즉, ServerHello, EncryptedExtensions, HelloRetryRequest, Certificate 메시지에) 보내서는 안 된다(MUST NOT). 단, HelloRetryRequest의 “cookie” 확장은 예외다. 그런 확장을 받으면 엔드포인트는 반드시 “unsupported_extension” 경고로 핸드셰이크를 중단해야 한다(MUST).

아래 표는 각 확장이 나타날 수 있는 메시지를 보여 준다. 표기는 CH(ClientHello), SH(ServerHello), EE(EncryptedExtensions), CT(Certificate), CR(CertificateRequest), NST(NewSessionTicket), HRR(HelloRetryRequest)이다. 구현이 인식하는 확장을, 그 확장이 규정되지 않은 메시지에서 받으면, 구현은 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

확장 TLS 1.3
server_name [RFC6066] [RFC9261] CH, EE, CR
status_request [RFC6066] [RFC9846] CH, CR, CT
supported_groups [RFC7919] [RFC9846] CH, EE
signature_algorithms [RFC9846] CH, CR
use_srtp [RFC5764] CH, EE
heartbeat [RFC6520] CH, EE
application_layer_protocol_negotiation [RFC7301] CH, EE
client_certificate_type [RFC7250] CH, EE
server_certificate_type [RFC7250] CH, EE
padding [RFC7685] CH
compress_certificate [RFC8879] CH, CR
record_size_limit [RFC8449] CH, EE
delegated_credential [RFC9345] CH, CR, CT
supported_ekt_ciphers [RFC8870] CH, EE
pre_shared_key [RFC9846] CH, SH
early_data [RFC9846] CH, EE, NST
supported_versions [RFC9846] CH, SH, HRR
cookie [RFC9846] CH, HRR
psk_key_exchange_modes [RFC9846] CH
certificate_authorities [RFC9846] CH, CR
oid_filters [RFC9846] CR
post_handshake_auth [RFC9846] CH
signature_algorithms_cert [RFC9846] CH, CR
key_share [RFC9846] CH, SH, HRR
transparency_info [RFC9162] CH, CR, CT
external_id_hash [RFC8844] CH, EE
external_session_id [RFC8844] CH, EE
quic_transport_parameters [RFC9001] CH, EE
ticket_request [RFC9149] CH, EE
ech_outer_extensions [RFC9849] CH
encrypted_client_hello [RFC9849] CH, HRR, EE

표 1: TLS 확장

참고: 이 표에는 이 문서를 쓰는 시점에 “Recommended”로 표시되고 TLS 1.3에서 허용된 확장만 들어 있다.

서로 다른 유형의 확장이 여러 개 있으면, 확장은 어떤 순서로든 나타날 수 있다(MAY). 단, “pre_shared_key”(4.3.11절)는 예외로, 반드시 ClientHello의 마지막 확장이어야 한다(MUST)(ServerHello 확장 블록에서는 어디에 나타나도 된다). 한 확장 블록에 같은 유형의 확장이 둘 이상 있어서는 안 된다(MUST NOT).

TLS 1.3에서는 TLS 1.2와 달리, 재개-PSK 모드에서도 핸드셰이크마다 확장을 협상한다. 그러나 0-RTT 매개 변수는 이전 핸드셰이크에서 협상한 것이다. 따라서 서로 맞지 않으면 0-RTT를 거절해야 할 수도 있다(4.3.10절 참고).

이 프로토콜에서는 새 기능과 기존 기능 사이에 미묘한(그리고 그다지 미묘하지 않은) 상호 작용이 일어나 전체 보안이 크게 떨어질 수 있다. 새 확장을 설계할 때는 다음 사항을 고려해야 한다.

4.3.1. Supported Versions

      struct {
          select (Handshake.msg_type) {
              case client_hello:
                   ProtocolVersion versions<2..254>;

              case server_hello: /* HelloRetryRequest도 같음 */
                   ProtocolVersion selected_version;
          };
      } SupportedVersions;

“supported_versions” 확장은 클라이언트가 자신이 지원하는 TLS 버전을 나타내고, 서버가 자신이 사용하는 버전을 나타내는 데 쓰인다. 이 확장에는 지원하는 버전의 목록이 선호 순서대로, 가장 선호하는 버전부터 들어 있다. 이 명세의 구현은 반드시 협상할 준비가 된 모든 TLS 버전을 담아 이 확장을 ClientHello에 넣어 보내야 한다(MUST)(이 명세에서는 최소한 0x0304를 뜻하지만, 이전 TLS 버전의 협상을 허용한다면 그 버전들도 반드시 들어 있어야 한다(MUST)).

이 확장이 없으면, 이 명세를 준수하면서 TLS 1.2도 지원하는 서버는 ClientHello.legacy_version이 0x0304 이상이더라도 반드시 [RFC5246]에 규정된 대로 TLS 1.2 이하를 협상해야 한다(MUST). 서버는 legacy_version이 0x0304 이상인 ClientHello를 받으면 핸드셰이크를 중단할 수 있다(MAY).

ClientHello에 이 확장이 있으면, 서버는 버전 협상에 ClientHello.legacy_version 값을 사용해서는 안 되며(MUST NOT), 클라이언트의 선호를 정하는 데 반드시 “supported_versions” 확장만 사용해야 한다(MUST). 서버는 반드시 그 확장에 들어 있는 TLS 버전만 선택해야 하며(MUST), 그 확장에 들어 있는 알 수 없는 버전은 반드시 무시해야 한다(MUST). 한쪽이 띄엄띄엄한 범위를 지원하면 이 메커니즘으로 TLS 1.2보다 이전 버전을 협상할 수 있게 된다는 점에 유의하라. 이전 TLS 버전을 지원하기로 한 TLS 1.3 구현은 TLS 1.2를 지원해야 한다(SHOULD). 서버는 이 확장은 들어 있지만 버전 목록에 0x0304가 없는 ClientHello를 받을 준비가 반드시 되어 있어야 한다(MUST).

TLS 1.3보다 이전 버전의 TLS를 협상하는 서버는 반드시 ServerHello.version을 설정해야 하며(MUST), “supported_versions” 확장을 보내서는 안 된다(MUST NOT). TLS 1.3을 협상하는 서버는 반드시 선택한 버전 값(0x0304)을 담은 “supported_versions” 확장을 보내 응답해야 한다(MUST). 이 서버는 반드시 ServerHello.legacy_version 필드를 0x0303(TLS 1.2)으로 설정해야 한다(MUST).

클라이언트는 ServerHello.random을 확인해 서버 핸드셰이크 메시지가 ServerHello인지 HelloRetryRequest인지 판단한 다음, ServerHello의 나머지를 처리하기 전에 반드시 이 확장이 있는지 확인해야 한다(MUST). 이를 위해 클라이언트는 확장을 읽으려고 ServerHello를 파싱해야 한다. 이 확장이 있으면, 클라이언트는 반드시 ServerHello.legacy_version 값을 무시해야 하며(MUST), 선택된 버전을 정하는 데 반드시 “supported_versions” 확장만 사용해야 한다(MUST). ServerHello의 “supported_versions” 확장에 클라이언트가 제시하지 않은 버전이나 TLS 1.3보다 이전 버전이 들어 있으면, 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

      struct {
          opaque cookie<1..2^16-1>;
      } Cookie;

쿠키에는 두 가지 주된 목적이 있다.

HelloRetryRequest를 보낼 때 서버는 클라이언트에게 “cookie” 확장을 제공할 수 있다(MAY)(이는 ClientHello에 나타난 확장만 보낼 수 있다는 일반 규칙의 예외다). 새 ClientHello를 보낼 때 클라이언트는 반드시 HelloRetryRequest에서 받은 확장의 내용을 새 ClientHello의 “cookie” 확장에 복사해야 한다(MUST). 클라이언트는 이후 연결의 처음 ClientHello에 쿠키를 사용해서는 안 된다(MUST NOT).

서버가 무상태로(statelessly) 동작할 때는 첫 번째와 두 번째 ClientHello 사이에 보호되지 않은 change_cipher_spec 유형의 레코드를 받을 수도 있다(5장 참고). 서버는 아무 상태도 저장하지 않으므로, 이 레코드는 처음 받는 메시지인 것처럼 보인다. 무상태로 동작하는 서버는 반드시 이런 레코드를 무시해야 한다(MUST).

4.3.3. Signature Algorithms

TLS 1.3은 디지털 서명에 쓸 수 있는 서명 알고리즘을 나타내는 확장 두 가지를 제공한다. “signature_algorithms_cert” 확장은 인증서 안의 서명에 적용되고, 원래 TLS 1.2에서 등장한 “signature_algorithms” 확장은 CertificateVerify 메시지의 서명에 적용된다. 인증서에 담긴 키도 반드시 함께 쓰는 서명 알고리즘에 맞는 유형이어야 한다(MUST). 이는 아래에 설명하듯 RSA 키와 PSS 서명에서 특히 문제가 된다. “signature_algorithms_cert” 확장이 없으면 “signature_algorithms” 확장이 인증서에 나타나는 서명에도 적용된다. 서버가 인증서로 자신을 인증하기를 원하는 클라이언트는 반드시 “signature_algorithms” 확장을 보내야 한다(MUST). 서버가 인증서로 인증하는데 클라이언트가 “signature_algorithms” 확장을 보내지 않았다면, 서버는 반드시 “missing_extension” 경고로 핸드셰이크를 중단해야 한다(MUST)(9.2절 참고).

“signature_algorithms_cert” 확장은 인증서용과 TLS 자체용으로 서로 다른 알고리즘 묶음을 지원하는 구현이 자신의 기능을 명확히 알릴 수 있도록 추가되었다. TLS 1.2 구현도 이 확장을 처리해야 한다(SHOULD). 두 경우에 같은 정책을 쓰는 구현은 “signature_algorithms_cert” 확장을 생략할 수 있다(MAY).

이 확장들의 “extension_data” 필드에는 SignatureSchemeList 값이 들어 있다.

      enum {
          /* RSASSA-PKCS1-v1_5 알고리즘 */
          rsa_pkcs1_sha256(0x0401),
          rsa_pkcs1_sha384(0x0501),
          rsa_pkcs1_sha512(0x0601),

          /* ECDSA 알고리즘 */
          ecdsa_secp256r1_sha256(0x0403),
          ecdsa_secp384r1_sha384(0x0503),
          ecdsa_secp521r1_sha512(0x0603),

          /* 공개 키 OID가 rsaEncryption인 RSASSA-PSS 알고리즘 */
          rsa_pss_rsae_sha256(0x0804),
          rsa_pss_rsae_sha384(0x0805),
          rsa_pss_rsae_sha512(0x0806),

          /* EdDSA 알고리즘 */
          ed25519(0x0807),
          ed448(0x0808),

          /* 공개 키 OID가 RSASSA-PSS인 RSASSA-PSS 알고리즘 */
          rsa_pss_pss_sha256(0x0809),
          rsa_pss_pss_sha384(0x080a),
          rsa_pss_pss_sha512(0x080b),

          /* 레거시 알고리즘 */
          rsa_pkcs1_sha1(0x0201),
          ecdsa_sha1(0x0203),

          /* 예약된 코드 포인트 */
          private_use(0xFE00..0xFFFF),
          (0xFFFF)
      } SignatureScheme;

      struct {
          SignatureScheme supported_signature_algorithms<2..2^16-2>;
      } SignatureSchemeList;

참고: TLS 1.2에 이미 SignatureAlgorithm 타입이 있고 이 열거형이 그것을 대체하므로, 이 열거형의 이름은 “SignatureScheme”이다. 본문에서는 줄곧 “서명 알고리즘(signature algorithm)”이라는 용어를 쓴다.

각 SignatureScheme 값은 클라이언트가 검증할 의향이 있는 서명 알고리즘 하나를 나타낸다. 값은 선호도의 내림차순으로 나열한다. 서명 알고리즘은 다이제스트가 아니라 임의 길이의 메시지를 입력으로 받는다는 점에 유의하라. 전통적으로 다이제스트에 대해 동작하는 알고리즘은 TLS에서 먼저 지정된 해시 알고리즘으로 입력을 해시한 다음 평소대로 진행하도록 정의해야 한다. 위에 나열한 코드 포인트 그룹의 의미는 다음과 같다.

자체 서명된 인증서나 신뢰 앵커(trust anchor)인 인증서는 인증 경로(certification path)를 시작하므로, 그 서명은 검증하지 않는다([RFC5280]의 3.2절 참고). 인증 경로를 시작하는 인증서는 “signature_algorithms”와 “signature_algorithms_cert” 확장에서 지원한다고 알리지 않은 서명 알고리즘을 사용할 수 있다(MAY).

TLS 1.2는 이 확장을 다르게 정의한다는 점에 유의하라. TLS 1.2를 협상할 의향이 있는 TLS 1.3 구현은 그 버전을 협상할 때 반드시 [RFC5246]의 요구 사항에 따라 동작해야 한다(MUST). 특히 다음과 같다.

4.3.4. Certificate Authorities

“certificate_authorities” 확장은 엔드포인트가 지원하는 인증 기관(certificate authority, CA)을 나타내는 데 쓰이며, 수신하는 엔드포인트는 이를 인증서 선택의 지침으로 사용해야 한다(SHOULD).

“certificate_authorities” 확장의 본문은 CertificateAuthoritiesExtension 구조체로 이루어진다.

      opaque DistinguishedName<1..2^16-1>;

      struct {
          DistinguishedName authorities<3..2^16-1>;
      } CertificateAuthoritiesExtension;

클라이언트는 “certificate_authorities” 확장을 ClientHello 메시지에 담아 보낼 수 있다(MAY). 서버는 이 확장을 CertificateRequest 메시지에 담아 보낼 수 있다(MAY).

비슷한 목적을 가졌지만 더 복잡한 “trusted_ca_keys” 확장 [RFC6066]은 TLS 1.3에서 쓰이지 않는다(다만 이전 TLS 버전을 제시하는 클라이언트의 ClientHello 메시지에는 나타날 수 있다).

4.3.5. OID Filters

“oid_filters” 확장은 서버가 클라이언트의 인증서와 일치하기를 바라는 OID/값 쌍의 목록을 제공할 수 있게 한다. 서버가 이 확장을 제공한다면, 반드시 CertificateRequest 메시지에만 담아 보내야 한다(MUST).

      struct {
          opaque certificate_extension_oid<1..2^8-1>;
          opaque certificate_extension_values<0..2^16-1>;
      } OIDFilter;

      struct {
          OIDFilter filters<0..2^16-1>;
      } OIDFilterExtension;

PKIX RFC들은 다양한 인증서 확장 OID와 그에 대응하는 값 유형을 정의한다. 유형에 따라, 일치하는 인증서 확장 값이 꼭 비트 단위로 같은 것은 아니다. TLS 구현은 인증서 확장 OID를 사용한 인증서 선택을 자신의 PKI 라이브러리에 맡길 것으로 예상된다.

이 문서는 [RFC5280]에 정의된 표준 인증서 확장 두 가지에 대한 일치 규칙을 정의한다.

다른 인증서 확장에 대한 일치 규칙은 별도의 명세에서 정의할 수 있다.

4.3.6. 핸드셰이크 후 인증서 기반 클라이언트 인증

“post_handshake_auth” 확장은 클라이언트가 핸드셰이크 후 인증(4.7.2절)을 할 의향이 있음을 나타내는 데 쓰인다. 서버는 이 확장을 제시하지 않는 클라이언트에게 핸드셰이크 후 CertificateRequest를 보내서는 안 된다(MUST NOT). 서버는 이 확장을 보내서는 안 된다(MUST NOT).

      struct {} PostHandshakeAuth;

“post_handshake_auth” 확장의 “extension_data” 필드는 길이가 0이다.

4.3.7. Supported Groups

클라이언트가 보낼 때 “supported_groups” 확장은 클라이언트가 키 교환에 지원하는 이름 붙은 그룹(named group)을 가장 선호하는 것부터 가장 덜 선호하는 것 순으로 나타낸다.

참고: TLS 1.3 이전 버전의 TLS에서 이 확장의 이름은 “elliptic_curves”였고 타원 곡선 그룹만 담았다. [RFC8422]와 [RFC7919]를 참고한다. 이 확장은 ECDSA 곡선을 협상하는 데에도 쓰였다. 이제 서명 알고리즘은 따로 협상한다(4.3.3절 참고).

이 확장의 “extension_data” 필드에는 “NamedGroupList” 값이 들어 있다.

      enum {

          /* 타원 곡선 그룹(ECDHE) */
          secp256r1(0x0017), secp384r1(0x0018), secp521r1(0x0019),
          x25519(0x001D), x448(0x001E),

          /* 유한체 그룹(DHE) */
          ffdhe2048(0x0100), ffdhe3072(0x0101), ffdhe4096(0x0102),
          ffdhe6144(0x0103), ffdhe8192(0x0104),

          /* 예약된 코드 포인트 */
          ffdhe_private_use(0x01FC..0x01FF),
          ecdhe_private_use(0xFE00..0xFEFF),
          (0xFFFF)
      } NamedGroup;

      struct {
          NamedGroup named_group_list<2..2^16-1>;
      } NamedGroupList;

“named_group_list”의 항목은 송신자의 선호에 따라(가장 선호하는 것부터) 나열한다. “named_group_list”에는 중복 항목이 들어 있어서는 안 된다(MUST NOT). 수신자는 중복 항목을 발견하면 치명적 “illegal_parameter” 경고로 연결을 중단할 수 있다(MAY).

TLS 1.3부터는 서버가 “supported_groups” 확장을 클라이언트에게 보내는 것이 허용된다. 클라이언트는 핸드셰이크가 성공적으로 끝나기 전에는 “supported_groups”에서 찾은 정보에 따라 행동해서는 안 되지만(MUST NOT), 성공적으로 끝난 핸드셰이크에서 알게 된 정보를 사용해 이후 연결의 “key_share” 확장에서 쓰는 그룹을 바꿀 수 있다(MAY). 서버가 “key_share” 확장에 있는 그룹보다 선호하는 그룹이 있지만 그래도 ClientHello를 받아들일 의향이 있다면, 서버는 클라이언트가 아는 서버의 선호를 갱신하기 위해 “supported_groups”를 보내야 한다(SHOULD). 이 확장에는 현재 클라이언트가 지원하는지와 관계없이 서버가 지원하는 모든 그룹이 들어 있어야 한다(SHOULD).

4.3.8. Key Share

“key_share” 확장은 엔드포인트의 암호 매개 변수를 담는다.

클라이언트는 추가 왕복을 대가로 서버에 그룹 선택을 요청하기 위해 빈 client_shares 목록을 보낼 수 있다(MAY)(4.2.4절 참고).

      struct {
          NamedGroup group;
          opaque key_exchange<1..2^16-1>;
      } KeyShareEntry;

ClientHello 메시지에서 이 확장의 “extension_data” 필드에는 “KeyShareClientHello” 값이 들어 있다.

      struct {
          KeyShareEntry client_shares<0..2^16-1>;
      } KeyShareClientHello;

클라이언트가 HelloRetryRequest를 요청한다면 이 목록은 비어 있을 수 있다(MAY). 각 KeyShareEntry 값은 반드시 “supported_groups” 확장에서 제시한 그룹에 대응해야 하며(MUST), 반드시 같은 순서로 나타나야 한다(MUST). 그러나 이 값들은 “supported_groups” 확장의 연속되지 않은 부분 집합일 수 있고(MAY), 가장 선호하는 그룹을 생략할 수도 있다(MAY). 가장 선호하는 그룹이 새것이라 키 공유값을 미리 생성해 두는 것이 효율적일 만큼 널리 지원될 가능성이 낮으면 이런 상황이 생길 수 있다.

그러므로 클라이언트가 그룹 A의 공유값을 생략하고 그룹 B의 공유값을 넣었다고 해서 B를 A보다 선호한다는 뜻은 아니다. KeyShareEntry를 바탕으로 그룹을 선택하면 HelloRetryRequest의 왕복은 절약하지만, 클라이언트와 서버가 함께 지원하는 그룹 가운데 더 선호하는 것이 있는데도 그보다 덜 선호하는 그룹을 쓰게 될 수 있다. 클라이언트의 그룹 선호를 존중하려는 서버는 먼저 “supported_groups”를 바탕으로 그룹을 선택한 다음, KeyshareClientHello의 내용에 따라 ServerHello 또는 HelloRetryRequest를 보내야 한다(SHOULD).

역주: KeyshareClientHello는 원문 표기 그대로이며 KeyShareClientHello를 뜻한다.

클라이언트는 자신이 제시하는 지원 그룹의 수만큼 KeyShareEntry 값을 제시할 수 있으며, 각 값은 키 교환 매개 변수 묶음 하나를 나타낸다. 예를 들어 클라이언트는 여러 타원 곡선이나 여러 FFDHE 그룹에 대한 공유값을 제시할 수도 있다. 각 KeyShareEntry의 key_exchange 값은 반드시 독립적으로 생성해야 한다(MUST). 클라이언트는 같은 그룹에 대해 KeyShareEntry 값을 여러 개 제시해서는 안 된다(MUST NOT). 클라이언트는 자신의 “supported_groups” 확장에 나열하지 않은 그룹에 대한 KeyShareEntry 값을 제시해서는 안 된다(MUST NOT). 서버는 이 규칙의 위반을 확인하고, 위반이 있으면 “illegal_parameter” 경고로 핸드셰이크를 중단할 수 있다(MAY).

클라이언트와 서버는 키 공유값을 여러 연결에 재사용해서는 안 된다(MUST NOT). [RFC8446]은 재사용을 허용했으므로, 상호 운용성 문제를 막기 위해 수신하는 구현은 반드시 피어의 재사용을 허용해야 한다(MUST).

HelloRetryRequest 메시지에서 이 확장의 “extension_data” 필드에는 KeyShareHelloRetryRequest 값이 들어 있다.

      struct {
          NamedGroup selected_group;
      } KeyShareHelloRetryRequest;

HelloRetryRequest에서 이 확장을 받으면, 클라이언트는 반드시 (1) selected_group 필드가 원래 ClientHello의 “supported_groups” 확장에서 제공한 그룹에 해당하는지, (2) selected_group 필드가 원래 ClientHello의 “key_share” 확장에서 제공한 그룹에 해당하지 않는지 확인해야 한다(MUST). 이 확인 중 하나라도 실패하면 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST). 그렇지 않으면 새 ClientHello를 보낼 때, 클라이언트는 반드시 원래 “key_share” 확장을, 계기가 된 HelloRetryRequest의 selected_group 필드가 가리키는 그룹에 대한 새 KeyShareEntry 하나만 담은 확장으로 바꿔야 한다(MUST).

ServerHello 메시지에서 이 확장의 “extension_data” 필드에는 KeyShareServerHello 값이 들어 있다.

      struct {
          KeyShareEntry server_share;
      } KeyShareServerHello;

비대칭 키 수립을 사용하면, 서버는 ServerHello에서 KeyShareEntry를 정확히 하나 제시한다. 이 값은 반드시 서버가 협상된 키 교환을 위해 선택한, 클라이언트가 제시한 KeyShareEntry 값과 같은 그룹이어야 한다(MUST). 서버는 클라이언트의 “supported_groups” 확장에 표시되지 않은 그룹에 대한 KeyShareEntry를 보내서는 안 되며(MUST NOT), “psk_ke” PskKeyExchangeMode를 사용할 때는 KeyShareEntry를 보내서는 안 된다(MUST NOT). 비대칭 키 수립을 사용하고 클라이언트가 “key_share” 확장을 담은 HelloRetryRequest를 받았다면, 클라이언트는 반드시 ServerHello에서 선택된 NamedGroup이 HelloRetryRequest의 것과 같은지 확인해야 한다(MUST). 이 확인에 실패하면 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

4.3.8.1. Diffie-Hellman 매개 변수

클라이언트와 서버의 Diffie-Hellman [DH76] 매개 변수는 모두 KeyShare 구조체 안 KeyShareEntry의 opaque key_exchange 필드에 인코딩된다. 이 opaque 값에는 지정된 그룹(그룹 정의는 [RFC7919] 참고)에 대한 Diffie-Hellman 공개 값(Y = g^X mod p)이 빅 엔디언 정수로 인코딩되어 들어 있으며, 왼쪽을 0으로 채워 p의 바이트 크기에 맞춘다.

참고: 주어진 Diffie-Hellman 그룹에서는 이 패딩 때문에 모든 공개 키의 길이가 같아진다.

피어는 반드시 1 < Y < p-1인지 확인해 서로의 공개 키 Y를 검증해야 한다(MUST). 이 확인으로 원격 피어가 올바르게 동작하며 로컬 시스템을 작은 부분군(subgroup)으로 몰아넣고 있지 않음을 보장한다.

4.3.8.2. ECDHE 매개 변수

클라이언트와 서버의 ECDHE 매개 변수는 모두 KeyShare 구조체 안 KeyShareEntry의 opaque key_exchange 필드에 인코딩된다.

secp256r1, secp384r1, secp521r1에서 내용은 다음 구조체를 직렬화한 값이다.

      struct {
          uint8 legacy_form = 4;
          opaque X[coordinate_length];
          opaque Y[coordinate_length];
      } UncompressedPointRepresentation;

X와 Y는 각각 x 값과 y 값을 네트워크 바이트 순서로 나타낸 이진 표현이다. 내부 길이 표지가 없으므로, 각 수의 표현은 곡선 매개 변수가 정하는 만큼의 옥텟을 차지한다. P-256에서는 X와 Y가 각각 32옥텟을 쓰며, 필요하면 왼쪽을 0으로 채운다. P-384에서는 각각 48옥텟, P-521에서는 각각 66옥텟을 차지한다.

secp256r1, secp384r1, secp521r1 곡선에서 피어는 반드시 서로의 공개 값 Q가 타원 곡선 위의 유효한 점인지 확인해 검증해야 한다(MUST). 적절한 검증 절차는 [ECDP]의 부록 D.1에 정의되어 있고, [KEYAGREEMENT]의 5.6.2.3절에도 대안으로 정의되어 있다. 이 과정은 세 단계로 이루어진다. (1) Q가 무한원점(point at infinity, O)이 아닌지 확인하고, (2) Q = (x, y)에서 두 정수 x와 y가 올바른 구간에 있는지 확인하고, (3) (x, y)가 타원 곡선 방정식의 올바른 해인지 확인한다. 이 곡선들에서는 구현자가 올바른 부분군에 속하는지 확인할 필요가 없다.

X25519와 X448에서 공개 값의 내용은 [RFC7748]의 6절에 설명된 K_A 또는 K_B 값이다. 이 값은 X25519에서 32바이트, X448에서 56바이트다.

참고: TLS 1.3 이전 버전의 TLS는 점 형식 협상을 허용했다. TLS 1.3은 이 기능을 없애고 곡선마다 단일 점 형식을 쓴다.

4.3.9. Pre-Shared Key Exchange Modes

PSK를 사용하려면 클라이언트는 반드시 “psk_key_exchange_modes” 확장도 보내야 한다(MUST). 이 확장의 의미는 클라이언트가 이 모드들로만 PSK 사용을 지원한다는 것이며, 이는 이 ClientHello에서 제시하는 PSK와 서버가 NewSessionTicket으로 제공할 수도 있는 PSK의 사용을 모두 제한한다.

클라이언트가 “pre_shared_key” 확장을 제시한다면 반드시 “psk_key_exchange_modes” 확장을 제공해야 한다(MUST). 클라이언트가 “psk_key_exchange_modes” 확장 없이 “pre_shared_key”를 제시하면, 서버는 반드시 핸드셰이크를 중단해야 한다(MUST). 서버는 클라이언트가 나열하지 않은 키 교환 모드를 선택해서는 안 된다(MUST NOT). 이 확장은 PSK 재개에 쓰는 모드도 제한한다. 서버는 알린 모드와 호환되지 않는 티켓을 담은 NewSessionTicket을 보내지 않아야 한다(SHOULD NOT). 그러나 서버가 그렇게 하더라도, 그 영향은 클라이언트의 재개 시도가 실패하는 것뿐이다.

서버는 “psk_key_exchange_modes” 확장을 보내서는 안 된다(MUST NOT).

      enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;

      struct {
          PskKeyExchangeMode ke_modes<1..255>;
      } PskKeyExchangeModes;

앞으로 할당되는 값은 전송되는 프로토콜 메시지만으로 서버가 어느 모드를 선택했는지 모호함 없이 식별할 수 있도록 보장해야 한다. 현재는 ServerHello에 “key_share”가 있는지로 이를 나타낸다.

4.3.10. 조기 데이터 표시

PSK를 사용하고 그 PSK에 대해 조기 데이터가 허용되면(예를 들어 부록 B.3.4 참고), 클라이언트는 메시지의 첫 번째 플라이트에 애플리케이션 데이터를 보낼 수 있다. 클라이언트가 그렇게 하기로 한다면 반드시 “pre_shared_key”와 “early_data” 확장을 모두 제공해야 한다(MUST).

이 확장의 “extension_data” 필드에는 “EarlyDataIndication” 값이 들어 있다.

      struct {} Empty;

      struct {
          select (Handshake.msg_type) {
              case new_session_ticket:   uint32 max_early_data_size;
              case client_hello:         Empty;
              case encrypted_extensions: Empty;
          };
      } EarlyDataIndication;

max_early_data_size 필드의 사용에 관한 자세한 내용은 4.7.1절을 참고한다.

0-RTT 데이터의 매개 변수(버전, 대칭 암호 스위트, ALPN(Application-Layer Protocol Negotiation) [RFC7301] 프로토콜 등)는 사용 중인 PSK에 연관된 것이다. 외부에서 프로비저닝한 PSK의 경우, 연관된 값은 키와 함께 프로비저닝한 값이다. NewSessionTicket 메시지로 수립한 PSK의 경우, 연관된 값은 그 PSK를 수립한 연결에서 협상한 값이다. 조기 데이터를 암호화하는 데 쓰는 PSK는 반드시 클라이언트의 “pre_shared_key” 확장에 처음 나열된 PSK여야 한다(MUST).

NewSessionTicket으로 프로비저닝한 PSK의 경우, 서버는 선택한 PSK 식별자의 티켓 나이(PskIdentity.obfuscated_ticket_age에서 ticket_age_add를 뺀 값을 2^32로 나눈 나머지)가 티켓 발급 뒤 지난 시간과 작은 허용 오차 안에서 같은지 반드시 검증해야 한다(MUST)(8장 참고). 티켓 나이가 허용 오차를 벗어나면, 서버는 핸드셰이크는 진행하되 0-RTT는 거절해야 하며(SHOULD), 이 ClientHello가 새로 보낸 것(fresh)이라고 가정하는 다른 어떤 조치도 하지 않아야 한다(SHOULD NOT).

첫 번째 플라이트에서 보내는 0-RTT 메시지는 다른 플라이트에서 보내는 같은 유형의 메시지와 (암호화된) 콘텐츠 유형(handshake, application_data)이 같지만, 다른 키로 보호된다. 서버의 Finished 메시지를 받은 뒤, 서버가 조기 데이터를 받아들였다면 키가 바뀌었음을 나타내기 위해 클라이언트가 EndOfEarlyData 메시지를 보낸다. 이 메시지는 0-RTT 트래픽 키로 암호화된다.

“early_data” 확장을 받은 서버는 반드시 다음 세 가지 중 하나로 동작해야 한다(MUST).

조기 데이터를 받아들이려면 서버는 반드시 클라이언트의 “pre_shared_key” 확장에서 처음 제시된 키를 선택했어야 한다(MUST). 또한 서버는 다음 값이 선택한 PSK에 연관된 값과 같은지 반드시 확인해야 한다(MUST).

이 요구 사항은 해당 PSK를 사용해 1-RTT 핸드셰이크를 하는 데 필요한 요구 사항의 상위 집합이다.

향후 확장은 반드시 0-RTT와의 상호 작용을 정의해야 한다(MUST).

이 확인 중 하나라도 실패하면, 서버는 이 확장으로 응답해서는 안 되며(MUST NOT), 위에 나열한 처음 두 메커니즘 중 하나를 사용해 첫 번째 플라이트의 데이터를 모두 버려야 한다(그래서 1-RTT 또는 2-RTT로 돌아간다). 클라이언트가 0-RTT 핸드셰이크를 시도했지만 서버가 거절한 경우, 서버에는 일반적으로 0-RTT 레코드 보호 키가 없으므로, 0-RTT가 아닌 첫 메시지를 찾으려면 대신 시험 복호화(trial decryption)를 써야 한다(1-RTT 핸드셰이크 키로 복호화를 시도하거나, HelloRetryRequest의 경우 평문 ClientHello를 찾는다).

서버가 “early_data” 확장을 받아들이기로 했다면, 조기 데이터 레코드를 처리할 때 모든 레코드에 규정된 것과 같은 오류 처리 요구 사항을 반드시 지켜야 한다(MUST). 구체적으로, 받아들인 “early_data” 확장 다음의 0-RTT 레코드를 복호화하지 못하면 서버는 반드시 5.2절에 따라 “bad_record_mac” 경고로 연결을 종료해야 한다(MUST).

서버가 “early_data” 확장을 거절하면, 클라이언트 애플리케이션은 핸드셰이크가 끝난 뒤 앞서 조기 데이터로 보낸 애플리케이션 데이터를 재전송하기로 할 수 있다(MAY). 조기 데이터를 자동으로 재전송하면 연결 상태에 관해 잘못된 가정을 하게 될 수 있다는 점에 유의하라. 예를 들어 협상된 연결이 조기 데이터에 쓴 것과 다른 ALPN 프로토콜을 선택하면, 애플리케이션은 다른 메시지를 구성할 필요가 있을 수도 있다. 마찬가지로 조기 데이터가 연결 상태에 대해 무언가를 가정한다면, 핸드셰이크가 끝난 뒤 잘못 전송될 수도 있다.

TLS 구현은 조기 데이터를 자동으로 다시 보내지 않아야 한다(SHOULD NOT). 언제 재전송이 적절한지는 애플리케이션이 더 잘 판단할 수 있다. 협상된 연결이 같은 ALPN 프로토콜을 선택하지 않는 한, TLS 구현은 조기 데이터를 자동으로 다시 보내서는 안 된다(MUST NOT).

4.3.11. Pre-Shared Key Extension

“pre_shared_key” 확장은 PSK 키 수립과 관련해, 주어진 핸드셰이크에 쓸 사전 공유 키의 식별자를 협상하는 데 쓰인다.

이 확장의 “extension_data” 필드에는 “PreSharedKeyExtension” 값이 들어 있다.

      struct {
          opaque identity<1..2^16-1>;
          uint32 obfuscated_ticket_age;
      } PskIdentity;

      opaque PskBinderEntry<32..255>;

      struct {
          PskIdentity identities<7..2^16-1>;
          PskBinderEntry binders<33..2^16-1>;
      } OfferedPsks;

      struct {
          select (Handshake.msg_type) {
              case client_hello: OfferedPsks;
              case server_hello: uint16 selected_identity;
          };
      } PreSharedKeyExtension;

각 PSK는 해시 알고리즘 하나에 연관된다. 티켓 메커니즘(4.7.1절)으로 수립한 PSK의 경우, 이 해시는 티켓을 수립한 연결의 KDF 해시 알고리즘이다. 외부에서 수립한 PSK의 경우, 해시 알고리즘은 반드시 PSK를 수립할 때 설정하거나, 그런 알고리즘이 정의되지 않았다면 SHA-256을 기본값으로 삼아야 한다(MUST). 서버는 반드시 호환되는 PSK(있다면)와 암호 스위트를 선택하도록 보장해야 한다(MUST).

TLS 1.3 이전 버전의 TLS에서 SNI(Server Name Indication) 값은 세션에 연관되도록 의도되었고([RFC6066]의 3절), 서버는 세션에 연관된 SNI 값이 재개 핸드셰이크에서 지정한 값과 일치하도록 강제해야 했다. 그러나 실제로 구현들은 제공된 두 SNI 값 중 어느 것을 쓸지 일관되지 않았고, 그 결과 일관성 요구 사항을 사실상 클라이언트가 강제하게 되었다. TLS 1.3에서는 SNI 값을 재개 핸드셰이크에서 항상 명시적으로 지정하며, 서버가 SNI 값을 티켓에 연관시킬 필요가 없다. 그러나 클라이언트는 4.7.1절의 요구 사항을 충족하기 위해 SNI를 PSK와 함께 저장해야 한다(SHOULD).

구현자 참고: 세션 재개가 PSK의 주된 사용 사례라면, PSK/암호 스위트 일치 요구 사항을 구현하는 가장 간단한 방법은 암호 스위트를 먼저 협상한 다음 호환되지 않는 PSK를 제외하는 것이다. 알 수 없는 PSK(예: PSK 데이터베이스에 없거나 알 수 없는 키로 암호화된 것)는 그냥 무시해야 한다(SHOULD). 받아들일 수 있는 PSK가 없으면, 서버는 가능하다면 PSK가 아닌 핸드셰이크를 수행해야 한다(SHOULD). 하위 호환성이 중요하다면, 클라이언트가 제공한 외부 수립 PSK가 암호 스위트 선택에 영향을 주어야 한다(SHOULD).

PSK 키 수립을 받아들이기 전에, 서버는 반드시 대응하는 바인더 값을 검증해야 한다(MUST)(아래 4.3.11.2절 참고). 이 값이 없거나 검증되지 않으면 서버는 반드시 핸드셰이크를 중단해야 한다(MUST). 서버는 여러 바인더를 검증하려 하지 않아야 하며(SHOULD NOT), 그 대신 PSK 하나를 선택하고 그 PSK에 대응하는 바인더만 검증해야 한다(SHOULD). 이 요구 사항의 보안상 근거는 8.2절과 부록 F.6을 참고한다. PSK 키 수립을 받아들이려면, 서버는 선택한 식별자를 나타내는 “pre_shared_key” 확장을 보낸다.

클라이언트는 반드시 서버의 selected_identity가 클라이언트가 제공한 범위 안에 있는지, 서버가 PSK에 연관된 해시를 나타내는 암호 스위트를 선택했는지, ClientHello의 “psk_key_exchange_modes” 확장이 요구한다면 서버의 “key_share” 확장이 있는지 확인해야 한다(MUST). 이 값들이 일관되지 않으면 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

서버가 “early_data” 확장을 제공하면, 클라이언트는 반드시 서버의 selected_identity가 0인지 확인해야 한다(MUST). 다른 값이 돌아오면 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

“pre_shared_key” 확장은 반드시 ClientHello의 마지막 확장이어야 한다(MUST)(아래 설명대로 구현을 쉽게 하기 위해서다). 서버는 반드시 이 확장이 마지막 확장인지 확인하고, 그렇지 않으면 “illegal_parameter” 경고로 핸드셰이크를 실패시켜야 한다(MUST).

4.3.11.1. 티켓 나이

클라이언트가 보는 티켓의 나이는 NewSessionTicket 메시지를 받은 뒤로 지난 시간이다. 클라이언트는 티켓과 함께 제공된 “ticket_lifetime” 값보다 나이가 많은 티켓을 사용하려 해서는 안 된다(MUST NOT). 각 PskIdentity의 “obfuscated_ticket_age” 필드에는 티켓 나이를 난독화한 값이 들어 있다. 이 값은 밀리초 단위의 나이에 티켓과 함께 받은 “ticket_age_add” 값(4.7.1절 참고)을 더하고 2^32로 나눈 나머지다. 이렇게 더하면, 티켓이나 키 공유값을 재사용하지 않는 한 수동 관찰자가 연결들을 서로 연관 짓지 못한다. NewSessionTicket 메시지의 “ticket_lifetime” 필드는 초 단위이지만 “obfuscated_ticket_age”는 밀리초 단위라는 점에 유의하라. 티켓 수명은 일주일로 제한되므로, 밀리초 단위로도 있을 법한 모든 나이를 32비트로 나타내기에 충분하다.

4.3.11.2. PSK 바인더

PSK 바인더 값은 PSK와 현재 핸드셰이크를 결합하고, (NewSessionTicket 메시지로 생성한 경우) PSK를 생성한 핸드셰이크와 현재 핸드셰이크도 결합한다. 바인더 목록의 각 항목은 PreSharedKeyExtension.identities 필드까지(그 필드 포함)의 부분 ClientHello를 담은 트랜스크립트 해시(4.1절 참고)에 대한 HMAC으로 계산한다. 즉, 이 해시에는 바인더 목록 자체를 뺀 ClientHello 전체가 들어간다. 메시지의 길이 필드(전체 길이, 확장 블록 길이, “pre_shared_key” 확장 길이)는 모두 올바른 길이의 바인더가 있는 것처럼 설정한다.

PskBinderEntry는 Finished 메시지(4.5.3절)와 같은 방법으로 계산하되, BaseKey로는 제시하는 해당 PSK에서 키 스케줄을 통해 유도한 binder_key를 쓴다(7.1절 참고).

핸드셰이크에 HelloRetryRequest가 들어 있으면, 처음 ClientHello와 HelloRetryRequest가 새 ClientHello와 함께 트랜스크립트에 들어간다. 예를 들어 클라이언트가 ClientHello1을 보내면, 그 바인더는 다음에 대해 계산한다.

      Transcript-Hash(Truncate(ClientHello1))

여기서 Truncate()는 ClientHello에서 바인더 목록을 제거한다. 클라이언트는 서버가 어떤 암호 스위트를 선택할지 모르므로, 이 해시는 PSK에 연관된 해시로 계산한다는 점에 유의하라.

서버가 HelloRetryRequest로 응답하고 클라이언트가 이어서 ClientHello2를 보내면, 그 바인더는 다음에 대해 계산한다.

      Transcript-Hash(ClientHello1,
                      HelloRetryRequest,
                      Truncate(ClientHello2))

그 밖의 모든 핸드셰이크 해시 계산에는 ClientHello1/ClientHello2 전체가 들어간다. 첫 번째 플라이트에서는 Truncate(ClientHello1)을 직접 해시하지만, 두 번째 플라이트에서는 4.1절에 설명된 대로 ClientHello1을 해시한 다음 “message_hash” 메시지로 다시 넣는다는 점에 유의하라. “message_hash”는 협상된 함수로 해시하며, 이 함수는 PSK에 연관된 해시와 같을 수도 있고 다를 수도 있다는 점에도 유의하라. 이는 나머지 핸드셰이크의 트랜스크립트를 계산하는 방식과 일관된다.

4.3.11.3. 처리 순서

클라이언트는 서버의 Finished를 받을 때까지 0-RTT 데이터를 “스트리밍”하다가, 그때에야 EndOfEarlyData 메시지를 보내고 나머지 핸드셰이크를 이어 갈 수 있다. 교착 상태(deadlock)를 피하기 위해, “early_data”를 받아들일 때 서버는 ServerHello를 보내기 전에 클라이언트의 EndOfEarlyData 메시지를 기다리지 말고, 반드시 클라이언트의 ClientHello를 처리한 뒤 곧바로 자신의 메시지 플라이트를 보내야 한다(MUST).

4.4. 서버 매개 변수

서버가 보내는 다음 두 메시지 EncryptedExtensions와 CertificateRequest에는 핸드셰이크의 나머지를 정하는 서버의 정보가 들어 있다. 이 메시지들은 server_handshake_traffic_secret에서 유도한 키로 암호화된다.

4.4.1. Encrypted Extensions

모든 핸드셰이크에서 서버는 반드시 ServerHello 메시지 바로 다음에 EncryptedExtensions 메시지를 보내야 한다(MUST). 이 메시지는 server_handshake_traffic_secret에서 유도한 키로 암호화되는 첫 메시지다.

EncryptedExtensions 메시지에는 보호할 수 있는 확장, 즉 암호 컨텍스트를 수립하는 데는 필요하지 않으면서 개별 인증서에 연관되지도 않은 확장이 들어 있다. 클라이언트는 반드시 EncryptedExtensions에 금지된 확장이 있는지 확인해야 하며(MUST), 발견하면 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).

이 메시지의 구조는 다음과 같다.

      struct {
          Extension extensions<0..2^16-1>;
      } EncryptedExtensions;

4.4.2. Certificate Request

인증서로 인증하는 서버는 선택적으로 클라이언트에게 인증서를 요청할 수 있다(MAY). 이 메시지를 보낸다면 반드시 EncryptedExtensions 다음에 와야 한다(MUST).

이 메시지의 구조는 다음과 같다.

      struct {
          opaque certificate_request_context<0..2^8-1>;
          Extension extensions<0..2^16-1>;
      } CertificateRequest;

이전 버전의 TLS에서 CertificateRequest 메시지는 서버가 받아들일 서명 알고리즘과 인증 기관의 목록을 담았다. TLS 1.3에서 전자는 “signature_algorithms” 확장과, 선택적으로 “signature_algorithms_cert” 확장을 보내 나타낸다. 후자는 “certificate_authorities” 확장을 보내 나타낸다(4.3.4절 참고).

재개 PSK로 인증하는 서버는 주 핸드셰이크에서 CertificateRequest 메시지를 보내서는 안 된다(MUST NOT). 다만 클라이언트가 “post_handshake_auth” 확장(4.3.6절 참고)을 보냈다면 핸드셰이크 후 인증(4.7.2절 참고)에서는 보낼 수 있다(MAY). 달리 정하는 다른 명세가 없는 한, 외부 PSK로 인증하는 서버는 주 핸드셰이크에서 CertificateRequest 메시지를 보내거나 핸드셰이크 후 인증을 요청해서는 안 된다(MUST NOT). [RFC8773]은 이를 허용하는 확장을 제공하지만, 이 명세보다 분석을 덜 받았다.

4.5. 인증 메시지

2장에서 논의했듯이 TLS는 일반적으로 인증, 키 확인, 핸드셰이크 무결성을 위해 Certificate, CertificateVerify, Finished라는 공통 메시지 묶음을 사용한다. (PSK 바인더도 비슷한 방식으로 키 확인을 한다.) 이 세 메시지는 항상 각 핸드셰이크 플라이트의 마지막 메시지로 보낸다. Certificate와 CertificateVerify 메시지는 아래에 정의된 특정 상황에서만 보낸다. Finished 메시지는 항상 인증 블록(Authentication Block)의 일부로 보낸다. 핸드셰이크 후 인증을 제외하면, 이 메시지들은 [sender]_handshake_traffic_secret에서 유도한 키로 암호화된다.

인증 메시지의 계산은 모두 똑같이 다음을 입력으로 받는다.

이 입력을 바탕으로 각 메시지에는 다음이 담긴다.

다음 표는 각 상황의 핸드셰이크 컨텍스트와 MAC 기본 키를 정의한다.

모드 핸드셰이크 컨텍스트 기본 키
서버 ClientHello … EncryptedExtensions/CertificateRequest 중 나중 것 server_handshake_traffic_secret
클라이언트 ClientHello … 서버 Finished/EndOfEarlyData 중 나중 것 client_handshake_traffic_secret
핸드셰이크 후 ClientHello … 클라이언트 Finished + CertificateRequest [sender]_application_traffic_secret_N

표 2: 인증 입력

4.5.1. Certificate

이 메시지는 엔드포인트의 인증서 체인을 피어에게 전달한다.

합의된 키 교환 방법이 인증에 인증서를 사용할 때마다(이 문서에 정의된 키 교환 방법 중 PSK를 제외한 모든 방법이 해당한다) 서버는 반드시 Certificate 메시지를 보내야 한다(MUST).

클라이언트는 서버가 CertificateRequest 메시지(4.4.2절)로 인증서 기반 클라이언트 인증을 요청한 경우에, 그리고 그 경우에만 반드시 Certificate 메시지를 보내야 한다(MUST). 서버가 인증서 기반 클라이언트 인증을 요청했지만 적절한 인증서가 없으면, 클라이언트는 반드시 인증서가 없는(즉, “certificate_list” 필드의 길이가 0인) Certificate 메시지를 보내야 한다(MUST). Certificate 메시지가 비어 있는지와 관계없이 Finished 메시지는 반드시 보내야 한다(MUST).

이 메시지의 구조는 다음과 같다.

          enum {
              X509(0),
              RawPublicKey(2),
              (255)
          } CertificateType;

          struct {
              select (certificate_type) {
                  case RawPublicKey:
                    /* RFC 7250의 ASN.1_subjectPublicKeyInfo */
                    opaque ASN1_subjectPublicKeyInfo<1..2^24-1>;

                  case X509:
                    opaque cert_data<1..2^24-1>;
              };
              Extension extensions<0..2^16-1>;
          } CertificateEntry;

          struct {
              opaque certificate_request_context<0..2^8-1>;
              CertificateEntry certificate_list<0..2^24-1>;
          } Certificate;

대응하는 인증서 유형 확장(“server_certificate_type” 또는 “client_certificate_type”)이 EncryptedExtensions에서 협상되지 않았거나 X.509 인증서 유형이 협상되었다면, 각 CertificateEntry에는 DER 인코딩된 X.509 인증서가 들어 있다. 송신자의 인증서는 반드시 목록의 첫 번째 CertificateEntry에 와야 한다(MUST). 그 뒤의 각 인증서는 바로 앞의 인증서를 직접 인증해야 한다(SHOULD). 인증서 검증에는 신뢰 앵커를 별도로 배포해야 하므로, 지원 대상 피어가 생략된 인증서를 모두 가지고 있다고 알려져 있다면 신뢰 앵커를 지정하는 인증서는 체인에서 생략할 수 있다(MAY).

참고: TLS 1.3 이전에는 “certificate_list”의 순서상 각 인증서가 바로 앞의 인증서를 인증해야 했다. 그러나 일부 구현은 어느 정도 유연성을 허용했다. 서버는 전환 목적으로 현재 중간 인증서와 사용이 중단된 중간 인증서를 함께 보내기도 하고, 단순히 잘못 구성된 서버도 있지만, 그래도 이런 경우는 올바르게 검증할 수 있다. 호환성을 최대한 높이기 위해, 모든 구현은 어떤 TLS 버전에서든 불필요할 수 있는 인증서와 임의의 순서를 처리할 준비가 되어 있어야 한다(SHOULD). 단, 최종 개체(end-entity) 인증서는 예외로, 반드시 첫 번째여야 한다(MUST).

RawPublicKey 인증서 유형이 협상되었다면, certificate_list에는 반드시 CertificateEntry가 하나 이하로 들어 있어야 하며(MUST), 그 항목에는 [RFC7250]의 3절에 정의된 ASN1_subjectPublicKeyInfo 값이 들어 있다.

OpenPGP 인증서 유형 [RFC6091]은 TLS 1.3과 함께 사용해서는 안 된다(MUST NOT).

서버의 certificate_list는 반드시 항상 항목을 하나 이상 담아야 한다(MUST). 클라이언트는 서버의 인증 요청에 응답해 보낼 적절한 인증서가 없으면 빈 certificate_list를 보낸다.

4.5.1.1. OCSP 상태와 SCT 확장

[RFC6066]과 [RFC6961]은 서버가 클라이언트에게 OCSP 응답을 보내는 것을 협상하는 확장을 제공한다. TLS 1.2 이하에서 서버는 이 확장의 협상을 나타내기 위해 빈 확장으로 응답하며, OCSP 정보는 CertificateStatus 메시지에 담긴다. TLS 1.3에서 서버의 OCSP 정보는 해당 인증서를 담은 CertificateEntry의 확장에 담긴다. 구체적으로, 서버가 보내는 “status_request” 확장의 본문은 반드시 [RFC6066]에 정의된 CertificateStatus 구조체여야 하며(MUST), 이 구조체는 [RFC6960]에 정의된 대로 해석한다.

참고: status_request_v2 확장 [RFC6961]은 사용이 중단되었다. TLS 1.3 서버는 ClientHello 메시지를 처리할 때 이 확장이 있다는 사실이나 그 안의 정보에 따라 행동해서는 안 된다(MUST NOT). 특히 EncryptedExtensions, CertificateRequest, Certificate 메시지에 status_request_v2 확장을 보내서는 안 된다(MUST NOT). 이전 프로토콜 버전에서 이 확장을 쓰려는 클라이언트가 보낼 수 있으므로(MAY), TLS 1.3 서버는 반드시 이 확장이 들어 있는 ClientHello 메시지를 처리할 수 있어야 한다(MUST).

서버는 CertificateRequest 메시지에 빈 “status_request” 확장을 보내, 클라이언트가 인증서와 함께 OCSP 응답을 제시하도록 요청할 수 있다(MAY). 클라이언트가 OCSP 응답을 보내기로 한다면, 클라이언트의 “status_request” 확장의 본문은 반드시 [RFC6066]에 정의된 CertificateStatus 구조체여야 한다(MUST).

마찬가지로 [RFC6962]는 TLS 1.2 이하에서 서버가 ServerHello의 확장으로 SCT(Signed Certificate Timestamp)를 보내는 메커니즘을 제공한다. TLS 1.3에서 서버의 SCT 정보는 CertificateEntry의 확장에 담긴다.

4.5.1.2. 인증서 선택

클라이언트나 서버가 보내는 인증서에는 다음 규칙이 적용된다.

클라이언트가 보내는 인증서에는 다음 규칙이 추가로 적용된다.

서버가 보내는 인증서에는 다음 규칙이 추가로 적용된다.

송신자가 그런 체인을 제공할 수 있다면, 송신자가 제공하는 모든 인증서는 반드시 피어가 알린 서명 알고리즘으로 서명된 것이어야 한다(MUST)(4.3.3절 참고). 자체 서명된 인증서나 신뢰 앵커로 예상되는 인증서는 체인의 일부로 검증하지 않으므로 어떤 알고리즘으로든 서명될 수 있다(MAY).

송신자가 서버이고, 서버가 표시된 지원 알고리즘만으로 서명된 인증서 체인을 만들 수 없다면, 서버는 클라이언트가 지원하는지 알 수 없는 알고리즘이 들어 있을 수도 있는, 자신이 고른 인증서 체인을 보내 핸드셰이크를 계속해야 한다(SHOULD). 이 대체(fallback) 체인은 클라이언트가 SHA-1을 받아들일 의향을 특별히 알리지 않은 한, 사용이 중단된 SHA-1 해시를 써서는 안 된다(MUST NOT).

송신자가 클라이언트라면, 클라이언트는 위와 같은 대체 체인을 쓰거나 익명으로 핸드셰이크를 계속할 수 있다(MAY).

수신자가 제공된 인증서로 받아들일 수 있는 체인을 구성하지 못해 핸드셰이크를 중단하기로 했다면, 반드시 적절한 인증서 관련 경고(기본값은 “unsupported_certificate”이며, 자세한 내용은 6.2절 참고)로 핸드셰이크를 중단해야 한다(MUST).

송신자에게 인증서가 여러 개 있으면, 송신자는 위에 언급한 기준(그리고 전송 계층 엔드포인트, 로컬 구성, 선호 같은 다른 기준)에 따라 그중 하나를 고른다.

4.5.1.3. Certificate 메시지 받기

일반적으로 자세한 인증서 검증 절차는 TLS의 범위 밖이다([RFC5280] 참고). 이 절은 TLS에 고유한 요구 사항을 제시한다.

서버가 빈 Certificate 메시지를 보내면, 클라이언트는 반드시 “decode_error” 경고로 핸드셰이크를 중단해야 한다(MUST).

클라이언트가 인증서를 하나도 보내지 않으면(즉, 빈 Certificate 메시지를 보내면), 서버는 재량에 따라 클라이언트 인증 없이 핸드셰이크를 계속하거나 “certificate_required” 경고로 핸드셰이크를 중단할 수 있다(MAY). 또한 인증서 체인의 어떤 측면을 받아들일 수 없다면(예: 알려진, 신뢰하는 CA가 서명하지 않은 경우), 서버는 재량에 따라 (클라이언트를 인증되지 않은 것으로 보고) 핸드셰이크를 계속하거나 핸드셰이크를 중단할 수 있다(MAY).

MD5 해시를 쓰는 서명 알고리즘으로 검증해야 하는 인증서를 받은 엔드포인트는 반드시 “bad_certificate” 경고로 핸드셰이크를 중단해야 한다(MUST). SHA-1은 사용이 중단되었으며, SHA-1 해시를 쓰는 서명 알고리즘으로 검증해야 하는 인증서를 받은 엔드포인트는 “bad_certificate” 경고로 핸드셰이크를 중단할 것을 권고한다(RECOMMENDED). 명확히 하자면, 이는 자체 서명된 인증서나 신뢰 앵커인 인증서에 대해서는 엔드포인트가 이 알고리즘들을 받아들일 수 있다는 뜻이다.

현재 SHA-1 지원을 단계적으로 없애고 있는 구현과의 상호 운용성을 유지하기 위해, 모든 엔드포인트는 가능한 한 빨리 SHA-256 이상으로 전환할 것을 권고한다(RECOMMENDED).

한 서명 알고리즘용 키를 담은 인증서는 다른 서명 알고리즘으로 서명될 수 있다(MAY)는 점에 유의하라(예: ECDSA 키로 서명된 RSA 키).

4.5.2. Certificate Verify

이 메시지는 엔드포인트가 자신의 인증서에 대응하는 개인 키를 가지고 있다는 명시적 증거를 제공하는 데 쓰인다. CertificateVerify 메시지는 이 시점까지의 핸드셰이크에 무결성도 제공한다. 서버는 인증서로 인증할 때 반드시 이 메시지를 보내야 한다(MUST). 클라이언트는 인증서로 인증할 때마다(즉, Certificate 메시지가 비어 있지 않을 때) 반드시 이 메시지를 보내야 한다(MUST). 이 메시지를 보낸다면 반드시 Certificate 메시지 바로 다음, Finished 메시지 바로 앞에 나타나야 한다(MUST).

이 메시지의 구조는 다음과 같다.

      struct {
          SignatureScheme algorithm;
          opaque signature<0..2^16-1>;
      } CertificateVerify;

algorithm 필드는 사용한 서명 알고리즘을 지정한다(이 타입의 정의는 4.3.3절 참고). signature는 그 알고리즘을 사용한 디지털 서명이다. 서명이 다루는 내용은 4.1절에 설명된 해시 출력, 즉 다음 값이다.

      Transcript-Hash(Handshake Context, Certificate)

그런 다음 디지털 서명은 다음을 이어 붙인 것에 대해 계산한다.

이 구조는 이전 버전의 TLS에 대한 공격, 즉 ServerKeyExchange 형식 때문에 공격자가 선택한 32바이트 접두사(ClientHello.random)가 붙은 메시지의 서명을 얻을 수 있었던 공격을 막기 위한 것이다. 처음의 64바이트 패딩이 그 접두사와 서버가 제어하는 ServerHello.random이 있던 자리를 함께 차지해 없앤다.

서버 서명의 컨텍스트 문자열은 “TLS 1.3, server CertificateVerify”다. 클라이언트 서명의 컨텍스트 문자열은 “TLS 1.3, client CertificateVerify”다. 이 문자열은 서로 다른 컨텍스트에서 만든 서명을 분리해, 일어날 수 있는 교차 프로토콜 공격을 막는 데 도움이 된다.

예를 들어 트랜스크립트 해시가 01로 된 32바이트라면(이 길이는 SHA-256에 맞다), 서버 CertificateVerify의 디지털 서명이 다루는 내용은 다음과 같다.

      2020202020202020202020202020202020202020202020202020202020202020
      2020202020202020202020202020202020202020202020202020202020202020
      544c5320312e332c207365727665722043657274696669636174655665726966
      79
      00
      0101010101010101010101010101010101010101010101010101010101010101

송신자 쪽에서 CertificateVerify 메시지의 signature 필드를 계산하는 과정은 다음을 입력으로 받는다.

서버가 CertificateVerify 메시지를 보낸다면, 지원되지 않는 알고리즘 없이는 유효한 인증서 체인을 만들 수 없는 경우가 아닌 한, 서명 알고리즘은 반드시 클라이언트의 “signature_algorithms” 확장에서 제시한 것 중 하나여야 한다(MUST)(4.3.3절 참고).

클라이언트가 보낸다면, 서명에 쓴 서명 알고리즘은 반드시 CertificateRequest 메시지의 “signature_algorithms” 확장 중 supported_signature_algorithms 필드에 있는 것 중 하나여야 한다(MUST).

또한 서명 알고리즘은 반드시 송신자의 최종 개체 인증서에 담긴 키와 호환되어야 한다(MUST). CertificateVerify 메시지의 서명에는 어떤 경우에도 SHA-1 알고리즘을 사용해서는 안 된다(MUST NOT). 이 명세의 모든 SHA-1 서명 알고리즘은 레거시 인증서에 쓰기 위해서만 정의되었으며, CertificateVerify 서명에는 유효하지 않다.

CertificateVerify 메시지의 수신자는 반드시 signature 필드를 검증해야 한다(MUST). 검증 과정은 다음을 입력으로 받는다.

검증에 실패하면 수신자는 반드시 “decrypt_error” 경고로 핸드셰이크를 종료해야 한다(MUST).

4.5.3. Finished

Finished 메시지는 인증 블록의 마지막 메시지다. 이 메시지는 핸드셰이크와 계산된 키를 인증하는 데 꼭 필요하다.

Finished 메시지의 수신자는 반드시 내용이 올바른지 확인해야 하며(MUST), 올바르지 않으면 반드시 “decrypt_error” 경고로 연결을 종료해야 한다(MUST).

한쪽이 자신의 Finished 메시지를 보내고 피어의 Finished 메시지를 받아 검증하고 나면, 그 연결로 애플리케이션 데이터를 보내고 받기 시작할 수 있다. 피어의 Finished를 받기 전에 데이터를 보내는 것이 허용되는 상황은 두 가지다.

  1. 4.3.10절에 설명된 대로 0-RTT 데이터를 보내는 클라이언트.

  2. 서버는 첫 번째 플라이트를 보낸 뒤 데이터를 보낼 수 있다(MAY). 그러나 핸드셰이크가 아직 끝나지 않았으므로, 서버는 피어의 신원도, 피어가 살아 있는지도(즉, ClientHello가 재전송된 것일 수도 있다) 확신할 수 없다.

Finished 메시지를 계산하는 데 쓰는 키는 4.5절에 정의된 기본 키로부터 HKDF를 사용해 계산한다(7.1절 참고). 구체적으로는 다음과 같다.

   finished_key =
       HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)

이 메시지의 구조는 다음과 같다.

      struct {
          opaque verify_data[Hash.length];
      } Finished;

verify_data 값은 다음과 같이 계산한다.

      verify_data =
          HMAC(finished_key,
               Transcript-Hash(Handshake Context,
                               Certificate*, CertificateVerify*))

*는 있을 때만 포함함을 뜻한다.

HMAC [RFC2104]은 핸드셰이크의 해시 알고리즘을 사용한다. 앞서 언급했듯이 HMAC 입력은 일반적으로 누적 해시, 즉 이 시점의 핸드셰이크 해시로 구현할 수 있다.

이전 버전의 TLS에서 verify_data는 항상 12옥텟 길이였다. TLS 1.3에서는 핸드셰이크에 쓰는 해시에 대한 HMAC 출력의 크기다.

참고: 경고와 핸드셰이크가 아닌 그 밖의 레코드 유형은 핸드셰이크 메시지가 아니며 해시 계산에 포함되지 않는다.

Finished 메시지 다음의 레코드는 모두 반드시 7.2절에 설명된 대로 적절한 애플리케이션 트래픽 키로 암호화되어야 한다(MUST). 특히 여기에는 클라이언트 Certificate와 CertificateVerify 메시지에 대한 응답으로 서버가 보내는 경고가 포함된다.

4.6. End of Early Data

      struct {} EndOfEarlyData;

서버가 EncryptedExtensions에 “early_data” 확장을 담아 보냈다면, 클라이언트는 서버 Finished를 받은 뒤 반드시 EndOfEarlyData 메시지를 보내야 한다(MUST). 서버가 EncryptedExtensions에 “early_data” 확장을 담아 보내지 않으면, 클라이언트는 EndOfEarlyData 메시지를 보내서는 안 된다(MUST NOT). 이 메시지는 0-RTT application_data 메시지가 (있다면) 모두 전송되었고, 이후 레코드는 핸드셰이크 트래픽 키로 보호된다는 것을 나타낸다. 서버는 이 메시지를 보내서는 안 되며(MUST NOT), 이 메시지를 받은 클라이언트는 반드시 “unexpected_message” 경고로 연결을 종료해야 한다(MUST). 이 메시지는 client_early_traffic_secret에서 유도한 키로 암호화된다.

4.7. 핸드셰이크 후 메시지

TLS에서는 주 핸드셰이크 뒤에도 다른 메시지를 보낼 수 있다. 이 메시지들은 handshake 콘텐츠 유형을 쓰며 적절한 애플리케이션 트래픽 키로 암호화된다.

4.7.1. New Session Ticket Message

클라이언트의 hello에 적절한 “psk_key_exchange_modes” 확장이 들어 있었다면, 서버는 클라이언트 Finished 메시지를 받은 뒤 언제든 NewSessionTicket 메시지를 보낼 수 있다(MAY). NewSessionTicket 메시지는 티켓 값과, 재개 비밀값(resumption secret)에서 유도한 비밀 PSK 사이에 고유한 연관을 만든다(7장 참고).

클라이언트는 ClientHello의 “pre_shared_key” 확장에 티켓 값을 넣어 이후 핸드셰이크에서 이 PSK를 사용할 수 있다(MAY)(4.3.11절). NewSessionTicket 메시지를 받았지만 재개를 지원하지 않는 클라이언트는 반드시 이 메시지를 조용히 무시해야 한다(MUST). 재개는 원래 연결이 아직 열려 있는 동안 할 수 있다(MAY). 서버는 한 연결에서 티켓을 여러 개 보낼 수 있으며(MAY), 연달아 보내거나 특정 사건 뒤에 보낼 수 있다(부록 C.4 참고). 예를 들어 서버는 핸드셰이크 후 인증 뒤에 새 티켓을 보내, 추가된 클라이언트 인증 상태를 캡슐화할 수도 있다. 여러 티켓은 클라이언트에게 다음을 비롯한 다양한 목적으로 유용하다.

모든 티켓은 반드시 원래 연결을 수립할 때 쓴 것과 같은 KDF 해시 알고리즘을 가진 암호 스위트로만 재개해야 한다(MUST).

클라이언트는 반드시 새 SNI 값이 원래 세션에서 제시된 서버 인증서에 유효한 경우에만 재개해야 하며(MUST), SNI 값이 원래 세션에서 쓴 값과 일치하는 경우에만 재개해야 한다(SHOULD). 후자는 성능 최적화다. 보통은 인증서 하나가 다루는 서로 다른 서버가 서로의 티켓을 받아들일 수 있다고 기대할 이유가 없으므로, 그런 경우에 재개를 시도하면 한 번만 쓸 수 있는 티켓을 낭비하게 된다. (외부에서든 다른 어떤 방법으로든) 그런 표시가 제공된다면, 클라이언트는 다른 SNI 값으로 재개할 수 있다(MAY).

재개할 때 호출한 애플리케이션에 SNI 값을 알린다면, 구현은 반드시 이전 세션에서 보낸 값이 아니라 재개 ClientHello에서 보낸 값을 사용해야 한다(MUST). 서버 구현이 SNI 값이 다른 PSK 식별자를 모두 거절한다면 이 두 값은 항상 같다는 점에 유의하라.

참고: 재개 비밀값은 클라이언트의 두 번째 플라이트에 의존하지만, 인증서 기반 클라이언트 인증을 요청하지 않는 서버는 트랜스크립트의 나머지를 독자적으로 계산한 다음, 클라이언트 Finished를 기다리지 않고 자신의 Finished를 보내자마자 NewSessionTicket을 보낼 수 있다(MAY). 이는 예를 들어 클라이언트가 여러 TLS 연결을 병렬로 열 것으로 예상되어 재개 핸드셰이크의 줄어든 오버헤드로 이득을 볼 수 있는 경우에 적절할 수 있다.

      struct {
          uint32 ticket_lifetime;
          uint32 ticket_age_add;
          opaque ticket_nonce<0..255>;
          opaque ticket<1..2^16-1>;
          Extension extensions<0..2^16-1>;
      } NewSessionTicket;

현재 NewSessionTicket에 정의된 확장은 “early_data” 하나뿐이며, 이 확장은 티켓을 0-RTT 데이터를 보내는 데 쓸 수 있음을 나타낸다(4.3.10절). 이 확장에는 다음 값이 들어 있다.

티켓에 연관된 PSK는 다음과 같이 계산한다.

       HKDF-Expand-Label(resumption_secret,
                         "resumption", ticket_nonce, Hash.length)

ticket_nonce 값은 NewSessionTicket 메시지마다 다르므로, 티켓마다 다른 PSK가 유도된다.

원칙적으로는, PSK를 쓰지 않은 최초 핸드셰이크(대개 피어의 인증서에 묶여 있었을 것이다)에서 원래 유도한 키 재료의 수명을 무기한 연장하는 새 티켓을 계속 발급할 수 있다는 점에 유의하라. 구현은 그런 키 재료의 전체 수명에 한도를 둘 것을 권고한다(RECOMMENDED). 이 한도는 피어 인증서의 수명, 그사이에 폐지되었을 가능성, 피어의 온라인 CertificateVerify 서명 이후 지난 시간을 고려해야 한다.

4.7.2. 핸드셰이크 후 인증

클라이언트가 “post_handshake_auth” 확장(4.3.6절 참고)을 보냈다면, 서버는 핸드셰이크가 끝난 뒤 언제든 CertificateRequest 메시지를 보내 인증서 기반 클라이언트 인증을 요청할 수 있다(MAY). 클라이언트는 반드시 적절한 인증 메시지로 응답해야 한다(MUST)(4.5절 참고). 클라이언트가 인증하기로 한다면 반드시 Certificate, CertificateVerify, Finished를 보내야 한다(MUST). 거절한다면 반드시 인증서가 없는 Certificate 메시지와 그 뒤의 Finished를 보내야 한다(MUST). 한 응답에 대한 클라이언트의 메시지는 모두 반드시 다른 유형의 메시지나 다른 응답의 메시지가 끼어들지 않은 채 와이어상에서 연속해서 나타나야 한다(MUST).

“post_handshake_auth” 확장을 보내지 않았는데 핸드셰이크 후 CertificateRequest 메시지를 받은 클라이언트는 반드시 “unexpected_message” 치명적 경고를 보내야 한다(MUST).

참고: 인증서 기반 클라이언트 인증에는 사용자에게 묻는 과정이 따를 수 있으므로, 서버는 CertificateRequest를 보내고 응답을 받기까지 다른 메시지를 임의 개수만큼 받는 것을 포함해 어느 정도의 지연에 반드시 대비해야 한다(MUST). 또한 짧은 간격으로 CertificateRequest를 여러 개 받은 클라이언트는 받은 순서와 다른 순서로 응답할 수 있다(MAY)(certificate_request_context 값으로 서버가 응답을 구별할 수 있다).

4.7.3. 키와 초기 벡터 갱신

KeyUpdate 핸드셰이크 메시지는 송신자가 자신의 송신용 암호 키를 갱신하고 있음을 나타내는 데 쓰인다. 어느 피어든 Finished 메시지를 보낸 뒤에는 이 메시지를 보낼 수 있다. Finished 메시지를 받기 전에 KeyUpdate 메시지를 받은 구현은 반드시 “unexpected_message” 경고로 연결을 종료해야 한다(MUST). KeyUpdate 메시지를 보낸 뒤 송신자는 반드시 7.2절에 설명된 대로 계산한 다음 세대의 키로 모든 트래픽을 보내야 한다(SHALL). KeyUpdate를 받으면 수신자는 반드시 자신의 수신용 키를 갱신해야 한다(MUST).

      enum {
          update_not_requested(0), update_requested(1), (255)
      } KeyUpdateRequest;

      struct {
          KeyUpdateRequest request_update;
      } KeyUpdate;

request_update 필드가 “update_requested”로 설정되어 있으면, 수신자는 다음 애플리케이션 데이터 레코드를 보내기 전에 반드시 request_update를 “update_not_requested”로 설정한 자신의 KeyUpdate를 보내야 한다(MUST). 이 메커니즘으로 어느 쪽이든 연결 전체의 갱신을 강제할 수 있지만, 아무것도 보내지 않는 동안 KeyUpdate를 여러 개 받은 구현은 갱신 한 번으로 응답하게 된다. 피어에게서 다음 KeyUpdate를 받을 때까지, 송신자는 request_update를 “update_requested”로 설정한 KeyUpdate를 또 보내서는 안 된다(MUST NOT).

구현은 request_update를 “update_requested”로 설정한 KeyUpdate를 보낸 뒤 피어의 KeyUpdate를 받기까지, 관계없는 KeyUpdate를 포함해 임의 개수의 메시지를 받을 수 있다는 점에 유의하라. 그 메시지들은 이미 전송 중일 수 있기 때문이다. 그러나 송신 키와 수신 키는 서로 독립적인 트래픽 비밀값에서 유도되므로, 수신 트래픽 비밀값을 유지하더라도 송신자가 키를 바꾸기 전에 보낸 데이터의 순방향 비밀성은 위협받지 않는다.

구현들이 각자 request_update를 “update_requested”로 설정한 KeyUpdate를 보내고 그 메시지들이 전송 중에 엇갈리면, 각자 응답도 보내게 되어 결과적으로 양쪽 모두 두 세대씩 증가한다.

송신자와 수신자 모두 반드시 이전 키로 KeyUpdate 메시지를 암호화해야 한다(MUST). 또한 양쪽 모두 새 키로 암호화된 메시지를 받아들이기 전에 이전 키로 암호화된 KeyUpdate를 먼저 받았는지 반드시 확인해야 한다(MUST). 그렇게 하지 않으면 메시지 절단 공격(message truncation attack)을 허용할 수 있다.

AES-128처럼 128비트 키를 쓰면, 키를 2^64번 다시 설정했을 때 한 연결 안에서 키가 재사용될 확률이 높다. 키가 반복되더라도 초기 벡터(IV)도 독립적으로 생성되므로, 키와 IV가 함께 충돌할 확률은 훨씬 낮다는 점에 유의하라. 보안 여유를 더하기 위해, 송신하는 구현은 에포크(epoch)가, 따라서 키 갱신 횟수가 2^48-1을 넘도록 허용해서는 안 된다(MUST NOT). 나중에 이 값을 바꿀 수 있도록(예: 128비트보다 긴 키를 쓰는 암호를 위해), 수신하는 구현은 이 규칙을 강제해서는 안 된다(MUST NOT). 송신하는 구현이 request_update를 “update_requested”로 설정한 KeyUpdate를 받았더라도, 자신의 KeyUpdate를 보내 이 한도를 넘게 되는 경우에는 보내서는 안 되며(MUST NOT), 그 대신 “update_requested” 플래그를 무시해야 한다(SHOULD). 그러면 결국 5.5절의 한도에 이르렀을 때 연결을 종료해야 할 수도 있다.

5. 레코드 프로토콜

TLS 레코드 프로토콜은 전송할 메시지를 받아 데이터를 다루기 쉬운 블록으로 단편화하고, 레코드를 보호한 뒤 그 결과를 전송한다. 받은 데이터는 검증하고 복호화하고 재조립한 다음 상위 계층 클라이언트에 전달한다.

TLS 레코드에는 유형이 있어, 같은 레코드 계층 위에서 여러 상위 계층 프로토콜을 다중화할 수 있다. 이 문서는 handshake, application_data, alert, change_cipher_spec 네 가지 콘텐츠 유형을 규정한다. change_cipher_spec 레코드는 호환성 목적으로만 쓰인다(부록 E.4 참고).

구현은 첫 ClientHello 메시지를 보내거나 받은 뒤부터 피어의 Finished 메시지를 받기 전까지 언제든, 단일 바이트 값 0x01로 이루어진 암호화되지 않은 change_cipher_spec 유형의 레코드를 받을 수 있다. 이 레코드는 반드시 더 처리하지 않고 그냥 버려야 한다(MUST). 이 레코드는 구현이 보호된 레코드를 기대하는 핸드셰이크 시점에 나타날 수 있으므로, 레코드를 보호 해제하려 시도하기 전에 이 상황을 감지할 필요가 있다는 점에 유의하라. 다른 change_cipher_spec 값을 받거나 보호된 change_cipher_spec 레코드를 받은 구현은 반드시 “unexpected_message” 경고로 핸드셰이크를 중단해야 한다(MUST). 구현이 첫 ClientHello 메시지 전이나 피어의 Finished 메시지 뒤에 받은 change_cipher_spec 레코드를 감지하면, 그 레코드는 반드시 예상하지 않은 레코드 유형으로 다뤄야 한다(MUST)(다만 무상태 서버는 이런 경우와 허용되는 경우를 구별하지 못할 수도 있다).

어떤 확장으로 협상하지 않는 한, 구현은 이 문서에 정의되지 않은 레코드 유형을 보내서는 안 된다(MUST NOT). TLS 구현이 예상하지 않은 레코드 유형을 받으면 반드시 “unexpected_message” 경고로 연결을 종료해야 한다(MUST). 새 레코드 콘텐츠 유형 값은 11장에 설명된 대로 IANA가 TLS ContentType 레지스트리에서 할당한다.

5.1. 레코드 계층

레코드 계층은 정보 블록을 2^14바이트 이하 단위의 데이터를 담은 TLSPlaintext 레코드로 단편화한다. 메시지 경계는 기반 ContentType에 따라 다르게 다룬다. 앞으로 정의되는 콘텐츠 유형은 모두 반드시 적절한 규칙을 규정해야 한다(MUST). 이 규칙은 TLS 1.2에서 강제하던 것보다 엄격하다는 점에 유의하라.

핸드셰이크 메시지는 다음 조건을 지킨다면 하나의 TLSPlaintext 레코드로 병합하거나 여러 레코드에 걸쳐 단편화할 수 있다(MAY).

구현은 길이가 0인 handshake 유형 조각(fragment)을 보내서는 안 된다(MUST NOT). 그 조각에 패딩이 들어 있더라도 마찬가지다.

경고 메시지(6장)는 여러 레코드에 걸쳐 단편화해서는 안 되며(MUST NOT), 여러 경고 메시지를 하나의 TLSPlaintext 레코드로 병합해서는 안 된다(MUST NOT). 다시 말해 alert 유형의 레코드에는 반드시 메시지가 정확히 하나 들어 있어야 한다(MUST).

애플리케이션 데이터 메시지에는 TLS가 해석하지 않는 데이터가 들어 있다. 애플리케이션 데이터 메시지는 항상 보호된다. 길이가 0인 애플리케이션 데이터 조각(즉, 내용의 길이가 0인 application_data 유형의 TLSInnerPlaintext 레코드)은 트래픽 분석 대응책으로 쓸모가 있을 수 있으므로 보낼 수 있다(MAY). 애플리케이션 데이터 조각은 여러 레코드로 나누거나 하나의 레코드로 병합할 수 있다(MAY).

      enum {
          invalid(0),
          change_cipher_spec(20),
          alert(21),
          handshake(22),
          application_data(23),
          (255)
      } ContentType;

      struct {
          ContentType type;
          ProtocolVersion legacy_record_version;
          uint16 length;
          opaque fragment[TLSPlaintext.length];
      } TLSPlaintext;

이 문서는 버전 0x0304를 쓰는 TLS 1.3을 설명한다. 이 버전 값은 역사적인 것으로, TLS 1.0에 0x0301을, SSL 3.0에 0x0300을 쓴 데서 비롯되었다. 하위 호환성을 최대한 높이기 위해, 처음 ClientHello를 담은 레코드는 버전 0x0301(TLS 1.0을 반영)이어야 하며(SHOULD), 두 번째 ClientHello나 ServerHello를 담은 레코드는 반드시 버전 0x0303(TLS 1.2를 반영)이어야 한다(MUST). 이전 버전의 TLS를 협상할 때 엔드포인트는 부록 E에 제시된 절차와 요구 사항을 따른다.

레코드 보호가 아직 적용되지 않았다면 TLSPlaintext 구조체를 그대로 와이어에 쓴다. 레코드 보호가 시작되면 TLSPlaintext 레코드는 다음 절에 설명된 대로 보호되어 전송된다. 애플리케이션 데이터 레코드는 보호되지 않은 채로 와이어에 써서는 안 된다(MUST NOT)는 점에 유의하라(자세한 내용은 2장 참고).

5.2. 레코드 페이로드 보호

레코드 보호 함수는 TLSPlaintext 구조체를 TLSCiphertext 구조체로 바꾼다. 보호 해제 함수는 그 과정을 되돌린다. TLS 1.3에서는 이전 버전의 TLS와 달리 모든 암호를 “Authenticated Encryption with Associated Data”(AEAD) [RFC5116]로 모델링한다. AEAD 함수는 평문을 인증된 암호문으로 바꾸고 다시 되돌리는, 암호화와 인증을 통합한 연산을 제공한다. 암호화된 각 레코드는 평문 헤더와 그 뒤의 암호화된 본문으로 이루어지며, 본문 자체에는 유형과 선택적 패딩이 들어 있다.

      struct {
          opaque content[TLSPlaintext.length];
          ContentType type;
          uint8 zeros[length_of_padding];
      } TLSInnerPlaintext;

      struct {
          ContentType opaque_type = application_data; /* 23 */
          ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
          uint16 length;
          opaque encrypted_record[TLSCiphertext.length];
      } TLSCiphertext;

AEAD 알고리즘은 [RFC5116]의 2.1절에 설명된 대로 키 하나, 논스, 평문, 그리고 인증 확인에 포함할 “추가 데이터(additional data)”를 입력으로 받는다. 키는 client_write_key 또는 server_write_key이고, 논스는 순서 번호(sequence number)와 client_write_iv 또는 server_write_iv에서 유도하며(5.3절 참고), 추가 데이터 입력은 레코드 헤더다. 즉, 다음과 같다.

      additional_data = TLSCiphertext.opaque_type ||
                        TLSCiphertext.legacy_record_version ||
                        TLSCiphertext.length

AEAD 알고리즘에 넣는 평문 입력은 인코딩한 TLSInnerPlaintext 구조체다. 트래픽 키의 유도는 7.3절에 정의되어 있다.

AEAD 출력은 AEAD 암호화 연산이 출력한 암호문으로 이루어진다. TLSInnerPlaintext.type과 송신자가 넣은 패딩이 포함되므로, 평문의 길이는 대응하는 TLSPlaintext.length보다 크다. AEAD 출력의 길이는 일반적으로 평문보다 크지만, 그 차이는 AEAD 알고리즘에 따라 다르다. 암호가 패딩을 포함할 수도 있으므로, 오버헤드의 양은 평문 길이에 따라 달라질 수 있다. 기호로 나타내면 다음과 같다.

      AEADEncrypted =
          AEAD-Encrypt(write_key, nonce, additional_data, plaintext)

TLSCiphertext의 encrypted_record 필드는 AEADEncrypted로 설정한다.

복호화하고 검증하려면, 암호는 키, 논스, 추가 데이터, AEADEncrypted 값을 입력으로 받는다. 출력은 평문이거나, 복호화에 실패했음을 나타내는 오류다. 별도의 무결성 검사는 없다. 기호로 나타내면 다음과 같다.

 plaintext of encrypted_record =
     AEAD-Decrypt(peer_write_key, nonce, additional_data, AEADEncrypted)

복호화에 실패하면 수신자는 반드시 “bad_record_mac” 경고로 연결을 종료해야 한다(MUST).

TLS 1.3에서 쓰는 AEAD 알고리즘은 255옥텟보다 큰 확장분을 만들어서는 안 된다(MUST NOT). 피어에게서 TLSCiphertext.length가 2^14 + 256옥텟보다 큰 레코드를 받은 엔드포인트는 반드시 “record_overflow” 경고로 연결을 종료해야 한다(MUST). 이 한도는 TLSInnerPlaintext의 최대 길이 2^14옥텟에 ContentType 1옥텟과 최대 AEAD 확장분 255옥텟을 더한 것이다.

5.3. 레코드별 논스

읽기와 쓰기에 대해 64비트 순서 번호를 따로 유지한다. 해당하는 순서 번호는 레코드를 하나 읽거나 쓴 뒤에 1씩 증가한다. 각 순서 번호는 연결이 시작될 때와 키가 바뀔 때마다 0으로 설정된다. 특정 트래픽 키로 전송하는 첫 레코드는 반드시 순서 번호 0을 사용해야 한다(MUST).

순서 번호의 크기가 64비트이므로 순서 번호가 한 바퀴 돌 일은 없을 것이다. TLS 구현이 순서 번호를 한 바퀴 돌려야 하는 상황이 되면, 반드시 키를 다시 설정하거나(4.7.3절) 연결을 종료해야 한다(MUST).

각 AEAD 알고리즘은 레코드별 논스로 가능한 길이의 범위를 N_MIN바이트부터 N_MAX바이트까지로 지정한다[RFC5116]. TLS 레코드별 논스의 길이(iv_length)는 8바이트와 그 AEAD 알고리즘의 N_MIN 중 큰 값으로 정한다([RFC5116]의 4절 참고). N_MAX가 8바이트보다 작은 AEAD 알고리즘은 TLS에 사용해서는 안 된다(MUST NOT). AEAD 구성에 쓰는 레코드별 논스는 다음과 같이 만든다.

  1. 64비트 레코드 순서 번호를 네트워크 바이트 순서로 인코딩하고, 왼쪽을 0으로 채워 iv_length에 맞춘다.

  2. 채운 순서 번호를 (역할에 따라) 고정된 client_write_iv 또는 server_write_iv와 XOR한다.

그 결과로 나온 (길이가 iv_length인) 값을 레코드별 논스로 쓴다.

참고: 이는 일부가 명시적인 논스를 규정한 TLS 1.2의 구성과 다르다.

5.4. 레코드 패딩

암호화된 TLS 레코드는 모두 패딩을 넣어 TLSCiphertext의 크기를 부풀릴 수 있다. 이렇게 하면 송신자는 관찰자에게 트래픽의 크기를 숨길 수 있다.

TLSCiphertext 레코드를 생성할 때 구현은 패딩을 넣기로 할 수 있다(MAY). 패딩이 없는 레코드는 패딩 길이가 0인 레코드일 뿐이다. 패딩은 암호화 전에 ContentType 필드 뒤에 덧붙이는 0 값 바이트의 열이다. 구현은 암호화하기 전에 패딩 옥텟을 반드시 모두 0으로 설정해야 한다(MUST).

송신자가 원하면 애플리케이션 데이터 레코드에는 길이가 0인 TLSInnerPlaintext.content가 들어 있을 수 있다. 이를 이용하면 활동이 있는지 없는지가 민감할 수 있는 상황에서 그럴듯한 크기의 위장 트래픽(cover traffic)을 만들 수 있다. 구현은 TLSInnerPlaintext.content의 길이가 0인 handshake 레코드와 alert 레코드를 보내서는 안 된다(MUST NOT). 그런 메시지를 받으면, 받은 구현은 반드시 “unexpected_message” 경고로 연결을 종료해야 한다(MUST).

보낸 패딩은 레코드 보호 메커니즘이 자동으로 검증한다. TLSCiphertext.encrypted_record를 성공적으로 복호화하면, 받은 구현은 0이 아닌 옥텟을 찾을 때까지 필드를 끝에서 앞으로 훑는다. 이 0이 아닌 옥텟이 메시지의 콘텐츠 유형이다. 이 패딩 방식을 택한 이유는 새 콘텐츠 유형을 도입하지 않고도 암호화된 TLS 레코드를 임의 크기만큼(0부터 TLS 레코드 크기 한도까지) 패딩할 수 있기 때문이다. 이 설계는 또한 패딩 옥텟이 모두 0이도록 강제하므로 패딩 오류를 빨리 감지할 수 있다.

구현은 반드시 AEAD 복호화가 돌려준 평문 안에서만 훑어야 한다(MUST). 받은 구현이 평문에서 0이 아닌 옥텟을 찾지 못하면 반드시 “unexpected_message” 경고로 연결을 종료해야 한다(MUST).

패딩이 있어도 전체 레코드 크기 한도는 바뀌지 않는다. 인코딩된 TLSInnerPlaintext 전체는 2^14 + 1옥텟을 넘어서는 안 된다(MUST NOT). 최대 조각 길이를 줄이면(예: [RFC8449]의 record_size_limit 확장으로), 줄어든 한도는 콘텐츠 유형과 패딩을 포함한 평문 전체에 적용된다.

언제 얼마나 패딩할지 정하는 패딩 정책을 고르는 것은 복잡한 주제이며 이 명세의 범위를 벗어난다. TLS 위의 애플리케이션 계층 프로토콜에 자체 패딩이 있다면, 애플리케이션 데이터 TLS 레코드는 애플리케이션 계층에서 패딩하는 편이 나을 수 있다. 다만 암호화된 handshake 레코드나 alert 레코드의 패딩은 여전히 TLS 계층에서 처리해야 한다. 이후 문서에서 패딩 선택 알고리즘을 정의하거나, TLS 확장 또는 다른 방법으로 패딩 정책 요청 메커니즘을 정의할 수 있다.

5.5. 키 사용 한도

주어진 키 묶음으로 안전하게 암호화할 수 있는 평문의 양에는 암호학적 한도가 있다. [AEAD-LIMITS]는 기반 프리미티브(AES 또는 ChaCha20)에 약점이 없다는 가정 아래 이 한도를 분석한다. 구현은 이 한도에 이르기 전에 반드시 연결을 닫거나 4.7.3절에 설명된 키 갱신을 해야 한다(MUST). 조기 데이터에 대해서는 KeyUpdate를 할 수 없다는 점에 유의하라. 따라서 구현은 조기 데이터를 보낼 때 이 한도를 넘어서는 안 된다(MUST NOT). 앞으로의 분석으로 값이 갱신될 수 있으므로, 수신하는 구현은 이 한도를 강제하지 않아야 한다(SHOULD NOT).

AES-GCM의 경우, 인증된 암호화(Authenticated Encryption, AE) 보안에 대해 약 2^-57의 안전 여유를 유지하면서 주어진 키 묶음으로 최대 크기 레코드를 2^24.5개(약 2,400만 개)까지 암호화할 수 있다. ChaCha20/Poly1305의 경우, 안전 한도에 이르기 전에 레코드 순서 번호가 먼저 한 바퀴 돌게 된다.

6. 경고 프로토콜

TLS는 종료 정보와 오류를 나타내는 alert 콘텐츠 유형을 제공한다. 다른 메시지와 마찬가지로 경고 메시지는 현재 연결 상태가 규정하는 대로 암호화된다.

경고 메시지는 경고의 설명(description)과, 이전 버전의 TLS에서 메시지의 심각도 수준을 전달하던 레거시 필드를 담는다. 경고는 종료 경고와 오류 경고 두 부류로 나뉜다. TLS 1.3에서 심각도는 보내는 경고의 유형에 함축되어 있으므로, “level” 필드는 무시해도 안전하다. “close_notify” 경고는 연결의 한 방향을 질서 있게 종료(orderly closure)함을 나타내는 데 쓰인다. 이 경고를 받으면 TLS 구현은 애플리케이션에 데이터의 끝을 알려야 한다(SHOULD).

오류 경고는 연결의 비정상 종료(abortive closure)를 나타낸다(6.2절 참고). 오류 경고를 받으면 TLS 구현은 애플리케이션에 오류를 알려야 하며(SHOULD), 그 연결에서 더 이상의 데이터를 보내거나 받도록 허용해서는 안 된다(MUST NOT). 서버와 클라이언트는 실패한 연결에서 수립한 비밀값과 키를 반드시 잊어야 한다(MUST). 단, 세션 티켓에 연관된 PSK는 예외로, 가능하면 폐기해야 한다(SHOULD).

6.2절에 나열한 모든 경고는 반드시 AlertLevel=fatal로 보내야 하며(MUST), 받았을 때는 메시지의 AlertLevel과 관계없이 반드시 오류 경고로 다뤄야 한다(MUST). 알 수 없는 경고 유형은 반드시 오류 경고로 다뤄야 한다(MUST).

참고: TLS는 메시지를 파싱하지 못했을 때 쓰는 일반 경고 두 가지를 정의한다(6장 참고). 문법대로 파싱할 수 없는 메시지(예: 길이가 메시지 경계를 넘어서거나 범위를 벗어난 길이를 담은 메시지)를 받은 피어는 반드시 “decode_error” 경고로 연결을 종료해야 한다(MUST). 문법상 올바르지만 의미상 유효하지 않은 메시지(예: p - 1인 DHE 공유값, 유효하지 않은 열거형 값)를 받은 피어는 반드시 “illegal_parameter” 경고로 연결을 종료해야 한다(MUST).

      enum { warning(1), fatal(2), (255) } AlertLevel;

      enum {
          close_notify(0),
          unexpected_message(10),
          bad_record_mac(20),
          record_overflow(22),
          handshake_failure(40),
          bad_certificate(42),
          unsupported_certificate(43),
          certificate_revoked(44),
          certificate_expired(45),
          certificate_unknown(46),
          illegal_parameter(47),
          unknown_ca(48),
          access_denied(49),
          decode_error(50),
          decrypt_error(51),
          protocol_version(70),
          insufficient_security(71),
          internal_error(80),
          inappropriate_fallback(86),
          user_canceled(90),
          missing_extension(109),
          unsupported_extension(110),
          unrecognized_name(112),
          bad_certificate_status_response(113),
          unknown_psk_identity(115),
          certificate_required(116),
          general_error(117),
          no_application_protocol(120),
          (255)
      } AlertDescription;

      struct {
          AlertLevel level;
          AlertDescription description;
      } Alert;

6.1. 종료 경고

클라이언트와 서버는 절단 공격(truncation attack)을 피하기 위해 연결이 끝나고 있다는 사실을 서로 알고 있어야 한다.

어느 쪽이든 “close_notify” 경고를 보내 자신의 쓰기 쪽 종료를 시작할 수 있다(MAY). “close_notify”보다 먼저 전송 계층 수준의 종료를 받으면, 수신자는 보낸 데이터를 모두 받았는지 알 수 없다.

각 당사자는 이미 어떤 오류 경고를 보낸 경우가 아니면, 연결의 자기 쓰기 쪽을 닫기 전에 반드시 “close_notify” 경고를 보내야 한다(MUST). 이는 그 연결의 읽기 쪽에는 아무 영향도 주지 않는다. TLS 1.3 이전 버전의 TLS와 달라진 점에 유의하라. 이전 버전에서 구현은 “close_notify”를 받으면 대기 중인 쓰기를 버리고 즉시 자신의 “close_notify” 경고를 보내야 했다. 이 이전 요구 사항은 읽기 쪽의 절단을 일으킬 수 있었다. 어느 쪽도 연결의 읽기 쪽을 닫기 전에 “close_notify” 경고를 받을 때까지 기다릴 필요는 없다. 다만 기다리지 않고 닫으면 절단이 일어날 가능성이 생긴다.

애플리케이션 프로토콜은 “close_notify”를 받으면 송신 버퍼에 남은 데이터를 모두 내보낸(flush) 뒤 즉시 “close_notify”를 보내기로 할 수 있다(MAY). 그러나 이렇게 하면 공격자가 “close_notify”를 지연시키거나 애플리케이션 패킷의 전송 계층 수준 전달을 지연시켜, 피어가 받는 데이터에 영향을 줄 수 있다. 이 문제는 애플리케이션 계층에서, 예를 들어 “close_notify”를 보낸 뒤에 받은 패킷을 무시하는 방식으로 해결할 수 있다.

TLS를 사용하는 애플리케이션 프로토콜이 TLS 연결이 닫힌 뒤에도 기반 전송 계층으로 데이터를 전달할 수 있게 한다면, TLS 구현은 애플리케이션 계층에 데이터의 끝을 알리기 전에 반드시 “close_notify” 경고를 받아야 한다(MUST). 이 표준은 연결을 언제 열고 닫는지를 포함해 TLS 사용 프로파일(usage profile)이 데이터 전송을 관리하는 방식을 규정하지 않는다.

참고: 연결의 쓰기 쪽을 닫으면 전송 계층을 없애기 전에 대기 중인 데이터가 확실히 전달된다고 가정한다.

6.2. 오류 경고

TLS의 오류 처리는 매우 단순하다. 오류가 감지되면, 감지한 쪽이 피어에게 메시지를 보낸다. 치명적 경고 메시지를 보내거나 받으면 양쪽 모두 반드시 즉시 연결을 닫아야 한다(MUST).

치명적 오류 상황을 만나면 구현은 적절한 치명적 경고를 보내야 하며(SHOULD), 추가 데이터를 보내거나 받지 않고 반드시 연결을 닫아야 한다(MUST). 이 명세 전체에서 “연결을 종료한다(terminate the connection)”와 “핸드셰이크를 중단한다(abort the handshake)”라는 표현을 특정 경고 없이 쓸 때는, 구현이 아래 설명에 나타난 경고를 보내야 한다(SHOULD)는 뜻이다. “X 경고로 연결을 종료한다”와 “X 경고로 핸드셰이크를 중단한다”라는 표현은, 구현이 경고를 보낸다면 반드시 경고 X를 보내야 한다(MUST)는 뜻이다. 이 절에서 아래에 정의한 모든 경고와 알 수 없는 모든 경고는 TLS 1.3부터 예외 없이 치명적인 것으로 간주한다(6장 참고). 구현은 경고를 보내고 받는 것을 로그로 남기기 쉽게 하는 방법을 제공해야 한다(SHOULD).

다음 오류 경고가 정의되어 있다.

새 경고 값은 11장에 설명된 대로 IANA가 할당한다.

7. 암호 계산

TLS 핸드셰이크는 하나 이상의 입력 비밀값을 수립하며, 아래에 자세히 설명하듯 이 비밀값들을 결합해 실제로 쓰는 키 재료를 만든다. 키 유도 과정에는 입력 비밀값과 핸드셰이크 트랜스크립트가 모두 들어간다. 핸드셰이크 트랜스크립트에는 Hello 메시지의 무작위 값이 들어 있으므로, 같은 PSK를 여러 연결에 쓰는 경우처럼 같은 입력 비밀값을 쓰더라도 핸드셰이크마다 트래픽 비밀값이 다르다는 점에 유의하라.

7.1. 키 스케줄

키 유도 과정은 HKDF [RFC5869]에 정의된 HKDF-Extract와 HKDF-Expand 함수, 그리고 아래에 정의한 함수를 사용한다.

       HKDF-Expand-Label(Secret, Label, Context, Length) =
            HKDF-Expand(Secret, HkdfLabel, Length)

여기서 HkdfLabel은 다음과 같이 규정한다.

       struct {
           uint16 length = Length;
           opaque label<7..255> = "tls13 " + Label;
           opaque context<0..255> = Context;
       } HkdfLabel;

       Derive-Secret(Secret, Label, Messages) =
            HKDF-Expand-Label(Secret, Label,
                              Transcript-Hash(Messages), Hash.length)

Transcript-Hash와 HKDF가 쓰는 Hash 함수는 암호 스위트의 해시 알고리즘이다. Hash.length는 그 출력 길이(바이트 단위)다. Messages는 지정된 핸드셰이크 메시지를 이어 붙인 것으로, 핸드셰이크 메시지 유형과 길이 필드는 포함하지만 레코드 계층 헤더는 포함하지 않는다. 일부 경우에는 길이가 0인 Context(““로 표시)를 HKDF-Expand-Label에 넘긴다는 점에 유의하라. 이 문서에 규정한 레이블은 모두 ASCII 문자열이며 끝에 NUL 바이트를 포함하지 않는다.

“HKDF-Expand-Label”을 쓰는 TLS 확장 명세는 함께 쓰이는 TLS 버전에 연관된 HkdfLabel 정의를 사용한다. 이 명세와 함께 쓰일 때는 위에 정의한 HkdfLabel을 쓴다는 뜻이고, DTLS [RFC9147]와 함께 쓰일 때는 [RFC9147]의 5.9절에 정의된 버전을 쓴다는 뜻이다.

참고: 흔히 쓰는 해시 함수에서는 12자보다 긴 레이블을 계산하려면 해시 함수를 한 번 더 반복해야 한다. 이 명세의 레이블은 모두 이 한도 안에 들어가도록 골랐다.

키는 HKDF-Extract와 Derive-Secret 함수를 사용해 두 입력 비밀값에서 유도한다. 새 비밀값을 더하는 일반적인 패턴은 HKDF-Extract를 사용하되, Salt 인수로 현재 비밀값 상태를, 입력 키 재료(Input Keying Material, IKM)로 더할 새 비밀값을 쓰는 것이다. 이 버전의 TLS 1.3에서 두 입력 비밀값은 다음과 같다.

이로부터 아래 그림(그림 5)에 보인 키 스케줄이 만들어진다. 이 그림에는 다음 표기 규칙이 적용된다.

참고: 이 값들을 “main” 비밀값이라고 부르는데도 키 유도 레이블에는 문자열 “master”를 쓴다. 이 불일치는 호환성을 유지하면서 값의 이름을 바꾼 결과다.

참고: 이 그림은 별도의 AEAD 키와 IV 키 같은 리프 키는 보여 주지 않고, 핸드셰이크에서 유도하는 첫 비밀값 묶음을 보여 준다.

                    0
                    |
                    v
        PSK -->  HKDF-Extract = Early Secret
                    |
                    +-----> Derive-Secret(.,
                    |                     "ext binder" |
                    |                     "res binder",
                    |                     "")
                    |              = binder_key
                    |
                    +-----> Derive-Secret(., "c e traffic",
                    |                     ClientHello)
                    |              = client_early_traffic_secret
                    |
                    +-----> Derive-Secret(., "e exp master",
                    |                     ClientHello)
                    |              = early_exporter_secret
                    v
               Derive-Secret(., "derived", "")
                    |
     asymmetric     v
      shared --> HKDF-Extract = Handshake Secret
      secret        |
                    +-----> Derive-Secret(., "c hs traffic",
                    |                     ClientHello...ServerHello)
                    |              = client_handshake_traffic_secret
                    |
                    +-----> Derive-Secret(., "s hs traffic",
                    |                     ClientHello...ServerHello)
                    |              = server_handshake_traffic_secret
                    v
               Derive-Secret(., "derived", "")
                    |
                    v
           0 --> HKDF-Extract = Main Secret
                    |
                    +-----> Derive-Secret(., "c ap traffic",
                    |                     ClientHello...server Finished)
                    |              = client_application_traffic_secret_0
                    |
                    +-----> Derive-Secret(., "s ap traffic",
                    |                     ClientHello...server Finished)
                    |              = server_application_traffic_secret_0
                    |
                    +-----> Derive-Secret(., "exp master",
                    |                     ClientHello...server Finished)
                    |              = exporter_secret
                    |
                    +-----> Derive-Secret(., "res master",
                                          ClientHello...client Finished)
                                   = resumption_secret

그림 5: TLS 1.3 주 키 스케줄

역주: 그림의 “asymmetric shared secret”은 비대칭 공유 비밀값이다.

여기서 일반적인 패턴은, 그림 왼쪽을 따라 세로로 늘어선 비밀값은 컨텍스트가 없는 원시 엔트로피일 뿐이고, 오른쪽을 따라 늘어선 비밀값은 핸드셰이크 컨텍스트를 포함하므로 추가 컨텍스트 없이 실제로 쓰는 키를 유도하는 데 쓸 수 있다는 것이다. 같은 비밀값이라도 Derive-Secret 호출마다 Messages 인수가 다를 수 있다는 점에 유의하라. 0-RTT 교환에서는 Derive-Secret을 서로 다른 트랜스크립트 네 개로 호출하고, 1-RTT만 쓰는 교환에서는 서로 다른 트랜스크립트 세 개로 호출한다.

어떤 비밀값을 쓸 수 없으면, 0으로 설정한 Hash.length 바이트의 문자열인 0 값을 쓴다. 이는 단계를 건너뛴다는 뜻이 아니므로, PSK를 쓰지 않더라도 Early Secret은 여전히 HKDF-Extract(0, 0)이라는 점에 유의하라. binder_key를 계산할 때 레이블은 외부 PSK(TLS 밖에서 프로비저닝한 것)에는 “ext binder”, 재개 PSK(이전 핸드셰이크의 재개 비밀값으로 프로비저닝한 것)에는 “res binder”를 쓴다. 레이블이 달라서 한 유형의 PSK를 다른 유형으로 바꿔치기하지 못한다.

서버가 최종적으로 어느 PSK를 선택하는지에 따라 Early Secret 값이 여러 개일 수 있다. 클라이언트는 가능한 PSK마다 하나씩 계산할 필요가 있으며, PSK가 선택되지 않으면 0 PSK에 대응하는 Early Secret을 계산할 필요가 있다.

주어진 비밀값에서 유도할 값을 모두 계산하고 나면, 그 비밀값은 지워야 한다(SHOULD).

7.2. 트래픽 비밀값 갱신

핸드셰이크가 끝나면 어느 쪽이든 4.7.3절에 정의된 KeyUpdate 핸드셰이크 메시지를 사용해 자신의 송신 트래픽 키를 갱신할 수 있다. 다음 세대의 트래픽 키는 이 절에 설명된 대로 client_/server_application_traffic_secret_N에서 client_/server_application_traffic_secret_N+1을 생성한 다음, 7.3절에 설명된 대로 트래픽 키를 다시 유도해 계산한다.

다음 세대의 application_traffic_secret은 다음과 같이 계산한다.

       application_traffic_secret_N+1 =
           HKDF-Expand-Label(application_traffic_secret_N,
                             "traffic upd", "", Hash.length)

client_/server_application_traffic_secret_N+1과 그에 연관된 트래픽 키를 계산하고 나면, 구현은 client_/server_application_traffic_secret_N과 그에 연관된 트래픽 키를 삭제해야 한다(SHOULD).

7.3. 트래픽 키 계산

트래픽 키 재료는 다음 입력 값으로 생성한다.

트래픽 키 재료는 입력 트래픽 비밀값으로부터 다음을 사용해 생성한다.

   [sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
   [sender]_write_iv  = HKDF-Expand-Label(Secret, "iv", "", iv_length)

[sender]는 보내는 쪽을 나타낸다. 데이터 범주별 Secret 값은 아래 표와 같다.

데이터 유형 Secret
0-RTT 애플리케이션 데이터와 EndOfEarlyData client_early_traffic_secret
처음 핸드셰이크 [sender]_handshake_traffic_secret
핸드셰이크 후 메시지와 애플리케이션 데이터 [sender]_application_traffic_secret_N

표 3: 트래픽 키용 비밀값

경고는 그때의 송신 키로 보낸다(그런 키가 아직 수립되지 않았으면 평문으로 보낸다). 기반 Secret이 바뀔 때마다(예: 핸드셰이크 키에서 애플리케이션 데이터 키로 바뀔 때나 키 갱신 때) 모든 트래픽 키 재료를 다시 계산한다.

7.4. 비대칭 공유 비밀값 계산

7.4.1. 유한체 Diffie-Hellman

유한체 그룹에서는 일반적인 Diffie-Hellman [KEYAGREEMENT] 계산을 수행한다. 협상된 키(Z)는 빅 엔디언 형식으로 인코딩해 바이트 문자열로 바꾸고, 소수의 크기까지 왼쪽을 0으로 채운다. 이 바이트 문자열을 위에 규정한 키 스케줄의 공유 비밀값으로 쓴다.

이 구성은 앞쪽의 0을 제거하던 이전 버전의 TLS와 다르다는 점에 유의하라.

7.4.2. 타원 곡선 Diffie-Hellman

secp256r1, secp384r1, secp521r1에서 ECDH 계산(키 생성과 공유 비밀값 계산 포함)은 Elliptic Curve Cryptography Cofactor Diffie-Hellman Primitive를 사용해 [KEYAGREEMENT]의 5.6.1.2절과 5.7.1.2절에 따라 수행한다. 공유 비밀값 Z는 ECDH 공유 비밀값인 타원 곡선 점의 x 좌표를 옥텟 문자열로 표현한 것이다. [KEYAGREEMENT]의 부록 C.2에 규정된 Field-Element-to-Byte String Conversion이 출력하는 옥텟 문자열 Z는 주어진 체에 대해 길이가 일정하다는 점에 유의하라. 이 옥텟 문자열의 앞쪽에 있는 0은 잘라 내서는 안 된다(MUST NOT). 공개 키 검증에 관한 요구 사항은 4.3.8.2절을 참고한다.

X25519와 X448에서 ECDH 계산은 다음과 같다.

이 곡선들에서 구현은 Diffie-Hellman 공유 비밀값을 계산하는 데 [RFC7748]에 규정된 방법을 써야 한다(SHOULD). 구현은 [RFC7748]의 6절에 설명된 대로, 계산한 Diffie-Hellman 공유 비밀값이 모두 0인 값인지 반드시 확인해야 하며, 그렇다면 중단해야 한다(MUST). 구현자가 이 타원 곡선의 다른 구현을 쓴다면, [RFC7748]의 7절에 규정된 추가 확인을 수행해야 한다(SHOULD).

7.5. 익스포터

[RFC5705]는 TLS 의사 난수 함수(pseudorandom function, PRF)를 기반으로 TLS의 키 재료 익스포터를 정의한다. 이 문서는 PRF를 HKDF로 바꾸므로 새 구성이 필요하다. 익스포터 인터페이스는 그대로다.

익스포터 값은 다음과 같이 계산한다.

   TLS-Exporter(label, context_value, key_length) =
       HKDF-Expand-Label(Derive-Secret(Secret, label, ""),
                         "exporter", Hash(context_value), key_length)

여기서 Secret은 early_exporter_secret 또는 exporter_secret이다. 애플리케이션이 명시적으로 지정하지 않는 한, 구현은 반드시 exporter_secret을 사용해야 한다(MUST). early_exporter_secret은 0-RTT 데이터에 익스포터가 필요한 상황에서 쓰도록 정의되어 있다. 조기 익스포터에는 별도의 인터페이스를 둘 것을 권고한다(RECOMMENDED). 그러면 익스포터 사용자가 일반 익스포터를 원할 때 실수로 조기 익스포터를 쓰거나 그 반대가 되는 일을 피할 수 있다.

컨텍스트를 제공하지 않으면 context_value의 길이는 0이다. 따라서 컨텍스트를 제공하지 않으면 빈 컨텍스트를 제공했을 때와 같은 값이 계산된다. 이는 빈 컨텍스트와 컨텍스트가 없는 경우가 서로 다른 출력을 내던 이전 버전의 TLS와 달라진 점이다. 이 문서의 발행 시점에, 할당된 익스포터 레이블 가운데 컨텍스트가 있는 경우와 없는 경우 모두에 쓰이는 것은 없다. 향후 명세는 같은 레이블에서 빈 컨텍스트와 컨텍스트 없음을 모두 허용하는 익스포터 사용을 정의해서는 안 된다(MUST NOT). 익스포터를 새로 쓰는 경우에는 값이 비어 있을 수는 있더라도 모든 익스포터 계산에 컨텍스트를 제공해야 한다(SHOULD).

익스포터 레이블의 형식에 관한 요구 사항은 [RFC5705]의 4절에 정의되어 있다.

8. 0-RTT와 재전송 방지

2.3절과 부록 F.5에서 언급했듯이, TLS는 0-RTT 데이터에 대해 자체적인 재전송 방지 보호를 제공하지 않는다. 염려해야 할 잠재적 위협은 두 가지다.

첫 번째 부류의 공격은 0-RTT 데이터를 최대 한 번만 받아들이도록 보장하는 상태를 공유해 막을 수 있다. 서버는 이 절에 설명한 방법 중 하나나 그에 준하는 수단을 구현해 그 수준의 재전송 안전성을 제공해야 한다(SHOULD). 그러나 운영상의 이유로 모든 배포가 그 수준의 상태를 유지하지는 않을 것으로 이해된다. 따라서 일반적인 운영에서 클라이언트는 서버가 이 메커니즘 중 어느 것을 실제로 구현하는지(구현하는 것이 있다면) 알 수 없다. 그러므로 클라이언트는 반드시 재전송되어도 안전하다고 판단하는 조기 데이터만 보내야 한다(MUST).

재전송의 직접적인 영향 외에도, 보통은 멱등(idempotent)하다고 여겨지는 연산마저 대량의 재전송으로 악용할 수 있는 부류의 공격이 있다(부록 F.5에 설명된 타이밍 공격, 자원 한도 고갈 등). 이런 공격은 모든 0-RTT 페이로드가 제한된 횟수만 재전송될 수 있도록 보장해 완화할 수 있다. 서버는 자신의 어떤 인스턴스(기계든, 스레드든, 관련 서비스 기반 시설 안의 다른 어떤 개체든)도 같은 0-RTT 핸드셰이크에 대해 0-RTT를 최대 한 번만 받아들이도록 반드시 보장해야 한다(MUST). 이렇게 하면 재전송 횟수가 배포에 있는 서버 인스턴스의 수로 제한된다. 이런 보장은 최근 받은 ClientHello의 데이터를 로컬에 기록하고 반복된 것을 거절하는 방법이나, 같거나 더 강한 보장을 제공하는 다른 어떤 방법으로든 이룰 수 있다. “서버 인스턴스마다 최대 한 번” 보장은 최소 요구 사항이다. 서버는 가능하면 0-RTT 재전송을 더 제한해야 한다(SHOULD).

두 번째 부류의 공격은 TLS 계층에서 막을 수 없으며, 모든 애플리케이션이 반드시 처리해야 한다(MUST)(예를 들어 0-RTT 데이터와 함께 HTTP를 쓰는 방법에 관한 지침을 제공하는 [RFC8470] 참고). 클라이언트가 어떤 종류든 재시도 동작을 구현하는 애플리케이션은 서로 다른 TLS 연결로 도착하는 중복된 애플리케이션 계층 요청을 올바르게 처리하는 것을 포함해, 이미 어떤 형태의 재전송 방지 대책을 구현할 필요가 있다는 점에 유의하라.

8.1. 일회용 티켓

가장 단순한 형태의 재전송 방지 대책은 서버가 각 세션 티켓을 한 번만 쓰도록 허용하는 것이다. 예를 들어 서버는 아직 남아 있는 유효한 티켓 전부의 데이터베이스를 유지하고, 티켓이 쓰일 때마다 데이터베이스에서 삭제할 수 있다. 알 수 없는 티켓이 제공되면 서버는 전체 핸드셰이크로 돌아간다.

티켓이 자체 완결적이지 않고 데이터베이스 키이며, 대응하는 PSK를 사용할 때 삭제한다면, PSK로 수립한 연결은 재전송 방지 보호뿐 아니라, 데이터베이스 항목에 있던 PSK의 사본이 모두 삭제되고 나면 순방향 비밀성도 누린다. 이 메커니즘은 PSK를 비대칭 키 교환 없이 쓸 때의 PSK 사용 보안도 높인다.

이 메커니즘은 여러 서버가 분산된 환경에서 서버 노드 사이에 세션 데이터베이스를 공유해야 하므로, 자체 암호화 티켓과 비교하면 PSK 0-RTT 연결의 성공률을 높이기 어려울 수 있다. 세션 데이터베이스와 달리 세션 티켓은 일관된 저장소 없이도 PSK 기반 세션 수립을 성공적으로 할 수 있다. 다만 0-RTT를 허용하는 경우에는, 다음 절에 자세히 설명하듯 0-RTT 데이터의 재전송 방지를 위해 여전히 일관된 저장소가 필요하다.

8.2. Client Hello 기록

다른 형태의 재전송 방지는 ClientHello에서 유도한 고유한 값(일반적으로 무작위 값이나 PSK 바인더)을 기록하고 중복을 거절하는 것이다. 모든 ClientHello를 기록하면 상태가 한없이 늘어나지만, 서버는 그 대신 주어진 시간 창(time window) 안의 ClientHello만 기록하고, “obfuscated_ticket_age”를 사용해 티켓이 그 창 밖에서 재사용되지 않도록 보장할 수 있다.

이를 구현하려면, ClientHello를 받았을 때 서버는 먼저 4.3.11절에 설명된 대로 PSK 바인더를 검증한다. 이어서 다음 절에 설명된 대로 expected_arrival_time을 계산하고, 그 값이 기록 창 밖에 있으면 0-RTT를 거절하고 1-RTT 핸드셰이크로 돌아간다.

expected_arrival_time이 창 안에 있으면, 서버는 일치하는 ClientHello를 기록해 두었는지 확인한다. 찾으면 “illegal_parameter” 경고로 핸드셰이크를 중단하거나, PSK는 받아들이되 0-RTT는 거절한다. 일치하는 ClientHello를 찾지 못하면 0-RTT를 받아들이고, expected_arrival_time이 창 안에 있는 동안 그 ClientHello를 저장한다. 서버는 블룸 필터(Bloom filter)처럼 거짓 양성(false positive)이 있는 데이터 저장소를 구현할 수도 있으며(MAY), 이 경우 재전송으로 보이는 것에는 반드시 0-RTT를 거절하는 것으로 대응해야 하며(MUST), 핸드셰이크를 중단해서는 안 된다(MUST NOT).

서버는 반드시 ClientHello의 검증된 부분에서만 저장 키를 유도해야 한다(MUST). ClientHello에 PSK 식별자가 여러 개 들어 있으면, 서버가 (4.3.11절의 권고대로) 덜 선호하는 식별자는 검증하지 않으리라는 가정 아래, 공격자는 그 식별자의 바인더 값만 다른 ClientHello를 여러 개 만들 수 있다. 즉, 클라이언트가 PSK A와 B를 보냈는데 서버가 A를 선호한다면, 공격자는 A의 바인더에 영향을 주지 않고 B의 바인더를 바꿀 수 있다. B의 바인더가 저장 키의 일부라면, 이 ClientHello는 중복으로 보이지 않아 받아들여지고, 재전송 캐시 오염 같은 부작용을 일으킬 수 있다. 다만 0-RTT 데이터는 다른 키를 쓰므로 복호화할 수 없다. 검증된 바인더나 ClientHello.random을 저장 키로 쓰면 이 공격은 불가능하다.

이 메커니즘은 아직 남아 있는 티켓을 모두 저장할 필요가 없으므로, 재개와 0-RTT 비율이 높은 분산 시스템에서 구현하기 더 쉬울 수 있다. 대신 받은 ClientHello 메시지를 확실하게 저장하고 찾아오기 어려워 재전송 방지가 더 약해질 수 있다. 그런 시스템 상당수에서는 받은 ClientHello 전부를 전역적으로 일관되게 저장하는 것이 비현실적이다. 이 경우 가장 좋은 재전송 방지 보호는 주어진 티켓에 대해 저장 구역(zone) 하나만 권한을 갖게 하고, 다른 구역에서는 그 티켓의 0-RTT를 거절하는 것이다. 이 방법은 하나의 구역만 0-RTT 데이터를 받아들이므로 공격자의 단순한 재전송을 막는다. 더 약한 설계는 구역마다 저장소를 따로 두되 어느 구역에서든 0-RTT를 허용하는 것이다. 이 방법은 재전송 횟수를 구역마다 한 번으로 제한한다. 물론 어느 설계에서든 애플리케이션 메시지 중복은 여전히 가능하다.

구현이 새로 시작되었을 때는, 기록 창의 어느 부분이든 시작 시점과 겹치는 동안 0-RTT를 거절해야 한다(SHOULD). 그렇지 않으면 원래 그 기간에 보내진 메시지의 재전송을 받아들일 위험이 있다.

참고: 클라이언트의 시계가 서버의 시계보다 훨씬 빠르게 가면, 미래 쪽으로 창을 벗어난 ClientHello를 받을 수 있다. 이 경우 그 ClientHello는 1-RTT로 받아들여져 클라이언트의 재시도를 일으키고, 나중에는 0-RTT로 받아들여질 수 있다. 이는 8장에 설명한 두 번째 형태의 공격의 또 다른 변형이다.

8.3. 신선도 검사

ClientHello에는 클라이언트가 보낸 시각이 나타나므로, ClientHello가 적당히 최근에 보내졌을 가능성이 높은지를 효율적으로 판단해, 그런 ClientHello에 대해서만 0-RTT를 받아들이고 그 밖에는 1-RTT 핸드셰이크로 돌아갈 수 있다. 이는 8.2절에 설명한 ClientHello 저장 메커니즘에 필요하다. 그렇지 않으면 서버가 무한한 수의 ClientHello를 저장해야 하기 때문이다. 또한 자체 완결적인 일회용 티켓에는 0-RTT에 쓸 수 없는 ClientHello를 효율적으로 거절할 수 있게 해 주는 유용한 최적화다.

이 메커니즘을 구현하려면, 서버는 세션 티켓을 생성한 시각에 클라이언트와 서버 사이의 왕복 시간 추정치를 더한 값을 저장할 필요가 있다. 즉, 다음과 같다.

       adjusted_creation_time = creation_time + estimated_RTT

이 값은 티켓에 인코딩할 수 있으므로, 아직 남아 있는 티켓마다 상태를 유지할 필요가 없다. 서버는 클라이언트의 “pre_shared_key” 확장에 담긴 “obfuscated_ticket_age” 매개 변수에서 티켓의 “ticket_age_add” 값을 빼서, 클라이언트가 보는 티켓 나이를 알아낼 수 있다. 서버는 ClientHello의 expected_arrival_time을 다음과 같이 알아낼 수 있다.

     expected_arrival_time = adjusted_creation_time + clients_ticket_age

새 ClientHello를 받으면 expected_arrival_time을 서버의 현재 정확한 표준시(wall clock time)와 비교하고, 둘의 차이가 일정량보다 크면 0-RTT를 거절한다. 다만 1-RTT 핸드셰이크는 끝나도록 허용할 수 있다.

expected_arrival_time과 측정한 시각이 어긋나게 만들 수 있는 잠재적 오차 원인은 여러 가지다. 클라이언트와 서버 시계 속도의 차이는 아주 작을 가능성이 높지만, 절대 시각은 크게 어긋날 수도 있다. 경과 시간의 정당한 값이 어긋나는 가장 흔한 원인은 네트워크 전파 지연이다. NewSessionTicket과 ClientHello 메시지는 모두 재전송되어 지연될 수 있으며, 이는 TCP 때문에 드러나지 않을 수도 있다. 인터넷의 클라이언트라면 이는 시계의 부정확성과 측정 편차를 고려해 10초 정도의 창이 필요하다는 뜻이다. 다른 배포 시나리오에는 다른 요구가 있을 수 있다. 시계 오차(clock skew)의 분포는 대칭이 아니므로, 최적의 절충은 허용하는 불일치 값의 범위를 비대칭으로 두는 것일 수 있다.

신선도 검사만으로는 재전송을 막기에 충분하지 않다는 점에 유의하라. 신선도 검사는 오차 창 안에서는 재전송을 감지하지 못하는데, 이 창은 대역폭과 시스템 용량에 따라 실제 환경에서 수십억 건의 재전송을 포함할 수 있다. 또한 이 신선도 검사는 ClientHello를 받을 때만 하고, 이후의 조기 애플리케이션 데이터 레코드를 받을 때는 하지 않는다. 조기 데이터가 받아들여진 뒤에는 레코드가 더 긴 기간에 걸쳐 서버로 계속 스트리밍될 수 있다.

9. 준수 요구 사항

9.1. 필수 구현 암호 스위트

달리 규정하는 애플리케이션 프로파일 표준이 없다면 다음과 같다.

9.2. 필수 구현 확장

달리 규정하는 애플리케이션 프로파일 표준이 없다면, TLS를 준수하는 애플리케이션은 반드시 다음 TLS 확장을 구현해야 한다(MUST).

모든 구현은 해당 기능을 제시할 때 반드시 이 확장들을 보내고 사용해야 한다(MUST).

본문에 0x0304가 들어 있는 “supported_versions” 확장이 ClientHello에 있으면, 클라이언트가 이 명세를 사용해 협상하려 한다고 본다. 그런 ClientHello 메시지는 반드시 다음 요구 사항을 충족해야 한다(MUST).

이 요구 사항을 충족하지 않는 ClientHello를 받은 서버는 반드시 “missing_extension” 경고로 핸드셰이크를 중단해야 한다(MUST).

또한 모든 구현은 “server_name” 확장을 쓸 수 있는 애플리케이션과 함께 그 확장을 쓰는 것을 반드시 지원해야 한다(MUST). 서버는 클라이언트가 유효한 “server_name” 확장을 보내도록 요구할 수 있다(MAY). 이 확장을 요구하는 서버는 “server_name” 확장이 없는 ClientHello에 “missing_extension” 경고로 연결을 종료해 응답해야 한다(SHOULD).

9.3. 프로토콜 불변 조건

이 절은 TLS 엔드포인트와 미들박스가 반드시 따라야 하는(MUST) 불변 조건(invariant)을 설명한다. 이는 이전 버전의 TLS에도 적용된다.

TLS는 안전하고 호환성 있게 확장할 수 있도록 설계되었다. 더 새로운 클라이언트나 서버는 더 새로운 피어와 통신할 때 가장 선호하는 공통 매개 변수를 협상하는 것이 바람직하다. TLS 핸드셰이크는 다운그레이드 방지를 제공한다. 더 새로운 클라이언트와 더 새로운 서버 사이에서 TLS를 터미네이션하지 않고 트래픽을 전달하는 미들박스는 핸드셰이크에 영향을 줄 수 없어야 마땅하다(부록 F.1 참고). 동시에 배포는 저마다 다른 속도로 갱신되므로, 더 새로운 클라이언트나 서버는 이전 매개 변수를 계속 지원할 수 있으며(MAY), 이로써 이전 엔드포인트와 상호 운용할 수 있다.

이것이 동작하려면 구현은 반드시 확장 가능한 필드를 올바르게 다뤄야 한다(MUST).

TLS 1.3의 설계는 널리 배포된, 규정을 준수하지 않는 TLS 미들박스에 제약을 받았다(부록 E.4 참고). 그러나 TLS 1.3은 불변 조건을 완화하지 않는다. 그런 미들박스는 여전히 규정을 준수하지 않는 것이다.

10. 보안 고려 사항

보안 문제는 이 메모 전체, 특히 부록 C, 부록 E, 부록 F에서 논의한다.

11. IANA 고려 사항

이 문서는 원래 [RFC4346]에서 만들어지고 [RFC8446]과 [RFC8447]에서 갱신된 여러 레지스트리를 사용한다. [RFC8446], [RFC8447], 이 문서 사이의 변경 사항은 11.1절에 설명되어 있다. IANA는 이 RFC들에 대한 참조를 이 문서에 대한 참조로 바꾸었다. 레지스트리와 그 할당 정책은 다음과 같다.

이 문서는 원래 [RFC4366]에서 만들어진 TLS ExtensionType 값(TLS ExtensionType Values) 레지스트리도 사용한다. IANA는 이 레지스트리가 이 문서를 참조하도록 갱신했다. 레지스트리의 변경 사항은 다음과 같다.

이 문서는 원래 [RFC6091]에서 만들어지고 [RFC8447]에서 갱신된 TLS 인증서 유형(TLS Certificate Types) 레지스트리의 항목 두 개를 갱신한다. IANA는 값 1의 항목을 이름 “OpenPGP_RESERVED”, “Recommended” 값 “N”, 주석 “Used in TLS versions prior to 1.3.”으로 갱신했다. IANA는 값 0의 항목을 이름 “X509”, “Recommended” 값 “Y”, 주석 “Was X.509 before TLS 1.3”으로 갱신했다.

이 문서는 원래 [RFC6961]에서 만들어진 TLS 인증서 상태 유형(TLS Certificate Status Types) 레지스트리의 항목 하나를 갱신한다. IANA는 값 2의 항목을 이름 “ocsp_multi_RESERVED”, 주석 “Used in TLS versions prior to 1.3”으로 갱신했다.

이 문서는 TLS 지원 그룹(TLS Supported Groups) 레지스트리(원래 [RFC4492]에서 다른 이름으로 만들어졌고, 지금은 [RFC8422]가 관리하며 [RFC7919]와 [RFC8447]에서 갱신됨)의 항목 두 개를 갱신한다. 값 29와 30(x25519와 x448)의 항목은 이 문서도 참조하도록 갱신되었다.

또한 이 문서는 IANA가 관리하는 새 레지스트리 두 개를 정의한다.

TLS ExtensionType 값, TLS 암호 스위트, TLS 지원 그룹, TLS 익스포터 레이블(TLS Exporter Labels), TLS HashAlgorithm, TLS 인증서 유형, TLSPskKeyExchangeMode, TLS SignatureScheme 레지스트리에는 “Recommended” 열이 있다. “Recommended” 열의 값과 설정에 관한 추가 정보는 [RFC9847]의 3절을 참고한다.

역주: “TLSPskKeyExchangeMode”는 원문 표기 그대로이며, 위의 TLS PskKeyExchangeMode 레지스트리를 가리킨다.

11.1. 이 RFC의 변경 사항

IANA는 IANA 레지스트리에 있는 [RFC8446]에 대한 모든 참조를 이 문서에 대한 참조로 갱신했다.

IANA는 TLS SignatureScheme 레지스트리와 PskKeyExchangeMode 레지스트리의 “Recommended” 열 절차를 [RFC9847]에 맞게 갱신했다.

IANA는 TLS ExtensionType 값 레지스트리의 “extended_master_secret” 값의 이름을 “extended_main_secret”으로 바꾸었다.

IANA는 TLS 경고 레지스트리에 6장에 주어진 값으로 “general_error” 경고 값을 만들었다.

IANA는 TLS ExtensionType 값 레지스트리의 status_request와 supported_groups 항목에 이 문서에 대한 참조를 추가했다.

IANA는 TLS ExtensionType 값 레지스트리에서 cached_info의 “TLS 1.3” 열 값을 “CH, EE”로 갱신했다.

12. 참고 문헌

12.1. 규범 참고 문헌

12.2. 참고용 참고 문헌

부록 A. 상태 머신

이 부록은 클라이언트와 서버 핸드셰이크에서 허용되는 상태 전이를 요약한다. 상태 이름(START처럼 모두 대문자)에는 형식적인 의미가 없으며 이해를 돕기 위해 붙인 것이다. 특정 상황에서만 하는 동작은 []로 표시한다. “K_{send,recv} = foo” 표기는 “송신/수신 키를 주어진 키로 설정한다”는 뜻이다.

역주: 그림의 “Send”는 보냄, “Recv”는 받음이다. 클라이언트 그림의 “Can send early data”는 조기 데이터를 보낼 수 있는 구간, 두 그림의 “Can send app data after here”는 여기부터 애플리케이션 데이터를 보낼 수 있음을 뜻한다. 서버 그림의 “Skip decrypt errors”는 복호화 오류를 건너뜀, “No auth”와 “Cert-based client auth”는 클라이언트 인증 없음과 인증서 기반 클라이언트 인증이다. 클라이언트 그림의 “Using PSK”와 “Using certificate”는 PSK 사용과 인증서 사용, 서버 그림의 “Select parameters”는 매개 변수 선택, “No 0-RTT”는 0-RTT 없음, “Recv empty Certificate”는 빈 Certificate 받음이다.

A.1. 클라이언트

                              START <----+
               Send ClientHello |        | Recv HelloRetryRequest
          [K_send = early data] |        |
                                v        |
           +->               WAIT_SH ----+
           |                    | Recv ServerHello
           |                    | K_recv = handshake
       Can |                    V
      send |                 WAIT_EE
     early |                    | Recv EncryptedExtensions
      data |          +---------+-------+
           |    Using |                 | Using certificate
           |      PSK |                 v
           |          |            WAIT_CERT_CR
           |          |        Recv |       | Recv CertificateRequest
           |          | Certificate |       v
           |          |             |    WAIT_CERT
           |          |             |       | Recv Certificate
           |          |             v       v
           |          |              WAIT_CV
           |          |                   | Recv CertificateVerify
           |          +-> WAIT_FINISHED <-+
           |                  | Recv Finished
           +->                | [Send EndOfEarlyData]
                              | K_send = handshake
                              | [Send Certificate [+ CertificateVerify]]
    Can send                  | Send Finished
    app data   -->            | K_send = K_recv = application
    after here                v
                          CONNECTED

위에 보인 전이에서는 클라이언트가 ServerHello 이후 메시지에서 비롯된 경고를 평문으로 또는 조기 데이터 키로 보내게 될 수 있다는 점에 유의하라. 클라이언트가 그런 경고를 보내야 한다면, 가능하면 먼저 핸드셰이크 키로 키를 다시 설정해야 한다(SHOULD).

A.2. 서버

                              START <-----+
               Recv ClientHello |         | Send HelloRetryRequest
                                v         |
                             RECVD_CH ----+
                                | Select parameters
                                v
                             NEGOTIATED
                                | Send ServerHello
                                | K_send = handshake
                                | Send EncryptedExtensions
                                | [Send CertificateRequest]
 Can send                       | [Send Certificate + CertificateVerify]
 app data                       | Send Finished
 after   -->                    | K_send = application
 here                  +--------+--------+
              No 0-RTT |                 | 0-RTT
                       |                 |
   K_recv = handshake  |                 | K_recv = early data
 [Skip decrypt errors] |    +------> WAIT_EOED --+
                       |    |       Recv |       | Recv EndOfEarlyData
                       |    | early data |       | K_recv = handshake
                       |    +------------+       |
                       |                         |
                       +-> WAIT_FLIGHT2 <--------+
                                |
                       +--------+--------+
               No auth |                 | Cert-based client auth
                       |                 |
                       |                 v
                       |             WAIT_CERT
                       |        Recv |       | Recv Certificate
                       |       empty |       v
                       | Certificate |    WAIT_CV
                       |             |       | Recv
                       |             v       | CertificateVerify
                       +-> WAIT_FINISHED <---+
                                | Recv Finished
                                | K_recv = application
                                v
                            CONNECTED

부록 B. 프로토콜 데이터 구조와 상수 값

이 부록은 규범적인 프로토콜 타입과 상수 정의를 제시한다. “_RESERVED”로 나열한 값은 이전 버전의 TLS에서 쓰였으며, 완전성을 위해 여기에 나열한다. TLS 1.3 구현은 이 값들을 보내서는 안 되지만(MUST NOT), 오래된 TLS 구현에서 받을 수도 있다.

B.1. 레코드 계층

      enum {
          invalid(0),
          change_cipher_spec(20),
          alert(21),
          handshake(22),
          application_data(23),
          (255)
      } ContentType;

      struct {
          ContentType type;
          ProtocolVersion legacy_record_version;
          uint16 length;
          opaque fragment[TLSPlaintext.length];
      } TLSPlaintext;

      struct {
          opaque content[TLSPlaintext.length];
          ContentType type;
          uint8 zeros[length_of_padding];
      } TLSInnerPlaintext;

      struct {
          ContentType opaque_type = application_data; /* 23 */
          ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
          uint16 length;
          opaque encrypted_record[TLSCiphertext.length];
      } TLSCiphertext;

B.2. 경고 메시지

      enum { warning(1), fatal(2), (255) } AlertLevel;

      enum {
          close_notify(0),
          unexpected_message(10),
          bad_record_mac(20),
          decryption_failed_RESERVED(21),
          record_overflow(22),
          decompression_failure_RESERVED(30),
          handshake_failure(40),
          no_certificate_RESERVED(41),
          bad_certificate(42),
          unsupported_certificate(43),
          certificate_revoked(44),
          certificate_expired(45),
          certificate_unknown(46),
          illegal_parameter(47),
          unknown_ca(48),
          access_denied(49),
          decode_error(50),
          decrypt_error(51),
          export_restriction_RESERVED(60),
          protocol_version(70),
          insufficient_security(71),
          internal_error(80),
          inappropriate_fallback(86),
          user_canceled(90),
          no_renegotiation_RESERVED(100),
          missing_extension(109),
          unsupported_extension(110),
          certificate_unobtainable_RESERVED(111),
          unrecognized_name(112),
          bad_certificate_status_response(113),
          bad_certificate_hash_value_RESERVED(114),
          unknown_psk_identity(115),
          certificate_required(116),
          general_error(117),
          no_application_protocol(120),
          (255)
      } AlertDescription;

      struct {
          AlertLevel level;
          AlertDescription description;
      } Alert;

B.3. 핸드셰이크 프로토콜

      enum {
          hello_request_RESERVED(0),
          client_hello(1),
          server_hello(2),
          hello_verify_request_RESERVED(3),
          new_session_ticket(4),
          end_of_early_data(5),
          hello_retry_request_RESERVED(6),
          encrypted_extensions(8),
          certificate(11),
          server_key_exchange_RESERVED(12),
          certificate_request(13),
          server_hello_done_RESERVED(14),
          certificate_verify(15),
          client_key_exchange_RESERVED(16),
          finished(20),
          certificate_url_RESERVED(21),
          certificate_status_RESERVED(22),
          supplemental_data_RESERVED(23),
          key_update(24),
          message_hash(254),
          (255)
      } HandshakeType;

      struct {
          HandshakeType msg_type;    /* 핸드셰이크 유형 */
          uint24 length;             /* 메시지의 남은 바이트 수 */
          select (Handshake.msg_type) {
              case client_hello:          ClientHello;
              case server_hello:          ServerHello;
              case end_of_early_data:     EndOfEarlyData;
              case encrypted_extensions:  EncryptedExtensions;
              case certificate_request:   CertificateRequest;
              case certificate:           Certificate;
              case certificate_verify:    CertificateVerify;
              case finished:              Finished;
              case new_session_ticket:    NewSessionTicket;
              case key_update:            KeyUpdate;
          };
      } Handshake;

B.3.1. 키 교환 메시지

    uint16 ProtocolVersion;
    opaque Random[32];

    uint8 CipherSuite[2];    /* 암호 스위트 선택자 */

    struct {
        ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
        Random random;
        opaque legacy_session_id<0..32>;
        CipherSuite cipher_suites<2..2^16-2>;
        opaque legacy_compression_methods<1..2^8-1>;
        Extension extensions<7..2^16-1>;
    } ClientHello;

    struct {
        ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
        Random random;
        opaque legacy_session_id_echo<0..32>;
        CipherSuite cipher_suite;
        uint8 legacy_compression_method = 0;
        Extension extensions<6..2^16-1>;
    } ServerHello;

    struct {
        ExtensionType extension_type;
        opaque extension_data<0..2^16-1>;
    } Extension;

    enum {
        server_name(0),                             /* RFC 6066, 9261 */
        status_request(5),                          /* RFC 6066, 9846 */
        supported_groups(10),                       /* RFC 7919, 9846 */
        signature_algorithms(13),                   /* RFC 9846 */
        use_srtp(14),                               /* RFC 5764 */
        heartbeat(15),                              /* RFC 6520 */
        application_layer_protocol_negotiation(16), /* RFC 7301 */
        client_certificate_type(19),                /* RFC 7250 */
        server_certificate_type(20),                /* RFC 7250 */
        padding(21),                                /* RFC 7685 */
        compress_certificate(27),                   /* RFC 8879 */
        record_size_limit(28),                      /* RFC 8449 */
        delegated_credential(34),                   /* RFC 9345 */
        supported_ekt_ciphers(39),                  /* RFC 8870 */
        pre_shared_key(41),                         /* RFC 9846 */
        early_data(42),                             /* RFC 9846 */
        supported_versions(43),                     /* RFC 9846 */
        cookie(44),                                 /* RFC 9846 */
        psk_key_exchange_modes(45),                 /* RFC 9846 */
        certificate_authorities(47),                /* RFC 9846 */
        oid_filters(48),                            /* RFC 9846 */
        post_handshake_auth(49),                    /* RFC 9846 */
        signature_algorithms_cert(50),              /* RFC 9846 */
        key_share(51),                              /* RFC 9846 */
        transparency_info(52),                      /* RFC 9162 */
        external_id_hash(55),                       /* RFC 8844 */
        external_session_id(56),                    /* RFC 8844 */
        quic_transport_parameters(57),              /* RFC 9001 */
        ticket_request(58),                         /* RFC 9149 */
        ech_outer_extensions(64768),                /* RFC 9849 */
        encrypted_client_hello(65037),              /* RFC 9849 */
        (65535)
    } ExtensionType;

    struct {
        NamedGroup group;
        opaque key_exchange<1..2^16-1>;
    } KeyShareEntry;

    struct {
        KeyShareEntry client_shares<0..2^16-1>;
    } KeyShareClientHello;

    struct {
        NamedGroup selected_group;
    } KeyShareHelloRetryRequest;

    struct {
        KeyShareEntry server_share;
    } KeyShareServerHello;

    struct {
        uint8 legacy_form = 4;
        opaque X[coordinate_length];
        opaque Y[coordinate_length];
    } UncompressedPointRepresentation;

    enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;

    struct {
        PskKeyExchangeMode ke_modes<1..255>;
    } PskKeyExchangeModes;

    struct {} Empty;

    struct {
        select (Handshake.msg_type) {
            case new_session_ticket:   uint32 max_early_data_size;
            case client_hello:         Empty;
            case encrypted_extensions: Empty;
        };
    } EarlyDataIndication;

    struct {
        opaque identity<1..2^16-1>;
        uint32 obfuscated_ticket_age;
    } PskIdentity;

    opaque PskBinderEntry<32..255>;

    struct {
        PskIdentity identities<7..2^16-1>;
        PskBinderEntry binders<33..2^16-1>;
    } OfferedPsks;

    struct {
        select (Handshake.msg_type) {
            case client_hello: OfferedPsks;
            case server_hello: uint16 selected_identity;
        };
    } PreSharedKeyExtension;
B.3.1.1. 버전 확장
      struct {
          select (Handshake.msg_type) {
              case client_hello:
                   ProtocolVersion versions<2..254>;

              case server_hello: /* HelloRetryRequest도 같음 */
                   ProtocolVersion selected_version;
          };
      } SupportedVersions;
B.3.1.2. 쿠키 확장
      struct {
          opaque cookie<1..2^16-1>;
      } Cookie;
B.3.1.3. 서명 알고리즘 확장
      enum {
          /* RSASSA-PKCS1-v1_5 알고리즘 */
          rsa_pkcs1_sha256(0x0401),
          rsa_pkcs1_sha384(0x0501),
          rsa_pkcs1_sha512(0x0601),

          /* ECDSA 알고리즘 */
          ecdsa_secp256r1_sha256(0x0403),
          ecdsa_secp384r1_sha384(0x0503),
          ecdsa_secp521r1_sha512(0x0603),

          /* 공개 키 OID가 rsaEncryption인 RSASSA-PSS 알고리즘 */
          rsa_pss_rsae_sha256(0x0804),
          rsa_pss_rsae_sha384(0x0805),
          rsa_pss_rsae_sha512(0x0806),

          /* EdDSA 알고리즘 */
          ed25519(0x0807),
          ed448(0x0808),

          /* 공개 키 OID가 RSASSA-PSS인 RSASSA-PSS 알고리즘 */
          rsa_pss_pss_sha256(0x0809),
          rsa_pss_pss_sha384(0x080a),
          rsa_pss_pss_sha512(0x080b),

          /* 레거시 알고리즘 */
          rsa_pkcs1_sha1(0x0201),
          ecdsa_sha1(0x0203),

          /* 예약된 코드 포인트 */
          obsolete_RESERVED(0x0000..0x0200),
          dsa_sha1_RESERVED(0x0202),
          obsolete_RESERVED(0x0204..0x0400),
          dsa_sha256_RESERVED(0x0402),
          obsolete_RESERVED(0x0404..0x0500),
          dsa_sha384_RESERVED(0x0502),
          obsolete_RESERVED(0x0504..0x0600),
          dsa_sha512_RESERVED(0x0602),
          obsolete_RESERVED(0x0604..0x06FF),
          private_use(0xFE00..0xFFFF),
          (0xFFFF)
      } SignatureScheme;

      struct {
          SignatureScheme supported_signature_algorithms<2..2^16-2>;
      } SignatureSchemeList;
B.3.1.4. 지원 그룹 확장
      enum {
          unallocated_RESERVED(0x0000),

          /* 타원 곡선 그룹(ECDHE) */
          obsolete_RESERVED(0x0001..0x0016),
          secp256r1(0x0017), secp384r1(0x0018), secp521r1(0x0019),
          obsolete_RESERVED(0x001A..0x001C),
          x25519(0x001D), x448(0x001E),

          /* 유한체 그룹(DHE) */
          ffdhe2048(0x0100), ffdhe3072(0x0101), ffdhe4096(0x0102),
          ffdhe6144(0x0103), ffdhe8192(0x0104),

          /* 예약된 코드 포인트 */
          ffdhe_private_use(0x01FC..0x01FF),
          ecdhe_private_use(0xFE00..0xFEFF),
          obsolete_RESERVED(0xFF01..0xFF02),
          (0xFFFF)
      } NamedGroup;

      struct {
          NamedGroup named_group_list<2..2^16-1>;
      } NamedGroupList;

“obsolete_RESERVED” 범위 안의 값은 이전 버전의 TLS에서 쓰인 것이며, TLS 1.3 구현은 이 값들을 제시하거나 협상해서는 안 된다(MUST NOT). 폐기된 곡선들은 알려진 또는 이론적인 약점이 여럿 있거나, 쓰임이 매우 적었다(일부는 의도하지 않은 서버 구성 문제 때문에만 쓰였다). 이 곡선들은 더 이상 일반적인 용도에 적절하다고 여겨지지 않으며, 안전하지 않을 수 있다고 보는 것이 바람직하다. 여기에 규정한 곡선 묶음은 현재 배포되어 올바르게 구성된 모든 TLS 구현과 상호 운용하기에 충분하다.

B.3.2. 서버 매개 변수 메시지

      opaque DistinguishedName<1..2^16-1>;

      struct {
          DistinguishedName authorities<3..2^16-1>;
      } CertificateAuthoritiesExtension;

      struct {
          opaque certificate_extension_oid<1..2^8-1>;
          opaque certificate_extension_values<0..2^16-1>;
      } OIDFilter;

      struct {
          OIDFilter filters<0..2^16-1>;
      } OIDFilterExtension;

      struct {} PostHandshakeAuth;

      struct {
          Extension extensions<0..2^16-1>;
      } EncryptedExtensions;

      struct {
          opaque certificate_request_context<0..2^8-1>;
          Extension extensions<0..2^16-1>;
      } CertificateRequest;

B.3.3. 인증 메시지

      enum {
          X509(0),
          OpenPGP_RESERVED(1),
          RawPublicKey(2),
          (255)
      } CertificateType;

      struct {
          select (certificate_type) {
              case RawPublicKey:
                /* RFC 7250의 ASN.1_subjectPublicKeyInfo */
                opaque ASN1_subjectPublicKeyInfo<1..2^24-1>;

              case X509:
                opaque cert_data<1..2^24-1>;
          };
          Extension extensions<0..2^16-1>;
      } CertificateEntry;

      struct {
          opaque certificate_request_context<0..2^8-1>;
          CertificateEntry certificate_list<0..2^24-1>;
      } Certificate;

      struct {
          SignatureScheme algorithm;
          opaque signature<0..2^16-1>;
      } CertificateVerify;

      struct {
          opaque verify_data[Hash.length];
      } Finished;

B.3.4. 티켓 수립

      struct {
          uint32 ticket_lifetime;
          uint32 ticket_age_add;
          opaque ticket_nonce<0..255>;
          opaque ticket<1..2^16-1>;
          Extension extensions<0..2^16-1>;
      } NewSessionTicket;

B.3.5. 키 갱신

      struct {} EndOfEarlyData;

      enum {
          update_not_requested(0), update_requested(1), (255)
      } KeyUpdateRequest;

      struct {
          KeyUpdateRequest request_update;
      } KeyUpdate;

B.4. 암호 스위트

암호 스위트는 AEAD 알고리즘과, HKDF와 함께 쓸 해시 알고리즘의 쌍을 정의한다. 암호 스위트 이름은 다음 명명 규칙을 따른다.

      CipherSuite TLS_AEAD_HASH = VALUE;
구성 요소 내용
TLS 문자열 “TLS”
AEAD 레코드 보호에 쓰는 AEAD 알고리즘
HASH HKDF와 Transcript-Hash에 쓰는 해시 알고리즘
VALUE 이 암호 스위트에 할당된 2바이트 ID

표 4: 암호 스위트 이름 구조

이 명세는 TLS 1.3에서 쓸 다음 암호 스위트를 정의한다.

Description Value
TLS_AES_128_GCM_SHA256 {0x13,0x01}
TLS_AES_256_GCM_SHA384 {0x13,0x02}
TLS_CHACHA20_POLY1305_SHA256 {0x13,0x03}
TLS_AES_128_CCM_SHA256 {0x13,0x04}
TLS_AES_128_CCM_8_SHA256 {0x13,0x05}

표 5: 암호 스위트 목록

대응하는 AEAD 알고리즘 AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_AES_128_CCM은 [RFC5116]에 정의되어 있다. AEAD_CHACHA20_POLY1305는 [RFC8439]에 정의되어 있다. AEAD_AES_128_CCM_8은 [RFC6655]에 정의되어 있다. 대응하는 해시 알고리즘은 [SHS]에 정의되어 있다.

TLS 1.3은 이전 버전의 TLS와 같은 암호 스위트 공간을 쓰지만, TLS 1.3 암호 스위트는 대칭 암호만 지정하는 식으로 다르게 정의되며 TLS 1.2에는 쓸 수 없다. 마찬가지로 TLS 1.2 이하의 암호 스위트는 TLS 1.3과 함께 쓸 수 없다.

새 암호 스위트 값은 11장에 설명된 대로 IANA가 할당한다.

부록 C. 구현 참고 사항

TLS 프로토콜은 흔한 보안 실수의 상당수를 막을 수 없다. 이 부록은 구현자에게 도움이 될 몇 가지 권고를 제시한다. [RFC8448]은 TLS 1.3 핸드셰이크의 시험 벡터(test vector)를 제공한다.

C.1. 난수 생성과 시드 설정

TLS에는 암호학적으로 안전한 의사 난수 생성기(cryptographically secure pseudorandom number generator, CSPRNG)가 필요하다. 성능이 좋고 적절하게 안전한 CSPRNG는 대부분의 운영 체제가 제공하거나 암호 라이브러리에서 얻을 수 있다. 새 CSPRNG를 만들기보다 기존 CSPRNG 구현을 쓸 것을 권고한다(RECOMMENDED). 적절한 암호 라이브러리 상당수를 이미 좋은 라이선스 조건으로 쓸 수 있다. 그런 라이브러리로 만족스럽지 않다면 [RFC4086]이 무작위 값 생성에 관한 지침을 제공한다.

TLS는 무작위 값을 (1) ClientHello와 ServerHello의 공개 Random 값 같은 공개 프로토콜 필드에, 그리고 (2) 키 재료를 생성하는 데 쓴다. CSPRNG가 제대로 동작한다면 출력에서 CSPRNG 상태를 알아낼 수 없으므로 이는 보안 문제가 되지 않는다. 그러나 CSPRNG가 망가져 있으면, [CHECKOWAY]와 [DSA-1571-1]에 기록된 것처럼 공격자가 공개 출력을 이용해 CSPRNG의 내부 상태를 알아내고 그로써 키 재료를 예측하는 것이 가능할 수도 있다.

구현은 공개 값과 비공개 값을 생성하는 데 서로 다른 CSPRNG를 써서 이런 형태의 공격에 대한 보안을 높일 수 있다.

[RFC8937]은 보안 프로토콜 구현이 장기 개인 키와 결정적 서명 함수를 사용해 자신의 (의사) 난수 생성기를 보강하는 방법을 설명한다. 이 방법은 망가졌거나 다른 방식으로 조작된 난수 생성기의 무작위성을 개선한다.

C.2. 인증서와 인증

구현은 인증서의 무결성을 검증할 책임이 있으며, 일반적으로 인증서 폐지 메시지를 지원하는 것이 바람직하다. 4.5.1.1절을 참고한다. 애플리케이션 프로파일에 특별한 지시가 없다면, 신뢰하는 인증 기관(CA)이 올바르게 서명했는지 확인하기 위해 인증서는 항상 검증하는 것이 마땅하다. 신뢰 앵커를 선택하고 추가하는 일은 매우 신중히 하는 것이 바람직하다. 사용자는 인증서와 신뢰 앵커에 관한 정보를 볼 수 있는 것이 바람직하다. 애플리케이션은 최소 키 크기와 최대 키 크기도 강제해야 한다(SHOULD). 예를 들어 2048비트 RSA나 224비트 ECDSA보다 약한 키나 서명이 들어 있는 인증 경로는 보안 애플리케이션에 적절하지 않다.

일부 프로토콜에서는 같은 인증서를 클라이언트 모드와 서버 모드 양쪽에 쓰는 것이 흔한 관행이라는 점에 유의하라. 이 설정은 충분히 분석되지 않았으며, 이 경우 상위 계층 의미에 모호함이 없도록 보장하는 것은 상위 계층 프로토콜의 책임이다.

C.3. 구현상의 함정

구현 경험상, 이전 TLS 명세의 일부는 이해하기 쉽지 않았고 상호 운용성 문제와 보안 문제의 원인이 되었다. 이 문서에서 그런 부분 상당수를 명확히 했지만, 이 부록에는 구현자가 특히 주의해야 할 가장 중요한 사항의 짧은 목록을 담았다.

TLS 프로토콜 문제:

암호 세부 사항:

C.4. 클라이언트와 서버 추적 방지

클라이언트는 티켓 하나를 여러 연결에 재사용하지 않아야 한다(SHOULD NOT). 티켓을 재사용하면 수동 관찰자가 서로 다른 연결을 연관 지을 수 있다. 티켓을 발급하는 서버는 클라이언트가 쓸 수도 있는 연결 수 이상의 티켓을 제공해야 한다(SHOULD). 예를 들어 HTTP/1.1 [RFC9112]을 쓰는 웹 브라우저는 서버에 연결을 여섯 개 열 수도 있다. 서버는 연결마다 새 티켓을 발급해야 한다(SHOULD). 그러면 클라이언트는 새 연결을 만들 때 항상 새 티켓을 쓸 수 있다.

서버에 티켓을 제시하면 서버도 서로 다른 연결을 연관 지을 수 있다. 이는 티켓 재사용과 관계없이 가능하다. 클라이언트 애플리케이션은 서로 연관되지 않아야 할 연결 사이에 걸쳐 티켓을 제시하지 않아야 한다(SHOULD NOT). 예를 들어 [FETCH]는 웹 브라우저에서 캐시 조회를 분리하기 위한 네트워크 파티션 키(network partition key)를 정의한다.

외부 식별자의 레이블은 사용자의 신원에 관한 추가 정보를 주지 않도록 고를 것을 권고한다(RECOMMENDED). 예를 들어 레이블에 이메일 주소가 들어 있으면, 암호화되는 클라이언트의 Certificate와 달리 수동 공격자에게 사용자를 쉽게 식별시킨다. 이 위험을 피하는 방법은 여러 가지가 있다. (1) 무작위 식별자 레이블을 쓰거나, (2) 서버가 아는 키로 식별자를 미리 암호화하거나, (3) Encrypted Client Hello [RFC9849]를 쓰는 것이다.

외부 PSK 식별자를 여러 연결에 쓰면, 일반적으로 외부 관찰자가 연결에 걸쳐 클라이언트나 서버를 추적할 수 있다. Encrypted Client Hello [RFC9849]를 쓰면 이 위험을 완화할 수 있으며, PSK 식별자를 바꾸거나 암호화하는 TLS 밖의 메커니즘도 마찬가지다.

C.5. 인증하지 않는 동작

이전 버전의 TLS는 익명 Diffie-Hellman에 기반한, 명시적으로 인증하지 않는 암호 스위트를 제공했다. 이 모드들은 TLS 1.3에서 사용이 중단되었다. 그러나 검증할 수 있는 서버 인증을 제공하지 않는 매개 변수를 여전히 여러 방법으로 협상할 수 있다. 예를 들면 다음과 같다.

이 기법은 어느 것이든 단독으로 쓰면 중간자 공격(man-in-the-middle attack)에 취약하므로 일반적인 용도에는 안전하지 않다. 그러나 서버 공개 키의 대역 외 검증, 최초 사용 시 신뢰(trust on first use), 채널 바인딩(channel binding) 같은 메커니즘을 통해 그런 연결을 외부 인증 메커니즘에 묶을 수도 있다(다만 [RFC5929]에 설명된 채널 바인딩은 TLS 1.3에 대해 정의되어 있지 않다). 그런 메커니즘을 쓰지 않으면 그 연결은 능동 중간자 공격에 대한 보호가 전혀 없다. 명시적인 구성이나 특정 애플리케이션 프로파일이 없는 한, 애플리케이션은 TLS를 그런 방식으로 사용해서는 안 된다(MUST NOT).

부록 D. TLS 1.2에 대한 갱신

이 문서에서 쓰는 이름에 맞추기 위해 [RFC5246]의 다음 용어 이름을 바꾼다.

이에 따라 [RFC7627]에 정의된 확장의 이름은 “Extended Main Secret” 확장으로 바뀐다. 확장 코드 포인트의 이름은 “extended_main_secret”으로 바뀐다. [RFC7627]의 4절에 있는 PRF 함수의 레이블 매개 변수는 호환성을 위해 바꾸지 않는다.

부록 E. 하위 호환성

TLS 프로토콜은 서로 다른 TLS 버전을 지원할 수 있는 엔드포인트 사이에서 버전을 협상하는 메커니즘을 내장하고 있다.

TLS 1.x와 SSL 3.0은 호환되는 ClientHello 메시지를 쓴다. 서버는 ClientHello 형식이 호환되는 상태로 유지되고 클라이언트와 서버가 모두 지원하는 프로토콜 버전이 하나 이상 있는 한, 향후 버전의 TLS를 쓰려는 클라이언트도 처리할 수 있다.

이전 버전의 TLS는 레코드 계층 버전 번호(TLSPlaintext.legacy_record_version과 TLSCiphertext.legacy_record_version)를 여러 목적으로 썼다. TLS 1.3부터 이 필드는 사용이 중단되었다. 모든 구현은 TLSPlaintext.legacy_record_version의 값을 반드시 무시해야 한다(MUST). TLSCiphertext.legacy_record_version의 값은 보호 해제를 위한 추가 데이터에 포함되지만, 그 밖에는 무시할 수도 있고(MAY) 고정 상수 값과 일치하는지 검증할 수도 있다(MAY). 버전 협상은 핸드셰이크 버전(ClientHello.legacy_version과 ServerHello.legacy_version, 그리고 ClientHello, HelloRetryRequest, ServerHello의 “supported_versions” 확장)만으로 한다. 오래된 엔드포인트와의 상호 운용성을 최대한 높이기 위해, TLS 1.0~1.2의 사용을 협상하는 구현은 ServerHello와 그 이후의 모든 레코드에서 레코드 계층 버전 번호를 협상된 버전으로 설정해야 한다(SHOULD).

예전의 비표준 동작과 잘못 구성된 배포와의 호환성을 최대한 높이기 위해, 모든 구현은 이전 TLS 버전의 핸드셰이크를 처리할 때에도 이 문서의 기대에 따른 인증 경로 검증을 지원해야 한다(SHOULD)(4.5.1.2절 참고).

TLS 1.2 이하는 핸드셰이크 트랜스크립트의 상당 부분을 비밀값과 유도된 키에 녹여 넣는 “Extended Main Secret” [RFC7627] 확장을 지원했다. 이 확장의 이름은 부록 D에서 바뀌었다는 점에 유의하라. TLS 1.3은 항상 서버 Finished까지의 트랜스크립트를 해시에 넣으므로, TLS 1.3과 이전 버전을 모두 지원하는 구현은 TLS 1.3을 쓸 때마다 자신의 API에서 Extended Main Secret 확장의 사용을 나타내야 한다(SHOULD).

E.1. 오래된 서버와 협상하기

TLS 1.3을 지원하지 않는 서버와 협상하려는 TLS 1.3 클라이언트는 ClientHello.legacy_version에 0x0303(TLS 1.2)을 담되 “supported_versions” 확장에는 올바른 버전을 담은 일반적인 TLS 1.3 ClientHello를 보낸다. 서버가 TLS 1.3을 지원하지 않으면 더 오래된 버전 번호를 담은 ServerHello로 응답한다. 클라이언트가 이 버전을 쓰기로 동의하면, 협상된 프로토콜에 맞게 협상이 진행된다. 재개에 티켓을 쓰는 클라이언트는 이전에 협상한 버전으로 연결을 시작해야 한다(SHOULD).

0-RTT 데이터는 오래된 서버와 호환되지 않으며, 서버가 TLS 1.3을 지원한다는 것을 알지 못하는 한 보내지 않아야 한다(SHOULD NOT)는 점에 유의하라. 부록 E.3을 참고한다.

서버가 선택한 버전을 클라이언트가 지원하지 않으면(또는 받아들일 수 없으면), 클라이언트는 반드시 “protocol_version” 경고로 핸드셰이크를 중단해야 한다(MUST).

일부 레거시 서버 구현은 TLS 명세를 제대로 구현하지 않아, 자신이 모르는 TLS 확장이나 버전을 만나면 연결을 중단할 수도 있다고 알려져 있다. 버그가 있는 서버와의 상호 운용성은 이 문서의 범위를 벗어나는 복잡한 주제다. 하위 호환되는 연결을 협상하려면 연결을 여러 번 시도해야 할 수도 있다. 그러나 이 방식은 다운그레이드 공격에 취약하므로 쓰지 않을 것을 권고한다(NOT RECOMMENDED).

E.2. 오래된 클라이언트와 협상하기

TLS 서버는 자신이 지원하는 가장 높은 버전보다 낮은 버전 번호를 나타내는 ClientHello를 받을 수도 있다. “supported_versions” 확장이 있으면, 서버는 반드시 4.3.1절에 설명된 대로 그 확장을 사용해 협상해야 한다(MUST). “supported_versions” 확장이 없으면, 서버는 반드시 ClientHello.legacy_version과 TLS 1.2 중 낮은 쪽으로 협상해야 한다(MUST). 예를 들어 서버가 TLS 1.0, 1.1, 1.2를 지원하고 legacy_version이 TLS 1.0이면, 서버는 TLS 1.0 ServerHello로 진행한다. “supported_versions” 확장이 없고 서버가 ClientHello.legacy_version보다 높은 버전만 지원한다면, 서버는 반드시 “protocol_version” 경고로 핸드셰이크를 중단해야 한다(MUST).

TLS 서버는 자신이 지원하는 가장 높은 버전보다 낮은 버전 번호를 나타내는 ClientHello를 받을 수도 있다. “supported_versions” 확장이 있으면, 서버는 반드시 4.3.1절에 설명된 대로 그 확장을 사용해 협상해야 한다(MUST). “supported_versions” 확장이 없고 ClientHello.legacy_version이 TLS 1.2이면, 서버는 반드시 TLS 1.2로 진행해야 한다(MUST). 그렇지 않으면 서버는 반드시 “protocol_version” 경고로 핸드셰이크를 중단해야 한다(MUST).

역주: 정오표 9040(Verified)에 따라 이 문단을 고쳤다. 정오표의 설명은 TLS 1.2 미만을 더 이상 지원하지 않는다는 것이다.

이전 버전의 TLS는 모든 경우에 레코드 계층 버전 번호 값(TLSPlaintext.legacy_record_version)을 명확히 규정하지 않았다는 점에 유의하라. 서버는 이 필드에서 다양한 TLS 1.x 버전을 받겠지만, 이 값은 반드시 항상 무시해야 한다(MUST).

E.3. 0-RTT 하위 호환성

0-RTT 데이터는 오래된 서버와 호환되지 않는다. 오래된 서버는 ClientHello에 오래된 ServerHello로 응답하지만, 0-RTT 데이터를 올바르게 건너뛰지 못해 핸드셰이크를 끝내지 못한다. 이는 클라이언트가 0-RTT를 쓰려 할 때, 특히 여러 서버로 된 배포에서 문제를 일으킬 수 있다. 예를 들어 어떤 배포는 일부 서버는 TLS 1.3을, 일부는 TLS 1.2를 구현하는 식으로 TLS 1.3을 점진적으로 배포할 수도 있고, TLS 1.3 배포가 TLS 1.2로 다운그레이드될 수도 있다.

0-RTT 데이터를 보내려는 클라이언트는 TLS 1.2 이하의 ServerHello를 받으면 반드시 연결을 실패시켜야 한다(MUST). 그런 다음 0-RTT를 끈 채로 연결을 재시도할 수 있다. 다운그레이드 공격을 피하기 위해, 클라이언트는 0-RTT만 끄고 TLS 1.3은 끄지 않아야 한다(SHOULD NOT).

이 오류 상황을 피하기 위해, 여러 서버로 된 배포는 0-RTT를 켜기 전에 0-RTT 없는 TLS 1.3이 균일하고 안정적으로 배포되었는지 확인해야 한다(SHOULD).

E.4. 미들박스 호환 모드

현장 측정 [Ben17a] [Ben17b] [Res17a] [Res17b]에서, TLS 클라이언트/서버 쌍이 TLS 1.3을 협상하면 상당수의 미들박스가 잘못 동작한다는 사실이 발견되었다. 구현은 TLS 1.3 핸드셰이크를 TLS 1.2 핸드셰이크처럼 보이게 만들어 그런 미들박스를 통과해 연결할 가능성을 높일 수 있다.

이 변경을 모두 합치면 TLS 1.3 핸드셰이크가 TLS 1.2 세션 재개와 비슷해져, 미들박스를 통과해 연결에 성공할 가능성이 높아진다. 이 “호환 모드(compatibility mode)”는 부분적으로 협상된다. 클라이언트는 세션 ID를 제공할지 말지 정할 수 있고, 서버는 그 값을 되돌려 보내야 한다. change_cipher_spec은 피어가 무시해야 하므로 어느 쪽이든 핸드셰이크 중 언제든 보낼 수 있지만, 클라이언트가 비어 있지 않은 세션 ID를 보내면 서버는 반드시 이 부록에 설명된 대로 change_cipher_spec을 보내야 한다(MUST).

E.5. 하위 호환성 관련 보안 제약

이전 버전의 TLS 사용을 협상하는 구현은, 가능하다면 순방향 비밀성을 제공하는 암호 스위트와 AEAD 암호 스위트를 선호해야 한다(SHOULD).

RC4 암호 스위트의 보안은 [RFC7465]에 든 이유로 불충분하다고 여겨진다. 구현은 어떤 TLS 버전에서도, 어떤 이유로도 RC4 암호 스위트를 제시하거나 협상해서는 안 된다(MUST NOT).

이전 버전의 TLS는 강도가 매우 낮은 암호의 사용을 허용했다. 강도가 112비트보다 낮은 암호는 어떤 TLS 버전에서도, 어떤 이유로도 제시하거나 협상해서는 안 된다(MUST NOT).

SSL 2.0 [SSL2], SSL 3.0 [RFC6101], TLS 1.0 [RFC2246], TLS 1.1 [RFC4346]의 보안은 [RFC6176], [RFC7568], [RFC8996]에 열거된 이유로 불충분하다고 여겨지며, 어떤 이유로도 협상해서는 안 된다(MUST NOT).

구현은 SSL 버전 2.0과 호환되는 CLIENT-HELLO를 보내서는 안 된다(MUST NOT). 구현은 SSL 버전 2.0과 호환되는 CLIENT-HELLO를 사용해 TLS 1.3 이상을 협상해서는 안 된다(MUST NOT). 구현은 이전 버전의 TLS를 협상하기 위해 SSL 버전 2.0과 호환되는 CLIENT-HELLO를 받아들이지 않을 것을 권고한다(NOT RECOMMENDED).

구현은 ClientHello.legacy_version과 ServerHello.legacy_version에 반드시 0x0303을 사용해야 한다(MUST). 다른 버전 번호의 Hello 메시지를 받은 구현은 반드시 4.2.2절과 4.2.3절에 설명된 대로 핸드셰이크를 중단해야 한다(MUST).

구현은 버전이 0x0300보다 낮은 레코드를 보내서는 안 된다(MUST NOT). 구현은 버전이 0x0300보다 낮은 레코드를 받아들이지 않아야 한다(SHOULD NOT)(다만 레코드 버전 번호를 완전히 무시한다면 의도치 않게 받아들일 수도 있다).

구현은 [RFC6066]의 7절에 정의된 Truncated HMAC 확장을 사용해서는 안 된다(MUST NOT). 이 확장은 AEAD 알고리즘에 적용할 수 없고, 일부 시나리오에서 안전하지 않다고 밝혀졌기 때문이다.

부록 F. 보안 속성 개요

TLS의 완전한 보안 분석은 이 문서의 범위를 벗어난다. 이 부록에서는 바라는 속성을 비형식적으로 설명하고, 더 형식적인 정의를 제공하는 연구 문헌의 더 자세한 연구를 참조로 제시한다.

핸드셰이크의 속성은 레코드 계층의 속성과 따로 다룬다.

F.1. 핸드셰이크

TLS 핸드셰이크는 단방향 인증(서버만)과 상호 인증(클라이언트와 서버) 기능을 모두 제공하려는 인증 키 교환(Authenticated Key Exchange, AKE) 프로토콜이다. 핸드셰이크가 끝나면 각 쪽은 다음 값에 대한 자신의 관점을 출력한다.

공격자는 능동 네트워크 공격자, 즉 당사자 사이의 통신에 쓰이는 네트워크를 완전히 장악한 공격자라고 가정한다[RFC3552]. 이런 조건에서도 핸드셰이크는 아래에 나열한 속성을 제공해야 한다. 이 속성들이 꼭 서로 독립적이지는 않으며, 프로토콜 사용자의 요구를 반영한 것이라는 점에 유의하라.

비형식적으로 말하면, TLS 1.3의 서명 기반 모드는 비대칭 키 교환으로 수립하고, 핸드셰이크 트랜스크립트에 대한 서버의 서명으로 인증하며, MAC으로 서버의 신원에 묶은, 고유하고 비밀인 공유 키의 수립을 제공한다. 클라이언트가 인증서로 인증된다면, 클라이언트도 핸드셰이크 트랜스크립트에 서명하고 양쪽 신원 모두에 묶인 MAC을 제공한다. [SIGMA]는 이런 유형의 키 교환 프로토콜의 설계와 분석을 설명한다. 연결마다 새 비대칭 키를 쓰면, 출력 키는 순방향 비밀성을 갖는다.

외부 PSK와 재개 PSK는 장기 공유 비밀값에서 출발해 연결마다 고유한 단기 세션 키 묶음을 만든다. 이 비밀값은 이전 핸드셰이크에서 수립된 것일 수 있다. 비대칭 키 수립과 함께 PSK를 쓰면 이 세션 키도 순방향 비밀성을 갖는다. 재개 PSK는 연결 N이 계산하고 연결 N+1을 만드는 데 필요한 재개 비밀값이 연결 N에서 쓰는 트래픽 키와 분리되도록 설계되었으며, 그래서 연결 사이에 순방향 비밀성을 제공한다. 또한 같은 연결에서 티켓을 여러 개 수립하면 티켓마다 다른 키에 연관되므로, 한 티켓에 연관된 PSK가 노출되더라도 다른 티켓에 연관된 PSK로 수립한 연결이 노출되지는 않는다. 이 속성은 티켓이 자체 암호화된 경우보다 데이터베이스에 저장되어(그래서 삭제할 수 있어) 있을 때 가장 의미가 있다.

순방향 비밀성은 키 유출의 영향을 한 방향으로 제한한다(시각 T2에 키가 노출되어도 T1 < T2인 시각 T1의 키는 노출되지 않는다). 반대 방향의 보호(시각 T1에 노출되어도 시각 T2의 키는 노출되지 않음)는 비대칭 키 수립을 다시 실행해 얻을 수 있다. 장기 인증 키가 노출되었다면, 비대칭 키 수립을 쓰는 전체 핸드셰이크가 수동 공격자에 대한 보호를 제공한다. resumption_secret이 노출되었다면, 비대칭 키 수립을 쓰는 재개 핸드셰이크가 수동 공격자에 대한 보호를, 비대칭 키 수립을 쓰는 전체 핸드셰이크가 능동 공격자에 대한 보호를 제공한다. 트래픽 비밀값이 노출되었다면, 비대칭 키 수립을 쓰는 어떤 핸드셰이크든 능동 공격자에 대한 보호를 제공한다. [RFC7624]의 용어로 말하면, 비대칭 키 수립을 다시 실행하지 않는 순방향 비밀성은 공격자의 정적 키 유출(static key exfiltration)을 막지 못한다. application_traffic_secret_N을 유출한 공격자는 예를 들어 application_traffic_secret_N+1, application_traffic_secret_N+2 등으로 암호화한 데이터를 포함해 그 연결로 앞으로 보내는 모든 데이터를 수동적으로 도청할 수 있다. 비대칭 키 수립을 자주 다시 실행하면 공격자는 동적 키 유출(dynamic key exfiltration)이나 내용 유출(content exfiltration)을 할 수밖에 없다.

PSK 바인더 값은 PSK와 현재 핸드셰이크를 결합하고, (NewSessionTicket 메시지로 수립한 경우) PSK를 수립한 세션과 현재 세션도 결합한다. 원래 핸드셰이크 트랜스크립트가 재개 비밀값을 만드는 값에 녹아들어 있으므로, 이 결합은 원래 핸드셰이크 트랜스크립트도 이행적으로 포함한다. 이를 위해서는 재개 비밀값을 만드는 데 쓰는 KDF와 바인더를 계산하는 데 쓰는 MAC이 모두 충돌 저항성을 가져야 한다. 자세한 내용은 부록 F.1.1을 참고한다. 참고로, 바인더는 다른 PSK의 바인더 값을 다루지 않지만, 그 값들은 Finished MAC에 포함된다.

참고: 이 명세는 현재 인증서 기반이 아닌 핸드셰이크(예: PSK)에서 서버가 CertificateRequest 메시지를 보내는 것을 허용하지 않는다. 앞으로 이 제한이 완화된다면, 클라이언트의 서명은 서버의 인증서를 직접 다루지 않게 된다. 그러나 PSK를 NewSessionTicket으로 수립했다면, 클라이언트의 서명은 PSK 바인더를 통해 서버의 인증서를 이행적으로 다루게 된다. [PSK-FINISHED]는 서버의 인증서에 묶이지 않는 구성에 대한 구체적인 공격을 설명한다([Kraw16]도 참고). 클라이언트가 같은 PSK/키 ID 쌍을 서로 다른 두 엔드포인트와 공유할 가능성이 있을 때 인증서 기반 클라이언트 인증을 쓰는 것은 안전하지 않다. 달리 정하는 다른 명세가 없는 한, 구현은 외부 PSK를 클라이언트나 서버의 인증서 기반 인증과 결합해서는 안 된다(MUST NOT). [RFC8773]은 이를 허용하는 확장을 제공하지만, 이 명세만큼의 분석을 받지 않았다.

익스포터를 쓰면, 익스포터는 (고유한 세션 키에서 생성되므로) 고유하고 비밀인 값을 만든다. 서로 다른 레이블과 컨텍스트로 계산한 익스포터는 계산상 독립적이므로, 하나에서 다른 하나를 계산하거나 내보낸 값에서 세션 비밀값을 계산하는 것은 불가능하다. 참고로, 익스포터는 임의 길이의 값을 만들 수 있다. 익스포터를 채널 바인딩으로 쓴다면, 내보낸 값은 반드시 충돌 저항성을 제공할 만큼 커야 한다(MUST). TLS 1.3의 조기 익스포터와 익스포터는 각각 조기 트래픽 키, 애플리케이션 트래픽 키와 같은 핸드셰이크 컨텍스트에서 유도되므로 보안 속성이 비슷하다. 익스포터에는 클라이언트의 인증서가 포함되지 않는다는 점에 유의하라. 클라이언트의 인증서에 묶이려는 향후 애플리케이션은 핸드셰이크 트랜스크립트 전체를 포함하는 새 익스포터를 정의할 필요가 있을 수도 있다.

모든 핸드셰이크 모드에서 Finished MAC(과, 있는 경우 서명)은 다운그레이드 공격을 막는다. 또한 4.2.3절에 설명한 대로 무작위 논스의 특정 바이트를 쓰면 이전 TLS 버전으로의 다운그레이드를 감지할 수 있다. TLS 1.3과 다운그레이드에 관한 자세한 내용은 [BBFGKZ16]을 참고한다.

클라이언트와 서버가 공유 키를 수립할 만큼 정보를 교환하면 핸드셰이크의 나머지는 암호화되므로, 계산된 공유 키가 인증되지 않았더라도 수동 공격자에 대한 보호를 제공한다. 서버가 클라이언트보다 먼저 인증하므로, 클라이언트는 자신이 서버에 인증할 때 인증된 서버에게만 자신의 신원을 드러내도록 보장할 수 있다. 구현은 길이 때문에 신원에 관한 정보가 새지 않도록 핸드셰이크 중에 제공된 레코드 패딩 메커니즘을 써야 한다는 점에 유의하라. 클라이언트가 제안하는 PSK 식별자는 암호화되지 않으며, 서버가 선택한 식별자도 마찬가지다.

역주: 정오표 9157(Reported, Editorial)은 이 절의 “main secret”(두 곳)을 그림 5의 이름 “Main Secret”으로 쓰자고 제안한다. 번역에서는 둘 다 “메인 비밀값”이라 차이가 없다.

F.1.1. 키 유도와 HKDF

TLS 1.3의 키 유도는 [RFC5869]에 정의된 HKDF와 그 두 구성 요소인 HKDF-Extract와 HKDF-Expand를 쓴다. HKDF 구성의 전체 근거는 [Kraw10]에, TLS 1.3에서 쓰는 방식의 근거는 [KW16]에 있다. 이 문서 전체에서 HKDF-Extract를 한 번 적용하면 그 뒤에 HKDF-Expand를 한 번 이상 호출한다. 이 순서는 (이 문서의 향후 개정판을 포함해) 항상 따라야 한다. 특히 HKDF-Extract의 출력을 중간에 HKDF-Expand 없이 다른 HKDF-Extract의 입력으로 쓰지 않아야 한다(SHOULD NOT). 같은 입력 일부에 HKDF-Expand를 여러 번 적용하는 것은, 키나 레이블로 서로 구별된다면 허용된다.

역주: “SHOULD NOT” 문장은 원문에서 “one SHOULD NOT use”처럼 주어가 일반 주체(“one”)라, 주어를 밝히지 않고 옮겼다.

HKDF-Expand는 입력과 출력의 길이가 모두 가변인 의사 난수 함수(PRF)를 구현한다는 점에 유의하라. 이 문서에서 HKDF를 쓰는 일부 경우(예: 익스포터와 resumption_secret을 생성할 때)에는 HKDF-Expand의 적용이 충돌 저항성을 가질 필요가 있다. 즉, HKDF-Expand에서 같은 값을 출력하는 서로 다른 두 입력을 찾는 것이 불가능해야 한다. 이를 위해서는 기반 해시 함수가 충돌 저항성을 가져야 하고, HKDF-Expand의 출력 길이가 256비트 이상(또는 해시 함수가 충돌을 찾지 못하도록 막는 데 필요한 만큼)이어야 한다.

F.1.2. 인증서 기반 클라이언트 인증

핸드셰이크 중이든 핸드셰이크 후 인증에서든 서버에 인증서 기반 인증 데이터를 보낸 클라이언트는, 서버가 그 뒤에 클라이언트를 인증된 것으로 보는지 확신할 수 없다. 서버가 연결을 단방향 인증으로 보는지 상호 인증으로 보는지 클라이언트가 알아야 한다면, 이는 애플리케이션 계층에서 마련해야 한다. 자세한 내용은 [CHHSV17]을 참고한다. 또한 [Kraw16]의 핸드셰이크 후 인증 분석은, 핸드셰이크 후 단계에서 보낸 인증서로 식별되는 클라이언트가 트래픽 키를 가지고 있음을 보여 준다. 따라서 (트래픽 키가 노출되지 않았다고 가정하면) 이 당사자는 원래 핸드셰이크에 참여한 클라이언트이거나, 원래 클라이언트가 트래픽 키를 위임한 상대다.

F.1.3. 0-RTT

0-RTT 동작 모드는 일반적으로 1-RTT 데이터와 비슷한 보안 속성을 제공하지만 두 가지 예외가 있다. 0-RTT 암호화 키는 완전한 순방향 비밀성을 제공하지 않으며, 서버는 지나치게 많아질 수 있는 상태를 유지하지 않고는 핸드셰이크의 고유성(재전송 불가능성)을 보장할 수 없다. 재전송 노출을 제한하는 메커니즘은 8장을 참고한다.

F.1.4. 익스포터 독립성

exporter_secret과 early_exporter_secret은 트래픽 키와 독립적으로 유도되므로, 그 키로 암호화한 트래픽의 보안을 위협하지 않는다. 그러나 이 비밀값들로 어떤 익스포터 값이든 계산할 수 있으므로, 가능한 한 빨리 지워야 한다(SHOULD). 익스포터 레이블 전체를 미리 안다면, 구현은 그 모든 레이블에 대해 익스포터 계산의 안쪽 Derive-Secret 단계를 미리 계산하고, 그다음 [early_]exporter_secret을 지운 뒤, 각 안쪽 값도 다시 필요하지 않다는 것을 알게 되는 즉시 지워야 한다(SHOULD).

F.1.5. 노출 후 보안

TLS는 피어의 장기 비밀값(서명 키나 외부 PSK)이 노출된 뒤에 이루어지는 핸드셰이크에 대해서는 보안을 제공하지 않는다. 따라서 TLS는 노출 후 보안(post-compromise security) [CCG16]을 제공하지 않는다. 노출 후 보안은 후방 비밀성(backwards secrecy) 또는 미래 비밀성(future secrecy)이라고도 부른다. 이 점은 자기 자신의 장기 비밀값이 노출된 뒤 당사자가 갖는 보안 보장을 설명하는 KCI 저항성과 대조된다.

F.1.6. 외부 참고 자료

TLS 핸드셰이크의 분석은 다음 참고 문헌을 참조한다. [DFGS15], [CHSV16], [DFGS16], [KW16], [Kraw16], [FGSW16], [LXZFH16], [FG17], [BBK17].

F.2. 레코드 계층

레코드 계층은 핸드셰이크가 양방향 암호화 키와 논스를 유도하는 데 쓸 수 있는 강력한 트래픽 비밀값을 만들어 낸다는 데 의존한다. 그것이 참이고, 키가 5.5절에 제시한 것보다 많은 데이터에 쓰이지 않는다고 가정하면, 레코드 계층은 다음을 보장해야 한다.

비형식적으로 말하면, TLS 1.3은 강력한 키로 평문을 AEAD 보호해 이 속성들을 제공한다. AEAD 암호화 [RFC5116]는 데이터의 기밀성과 무결성을 제공한다. 재전송 불가능성은 레코드마다 별도의 논스를 쓰는 것으로 제공한다. 이 논스는 레코드 순서 번호(5.3절)에서 유도하며, 순서 번호는 양쪽에서 독립적으로 유지한다. 그래서 순서가 바뀌어 전달된 레코드는 AEAD 보호 해제에 실패한다. 같은 평문을 여러 사용자가 같은 키로 반복해서 암호화할 때(HTTP에서 흔히 그렇듯) 대량 암호 분석을 막기 위해, 논스는 순서 번호를 트래픽 키와 함께 유도한 연결별 비밀 초기 벡터와 섞어서 만든다. 이 구성의 분석은 [BT16]을 참고한다.

TLS 1.3의 키 재설정(rekeying) 기법(7.2절 참고)은 [REKEY]에서 논의한 직렬 생성기(serial generator) 구성을 따른다. [REKEY]는 키를 재설정하면 그렇지 않을 때보다 더 많은 횟수의 암호화에 키를 쓸 수 있음을 보여 준다. 이는 HKDF-Expand-Label 함수가 의사 난수 함수(PRF)로서 안전하다는 데 의존한다. 또한 이 함수가 진정한 일방향 함수인 한, 키 변경 이전의 트래픽 키를 계산할 수 없다(순방향 비밀성).

TLS는 연결의 트래픽 비밀값이 노출된 뒤에 그 연결로 주고받는 데이터에 대해서는 보안을 제공하지 않는다. 즉, TLS는 트래픽 비밀값에 대해 노출 후 보안/미래 비밀성/후방 비밀성을 제공하지 않는다. 실제로 트래픽 비밀값을 알아낸 공격자는 그 연결에서 앞으로 쓸 모든 트래픽 비밀값을 계산할 수 있다. 그런 보장을 원하는 시스템은 새 핸드셰이크를 하고 비대칭 키 교환으로 새 연결을 수립할 필요가 있다.

F.2.1. 외부 참고 자료

TLS 레코드 계층의 분석은 다음 참고 문헌을 참조한다. [BMMRT15], [BT16], [BDFKPPRSZZ16], [BBK17], [PS18].

F.3. 트래픽 분석

TLS는 암호화된 패킷의 길이와 타이밍을 관찰하는 데 기반한 다양한 트래픽 분석 공격에 취약하다[CLINIC][HCJC16]. 고정된 콘텐츠 모음을 제공하는 비디오 서버처럼 구별할 수 있는 메시지의 종류가 적을 때 특히 쉽지만, 더 복잡한 시나리오에서도 쓸 만한 정보를 제공한다.

TLS는 이런 형태의 공격에 대한 특별한 방어를 제공하지 않지만, 애플리케이션이 쓸 수 있는 패딩 메커니즘을 포함한다. AEAD 함수가 보호하는 평문은 내용과 가변 길이 패딩으로 이루어지며, 이로써 애플리케이션은 임의 길이의 암호화된 레코드와, 전송 기간과 무전송 기간의 차이를 감추는 패딩만 있는 위장 트래픽을 만들 수 있다. 패딩은 실제 내용과 함께 암호화되므로 공격자는 패딩의 길이를 직접 알아낼 수 없지만, 레코드 처리 중에 드러나는 타이밍 채널을 이용해(즉, 레코드를 처리하는 데 얼마나 걸리는지 보거나, 레코드를 조금씩 흘려 넣어 어느 레코드가 서버의 응답을 끌어내는지 보는 식으로) 간접적으로 측정할 수 있을지도 모른다. 일반적으로 이런 채널을 모두 없애는 방법은 알려져 있지 않다. 상수 시간 패딩 제거 함수라도 내용을 데이터에 따라 동작이 달라지는 함수에 넘기게 될 가능성이 높기 때문이다. 최소한, 완전히 상수 시간으로 동작하는 서버나 클라이언트를 만들려면 상위 계층 프로토콜도 상수 시간으로 만드는 것을 포함해 애플리케이션 계층 프로토콜 구현과 긴밀하게 협력해야 한다.

참고: 견고한 트래픽 분석 방어는 패킷 전송 지연과 트래픽 양 증가 때문에 성능 저하로 이어질 가능성이 높다.

F.4. 부채널 공격

일반적으로 TLS에는 부채널 공격(즉, 타이밍 같은 부차적인 채널로 통신을 공격하는 것)에 대한 특별한 방어가 없으며, 이는 관련 암호 프리미티브의 구현에 맡긴다. 그러나 TLS의 일부 기능은 부채널 공격에 저항하는 코드를 쉽게 작성할 수 있도록 설계되었다.

부채널을 통한 정보 유출은 TLS 위의 계층, 즉 애플리케이션 프로토콜과 그것을 쓰는 애플리케이션에서도 일어날 수 있다. 부채널 공격에 대한 저항성은 애플리케이션과 애플리케이션 프로토콜이 각자 기밀 정보가 의도치 않게 유출되지 않도록 보장하는 데 달려 있다.

F.5. 0-RTT에 대한 재전송 공격

재전송할 수 있는 0-RTT 데이터는, 재전송되어도 안전하도록(최소한 멱등이어야 하고, 많은 경우 상수 시간 응답 같은 더 강한 조건도 필요할 수 있다) 특별히 설계되지 않은 TLS 사용 애플리케이션에 여러 보안 위협이 된다. 가능한 공격은 다음과 같다.

데이터를 아주 많이 재전송할 수 있다면, 암호 연산 속도를 반복해서 측정하는 것 같은 추가 공격이 가능해진다. 또한 공격자는 속도 제한 시스템에 과부하를 줄 수도 있다. 이런 공격에 관한 더 자세한 설명은 [Mac17]을 참고한다.

궁극적으로 0-RTT 데이터 복제를 이용한 공격으로부터 자신을 보호할 책임은 서버에 있다. 8장에 설명한 메커니즘은 TLS 계층에서 재전송을 막기 위한 것이지만, 클라이언트 데이터의 사본을 여러 개 받는 것까지 완전히 막아 주지는 않는다. 서버에 클라이언트에 관한 정보가 전혀 없으면(예: 상태를 공유하지 않는 다른 클러스터에 있거나, 8.1절에 설명한 대로 티켓이 삭제되었기 때문에) TLS 1.3은 1-RTT 핸드셰이크로 돌아간다. 이런 상황에서 애플리케이션 계층 프로토콜이 데이터를 재전송한다면, 공격자는 ClientHello를 원래 클러스터(데이터를 즉시 처리함)와, 1-RTT로 돌아가 애플리케이션 계층 재전송 때 데이터를 처리할 다른 클러스터 양쪽에 보내 메시지 중복을 일으킬 수 있다. 이 공격의 규모는 클라이언트가 트랜잭션을 기꺼이 재시도하려는 정도에 따라 제한되므로 제한된 양의 중복만 가능하며, 각 사본은 서버에서 새 연결로 나타난다.

8.1절과 8.2절에 설명한 메커니즘을 올바르게 구현하면, 일관된 상태를 가진 어떤 클러스터도 재전송된 ClientHello와 그에 연관된 0-RTT 데이터를 여러 번 받아들이지 않는다. 티켓 하나에 대해 0-RTT 사용을 클러스터 하나로 제한하는 서버라면, 주어진 ClientHello와 그에 연관된 0-RTT 데이터는 한 번만 받아들여진다. 그러나 상태가 완전히 일관되지 않다면, 공격자는 복제 창 동안 데이터의 사본 여러 개가 받아들여지게 할 수도 있다. 클라이언트는 서버 동작의 정확한 세부 사항을 알지 못하므로, 재전송되어도 안전하지 않거나 여러 1-RTT 연결에 걸쳐 재시도할 의향이 없는 메시지를 조기 데이터로 보내서는 안 된다(MUST NOT).

애플리케이션 프로토콜은 0-RTT 데이터의 사용을 정의하는 프로파일 없이 0-RTT 데이터를 사용해서는 안 된다(MUST NOT). 그 프로파일은 어떤 메시지나 상호 작용을 0-RTT와 함께 써도 안전한지, 그리고 서버가 0-RTT를 거절하고 1-RTT로 돌아가는 상황을 어떻게 처리할지 밝힐 필요가 있다.

또한 실수로 잘못 쓰는 것을 막기 위해, TLS 구현은 애플리케이션이 특별히 요청하지 않는 한 0-RTT를 (보내기든 받아들이기든) 켜서는 안 되며(MUST NOT), 애플리케이션이 지시하지 않는 한 서버가 거절한 0-RTT 데이터를 자동으로 다시 보내서는 안 된다(MUST NOT). 서버 쪽 애플리케이션은 일부 종류의 애플리케이션 트래픽에 대해 0-RTT 데이터를 특별히 처리하고 싶을 수 있다(예: 연결을 중단하거나, 애플리케이션 계층에서 데이터를 다시 보내라고 요청하거나, 핸드셰이크가 끝날 때까지 처리를 미룸). 애플리케이션이 이런 처리를 구현할 수 있도록, TLS 구현은 반드시 애플리케이션이 핸드셰이크가 끝났는지 알 수 있는 방법을 제공해야 한다(MUST).

F.5.1. 재전송과 익스포터

ClientHello를 재전송하면 같은 조기 익스포터가 만들어지므로, 이 익스포터를 쓰는 애플리케이션은 각별히 주의해야 한다. 특히 이 익스포터를 인증 채널 바인딩으로 쓴다면(예: 익스포터의 출력에 서명), PSK를 손에 넣은 공격자는 인증 키를 손에 넣지 않고도 연결 사이에서 인증자(authenticator)를 옮겨 심을 수 있다.

또한 조기 익스포터는 서버에서 클라이언트 방향의 암호화 키를 생성하는 데 쓰지 않아야 한다(SHOULD NOT). 그렇게 하면 그 키를 재사용하게 되기 때문이다. 이는 조기 애플리케이션 트래픽 키를 클라이언트에서 서버 방향으로만 쓰는 것과 같은 이치다.

F.6. PSK 식별자 노출

구현은 유효하지 않은 PSK 바인더에 핸드셰이크를 중단하는 것으로 응답하므로, 공격자가 주어진 PSK 식별자가 유효한지 확인할 수 있을지도 모른다. 구체적으로, 서버가 외부 PSK 핸드셰이크와 인증서 기반 핸드셰이크를 모두 받아들이면, 유효한 PSK 식별자는 핸드셰이크 실패로 이어지는 반면, 유효하지 않은 식별자는 그냥 건너뛰므로 인증서 핸드셰이크가 성공한다. PSK 핸드셰이크만 지원하는 서버는, 유효한 PSK 식별자가 없는 경우와 식별자는 있지만 바인더가 유효하지 않은 경우를 똑같이 다뤄 이런 형태의 공격에 저항할 수 있을지도 모른다.

F.7. 프로토콜 버전 간 PSK 공유

TLS 1.3은 PSK를 특정 KDF에 묶는 보수적인 방식을 쓴다. 반면 TLS 1.2는 PSK를 어떤 해시 함수와 TLS 1.2 PRF와도 함께 쓸 수 있게 한다. 따라서 TLS 1.2와 TLS 1.3 양쪽에서 쓰는 PSK는 TLS 1.3에서 해시 하나와만 써야 하며, 사용자가 PSK 하나만 프로비저닝하려 한다면 이는 최적이 아니다. TLS 1.2와 TLS 1.3의 구성은 둘 다 HMAC에 기반하지만 서로 다르다. 같은 PSK가 두 버전에서 서로 관련된 출력을 만드는 방법은 알려져 있지 않지만, 분석은 제한적으로만 이루어졌다. 구현은 TLS 1.3과 TLS 1.2 사이에서 PSK를 재사용하지 않는 것으로 교차 프로토콜 관련 출력에 대한 안전을 보장할 수 있다.

F.8. 외부 PSK와 경로 변경

TLS의 외부 PSK는 정확히 클라이언트 하나와 서버 하나만 알도록 설계되었다. 그러나 [RFC9257]에서 언급했듯이, PSK를 셋 이상의 개체가 공유하는 사용 사례도 있다. 그런 시나리오에서는, 노출된 그룹 구성원이 다른 어떤 구성원으로든 위장할 수 있다는 예상되는 보안 약점 외에도, 악의적인 비구성원이 정직한 그룹 구성원 사이의 핸드셰이크 경로를 바꿔(reroute) 의도하지 않은 방식으로 연결할 수 있다[Selfie]. [RFC9257]은 [RFC9258]에 정의된 외부 PSK 임포터(importer)의 사용을 포함해, 이런 악의적인 메시지 경로 변경을 막는 외부 PSK 사용 권고를 제공한다.

F.9. 자체 서명 인증서나 원시 공개 키를 쓸 때의 잘못된 결합

쓸모 있는 신원이 없는 자체 서명 인증서(DTLS-SRTP [RFC5763]처럼)나 원시 공개 키 [RFC7250]를 피어 인증에 쓰는 TLS 1.3은 잘못된 결합 공격(misbinding attack) [MM24]에 취약할 수 있다. 이 위험은 “external_id_hash” 확장 [RFC8844]을 쓰거나, 서버만 인증하는 경우에는 서버가 “server_name” 확장이 자신의 예상 신원과 일치하는지 확인하는 것으로 완화할 수 있다.

F.10. 정적 RSA에 대한 공격

TLS 1.3은 RSA 키 전송을 쓰지 않으므로 Bleichenbacher 유형 공격 [Blei98]에 직접 취약하지는 않지만, TLS 1.3 서버가 이전 버전 TLS의 맥락에서 정적 RSA도 지원한다면 TLS 1.3 연결에서 서버로 위장하는 것이 가능할 수도 있다[JSS15]. TLS 1.3 구현은 모든 TLS 버전에서 정적 RSA 지원을 끄는 것으로 이 공격을 막을 수 있다. 원칙적으로는 구현이 정적 RSA 복호화용과 RSA 서명용으로 keyUsage 비트가 다른 인증서를 분리할 수도 있지만, 이 기법은 클라이언트가 digitalSignature 비트가 설정되지 않은 인증서의 키를 쓰는 서명을 거부하는 데 의존하며, 이 제약을 강제하지 않는 클라이언트가 많다.

기여자

Martin Abadi
University of California, Santa Cruz
abadi@cs.ucsc.edu

Christopher Allen
(co-editor of TLS 1.0)
Alacrity Ventures
ChristopherA@AlacrityManagement.com

Nimrod Aviram
Tel Aviv University
nimrod.aviram@gmail.com

Richard Barnes
Cisco
rlb@ipv.sx

Steven M. Bellovin
Columbia University
smb@cs.columbia.edu

David Benjamin
Google
davidben@google.com

Benjamin Beurdouche
INRIA & Microsoft Research
benjamin.beurdouche@ens.fr

Karthikeyan Bhargavan
(editor of RFC 7627)
INRIA
karthikeyan.bhargavan@inria.fr

Simon Blake-Wilson
(co-author of RFC 4492)
BCI
sblakewilson@bcisse.com

Nelson Bolyard
(co-author of RFC 4492)
Sun Microsystems, Inc.
nelson@bolyard.com

Ran Canetti
IBM
canetti@watson.ibm.com

Matt Caswell
OpenSSL
matt@openssl.org

Stephen Checkoway
University of Illinois at Chicago
sfc@uic.edu

Pete Chown
Skygate Technology Ltd
pc@skygate.co.uk

Katriel Cohn-Gordon
University of Oxford
me@katriel.co.uk

Cas Cremers
University of Oxford
cas.cremers@cs.ox.ac.uk

Antoine Delignat-Lavaud
(co-author of RFC 7627)
INRIA
antdl@microsoft.com

Tim Dierks
(co-author of TLS 1.0, co-editor of TLS 1.1 and 1.2)
Independent
tim@dierks.org

Roelof DuToit
Symantec Corporation
roelof_dutoit@symantec.com

Taher Elgamal
Securify
taher@securify.com

Pasi Eronen
Nokia
pasi.eronen@nokia.com

Cedric Fournet
Microsoft
fournet@microsoft.com

Anil Gangolli
anil@busybuddha.org

David M. Garrett
dave@nulldereference.com

Illya Gerasymchuk
Independent
illya@iluxonchik.me

Alessandro Ghedini
Cloudflare Inc.
alessandro@cloudflare.com

Daniel Kahn Gillmor
ACLU
dkg@fifthhorseman.net

Matthew Green
Johns Hopkins University
mgreen@cs.jhu.edu

Jens Guballa
ETAS
jens.guballa@etas.com

Felix Guenther
TU Darmstadt
mail@felixguenther.info

Vipul Gupta
(co-author of RFC 4492)
Sun Microsystems Laboratories
vipul.gupta@sun.com

Chris Hawk
(co-author of RFC 4492)
Corriente Networks LLC
chris@corriente.net

Kipp Hickman

Alfred Hoenes

David Hopwood
Independent Consultant
david.hopwood@blueyonder.co.uk

Marko Horvat
MPI-SWS
mhorvat@mpi-sws.org

Jonathan Hoyland
Royal Holloway, University of London
jonathan.hoyland@gmail.com

Subodh Iyengar
Facebook
subodh@fb.com

Benjamin Kaduk
Akamai Technologies
kaduk@mit.edu

Hubert Kario
Red Hat Inc.
hkario@redhat.com

Phil Karlton
(co-author of SSL 3.0)

Leon Klingele
Independent
mail@leonklingele.de

Paul Kocher
(co-author of SSL 3.0)
Cryptography Research
paul@cryptography.com

Hugo Krawczyk
IBM
hugokraw@us.ibm.com

Adam Langley
(co-author of RFC 7627)
Google
agl@google.com

Olivier Levillain
ANSSI
olivier.levillain@ssi.gouv.fr

Xiaoyin Liu
University of North Carolina at Chapel Hill
xiaoyin.l@outlook.com

Ilari Liusvaara
Independent
ilariliusvaara@welho.com

Atul Luykx
K.U. Leuven
atul.luykx@kuleuven.be

Colm MacCarthaigh
Amazon Web Services
colm@allcosts.net

Carl Mehner
USAA
carl.mehner@usaa.com

Jan Mikkelsen
Transactionware
janm@transactionware.com

Bodo Moeller
(co-author of RFC 4492)
Google
bodo@acm.org

Kyle Nekritz
Facebook
knekritz@fb.com

Erik Nygren
Akamai Technologies
erik+ietf@nygren.org

Magnus Nystrom
Microsoft
mnystrom@microsoft.com

Kazuho Oku
DeNA Co., Ltd.
kazuhooku@gmail.com

Kenny Paterson
Royal Holloway, University of London
kenny.paterson@rhul.ac.uk

Christopher Patton
University of Florida
cjpatton@ufl.edu

Alfredo Pironti
(co-author of RFC 7627)
INRIA
alfredo.pironti@inria.fr

Andrei Popov
Microsoft
andrei.popov@microsoft.com

John Preuß Mattsson
Ericsson
john.mattsson@ericsson.com

Marsh Ray
(co-author of RFC 7627)
Microsoft
maray@microsoft.com

Robert Relyea
Netscape Communications
relyea@netscape.com

Kyle Rose
Akamai Technologies
krose@krose.org

Jim Roskind
Amazon
jroskind@amazon.com

Michael Sabin

Joe Salowey
Tableau Software
joe@salowey.net

Rich Salz
Akamai
rsalz@akamai.com

David Schinazi
Apple Inc.
dschinazi@apple.com

Sam Scott
Royal Holloway, University of London
me@samjs.co.uk

Mohit Sethi
Aalto University
mohit@iki.fi

Thomas Shrimpton
University of Florida
teshrim@ufl.edu

Dan Simon
Microsoft, Inc.
dansimon@microsoft.com

Brian Smith
Independent
brian@briansmith.org

Ben Smyth
Ampersand
www.bensmyth.com

Brian Sniffen
Akamai Technologies
ietf@bts.evenmere.org

Nick Sullivan
Cloudflare Inc.
nick@cloudflare.com

Bjoern Tackmann
University of California, San Diego
btackmann@eng.ucsd.edu

Tim Taubert
Mozilla
ttaubert@mozilla.com

Martin Thomson
Mozilla
mt@mozilla.com

Hannes Tschofenig
Arm Limited
Hannes.Tschofenig@arm.com

Sean Turner
sn3rd
sean@sn3rd.com

Steven Valdez
Google
svaldez@google.com

Filippo Valsorda
Cloudflare Inc.
filippo@cloudflare.com

Thyla van der Merwe
Royal Holloway, University of London
tjvdmerwe@gmail.com

Victor Vasiliev
Google
vasilvv@google.com

Loganaden Velvindron
cyberstorm.mu
logan@cyberstorm.mu

Hoeteck Wee
Ecole Normale Superieure, Paris
hoeteck@alum.mit.edu

Tom Weinstein

David Wong
NCC Group
david.wong@nccgroup.trust

Christopher A. Wood
Apple Inc.
cawood@apple.com

Tim Wright
Vodafone
timothy.wright@vodafone.com

Peter Wu
Independent
peter@lekensteyn.nl

Kazu Yamamoto
Internet Initiative Japan Inc.
kazu@iij.ad.jp

역주: 기여자 목록(이름, 소속, 이메일)은 원문 그대로 두었다.

저자 주소

Eric Rescorla
Independent
Email: ekr@rtfm.com

역주: 저자 주소는 2026년 발행 당시의 정보를 원문 그대로 옮긴 것이다.

번역자

김마리 (Mariel Kim)
Email:  marikim.code@gmail.com
GitHub: https://github.com/marik0de