mirror of
https://github.com/refraction-networking/uquic.git
synced 2025-03-31 10:47:35 +03:00
The lowest offset of CRYPTO frames in a QUIC packet does not necessarily start at zero, such as the second packet of a connection using Kyber key in the client hello. Also updates clienthellod to new repo.
304 lines
10 KiB
Go
304 lines
10 KiB
Go
package quic
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"errors"
|
|
"math"
|
|
"math/big"
|
|
mrand "math/rand"
|
|
|
|
"github.com/refraction-networking/clienthellod"
|
|
"github.com/refraction-networking/uquic/quicvarint"
|
|
)
|
|
|
|
type QUICFrameBuilder interface {
|
|
// Build ingests data from crypto frames without the crypto frame header
|
|
// and returns the byte representation of all frames.
|
|
Build(cryptoData []byte) (allFrames []byte, err error)
|
|
}
|
|
|
|
// QUICFrames is a slice of QUICFrame that implements QUICFrameBuilder.
|
|
// It could be used to deterministically build QUIC Frames from crypto data.
|
|
type QUICFrames []QUICFrame
|
|
|
|
// Build ingests data from crypto frames without the crypto frame header
|
|
// and returns the byte representation of all frames as specified in
|
|
// the slice.
|
|
func (qfs QUICFrames) Build(cryptoData []byte) (payload []byte, err error) {
|
|
if len(qfs) == 0 { // If no frames specified, send a single crypto frame
|
|
qfsCryptoOnly := QUICFrames{QUICFrameCrypto{0, 0}}
|
|
return qfsCryptoOnly.Build(cryptoData)
|
|
}
|
|
|
|
lowestOffset := math.MaxUint16
|
|
for _, frame := range qfs {
|
|
if offset, _, _ := frame.CryptoFrameInfo(); offset < lowestOffset {
|
|
lowestOffset = offset
|
|
}
|
|
}
|
|
|
|
for _, frame := range qfs {
|
|
var frameBytes []byte
|
|
if offset, length, cryptoOK := frame.CryptoFrameInfo(); cryptoOK {
|
|
lengthOffset := offset - lowestOffset
|
|
if length == 0 {
|
|
// calculate length: from offset to the end of cryptoData
|
|
length = len(cryptoData) - lengthOffset
|
|
}
|
|
frameBytes = []byte{0x06} // CRYPTO frame type
|
|
frameBytes = quicvarint.Append(frameBytes, uint64(offset))
|
|
frameBytes = quicvarint.Append(frameBytes, uint64(length))
|
|
frameCryptoData := make([]byte, length)
|
|
copy(frameCryptoData, cryptoData[lengthOffset:]) // copy at most length bytes
|
|
frameBytes = append(frameBytes, frameCryptoData...)
|
|
} else { // Handle none crypto frames: read and append to payload
|
|
frameBytes, err = frame.Read()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
payload = append(payload, frameBytes...)
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
// BuildFromFrames ingests data from all input frames and returns the byte representation
|
|
// of all frames as specified in the slice.
|
|
func (qfs QUICFrames) BuildFromFrames(frames []byte) (payload []byte, err error) {
|
|
// parse frames
|
|
r := bytes.NewReader(frames)
|
|
qchframes, err := clienthellod.ReadAllFrames(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// parse crypto data
|
|
cryptoData, err := clienthellod.ReassembleCRYPTOFrames(qchframes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// marshal
|
|
return qfs.Build(cryptoData)
|
|
}
|
|
|
|
// QUICFrame is the interface for all QUIC frames to be included in the Initial Packet.
|
|
type QUICFrame interface {
|
|
// None crypto frames should return false for cryptoOK
|
|
CryptoFrameInfo() (offset, length int, cryptoOK bool)
|
|
|
|
// None crypto frames should return the byte representation of the frame.
|
|
// Crypto frames' behavior is undefined and unused.
|
|
Read() ([]byte, error)
|
|
}
|
|
|
|
// QUICFrameCrypto is used to specify the crypto frames containing the TLS ClientHello
|
|
// to be sent in the first Initial packet.
|
|
type QUICFrameCrypto struct {
|
|
// Offset is used to specify the starting offset of the crypto frame.
|
|
// Used when sending multiple crypto frames in a single packet.
|
|
//
|
|
// Multiple crypto frames in a single packet must not overlap and must
|
|
// make up an entire crypto stream continuously.
|
|
Offset int
|
|
|
|
// Length is used to specify the length of the crypto frame.
|
|
//
|
|
// Must be set if it is NOT the last crypto frame in a packet.
|
|
Length int
|
|
}
|
|
|
|
// CryptoFrameInfo() implements the QUICFrame interface.
|
|
//
|
|
// Crypto frames are later replaced by the crypto message using the information
|
|
// returned by this function.
|
|
func (q QUICFrameCrypto) CryptoFrameInfo() (offset, length int, cryptoOK bool) {
|
|
return q.Offset, q.Length, true
|
|
}
|
|
|
|
// Read() implements the QUICFrame interface.
|
|
//
|
|
// Crypto frames are later replaced by the crypto message, so they are not Read()-able.
|
|
func (q QUICFrameCrypto) Read() ([]byte, error) {
|
|
return nil, errors.New("crypto frames are not Read()-able")
|
|
}
|
|
|
|
// QUICFramePadding is used to specify the padding frames to be sent in the first Initial
|
|
// packet.
|
|
type QUICFramePadding struct {
|
|
// Length is used to specify the length of the padding frame.
|
|
Length int
|
|
}
|
|
|
|
// CryptoFrameInfo() implements the QUICFrame interface.
|
|
func (q QUICFramePadding) CryptoFrameInfo() (offset, length int, cryptoOK bool) {
|
|
return 0, 0, false
|
|
}
|
|
|
|
// Read() implements the QUICFrame interface.
|
|
//
|
|
// Padding simply returns a slice of bytes of the specified length filled with 0.
|
|
func (q QUICFramePadding) Read() ([]byte, error) {
|
|
return make([]byte, q.Length), nil
|
|
}
|
|
|
|
// QUICFramePing is used to specify the ping frames to be sent in the first Initial
|
|
// packet.
|
|
type QUICFramePing struct{}
|
|
|
|
// CryptoFrameInfo() implements the QUICFrame interface.
|
|
func (q QUICFramePing) CryptoFrameInfo() (offset, length int, cryptoOK bool) {
|
|
return 0, 0, false
|
|
}
|
|
|
|
// Read() implements the QUICFrame interface.
|
|
//
|
|
// Ping simply returns a slice of bytes of size 1 with value 0x01(PING).
|
|
func (q QUICFramePing) Read() ([]byte, error) {
|
|
return []byte{0x01}, nil
|
|
}
|
|
|
|
// QUICRandomFrames could be used to indeterministically build QUIC Frames from
|
|
// crypto data. A caller may specify how many PING and CRYPTO frames are expected
|
|
// to be included in the Initial Packet, as well as the total length plus PADDING
|
|
// frames in the end.
|
|
type QUICRandomFrames struct {
|
|
// MinPING specifies the inclusive lower bound of the number of PING frames to be
|
|
// included in the Initial Packet.
|
|
MinPING uint8
|
|
|
|
// MaxPING specifies the exclusive upper bound of the number of PING frames to be
|
|
// included in the Initial Packet. It must be at least MinPING+1.
|
|
MaxPING uint8
|
|
|
|
// MinCRYPTO specifies the inclusive lower bound of the number of CRYPTO frames to
|
|
// split the Crypto data into. It must be at least 1.
|
|
MinCRYPTO uint8
|
|
|
|
// MaxCRYPTO specifies the exclusive upper bound of the number of CRYPTO frames to
|
|
// split the Crypto data into. It must be at least MinCRYPTO+1.
|
|
MaxCRYPTO uint8
|
|
|
|
// MinPADDING specifies the inclusive lower bound of the number of PADDING frames
|
|
// to be included in the Initial Packet. It must be at least 1 if Length is not 0.
|
|
MinPADDING uint8
|
|
|
|
// MaxPADDING specifies the exclusive upper bound of the number of PADDING frames
|
|
// to be included in the Initial Packet. It must be at least MinPADDING+1 if
|
|
// Length is not 0.
|
|
MaxPADDING uint8
|
|
|
|
// Length specifies the total length of all frames including PADDING frames.
|
|
// If the Length specified is already exceeded by the CRYPTO+PING frames, no
|
|
// PADDING frames will be included.
|
|
Length uint16 // 2 bytes, max 65535
|
|
}
|
|
|
|
// Build ingests data from crypto frames without the crypto frame header
|
|
// and returns the byte representation of all frames as specified in
|
|
// the slice.
|
|
func (qrf *QUICRandomFrames) Build(cryptoData []byte) (payload []byte, err error) {
|
|
// check all bounds
|
|
if qrf.MinPING > qrf.MaxPING {
|
|
return nil, errors.New("MinPING must be less than or equal to MaxPING")
|
|
}
|
|
if qrf.MinCRYPTO < 1 {
|
|
return nil, errors.New("MinCRYPTO must be at least 1")
|
|
}
|
|
if qrf.MinCRYPTO > qrf.MaxCRYPTO {
|
|
return nil, errors.New("MinCRYPTO must be less than or equal to MaxCRYPTO")
|
|
}
|
|
if qrf.MinPADDING < 1 && qrf.Length != 0 {
|
|
return nil, errors.New("MinPADDING must be at least 1 if Length is not 0")
|
|
}
|
|
if qrf.MinPADDING > qrf.MaxPADDING && qrf.Length != 0 {
|
|
return nil, errors.New("MinPADDING must be less than or equal to MaxPADDING if Length is not 0")
|
|
}
|
|
|
|
var frameList QUICFrames = make([]QUICFrame, 0)
|
|
|
|
var cryptoSafeRandUint64 = func(min, max uint64) (uint64, error) {
|
|
minMaxDiff := big.NewInt(int64(max - min))
|
|
offset, err := rand.Int(rand.Reader, minMaxDiff)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return min + offset.Uint64(), nil
|
|
}
|
|
|
|
// determine number of PING frames with crypto.rand
|
|
numPING, err := cryptoSafeRandUint64(uint64(qrf.MinPING), uint64(qrf.MaxPING))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// append PING frames
|
|
for i := uint64(0); i < numPING; i++ {
|
|
frameList = append(frameList, QUICFramePing{})
|
|
}
|
|
|
|
// determine number of CRYPTO frames with crypto.rand
|
|
numCRYPTO, err := cryptoSafeRandUint64(uint64(qrf.MinCRYPTO), uint64(qrf.MaxCRYPTO))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lenCryptoData := uint64(len(cryptoData))
|
|
offsetCryptoData := uint64(0)
|
|
for i := uint64(0); i < numCRYPTO-1; i++ { // select n-1 times, since the last one must be the remaining
|
|
// randomly select length of CRYPTO frame.
|
|
// Length must be at least 1 byte and at most the remaining length of cryptoData minus the remaining number of CRYPTO frames.
|
|
// i.e. len in [1, len(cryptoData)-offsetCryptoData-(numCRYPTO-i-2))
|
|
lenCRYPTO, err := cryptoSafeRandUint64(1, lenCryptoData-(numCRYPTO-i-2))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
frameList = append(frameList, QUICFrameCrypto{Offset: int(offsetCryptoData), Length: int(lenCRYPTO)})
|
|
offsetCryptoData += lenCRYPTO
|
|
lenCryptoData -= lenCRYPTO
|
|
}
|
|
|
|
// append the last CRYPTO frame
|
|
frameList = append(frameList, QUICFrameCrypto{Offset: int(offsetCryptoData), Length: 0}) // 0 means the remaining
|
|
|
|
// dry-run to determine the total length of all frames so far
|
|
dryrunPayload, err := frameList.Build(cryptoData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// determine length of PADDING frames to append
|
|
lenPADDINGsigned := int64(qrf.Length) - int64(len(dryrunPayload))
|
|
if lenPADDINGsigned > 0 {
|
|
lenPADDING := uint64(lenPADDINGsigned)
|
|
// determine number of PADDING frames to append
|
|
numPADDING, err := cryptoSafeRandUint64(uint64(qrf.MinPADDING), uint64(qrf.MaxPADDING))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := uint64(0); i < numPADDING-1; i++ { // select n-1 times, since the last one must be the remaining
|
|
// randomly select length of PADDING frame.
|
|
// Length must be at least 1 byte and at most the remaining length of cryptoData minus the remaining number of CRYPTO frames.
|
|
// i.e. len in [1, lenPADDING-(numPADDING-i-2))
|
|
lenPADDINGFrame, err := cryptoSafeRandUint64(1, lenPADDING-(numPADDING-i-2))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
frameList = append(frameList, QUICFramePadding{Length: int(lenPADDINGFrame)})
|
|
lenPADDING -= lenPADDINGFrame
|
|
}
|
|
|
|
// append the last CRYPTO frame
|
|
frameList = append(frameList, QUICFramePadding{Length: int(lenPADDING)}) // 0 means the remaining
|
|
}
|
|
|
|
// shuffle the frameList
|
|
mrand.Shuffle(len(frameList), func(i, j int) {
|
|
frameList[i], frameList[j] = frameList[j], frameList[i]
|
|
})
|
|
|
|
// build the payload
|
|
return frameList.Build(cryptoData)
|
|
}
|