https://datatracker.ietf.org/doc/html/rfc8446
- TLS は 2 つのピア間の通信にセキュアなチャンネルを提供する
- 下の層の通信には信頼できる、順序の正しいストリームだけが要求される
- (TCP であることは前提ではない?)
- セキュアなチャンネルが提供するもの
- 認証
- サーバーサイドは必須、クライアントサイドはオプション
- 非対称鍵もしくは対象鍵によって行われる
- 機密性
- チャネル確立後のデータはエンドポイントからしか見えない
- 送信データ長は隠さないが、 TLS レコードにパディングしてデータ長をわからなくしたり、トラフィックを分析する技術からの保護を改善することはできる
- 完全性
- チャネルで送信したデータを攻撃者はいじれない(いじってもばれる)
- 認証
- これはネットワークが完全に攻撃者のコントロール下にあっても適用される
- TLS の主なコンポーネント
- ハンドシェイクプロトコル
- party 間を認証したり暗号のパラメーターをやりとりしたり共有鍵のマテリアルをエスタブリッシュしたりする
- (どう訳すかわからん)
- (プレマスターシークレットを交換するよ、みたいな話だと思う)
- 改ざんに体制があるようにデザインされてる
- 攻撃者がピアにネゴシエーションのパラメーターを強制させて、通常の接続と異なるようなパラメーターを使わせる、みたいなことが出来ないようにする必要がある
- party 間を認証したり暗号のパラメーターをやりとりしたり共有鍵のマテリアルをエスタブリッシュしたりする
- レコードプロトコル
- 通信するピア間のトラフィック保護のため、ハンドシェイクプロトコルでエスタブリッシュしたパラメーターを使う
- プロトコルはトラフィックを一連のレコードたちに分割し、各レコードをトラフィック鍵を使って独立して保護する
- (レコードごとに鍵が違う、みたいなニュアンスがあるんだろうか)
- ハンドシェイクプロトコル
- アプリケーションのプロトコルに依存してない
- TLS の上のレイヤーなんでもいいし、TLS を意識しなくていい
- が、上のレイヤーのアプリケーションが TLS をどう使うかは規定がない
- 証明書の解釈をどうするか?とか
- TLS1.3 は前のバージョンと直接互換性はない
- バージョニングの機構があるのでピア間で共通のバージョンをサポートしてればネゴシエーションできる
- 古いバージョンの
- チケットの仕組みは 2.2 で定義されてる仕組みに代えられた
- 鍵の導出方法も変わった(7.5)
- OCSP のメッセージの送信方法もかわった
- client : TLS コネクションを始める側のエンドポイント
- connection : エンドポイント間の L4 のコネクション
- endpoint : クライアントかサーバー
- handshake : 後続の TLS のインタラクションのパラメーターを確立するためのクラサバ間の初期ネゴシエーション
- peer : エンドポイント
- クライアントの話が話題の場合、「サーバー」(逆も)
- receiver : レコードを受け取る側
- sender : 逆
- server : TLS コネクションを始めない側
- 1.2 と 1.3 の主な差分(網羅的ではない)
- 共通鍵暗号のアルゴリズムでレガシー無いやつを削除
- AEAD のものだけが残っている
- (平文を暗号化して暗号文から MAC 計算してくっつけておくる)
- 暗号スイートの概念が変わった
- レコード保護のアルゴリズム(秘密鍵の長さを含む)から認証、鍵交換の機構、鍵導出関数とハンドシェイクの MAC で使われるハッシュが分離された
- 0-RTT モードが追加された * (ハンドシェイク無しでいきなりデータが送れる?)
- PFS なしの鍵交換暗号スイートが削除された
- ServerHello の後のハンドシェイクのメッセージが暗号化された * 新しい EncryptedExtensions によって ServerHello によって平文でいままで送られてた拡張が保護される
- 鍵導出関数が再設計された
- ハンドシェイクのステートマシンが大幅に再構成された
- 一貫性を持ち、ChangeCipherSpec みたいな余分なメッセージをなくした
- 楕円曲線アルゴリズムが仕様にのった
- 暗号の改善いろいろ
- RSA のパディングが RSASSA-PSS に
- 圧縮、DSA(Digital Signature Algorithm)、DHE が削除
- (a version list in an extension is)
- セッションの再開が PSK のものに統合された
- https://milestone-of-se.nesuke.com/nw-basic/tls/tls-version-1-3/
- (サーバーだけが使えるチケットでどうこう、がなくなった)(元をあんまり理解していない)
- クライアント、サーバー間の最初のやりとりでハンドシェイクが行われる
- ハンドシェイクによってプロトコルのバージョン調整、暗号アルゴリズムの選択、お互いの認証(Optional) 共通鍵マテリアルの確立が行われる
- ハンドシェイクが完了したら交換した鍵でアプリケーションレイヤのトラフィックを保護する
- ハンドシェイクには 3 つのフェーズがある
- 鍵交換
- shared keying material を交換し、暗号パラメーターを選ぶ
- このフェーズ以降の全ては暗号化される
- サーバーパラメーター
- 他のハンドシェイクパラメーターを確立する
- 認証
- サーバー、クライアント(Optional) を認証
- 鍵を確認し、ハンドシェイクの完全性を担保する
- 鍵交換
- 鍵交換
- クライアントは
- ClientHello メッセージを送る
- これにはランダムな nonce、プロトコルのバージョン、共通 cipher/HKDF ハッシュペア(?)
- Diffie-Hellman key shares(in key_share)
- プリシェアードキーラベルを含む
- その他追加の拡張が送られるかも
- サーバーは
- ClientHallo を処理し、コネクションの適切な暗号パラメーターを決める
- その後決まったパラメーターを示す ServerHello を返す
- ClientHello/ServerHello の込合わせで共通鍵を決める
- (EC)DHE が使われる場合、ServerHello は key_share 拡張を含む
- PSK の場合、pre_shared_key 拡張を含む
- 両方使われる場合、両方とも提供される
- その後 2 つのメッセージを送る
- EncryptedExtensions
- 暗号パラメーターの決定に必要ない ClientHello に対するレスポンス(暗号化パラメーターだけ先送って後は暗号化したい?)
- CertificateRequest
- 証明書ベースの認証が必要な場合
- EncryptedExtensions
- クライアント、サーバーは認証メッセージを交換する
- TLS は証明書ベースの認証が必要になったらいつも同じメッセージの組を使う
- 具体的に
- Certificate: エンドポイントの証明書と証明書ごとの拡張
- サーバーが証明書による認証を行わない場合はクライアントによって省略される
- サーバーが CertificateRequest を送らない(クライアント認証を要求しない)場合、クライアントによって省略される * CertificateVerify
- Certificate メッセージの証明書に対応する秘密鍵でハンドシェイク全体を署名したもの
- Finished
- ハンドシェイク全体の MAC
- Certificate: エンドポイントの証明書と証明書ごとの拡張
- クライアントは
- クライアントが十分な
key_share
拡張(DH 公開鍵とか入ってる) を送らなかった場合、Server は HelloRetryRequest を送信し、Client は ClientHello を再送信する - (リトライで複数回 ClientHello を送った場合も、ハンドシェイクのメッセージはリトライから開始したことにはならないみたいなことが書いてあって、フィンガープリントの計算とかに使う気がするんだけど、よくわからん)
2.2. Resumption and Pre-Shared Key (PSK)
- PSK の共有方法は out of band だが、前回のコネクションで確立して新しいコネクションの確立時に利用できる
- PSK による session resumption or resuming
- ハンドシェイクが完了したらサーバーはクライアントに PSK Identity を送る
- PSK Identity は最初のハンドシェイクで導出した鍵に対応している
- クライアントがこれをサーバーに送ってサーバーが受領すると、フルハンドシェイクをせずに、最初のハンドシェイクで導出した鍵を使ってコネクションの暗号状態を確立する
- TLS 1.2 の session ID とか session ticket を使ったセッション再開のメカニズムはもう使われない
- PSK は FS のために共通鍵と組み合わせて ECDHE 鍵交換とともに使われるが、アプリケーション層のデータの FS がなくなるリスクをとって単独で利用することもできる
- サーバーが PSK 経由で認証を行う場合、Certificate/CertificateVerify メッセージを送信しない
- クライアントが PSK による resumption をしようとした場合、key_share エクステンションをおくるべき
- (これ FS のために鍵交換自体はする、みたいな話?)
- クライアント/サーバー間で PSK を共有した場合、TLS ではクライアントが初っ端からデータ(early data)を送れる
- クライアントは PSK をサーバーの認証と early data の暗号化に使う
- Early data には FS が無い
- PSK から導出できる鍵で暗号化するので
- リプレイ攻撃できる
- サーバーが乱数を提供することでリプレイの対策となっているが、ServerHello に依存しないので
T T'[n];
- n はバイト数
- T' の表現に 2 バイト必要で、T'` が 4 つの配列なら T T'[8]
opaque Datum[3]; /* three uninterpreted bytes */
Datum Data[9]; /* three consecutive 3-byte vectors */
- Datum は 3 バイトのデータ
- Data は 9 バイト文の Datam の Vector(つまり Datum 3 つ)
opaque mandatory<300..400>;
/* length field is two bytes, cannot be empty */
uint16 longer<0..800>;
/* zero to 400 16-bit unsigned integers */
- 可変長な場合
- mandatory は 300-400 bytes
- エンコードした場合、Vector の前 2 bytes が長さ
- Enumerateds が利用するバイトは定義済みの値の一番でかいやつに依存する
enum { sweet(1), sour(2), bitter(4), (32000) } Taste;
- Taste は 2 bytes
For example:
enum { apple(0), orange(1) } VariantTag;
struct {
uint16 number;
opaque string<0..10>; /* variable length */
} V1;
struct {
uint32 number;
opaque string[10]; /* fixed length */
} V2;
struct {
VariantTag type;
select (VariantRecord.type) {
case apple: V1;
case orange: V2;
};
} VariantRecord;
- ハンドシェイクプロトコルはコネクションのセキュリティパラメーターをやりとりするのに使われる
- ClientHello には以下が含まれる
- クライアントがサポートしている暗号スイート(AEAD/HKDF)
- supported_groups 拡張
- ECDHE の楕円曲線グループ
- key_share 拡張
- 鍵交換用のパラメーター(diffie-hellman の公開鍵とか)
- signature_algorithms 拡張
- クライアントが受け入れる署名アルゴリズム
- signature_algorithms_cert
- 証明書の署名アルゴリズム
- (signature_algorithms の方はハンドシェイクのメッセージ署名に使われる?)
- pre_shared_key 拡張
- クライアントがすでに知っている共通鍵の ID のリストを含む
- psk_key_exchange_modes
- PSK と一緒に使える鍵交換モード
- サーバーがクライアントの ClientHello になんらかの理由で対応出来ない場合(supported_groups に指定された値にサーバーが対応してないとか)場合、エラーを返したり HelloRetryRequest でリトライを要求したりする
- そうでない場合(成功した場合)、ServerHello は
- pre_shared_key 拡張
- PSK を使う場合サーバーが選択した鍵
- key_share 拡張
- ECDHE を使う場合
- Certificate / CertificateVerify メッセージ
- 証明書で認証する場合
- PSK と証明書の認証を同時には行わない
- pre_shared_key 拡張
- クライアントが最初にサーバーに接続するときに送る
- サーバーが ClientHello に対して HelloRetryRequest を送ってきたときも送る
- この場合、クライアントは↓をのぞいて同じ ClientHello を送る
- key_share 拡張
- early_data 拡張
- cookie 拡張
- pre_shared_key 拡張
- padding 拡張
- early_data 拡張
- サーバーが TLS 1.3 をネゴシエートしてる状態で ClientHello を受け取ったらコネクションを終了する
- リネゴシエーションが許可されてない
- TLS1.3 以前のバージョンでコネクションを確立した状態で TLS1.3 の ClientHello を受け取った場合、プロトコルバージョンは保つこと
- 1.3 にしちゃダメ
Structure of this message:
uint16 ProtocolVersion;
opaque Random[32];
uint8 CipherSuite[2]; /* Cryptographic suite selector */
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<8..2^16-1>;
} ClientHello;
- legacy_version
- TLS1.2 で固定
- (サーバーが TLS1.2 だったら、1.2 として透過的に動くためらしい)
- 代わりに実際にサポートしてるバージョンは supported_versions 拡張で送る
- (TLS1.2 のサーバーはこの拡張を知らないので無視する)
- random
- legacy_session_id
- TLS1.2 の session resumption は TLS1.3 で PSK を使うやり方にマージされた
- TLS1.2 の場合、クライアントは事前のハンドシェイクでキャッシュした session id をここにセットする
- んだけど、TLS1.2 でこのフィールドは必須なのでなんか入れてあげる必要がある
- 長さ 0 の vector でいいらしい
- cipher_suites
- クライアントがサポートしてる暗号スイートのリスト(使いたい降順)
- サーバーはつ開けない、使いたくないやつを無視して残ったやつを使って処理する
- クライアントが PSK を使う場合、PSK のハッシュアルゴリズムが含まれる必要がある
- legacy_compression_methods
- TLS1.3 では null
- それ以外がきたらサーバーは拒否
- extensions
- (extension ってか TLS1.2 の互換性のために TLS1.3 固有のフィールドを全部ここに突っ込んでる)
- サーバーはこれ見て ClientHello が TLS1.2 なのか 1.3 なのか判断する
- ClientHello を送ったらクライアントは ServerHello か HelloRetryRequest を待つ
- ClientHello のパラメーターに対してハンドシェイクを進められる場合、サーバーは ServerHello を返す
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
- TLS1.3 では supported_versions 拡張でサポートしてるバージョンを返すので、ここは 1.2 固定
- (これなんで必要なのかわからん)
- random
- legacy_session_id_echo
- ClientHello の legacy_session_id をそのまま
- Client はこの値が ClientHello の legacy_session_id にマッチしてなかったらハンドシェイクを終了する(TLS1.3 のハンドシェイクで session_id による resumption をしない場合でも)
- cipher_suite
- ClientHello.cipher_suites のなかからサーバーが選んだ cipher suite
- legacy_compression_method
- 0 固定
- extensions
- 後方互換性のため、HelloRetryRequest は ServerHello と構造が一緒(なんで?)
- ただし、random には 'HelloRetryRequest' の SHA-256 が設定される
- ダウングレード攻撃の保護のため、クライアントは ServerHello で「サーバーがどのバージョンの TLS クライアントとハンドシェイクしているか」を確認できる
- random の最後の 8 bytes が TLS1.2 かそれ以下の場合、固定値になる
- クライアントは予期しないバージョンだったらハンドシェイクを拒否する
- (本当に TLS1.2 にしか対応していないサーバーだったら、このランダム値にならない、みたいな話?)
- ハンドシェイクに必要なパラメーターを ClientHello で送ってこなかった場合に送る
- supported_versions 拡張が必須
- クライアントが正しい ClientHello を送るのに必要な拡張も返す
- クライアントが送ってこなかった拡張については、cookie 拡張をのぞいて送っちゃ駄目
struct {
ExtensionType extension_type;
opaque extension_data<0..2^16-1>;
} Extension;
- 拡張は基本リクエスト/レスポンスの構造だが、リクエストに対応するレスポンスがないものもある
- クライアントは拡張を ClientHello に含め、サーバーは ServerHello, EncryptedExtensions, HelloRetryRequest, and Certificate に含める
- メッセージに複数の拡張を指定する場合、順番は任意
- ただし ClientHello の pre_shared_key は最後でないと駄目
struct {
select (Handshake.msg_type) {
case client_hello:
ProtocolVersion versions<2..254>;
case server_hello: /* and HelloRetryRequest */
ProtocolVersion selected_version;
};
} SupportedVersions;
- クライアントはどのバージョンをサポートしているか、サーバーはどのバージョンを利用するかを指示するのに利用する
- この拡張が ClientHello に含まれていた場合、サーバーは ClientHello.legacy_version を使ってはいけない
- クライアントも同様
- (DTLS で DoS 対策に使うらしいけどよくわからん)
- HelloRetryRequest に cookie を入れた場合、クライアントが再送信する ClientHello にその値をコピーするらしい
- サーバーは ClientHello のハッシュを HelloRetryRequest のクッキーに保存する
- (意図がわからない)
- TLS1.3 はデジタル署名で使われる署名アルゴリズムの指定に 2 つの拡張を使う
- signature_algorithms_cert は証明書の署名に適用され、TLS1.2 にもあった signature_algorithms は CertificateVerify の署名に適用される
- signature_algorithms_cert 拡張がなければ、証明書の署名にも signature_algorithms を適用する
- クライアントが自身を証明書によって認証したい場合、signature_algorithms を送らなければならない
- certificate_authorities 拡張
- (クライアントが受け入れる CA を制限する場合に使う?)
- CertificateRequest に oid_filters 拡張があったら、クライアントは指定された oid とその値にマッチする値を持つ証明書を送る
- post_handshake_auth 拡張はクライアントが post-handshake authentication を望む場合送る
- サーバーはこの拡張が送られてきた場合を除いて、post-handshake CertificateRequest を送ってはならない
- supported_groups
- ECDHE の楕円曲線を決めるのに使う
- key_share 拡張は暗号パラメーターをもつ
struct {
NamedGroup group;
opaque key_exchange<1..2^16-1>;
} KeyShareEntry;
- クライアントは ClientHello で複数の KeyShareEntry を送る
- この拡張の値は supported_groups に指定された NamedGroup に含まれる必要がある
- 完全なサブセットである必要はない
- HelloRetryRequest ではサーバーがネゴシエーションに利用する、ClientHello でリトライを要求する際に利用する NamedGroup を送る
struct {
NamedGroup selected_group;
} KeyShareHelloRetryRequest;
- ServerHello ではサーバー側のパラメーターを送る
struct {
KeyShareEntry server_share;
} KeyShareServerHello;
- Diffie-Hellman の場合、DH 公開鍵(Y = g^X mod p)が key_exchange フィールドに入る
- ECDHE の場合、起点を x 倍(xは秘密鍵)した座標 X, Y が key_exchange フィールドに入る
- この後のサーバーからのメッセージ EncryptedExtensions/CertificateRequest は、ハンドシェイクの残りを行うための情報を持っている
- これは server_handshake_traffic_secret から導出される鍵で暗号化される
- すべてのハンドシェイクでサーバーは ServerHello メッセージの直後に EncryptedExtensions を送る
- サーバーが証明書で認証を行うクライアントからの証明書を要求する
- このメッセージは Encrypted Extensions に続く
- (これは暗号化されてるのか?)
struct {
opaque certificate_request_context<0..2^8-1>;
Extension extensions<2..2^16-1>;
} CertificateRequest;
- certificate_request_context
- クライアントは Certificate メッセージでこの値をエコーする
- post-handshake authentication ではない場合はゼロ
- post-handshake authentication の場合、予期できない値にする
- (CertificateVerify のリプレイ攻撃対策っぽいことが書いてあるがいまいちわからない)
- (Certificate に対して関係ない CertificateVerify を返したときに context の値が異なる、みたいな?)
- extensions
- 証明書要求のパラメーター
- signature_algorithms 拡張は必須
- メッセージをくっつけてハッシュするやつ
Transcript-Hash(M1, M2, ... Mn) = Hash(M1 || M2 || ... || Mn)
- ClientHello を HelloRetryRequest に対してリトライした場合、これについてはメッセージそのものではなく ClientHello1 のハッシュを結合してハッシュにする
Transcript-Hash(ClientHello1, HelloRetryRequest, ... Mn) =
Hash(message_hash || /* Handshake type */
00 00 Hash.length || /* Handshake message length (bytes) */
Hash(ClientHello1) || /* Hash of ClientHello1 */
HelloRetryRequest || ... || Mn)
- 証明書チェーンをピアに渡すメッセージ
enum {
X509(0),
RawPublicKey(2),
(255)
} CertificateType;
struct {
select (certificate_type) {
case RawPublicKey:
/* From 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 のレスポンスだった場合のみ
- 値は CertificateRequest の certificate_request_context の値そのまま
- certificate_list
- CertificateEntry のリスト
- extensions
- CertificateEntry のための拡張
- EncryptedExtensions で server_certificate_type/client_certificate_type によって証明書のタイプがネゴシエーションされていないか、X509 が指定されている場合、CertificateEntry は DER-encoded X.509 証明書となる
- 送信者の証明書は CertificateEntry リストの先頭にないと駄目
- その後に続く証明書は、その直前の証明書を証明しているものであるべきである
- トラストアンカーを指定する証明書は、その証明書をピアが保持しているなら省略できる
- (ルート証明書はチェーンに含めなくてよい、みたいな話だと思う)
- TLS 1.3 では OCSP レスポンスを CertificateEntry の拡張に含める
- サーバーが送る証明書は
- X.509v3
- サーバー証明書の公開鍵はクライアントの signature_algotrithms 拡張から選ばれた認証アルゴリズムと互換性がないと駄目
- RSA, ECDSA, EdDSA
- 証明書はその鍵をクライアントの signature_algorithms/signature_algorithms_cert で指定された署名スキームとともに署名に利用されることを許可しなければならない
- つまり、Key Usage extension があるなら、digitalSignature bit がたつ
- server_name / certificate_authorities 拡張が証明書の選択のために利用される
- サーバーが server_name 拡張の提示を要求した場合、クライアントはこの拡張を送るべき
- サーバーが提示するすべての証明書はクライアントが指示した署名アルゴリズムで署名されている必要がある(サーバーがそれをできるなら)
- 自己証明書やトラストアンカーは証明書チェーンの一部として検証されることはなく、任意のアルゴリズムで署名されていて良い
- できないなら別のアルゴリズムにフォールバックしてもいい
- クライアントが与えられた証明書で有効なチェーンを組み立てられないなら、unsupported_certificate で abort する
- サーバーが送る証明書は
- X.509v3
- CertificateRequest に certificate_authorities 拡張がいたら、証明書チェーンの証明書の 1 つは指定された CA が発行したやつじゃないと駄目
- CertificateRequest の signature_algorithms 拡張で指定されてる署名アルゴリズムで署名されてないと駄目
- CertificateRequest に oid_filters 拡張が指定されていたら、クライアント証明書は拡張の OID にマッチしてないと駄目
- 詳細な証明書の検証は out of scope(X509 の仕様を参照)
- サーバーが空の Certificate メッセージを送ってきたらクライアントは abort
- クライアントが証明書を送ってこなかったり、証明書チェーンがサーバーにとって有効でない場合、サーバーの裁量でハンドシェイクを続けたり中止したりする
- 署名アルゴリズムが MD5 ハッシュを使ってたら abort(MUST)
- SHA-1 も abort(RECOMMENDED)
- 自己署名、トラストアンカーでは良いらしい(よくわからん)
- エンドポイントが自身の証明書に対応する秘密鍵を持っていることを証明するのに使うメッセージ
- ここまでのハンドシェイクの完全性担保にも使う
- 証明書を送る場合、Certificate メッセージの直後、Finished メッセージの直前に送る
struct {
SignatureScheme algorithm;
opaque signature<0..2^16-1>;
} CertificateVerify;
- 署名はここまでのメッセージと証明書をくっつけて Transcript-Hash を計算した後、「0x20 * 64 | "TLS 1.3, server CertificateVerify" | single 0 byte | Transcript-Hash の結果」に対して行われる
Transcript-Hash(Handshake Context, Certificate)
- CertificateVerify の受信者は「ここまでに受信したメッセージ」「Certificate メッセージの entity の証明書」「CertificateVerify の署名」を利用して、署名検証する
- Finished メッセージを受信したら内容が正しいか検証する
- 検証したらコネクション経由でアプリケーションのデータ通信を始める
- Finished メッセージの計算に使う鍵は 4.4 の base key から HKDF を使って導出される
- Base key は server_handshake_traffic_secret とかなんだけど、そもそもこの鍵がどのタイミングででてくるのかよくわからない
struct {
opaque verify_data[Hash.length];
} Finished;
verify_data =
HMAC(finished_key,
Transcript-Hash(Handshake Context,
Certificate*, CertificateVerify*))
- TLS ではメインのハンドシェイク後に他のメッセージを送ることができる
- Record Layer はデータを 2^14 bytes 以下のチャンクで送信する TLSPlaintext record に分割する
- メッセージ協会のハンドリングは ContentType による
- ハンドシェイクメッセージは 1 つの TLSPlaintext にまとめられるか、複数のフラグメント化されたレコードに分割される
- フラグメントの要件
- ハンドシェイクメッセージに他のレコード型が挟まっちゃ駄目
- ハンドシェイクメッセージが複数のレコードに別れたとき、それらの間に他のメッセージが入ってはいけない
- ハンドシェイクメッセージが鍵の変更をまたいではいけない
- (ハンドシェイクのはじめから終わりまで同じ鍵を使え、みたいな話?)
- handshake type のレコードでは、fragment が 0 のレコードを送ってはいけない
- アラートメッセージはレコードをまたいで分割してはいけない
- 複数のアラートメッセージを 1 つの TLSPlaintext レコードに含めてはいけない
- つまり alert type のレコードには 1 つだけメッセージが入る
- アプリケーションのデータは
- 常に保護される。
- 長さ 0 のフラグメントも送っていい
- トラフィック分析対策に使えるかもしれない(?)
- 復数のレコードに分割されたり、1つのレコードにまとまられたりする
enum {
invalid(0),
change_cipher_spec(20),
alert(21),
handshake(22),
application_data(23),
(255)
} ContentType;
struct {
ContentType type;
ProtocolVersion legacy_record_version;
uint16 length;
opaque fragment[TLSPlaintext.length];
} TLSPlaintext;
- type
- フラグメントを処理する上位のプロトコル
- legacy_record_version
- 0x0303
- length
- fragment の長さ
- fragment
- データ
- record protection functions
- TLSPlaintext を TLSCiphertext にする
- deprotection functions は逆
- 以前のバージョンに対して、TLS 1.3 はすべての暗号が AEAD である
- AEAD functions は暗号化と認証を統合した操作を提供する
- これが plaintext を認証済みで暗号化されたテキストに変換する
- AEAD functions は暗号化と認証を統合した操作を提供する
- 暗号化されたレコードは暗号化された body に続いて平文のヘッダーをもつ
- これには type と optional な padding が含まれる
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
- padding
- opaque_type
- 23(application_data)
- legacy_record_version
- 0x0303
- length
- TLSCiphertext.encrypted_record の長さ
- content + padding + inner content type + AEAD の拡張の長さ
- TLSCiphertext.encrypted_record の長さ
- encrypted_record
- AEAD で暗号化された TLSInnerPlaintext
- AEAD アルゴリズムの入力は
- 1つの鍵
- client_write_key or server_write_key
- nonce
- シーケンス番号と client_write_iv/server_write_iv から導出
- plaintext
- TLSInnerPlaintext でエンコードされている
- traffic keys の導出は 7.3 参照
- additional data
- 1つの鍵
additional_data = TLSCiphertext.opaque_type ||
TLSCiphertext.legacy_record_version ||
TLSCiphertext.length
- AEAD の出力は AEAD 暗号化の暗号文を含む
- 平文の長さは TLSPlaintext.length よりも長くなる
- TLSInnerPlaintext.type やパディングが含まれるので
- AEAD の出力長は平文より普通大きくなるが、その大きさはアルゴリズムによる
AEADEncrypted =
AEAD-Encrypt(write_key, nonce, additional_data, plaintext)
- AEADEncrypted の値は TLSCiphertext.encrypted_record にセットされる
- 復号と検証のため、cipher は引数として鍵、nonce、additional data、AEAD Encrypted value の値を取る
- 出力は平文か復号失敗を表すエラーとなる
- separate integrity check はしない
- (レコード間の完全性チェックの話か?)
plaintext of encrypted_record =
AEAD-Decrypt(peer_write_key, nonce,
additional_data, AEADEncrypted)
- 64 bit のシーケンス番号が reading / writing レコードに別々に維持される
- 適切なシーケンス番号が読み書き後に 1 ずつインクリメントされる
- それぞれのシーケンス番号はコネクション開始時や鍵変更時に 0 とセットされる。
- AEAD の各アルゴリズムではレコードごとに利用できる nonce の長さを規定している
- nonce の組み立て
- network byte order でエンコードされた 64 bit レコードシーケンス番号を iv_length まで左からパディング
- パディングしたシーケンス番号を client_write_iv / server_write_iv で XOR
- この結果をレコードごとの nonce とする
- nonce の組み立て
- 暗号化された TLS レコードは TLSCiphertext にパディングを入れて大きくすることができる
- 送信者が中間者からトラフィックのサイズを隠すことができる
- アプリケーションデータのレコードはサーバーが望むなら長さ 0 の TLSInnerPlaintext.content を含めても良い
- ハンドシェイクとかアラートのレコードは駄目
- 送信されたパディングは record protection mechanism によって検証される
- パディングは zero octets で、zero octets な値を受信者はパディングでない値だと判断する(それは content type である)
- TLS ハンドシェイクでは 1 つ以上の入力シークレットを使えるようにする(pre shared key ってやつ?
- それらのシークレットを組み合わせることで実際に使うキーマテリアルを作る
- 鍵の導出プロセスは入力シークレットとハンドシェイクそのものからなる
- ハンドシェイクのメッセージはランダムな値を含むので、鍵が同じだったとしてもトラフィックのやり取りに使う鍵は別の値になる
- 鍵の導出プロセスでは HKDF で定義されている HKDF-Extract / HKDF-Expand をう利用する。
HKDF-Expand-Label(Secret, Label, Context, Length) =
HKDF-Expand(Secret, HkdfLabel, Length)
Where HkdfLabel is specified as:
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 で使われるハッシュ関数は cihper suites hash algorithm である
- (TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 なら SHA256)
- Hash.length は出力のバイト長
- Messages はハンドシェイクのメッセージをつなげたやつ
- type/length フィールドは含むが record レイヤーのヘッダーは含まない
- (struct Handshake まるごとという意味だと思う)
- type/length フィールドは含むが record レイヤーのヘッダーは含まない
- HKDF-Expand-Label に渡される Context は長さ 0 のケースがある
- label はこのドキュメントに書いてある
- 鍵は PSK / (EC)DHE shared secret の 2 つを入力にして HKDF-Extract と Derive-Secret 関数から導出される
(EC)DHE -> HKDF-Extract = Handshake 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
の client_handshake_traffic_secret って
Handshake Secret = HKDF-Extract(salt, ECDHE で交換したシークレット)
Derive-Secret(Handshake Secret, "c hs traffic", ClientHello...ServerHello)
でいいんかな PSK がない場合は salt はなしになる、でいいんだろうか
参考: https://zenn.dev/0a24/articles/tls1_3-rfc8448#%7Bserver%7D-extract-secret-%22early%22%3A 参考: https://crypto.stackexchange.com/questions/101882/in-tls-1-3-is-the-first-derived-secret-always-a-constant-value-in-non-psk-hand
PSK がない場合
Early Secret = HKDF-Extract(0, 0)
Input For HandshakeSecret = Derive-Secret(Early Secret, "derived", "")
Handshake Secret = HKDF-Extract(Input For HandshakeSecret, ECDHE で交換したシークレット)
client_handshake_traffic_secret = Derive-Secret(Handshake Secret, "c hs traffic", ClientHello...ServerHello)
- KeyUpdate でキーのローテーションするやつ