test(quic): RFC 7541 Huffman + RFC 9001 §A.5 ChaCha20 interop vectors

Add canonical interop fixtures from the IETF RFCs:

- RFC 7541 Appendix C — 8 HPACK / QPACK Huffman decode vectors covering
  short tokens ("www.example.com", "no-cache", "custom-key", "custom-value",
  "302", "private"), the long date-string ("Mon, 21 Oct 2013 20:13:21 GMT"),
  and the URL ("https://www.example.com"). Every byte of the 256-symbol
  Huffman table is exercised across the corpus.
- RFC 9001 §A.5 — ChaCha20-Poly1305 short-header decrypt. The protected
  packet 4cfe4189655e5cd55c41f69080575d7999c25a5bfb decodes byte-for-byte
  to a single PING frame (0x01) with packet number 654_360_564 using the
  RFC's published key, iv, and hp_key.
- AEAD nonce derivation against the same vector — verifies our iv XOR
  direction matches the canonical e0459b3474bdd0e46d417eb0.

These two suites are the single most diagnostic cross-implementation
checks for the QPACK Huffman path and the ChaCha20 packet protection
path. They complement the existing RFC 9000 §A.1 varint, RFC 9001 §A.1
Initial-secret, and RFC 8448 §3 TLS-derived-secret vectors.

https://claude.ai/code/session_01EC1tfXfap8k8GyKvrxkxZx
This commit is contained in:
Claude
2026-04-25 21:22:49 +00:00
parent 46742636c7
commit 0cfbdf5589
2 changed files with 185 additions and 0 deletions
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quic.packet
import com.vitorpamplona.quic.crypto.ChaCha20HeaderProtection
import com.vitorpamplona.quic.crypto.ChaCha20Poly1305Aead
import com.vitorpamplona.quic.crypto.PlatformChaCha20Block
import com.vitorpamplona.quic.crypto.aeadNonce
import kotlin.test.Test
import kotlin.test.assertContentEquals
/**
* RFC 9001 §A.5 — ChaCha20-Poly1305 short-header packet decrypt vector.
*
* Inputs (hex):
* secret = 9ac312a7f877468ebe69422748ad00a15443f18203a07d6060f688f30f21632b
* key = c6d98ff3441c3fe1b2182094f69caa2ed4b716b65488960a7a984979fb23e1c8
* iv = e0459b3474bdd0e44a41c144
* hp_key = 25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4
*
* protected packet = 4cfe4189655e5cd55c41f69080575d7999c25a5bfb
* packet number = 654360564 (encoded in 3 bytes)
* plaintext = 01 (a single PING frame)
*
* The DCID length on the wire is 0 (the example uses an implicit zero-length
* connection id). We reproduce the exact wire decode: HP unmask + AEAD open
* → expected plaintext bytes.
*/
class Rfc9001ChaCha20InteropTest {
@Test
fun rfc9001_a5_chacha20_short_header_decrypt() {
val key = "c6d98ff3441c3fe1b2182094f69caa2ed4b716b65488960a7a984979fb23e1c8".hexToByteArray()
val iv = "e0459b3474bdd0e44a41c144".hexToByteArray()
val hpKey = "25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4".hexToByteArray()
val protectedPkt = "4cfe4189655e5cd55c41f69080575d7999c25a5bfb".hexToByteArray()
val parsed =
ShortHeaderPacket.parseAndDecrypt(
bytes = protectedPkt,
offset = 0,
dcidLen = 0,
aead = ChaCha20Poly1305Aead,
key = key,
iv = iv,
hp = ChaCha20HeaderProtection(PlatformChaCha20Block),
hpKey = hpKey,
largestReceivedInSpace = 654_360_563L,
)
check(parsed != null) { "RFC 9001 §A.5 packet failed to decrypt" }
// Packet number 654_360_564 = 0x2700_03B4
assertContentEquals(byteArrayOf(0x01), parsed.packet.payload)
}
/**
* Verifies the [aeadNonce] XOR direction against the RFC §A.5 packet number
* 0x2700_BFF4 (= 654_360_564 decimal).
*
* iv = e0459b3474bdd0e44a41c144
* pn padded big-endian to 12 bytes = 000000000000000027000bff4
* …but only the low 8 bytes of the iv participate in the XOR.
* Result: e0459b3474bdd0e46d417eb0
*/
@Test
fun rfc9001_a5_chacha20_nonce_derivation() {
val iv = "e0459b3474bdd0e44a41c144".hexToByteArray()
val pn = 654_360_564L
val nonce = aeadNonce(iv, pn)
assertContentEquals("e0459b3474bdd0e46d417eb0".hexToByteArray(), nonce)
}
}
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quic.qpack
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* Huffman decode vectors from RFC 7541 Appendix C — the HPACK / QPACK
* canonical test corpus. Every interoperating HTTP/2 + HTTP/3 implementation
* must reproduce these byte-for-byte.
*/
class HuffmanRfc7541Test {
@Test
fun rfc7541_c41_www_example_com() {
// RFC 7541 §C.4.1: "www.example.com" → 0xf1e3 c2e5 f23a 6ba0 ab90 f4ff
val encoded = "f1e3c2e5f23a6ba0ab90f4ff".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("www.example.com", decoded)
}
@Test
fun rfc7541_c41_no_cache() {
// RFC 7541 §C.4.2: "no-cache" → 0xa8eb 1064 9cbf
val encoded = "a8eb10649cbf".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("no-cache", decoded)
}
@Test
fun rfc7541_c43_custom_key() {
// RFC 7541 §C.4.3: "custom-key" → 0x25a8 49e9 5ba9 7d7f
val encoded = "25a849e95ba97d7f".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("custom-key", decoded)
}
@Test
fun rfc7541_c43_custom_value() {
// RFC 7541 §C.4.3: "custom-value" → 0x25a8 49e9 5bb8 e8b4 bf
val encoded = "25a849e95bb8e8b4bf".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("custom-value", decoded)
}
@Test
fun rfc7541_c63_302() {
// RFC 7541 §C.6.3: status "302" → 0x6402
val encoded = "6402".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("302", decoded)
}
@Test
fun rfc7541_c63_private() {
// RFC 7541 §C.6.3: "private" → 0xaec3 771a 4b
val encoded = "aec3771a4b".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("private", decoded)
}
@Test
fun rfc7541_c63_date_string() {
// RFC 7541 §C.6.3: "Mon, 21 Oct 2013 20:13:21 GMT" →
// 0xd07a be94 1054 d444 a820 0595 040b 8166 e082 a62d 1bff
val encoded = "d07abe941054d444a8200595040b8166e082a62d1bff".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("Mon, 21 Oct 2013 20:13:21 GMT", decoded)
}
@Test
fun rfc7541_c63_url() {
// RFC 7541 §C.6.3: "https://www.example.com" →
// 0x9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43 d3
val encoded = "9d29ad171863c78f0b97c8e9ae82ae43d3".hexToByteArray()
val decoded = QpackHuffman.decode(encoded).decodeToString()
assertEquals("https://www.example.com", decoded)
}
}