supported_versions 확장이 없을 때 TLS 1.2 미만으로 협상하지 않고 TLS 1.2로 진행하거나 중단).역주: 용어는 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를 그대로 두고 그 아래에 한국어 번역을 덧붙였으며, 법적 효력은 원문에 있다. 마지막 문단에 따라 이 문서의 번역은 허용된다.
TLS의 주된 목표는 통신하는 두 피어 사이에 보안 채널(secure channel)을 제공하는 것이다. 기반 전송 계층에 요구하는 것은 신뢰할 수 있고 순서가 보장되는 데이터 스트림뿐이다. 구체적으로, 보안 채널은 다음 속성을 제공해야 한다.
인증(authentication): 채널의 서버 쪽은 항상 인증된다. 클라이언트 쪽은 선택적으로 인증된다. 인증은 비대칭 암호(예: RSA [RSA], ECDSA(Elliptic Curve Digital Signature Algorithm) [DSS], EdDSA(Edwards-Curve Digital Signature Algorithm) [RFC8032])나 대칭 사전 공유 키(pre-shared key, PSK)로 이루어질 수 있다.
기밀성(confidentiality): 채널이 수립된 뒤 채널로 보낸 데이터는 엔드포인트에만 보인다. TLS는 자신이 전송하는 데이터의 길이를 숨기지 않는다. 다만 엔드포인트는 TLS 레코드에 패딩을 넣어 길이를 감추고 트래픽 분석 기법에 대한 보호를 강화할 수 있다.
무결성(integrity): 채널이 수립된 뒤 채널로 보낸 데이터는 공격자가 탐지되지 않은 채로 수정할 수 없다.
이 속성들은 [RFC3552]에 설명된 것처럼 네트워크를 완전히 장악한 공격자 앞에서도 성립해야 한다. 관련 보안 속성을 더 완전하게 서술한 내용은 부록 F를 참고한다.
TLS는 두 가지 주요 구성 요소로 이루어진다.
핸드셰이크 프로토콜(handshake protocol, 4장): 통신하는 당사자를 인증하고, 암호 알고리즘과 매개 변수를 협상하며, 공유 키 재료(keying material)를 수립한다. 핸드셰이크 프로토콜은 변조(tampering)에 견디도록 설계되었다. 능동 공격자는 연결이 공격받지 않았을 때와 다른 매개 변수를 피어들이 협상하도록 강제할 수 없어야 한다.
레코드 프로토콜(record protocol, 5장): 핸드셰이크 프로토콜이 수립한 매개 변수를 사용해, 통신하는 피어 사이의 트래픽을 보호한다. 레코드 프로토콜은 트래픽을 일련의 레코드로 나누고, 각 레코드는 트래픽 키(traffic key)를 사용해 독립적으로 보호된다.
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]을 대체한다.
이 문서의 키워드 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, “OPTIONAL”은 여기에 보인 것처럼 모두 대문자로 쓰였을 때, 그리고 그때에만 BCP 14 [RFC2119] [RFC8174]에 설명된 대로 해석한다.
이 문서에서는 다음 용어를 쓴다.
TLS 1.3은 처음에 [RFC8446]에서 규정되었다. 이 문서는 같은 버전 번호를 유지하고 하위 호환되는, TLS 1.3의 작은 갱신이다. 이 문서는 일부 요구 사항을 강화하고, 불명확하다고 밝혀진 부분의 문구를 고쳤으며, 그 밖의 편집상 개선을 담았다. 또한 비밀값(secret)에 붙이던 용어 “master”를 없애고, 그 대신 용어 “main”을 쓰거나 용어가 필요 없는 곳에서는 더 짧은 이름을 쓴다. 이 문서의 구체적인 기술적 변경은 다음과 같다.
연결 사이에 KeyShare 값을 재사용하는 것을 금지한다.
TLS 1.0과 1.1은 이제 [RFC8996]에서 사용 중단되었으므로, 이 버전들로 협상하는 것을 금지한다.
PSK와 HelloRetryRequest에 어떤 해시를 쓰는지에 관한 모호함을 없앤다.
재개(resumption)를 지원하지 않는 클라이언트는 NewSessionTicket을 무시하도록 요구한다.
키 사용 한도(key usage limit)를 넘기 전에 키 갱신(key update)을 시작하라는 요구 사항을 MUST로 올리고, 키 사용 한도의 범위를 명확히 한다.
허용되는 KeyUpdate 메시지의 수를 제한한다.
“close_notify”의 수준(level)을 “warning”으로 정의하는 문구를 되살린다.
“user_canceled”에 관한 동작을 명확히 한다. “close_notify”를 보내도록 요구하고, “user_canceled”는 무시하도록 한다.
범용 경고(alert) “general_error”를 추가한다.
CertificateRequest.extensions의 하한을 0바이트로 바로잡는다. 확장을 하나도 보내지 않을 수 있고 그러면 길이가 0이 되므로, 이는 문법상 오류였다.
역주: 정오표 9161(Reported, 미확인)은 이 항목을, 빈 벡터를 표현할 수 있게 표현 문법을 바로잡은 것이고 4.4.2절은 여전히
CertificateRequest에 “signature_algorithms” 확장을 요구한다는 내용으로 고치자고 제안한다.
ClientHello.extensions의 하한을 바로잡는다. 이는 계산 오류였다.
NewSessionTicket.extensions의 상한을 바로잡는다. 이는 계산 오류였다.
비대칭 키 교환을 더 일반적인 말로 서술한다. 이전에는 (EC)DHE만을 가리켰으나, KEM 기반 키 교환을 쓰는 경우를 반영한다.
트랜스크립트 해시(transcript hash)와 향후 명세의 상호 작용을 명확히 한다.
[RFC9963]에 맞춰 RSA PSS를 요구하는 문구를 없앤다.
이 밖에도 보안 고려 사항, 특히 프라이버시에 관한 부분이 일부 개선되었다.
다음은 TLS 1.2와 TLS 1.3 사이의 주요 기능상 차이를 나열한 것이다. 모든 차이를 다 담으려는 목록은 아니며, 작은 차이는 이 밖에도 많다.
지원하는 대칭 암호 알고리즘 목록에서 레거시로 여겨지는 알고리즘을 모두 걷어 냈다. 남은 알고리즘은 모두 AEAD(Authenticated Encryption with Associated Data, 연관 데이터를 포함한 인증된 암호화) 알고리즘이다. 암호 스위트(cipher suite) 개념이 바뀌어, 인증·키 교환 메커니즘을 레코드 보호 알고리즘(비밀 키 길이 포함)과도, 키 유도 함수(key derivation function)와 핸드셰이크 메시지 인증 코드(MAC) 양쪽에 쓰는 해시와도 분리했다.
0-RTT(zero round-trip time) 모드를 추가했다. 이 모드는 일부 보안 속성을 희생하는 대신, 일부 애플리케이션 데이터에 대해 연결 설정 때 왕복 한 번을 줄인다.
정적 RSA와 정적 Diffie-Hellman 암호 스위트를 없앴다. 이제 공개 키 기반 키 교환 메커니즘은 모두 순방향 비밀성(forward secrecy)을 제공한다.
이제 ServerHello 다음의 핸드셰이크 메시지는 모두 암호화된다. 새로 도입한 EncryptedExtensions 메시지 덕분에, 이전에 ServerHello에 담겨 평문으로 전송되던 여러 확장도 기밀성 보호를 받을 수 있다.
키 유도 함수를 다시 설계했다. 새 설계는 키 분리 속성이 개선되어 암호학자가 분석하기 더 쉽다. HKDF(HMAC-based Extract-and-Expand Key Derivation Function)를 기반이 되는 프리미티브(primitive)로 사용한다.
핸드셰이크 상태 머신(state machine)을 크게 재구성해 일관성을 높이고 ChangeCipherSpec 같은 불필요한 메시지를 없앴다(미들박스 호환성에 필요한 경우는 제외).
타원 곡선 알고리즘이 이제 기본 명세에 들어 있고, EdDSA 같은 새 서명 알고리즘이 포함되었다. TLS 1.3은 점 형식(point format) 협상을 없애고 곡선마다 단일 점 형식을 쓴다.
그 밖에도 암호 측면을 개선했다. RSA 패딩을 RSASSA-PSS(RSA Probabilistic Signature Scheme)로 바꾸었고, 압축, DSA(Digital Signature Algorithm), 사용자 정의 DHE(Ephemeral Diffie-Hellman) 그룹을 없앴다.
TLS 1.2의 버전 협상 메커니즘은 사용이 중단되었고, 그 대신 확장에 담은 버전 목록을 쓴다. 이렇게 하면 버전 협상을 잘못 구현한 기존 서버와의 호환성이 높아진다.
서버 쪽 상태를 쓰는 세션 재개와 쓰지 않는 세션 재개, 그리고 이전 TLS 버전의 PSK 기반 암호 스위트를 단일한 새 PSK 교환으로 바꾸었다.
참고 문헌을 필요에 따라 갱신된 버전의 RFC를 가리키도록 고쳤다(예: RFC 3280 대신 RFC 5280).
이 문서는 TLS 1.2 구현에 선택적으로 영향을 주는 몇 가지 변경을 정의한다. TLS 1.3을 지원하지 않는 TLS 1.2 구현도 여기에 포함된다.
버전 다운그레이드 방지 메커니즘은 4.2.3절에 설명되어 있다.
RSASSA-PSS 서명 방식(signature scheme)은 4.3.3절에 정의되어 있다.
ClientHello의 “supported_versions” 확장을 사용해, ClientHello의 legacy_version 필드보다 우선하여 사용할 TLS 버전을 협상할 수 있다.
클라이언트는 “signature_algorithms_cert” 확장으로, 자신이 X.509 인증서에서 검증할 수 있는 서명 알고리즘을 알릴 수 있다.
비밀값에 붙이던 용어 “master”를 없앴고, “extended_master_secret” 확장 [RFC7627]의 이름을 “extended_main_secret”으로 바꾸었다.
이와 함께 이 문서는 이전 TLS 버전에 대한 일부 준수 요구 사항을 명확히 한다. 9.3절을 참고한다.
보안 채널이 사용하는 암호 매개 변수는 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(인증)는 아래에서 설명하는 핸드셰이크의 세 단계를 나타낸다.
핸드셰이크는 세 단계로 이루어진다고 볼 수 있다(위 그림에 표시됨).
키 교환(Key Exchange): 공유 키 재료를 수립하고 암호 매개 변수를 선택한다. 이 단계 뒤의 모든 것은 암호화된다.
서버 매개 변수(Server Parameters): 그 밖의 핸드셰이크 매개 변수(클라이언트를 인증하는지, 애플리케이션 계층 프로토콜 지원 등)를 정한다.
인증(Authentication): 서버를(그리고 선택적으로 클라이언트를) 인증하고, 키 확인(key confirmation)과 핸드셰이크 무결성을 제공한다.
키 교환 단계에서 클라이언트는 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를 함께 쓸 수 있으며, 그 경우 두 확장이 모두 들어간다는 점에 유의하라.
이어서 서버는 서버 매개 변수를 정하기 위해 메시지 두 개를 보낸다.
EncryptedExtensions: 암호 매개 변수를 정하는 데 필요하지 않은 ClientHello 확장에 대한 응답. 개별 인증서에 고유한 확장에 대한 응답은 제외한다. [4.4.1절]CertificateRequest: 인증서 기반 클라이언트 인증을 원할 때, 그 인증서에 원하는 매개 변수. 클라이언트 인증을 원하지 않으면 이 메시지를 생략한다. [4.4.2절]마지막으로 클라이언트와 서버는 인증 메시지를 교환한다. TLS는 인증서 기반 인증이 필요할 때마다 같은 메시지 묶음을 사용한다. (PSK 기반 인증은 키 교환의 부수 효과로 이루어진다.) 구체적으로는 다음과 같다.
Certificate: 엔드포인트의 인증서와 인증서별 확장. 서버는 인증서로 인증하지 않으면 이 메시지를 생략하고, 클라이언트는 서버가 CertificateRequest를 보내지 않았으면(클라이언트가 인증서로 인증하지 말아야 함을 나타냄) 이 메시지를 생략한다. 원시 공개 키(raw public key) [RFC7250]나 캐시 정보 확장(cached information extension) [RFC7924]을 사용하는 경우, 이 메시지에는 인증서가 아니라 서버의 장기 키(long-term key)에 대응하는 다른 값이 담긴다는 점에 유의하라. [4.5.1절]CertificateVerify: Certificate 메시지에 담긴 공개 키에 대응하는 개인 키로 핸드셰이크 전체에 대해 만든 서명. 엔드포인트가 인증서로 인증하지 않으면 이 메시지를 생략한다. [4.5.2절]Finished: 핸드셰이크 전체에 대한 MAC(Message Authentication Code). 이 메시지는 핸드셰이크에서 수립한 공유 비밀값에 대한 키 확인을 제공하고, 엔드포인트의 신원을 교환한 키에 결합하며, PSK 모드에서는 핸드셰이크도 인증한다. [4.5.3절]서버의 메시지를 받으면 클라이언트는 자신의 인증 메시지, 즉 Certificate와 CertificateVerify(요청받은 경우), 그리고 Finished로 응답한다.
이 시점에서 핸드셰이크가 끝나고, 클라이언트와 서버는 인증된 암호화(authenticated encryption)로 보호된 애플리케이션 계층 데이터를 레코드 계층이 교환하는 데 필요한 키 재료를 유도한다. 2.3절에 명시된 경우를 제외하면, 애플리케이션 데이터는 Finished 메시지를 보내기 전에 보내서는 안 된다(MUST NOT). 서버는 클라이언트의 인증 메시지를 받기 전에 애플리케이션 데이터를 보낼 수 있지만, 그 시점에 보낸 데이터는 물론 인증되지 않은 피어에게 보내는 것이라는 점에 유의하라.
클라이언트가 충분한 “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에서는 기본 핸드셰이크를 최적화한 변형도 몇 가지 쓸 수 있다. 다음 절들에서 이를 설명한다.
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)을 하는 것을 막지 못한다.
클라이언트와 서버가 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 데이터보다 약하다. 구체적으로는 다음과 같다.
프로토콜은 이 데이터에 대해 어떤 순방향 비밀성도 보장하지 않는다. 순방향 비밀성 보장이 적용되는지, 적용된다면 어떤 것인지는 서버의 동작에 따라 정해진다(8.1절 참고). 이 동작은 프로토콜의 일부로 클라이언트에 전달되지 않는다. 따라서 서버의 동작을 대역 외(out-of-band)에서 알지 못하는 한, 클라이언트는 이 데이터가 순방향 비밀성을 갖지 않는다고 가정해야 한다.
연결 사이에 재전송이 없다는 보장이 없다. 일반적인 TLS 1.3 1-RTT 데이터는 서버의 Random 값으로 재전송을 막지만, 0-RTT 데이터는 ServerHello에 의존하지 않으므로 보장이 더 약하다. 이 점은 데이터를 TLS 클라이언트 인증으로 인증하든 애플리케이션 프로토콜 안에서 인증하든, 데이터가 인증되는 경우에 특히 중요하다. early_exporter_secret을 쓰는 모든 경우에도 같은 경고가 적용된다.
0-RTT 데이터는 한 연결 안에서 중복될 수 없다(즉, 서버는 같은 연결에서 같은 데이터를 두 번 처리하지 않는다). 또한 공격자는 0-RTT 데이터를 1-RTT 데이터처럼 보이게 만들 수 없다(서로 다른 키로 보호되기 때문이다). 부록 F.5에 가능한 공격이 설명되어 있고, 8장에서는 서버가 재전송의 영향을 제한하는 데 쓸 수 있는 메커니즘을 설명한다.
이 문서는 데이터를 외부 표현(external representation)으로 나타내는 형식을 다룬다. 이 문서에서는 다음과 같은, 아주 기본적이고 다소 느슨하게 정의된 표현 문법(presentation syntax)을 쓴다.
아래 정의에서 이 문법의 선택적 구성 요소는 [[ ]](이중 대괄호)로 둘러싸 나타낸다.
모든 데이터 항목의 표현은 명시적으로 규정된다. 기본 데이터 블록 크기는 1바이트(즉, 8비트)다. 멀티바이트(multibyte) 데이터 항목은 바이트를 왼쪽에서 오른쪽으로, 위에서 아래로 이어 붙인 것이다. 바이트 스트림에서 멀티바이트 항목(다음 예에서는 숫자)은 (C 표기법으로) 다음과 같이 만들어진다.
value = (byte[0] << 8*(n-1)) | (byte[1] << 8*(n-2)) |
... | byte[n-1];
멀티바이트 값의 이 바이트 순서는 흔히 쓰는 네트워크 바이트 순서(network byte order), 곧 빅 엔디언(big-endian) 형식이다.
주석은 /*로 시작해 */로 끝난다.
해석하지 않는 데이터를 담는 1바이트 단위의 개체는 opaque 타입(type)이다.
기존 타입 T에 대한 타입 별칭 T'은 다음과 같이 정의한다.
T T';
기본 숫자 데이터 타입은 부호 없는 바이트(uint8)다. 그보다 큰 숫자 데이터 타입은 모두 3.1절에 설명한 대로 이어 붙인 고정 길이의 바이트 열로 구성되며, 역시 부호가 없다. 다음 숫자 타입이 미리 정의되어 있다.
uint8 uint16[2];
uint8 uint24[3];
uint8 uint32[4];
uint8 uint64[8];
여기서든 명세의 다른 곳에서든 모든 값은 네트워크 바이트(빅 엔디언) 순서로 전송된다. 16진수 바이트 01 02 03 04로 표현되는 uint32는 10진수 값 16909060과 같다.
벡터(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개 */
“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;
편의를 위해 원시 타입(primitive type)으로 구조체 타입을 구성할 수 있다. 구조체를 규정할 때마다 고유한 새 타입이 선언된다. 정의에 쓰는 문법은 C의 문법과 매우 비슷하다.
struct {
T1 f1;
T2 f2;
...
Tn fn;
} T;
고정 길이와 가변 길이 목록(벡터) 필드는 표준 목록 문법으로 쓸 수 있다. 변형 예(3.8절)의 구조체 V1과 V2가 이를 보여 준다.
구조체 안의 필드는 열거형에서 쓸 수 있는 것과 매우 비슷한 문법으로, 타입 이름으로 한정할 수 있다. 예를 들어 T.f2는 앞 선언의 두 번째 필드를 가리킨다.
필드와 변수에는 다음처럼 =를 사용해 고정 값을 할당할 수 있다.
struct {
T1 f1 = 8; /* T.f1은 항상 8이어야 함 */
T2 f2;
} T;
정의된 구조체는 환경 안에서 알 수 있는 정보에 따라 변형(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;
핸드셰이크 프로토콜은 연결의 보안 매개 변수를 협상하는 데 쓰인다. 핸드셰이크 메시지는 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가 할당한다.
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에서 해시가 선택되고 나면, 협상된 해시를 바탕으로 누적 트랜스크립트 해시 값을 유지하는 방식으로 트랜스크립트를 구현할 수 있다.
다만 이후의 핸드셰이크 후 인증들은 서로를 포함하지 않고, 주 핸드셰이크가 끝날 때까지의 메시지만 포함한다는 점에 유의하라.
키 교환 메시지는 클라이언트와 서버가 지원하는 보안 기능을 파악하고, 핸드셰이크의 나머지 부분과 데이터를 보호하는 데 쓰는 트래픽 키를 비롯한 공유 비밀값을 수립하는 데 쓰인다.
TLS에서 암호 협상은 클라이언트가 ClientHello에 다음 네 가지 옵션 묶음을 제시하는 것으로 진행된다.
클라이언트가 지원하는 AEAD 알고리즘/HKDF 해시 쌍을 나타내는 암호 스위트 목록.
클라이언트가 지원하는 키 교환 그룹을 나타내는 “supported_groups”(4.3.7절) 확장과, 그 그룹 중 일부 또는 전부에 대한 키 교환 공유값을 담은 “key_share”(4.3.8절) 확장. “그룹(group)”이라는 용어는 DHE가 아닌 키 수립 알고리즘을 지원하지 않던 TLS 1.3 최초 버전에서 비롯된 역사적인 이름이다.
역주: 원문의 이 문장에는 “key key”와 짝이 없는 닫는 괄호가 있다. 오타로 보고 뜻대로 옮겼다.
클라이언트가 받아들일 수 있는 서명 알고리즘을 나타내는 “signature_algorithms”(4.3.3절) 확장. 인증서에 쓰는 서명 알고리즘을 따로 나타내기 위해 “signature_algorithms_cert” 확장(4.3.3절)을 추가할 수도 있다.
클라이언트가 아는 대칭 키 식별자의 목록을 담은 “pre_shared_key”(4.3.11절) 확장과, PSK와 함께 쓸 수 있는 키 교환 모드를 나타내는 “psk_key_exchange_modes”(4.3.9절) 확장.
서버가 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에 다음과 같이 나타낸다.
PSK를 사용하면, 서버는 선택한 키를 나타내는 “pre_shared_key” 확장을 보낸다.
비대칭 키 교환을 사용하면, 서버는 “key_share” 확장도 제공한다. PSK를 사용하지 않으면 비대칭 키 교환과 인증서 기반 인증을 항상 사용한다.
인증서로 인증하는 경우, 서버는 Certificate(4.5.1절)와 CertificateVerify(4.5.2절) 메시지를 보낸다. 이 문서가 정의하는 TLS 1.3에서는 PSK와 인증서 중 항상 하나만 쓰고 둘을 함께 쓰지는 않는다. 둘을 함께 쓰는 방법은 향후 문서에서 정의할 수도 있다.
서버가 지원하는 매개 변수 묶음을 협상할 수 없으면(즉, 클라이언트와 서버의 매개 변수 사이에 겹치는 것이 없으면), 서버는 반드시 “handshake_failure” 또는 “insufficient_security” 치명적(fatal) 경고로 핸드셰이크를 중단해야 한다(MUST). 6장을 참고한다.
클라이언트가 서버에 처음 연결할 때는 첫 TLS 메시지로 반드시 ClientHello를 보내야 한다(REQUIRED). 서버가 클라이언트의 ClientHello에 HelloRetryRequest로 응답한 경우에도 클라이언트는 ClientHello를 보낸다. 이 경우 클라이언트는 다음을 제외하고는 반드시 같은 ClientHello를 수정 없이 보내야 한다(MUST).
HelloRetryRequest에 “key_share” 확장이 들어 있었다면, 공유값 목록을 지정된 그룹의 KeyShareEntry 하나만 담은 목록으로 바꾼다.
“early_data” 확장(4.3.10절)이 있었다면 뺀다. HelloRetryRequest 다음에는 조기 데이터가 허용되지 않는다.
HelloRetryRequest에 “cookie” 확장이 들어 있었다면 그 확장을 넣는다.
“pre_shared_key” 확장이 있다면 “obfuscated_ticket_age”와 바인더 값을 다시 계산해 갱신하고, (선택적으로) 서버가 지정한 암호 스위트와 호환되지 않는 PSK를 뺀다.
“padding” 확장 [RFC7685]을 선택적으로 추가하거나 빼거나 그 길이를 바꾼다.
앞으로 정의되어 HelloRetryRequest에 들어 있는 확장이 허용하는 그 밖의 수정.
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;
legacy_version: 이전 버전의 TLS에서 이 필드는 버전 협상에 쓰였고, 클라이언트가 지원하는 가장 높은 버전 번호를 나타냈다. 그러나 경험상 많은 서버가 버전 협상을 제대로 구현하지 않았고, 그 결과 “버전 불내성(version intolerance)”이 생겼다. 버전 불내성은 다른 점에서는 받아들일 수 있는 ClientHello라도 버전 번호가 서버가 지원하는 것보다 높으면 서버가 거부하는 현상이다. TLS 1.3에서 클라이언트는 자신이 선호하는 버전을 “supported_versions” 확장(4.3.1절)으로 나타내며, legacy_version 필드는 반드시 TLS 1.2의 버전 번호인 0x0303으로 설정해야 한다(MUST). TLS 1.3 ClientHello는 legacy_version이 0x0303이고, supported_versions 확장이 있으며 그 안에 표시된 가장 높은 버전이 0x0304인 것으로 식별한다. (하위 호환성에 관한 자세한 내용은 부록 E를 참고한다.) legacy_version 값이 0x0303이 아닌 ClientHello를 받은 서버는 반드시 “protocol_version” 경고로 핸드셰이크를 중단해야 한다(MUST).
random: 안전한 난수 생성기로 생성한 32바이트. 자세한 내용은 부록 C를 참고한다.
legacy_session_id: TLS 1.3 이전 버전의 TLS는 “세션 재개” 기능을 지원했고, 이 버전에서는 그 기능이 사전 공유 키와 합쳐졌다(2.2절 참고). TLS 1.3 이전 서버가 설정한 세션 ID를 캐시해 둔 클라이언트는 이 필드를 그 값으로 설정해야 한다(SHOULD). 호환 모드(부록 E.4 참고)에서는 이 필드에 반드시 값이 들어 있어야 하므로(MUST), TLS 1.3 이전 세션을 제시하지 않는 클라이언트는 반드시 새 32바이트 값을 생성해야 한다(MUST). 이 값은 무작위일 필요는 없지만, 구현이 특정 값에 고착되는 것(경직화(ossification)라고도 한다)을 피하기 위해 예측할 수 없어야 한다(SHOULD). 그 밖의 경우에는 이 필드를 반드시 길이가 0인 목록(즉, 값이 0인 1바이트 길이 필드)으로 설정해야 한다(MUST).
cipher_suites: 클라이언트가 지원하는 대칭 암호 옵션, 구체적으로는 레코드 보호 알고리즘(비밀 키 길이 포함)과 HKDF에 쓸 해시의 목록. 클라이언트가 선호하는 것부터 차례로 나열한다. 값은 부록 B.4에 정의되어 있다. 목록에 서버가 인식하지 못하거나, 지원하지 않거나, 쓰고 싶지 않은 암호 스위트가 있으면, 서버는 반드시 그 암호 스위트를 무시하고 나머지를 평소대로 처리해야 한다(MUST). 클라이언트가 PSK 키 수립을 시도한다면, PSK에 연관된 해시를 나타내는 암호 스위트를 하나 이상 알려야 한다(SHOULD).
legacy_compression_methods: TLS 1.3 이전 버전의 TLS는 압축을 지원했고, 지원하는 압축 방법의 목록을 이 필드로 보냈다. 모든 TLS 1.3 ClientHello에서 이 목록은 반드시 0으로 설정된 정확히 1바이트를 담아야 한다(MUST). 이 값은 이전 버전 TLS의 “null” 압축 방법에 해당한다. 이 필드에 다른 값이 담긴 TLS 1.3 ClientHello를 받으면, 서버는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST). TLS 1.3 서버가 다른 압축 방법을 담은 TLS 1.2 이하의 ClientHello를 받을 수도 있으며, (그런 이전 버전을 협상한다면) 해당 이전 버전 TLS의 절차를 반드시 따라야 한다(MUST)는 점에 유의하라.
extensions: 클라이언트는 확장 필드에 데이터를 담아 보내 서버에 확장 기능을 요청한다. 실제 “Extension” 형식은 4.3절에 정의되어 있다. TLS 1.3에서는 일부 확장의 사용이 필수다. 이전 버전 TLS와의 ClientHello 호환성을 유지하기 위해 기능이 확장으로 옮겨졌기 때문이다. 서버는 인식하지 못한 확장을 반드시 무시해야 한다(MUST).
모든 버전의 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절)를 전송할 수 있다.
서버는 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;
legacy_version: 이전 버전의 TLS에서 이 필드는 버전 협상에 쓰였고, 연결에 선택된 버전 번호를 나타냈다. 안타깝게도 일부 미들박스는 새 값을 받으면 실패한다. TLS 1.3에서 TLS 서버는 자신의 버전을 “supported_versions” 확장(4.3.1절)으로 나타내며, legacy_version 필드는 반드시 TLS 1.2의 버전 번호인 0x0303으로 설정해야 한다(MUST). (하위 호환성에 관한 자세한 내용은 부록 E를 참고한다.) legacy_version 값이 0x0303이 아닌 TLS 1.3 ServerHello를 받은 클라이언트는 반드시 “protocol_version” 경고로 핸드셰이크를 중단해야 한다(MUST).
random: 안전한 난수 생성기로 생성한 32바이트. 자세한 내용은 부록 C를 참고한다. TLS 1.2 또는 TLS 1.1을 협상하는 경우 마지막 8바이트는 반드시 아래 설명대로 덮어써야 하지만(MUST), 나머지 바이트는 반드시 무작위여야 한다(MUST). 이 구조체는 서버가 생성하며, 반드시 ClientHello.random과 독립적으로 생성해야 한다(MUST).
역주: 정오표 9042와 9043(둘 다 Reported, 미확인, 같은 내용)은 TLS 1.1이 RFC 8996으로 사용 중단되었으므로 “또는 TLS 1.1”을 빼자고 제안한다.
legacy_session_id_echo: 클라이언트의 legacy_session_id 필드 내용. 클라이언트의 값이 캐시된 TLS 1.3 이전 세션에 해당하고 서버가 그 세션을 재개하지 않기로 했더라도 이 필드는 그대로 되돌려 보낸다는 점에 유의하라. ClientHello에 보낸 값과 일치하지 않는 legacy_session_id_echo 필드를 받은 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).
cipher_suite: 서버가 ClientHello.cipher_suites 목록에서 선택한 암호 스위트 하나. 자신이 제시하지 않은 암호 스위트를 받은 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).
legacy_compression_method: 반드시 값이 0이어야 하는(MUST) 1바이트. 이 필드에 다른 값이 담긴 TLS 1.3 ServerHello를 받으면, 클라이언트는 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).
extensions: 확장 목록. ServerHello에는 반드시 암호 컨텍스트를 수립하고 프로토콜 버전을 협상하는 데 필요한 확장만 넣어야 한다(MUST). 모든 TLS 1.3 ServerHello 메시지에는 반드시 “supported_versions” 확장이 들어 있어야 한다(MUST). 현재의 ServerHello 메시지에는 여기에 더해 “pre_shared_key” 확장이나 “key_share” 확장, 또는 (PSK를 비대칭 키 수립과 함께 쓸 때는) 둘 다 들어 있다. 그 밖의 확장(4.3절 참고)은 EncryptedExtensions 메시지에 담아 따로 보낸다.
미들박스와의 하위 호환성 때문에(부록 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을 협상한 뒤에는 재협상을 할 수 없다는 점에 유의하라.
서버는 받아들일 수 있는 매개 변수 묶음을 찾을 수 있지만 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).
여러 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_type”은 특정 확장 유형을 식별한다.
“extension_data”는 그 확장 유형에 고유한 정보를 담는다.
“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절 참고).
이 프로토콜에서는 새 기능과 기존 기능 사이에 미묘한(그리고 그다지 미묘하지 않은) 상호 작용이 일어나 전체 보안이 크게 떨어질 수 있다. 새 확장을 설계할 때는 다음 사항을 고려해야 한다.
서버가 확장에 동의하지 않는 경우 중에는 오류 상황인 것(예: 핸드셰이크를 계속할 수 없음)도 있고, 단순히 특정 기능의 지원을 거부하는 것도 있다. 일반적으로 전자에는 오류 경고를, 후자에는 서버 확장 응답의 필드를 써야 한다.
확장은 가능한 한, 핸드셰이크 메시지를 조작해 특정 기능의 사용(또는 사용하지 않음)을 강제하는 공격을 막도록 설계해야 한다. 이 원칙은 그 기능이 보안 문제를 일으킨다고 여겨지는지와 관계없이 따라야 한다. 확장 필드가 Finished 메시지 해시의 입력에 포함된다는 사실만으로 충분한 경우가 많지만, 확장이 핸드셰이크 단계에서 보내는 메시지의 의미를 바꿀 때는 각별히 주의해야 한다. 설계자와 구현자는 핸드셰이크가 인증되기 전까지는 능동 공격자가 메시지를 수정하고 확장을 삽입·제거·교체할 수 있다는 사실을 알고 있어야 한다.
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;
쿠키에는 두 가지 주된 목적이 있다.
서버가 클라이언트에게, 클라이언트로 보이는 네트워크 주소에 실제로 도달할 수 있음을 입증하도록 강제할 수 있게 한다(그래서 일정한 DoS 방어를 제공한다). 이는 주로 연결 지향이 아닌 전송에 유용하다(그 예는 [RFC9147] 참고).
서버가 상태를 클라이언트에게 떠넘겨, 아무 상태도 저장하지 않고 HelloRetryRequest를 보낼 수 있게 한다. 서버는 ClientHello의 해시를 (적절한 무결성 보호 알고리즘으로 보호해) HelloRetryRequest의 쿠키에 저장하는 방식으로 이렇게 할 수 있다.
HelloRetryRequest를 보낼 때 서버는 클라이언트에게 “cookie” 확장을 제공할 수 있다(MAY)(이는 ClientHello에 나타난 확장만 보낼 수 있다는 일반 규칙의 예외다). 새 ClientHello를 보낼 때 클라이언트는 반드시 HelloRetryRequest에서 받은 확장의 내용을 새 ClientHello의 “cookie” 확장에 복사해야 한다(MUST). 클라이언트는 이후 연결의 처음 ClientHello에 쿠키를 사용해서는 안 된다(MUST NOT).
서버가 무상태로(statelessly) 동작할 때는 첫 번째와 두 번째 ClientHello 사이에 보호되지 않은 change_cipher_spec 유형의 레코드를 받을 수도 있다(5장 참고). 서버는 아무 상태도 저장하지 않으므로, 이 레코드는 처음 받는 메시지인 것처럼 보인다. 무상태로 동작하는 서버는 반드시 이런 레코드를 무시해야 한다(MUST).
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에서 먼저 지정된 해시 알고리즘으로 입력을 해시한 다음 평소대로 진행하도록 정의해야 한다. 위에 나열한 코드 포인트 그룹의 의미는 다음과 같다.
RSASSA-PKCS1-v1_5 알고리즘: [SHS]에 정의된 해당 해시 알고리즘과 함께 RSASSA-PKCS1-v1_5 [RFC8017]를 쓰는 서명 알고리즘을 나타낸다. 이 값들은 인증서에 나타나는 서명만 가리키며(4.5.1.2절 참고) 서명된 TLS 핸드셰이크 메시지에 쓰도록 정의되지 않았다. 다만 TLS 1.2와의 하위 호환성을 위해 “signature_algorithms”와 “signature_algorithms_cert”에 나타날 수는 있다(MAY).
ECDSA 알고리즘: ECDSA [DSS], NIST SP 800-186 [ECDP]에 정의된 해당 곡선, [SHS]에 정의된 해당 해시 알고리즘을 쓰는 서명 알고리즘을 나타낸다. 서명은 [RFC4492]에 정의된, DER 인코딩된 [X690] ECDSA-Sig-Value 구조체로 표현한다.
RSASSA-PSS RSAE 알고리즘: [RFC8017]에 정의된 대로 마스크 생성 함수(mask generation function) MGF1과 함께 RSASSA-PSS를 쓰는 서명 알고리즘을 나타낸다. MGF1에 쓰는 다이제스트와 서명 대상 다이제스트는 모두 [SHS]에 정의된 해당 해시 알고리즘이다. 솔트(Salt)의 길이는 반드시 다이제스트 알고리즘 출력의 길이와 같아야 한다(MUST). 공개 키를 X.509 인증서에 담는다면 반드시 rsaEncryption OID [RFC3279]를 사용해야 한다(MUST).
EdDSA 알고리즘: [RFC8032] 또는 그 후속 문서에 정의된 EdDSA를 쓰는 서명 알고리즘을 나타낸다. 이는 “prehash” 변형이 아니라 “PureEdDSA” 알고리즘에 해당한다는 점에 유의하라.
RSASSA-PSS PSS 알고리즘: [RFC8017]에 정의된 대로 마스크 생성 함수 MGF1과 함께 RSASSA-PSS를 쓰는 서명 알고리즘을 나타낸다. MGF1에 쓰는 다이제스트와 서명 대상 다이제스트는 모두 [SHS]에 정의된 해당 해시 알고리즘이다. 솔트의 길이는 반드시 다이제스트 알고리즘의 길이와 같아야 한다(MUST). 공개 키를 X.509 인증서에 담는다면 반드시 RSASSA-PSS OID [RFC5756]를 사용해야 한다(MUST). 인증서 서명에 쓸 때 알고리즘 매개 변수는 반드시 DER로 인코딩해야 한다(MUST). 대응하는 공개 키의 매개 변수가 있으면, 서명의 매개 변수는 반드시 공개 키의 매개 변수와 같아야 한다(MUST).
레거시 알고리즘: 약점이 알려진 알고리즘을 쓰기 때문에 사용이 중단되고 있는 알고리즘을 나타낸다. 여기서 그 알고리즘은 SHA-1이며, 이 맥락에서는 (1) RSASSA-PKCS1-v1_5를 쓰는 RSA 또는 (2) ECDSA와 함께 쓰인다. 이 값들은 인증서에 나타나는 서명만 가리키며(4.5.1.2절 참고) 서명된 TLS 핸드셰이크 메시지에 쓰도록 정의되지 않았다. 다만 TLS 1.2와의 하위 호환성을 위해 “signature_algorithms”와 “signature_algorithms_cert”에 나타날 수는 있다(MAY). 엔드포인트는 이 알고리즘들을 협상하지 않아야 하지만(SHOULD NOT), 하위 호환성만을 위해 협상하는 것은 허용된다. 이 값들을 제시하는 클라이언트는 반드시 가장 낮은 우선순위로(SignatureSchemeList에서 다른 모든 알고리즘 뒤에) 나열해야 한다(MUST). TLS 1.3 서버는 SHA-1로 서명된 인증서 없이는 유효한 인증서 체인을 만들 수 없는 경우가 아니면 SHA-1로 서명된 인증서를 제시해서는 안 된다(MUST NOT)(4.5.1.2절 참고).
자체 서명된 인증서나 신뢰 앵커(trust anchor)인 인증서는 인증 경로(certification path)를 시작하므로, 그 서명은 검증하지 않는다([RFC5280]의 3.2절 참고). 인증 경로를 시작하는 인증서는 “signature_algorithms”와 “signature_algorithms_cert” 확장에서 지원한다고 알리지 않은 서명 알고리즘을 사용할 수 있다(MAY).
TLS 1.2는 이 확장을 다르게 정의한다는 점에 유의하라. TLS 1.2를 협상할 의향이 있는 TLS 1.3 구현은 그 버전을 협상할 때 반드시 [RFC5246]의 요구 사항에 따라 동작해야 한다(MUST). 특히 다음과 같다.
TLS 1.2 ClientHello는 이 확장을 생략할 수 있다(MAY).
TLS 1.2에서 이 확장은 해시/서명 쌍을 담았다. 이 쌍은 2옥텟으로 인코딩되므로, SignatureScheme 값은 TLS 1.2의 인코딩에 맞춰 할당되었다. 일부 레거시 쌍은 할당되지 않은 채 남아 있다. 이 알고리즘들은 TLS 1.3부터 사용이 중단되었다. 어떤 구현도 이 알고리즘들을 제시하거나 협상해서는 안 된다(MUST NOT). 특히 MD5 [SLOTH], SHA-224, DSA는 사용해서는 안 된다(MUST NOT).
ECDSA 서명 방식은 TLS 1.2의 ECDSA 해시/서명 쌍에 맞춰져 있다. 그러나 옛 의미 규칙은 서명 곡선을 제한하지 않았다. TLS 1.2를 협상하면, 구현은 자신이 “supported_groups” 확장에서 알린 어떤 곡선을 쓰는 서명이든 받아들일 준비가 반드시 되어 있어야 한다(MUST).
RSASSA-PSS(TLS 1.3에서 필수) 지원을 알리는 구현은 TLS 1.2를 협상하더라도 그 방식을 쓰는 서명을 받아들일 준비가 반드시 되어 있어야 한다(MUST). TLS 1.2에서 RSASSA-PSS는 RSA 암호 스위트와 함께 쓰인다.
“certificate_authorities” 확장은 엔드포인트가 지원하는 인증 기관(certificate authority, CA)을 나타내는 데 쓰이며, 수신하는 엔드포인트는 이를 인증서 선택의 지침으로 사용해야 한다(SHOULD).
“certificate_authorities” 확장의 본문은 CertificateAuthoritiesExtension 구조체로 이루어진다.
opaque DistinguishedName<1..2^16-1>;
struct {
DistinguishedName authorities<3..2^16-1>;
} CertificateAuthoritiesExtension;
authorities: 받아들일 수 있는 인증 기관의 식별 이름(distinguished name) [X501] 목록. DER 인코딩된 [X690] 형식으로 표현한다. 이 식별 이름들은 신뢰 앵커나 하위 CA로 원하는 식별 이름을 지정한다. 따라서 이 메시지는 알려진 신뢰 앵커뿐 아니라 원하는 인가 공간(authorization space)을 설명하는 데에도 쓸 수 있다.클라이언트는 “certificate_authorities” 확장을 ClientHello 메시지에 담아 보낼 수 있다(MAY). 서버는 이 확장을 CertificateRequest 메시지에 담아 보낼 수 있다(MAY).
비슷한 목적을 가졌지만 더 복잡한 “trusted_ca_keys” 확장 [RFC6066]은 TLS 1.3에서 쓰이지 않는다(다만 이전 TLS 버전을 제시하는 클라이언트의 ClientHello 메시지에는 나타날 수 있다).
“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;
filters: 허용되는 값과 함께 나열한 인증서 확장 OID [RFC5280]의 목록. DER 인코딩된 [X690] 형식으로 표현한다. 일부 인증서 확장 OID(예: Extended Key Usage)는 값을 여러 개 허용한다. 서버가 비어 있지 않은 filters 목록을 넣었다면, 응답에 들어가는 클라이언트 인증서는 지정된 확장 OID 가운데 클라이언트가 인식하는 것을 반드시 모두 담고 있어야 한다(MUST). 클라이언트가 인식하는 각 확장 OID에 대해, 지정된 값은 반드시 모두 클라이언트 인증서에 있어야 한다(MUST)(다만 인증서에는 다른 값이 더 있을 수 있다(MAY)). 그러나 클라이언트는 인식하지 못한 인증서 확장 OID는 반드시 무시하고 건너뛰어야 한다(MUST). 클라이언트가 요구된 인증서 확장 OID 일부를 무시하고 요청을 충족하지 않는 인증서를 제공했다면, 서버는 재량에 따라 클라이언트 인증 없이 연결을 계속하거나 “unsupported_certificate” 경고로 핸드셰이크를 중단할 수 있다(MAY). 같은 OID가 filters 목록에 두 번 이상 나타나서는 안 된다(MUST NOT).PKIX RFC들은 다양한 인증서 확장 OID와 그에 대응하는 값 유형을 정의한다. 유형에 따라, 일치하는 인증서 확장 값이 꼭 비트 단위로 같은 것은 아니다. TLS 구현은 인증서 확장 OID를 사용한 인증서 선택을 자신의 PKI 라이브러리에 맡길 것으로 예상된다.
이 문서는 [RFC5280]에 정의된 표준 인증서 확장 두 가지에 대한 일치 규칙을 정의한다.
인증서의 Key Usage 확장은 요청에 설정된 모든 키 용도 비트가 Key Usage 인증서 확장에도 설정되어 있을 때 요청과 일치한다.
인증서의 Extended Key Usage 확장은 요청에 있는 모든 키 목적 OID가 Extended Key Usage 인증서 확장에도 있을 때 요청과 일치한다. 특수한 anyExtendedKeyUsage OID는 요청에 사용해서는 안 된다(MUST NOT).
다른 인증서 확장에 대한 일치 규칙은 별도의 명세에서 정의할 수 있다.
“post_handshake_auth” 확장은 클라이언트가 핸드셰이크 후 인증(4.7.2절)을 할 의향이 있음을 나타내는 데 쓰인다. 서버는 이 확장을 제시하지 않는 클라이언트에게 핸드셰이크 후 CertificateRequest를 보내서는 안 된다(MUST NOT). 서버는 이 확장을 보내서는 안 된다(MUST NOT).
struct {} PostHandshakeAuth;
“post_handshake_auth” 확장의 “extension_data” 필드는 길이가 0이다.
클라이언트가 보낼 때 “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;
타원 곡선 그룹(ECDHE): NIST SP 800-186 [ECDP] 또는 [RFC7748]에 정의된 해당 이름 붙은 곡선의 지원을 나타낸다. 0xFE00부터 0xFEFF까지의 값은 사설 용도(Private Use) [RFC8126]로 예약되어 있다.
유한체 그룹(DHE): [RFC7919]에 정의된 해당 유한체 그룹의 지원을 나타낸다. 0x01FC부터 0x01FF까지의 값은 사설 용도로 예약되어 있다.
“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).
“key_share” 확장은 엔드포인트의 암호 매개 변수를 담는다.
클라이언트는 추가 왕복을 대가로 서버에 그룹 선택을 요청하기 위해 빈 client_shares 목록을 보낼 수 있다(MAY)(4.2.4절 참고).
struct {
NamedGroup group;
opaque key_exchange<1..2^16-1>;
} KeyShareEntry;
group: 교환하는 키의 이름 붙은 그룹.
key_exchange: 키 교환 정보. 이 필드의 내용은 지정된 그룹과 그에 대응하는 정의에 따라 정해진다. 유한체 Diffie-Hellman [DH76] 매개 변수는 4.3.8.1절에, 타원 곡선 Diffie-Hellman 매개 변수는 4.3.8.2절에 설명되어 있다.
ClientHello 메시지에서 이 확장의 “extension_data” 필드에는 “KeyShareClientHello” 값이 들어 있다.
struct {
KeyShareEntry client_shares<0..2^16-1>;
} KeyShareClientHello;
client_shares: 제시하는 KeyShareEntry 값의 목록. 클라이언트가 선호하는 것부터 나열한다.클라이언트가 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;
selected_group: 서버가 협상하려는, 양쪽이 함께 지원하는 그룹. 서버는 이 그룹으로 ClientHello/KeyShare를 다시 보내 달라고 요청한다.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;
server_share: 클라이언트의 공유값 중 하나와 같은 그룹에 속하는 KeyShareEntry 값 하나.비대칭 키 수립을 사용하면, 서버는 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).
클라이언트와 서버의 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)으로 몰아넣고 있지 않음을 보장한다.
클라이언트와 서버의 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은 이 기능을 없애고 곡선마다 단일 점 형식을 쓴다.
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;
psk_ke: PSK 단독 키 수립. 이 모드에서 서버는 “key_share” 값을 제공해서는 안 된다(MUST NOT).
psk_dhe_ke: PSK와 비대칭 키 수립의 결합(레이블의 “dhe”는 DHE가 아닌 키 수립 알고리즘을 지원하지 않던 TLS 1.3 최초 버전에서 비롯된 역사적인 이름이다). 이 모드에서 클라이언트와 서버는 반드시 4.3.8절에 설명된 대로 “key_share” 값을 제공해야 한다(MUST).
역주: 원문의 이 괄호 안에도 앞서 본 것과 같은 “key key” 오타가 있다.
앞으로 할당되는 값은 전송되는 프로토콜 메시지만으로 서버가 어느 모드를 선택했는지 모호함 없이 식별할 수 있도록 보장해야 한다. 현재는 ServerHello에 “key_share”가 있는지로 이를 나타낸다.
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).
확장을 무시하고 일반 1-RTT 응답을 돌려준다. 그런 다음 서버는 받은 레코드를 핸드셰이크 트래픽 키로 보호 해제(deprotect)하려 시도하고, 보호 해제에 실패한 레코드는 (구성된 max_early_data_size까지) 버리는 방식으로 조기 데이터를 건너뛴다. 레코드가 성공적으로 보호 해제되면, 서버는 그 레코드를 클라이언트의 두 번째 플라이트의 시작으로 보고 일반 1-RTT 핸드셰이크처럼 진행한다.
HelloRetryRequest로 응답해 클라이언트가 다른 ClientHello를 보내도록 요청한다. 클라이언트는 이어지는 ClientHello에 “early_data” 확장을 넣어서는 안 된다(MUST NOT). 그런 다음 서버는 외부 콘텐츠 유형이 “application_data”인(암호화되었음을 나타내는) 레코드를 구성된 max_early_data_size까지 모두 건너뛰는 방식으로 조기 데이터를 무시한다.
EncryptedExtensions에 자신의 “early_data” 확장을 담아 돌려주어, 조기 데이터를 처리하려 함을 나타낸다. 서버가 조기 데이터 메시지 중 일부만 받아들일 수는 없다. 서버가 조기 데이터를 받아들이는 메시지를 보내더라도, 서버가 이 메시지를 생성할 무렵에는 실제 조기 데이터가 이미 전송 중일 수 있다.
조기 데이터를 받아들이려면 서버는 반드시 클라이언트의 “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).
“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;
identity: 키의 레이블. 예를 들어 티켓(부록 B.3.4에 정의됨)이나 외부에서 수립한 사전 공유 키의 레이블이다.
obfuscated_ticket_age: 키 나이를 난독화(obfuscate)한 값. NewSessionTicket 메시지로 수립한 식별자에 대해 이 값을 만드는 방법은 4.3.11.1절에 설명되어 있다. 외부에서 수립한 식별자에는 obfuscated_ticket_age로 0을 써야 하며(SHOULD), 서버는 반드시 그 값을 무시해야 한다(MUST).
identities: 클라이언트가 서버와 협상할 의향이 있는 식별자 목록. “early_data” 확장(4.3.10절 참고)과 함께 보낸다면, 첫 번째 식별자가 0-RTT 데이터에 쓰는 식별자다.
binders: identities 목록의 값마다 하나씩, 같은 순서로 나열한 일련의 HMAC 값. 아래 설명대로 계산한다.
selected_identity: 서버가 선택한 식별자. 클라이언트의 “OfferedPsks.identities” 목록 안 식별자에 대한 (0부터 시작하는) 인덱스로 나타낸다.
각 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).
클라이언트가 보는 티켓의 나이는 NewSessionTicket 메시지를 받은 뒤로 지난 시간이다. 클라이언트는 티켓과 함께 제공된 “ticket_lifetime” 값보다 나이가 많은 티켓을 사용하려 해서는 안 된다(MUST NOT). 각 PskIdentity의 “obfuscated_ticket_age” 필드에는 티켓 나이를 난독화한 값이 들어 있다. 이 값은 밀리초 단위의 나이에 티켓과 함께 받은 “ticket_age_add” 값(4.7.1절 참고)을 더하고 2^32로 나눈 나머지다. 이렇게 더하면, 티켓이나 키 공유값을 재사용하지 않는 한 수동 관찰자가 연결들을 서로 연관 짓지 못한다. NewSessionTicket 메시지의 “ticket_lifetime” 필드는 초 단위이지만 “obfuscated_ticket_age”는 밀리초 단위라는 점에 유의하라. 티켓 수명은 일주일로 제한되므로, 밀리초 단위로도 있을 법한 모든 나이를 32비트로 나타내기에 충분하다.
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에 연관된 해시와 같을 수도 있고 다를 수도 있다는 점에도 유의하라. 이는 나머지 핸드셰이크의 트랜스크립트를 계산하는 방식과 일관된다.
클라이언트는 서버의 Finished를 받을 때까지 0-RTT 데이터를 “스트리밍”하다가, 그때에야 EndOfEarlyData 메시지를 보내고 나머지 핸드셰이크를 이어 갈 수 있다. 교착 상태(deadlock)를 피하기 위해, “early_data”를 받아들일 때 서버는 ServerHello를 보내기 전에 클라이언트의 EndOfEarlyData 메시지를 기다리지 말고, 반드시 클라이언트의 ClientHello를 처리한 뒤 곧바로 자신의 메시지 플라이트를 보내야 한다(MUST).
서버가 보내는 다음 두 메시지 EncryptedExtensions와 CertificateRequest에는 핸드셰이크의 나머지를 정하는 서버의 정보가 들어 있다. 이 메시지들은 server_handshake_traffic_secret에서 유도한 키로 암호화된다.
모든 핸드셰이크에서 서버는 반드시 ServerHello 메시지 바로 다음에 EncryptedExtensions 메시지를 보내야 한다(MUST). 이 메시지는 server_handshake_traffic_secret에서 유도한 키로 암호화되는 첫 메시지다.
EncryptedExtensions 메시지에는 보호할 수 있는 확장, 즉 암호 컨텍스트를 수립하는 데는 필요하지 않으면서 개별 인증서에 연관되지도 않은 확장이 들어 있다. 클라이언트는 반드시 EncryptedExtensions에 금지된 확장이 있는지 확인해야 하며(MUST), 발견하면 반드시 “illegal_parameter” 경고로 핸드셰이크를 중단해야 한다(MUST).
이 메시지의 구조는 다음과 같다.
struct {
Extension extensions<0..2^16-1>;
} EncryptedExtensions;
extensions: 확장 목록. 자세한 내용은 4.3절의 표를 참고한다.인증서로 인증하는 서버는 선택적으로 클라이언트에게 인증서를 요청할 수 있다(MAY). 이 메시지를 보낸다면 반드시 EncryptedExtensions 다음에 와야 한다(MUST).
이 메시지의 구조는 다음과 같다.
struct {
opaque certificate_request_context<0..2^8-1>;
Extension extensions<0..2^16-1>;
} CertificateRequest;
certificate_request_context: 인증서 요청을 식별하는 불투명한 문자열로, 클라이언트의 Certificate 메시지에서 그대로 되돌아온다. certificate_request_context는 반드시 이 연결의 범위 안에서 고유해야 하며(MUST), 이로써 클라이언트 CertificateVerify 메시지의 재전송을 막는다. 이 필드는 4.7.2절에 설명된 핸드셰이크 후 인증 교환에 쓰는 경우가 아니면 반드시 길이가 0이어야 한다(SHALL). 핸드셰이크 후 인증을 요청할 때, 서버는 클라이언트의 개인 키에 일시적으로 접근할 수 있는 공격자가 유효한 CertificateVerify 메시지를 미리 계산하지 못하도록, 컨텍스트를 클라이언트가 예측할 수 없게 만들어야 한다(SHOULD)(예: 무작위로 생성).
extensions: 요청하는 인증서의 매개 변수를 설명하는 확장 목록. “signature_algorithms” 확장은 반드시 지정해야 하며(MUST), 이 메시지에 대해 정의된 다른 확장도 선택적으로 넣을 수 있다. 클라이언트는 인식하지 못한 확장을 반드시 무시해야 한다(MUST).
이전 버전의 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]은 이를 허용하는 확장을 제공하지만, 이 명세보다 분석을 덜 받았다.
2장에서 논의했듯이 TLS는 일반적으로 인증, 키 확인, 핸드셰이크 무결성을 위해 Certificate, CertificateVerify, Finished라는 공통 메시지 묶음을 사용한다. (PSK 바인더도 비슷한 방식으로 키 확인을 한다.) 이 세 메시지는 항상 각 핸드셰이크 플라이트의 마지막 메시지로 보낸다. Certificate와 CertificateVerify 메시지는 아래에 정의된 특정 상황에서만 보낸다. Finished 메시지는 항상 인증 블록(Authentication Block)의 일부로 보낸다. 핸드셰이크 후 인증을 제외하면, 이 메시지들은 [sender]_handshake_traffic_secret에서 유도한 키로 암호화된다.
인증 메시지의 계산은 모두 똑같이 다음을 입력으로 받는다.
사용할 인증서와 서명 키.
트랜스크립트 해시에 포함할 메시지 목록으로 이루어진 핸드셰이크 컨텍스트(Handshake Context).
MAC 키를 계산하는 데 쓸 기본 키(Base Key).
이 입력을 바탕으로 각 메시지에는 다음이 담긴다.
Certificate: 인증에 쓸 인증서와 체인 안의 보조 인증서. 인증서 기반 클라이언트 인증은 PSK 핸드셰이크 흐름(0-RTT 포함)에서는 쓸 수 없다는 점에 유의하라.
CertificateVerify: Transcript-Hash(Handshake Context, Certificate) 값에 대한 서명.
Finished: 기본 키에서 유도한 MAC 키를 사용해 Transcript-Hash(Handshake Context, Certificate, CertificateVerify) 값에 대해 계산한 MAC.
다음 표는 각 상황의 핸드셰이크 컨텍스트와 MAC 기본 키를 정의한다.
| 모드 | 핸드셰이크 컨텍스트 | 기본 키 |
|---|---|---|
| 서버 | ClientHello … EncryptedExtensions/CertificateRequest 중 나중 것 |
server_handshake_traffic_secret |
| 클라이언트 | ClientHello … 서버 Finished/EndOfEarlyData 중 나중 것 |
client_handshake_traffic_secret |
| 핸드셰이크 후 | ClientHello … 클라이언트 Finished + CertificateRequest |
[sender]_application_traffic_secret_N |
표 2: 인증 입력
이 메시지는 엔드포인트의 인증서 체인을 피어에게 전달한다.
합의된 키 교환 방법이 인증에 인증서를 사용할 때마다(이 문서에 정의된 키 교환 방법 중 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;
certificate_request_context: 이 메시지가 CertificateRequest에 대한 응답이면, 그 메시지의 certificate_request_context 값. 그렇지 않으면(서버 인증의 경우) 이 필드는 반드시 길이가 0이어야 한다(SHALL).
certificate_list: CertificateEntry 구조체의 목록(체인). 각 구조체에는 인증서 하나와 확장 목록이 들어 있다.
extensions: CertificateEntry에 대한 확장 값의 목록. “Extension” 형식은 4.3절에 정의되어 있다. 현재 서버 인증서에 유효한 확장으로는 OCSP Status 확장 [RFC6066]과 SignedCertificateTimestamp 확장 [RFC6962]이 있으며, 앞으로 이 메시지에 대한 확장이 더 정의될 수 있다. 서버가 보낸 Certificate 메시지의 확장은 반드시 ClientHello 메시지의 확장에 대응해야 한다(MUST). 클라이언트가 보낸 Certificate 메시지의 확장은 반드시 서버가 보낸 CertificateRequest 메시지의 확장에 대응해야 한다(MUST). 확장이 체인 전체에 적용된다면, 그 확장은 첫 번째 CertificateEntry에 넣어야 한다(SHOULD).
대응하는 인증서 유형 확장(“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를 보낸다.
[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의 확장에 담긴다.
클라이언트나 서버가 보내는 인증서에는 다음 규칙이 적용된다.
달리 명시적으로 협상하지 않는 한(예: [RFC7250]), 인증서 유형은 반드시 X.509v3 [RFC5280]이어야 한다(MUST).
최종 개체 인증서는 반드시 피어의 “signature_algorithms” 확장에 표시된 서명 방식으로 서명하는 데 키를 쓰도록 허용해야 한다(MUST)(4.3.3절 참고). 즉, Key Usage 확장이 있으면 반드시 digitalSignature 비트가 설정되어 있어야 하며(MUST), 공개 키(와 연관된 제약)는 반드시 지원되는 서명 방식 중 하나와 호환되어야 한다(MUST).
피어가 “certificate_authorities” 확장을 보냈다면, 인증서 체인의 인증서 중 하나 이상은 나열된 CA 중 하나가 발급한 것이어야 한다(SHOULD).
클라이언트가 보내는 인증서에는 다음 규칙이 추가로 적용된다.
CertificateRequest 메시지에 비어 있지 않은 “oid_filters” 확장이 들어 있었다면, 최종 개체 인증서는 4.3.5절에 설명된 대로 클라이언트가 인식하는 확장 OID와 반드시 일치해야 한다(MUST).서버가 보내는 인증서에는 다음 규칙이 추가로 적용된다.
송신자가 그런 체인을 제공할 수 있다면, 송신자가 제공하는 모든 인증서는 반드시 피어가 알린 서명 알고리즘으로 서명된 것이어야 한다(MUST)(4.3.3절 참고). 자체 서명된 인증서나 신뢰 앵커로 예상되는 인증서는 체인의 일부로 검증하지 않으므로 어떤 알고리즘으로든 서명될 수 있다(MAY).
송신자가 서버이고, 서버가 표시된 지원 알고리즘만으로 서명된 인증서 체인을 만들 수 없다면, 서버는 클라이언트가 지원하는지 알 수 없는 알고리즘이 들어 있을 수도 있는, 자신이 고른 인증서 체인을 보내 핸드셰이크를 계속해야 한다(SHOULD). 이 대체(fallback) 체인은 클라이언트가 SHA-1을 받아들일 의향을 특별히 알리지 않은 한, 사용이 중단된 SHA-1 해시를 써서는 안 된다(MUST NOT).
송신자가 클라이언트라면, 클라이언트는 위와 같은 대체 체인을 쓰거나 익명으로 핸드셰이크를 계속할 수 있다(MAY).
수신자가 제공된 인증서로 받아들일 수 있는 체인을 구성하지 못해 핸드셰이크를 중단하기로 했다면, 반드시 적절한 인증서 관련 경고(기본값은 “unsupported_certificate”이며, 자세한 내용은 6.2절 참고)로 핸드셰이크를 중단해야 한다(MUST).
송신자에게 인증서가 여러 개 있으면, 송신자는 위에 언급한 기준(그리고 전송 계층 엔드포인트, 로컬 구성, 선호 같은 다른 기준)에 따라 그중 하나를 고른다.
일반적으로 자세한 인증서 검증 절차는 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 키).
이 메시지는 엔드포인트가 자신의 인증서에 대응하는 개인 키를 가지고 있다는 명시적 증거를 제공하는 데 쓰인다. 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). 검증 과정은 다음을 입력으로 받는다.
Certificate 메시지에 있는 최종 개체 인증서에 담긴 공개 키CertificateVerify 메시지의 signature 필드로 받은 디지털 서명검증에 실패하면 수신자는 반드시 “decrypt_error” 경고로 핸드셰이크를 종료해야 한다(MUST).
Finished 메시지는 인증 블록의 마지막 메시지다. 이 메시지는 핸드셰이크와 계산된 키를 인증하는 데 꼭 필요하다.
Finished 메시지의 수신자는 반드시 내용이 올바른지 확인해야 하며(MUST), 올바르지 않으면 반드시 “decrypt_error” 경고로 연결을 종료해야 한다(MUST).
한쪽이 자신의 Finished 메시지를 보내고 피어의 Finished 메시지를 받아 검증하고 나면, 그 연결로 애플리케이션 데이터를 보내고 받기 시작할 수 있다. 피어의 Finished를 받기 전에 데이터를 보내는 것이 허용되는 상황은 두 가지다.
4.3.10절에 설명된 대로 0-RTT 데이터를 보내는 클라이언트.
서버는 첫 번째 플라이트를 보낸 뒤 데이터를 보낼 수 있다(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 메시지에 대한 응답으로 서버가 보내는 경고가 포함된다.
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에서 유도한 키로 암호화된다.
TLS에서는 주 핸드셰이크 뒤에도 다른 메시지를 보낼 수 있다. 이 메시지들은 handshake 콘텐츠 유형을 쓰며 적절한 애플리케이션 트래픽 키로 암호화된다.
클라이언트의 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 참고). 예를 들어 서버는 핸드셰이크 후 인증 뒤에 새 티켓을 보내, 추가된 클라이언트 인증 상태를 캡슐화할 수도 있다. 여러 티켓은 클라이언트에게 다음을 비롯한 다양한 목적으로 유용하다.
여러 HTTP 연결을 병렬로 열기.
(예를 들어) Happy Eyeballs [RFC8305]나 관련 기법으로 인터페이스와 주소 체계에 걸쳐 연결 경쟁(connection racing) 수행하기.
모든 티켓은 반드시 원래 연결을 수립할 때 쓴 것과 같은 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;
ticket_lifetime: 티켓 발급 시점부터의 수명을 초 단위로, 네트워크 바이트 순서의 32비트 부호 없는 정수로 나타낸다. 서버는 604800초(7일)보다 큰 값을 사용해서는 안 된다(MUST NOT). 값 0은 티켓을 즉시 폐기해야 함을 나타낸다. 클라이언트는 ticket_lifetime과 관계없이 발급 후 7일보다 오래 티켓을 사용해서는 안 되며(MUST NOT), 로컬 정책에 따라 더 일찍 티켓을 삭제할 수 있다(MAY). 서버는 티켓을 ticket_lifetime에 명시된 것보다 짧은 기간 동안만 유효한 것으로 다룰 수 있다(MAY).
ticket_age_add: 안전하게 생성한 무작위 32비트 값으로, 클라이언트가 “pre_shared_key” 확장에 넣는 티켓 나이를 감추는 데 쓰인다. 클라이언트 쪽 티켓 나이에 이 값을 더하고 2^32로 나눈 나머지가 클라이언트가 전송하는 값이다. 서버는 보내는 티켓마다 반드시 새 값을 생성해야 한다(MUST).
ticket_nonce: 티켓마다 다른 값으로, 이 연결에서 발급한 모든 티켓 사이에서 고유하다.
ticket: PSK 식별자로 쓸 티켓의 값. 티켓 자체는 불투명한 레이블이다. 티켓은 데이터베이스 조회 키일 수도 있고, 자체 암호화·자체 인증한 값일 수도 있다(MAY).
extensions: 티켓에 대한 확장 값의 목록. “Extension” 형식은 4.3절에 정의되어 있다. 클라이언트는 인식하지 못한 확장을 반드시 무시해야 한다(MUST).
현재 NewSessionTicket에 정의된 확장은 “early_data” 하나뿐이며, 이 확장은 티켓을 0-RTT 데이터를 보내는 데 쓸 수 있음을 나타낸다(4.3.10절). 이 확장에는 다음 값이 들어 있다.
max_early_data_size: 클라이언트가 이 티켓을 사용할 때 보내도록 허용된 0-RTT 데이터의 최대량(바이트 단위). 애플리케이션 데이터 페이로드(즉, 평문이되 패딩이나 내부 콘텐츠 유형 바이트는 뺀 것)만 센다. max_early_data_size 바이트보다 많은 0-RTT 데이터를 받은 서버는 “unexpected_message” 경고로 연결을 종료해야 한다(SHOULD). 암호 재료가 없어 조기 데이터를 거절하는 서버는 패딩과 내용을 구별할 수 없으므로, 클라이언트는 조기 데이터 레코드에 많은 양의 패딩을 보낼 수 있다는 데 의존하지 않아야 한다(SHOULD NOT)는 점에 유의하라.티켓에 연관된 PSK는 다음과 같이 계산한다.
HKDF-Expand-Label(resumption_secret,
"resumption", ticket_nonce, Hash.length)
ticket_nonce 값은 NewSessionTicket 메시지마다 다르므로, 티켓마다 다른 PSK가 유도된다.
원칙적으로는, PSK를 쓰지 않은 최초 핸드셰이크(대개 피어의 인증서에 묶여 있었을 것이다)에서 원래 유도한 키 재료의 수명을 무기한 연장하는 새 티켓을 계속 발급할 수 있다는 점에 유의하라. 구현은 그런 키 재료의 전체 수명에 한도를 둘 것을 권고한다(RECOMMENDED). 이 한도는 피어 인증서의 수명, 그사이에 폐지되었을 가능성, 피어의 온라인 CertificateVerify 서명 이후 지난 시간을 고려해야 한다.
클라이언트가 “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값으로 서버가 응답을 구별할 수 있다).
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: KeyUpdate의 수신자가 자신의 KeyUpdate로 응답해야 하는지를 나타낸다. 구현이 다른 값을 받으면 반드시 “illegal_parameter” 경고로 연결을 종료해야 한다(MUST).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절의 한도에 이르렀을 때 연결을 종료해야 할 수도 있다.
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 레지스트리에서 할당한다.
레코드 계층은 정보 블록을 2^14바이트 이하 단위의 데이터를 담은 TLSPlaintext 레코드로 단편화한다. 메시지 경계는 기반 ContentType에 따라 다르게 다룬다. 앞으로 정의되는 콘텐츠 유형은 모두 반드시 적절한 규칙을 규정해야 한다(MUST). 이 규칙은 TLS 1.2에서 강제하던 것보다 엄격하다는 점에 유의하라.
핸드셰이크 메시지는 다음 조건을 지킨다면 하나의 TLSPlaintext 레코드로 병합하거나 여러 레코드에 걸쳐 단편화할 수 있다(MAY).
핸드셰이크 메시지를 다른 레코드 유형과 인터리빙해서는 안 된다(MUST NOT). 즉, 핸드셰이크 메시지가 두 개 이상의 레코드로 나뉜다면 그 사이에 다른 레코드가 있어서는 안 된다(MUST NOT).
핸드셰이크 메시지는 키 변경에 걸쳐 있어서는 안 된다(MUST NOT). 구현은 키 변경 바로 앞의 모든 메시지가 레코드 경계에 맞춰져 있는지 반드시 확인해야 하며(MUST), 그렇지 않으면 반드시 “unexpected_message” 경고로 연결을 종료해야 한다(MUST). ClientHello, EndOfEarlyData, ServerHello, Finished, KeyUpdate 메시지는 키 변경 바로 앞에 올 수 있으므로, 구현은 반드시 이 메시지들을 레코드 경계에 맞춰 보내야 한다(MUST).
구현은 길이가 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;
type: 들어 있는 조각을 처리하는 데 쓰는 상위 계층 프로토콜.
legacy_record_version: TLS 1.3 구현이 생성하는 모든 레코드에서 반드시 0x0303으로 설정해야 한다(MUST). 단, 처음 ClientHello(즉, HelloRetryRequest 뒤에 생성되지 않은 것)는 예외로, 호환성을 위해 0x0301일 수도 있다(MAY). 이 필드는 사용이 중단되었으며 어떤 목적으로든 반드시 무시해야 한다(MUST). 이전 버전의 TLS는 일부 상황에서 이 필드에 다른 값을 썼다.
length: 뒤따르는 TLSPlaintext.fragment의 길이(바이트 단위). 이 길이는 2^14바이트를 넘어서는 안 된다(MUST NOT). 이 길이를 넘는 레코드를 받은 엔드포인트는 반드시 “record_overflow” 경고로 연결을 종료해야 한다(MUST).
fragment: 전송하는 데이터. 이 값은 투명하며, type 필드가 지정하는 상위 계층 프로토콜이 다룰 독립적인 블록으로 취급된다.
이 문서는 버전 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장 참고).
레코드 보호 함수는 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;
content: TLSPlaintext.fragment 값. 핸드셰이크 메시지나 경고 메시지의 바이트 인코딩, 또는 보낼 애플리케이션 데이터의 원시 바이트가 들어 있다.
type: 레코드의 콘텐츠 유형을 담은 TLSPlaintext.type 값.
zeros: 평문에서 type 필드 뒤에 임의 길이의 0 값 바이트가 나타날 수 있다. 이로써 송신자는 전체가 레코드 크기 한도 안에 머무는 한, 어떤 TLS 레코드든 선택한 양만큼 패딩할 기회를 얻는다. 자세한 내용은 5.4절을 참고한다.
opaque_type: TLSCiphertext 레코드의 바깥쪽 opaque_type 필드는 이전 버전의 TLS를 파싱하는 데 익숙한 미들박스와 겉으로 호환되도록 항상 값 23(application_data)으로 설정한다. 레코드의 실제 콘텐츠 유형은 복호화한 뒤 TLSInnerPlaintext.type에서 찾을 수 있다.
legacy_record_version: legacy_record_version 필드는 항상 0x0303이다. TLS 1.3 TLSCiphertext는 TLS 1.3이 협상된 뒤에야 생성되므로, 다른 값을 받을 수도 있는 역사적 호환성 문제는 없다. ClientHello와 ServerHello 메시지를 비롯한 핸드셰이크 프로토콜이 프로토콜 버전을 인증하므로 이 값은 중복이라는 점에 유의하라.
length: 뒤따르는 TLSCiphertext.encrypted_record의 길이(바이트 단위). 이 길이는 내용과 패딩의 길이 합에 내부 콘텐츠 유형에 쓰는 1바이트를 더하고, AEAD 알고리즘이 덧붙인 확장분(expansion)을 더한 값이다. 이 길이는 2^14 + 256바이트를 넘어서는 안 된다(MUST NOT). 이 길이를 넘는 레코드를 받은 엔드포인트는 반드시 “record_overflow” 경고로 연결을 종료해야 한다(MUST).
encrypted_record: 직렬화한 TLSInnerPlaintext 구조체를 AEAD로 암호화한 형태.
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옥텟을 더한 것이다.
읽기와 쓰기에 대해 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 구성에 쓰는 레코드별 논스는 다음과 같이 만든다.
64비트 레코드 순서 번호를 네트워크 바이트 순서로 인코딩하고, 왼쪽을 0으로 채워 iv_length에 맞춘다.
채운 순서 번호를 (역할에 따라) 고정된 client_write_iv 또는 server_write_iv와 XOR한다.
그 결과로 나온 (길이가 iv_length인) 값을 레코드별 논스로 쓴다.
참고: 이는 일부가 명시적인 논스를 규정한 TLS 1.2의 구성과 다르다.
암호화된 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 확장 또는 다른 방법으로 패딩 정책 요청 메커니즘을 정의할 수 있다.
주어진 키 묶음으로 안전하게 암호화할 수 있는 평문의 양에는 암호학적 한도가 있다. [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의 경우, 안전 한도에 이르기 전에 레코드 순서 번호가 먼저 한 바퀴 돌게 된다.
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;
클라이언트와 서버는 절단 공격(truncation attack)을 피하기 위해 연결이 끝나고 있다는 사실을 서로 알고 있어야 한다.
close_notify: 이 경고는 송신자가 이 연결에서 더 이상 메시지를 보내지 않을 것임을 수신자에게 알린다. 종료 경고를 받은 뒤에 받은 데이터는 반드시 무시해야 한다(MUST). 이 경고는 반드시 AlertLevel=warning으로 보내야 한다(MUST).
user_canceled: 이 경고는 송신자가 프로토콜 실패와 관계없는 어떤 이유로 핸드셰이크를 취소하고 있음을 수신자에게 알린다. 핸드셰이크가 끝난 뒤 사용자가 작업을 취소한다면, “close_notify”를 보내 연결을 닫기만 하는 편이 더 적절하다. 이 경고 뒤에는 반드시 “close_notify”가 와야 한다(MUST). 이 경고는 일반적으로 AlertLevel=warning이다. 받는 구현은 “close_notify”를 받을 때까지 피어에게서 데이터를 계속 읽어야 한다(SHOULD). 다만 이 경고를 로그에 남기거나 달리 기록할 수는 있다(MAY).
어느 쪽이든 “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)이 데이터 전송을 관리하는 방식을 규정하지 않는다.
참고: 연결의 쓰기 쪽을 닫으면 전송 계층을 없애기 전에 대기 중인 데이터가 확실히 전달된다고 가정한다.
TLS의 오류 처리는 매우 단순하다. 오류가 감지되면, 감지한 쪽이 피어에게 메시지를 보낸다. 치명적 경고 메시지를 보내거나 받으면 양쪽 모두 반드시 즉시 연결을 닫아야 한다(MUST).
치명적 오류 상황을 만나면 구현은 적절한 치명적 경고를 보내야 하며(SHOULD), 추가 데이터를 보내거나 받지 않고 반드시 연결을 닫아야 한다(MUST). 이 명세 전체에서 “연결을 종료한다(terminate the connection)”와 “핸드셰이크를 중단한다(abort the handshake)”라는 표현을 특정 경고 없이 쓸 때는, 구현이 아래 설명에 나타난 경고를 보내야 한다(SHOULD)는 뜻이다. “X 경고로 연결을 종료한다”와 “X 경고로 핸드셰이크를 중단한다”라는 표현은, 구현이 경고를 보낸다면 반드시 경고 X를 보내야 한다(MUST)는 뜻이다. 이 절에서 아래에 정의한 모든 경고와 알 수 없는 모든 경고는 TLS 1.3부터 예외 없이 치명적인 것으로 간주한다(6장 참고). 구현은 경고를 보내고 받는 것을 로그로 남기기 쉽게 하는 방법을 제공해야 한다(SHOULD).
다음 오류 경고가 정의되어 있다.
unexpected_message: 부적절한 메시지(예: 잘못된 핸드셰이크 메시지, 너무 이른 애플리케이션 데이터 등)를 받았다. 올바른 구현 사이의 통신에서는 이 경고가 나타날 일이 없다.
bad_record_mac: 보호 해제할 수 없는 레코드를 받으면 이 경고를 돌려준다. AEAD 알고리즘은 복호화와 검증을 한데 묶어 수행하기 때문에, 또 부채널 공격(side-channel attack)을 피하기 위해서도, 이 경고는 모든 보호 해제 실패에 쓴다. 네트워크에서 메시지가 손상된 경우를 제외하면, 올바른 구현 사이의 통신에서는 이 경고가 나타날 일이 없다.
record_overflow: 길이가 2^14 + 256바이트보다 큰 TLSCiphertext 레코드를 받았거나, 복호화한 레코드가 2^14바이트(또는 협상된 다른 한도)보다 큰 TLSPlaintext 레코드가 되었다. 네트워크에서 메시지가 손상된 경우를 제외하면, 올바른 구현 사이의 통신에서는 이 경고가 나타날 일이 없다.
handshake_failure: “handshake_failure” 경고 메시지를 받았다는 것은 송신자가 주어진 선택지로는 받아들일 수 있는 보안 매개 변수 묶음을 협상할 수 없었다는 뜻이다.
bad_certificate: 인증서가 손상되었거나, 올바르게 검증되지 않는 서명이 들어 있는 등의 문제가 있다.
unsupported_certificate: 인증서가 지원하지 않는 유형이다.
certificate_revoked: 인증서를 서명자가 폐지했다.
certificate_expired: 인증서가 만료되었거나 현재 유효하지 않다.
certificate_unknown: 인증서를 처리하다 그 밖의 (명시되지 않은) 문제가 생겨 인증서를 받아들일 수 없다.
illegal_parameter: 핸드셰이크의 필드가 올바르지 않거나 다른 필드와 일관되지 않다. 이 경고는 형식적인 프로토콜 문법에는 맞지만 그 밖의 점에서 올바르지 않은 오류에 쓰인다.
unknown_ca: 유효한 인증서 체인이나 부분 체인을 받았지만, CA 인증서를 찾을 수 없거나 알려진 신뢰 앵커와 대조할 수 없어 인증서를 받아들이지 않았다.
access_denied: 유효한 인증서나 PSK를 받았지만, 접근 제어를 적용한 결과 송신자가 협상을 진행하지 않기로 했다.
decode_error: 어떤 필드가 지정된 범위를 벗어났거나 메시지 길이가 올바르지 않아 메시지를 디코딩할 수 없었다. 이 경고는 메시지가 형식적인 프로토콜 문법에 맞지 않는 오류에 쓰인다. 네트워크에서 메시지가 손상된 경우를 제외하면, 올바른 구현 사이의 통신에서는 이 경고가 나타날 일이 없다.
decrypt_error: 핸드셰이크(레코드 계층이 아님)의 암호 연산이 실패했다. 서명을 올바르게 검증하지 못하거나 Finished 메시지 또는 PSK 바인더를 검증하지 못한 경우를 포함한다.
protocol_version: 피어가 협상하려 한 프로토콜 버전을 인식하지만 지원하지 않는다(부록 E 참고).
insufficient_security: 서버가 클라이언트가 지원하는 것보다 더 안전한 매개 변수를 요구했다는 바로 그 이유로 협상이 실패한 경우에 “handshake_failure” 대신 돌려준다.
internal_error: 피어나 프로토콜의 정확성과 관계없는 내부 오류(메모리 할당 실패 등) 때문에 계속할 수 없다.
inappropriate_fallback: 클라이언트의 유효하지 않은 연결 재시도에 대한 응답으로 서버가 보낸다([RFC7507] 참고).
missing_extension: 제시한 TLS 버전이나 협상된 다른 매개 변수에서 보내는 것이 필수인 확장이 없는 핸드셰이크 메시지를 받은 엔드포인트가 보낸다.
unsupported_extension: ServerHello, HelloRetryRequest, EncryptedExtensions, Certificate에 담긴 확장 가운데, 대응하는 ClientHello나 CertificateRequest에서 먼저 제시하지 않은 확장이 들어 있는 핸드셰이크 메시지를 받은 엔드포인트가 보낸다.
unrecognized_name: 클라이언트가 “server_name” 확장으로 제공한 이름으로 식별되는 서버가 없을 때 서버가 보낸다([RFC6066] 참고).
bad_certificate_status_response: 서버가 “status_request” 확장으로 유효하지 않거나 받아들일 수 없는 OCSP 응답을 제공했을 때 클라이언트가 보낸다([RFC6066] 참고).
unknown_psk_identity: PSK 키 수립을 원하지만 클라이언트가 받아들일 수 있는 PSK 식별자를 제공하지 않았을 때 서버가 보낸다. 이 경고를 보내는 것은 선택 사항이다(OPTIONAL). 서버는 그 대신 단지 유효하지 않은 PSK 식별자임을 나타내기 위해 “decrypt_error” 경고를 보내기로 할 수 있다(MAY).
certificate_required: 클라이언트 인증서를 원했지만 클라이언트가 제공하지 않았을 때 서버가 보낸다.
general_error: 더 구체적인 오류를 쓸 수 없거나, 송신자가 구체적인 오류 코드를 감추려는 경우에 오류 상황을 나타내기 위해 보낸다. 구현은 더 구체적인 오류를 쓸 수 있으면 그 오류를 써야 한다(SHOULD).
no_application_protocol: 클라이언트의 “application_layer_protocol_negotiation” 확장이 서버가 지원하지 않는 프로토콜만 알릴 때 서버가 보낸다([RFC7301] 참고).
새 경고 값은 11장에 설명된 대로 IANA가 할당한다.
TLS 핸드셰이크는 하나 이상의 입력 비밀값을 수립하며, 아래에 자세히 설명하듯 이 비밀값들을 결합해 실제로 쓰는 키 재료를 만든다. 키 유도 과정에는 입력 비밀값과 핸드셰이크 트랜스크립트가 모두 들어간다. 핸드셰이크 트랜스크립트에는 Hello 메시지의 무작위 값이 들어 있으므로, 같은 PSK를 여러 연결에 쓰는 경우처럼 같은 입력 비밀값을 쓰더라도 핸드셰이크마다 트래픽 비밀값이 다르다는 점에 유의하라.
키 유도 과정은 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에서 두 입력 비밀값은 다음과 같다.
PSK(외부에서 수립한 사전 공유 키, 또는 이전 연결의 resumption_secret 값에서 유도한 키)
비대칭 키 수립의 결과인 비대칭 공유 비밀값(7.4절 참고)
이로부터 아래 그림(그림 5)에 보인 키 스케줄이 만들어진다. 이 그림에는 다음 표기 규칙이 적용된다.
HKDF-Extract는 위에서 Salt 인수를, 왼쪽에서 IKM 인수를 받고, 출력은 아래로, 출력의 이름은 오른쪽에 그린다.
Derive-Secret의 Secret 인수는 들어오는 화살표로 나타낸다. 예를 들어 Early Secret은 client_early_traffic_secret을 생성하는 Secret이다.
“0”은 0으로 설정한 Hash.length 바이트의 문자열을 나타낸다.
참고: 이 값들을 “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).
핸드셰이크가 끝나면 어느 쪽이든 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).
트래픽 키 재료는 다음 입력 값으로 생성한다.
트래픽 키 재료는 입력 트래픽 비밀값으로부터 다음을 사용해 생성한다.
[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이 바뀔 때마다(예: 핸드셰이크 키에서 애플리케이션 데이터 키로 바뀔 때나 키 갱신 때) 모든 트래픽 키 재료를 다시 계산한다.
유한체 그룹에서는 일반적인 Diffie-Hellman [KEYAGREEMENT] 계산을 수행한다. 협상된 키(Z)는 빅 엔디언 형식으로 인코딩해 바이트 문자열로 바꾸고, 소수의 크기까지 왼쪽을 0으로 채운다. 이 바이트 문자열을 위에 규정한 키 스케줄의 공유 비밀값으로 쓴다.
이 구성은 앞쪽의 0을 제거하던 이전 버전의 TLS와 다르다는 점에 유의하라.
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 계산은 다음과 같다.
KeyShareEntry.key_exchange 구조체에 넣을 공개 키는, 적절한 길이의 비밀 키(스칼라 입력으로)와 표준 공개 기준점(u 좌표 점 입력으로)에 ECDH 스칼라 곱셈 함수를 적용한 결과다.
ECDH 공유 비밀값은 비밀 키(스칼라 입력으로)와 피어의 공개 키(u 좌표 점 입력으로)에 ECDH 스칼라 곱셈 함수를 적용한 결과다. 출력은 아무 처리 없이 그대로 쓴다.
이 곡선들에서 구현은 Diffie-Hellman 공유 비밀값을 계산하는 데 [RFC7748]에 규정된 방법을 써야 한다(SHOULD). 구현은 [RFC7748]의 6절에 설명된 대로, 계산한 Diffie-Hellman 공유 비밀값이 모두 0인 값인지 반드시 확인해야 하며, 그렇다면 중단해야 한다(MUST). 구현자가 이 타원 곡선의 다른 구현을 쓴다면, [RFC7748]의 7절에 규정된 추가 확인을 수행해야 한다(SHOULD).
[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절에 정의되어 있다.
2.3절과 부록 F.5에서 언급했듯이, TLS는 0-RTT 데이터에 대해 자체적인 재전송 방지 보호를 제공하지 않는다. 염려해야 할 잠재적 위협은 두 가지다.
0-RTT 데이터의 플라이트를 단순히 복제해 재전송 공격을 하는 네트워크 공격자.
클라이언트의 재시도 동작을 이용해 서버가 애플리케이션 메시지의 사본을 여러 개 받게 만드는 네트워크 공격자. 견고성을 중시하는 클라이언트는 네트워크 오류에 요청을 재시도하는 것으로 대응하므로 이 위협은 이미 어느 정도 존재한다. 그러나 클라이언트가 핸드셰이크가 끝난 뒤 거절된 0-RTT 데이터를 자동으로 재전송한다면, 공격자가 그 데이터를 연결 1에서는 0-RTT 데이터로, 연결 2에서는 1-RTT 데이터로 처리되게 할 수 있으므로 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 연결로 도착하는 중복된 애플리케이션 계층 요청을 올바르게 처리하는 것을 포함해, 이미 어떤 형태의 재전송 방지 대책을 구현할 필요가 있다는 점에 유의하라.
가장 단순한 형태의 재전송 방지 대책은 서버가 각 세션 티켓을 한 번만 쓰도록 허용하는 것이다. 예를 들어 서버는 아직 남아 있는 유효한 티켓 전부의 데이터베이스를 유지하고, 티켓이 쓰일 때마다 데이터베이스에서 삭제할 수 있다. 알 수 없는 티켓이 제공되면 서버는 전체 핸드셰이크로 돌아간다.
티켓이 자체 완결적이지 않고 데이터베이스 키이며, 대응하는 PSK를 사용할 때 삭제한다면, PSK로 수립한 연결은 재전송 방지 보호뿐 아니라, 데이터베이스 항목에 있던 PSK의 사본이 모두 삭제되고 나면 순방향 비밀성도 누린다. 이 메커니즘은 PSK를 비대칭 키 교환 없이 쓸 때의 PSK 사용 보안도 높인다.
이 메커니즘은 여러 서버가 분산된 환경에서 서버 노드 사이에 세션 데이터베이스를 공유해야 하므로, 자체 암호화 티켓과 비교하면 PSK 0-RTT 연결의 성공률을 높이기 어려울 수 있다. 세션 데이터베이스와 달리 세션 티켓은 일관된 저장소 없이도 PSK 기반 세션 수립을 성공적으로 할 수 있다. 다만 0-RTT를 허용하는 경우에는, 다음 절에 자세히 설명하듯 0-RTT 데이터의 재전송 방지를 위해 여전히 일관된 저장소가 필요하다.
다른 형태의 재전송 방지는 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장에 설명한 두 번째 형태의 공격의 또 다른 변형이다.
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를 받을 때만 하고, 이후의 조기 애플리케이션 데이터 레코드를 받을 때는 하지 않는다. 조기 데이터가 받아들여진 뒤에는 레코드가 더 긴 기간에 걸쳐 서버로 계속 스트리밍될 수 있다.
달리 규정하는 애플리케이션 프로파일 표준이 없다면 다음과 같다.
TLS를 준수하는 애플리케이션은 반드시 TLS_AES_128_GCM_SHA256 [GCM] 암호 스위트를 구현해야 하며(MUST), TLS_AES_256_GCM_SHA384 [GCM]와 TLS_CHACHA20_POLY1305_SHA256 [RFC8439] 암호 스위트를 구현해야 한다(SHOULD)(부록 B.4 참고).
TLS를 준수하는 애플리케이션은 반드시 rsa_pkcs1_sha256(인증서용), rsa_pss_rsae_sha256(CertificateVerify와 인증서용), ecdsa_secp256r1_sha256을 쓰는 디지털 서명을 지원해야 한다(MUST). TLS를 준수하는 애플리케이션은 반드시 secp256r1(NIST P-256)을 쓰는 키 교환을 지원해야 하며(MUST), X25519 [RFC7748]를 쓰는 키 교환을 지원해야 한다(SHOULD).
달리 규정하는 애플리케이션 프로파일 표준이 없다면, TLS를 준수하는 애플리케이션은 반드시 다음 TLS 확장을 구현해야 한다(MUST).
모든 구현은 해당 기능을 제시할 때 반드시 이 확장들을 보내고 사용해야 한다(MUST).
ClientHello, ServerHello, HelloRetryRequest 메시지에는 반드시 “supported_versions”가 있어야 한다(REQUIRED).ClientHello 메시지에는 반드시 “supported_groups”가 있어야 한다(REQUIRED).본문에 0x0304가 들어 있는 “supported_versions” 확장이 ClientHello에 있으면, 클라이언트가 이 명세를 사용해 협상하려 한다고 본다. 그런 ClientHello 메시지는 반드시 다음 요구 사항을 충족해야 한다(MUST).
“pre_shared_key” 확장이 없다면, 반드시 “signature_algorithms” 확장과 “supported_groups” 확장을 모두 담고 있어야 한다(MUST).
“supported_groups” 확장이 있다면 반드시 “key_share” 확장도 담고 있어야 하며(MUST), 그 반대도 마찬가지다. 빈 KeyShare.client_shares 목록은 허용된다.
이 요구 사항을 충족하지 않는 ClientHello를 받은 서버는 반드시 “missing_extension” 경고로 핸드셰이크를 중단해야 한다(MUST).
또한 모든 구현은 “server_name” 확장을 쓸 수 있는 애플리케이션과 함께 그 확장을 쓰는 것을 반드시 지원해야 한다(MUST). 서버는 클라이언트가 유효한 “server_name” 확장을 보내도록 요구할 수 있다(MAY). 이 확장을 요구하는 서버는 “server_name” 확장이 없는 ClientHello에 “missing_extension” 경고로 연결을 종료해 응답해야 한다(SHOULD).
이 절은 TLS 엔드포인트와 미들박스가 반드시 따라야 하는(MUST) 불변 조건(invariant)을 설명한다. 이는 이전 버전의 TLS에도 적용된다.
TLS는 안전하고 호환성 있게 확장할 수 있도록 설계되었다. 더 새로운 클라이언트나 서버는 더 새로운 피어와 통신할 때 가장 선호하는 공통 매개 변수를 협상하는 것이 바람직하다. TLS 핸드셰이크는 다운그레이드 방지를 제공한다. 더 새로운 클라이언트와 더 새로운 서버 사이에서 TLS를 터미네이션하지 않고 트래픽을 전달하는 미들박스는 핸드셰이크에 영향을 줄 수 없어야 마땅하다(부록 F.1 참고). 동시에 배포는 저마다 다른 속도로 갱신되므로, 더 새로운 클라이언트나 서버는 이전 매개 변수를 계속 지원할 수 있으며(MAY), 이로써 이전 엔드포인트와 상호 운용할 수 있다.
이것이 동작하려면 구현은 반드시 확장 가능한 필드를 올바르게 다뤄야 한다(MUST).
ClientHello를 보내는 클라이언트는 반드시 그 안에서 알린 GREASE [RFC8701]가 아닌 매개 변수를 모두 지원해야 한다(MUST). 그렇지 않으면 서버가 그 매개 변수 중 하나를 선택해 상호 운용에 실패할 수 있다.
ClientHello를 받는 서버는 반드시 인식하지 못한 암호 스위트, 확장, 그 밖의 매개 변수를 모두 올바르게 무시해야 한다(MUST). 그렇지 않으면 더 새로운 클라이언트와 상호 운용에 실패할 수 있다. TLS 1.3에서는 CertificateRequest나 NewSessionTicket을 받는 클라이언트도 반드시 인식하지 못한 확장을 모두 무시해야 한다(MUST).
TLS 연결을 터미네이션하는 미들박스는 반드시 (원래 클라이언트에 대해) 클라이언트가 받아들일 의향이 있는 인증서를 갖는 것을 포함해 규정을 준수하는 TLS 서버로 동작해야 하며, (원래 서버에 대해) 원래 서버의 인증서를 검증하는 것을 포함해 규정을 준수하는 TLS 클라이언트로도 동작해야 한다(MUST). 특히 미들박스는 반드시 자신이 이해하는 매개 변수만 담은 자신의 ClientHello를 생성해야 하며(MUST), 엔드포인트의 값을 전달하지 않고 반드시 새 ServerHello 무작위 값을 생성해야 한다(MUST).
TLS의 프로토콜 요구 사항과 보안 분석은 두 연결 각각에만 적용된다는 점에 유의하라. TLS 터미네이터를 안전하게 배포하려면 이 문서의 범위를 벗어나는 추가 보안 고려가 필요하다.
자신이 이해하지 못하는 ClientHello 매개 변수를 전달하는 미들박스는 그 ClientHello 이후의 메시지를 처리해서는 안 된다(MUST NOT). 미들박스는 반드시 이후의 모든 트래픽을 수정 없이 전달해야 한다(MUST). 그렇지 않으면 더 새로운 클라이언트, 서버와 상호 운용에 실패할 수 있다.
전달된 ClientHello에는 미들박스가 지원하지 않는 기능을 알리는 내용이 들어 있을 수 있으므로, 응답에는 미들박스가 인식하지 못하는 향후 TLS 추가 사항이 들어 있을 수 있다. 이 추가 사항은 ClientHello 이후의 어떤 메시지든 임의로 바꿀 수 있다(MAY). 특히 ServerHello에 담겨 보내는 값이 바뀔 수도 있고, ServerHello 형식이 바뀔 수도 있고, TLSCiphertext 형식이 바뀔 수도 있다.
TLS 1.3의 설계는 널리 배포된, 규정을 준수하지 않는 TLS 미들박스에 제약을 받았다(부록 E.4 참고). 그러나 TLS 1.3은 불변 조건을 완화하지 않는다. 그런 미들박스는 여전히 규정을 준수하지 않는 것이다.
보안 문제는 이 메모 전체, 특히 부록 C, 부록 E, 부록 F에서 논의한다.
이 문서는 원래 [RFC4346]에서 만들어지고 [RFC8446]과 [RFC8447]에서 갱신된 여러 레지스트리를 사용한다. [RFC8446], [RFC8447], 이 문서 사이의 변경 사항은 11.1절에 설명되어 있다. IANA는 이 RFC들에 대한 참조를 이 문서에 대한 참조로 바꾸었다. 레지스트리와 그 할당 정책은 다음과 같다.
TLS 암호 스위트(TLS Cipher Suites) 레지스트리: 첫 바이트가 0~254(10진수) 범위인 값은 Specification Required [RFC8126]로 할당한다. 첫 바이트가 255(10진수)인 값은 Private Use [RFC8126]로 예약되어 있다.
IANA는 부록 B.4에 나열한 암호 스위트를 레지스트리에 추가했다. “Value”와 “Description” 열은 그 표에서 가져온다. 이 암호 스위트들의 “DTLS-OK”와 “Recommended” 열은 모두 “Y”로 표시되어 있다.
TLS ContentType 레지스트리: 향후 값은 Standards Action [RFC8126]으로 할당한다.
TLS 경고(TLS Alerts) 레지스트리: 향후 값은 Standards Action [RFC8126]으로 할당한다. IANA는 이 레지스트리를 부록 B.2의 값으로 채웠다. 그런 값의 “DTLS-OK” 열은 모두 “Y”로 표시되어 있다. “_RESERVED”로 표시된 값에는 이전 용도를 설명하는 주석이 있다.
TLS HandshakeType 레지스트리: 향후 값은 Standards Action [RFC8126]으로 할당한다. IANA는 이 레지스트리의 항목 4의 이름을 “NewSessionTicket”에서 “new_session_ticket”으로 바꾸고, 레지스트리를 부록 B.3의 값으로 채웠다. 그런 값의 “DTLS-OK” 열은 모두 “Y”로 표시되어 있다. “_RESERVED”로 표시된 값에는 이전 용도나 임시 용도를 설명하는 주석이 있다.
이 문서는 원래 [RFC4366]에서 만들어진 TLS ExtensionType 값(TLS ExtensionType Values) 레지스트리도 사용한다. IANA는 이 레지스트리가 이 문서를 참조하도록 갱신했다. 레지스트리의 변경 사항은 다음과 같다.
IANA는 등록 정책을 다음과 같이 갱신했다.
첫 바이트가 0~254(10진수) 범위인 값은 Specification Required [RFC8126]로 할당한다. 첫 바이트가 255(10진수)인 값은 Private Use [RFC8126]로 예약되어 있다.
IANA는 이 레지스트리에 “key_share”, “pre_shared_key”, “psk_key_exchange_modes”, “early_data”, “cookie”, “supported_versions”, “certificate_authorities”, “oid_filters”, “post_handshake_auth”, “signature_algorithms_cert” 확장을 이 문서에 정의된 값과 “Recommended” 값 “Y”로 포함하도록 갱신했다.
IANA는 이 레지스트리에 확장이 나타날 수 있는 메시지를 나열하는 “TLS 1.3” 열을 포함하도록 갱신했다. 이 열은 처음에 4.3절의 표로 채웠으며, 그 표에 없는 확장은 TLS 1.3에서 쓰이지 않음을 나타내기 위해 “-“로 표시했다.
이 문서는 원래 [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 SignatureScheme 레지스트리: 첫 바이트가 0~253(10진수) 범위인 값은 Specification Required [RFC8126]로 할당한다. 첫 바이트가 254 또는 255(10진수)인 값은 Private Use [RFC8126]로 예약되어 있다. 첫 바이트가 0~6 범위이거나 두 번째 바이트가 0~3 범위인 값 가운데 현재 할당되지 않은 값은 하위 호환성을 위해 예약되어 있다. 이 레지스트리에는 “Recommended” 열이 있다. 이 레지스트리는 처음에 4.3.3절에 설명된 값으로 채웠다. 다음 값은 “Recommended”로 표시되어 있다. ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, ed25519.
TLS PskKeyExchangeMode 레지스트리: 0~253(10진수) 범위의 값은 Specification Required [RFC8126]로 할당한다. 값 254와 255(10진수)는 Private Use [RFC8126]로 예약되어 있다. 이 레지스트리에는 “Recommended” 열이 있다. 이 레지스트리는 처음에 psk_ke(0)와 psk_dhe_ke(1)로 채웠다. 둘 다 “Recommended”로 표시되어 있다.
TLS ExtensionType 값, TLS 암호 스위트, TLS 지원 그룹, TLS 익스포터 레이블(TLS Exporter Labels), TLS HashAlgorithm, TLS 인증서 유형, TLSPskKeyExchangeMode, TLS SignatureScheme 레지스트리에는 “Recommended” 열이 있다. “Recommended” 열의 값과 설정에 관한 추가 정보는 [RFC9847]의 3절을 참고한다.
역주: “TLSPskKeyExchangeMode”는 원문 표기 그대로이며, 위의 TLS PskKeyExchangeMode 레지스트리를 가리킨다.
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”로 갱신했다.
이 부록은 클라이언트와 서버 핸드셰이크에서 허용되는 상태 전이를 요약한다. 상태 이름(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받음이다.
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).
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
이 부록은 규범적인 프로토콜 타입과 상수 정의를 제시한다. “_RESERVED”로 나열한 값은 이전 버전의 TLS에서 쓰였으며, 완전성을 위해 여기에 나열한다. TLS 1.3 구현은 이 값들을 보내서는 안 되지만(MUST NOT), 오래된 TLS 구현에서 받을 수도 있다.
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;
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;
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;
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;
struct {
select (Handshake.msg_type) {
case client_hello:
ProtocolVersion versions<2..254>;
case server_hello: /* HelloRetryRequest도 같음 */
ProtocolVersion selected_version;
};
} SupportedVersions;
struct {
opaque cookie<1..2^16-1>;
} Cookie;
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;
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 구현과 상호 운용하기에 충분하다.
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;
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;
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;
struct {} EndOfEarlyData;
enum {
update_not_requested(0), update_requested(1), (255)
} KeyUpdateRequest;
struct {
KeyUpdateRequest request_update;
} KeyUpdate;
암호 스위트는 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가 할당한다.
TLS 프로토콜은 흔한 보안 실수의 상당수를 막을 수 없다. 이 부록은 구현자에게 도움이 될 몇 가지 권고를 제시한다. [RFC8448]은 TLS 1.3 핸드셰이크의 시험 벡터(test vector)를 제공한다.
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]은 보안 프로토콜 구현이 장기 개인 키와 결정적 서명 함수를 사용해 자신의 (의사) 난수 생성기를 보강하는 방법을 설명한다. 이 방법은 망가졌거나 다른 방식으로 조작된 난수 생성기의 무작위성을 개선한다.
구현은 인증서의 무결성을 검증할 책임이 있으며, 일반적으로 인증서 폐지 메시지를 지원하는 것이 바람직하다. 4.5.1.1절을 참고한다. 애플리케이션 프로파일에 특별한 지시가 없다면, 신뢰하는 인증 기관(CA)이 올바르게 서명했는지 확인하기 위해 인증서는 항상 검증하는 것이 마땅하다. 신뢰 앵커를 선택하고 추가하는 일은 매우 신중히 하는 것이 바람직하다. 사용자는 인증서와 신뢰 앵커에 관한 정보를 볼 수 있는 것이 바람직하다. 애플리케이션은 최소 키 크기와 최대 키 크기도 강제해야 한다(SHOULD). 예를 들어 2048비트 RSA나 224비트 ECDSA보다 약한 키나 서명이 들어 있는 인증 경로는 보안 애플리케이션에 적절하지 않다.
일부 프로토콜에서는 같은 인증서를 클라이언트 모드와 서버 모드 양쪽에 쓰는 것이 흔한 관행이라는 점에 유의하라. 이 설정은 충분히 분석되지 않았으며, 이 경우 상위 계층 의미에 모호함이 없도록 보장하는 것은 상위 계층 프로토콜의 책임이다.
구현 경험상, 이전 TLS 명세의 일부는 이해하기 쉽지 않았고 상호 운용성 문제와 보안 문제의 원인이 되었다. 이 문서에서 그런 부분 상당수를 명확히 했지만, 이 부록에는 구현자가 특히 주의해야 할 가장 중요한 사항의 짧은 목록을 담았다.
TLS 프로토콜 문제:
여러 TLS 레코드로 단편화된 핸드셰이크 메시지를 올바르게 처리하는가(5.1절 참고)? 여러 개의 작은 조각으로 나뉜 ClientHello 같은 경계 상황(corner case)을 올바르게 처리하는가? 최대 조각 크기를 넘는 핸드셰이크 메시지를 단편화하는가? 특히 Certificate와 CertificateRequest 핸드셰이크 메시지는 단편화해야 할 만큼 커질 수 있다. [RFC8879]에 정의된 인증서 압축을 쓰면 단편화해야 할 위험을 줄일 수 있다.
암호화되지 않은 모든 TLS 레코드에서 TLS 레코드 계층 버전 번호를 무시하는가(부록 E 참고)?
TLS 1.3 이상을 지원하는 모든 가능한 구성에서 SSL, RC4, EXPORT 암호, (“signature_algorithms” 확장을 통한) MD5 지원을 완전히 없앴는지, 그리고 이 폐기된 기능을 쓰려는 시도가 올바르게 실패하는지 확인했는가(부록 E 참고)?
알 수 없는 확장을 포함해 ClientHello의 TLS 확장을 올바르게 처리하는가?
서버가 클라이언트 인증서를 요청했지만 적절한 인증서가 없을 때, 메시지 전체를 생략하지 않고 빈 Certificate 메시지를 올바르게 보내는가(4.5.1절 참고)?
AEAD-Decrypt가 만든 평문 조각을 처리하며 끝에서부터 ContentType을 찾아 훑을 때, 피어가 모두 0인 잘못된 평문을 보낸 경우에 평문의 시작을 넘어서까지 훑지 않도록 하는가?
ClientHello에서 인식하지 못한 암호 스위트(4.2.2절), hello 확장(4.3절), 이름 붙은 그룹(4.3.7절), 키 공유값(4.3.8절), 지원 버전(4.3.1절), 서명 알고리즘(4.3.3절)을 제대로 무시하는가?
서버라면, 호환되는 비대칭 키 교환 그룹을 지원하지만 “key_share” 확장에서 그 그룹을 예측하지 않은 클라이언트에게 HelloRetryRequest를 보내는가? 클라이언트라면, 서버의 HelloRetryRequest를 올바르게 처리하는가?
암호 세부 사항:
타이밍 공격 [TIMING]을 막는 데 어떤 대응책을 쓰는가?
Diffie-Hellman 키 교환을 쓸 때, 협상된 키의 앞쪽 0 바이트를 올바르게 보존하는가(7.4.1절 참고)?
TLS 클라이언트는 서버가 보낸 Diffie-Hellman 매개 변수를 받아들일 수 있는지 확인하는가(4.3.8.1절 참고)?
Diffie-Hellman 비공개 값, ECDSA “k” 매개 변수, 그 밖의 보안에 중요한 값을 생성할 때 강력하고, 무엇보다 시드가 제대로 설정된 난수 생성기(부록 C.1 참고)를 쓰는가? 구현은 [RFC6979]에 규정된 “결정적 ECDSA(deterministic ECDSA)”를 구현할 것을 권고한다(RECOMMENDED). 결정적 ECDSA나 EdDSA 같은 순수 결정적 타원 곡선 암호(Elliptic Curve Cryptography, ECC) 서명은 쉽게 접근할 수 있는 사물 인터넷(Internet of Things, IoT) 기기에서 일부 부채널 공격과 오류 주입 공격(fault injection attack)에 취약할 수 있다는 점에 유의하라.
Diffie-Hellman 공개 키 값과 공유 비밀값을 그룹 크기까지 0으로 채우는가(4.3.8.1절과 7.4.1절 참고)?
RSA-CRT 키 유출 [FW15]을 막기 위해 서명을 만든 뒤 그 서명을 검증하는가?
클라이언트는 티켓 하나를 여러 연결에 재사용하지 않아야 한다(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 밖의 메커니즘도 마찬가지다.
이전 버전의 TLS는 익명 Diffie-Hellman에 기반한, 명시적으로 인증하지 않는 암호 스위트를 제공했다. 이 모드들은 TLS 1.3에서 사용이 중단되었다. 그러나 검증할 수 있는 서버 인증을 제공하지 않는 매개 변수를 여전히 여러 방법으로 협상할 수 있다. 예를 들면 다음과 같다.
원시 공개 키 [RFC7250].
인증서에 담긴 공개 키를 쓰되, 인증서 체인이나 그 내용을 전혀 검증하지 않는 것.
이 기법은 어느 것이든 단독으로 쓰면 중간자 공격(man-in-the-middle attack)에 취약하므로 일반적인 용도에는 안전하지 않다. 그러나 서버 공개 키의 대역 외 검증, 최초 사용 시 신뢰(trust on first use), 채널 바인딩(channel binding) 같은 메커니즘을 통해 그런 연결을 외부 인증 메커니즘에 묶을 수도 있다(다만 [RFC5929]에 설명된 채널 바인딩은 TLS 1.3에 대해 정의되어 있지 않다). 그런 메커니즘을 쓰지 않으면 그 연결은 능동 중간자 공격에 대한 보호가 전혀 없다. 명시적인 구성이나 특정 애플리케이션 프로파일이 없는 한, 애플리케이션은 TLS를 그런 방식으로 사용해서는 안 된다(MUST NOT).
이 문서에서 쓰는 이름에 맞추기 위해 [RFC5246]의 다음 용어 이름을 바꾼다.
[RFC5246]의 8.1절에서 계산하는 마스터 비밀값(master secret)은 메인 비밀값(main secret)으로 이름을 바꾼다. 식과 구조체에서는 master_secret 대신 main_secret이라고 부른다. 그러나 PRF 함수의 레이블 매개 변수는 호환성을 위해 바꾸지 않는다.
프리마스터 비밀값(premaster secret)은 예비 비밀값(preliminary secret)으로 이름을 바꾼다. 식과 구조체에서는 pre_master_secret 대신 preliminary_secret이라고 부른다.
[RFC5246]의 7.4.7.1절에 정의된 PreMasterSecret과 EncryptedPreMasterSecret 구조체는 각각 PreliminarySecret과 EncryptedPreliminarySecret으로 이름을 바꾼다.
이에 따라 [RFC7627]에 정의된 확장의 이름은 “Extended Main Secret” 확장으로 바뀐다. 확장 코드 포인트의 이름은 “extended_main_secret”으로 바뀐다. [RFC7627]의 4절에 있는 PRF 함수의 레이블 매개 변수는 호환성을 위해 바꾸지 않는다.
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).
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).
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).
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).
현장 측정 [Ben17a] [Ben17b] [Res17a] [Res17b]에서, TLS 클라이언트/서버 쌍이 TLS 1.3을 협상하면 상당수의 미들박스가 잘못 동작한다는 사실이 발견되었다. 구현은 TLS 1.3 핸드셰이크를 TLS 1.2 핸드셰이크처럼 보이게 만들어 그런 미들박스를 통과해 연결할 가능성을 높일 수 있다.
클라이언트는 4.2.2절의 legacy_session_id 항목에 설명된 대로 ClientHello에 항상 비어 있지 않은 세션 ID를 제공한다.
조기 데이터를 제시하지 않는다면, 클라이언트는 두 번째 플라이트 바로 앞에 더미 change_cipher_spec 레코드(5장의 셋째 문단 참고)를 보낸다. 이는 두 번째 ClientHello 앞일 수도 있고, 암호화된 핸드셰이크 플라이트 앞일 수도 있다. 조기 데이터를 제시한다면, 이 레코드는 첫 ClientHello 바로 다음에 둔다.
서버는 첫 핸드셰이크 메시지 바로 다음에 더미 change_cipher_spec 레코드를 보낸다. 이는 ServerHello 다음일 수도 있고 HelloRetryRequest 다음일 수도 있다.
이 변경을 모두 합치면 TLS 1.3 핸드셰이크가 TLS 1.2 세션 재개와 비슷해져, 미들박스를 통과해 연결에 성공할 가능성이 높아진다. 이 “호환 모드(compatibility mode)”는 부분적으로 협상된다. 클라이언트는 세션 ID를 제공할지 말지 정할 수 있고, 서버는 그 값을 되돌려 보내야 한다. change_cipher_spec은 피어가 무시해야 하므로 어느 쪽이든 핸드셰이크 중 언제든 보낼 수 있지만, 클라이언트가 비어 있지 않은 세션 ID를 보내면 서버는 반드시 이 부록에 설명된 대로 change_cipher_spec을 보내야 한다(MUST).
이전 버전의 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 알고리즘에 적용할 수 없고, 일부 시나리오에서 안전하지 않다고 밝혀졌기 때문이다.
TLS의 완전한 보안 분석은 이 문서의 범위를 벗어난다. 이 부록에서는 바라는 속성을 비형식적으로 설명하고, 더 형식적인 정의를 제공하는 연구 문헌의 더 자세한 연구를 참조로 제시한다.
핸드셰이크의 속성은 레코드 계층의 속성과 따로 다룬다.
TLS 핸드셰이크는 단방향 인증(서버만)과 상호 인증(클라이언트와 서버) 기능을 모두 제공하려는 인증 키 교환(Authenticated Key Exchange, AKE) 프로토콜이다. 핸드셰이크가 끝나면 각 쪽은 다음 값에 대한 자신의 관점을 출력한다.
실제로 쓰는 키 묶음을 유도할 수 있는 “세션 키” 묶음(메인 비밀값에서 유도한 여러 비밀값). 조기 데이터를 쓸 때는 Early Secret에서도 비밀값을 유도한다는 점에 유의하라. 아래에 자세히 설명하듯, 이 비밀값들은 메인 비밀값에서 유도한 것보다 다소 약한 속성을 갖는다.
암호 매개 변수(알고리즘 등)의 묶음.
통신하는 당사자의 신원.
공격자는 능동 네트워크 공격자, 즉 당사자 사이의 통신에 쓰이는 네트워크를 완전히 장악한 공격자라고 가정한다[RFC3552]. 이런 조건에서도 핸드셰이크는 아래에 나열한 속성을 제공해야 한다. 이 속성들이 꼭 서로 독립적이지는 않으며, 프로토콜 사용자의 요구를 반영한 것이라는 점에 유의하라.
같은 세션 키 수립: 핸드셰이크가 각 엔드포인트에서 성공적으로 끝난다면, 핸드셰이크는 양쪽에서 같은 세션 키 묶음을 출력할 필요가 있다([CK01]의 정의 1, 1부 참고).
세션 키의 비밀성: 공유 세션 키는 통신하는 당사자만 알고 공격자는 몰라야 한다([CK01]의 정의 1, 2부 참고). 단방향으로 인증된 연결에서는 공격자가 서버와 자신의 세션 키를 수립할 수 있지만, 그 세션 키는 클라이언트가 수립한 세션 키와 다르다는 점에 유의하라.
피어 인증: 클라이언트가 보는 피어 신원은 서버의 신원을 반영해야 한다. 클라이언트가 인증된다면, 서버가 보는 피어 신원은 클라이언트의 신원과 일치해야 한다.
세션 키의 고유성: 서로 다른 두 핸드셰이크는 서로 다르고 관계없는 세션 키를 만들어야 한다. 한 핸드셰이크가 만드는 개별 세션 키도 서로 다르고 독립적이어야 한다.
다운그레이드 방지: 암호 매개 변수는 양쪽에서 같아야 하며, 공격이 없는 상태에서 피어들이 통신했을 때와 같아야 한다([BBFGKZ16]의 정의 8과 9 참고).
장기 키에 대한 순방향 비밀성: 장기 키 재료(이 경우 인증서 기반 인증 모드의 서명 키, 또는 비대칭 키 교환을 쓰는 PSK 모드의 외부/재개 PSK)가 핸드셰이크가 끝난 뒤 노출되더라도, 세션 키 자체(와 세션 키를 다시 만드는 데 쓸 수 있는 모든 재료)가 지워졌다면 세션 키의 보안은 손상되지 않는다([DOW92] 참고). 특히 키 공유값에 대응하는 개인 키, 공유 비밀값, 그리고 TLS 키 스케줄에서 유도한 키 가운데 binder_key, resumption_secret, resumption_secret에서 유도한 PSK를 제외한 것도 지울 필요가 있다. PSK를 “psk_ke” PskKeyExchangeMode로 쓰면 순방향 비밀성 속성이 충족되지 않는다. 임시로 또는 연결 전용으로 쓰려던 키나 비밀값을 지우지 못하면, 사실상 보호해야 할 장기 키가 추가로 생기는 것이다. 그런 장기 키가 (핸드셰이크가 끝난 뒤에라도) 노출되면 그 연결의 트래픽에 대한 보호를 잃을 수 있다.
키 노출 위장(Key Compromise Impersonation, KCI) 저항성: 인증서로 상호 인증한 연결에서, 한 행위자의 장기 비밀값이 노출되더라도 그 연결에서 그 행위자가 피어를 인증하는 것이 깨지지 않아야 한다([HGFS15] 참고). 예를 들어 클라이언트의 서명 키가 노출되더라도, 이후 핸드셰이크에서 그 클라이언트에게 임의의 서버로 위장할 수 없어야 한다.
엔드포인트 신원 보호: 서버의 신원(인증서)은 수동 공격자로부터 보호되어야 한다. 클라이언트의 신원(인증서)은 수동 공격자와 능동 공격자 모두로부터 보호되어야 한다. 이 속성은 기밀성이 없는 암호 스위트에서는 성립하지 않는다. 이 명세는 그런 암호 스위트를 정의하지 않지만 다른 문서에서는 정의할 수도 있다.
비형식적으로 말하면, 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”으로 쓰자고 제안한다. 번역에서는 둘 다 “메인 비밀값”이라 차이가 없다.
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비트 이상(또는 해시 함수가 충돌을 찾지 못하도록 막는 데 필요한 만큼)이어야 한다.
핸드셰이크 중이든 핸드셰이크 후 인증에서든 서버에 인증서 기반 인증 데이터를 보낸 클라이언트는, 서버가 그 뒤에 클라이언트를 인증된 것으로 보는지 확신할 수 없다. 서버가 연결을 단방향 인증으로 보는지 상호 인증으로 보는지 클라이언트가 알아야 한다면, 이는 애플리케이션 계층에서 마련해야 한다. 자세한 내용은 [CHHSV17]을 참고한다. 또한 [Kraw16]의 핸드셰이크 후 인증 분석은, 핸드셰이크 후 단계에서 보낸 인증서로 식별되는 클라이언트가 트래픽 키를 가지고 있음을 보여 준다. 따라서 (트래픽 키가 노출되지 않았다고 가정하면) 이 당사자는 원래 핸드셰이크에 참여한 클라이언트이거나, 원래 클라이언트가 트래픽 키를 위임한 상대다.
0-RTT 동작 모드는 일반적으로 1-RTT 데이터와 비슷한 보안 속성을 제공하지만 두 가지 예외가 있다. 0-RTT 암호화 키는 완전한 순방향 비밀성을 제공하지 않으며, 서버는 지나치게 많아질 수 있는 상태를 유지하지 않고는 핸드셰이크의 고유성(재전송 불가능성)을 보장할 수 없다. 재전송 노출을 제한하는 메커니즘은 8장을 참고한다.
exporter_secret과 early_exporter_secret은 트래픽 키와 독립적으로 유도되므로, 그 키로 암호화한 트래픽의 보안을 위협하지 않는다. 그러나 이 비밀값들로 어떤 익스포터 값이든 계산할 수 있으므로, 가능한 한 빨리 지워야 한다(SHOULD). 익스포터 레이블 전체를 미리 안다면, 구현은 그 모든 레이블에 대해 익스포터 계산의 안쪽 Derive-Secret 단계를 미리 계산하고, 그다음 [early_]exporter_secret을 지운 뒤, 각 안쪽 값도 다시 필요하지 않다는 것을 알게 되는 즉시 지워야 한다(SHOULD).
TLS는 피어의 장기 비밀값(서명 키나 외부 PSK)이 노출된 뒤에 이루어지는 핸드셰이크에 대해서는 보안을 제공하지 않는다. 따라서 TLS는 노출 후 보안(post-compromise security) [CCG16]을 제공하지 않는다. 노출 후 보안은 후방 비밀성(backwards secrecy) 또는 미래 비밀성(future secrecy)이라고도 부른다. 이 점은 자기 자신의 장기 비밀값이 노출된 뒤 당사자가 갖는 보안 보장을 설명하는 KCI 저항성과 대조된다.
TLS 핸드셰이크의 분석은 다음 참고 문헌을 참조한다. [DFGS15], [CHSV16], [DFGS16], [KW16], [Kraw16], [FGSW16], [LXZFH16], [FG17], [BBK17].
레코드 계층은 핸드셰이크가 양방향 암호화 키와 논스를 유도하는 데 쓸 수 있는 강력한 트래픽 비밀값을 만들어 낸다는 데 의존한다. 그것이 참이고, 키가 5.5절에 제시한 것보다 많은 데이터에 쓰이지 않는다고 가정하면, 레코드 계층은 다음을 보장해야 한다.
기밀성: 공격자는 주어진 레코드의 평문 내용을 알아낼 수 없어야 한다.
무결성: 공격자는 기존 레코드와 다르면서 수신자가 받아들일 새 레코드를 만들어 낼 수 없어야 한다.
순서 보호/재전송 불가능성: 공격자는 수신자가 이미 받아들인 레코드를 다시 받아들이게 하거나, 수신자가 레코드 N을 먼저 처리하지 않고 레코드 N+1을 받아들이게 할 수 없어야 한다.
길이 은닉: 외부 길이가 주어진 레코드에서, 공격자는 레코드 가운데 내용이 얼마이고 패딩이 얼마인지 알아낼 수 없어야 한다.
키 변경 후 순방향 비밀성: 4.7.3절에 설명된 트래픽 키 갱신 메커니즘을 썼고 이전 세대의 키가 삭제되었다면, 엔드포인트를 장악한 공격자는 이전 키로 암호화한 트래픽을 복호화할 수 없어야 한다.
비형식적으로 말하면, 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는 트래픽 비밀값에 대해 노출 후 보안/미래 비밀성/후방 비밀성을 제공하지 않는다. 실제로 트래픽 비밀값을 알아낸 공격자는 그 연결에서 앞으로 쓸 모든 트래픽 비밀값을 계산할 수 있다. 그런 보장을 원하는 시스템은 새 핸드셰이크를 하고 비대칭 키 교환으로 새 연결을 수립할 필요가 있다.
TLS 레코드 계층의 분석은 다음 참고 문헌을 참조한다. [BMMRT15], [BT16], [BDFKPPRSZZ16], [BBK17], [PS18].
TLS는 암호화된 패킷의 길이와 타이밍을 관찰하는 데 기반한 다양한 트래픽 분석 공격에 취약하다[CLINIC][HCJC16]. 고정된 콘텐츠 모음을 제공하는 비디오 서버처럼 구별할 수 있는 메시지의 종류가 적을 때 특히 쉽지만, 더 복잡한 시나리오에서도 쓸 만한 정보를 제공한다.
TLS는 이런 형태의 공격에 대한 특별한 방어를 제공하지 않지만, 애플리케이션이 쓸 수 있는 패딩 메커니즘을 포함한다. AEAD 함수가 보호하는 평문은 내용과 가변 길이 패딩으로 이루어지며, 이로써 애플리케이션은 임의 길이의 암호화된 레코드와, 전송 기간과 무전송 기간의 차이를 감추는 패딩만 있는 위장 트래픽을 만들 수 있다. 패딩은 실제 내용과 함께 암호화되므로 공격자는 패딩의 길이를 직접 알아낼 수 없지만, 레코드 처리 중에 드러나는 타이밍 채널을 이용해(즉, 레코드를 처리하는 데 얼마나 걸리는지 보거나, 레코드를 조금씩 흘려 넣어 어느 레코드가 서버의 응답을 끌어내는지 보는 식으로) 간접적으로 측정할 수 있을지도 모른다. 일반적으로 이런 채널을 모두 없애는 방법은 알려져 있지 않다. 상수 시간 패딩 제거 함수라도 내용을 데이터에 따라 동작이 달라지는 함수에 넘기게 될 가능성이 높기 때문이다. 최소한, 완전히 상수 시간으로 동작하는 서버나 클라이언트를 만들려면 상위 계층 프로토콜도 상수 시간으로 만드는 것을 포함해 애플리케이션 계층 프로토콜 구현과 긴밀하게 협력해야 한다.
참고: 견고한 트래픽 분석 방어는 패킷 전송 지연과 트래픽 양 증가 때문에 성능 저하로 이어질 가능성이 높다.
일반적으로 TLS에는 부채널 공격(즉, 타이밍 같은 부차적인 채널로 통신을 공격하는 것)에 대한 특별한 방어가 없으며, 이는 관련 암호 프리미티브의 구현에 맡긴다. 그러나 TLS의 일부 기능은 부채널 공격에 저항하는 코드를 쉽게 작성할 수 있도록 설계되었다.
복합 MAC-then-encrypt 구조를 쓰던 이전 버전의 TLS와 달리, TLS 1.3은 AEAD 알고리즘만 쓰므로 구현은 그 프리미티브의 자체 완결적인 상수 시간 구현을 쓸 수 있다.
TLS는 모든 복호화 오류에 균일한 “bad_record_mac” 경고를 쓴다. 이는 공격자가 메시지의 일부에 대한 단편적인 정보를 얻지 못하게 하려는 것이다. 이런 오류에서 연결을 종료하면 추가 저항성을 얻는다. 새 연결은 다른 암호 재료를 가지므로, 여러 번 시도해야 하는 암호 프리미티브 공격을 막는다.
부채널을 통한 정보 유출은 TLS 위의 계층, 즉 애플리케이션 프로토콜과 그것을 쓰는 애플리케이션에서도 일어날 수 있다. 부채널 공격에 대한 저항성은 애플리케이션과 애플리케이션 프로토콜이 각자 기밀 정보가 의도치 않게 유출되지 않도록 보장하는 데 달려 있다.
재전송할 수 있는 0-RTT 데이터는, 재전송되어도 안전하도록(최소한 멱등이어야 하고, 많은 경우 상수 시간 응답 같은 더 강한 조건도 필요할 수 있다) 특별히 설계되지 않은 TLS 사용 애플리케이션에 여러 보안 위협이 된다. 가능한 공격은 다음과 같다.
부작용을 일으키는 동작(예: 물건 구매나 송금)이 중복되어 사이트나 사용자에게 피해를 준다.
공격자가 0-RTT 메시지를 저장했다가 재전송해, 다른 메시지와의 순서를 바꿀 수 있다(예: 삭제를 생성 뒤로 옮김).
캐시 같은 부작용 때문에 생기는 기존 정보 유출을 증폭한다. 공격자는 관심 있는 리소스를 캐시하지 않은 캐시 노드에 0-RTT 메시지를 재전송한 다음, 별도의 연결로 그 리소스가 캐시에 추가되었는지 확인해 0-RTT 메시지의 내용에 관한 정보를 알아낼 수도 있다. 이는 0-RTT 메시지를 재전송할 수 있는 만큼 여러 캐시 노드로 반복할 수 있다.
데이터를 아주 많이 재전송할 수 있다면, 암호 연산 속도를 반복해서 측정하는 것 같은 추가 공격이 가능해진다. 또한 공격자는 속도 제한 시스템에 과부하를 줄 수도 있다. 이런 공격에 관한 더 자세한 설명은 [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).
ClientHello를 재전송하면 같은 조기 익스포터가 만들어지므로, 이 익스포터를 쓰는 애플리케이션은 각별히 주의해야 한다. 특히 이 익스포터를 인증 채널 바인딩으로 쓴다면(예: 익스포터의 출력에 서명), PSK를 손에 넣은 공격자는 인증 키를 손에 넣지 않고도 연결 사이에서 인증자(authenticator)를 옮겨 심을 수 있다.
또한 조기 익스포터는 서버에서 클라이언트 방향의 암호화 키를 생성하는 데 쓰지 않아야 한다(SHOULD NOT). 그렇게 하면 그 키를 재사용하게 되기 때문이다. 이는 조기 애플리케이션 트래픽 키를 클라이언트에서 서버 방향으로만 쓰는 것과 같은 이치다.
구현은 유효하지 않은 PSK 바인더에 핸드셰이크를 중단하는 것으로 응답하므로, 공격자가 주어진 PSK 식별자가 유효한지 확인할 수 있을지도 모른다. 구체적으로, 서버가 외부 PSK 핸드셰이크와 인증서 기반 핸드셰이크를 모두 받아들이면, 유효한 PSK 식별자는 핸드셰이크 실패로 이어지는 반면, 유효하지 않은 식별자는 그냥 건너뛰므로 인증서 핸드셰이크가 성공한다. PSK 핸드셰이크만 지원하는 서버는, 유효한 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를 재사용하지 않는 것으로 교차 프로토콜 관련 출력에 대한 안전을 보장할 수 있다.
TLS의 외부 PSK는 정확히 클라이언트 하나와 서버 하나만 알도록 설계되었다. 그러나 [RFC9257]에서 언급했듯이, PSK를 셋 이상의 개체가 공유하는 사용 사례도 있다. 그런 시나리오에서는, 노출된 그룹 구성원이 다른 어떤 구성원으로든 위장할 수 있다는 예상되는 보안 약점 외에도, 악의적인 비구성원이 정직한 그룹 구성원 사이의 핸드셰이크 경로를 바꿔(reroute) 의도하지 않은 방식으로 연결할 수 있다[Selfie]. [RFC9257]은 [RFC9258]에 정의된 외부 PSK 임포터(importer)의 사용을 포함해, 이런 악의적인 메시지 경로 변경을 막는 외부 PSK 사용 권고를 제공한다.
쓸모 있는 신원이 없는 자체 서명 인증서(DTLS-SRTP [RFC5763]처럼)나 원시 공개 키 [RFC7250]를 피어 인증에 쓰는 TLS 1.3은 잘못된 결합 공격(misbinding attack) [MM24]에 취약할 수 있다. 이 위험은 “external_id_hash” 확장 [RFC8844]을 쓰거나, 서버만 인증하는 경우에는 서버가 “server_name” 확장이 자신의 예상 신원과 일치하는지 확인하는 것으로 완화할 수 있다.
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