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()
|
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. */
|
/** RFC 8446 §4.2.11.2 — SHA-256 binder size for our cipher suites. */
|
||||||
const val BINDER_BYTES: Int = 32
|
const val BINDER_BYTES: Int = 32
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,15 @@ class TlsKeySchedule(
|
|||||||
var serverApplicationSecret: ByteArray? = null
|
var serverApplicationSecret: ByteArray? = null
|
||||||
private set
|
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
|
* RFC 8446 §7.1 resumption master secret. Derived AFTER client Finished
|
||||||
* is sent (transcript = CH..client.Finished). Used as the input keying
|
* is sent (transcript = CH..client.Finished). Used as the input keying
|
||||||
@@ -99,6 +108,22 @@ class TlsKeySchedule(
|
|||||||
earlySecret = HKDF.extract(psk, zeros)
|
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. */
|
/** Step 2: derive Handshake Secret using ECDHE shared secret. */
|
||||||
fun deriveHandshake(ecdheSharedSecret: ByteArray) {
|
fun deriveHandshake(ecdheSharedSecret: ByteArray) {
|
||||||
val early = earlySecret ?: error("call deriveEarly first")
|
val early = earlySecret ?: error("call deriveEarly first")
|
||||||
|
|||||||
Reference in New Issue
Block a user