prep(quic): TLS 0-RTT key derivation + early_data extension encoder
Foundation for the 0-RTT path that follows. Two additive pieces: - TlsKeySchedule.clientEarlyTrafficSecret + deriveEarlyTraffic (transcriptAfterClientHello). RFC 8446 §7.1: client_early_traffic_secret = Derive-Secret(early_secret, "c e traffic", H(ClientHello)). Driven by the QUIC layer right after the resumption ClientHello is appended to the transcript so the early-data keys are available for the writer to install before ServerHello arrives. - encodeEarlyDataEmpty for the ClientHello-side early_data extension body (empty per RFC 8446 §4.2.10 — its mere presence signals "I'm about to send 0-RTT"). NewSessionTicket carries a uint32 max_early_data_size variant which is parsed but not yet acted on; the resumption path doesn't require it. Wire build, packet protection, and pre-handshake stream creation follow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -178,6 +178,20 @@ fun encodePreSharedKeyPlaceholder(
|
||||
return w.toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the `early_data` extension body. In a ClientHello the body is
|
||||
* empty (the extension's mere presence signals "I'm sending 0-RTT
|
||||
* data"). In a NewSessionTicket the body is `uint32 max_early_data_size`.
|
||||
* In an EncryptedExtensions message the body is empty (server's
|
||||
* acceptance signal). We only emit the empty form (ClientHello side).
|
||||
*
|
||||
* RFC 8446 §4.2.10. Trailing position requirement: it goes WITH the
|
||||
* pre_shared_key extension in the ClientHello extensions list — we put
|
||||
* it just before pre_shared_key for symmetry with what aioquic / picoquic
|
||||
* emit.
|
||||
*/
|
||||
fun encodeEarlyDataEmpty(): ByteArray = ByteArray(0)
|
||||
|
||||
/** RFC 8446 §4.2.11.2 — SHA-256 binder size for our cipher suites. */
|
||||
const val BINDER_BYTES: Int = 32
|
||||
|
||||
|
||||
@@ -59,6 +59,15 @@ class TlsKeySchedule(
|
||||
var serverApplicationSecret: ByteArray? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* RFC 8446 §7.1 — `client_early_traffic_secret`, derived from the
|
||||
* early secret + transcript-up-to-and-including-ClientHello. Used to
|
||||
* encrypt 0-RTT packets the client sends before ServerHello arrives.
|
||||
* Only non-null on resumption-with-early-data connections.
|
||||
*/
|
||||
var clientEarlyTrafficSecret: ByteArray? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* RFC 8446 §7.1 resumption master secret. Derived AFTER client Finished
|
||||
* is sent (transcript = CH..client.Finished). Used as the input keying
|
||||
@@ -99,6 +108,22 @@ class TlsKeySchedule(
|
||||
earlySecret = HKDF.extract(psk, zeros)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the client early-data traffic secret. RFC 8446 §7.1:
|
||||
*
|
||||
* client_early_traffic_secret = Derive-Secret(Early Secret,
|
||||
* "c e traffic", H(ClientHello))
|
||||
*
|
||||
* Caller passes the post-ClientHello transcript hash explicitly so
|
||||
* the schedule doesn't have to track which transcript snapshot is
|
||||
* needed (this is the binder-substituted ClientHello, exactly the
|
||||
* bytes the server will hash on its side).
|
||||
*/
|
||||
fun deriveEarlyTraffic(transcriptAfterClientHello: ByteArray) {
|
||||
val es = earlySecret ?: error("call deriveEarlyFromPsk first")
|
||||
clientEarlyTrafficSecret = deriveSecret(es, "c e traffic", transcriptAfterClientHello)
|
||||
}
|
||||
|
||||
/** Step 2: derive Handshake Secret using ECDHE shared secret. */
|
||||
fun deriveHandshake(ecdheSharedSecret: ByteArray) {
|
||||
val early = earlySecret ?: error("call deriveEarly first")
|
||||
|
||||
Reference in New Issue
Block a user