diff --git a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsExtension.kt b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsExtension.kt index 1f66ef0a4..2926bccbe 100644 --- a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsExtension.kt +++ b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsExtension.kt @@ -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 diff --git a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsKeySchedule.kt b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsKeySchedule.kt index b69504a96..a64d0cb34 100644 --- a/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsKeySchedule.kt +++ b/quic/src/commonMain/kotlin/com/vitorpamplona/quic/tls/TlsKeySchedule.kt @@ -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")