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