Last active
April 6, 2022 21:12
-
-
Save s-macke/afafd39bda59fc292c5b23d47bb5cf8b to your computer and use it in GitHub Desktop.
QUIC HTTP/3 Parsing of the Initial Packet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
The HTTP/3 protocol is on the rise and brings huge improvements in terms of speed and security. | |
One of them is, that the so called "Server Name Identification" or SNI is mandatory and is sent | |
in the first packet by the client. | |
https://en.wikipedia.org/wiki/Server_Name_Indication | |
Parsing and retrieving the SNI should be easy, right? | |
Well, here is a complete code example to retrieve the SNI and other information from the | |
initial QUIC packet. The first packet is encrypted and protected. But the encryption keys | |
are given and part of the specification. The real handshake to establish a secure | |
connection is done afterwards. | |
Specification | |
https://www.rfc-editor.org/info/rfc9000 | |
https://www.rfc-editor.org/info/rfc9001 | |
The code is written Go. Compile with | |
go mod init http3 | |
go mod tidy | |
go build | |
The program listens on a UDP ports and waits for connection attempts, but does not respond. | |
Currently, testing is not easy because the specification is not yet ready and not adopted by the browsers. | |
You can use quic-go for example: | |
https://github.com/lucas-clemente/quic-go | |
*/ | |
package main | |
import ( | |
"crypto/aes" | |
"crypto/cipher" | |
"crypto/sha256" | |
"encoding/hex" | |
"errors" | |
"flag" | |
"fmt" | |
"golang.org/x/crypto/hkdf" | |
"io" | |
"log" | |
"net" | |
"net/http" | |
"strconv" | |
) | |
type ReadBuffer struct { | |
b []byte | |
offset int | |
} | |
func NewReadBuffer(b []byte) *ReadBuffer { | |
return &ReadBuffer{ | |
b: b, | |
offset: 0, | |
} | |
} | |
func (rb *ReadBuffer) Length() int { | |
return len(rb.b) | |
} | |
func (rb *ReadBuffer) ReadNextByte() byte { | |
if rb.offset >= len(rb.b) { | |
return 0 | |
} | |
b := rb.b[rb.offset] | |
rb.offset++ | |
return b | |
} | |
func (rb *ReadBuffer) ReadSlice(n int) []byte { | |
//fmt.Println("ReadSlice", n, rb.offset, len(rb.b)) | |
b := rb.b[rb.offset : rb.offset+n] | |
rb.offset = rb.offset + n | |
return b | |
} | |
func (rb *ReadBuffer) NewReadBuffer(n int) *ReadBuffer { | |
b := rb.b[rb.offset : rb.offset+n] | |
rb.offset = rb.offset + n | |
return NewReadBuffer(b) | |
} | |
func (rb *ReadBuffer) SkipNBytes(n int) { | |
rb.offset = rb.offset + n | |
} | |
func (rb *ReadBuffer) EOF() bool { | |
return rb.offset >= len(rb.b) | |
} | |
/* ReadVarInt: | |
RFC 9000Chapter 16 | |
+======+========+=============+=======================+ | |
| 2MSB | Length | Usable Bits | Range | | |
+======+========+=============+=======================+ | |
| 00 | 1 | 6 | 0-63 | | |
+------+--------+-------------+-----------------------+ | |
| 01 | 2 | 14 | 0-16383 | | |
+------+--------+-------------+-----------------------+ | |
| 10 | 4 | 30 | 0-1073741823 | | |
+------+--------+-------------+-----------------------+ | |
| 11 | 8 | 62 | 0-4611686018427387903 | | |
+------+--------+-------------+-----------------------+ | |
*/ | |
func (rb *ReadBuffer) ReadVarInt() int64 { | |
// The length of variable-length integers is encoded in the | |
// first two bits of the first byte. | |
v := int64(rb.ReadNextByte()) | |
prefix := v >> 6 | |
length := 1 << prefix | |
// Once the length is known, remove these bits and read any | |
// remaining bytes. | |
v = v & 0x3f | |
for i := 0; i < length-1; i++ { | |
v = (v << 8) | int64(rb.ReadNextByte()) | |
} | |
return v | |
} | |
func (rb *ReadBuffer) ReadNextInt(bytes int) int64 { | |
var value int64 = 0 | |
for i := 0; i < bytes; i++ { | |
value = (value << 8) | int64(rb.ReadNextByte()) | |
} | |
return value | |
} | |
type ClientHello struct { | |
length int64 | |
versionHigh byte | |
versionLow byte | |
random []byte | |
sessionID []byte | |
cipherSuites []int | |
extensions []int | |
hostname string // from the SNI extension | |
} | |
func UnmarshallClientHello(data []byte) (*ClientHello, error) { | |
ch := &ClientHello{} | |
rb := NewReadBuffer(data) | |
handshakeType := rb.ReadNextByte() | |
if handshakeType != 0x01 { | |
return nil, errors.New("not a TLS ClientHello") | |
} | |
ch.length = rb.ReadNextInt(3) | |
ch.versionHigh = rb.ReadNextByte() | |
ch.versionLow = rb.ReadNextByte() | |
if ch.versionHigh != 0x03 || ch.versionLow != 0x03 { | |
return nil, errors.New("not a TLS 1.2 ClientHello") | |
} | |
ch.random = rb.ReadSlice(32) | |
sessionIdLength := rb.ReadNextByte() | |
ch.sessionID = rb.ReadSlice(int(sessionIdLength)) | |
cipherSuitesLength := rb.ReadNextInt(2) | |
if cipherSuitesLength&1 != 0 { | |
return nil, errors.New("cipherSuitesLength is not even") | |
} | |
for i := 0; i < int(cipherSuitesLength)/2; i++ { | |
cipherSuite := rb.ReadNextInt(2) | |
ch.cipherSuites = append(ch.cipherSuites, int(cipherSuite)) | |
} | |
compressionMethodsLength := rb.ReadNextByte() | |
/* | |
if compressionMethodsLength != 0 { | |
return nil, errors.New("compressionMethodsLength is not zero") | |
} | |
*/ | |
rb.SkipNBytes(int(compressionMethodsLength)) | |
extensionsLength := rb.ReadNextInt(2) | |
rb = rb.NewReadBuffer(int(extensionsLength)) | |
for !rb.EOF() { | |
extensionType := rb.ReadNextInt(2) | |
extensionLength := rb.ReadNextInt(2) | |
ch.extensions = append(ch.extensions, int(extensionType)) | |
switch extensionType { | |
case 0x00: | |
listEntryLength := rb.ReadNextInt(2) | |
_ = listEntryLength | |
listEntryType := rb.ReadNextByte() | |
_ = listEntryType | |
hostnameLength := rb.ReadNextInt(2) | |
ch.hostname = string(rb.ReadSlice(int(hostnameLength))) | |
default: | |
rb.SkipNBytes(int(extensionLength)) | |
} | |
} | |
return ch, nil | |
} | |
func ParseFrames(data []byte) (*ClientHello, error) { | |
rb := NewReadBuffer(data) | |
var ch *ClientHello = nil | |
var err error | |
for !rb.EOF() { | |
frameType := rb.ReadNextByte() // or rb.ReadVarInt() ? | |
switch frameType { | |
case 0x00: // Chapter 19.1 PADDING Frames. Ignore | |
case 0x06: // RFC 9000 Chapter 19.6. CRYPTO Frames | |
_ = rb.ReadVarInt() // read offset, not used here | |
length := rb.ReadVarInt() | |
ch, err = UnmarshallClientHello(rb.ReadSlice(int(length))) | |
if err != nil { | |
return nil, err | |
} | |
default: | |
return nil, fmt.Errorf("unknown frame type in initial packet: %d", frameType) | |
} | |
} | |
if ch == nil { | |
return nil, errors.New("no ClientHello found") | |
} | |
return ch, nil | |
} | |
/* | |
+======+===========+================+ | |
| Type | Name | Section | | |
+======+===========+================+ | |
| 0x00 | Initial | Section 17.2.2 | | |
+------+-----------+----------------+ | |
| 0x01 | 0-RTT | Section 17.2.3 | | |
+------+-----------+----------------+ | |
| 0x02 | Handshake | Section 17.2.4 | | |
+------+-----------+----------------+ | |
| 0x03 | Retry | Section 17.2.5 | | |
+------+-----------+----------------+ | |
*/ | |
type PacketType int | |
const ( | |
PacketTypeInitial PacketType = 0 | |
PacketType0RTT = 1 | |
PacketTypeHandshake = 2 | |
PacketTypeRetry = 3 | |
) | |
type longHeader struct { | |
isLongHeader bool | |
isVersionNegotiation bool | |
packetType PacketType | |
version int32 | |
destId []byte | |
srcId []byte | |
token []byte | |
packetLength int64 | |
headerLength int // header length in bytes without packet number length | |
// Protected Header fields. Only valid after the header is unprotected | |
reserved byte | |
packetNumberLength int // packet number length in bytes | |
packetNumber int64 | |
payloadOffset int // header length in bytes with packet number length. Identical to the payload offset | |
} | |
func ParseInitialLongHeader(data []byte) (*longHeader, error) { | |
var lh longHeader | |
rb := NewReadBuffer(data) | |
firstByte := rb.ReadNextByte() | |
lh.isLongHeader = (firstByte & 0x80) != 0 | |
if !lh.isLongHeader { | |
return nil, errors.New("not a long header") | |
} | |
lh.isVersionNegotiation = (firstByte & 0x40) == 0 | |
if lh.isVersionNegotiation { | |
return nil, errors.New("version negotiation not supported") | |
} | |
lh.reserved = firstByte & 0xc | |
// reserved bits must be zero, but only after the header is unprotected | |
/* | |
if lh.reserved != 0 { | |
return nil, errors.New("reserved bits not zero") | |
} | |
*/ | |
lh.packetType = PacketType((firstByte >> 4) & 3) | |
if lh.packetType != PacketTypeInitial { | |
return nil, errors.New("packet type not initial") | |
} | |
// from here on only valid for initial packets | |
lh.packetNumberLength = int((firstByte & 3) + 1) // header protected | |
lh.version = int32(rb.ReadNextInt(4)) | |
if lh.version != 0x0000001 { | |
return nil, fmt.Errorf("unsupported version: %x", lh.version) | |
} | |
destIdLen := rb.ReadNextByte() | |
if destIdLen == 0 { | |
return nil, errors.New("destination ID length zero") | |
} | |
lh.destId = rb.ReadSlice(int(destIdLen)) | |
srcIdLen := rb.ReadNextByte() | |
if srcIdLen != 0 { | |
return nil, errors.New("src ID length not zero") | |
} | |
lh.srcId = rb.ReadSlice(int(srcIdLen)) | |
tokenLen := rb.ReadVarInt() | |
if srcIdLen != 0 { | |
return nil, errors.New("token length not zero") | |
} | |
lh.token = rb.ReadSlice(int(tokenLen)) | |
lh.packetLength = rb.ReadVarInt() | |
if lh.packetLength == 0 { | |
return nil, errors.New("packet length zero") | |
} | |
if lh.packetLength >= int64(len(data)) { | |
return nil, errors.New("packet length exceeds received packet length") | |
} | |
lh.headerLength = rb.offset | |
lh.packetNumber = rb.ReadNextInt(lh.packetNumberLength) | |
lh.payloadOffset = rb.offset | |
return &lh, nil | |
} | |
type CryptoSetup struct { | |
aesGcm cipher.AEAD | |
blockHp cipher.Block | |
iv []byte | |
} | |
func createCrypto(lh *longHeader) (*CryptoSetup, error) { | |
var cs CryptoSetup | |
initialSalt, _ := hex.DecodeString("38762cf7f55934b34d179ae6a4c80cadccbb7f0a") | |
clientIn, _ := hex.DecodeString("00200f746c73313320636c69656e7420696e00") | |
//serverIn, _ := hex.DecodeString("00200f746c7331332073657276657220696e00") // we don't respond to the request | |
quicKey, _ := hex.DecodeString("00100e746c7331332071756963206b657900") | |
quicIv, _ := hex.DecodeString("000c0d746c733133207175696320697600") | |
quichp, _ := hex.DecodeString("00100d746c733133207175696320687000") | |
hash := sha256.New | |
initialSecret := hkdf.Extract(hash, lh.destId, initialSalt) | |
clientInitialSecret := make([]byte, 32) | |
_, err := hkdf.Expand(hash, initialSecret, clientIn).Read(clientInitialSecret) | |
if err != nil { | |
return nil, err | |
} | |
key := make([]byte, 16) | |
_, err = hkdf.Expand(hash, clientInitialSecret, quicKey).Read(key) | |
if err != nil { | |
return nil, err | |
} | |
cs.iv = make([]byte, 12) | |
_, err = hkdf.Expand(hash, clientInitialSecret, quicIv).Read(cs.iv) | |
if err != nil { | |
return nil, err | |
} | |
hp := make([]byte, 16) | |
_, err = hkdf.Expand(hash, clientInitialSecret, quichp).Read(hp) | |
if err != nil { | |
return nil, err | |
} | |
// https://pkg.go.dev/crypto/cipher | |
block, err := aes.NewCipher(key) | |
if err != nil { | |
return nil, err | |
} | |
cs.aesGcm, err = cipher.NewGCM(block) | |
if err != nil { | |
return nil, err | |
} | |
cs.blockHp, err = aes.NewCipher(hp) | |
if err != nil { | |
return nil, err | |
} | |
return &cs, nil | |
} | |
func (cs *CryptoSetup) DecryptMask(block []byte) []byte { | |
mask := make([]byte, cs.blockHp.BlockSize()) | |
cs.blockHp.Encrypt(mask, block) | |
return mask | |
} | |
func (cs *CryptoSetup) DecryptPayload(data []byte, lh *longHeader) []byte { | |
start := lh.headerLength + lh.packetNumberLength | |
end := start + int(lh.packetLength) - lh.packetNumberLength | |
src := data[start:end] | |
// TODO: xor with packetNumber ? | |
data2, err := cs.aesGcm.Open(nil, cs.iv, src, data[0:lh.headerLength+lh.packetNumberLength]) | |
if err != nil { | |
panic(err) | |
} | |
return data2 | |
} | |
func UnprotectHeader(data []byte, lh *longHeader, cs *CryptoSetup) { | |
mask := cs.DecryptMask(data[lh.headerLength+4 : lh.headerLength+4+16]) | |
if lh.isLongHeader { | |
data[0] ^= mask[0] & 0xf | |
} else { | |
data[0] ^= mask[0] & 0x1f | |
} | |
switch lh.packetType { | |
case PacketTypeInitial: | |
lh.packetNumberLength = int((data[0] & 3) + 1) // header protected | |
for i := 0; i < lh.packetNumberLength; i++ { | |
data[lh.headerLength+i] = data[lh.headerLength+i] ^ mask[i+1] | |
} | |
default: | |
panic("Unsupported packet") | |
} | |
} | |
func ParseInitialPacket(data []byte) error { | |
lh, err := ParseInitialLongHeader(data) | |
if err != nil { | |
return err | |
} | |
cs, err := createCrypto(lh) | |
if err != nil { | |
return err | |
} | |
UnprotectHeader(data, lh, cs) | |
lh, err = ParseInitialLongHeader(data) | |
if err != nil { | |
return err | |
} | |
payload := cs.DecryptPayload(data, lh) | |
clientHello, err := ParseFrames(payload) | |
if err != nil { | |
return err | |
} | |
fmt.Println("Received initial Packet with SNI: '" + clientHello.hostname + "'") | |
return nil | |
} | |
func listenUDP(port int) { | |
addr := net.UDPAddr{ | |
Port: port, | |
IP: net.IPv4(0, 0, 0, 0), | |
} | |
fmt.Println("Listening on UDP", addr) | |
conn, err := net.ListenUDP("udp", &addr) // code does not block here | |
if err != nil { | |
panic(err) | |
} | |
defer conn.Close() | |
var buffer = make([]byte, 1024*1024) // 1 MB | |
for { | |
rlen, remote, err := conn.ReadFromUDP(buffer) | |
if err != nil { | |
fmt.Println(err) | |
} | |
fmt.Println("Connection from", remote, "with", rlen, "bytes") | |
err = ParseInitialPacket(buffer) | |
if err != nil { | |
fmt.Println(err) | |
} | |
} | |
} | |
func main() { | |
var port = flag.Int("port", 4242, "port to listen on") | |
flag.Parse() | |
listenUDP(*port) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment