TLS 1.3 の認証
TLS 1.2以前ではサーバー認証と鍵交換が分離しておらず、処理フローが難解でした。
TLS 1.3では通常の認証で使用するメッセージ群が他のメッセージから分離され統一されました。
TLS 1.3 証明書認証に関連するメッセージは以下の2つ
・Certificateメッセージ: 証明書の送信に使用
・CertificateVerifyメッセージ: 秘密鍵を持っている事の証明に使用
上記2つはサーバー・クライアント側両方で共通です。(ただしクライアント認証はオプションなのでサーバーからの要求があった場合のみ実行されます。)
この他には以下があります。
・Finishedメッセージ: ハンドシェイク完遂時のサーバーとクライアントの双方が使用
トランスクリプトハッシュ transcript hash
TLSでの認証の核はトランスクリプトハッシュ transcript hash と呼ばれる概念です。
これは一言で言うとハンドシェイクでやり取りする複数の一連のメッセージM1,M2,M3,...Mnを連結してハッシュを計算したものをメッセージ認証に使用する、というものです。
TranscriptHash(M1,M2,M3,,,,Mn) = Hash(M1||M2||M3||...||Mn)
連結するメッセージにはRecordプロトコルのヘッダーは含めません。
またサーバーがClientHelloメッセージに対してHelloRetryRequestメッセージで応答する場合は、HelloRetryRequestメッセージの一部にCookie拡張を使用してクライアントが最初に送信したClientHelloメッセージのハッシュを送り返します。
Certificateメッセージ
通信相手に証明書を送付する際に使用します。Certificateメッセージには複数の証明書を格納可能です。個々の証明書に対して任意の拡張を付与できる構造としています。例としてOCSP : Online Certificate Status Protocol の失効情報や CT : Certificate Transparency 証明書の透明性 に登録されていることを証明する SCT : Signed Certificate Timestamps 署名済み証明書タイムスタンプ を含めることが可能です。
Certificateメッセージの例
enum {
X509(0),
RawPublickey(2),
(255)
} CertificateType;
struct {
select (certificate_type) {
sase RawPublicKey:
/* RFC 7200 ANS.1_subjectPublicKeyInfo */
opaque ASN1_subjectPublicKeyInfo<1..2^24-1>;
case X509;
opaque cert_date<1..2^24-1>;
};
Extention extensions<0..2^16-1>;
} CertificateEntry;
struct {
opaque certificate_request_context<0..2^8-1>;
CertificateEntry certificate_list<0..2^24-1>;
} Certificate;
TLS 1.2とは異なりTLS 1.3では証明書チェーンが順番通りに並んでいなくてもOKです。サーバー証明書は先頭である必要がありますが、それ以外の証明書は順番に関する規則はありません。
この設計の背景には、実際に使用される証明書では順序が正しくない証明書チェーンが多数存在している、という事があります。
証明書の送信にはサーバーもクライアントもCertificateメッセージを使用します。クライアントは複数の証明書を利用して自分自身を複数可認証する事もあり得ます。毎回のクライアント認証で別々の証明書を使用する場合にはサーバーから送信するCertificateRequestメッセージ中のcertificate_request_contextフィールドを使用します。
CertificateVerify メッセージ
Certificateメッセージで証明書を送信した直後に、CertificateVerifyメッセージで送信した証明書に対する秘密鍵を保持している事を証明します。CertificateVerifyメッセージには、ハンドシェイクと証明書を関連づけるデジタル署名と署名アルゴリズムを特定する情報を送信しなければなりません。またCertificateVerifyメッセージの後にはFinishedメッセージを送信する必要があります。
CertificateVerifyメッセージの例
struct {
SignatureSchema algorithm;
opaque signature<0..2^16-1>;
} CertificateVerify;
署名をするには証明書に合致する秘密鍵を使わなければなりません。署名の対象は以下の要素からで構成されたデータです。
| No. | データ値 | 説明 |
|---|---|---|
| 1 | 64バイトのスペース(0X20)文字列 | ※旧バージョンのTLSであった、ClientHelloメッセージのRandomフィールドを使った32バイトの選択プレフィックス衝突攻撃?によりメッセージに対する署名を攻撃者が取得できるという問題への対策。64バイトのスペース文字プレフィックスがあることでクライアントとサーバー両方のRandomフィールドのデータがクリアーされます。 |
| 2 | (サーバーから)TLS 1.3, server CertificateVerify (クライアントから)TLS 1.3, client CertificateVerify | 署名の目的を表す。サーバー、クライアント其々で先の文字列をセットして送信する。 |
| 3 | 1バイトのゼロ(0x00) | |
| 4 | トランスクリプトハッシュ値 |
CertificationRequestメッセージ
クライアント認証を要求する場合、サーバーはCertificateRequestメッセージを送信します。CertificateRequestメッセージには証明書の要求であることを示すコンテキストと1つ以上のExtensionが含まれます。
CertificationRequestメッセージの例
stuct {
opaque certificate_request_context<0..2^8-1>;
Extension extensions>2^16-1>;
} CertificateRequest;
Certificate_request_contextフィールドは特定の証明書を一意に特定するために利用される。具体的にはサーバーは同一の接続において一意かつ予測不可能な値を選択してこのフィールドにセットする。クライアントはこのフィールド値をCertificateVerifyメッセージに入れてサーバーに返す。
Certificate_request_contextフィールドはハンドシェイク後にクライアント認証を実施する場合のみ使用され、それ以外の場合はブランクのままになる。
CertficationRequestメッセージのExtensionの例として以下の2つがあります。
signature_algorithms 証明書の要求時に必須。これによりサーバーは自身が受け入れする署名アルゴリズムを指定します。
certificate_authorities 証明書に対応するCA(認証局)を示す場合に指定が可能(オプション)
Finishedメッセージ
毎回のハンドシェイクの最後にFinishedメッセージが送信されます。ハンドシェイクの完全性を検証するために、クライアントとサーバーは交換したデータに対する署名を送信します。この署名が検証できた場合のみ、ハンドシェイクを先に進めます。
この検証が失敗する場合は第三者によってトラフィックが改竄されたことを意味します。
Finishedメッセージの例
struct {
opaque verify_data[Hash.length];
} Finished;
Finishedメッセージはverify_dataフィールドだけが存在します。verify_dataフィールドの値はトランスクリプトハッシュから計算したHMAC値をセットします。
verify_data = HMAC(finished_key,
Transcript-Hash(Handshake Context, Certificate, CertificateVerify))
上記のハッシュ計算にCertificateメッセージとCertificateVerifyメッセージを含めるのはこれらのメッセージがハンドシェイクで使用された場合のみです。
署名に使う鍵は鍵スケジュール(後述)の際に生成されます。
ハンドシェイク後の認証
ハンドシェイク後の認証に対応しているクライアントの場合、クライアントはpost_handshake_authCertificateRequestメッセージを送信してサーバーにクライアント認証を要求できます。
事前共有鍵による認証
認証の方法として最も一般的なのは証明書ですが、TLS 1.3では事前共有鍵(pre-shared keys)による認証も対応しています。
事前共有鍵による認証の場合、クライアントとサーバーは事前に確立した共有鍵で相互に認証するので証明書は不要になります。
事前共有鍵による認証はTLS 1.3では session resumption の実行時必要になる重要な構成要素です。
サーバーはいったん証明書による認証でハンドシェイクに成功するとそのクライアントに対して1つまたは福栖の事前共有鍵を発行できます。移行の接続ではクライアントがその事前共有鍵を使って証明書による認証をバイパスしハンドシェイクのパフォーマンスを向上できます。
クライアントは自身が事前共有鍵に対応している場合、psk_key_exchange_modes拡張で提示します。 psk_key_exchange_modes拡張には以下の2つのオプションがあります。
psk_dhe_ke 前方秘匿性が有効になるオプション。DHEによる鍵交換とECDHEによる鍵交換で利用。
PSK_KE 前方秘匿性が有効にならないオプション。DHEによる鍵交換で利用。(ECDHEでは使用できない)
psk_key_exchange_modeの例
struct {
PskkeyExchangeMode ke_modes<1..255>;
} PskkeyExchangeModes;
enum { psk_ke(0), psk_dhe_key(1), (255) } PskkeyEchngeMode;
認証に必要な情報の転送は pre_shared_key拡張を使用します。
※上記例のPskkey... = Pre_shared_key...の略のようです。
クライアントはPre_shaerd_key拡張のPskIdentityフィールドに事前共有鍵のIDを1つまたは複数提示できます。ID毎に設定する obfuscated_ticket_age はクライアントからみたClientHelloの鮮度(送信ごとにインクリメントされるような値?)を表しリプライ攻撃への防御に必要です。
session_resumption で利用する事前共有鍵の場合には具体的な値がセットされ、それ以外の場合はゼロがセットされます。
クライアントは0-RTTで最初に送信されるデータ(TLS 1.3の?仕様では early data と呼ばれる)をクライアントが最初に提示する事前共有鍵で暗号化しなければなりません。PskBinderEntryフィールドにはクライアントが事前共有鍵を所持していることを証明するためにHMACの値をセットします。この値はTLS 1.3の?仕様では binder と呼ばれます。
Pre_sharedkey拡張の例
struct {
PskIdentity identities<7..2^16-1>;
PskBinderEntry binders<33..2^16-1>;
} OfferdPsks;
struct {
opaque identity<1..2^16-1>;
uint32 obfuscated_ticket_age;
} PskIdentity;
opaque PskBinderEntry<32..255>;
事前共有鍵を用いた認証を受諾する場合、サーバーは事前共有鍵のIDを選択してハンドシェイクを先に進めます。接続およびハンドシェイクの完遂に使う鍵はサーバー・クライアントの双方がこのサーバーが選択した事前共有鍵を使って生成します。
TLS 1.3での事前共有鍵の設計で奇妙に思われる点は、と前置きして以下の説明があります。(筆者としては改善できると考えている、ということでしょうか?)
事前共有鍵のIDを指定するフィールドの書式は定まっておらず、サーバーが session_resumption に対応している・していないで二通りの使い道がある点です。
用途1. サーバーにセッションに関するデータが保存されていてそれをストレージから取り出して有効にするために事前共有鍵のIDを使用する。
用途2. セッションの状態を丸ごと保持するために使用する。この用法の場合、サーバーが事前共有鍵のIDをほぼして改竄を防ぐ必要があります。
session resumption
※原書ではセッションリザンプション、とカタカナ表記ですが、個人的にresumption、と紐づきにくいため英語表記にしています。
TLS 1.3でのsession resumptionは事前共有鍵を使って実装されています。session resumptionに対応するサーバーはハンドシェイクが完遂するまで待ってその後 NewsessionTicketメッセージを使ってチケットを送信します。(複数のチケット送信も可能)
NewsessionTicketメッセージの例
struct {
uint32 ticket_lifetime;
uint32 ticket_age_add;
opapue ticket_nonce<0..255>;
opaque ticket<1..2^16-2>;
} NewSessionTicket;
**クライアントとサーバーはハンドシェイクでやり取りするメッセージとチケットの中のticket_nonceを使用して事前共有鍵を計算します。計算に使われる鍵 resumption_master_secret は後述する鍵スケジュールの祭儀の生成される鍵のうちの1つです。
HKDF-Expand-Laber(resumption_master_secret, "resumption", ticket_nonce, Hash.length)
NewSessionTicketメッセージの残りのフィールドはチケット管理や付加的なシグナリングに利用されます。
ticket_lifetime チケットの寿命を示す秒数。チケット寿命は7日以内=604,800秒以下を指定。
ticket_age_add チケットの有効期限は最初は暗号化されるがそれ以降は平文で見えてしまうので、このフィールドにランダムな値を加算して送信することでチケットが受動的なフィンガープリンティングに使われる手段としている。
ticket_nonce チケットナンス値は同一の接続ごとに一意で、単一のハンドシェイクで複数の事前共有鍵を使えるようにするため利用される。
ticket ticketフィールドの書式は決められておらず、サーバーのsession resumptionの実装に応じて値がセットされる。例として(前述の説明に呼応しています)、サーバーがデータベースからの検索に使う一意な識別子の場合もあれば党外のセッションを復元するのに必要な情報をすべて含んだデータの塊を暗号化したものの場合もあり。(セッション復元時には復号化して利用する)
Extention チケットを利用するためにさらに付加情報が必要な場合に、NewSessionTicketメッセージのExtensionフィールドに指定できます。TLS 1.3では early_data という拡張フィールドだけが定義されています。early_data拡張が指定された場合、そのチケットは遅延無しの 0-RTTで利用できます。 0-RTTを使って転送できるデータの量もこの拡張で指定します。