要約

  • RFC 9846 の KeyUpdate は、送信側の application traffic secret を一方向だけ次世代へ進める。更新message自身は旧送信鍵で保護され、その後のrecordから新世代が使われる。
  • update_requested は相手に逆方向の更新を求めるが、無制限の作業権限ではない。方向は独立し、crossしたrequestは二世代進むことがあり、silent中の複数requestは一回にまとめられ、epochとAEADの上限はlocalに執行される。
  • 証拠は API scheduling、wireへの送出、peerの受理、new-generation data成功を分離する。これらがそろっても、旧secretのmemory消去、再認証、application authorizationまでは証明しない。

一行の成功logを四つに分解する

長時間接続の試験で、server側から update_requested を発行する。applicationが最初に観測するのはAPIの成功かもしれない。しかしOpenSSLでは実際の処理が次のI/Oまで遅れることがある。まだwireには何も出ていない可能性がある。

TLS stackが送るKeyUpdate recordは旧server送信鍵で暗号化される。続くserver発recordは新しいserver application traffic secretを使う。clientは旧受信鍵でtransitionを認証した後に、対応するreceive chainを進める。逆方向を更新する返信はclient自身の旧送信鍵で送り、その後のclient recordが新世代になる。

この順序から、接続全体に一つのepochしかないという見方は成立しない。sendとreceive、clientとserverの状態は対応していても、観測時刻は同じではない。incident時に必要なのは「双方とも7」ではなく、どの方向が、どのmessageを根拠に、いつ6から7へ移ったかである。

現行仕様は RFC 9846

2026年のRFC 9846はRFC 8446をobsoletesし、wire上のTLS 1.3との互換性を維持した。KeyUpdateはhandshake type 24であり、値は update_not_requested(0) と update_requested(1) だけである。Finished前の受信は unexpected_message、未定義値は illegal_parameter となる。

更新messageはkey change直前のrecord boundaryに整列しなければならない。新世代のrecordを受理する前に、旧鍵で保護されたKeyUpdateを受理する必要がある。このbridgeを省けば、truncation attackの余地が生じる。したがって「次鍵でdecryptできた」は、transition authorizationの代わりにならない。

次のsecretは現在の方向別secretから traffic upd labelでHKDF展開される。新しいkeyとIVを得た後、旧secretとkeyは削除すべきとされる。現行仕様は送信epochを 2^48-1 以下に制限し、受信側が相手の上限を強制することは禁じる。応答更新が自分の上限を越えるなら実行せず、request flagを無視すべきである。

Reciprocal request の力は有限である

update_requested は単なるhintではない。受信側は原則として、次のApplication Dataを送る前に update_not_requested のKeyUpdateを返す。それでもpeerがconnection schedulerを所有するわけではない。

未解決のrequested updateがある間、同じ送信側は次のrequested updateを送れない。受信側がsilentな間に複数届いても、応答は一回に集約できる。両側が同時にrequestしてmessageがcrossすると、それぞれが追加応答を送り、両方向が二世代進む場合がある。

仕様は中心的なrekey masterを置かず、並行する二つの主体を扱う。認証されたpeerはbounded requestを出せるが、CPU budget、I/O scheduling、connection termination、memory deletionの決定までは取得しない。

旧secretの削除は遠隔証明できない

new-generation recordが成功すれば、双方がその方向で互換なsecretを導出したことは分かる。分からないことの方が重要である。remote heap、HSM slot、kernel offload、core dump、backupに旧materialが残っているかはwireから見えない。

Forward secrecy after updateは、旧世代を本当に削除するlocal code pathに依存する。peerの返信をもって「安全消去済み」と記録するのは、protocol interoperabilityをcompliance attestationにすり替える行為になる。

KeyUpdateはidentityも更新しない。certificate verification、client identity、service name、application permissionは既存connectionの判断を引き継ぐ。新しいrecord keyは暗号上のfreshnessを与えるが、compromised endpointや誤ったbusiness authorizationを修復しない。

実装の違いが完了時刻を変える

OpenSSLの SSL_key_update() はhandshake完了後に使い、実処理はread/writeまたは明示的なhandshake driveで進む。applicationはpending writeとの順序を管理する必要がある。

GnuTLSの gnutls_session_key_update() はlocal送信方向だけを更新でき、GNUTLS_KU_PEER で相手にも更新を依頼する。record sendと同様にnonblocking結果を返し得る。manualはrekeyとre-authenticationを別機能として説明する。

rustlsの refresh_traffic_keys() も更新messageをscheduleする。通常はcipher suiteのconfidentiality limitをlibraryが追跡するが、kernel APIでrecord protectionを外部化した場合、count、refresh、abortは利用側の責任となる。API名が同じでも、authority boundaryはintegrationによって変わる。

QUICとDTLSを同じeventにしない

QUICはTLS 1.3をhandshakeに使うが、TLS KeyUpdateを送ることを禁止する。QUIC v1はKey Phase、ACK、quic ku 派生を用いる。TLS type 24だけを数えるmonitorは、正常なQUIC更新をゼロと誤認する。

DTLS 1.3はKeyUpdateを使いながら、lossとreorderingのためepochとACKを追加し、遅延datagram用に旧受信materialを保持する。返信がrequestとcrossし得るため、返信自体はacknowledgementではない。上限に近い場合の応答規則もstream TLSと同一ではない。

従ってevent schemaにはtransportとversionが必須である。曖昧な rekeyed=true では、三つの異なる証拠体系を復元できない。

Secretを書かない証拠設計

必要なのはsecret値ではなく、connection reference、role、transport、direction、old/new generation、initiator、request flag、旧鍵transitionの認証、record alignment、pending write、最初のnew-generation record、cross/coalesce、rate-limit判断、terminal alertである。

状態はscheduled、emitted、accepted、data-succeededに分ける。TLS 1.3のKeyUpdateは暗号化されるため、packet capture単独ではmessageを同定しにくい。isolated testではendpoint callback、counter、限定key logを対応させられるが、production observabilityへsecretを常時流してはならない。

memory erasureは別のlocal assurance recordに残す。関連付けはしても、wire eventで代用しない。

Sources