Skip to content

Instantly share code, notes, and snippets.

@s-macke
Last active April 6, 2022 21:12
Show Gist options
  • Save s-macke/afafd39bda59fc292c5b23d47bb5cf8b to your computer and use it in GitHub Desktop.
Save s-macke/afafd39bda59fc292c5b23d47bb5cf8b to your computer and use it in GitHub Desktop.
QUIC HTTP/3 Parsing of the Initial Packet
/*
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