feat(quic): Phase B — TLS 1.3 client on Quartz primitives

Implement a TLS 1.3 client state machine that drives the QUIC handshake using
only Quartz's existing crypto. No BouncyCastle dependency.

- HKDF-Expand and HKDF-Expand-Label upstreamed to Quartz's Hkdf class with
  RFC 5869 + RFC 8448 test vectors covering them.
- :quic crypto stack: AEAD (AES-128-GCM via Quartz's AESGCM, ChaCha20-Poly1305
  via Quartz's pure-Kotlin impl), header protection (AES-ECB via JCA single
  block + ChaCha20 keystream), QUIC Initial-secret derivation matching
  RFC 9001 Appendix A.1 bit-for-bit.
- TLS 1.3 transcript hash, key schedule (early/handshake/master + per-direction
  client/server traffic secrets), Finished MAC.
- ClientHello + extension encoders carrying SNI, supported_versions=[TLS 1.3],
  supported_groups=[X25519], signature_algorithms covering ECDSA/RSA-PSS/Ed25519,
  X25519 key_share, psk_dhe_ke, ALPN=[h3], and the QUIC transport_parameters
  extension.
- ServerHello + EncryptedExtensions + Certificate + CertificateVerify + Finished
  parsers. The state machine handles the certificate path and the PSK-style
  no-cert path; certificate validation is wired through a CertificateValidator
  SPI (real impl lands in Phase L).
- Transport parameters codec covering all RFC 9000 §18.2 + RFC 9221 fields.
- QuicWriter/QuicReader buffer helpers shared across the rest of the stack.

Round-trip test: a minimal in-process TLS server built from the same primitives
drives a full ClientHello → ServerHello → EE → Finished → client Finished
exchange. Both sides reach handshake-complete and agree bit-for-bit on the
handshake & application traffic secrets. ALPN + transport parameters round-trip
through EncryptedExtensions cleanly.

https://claude.ai/code/session_01EC1tfXfap8k8GyKvrxkxZx
This commit is contained in:
Claude
2026-04-25 17:33:54 +00:00
parent 2d541c6fd4
commit 692b034566
22 changed files with 2537 additions and 0 deletions
@@ -106,4 +106,91 @@ class HkdfText {
assertEquals("3769af12ff4dbf44e516a22d1d0512e8bc42516d59e8bf401ea346a4d60dccf7", result2.chachaKey.toHexKey())
assertEquals("77938d29bb13ea73f677ac27", result2.chachaNonce.toHexKey())
}
/**
* RFC 5869 Test Case 1 — basic HKDF-SHA256 with non-empty info.
*/
@Test
fun rfc5869_test_case_1() {
val ikm = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b".hexToByteArray()
val salt = "000102030405060708090a0b0c".hexToByteArray()
val info = "f0f1f2f3f4f5f6f7f8f9".hexToByteArray()
val prk = hkdf.extract(ikm, salt)
assertEquals(
"077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5",
prk.toHexKey(),
)
val okm = hkdf.expand(prk, info, 42)
assertEquals(
"3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
okm.toHexKey(),
)
}
/**
* RFC 5869 Test Case 2 — longer inputs, 82-byte output (spans multiple HMAC rounds).
*/
@Test
fun rfc5869_test_case_2() {
val ikm =
(
"000102030405060708090a0b0c0d0e0f" +
"101112131415161718191a1b1c1d1e1f" +
"202122232425262728292a2b2c2d2e2f" +
"303132333435363738393a3b3c3d3e3f" +
"404142434445464748494a4b4c4d4e4f"
).hexToByteArray()
val salt =
(
"606162636465666768696a6b6c6d6e6f" +
"707172737475767778797a7b7c7d7e7f" +
"808182838485868788898a8b8c8d8e8f" +
"909192939495969798999a9b9c9d9e9f" +
"a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
).hexToByteArray()
val info =
(
"b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" +
"c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
"d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" +
"e0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
"f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
).hexToByteArray()
val prk = hkdf.extract(ikm, salt)
assertEquals(
"06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244",
prk.toHexKey(),
)
val okm = hkdf.expand(prk, info, 82)
assertEquals(
"b11e398dc80327a1c8e7f78c596a4934" +
"4f012eda2d4efad8a050cc4c19afa97c" +
"59045a99cac7827271cb41c65e590e09" +
"da3275600c2f09b8367793a9aca3db71" +
"cc30c58179ec3e87c14c01d5c1f3434f1d87",
okm.toHexKey(),
)
}
/**
* RFC 8448 §3 — TLS 1.3 ClientHello derived "early secret" expand-label vectors.
*
* Verifies our expandLabel implementation matches the canonical TLS 1.3 derivation.
*/
@Test
fun rfc8448_early_secret_derived() {
// PSK = all zeros, salt = all zeros → standard early-secret PRK
val earlySecret = hkdf.extract(ByteArray(32), ByteArray(32))
assertEquals(
"33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a",
earlySecret.toHexKey(),
)
// SHA-256 of empty string
val emptyHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".hexToByteArray()
val derived = hkdf.expandLabel(earlySecret, "derived", emptyHash, 32)
assertEquals(
"6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba",
derived.toHexKey(),
)
}
}