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:
+87
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user