mirror of
https://github.com/soypat/lneto.git
synced 2026-08-11 18:33:46 +00:00
use top level method signature for iterators
This commit is contained in:
+116
-138
@@ -200,9 +200,9 @@ func (pc *PacketBreakdown) captureTLSHandshake(dst []Frame, pkt []byte, bitOffse
|
||||
}
|
||||
switch mtype {
|
||||
case tls.HandshakeTypeClientHello:
|
||||
pc.captureTLSHello(finfo, body, true)
|
||||
pc.captureTLSClientHello(finfo, body)
|
||||
case tls.HandshakeTypeServerHello:
|
||||
pc.captureTLSHello(finfo, body, false)
|
||||
pc.captureTLSServerHello(finfo, body)
|
||||
default:
|
||||
if len(body) > 0 {
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
@@ -218,212 +218,190 @@ func (pc *PacketBreakdown) captureTLSHandshake(dst []Frame, pkt []byte, bitOffse
|
||||
return dst
|
||||
}
|
||||
|
||||
// captureTLSHello appends the fields of a ClientHello (isClient) or ServerHello
|
||||
// body to finfo. The two differ only in that the client offers vectors of
|
||||
// cipher suites and compression methods where the server names exactly one of
|
||||
// each. Offsets are relative to the start of the handshake message, so the
|
||||
// handshake header size is added throughout.
|
||||
//
|
||||
// The walk is deliberately more permissive than [tls.NewClientHelloFrame]: a
|
||||
// capture must show what a malformed hello contains, so it reports the fields
|
||||
// it did decode and stops at the first inconsistent length instead of
|
||||
// discarding the message.
|
||||
func (pc *PacketBreakdown) captureTLSHello(finfo *Frame, body []byte, isClient bool) {
|
||||
const hdr = tls.SizeHeaderHandshake
|
||||
const fixed = 2 + tls.SizeRandom + 1 // legacy_version + random + session id length
|
||||
if len(body) < fixed {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
// captureTLSClientHello appends the fields of a ClientHello body to finfo.
|
||||
// Field positions come from the parsed message, so the only arithmetic here is
|
||||
// shifting past the handshake header.
|
||||
func (pc *PacketBreakdown) captureTLSClientHello(finfo *Frame, body []byte) {
|
||||
msg, err := tls.ParseClientHello(body)
|
||||
if err != nil {
|
||||
appendTLSHelloError(finfo, body, err)
|
||||
return
|
||||
}
|
||||
sp := msg.Spans()
|
||||
appendTLSHelloHead(finfo, sp)
|
||||
pc.appendTLSCipherSuites(finfo, msg, sp.CipherSuites)
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
// Pinned to 0x0303 by TLS 1.3 whatever version is really negotiated;
|
||||
// the real version travels in the supported_versions extension.
|
||||
Name: "compression methods",
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: helloBitOffset(sp.Compression.Off),
|
||||
BitLength: sp.Compression.Len * octet,
|
||||
Flags: FlagLegacy,
|
||||
})
|
||||
pc.appendTLSExtensions(finfo, msg.ExtensionList(), sp.Extensions)
|
||||
}
|
||||
|
||||
// captureTLSServerHello appends the fields of a ServerHello body to finfo. It
|
||||
// differs from the client's in naming one suite and one compression method
|
||||
// where the client offers a list of each.
|
||||
func (pc *PacketBreakdown) captureTLSServerHello(finfo *Frame, body []byte) {
|
||||
msg, err := tls.ParseServerHello(body)
|
||||
if err != nil {
|
||||
appendTLSHelloError(finfo, body, err)
|
||||
return
|
||||
}
|
||||
sp := msg.Spans()
|
||||
appendTLSHelloHead(finfo, sp)
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: msg.CipherSuite().StringConst(),
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: helloBitOffset(sp.CipherSuites.Off),
|
||||
BitLength: sp.CipherSuites.Len * octet,
|
||||
}, FrameField{
|
||||
Name: "compression method",
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: helloBitOffset(sp.Compression.Off),
|
||||
BitLength: sp.Compression.Len * octet,
|
||||
Flags: FlagLegacy,
|
||||
})
|
||||
pc.appendTLSExtensions(finfo, msg.ExtensionList(), sp.Extensions)
|
||||
}
|
||||
|
||||
// helloBitOffset converts an offset within a hello body to one within the
|
||||
// handshake message frame.
|
||||
func helloBitOffset(bodyOff int) int {
|
||||
return (tls.SizeHeaderHandshake + bodyOff) * octet
|
||||
}
|
||||
|
||||
// appendTLSHelloHead appends the fields both hellos begin with.
|
||||
func appendTLSHelloHead(finfo *Frame, sp tls.HelloSpans) {
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
// legacy_version. TLS 1.3 pins it to 0x0303 and carries the real version
|
||||
// in supported_versions.
|
||||
Class: FieldClassVersion,
|
||||
FrameBitOffset: hdr * octet,
|
||||
FrameBitOffset: helloBitOffset(0),
|
||||
BitLength: 2 * octet,
|
||||
Flags: FlagLegacy,
|
||||
}, FrameField{
|
||||
Name: "Random",
|
||||
Class: FieldClassID,
|
||||
FrameBitOffset: (hdr + 2) * octet,
|
||||
BitLength: tls.SizeRandom * octet,
|
||||
FrameBitOffset: helloBitOffset(sp.Random.Off),
|
||||
BitLength: sp.Random.Len * octet,
|
||||
})
|
||||
|
||||
off := 2 + tls.SizeRandom
|
||||
sidLen := int(body[off])
|
||||
off++
|
||||
if sidLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
if sidLen > 0 {
|
||||
// TLS 1.3 has no session resumption by ID; a non-empty value means
|
||||
if sp.SessionID.Len > 0 {
|
||||
// TLS 1.3 has no resumption by session ID; a non-empty value means
|
||||
// middlebox compatibility mode, echoed verbatim by the server.
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: "Session ID",
|
||||
Class: FieldClassID,
|
||||
FrameBitOffset: (hdr + off) * octet,
|
||||
BitLength: sidLen * octet,
|
||||
FrameBitOffset: helloBitOffset(sp.SessionID.Off),
|
||||
BitLength: sp.SessionID.Len * octet,
|
||||
})
|
||||
}
|
||||
off += sidLen
|
||||
}
|
||||
|
||||
if isClient {
|
||||
if len(body)-off < 2 {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
suitesLen := int(binary.BigEndian.Uint16(body[off:]))
|
||||
off += 2
|
||||
if suitesLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
pc.appendTLSCipherSuites(finfo, body[off:off+suitesLen], hdr+off)
|
||||
off += suitesLen
|
||||
|
||||
compLen := int(body[off])
|
||||
off++
|
||||
if compLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: "compression methods",
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: (hdr + off) * octet,
|
||||
BitLength: compLen * octet,
|
||||
Flags: FlagLegacy,
|
||||
})
|
||||
off += compLen
|
||||
} else {
|
||||
if len(body)-off < 3 {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
suite := tls.CipherSuite(binary.BigEndian.Uint16(body[off:]))
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Name: suite.StringConst(),
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: (hdr + off) * octet,
|
||||
BitLength: 2 * octet,
|
||||
}, FrameField{
|
||||
Name: "compression method",
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: (hdr + off + 2) * octet,
|
||||
BitLength: octet,
|
||||
Flags: FlagLegacy,
|
||||
})
|
||||
off += 3
|
||||
}
|
||||
|
||||
if len(body)-off < 2 {
|
||||
return // No extensions block. Not a legal 1.3 hello, but not a framing error either.
|
||||
}
|
||||
extsLen := int(binary.BigEndian.Uint16(body[off:]))
|
||||
off += 2
|
||||
if extsLen > len(body)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
extsLen = len(body) - off
|
||||
}
|
||||
pc.appendTLSExtensions(finfo, body[off:off+extsLen], hdr+off)
|
||||
// appendTLSHelloError reports a hello that did not parse. The parsers reject a
|
||||
// whole message on any inconsistent length, which is what makes their iterators
|
||||
// error-free, so there are no field offsets to show: the error and the raw bytes
|
||||
// are all a capture can say.
|
||||
func appendTLSHelloError(finfo *Frame, body []byte, err error) {
|
||||
finfo.Errors = append(finfo.Errors, err)
|
||||
finfo.Fields = append(finfo.Fields, FrameField{
|
||||
Class: FieldClassPayload,
|
||||
FrameBitOffset: helloBitOffset(0),
|
||||
BitLength: len(body) * octet,
|
||||
})
|
||||
}
|
||||
|
||||
// appendTLSCipherSuites appends a cipher_suites container field whose subfields
|
||||
// name each offered suite. base is the byte offset of suites within the frame.
|
||||
// GREASE values show up as their numeric type, which is what a capture should
|
||||
// display: they carry no meaning and are not an error.
|
||||
func (pc *PacketBreakdown) appendTLSCipherSuites(finfo *Frame, suites []byte, base int) {
|
||||
// name each offered suite. GREASE values show up as such, which is what a
|
||||
// capture should display: they carry no meaning and are not an error.
|
||||
func (pc *PacketBreakdown) appendTLSCipherSuites(finfo *Frame, msg tls.ClientHelloMsg, sp tls.Span) {
|
||||
// Reclaim from the Fields backing array to reuse its SubFields backing array.
|
||||
sfield := internal.SliceReclaim(&finfo.Fields)
|
||||
*sfield = FrameField{
|
||||
Name: "cipher suites",
|
||||
Class: FieldClassOptions,
|
||||
SubFields: sfield.SubFields[:0],
|
||||
FrameBitOffset: base * octet,
|
||||
BitLength: len(suites) * octet,
|
||||
FrameBitOffset: helloBitOffset(sp.Off),
|
||||
BitLength: sp.Len * octet,
|
||||
}
|
||||
if pc.SubfieldLimit <= 0 {
|
||||
return
|
||||
}
|
||||
for off := 0; off+2 <= len(suites); off += 2 {
|
||||
for off, suite := range msg.CipherSuites {
|
||||
if len(sfield.SubFields) >= pc.SubfieldLimit {
|
||||
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
|
||||
return
|
||||
}
|
||||
suite := tls.CipherSuite(binary.BigEndian.Uint16(suites[off:]))
|
||||
sfield.SubFields = append(sfield.SubFields, FrameField{
|
||||
Name: suite.StringConst(),
|
||||
Class: FieldClassType,
|
||||
FrameBitOffset: (base + off) * octet,
|
||||
FrameBitOffset: helloBitOffset(off),
|
||||
BitLength: 2 * octet,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// appendTLSExtensions appends an extensions container field whose subfields are
|
||||
// the individual extensions. base is the byte offset of exts within the frame.
|
||||
func (pc *PacketBreakdown) appendTLSExtensions(finfo *Frame, exts []byte, base int) {
|
||||
// the individual extensions.
|
||||
func (pc *PacketBreakdown) appendTLSExtensions(finfo *Frame, exts tls.ExtensionList, sp tls.Span) {
|
||||
extfield := internal.SliceReclaim(&finfo.Fields)
|
||||
*extfield = FrameField{
|
||||
Name: "extensions",
|
||||
Class: FieldClassOptions,
|
||||
SubFields: extfield.SubFields[:0],
|
||||
FrameBitOffset: base * octet,
|
||||
BitLength: len(exts) * octet,
|
||||
FrameBitOffset: helloBitOffset(sp.Off),
|
||||
BitLength: sp.Len * octet,
|
||||
}
|
||||
if pc.SubfieldLimit <= 0 {
|
||||
return
|
||||
}
|
||||
for off := 0; off+4 <= len(exts); {
|
||||
ext := tls.ExtensionType(binary.BigEndian.Uint16(exts[off:]))
|
||||
n := int(binary.BigEndian.Uint16(exts[off+2:]))
|
||||
off += 4
|
||||
if n > len(exts)-off {
|
||||
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||
return
|
||||
}
|
||||
for off, ext := range exts.All {
|
||||
if len(extfield.SubFields) >= pc.SubfieldLimit {
|
||||
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
|
||||
return
|
||||
}
|
||||
extfield.SubFields = append(extfield.SubFields, tlsExtensionField(ext, exts[off:off+n], base+off))
|
||||
off += n
|
||||
extfield.SubFields = append(extfield.SubFields, tlsExtensionField(ext, off))
|
||||
}
|
||||
}
|
||||
|
||||
// tlsExtensionField describes a single hello extension. Extensions carrying a
|
||||
// human readable value point at that value instead of at the whole extension
|
||||
// body, and the bulky opaque ones are classed as payload so that a
|
||||
// human readable value point at that value, located by the extension's own
|
||||
// iterators, and the bulky opaque ones are classed as payload so that a
|
||||
// [Formatter.FilterClasses] can drop them without losing the rest of the hello.
|
||||
func tlsExtensionField(ext tls.ExtensionType, data []byte, base int) FrameField {
|
||||
func tlsExtensionField(ext tls.ExtensionFrame, dataOff int) FrameField {
|
||||
field := FrameField{
|
||||
Name: ext.StringConst(),
|
||||
Name: ext.Type().StringConst(),
|
||||
Class: FieldClassOptions,
|
||||
FrameBitOffset: base * octet,
|
||||
BitLength: len(data) * octet,
|
||||
FrameBitOffset: helloBitOffset(dataOff),
|
||||
BitLength: len(ext.Data()) * octet,
|
||||
}
|
||||
switch ext {
|
||||
switch ext.Type() {
|
||||
case tls.ExtServerName:
|
||||
// server_name_list(2) + name_type(1) + HostName length(2), then the name.
|
||||
// Only host_name(0) is defined, and no client has ever sent a second entry.
|
||||
const nameOff = 5
|
||||
if len(data) >= nameOff && data[2] == 0 {
|
||||
n := int(binary.BigEndian.Uint16(data[3:5]))
|
||||
if n <= len(data)-nameOff {
|
||||
field.Class = FieldClassText
|
||||
field.FrameBitOffset = (base + nameOff) * octet
|
||||
field.BitLength = n * octet
|
||||
for off, name := range ext.ServerNames {
|
||||
if name.Type != 0 {
|
||||
continue // Only host_name is defined.
|
||||
}
|
||||
field.Class = FieldClassText
|
||||
field.FrameBitOffset = helloBitOffset(off)
|
||||
field.BitLength = len(name.Name) * octet
|
||||
break
|
||||
}
|
||||
|
||||
case tls.ExtALPN:
|
||||
// Each protocol name is length prefixed. Quoting the whole list keeps
|
||||
// every name visible, with the length bytes showing up as escapes.
|
||||
if len(data) >= 2 {
|
||||
// Span the first name through the last so every offered protocol stays
|
||||
// visible; the length bytes between them show up as escapes.
|
||||
first, end := -1, 0
|
||||
for off, proto := range ext.ALPNProtos {
|
||||
if first < 0 {
|
||||
first = off
|
||||
}
|
||||
end = off + len(proto)
|
||||
}
|
||||
if first >= 0 {
|
||||
field.Class = FieldClassText
|
||||
field.FrameBitOffset = (base + 2) * octet
|
||||
field.BitLength = (len(data) - 2) * octet
|
||||
field.FrameBitOffset = helloBitOffset(first)
|
||||
field.BitLength = (end - first) * octet
|
||||
}
|
||||
|
||||
case tls.ExtKeyShare, tls.ExtPreSharedKey, tls.ExtPadding, tls.ExtSessionTicket,
|
||||
|
||||
+17
-19
@@ -18,8 +18,8 @@ func buildSupportedVersions(b *tls.Builder) {
|
||||
}
|
||||
|
||||
func TestBuilderRoundTripThroughParser(t *testing.T) {
|
||||
// Build an extensions block, then walk it back with the parser. Agreement
|
||||
// between the two is the property that matters.
|
||||
// Build a ServerHello extensions block, then walk it back with the parser.
|
||||
// Agreement between the two is the property that matters.
|
||||
var b tls.Builder
|
||||
buf := make([]byte, 64)
|
||||
b.Reset(buf)
|
||||
@@ -27,20 +27,18 @@ func TestBuilderRoundTripThroughParser(t *testing.T) {
|
||||
buildSupportedVersions(&b)
|
||||
b.AddU16(uint16(tls.ExtKeyShare))
|
||||
b.OpenU16()
|
||||
b.OpenU16() // client_shares
|
||||
b.AddU16(uint16(tls.GroupX25519))
|
||||
b.AddU16(uint16(tls.GroupX25519)) // server form: one entry, no list prefix
|
||||
b.OpenU16()
|
||||
b.AddBytes(make([]byte, 8))
|
||||
b.Close()
|
||||
b.Close()
|
||||
b.Close()
|
||||
b.Close()
|
||||
|
||||
out, err := b.Bytes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Strip the outer block length the way ClientHelloFrame.Extensions does.
|
||||
// Strip the outer block length the way ServerHelloMsg.ExtensionBytes does.
|
||||
if len(out) < 2 {
|
||||
t.Fatal("output too short")
|
||||
}
|
||||
@@ -49,22 +47,22 @@ func TestBuilderRoundTripThroughParser(t *testing.T) {
|
||||
t.Fatalf("outer length %d != %d", int(out[0])<<8|int(out[1]), len(exts))
|
||||
}
|
||||
|
||||
var seen []tls.ExtensionType
|
||||
err = tls.ForEachExtension(exts, func(ext tls.ExtensionType, data []byte) error {
|
||||
seen = append(seen, ext)
|
||||
if ext == tls.ExtKeyShare {
|
||||
return tls.ForEachKeyShare(data, func(g tls.NamedGroup, key []byte) error {
|
||||
if g != tls.GroupX25519 || len(key) != 8 {
|
||||
t.Errorf("key share got %v len %d", g, len(key))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
list, err := tls.ParseServerExtensions(exts, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var seen []tls.ExtensionType
|
||||
for _, ext := range list.All {
|
||||
seen = append(seen, ext.Type())
|
||||
if ext.Type() != tls.ExtKeyShare {
|
||||
continue
|
||||
}
|
||||
for _, ks := range ext.KeyShares {
|
||||
if ks.Group != tls.GroupX25519 || len(ks.Key) != 8 {
|
||||
t.Errorf("key share got %v len %d", ks.Group, len(ks.Key))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(seen) != 2 || seen[0] != tls.ExtSupportedVersions || seen[1] != tls.ExtKeyShare {
|
||||
t.Errorf("walked %v", seen)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
// Iterators in this package have the signature of an [iter.Seq2] instead of
|
||||
// returning one, so they are ranged over as a method value without a call:
|
||||
//
|
||||
// for off, ext := range list.All {
|
||||
// for off, ks := range ext.KeyShares {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// A method value bound to a receiver stays on the caller's stack, while a
|
||||
// closure returned from a method escapes to the heap. The int key is always the
|
||||
// item's offset within the enclosing message body, and nested iterators inherit
|
||||
// that base, so a decoder never has to add up prefix widths.
|
||||
//
|
||||
// Every length prefix was checked when the message was parsed, so no iterator
|
||||
// can fail and none returns an error.
|
||||
|
||||
// ExtensionFrame provides access to a single hello extension (RFC 8446 4.2):
|
||||
//
|
||||
// struct {
|
||||
// ExtensionType extension_type; // 2 bytes
|
||||
// opaque extension_data<0..2^16-1>;
|
||||
// } Extension;
|
||||
//
|
||||
// The lists nested inside an extension are reached with the iterators below,
|
||||
// each of which walks only its own extension type.
|
||||
type ExtensionFrame struct {
|
||||
buf []byte
|
||||
// base is where buf starts within the enclosing message body.
|
||||
base int
|
||||
// server records that this came from a server hello. The server forms of
|
||||
// key_share and supported_versions carry a single value, not a list.
|
||||
server bool
|
||||
}
|
||||
|
||||
// NewExtensionFrame wraps buf as an [ExtensionFrame]. It checks the extension's
|
||||
// own framing but not the structure nested inside extension_data; prefer
|
||||
// [ParseClientExtensions] or [ParseServerExtensions], which check both.
|
||||
func NewExtensionFrame(buf []byte) (ExtensionFrame, error) {
|
||||
return newExtensionFrame(buf, 0, false)
|
||||
}
|
||||
|
||||
func newExtensionFrame(buf []byte, base int, server bool) (ExtensionFrame, error) {
|
||||
if len(buf) < 4 {
|
||||
return ExtensionFrame{}, lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(buf[2:4]))
|
||||
if 4+n > len(buf) {
|
||||
return ExtensionFrame{}, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return ExtensionFrame{buf: buf[:4+n], base: base, server: server}, nil
|
||||
}
|
||||
|
||||
// Type returns the extension type.
|
||||
func (ef ExtensionFrame) Type() ExtensionType {
|
||||
return ExtensionType(binary.BigEndian.Uint16(ef.buf[0:2]))
|
||||
}
|
||||
|
||||
// Length returns the declared extension_data length.
|
||||
func (ef ExtensionFrame) Length() uint16 {
|
||||
return binary.BigEndian.Uint16(ef.buf[2:4])
|
||||
}
|
||||
|
||||
// Data returns the extension_data bytes.
|
||||
func (ef ExtensionFrame) Data() []byte { return ef.buf[4:] }
|
||||
|
||||
// RawData returns the extension bytes, type and length included.
|
||||
func (ef ExtensionFrame) RawData() []byte { return ef.buf }
|
||||
|
||||
// DataOffset is where Data starts within the enclosing message body.
|
||||
func (ef ExtensionFrame) DataOffset() int { return ef.base + 4 }
|
||||
|
||||
// ExtensionList is an extensions block whose framing has been checked, down to
|
||||
// the lists inside the extensions this package recognizes.
|
||||
type ExtensionList struct {
|
||||
buf []byte
|
||||
base int
|
||||
server bool
|
||||
}
|
||||
|
||||
// ParseClientExtensions validates a ClientHello extensions block. exts is the
|
||||
// block contents with the outer two-byte length already stripped, and base is
|
||||
// where exts starts within the enclosing message body.
|
||||
//
|
||||
// Unknown extension types, GREASE among them, are accepted and left unchecked.
|
||||
// A repeated known type is rejected with [lneto.ErrInvalidField]: RFC 8446 4.2
|
||||
// forbids duplicates, and tolerating them lets this parser and a middlebox act
|
||||
// on different copies.
|
||||
func ParseClientExtensions(exts []byte, base int) (ExtensionList, error) {
|
||||
return parseExtensions(exts, base, false)
|
||||
}
|
||||
|
||||
// ParseServerExtensions validates a ServerHello extensions block. It differs
|
||||
// from [ParseClientExtensions] in the key_share and supported_versions forms.
|
||||
func ParseServerExtensions(exts []byte, base int) (ExtensionList, error) {
|
||||
return parseExtensions(exts, base, true)
|
||||
}
|
||||
|
||||
func parseExtensions(exts []byte, base int, server bool) (ExtensionList, error) {
|
||||
var seen extSeen
|
||||
for off := 0; off < len(exts); {
|
||||
ef, err := newExtensionFrame(exts[off:], base+off, server)
|
||||
if err != nil {
|
||||
return ExtensionList{}, err
|
||||
}
|
||||
if seen.mark(ef.Type()) {
|
||||
return ExtensionList{}, lneto.ErrInvalidField
|
||||
}
|
||||
err = ef.validate()
|
||||
if err != nil {
|
||||
return ExtensionList{}, err
|
||||
}
|
||||
off += len(ef.RawData())
|
||||
}
|
||||
return ExtensionList{buf: exts, base: base, server: server}, nil
|
||||
}
|
||||
|
||||
// All iterates the extensions in wire order, keyed by the offset of each
|
||||
// extension's data within the enclosing message body.
|
||||
func (l ExtensionList) All(yield func(off int, ext ExtensionFrame) bool) {
|
||||
for off := 0; off < len(l.buf); {
|
||||
ef, err := newExtensionFrame(l.buf[off:], l.base+off, l.server)
|
||||
if err != nil {
|
||||
return // Framing was checked at parse.
|
||||
}
|
||||
if !yield(ef.DataOffset(), ef) {
|
||||
return
|
||||
}
|
||||
off += len(ef.RawData())
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes returns the block contents.
|
||||
func (l ExtensionList) Bytes() []byte { return l.buf }
|
||||
|
||||
// validate checks the structure nested inside extension_data for the types this
|
||||
// package walks. The switch mirrors the iterators below: an extension with no
|
||||
// iterator has no shape to check here.
|
||||
func (ef ExtensionFrame) validate() error {
|
||||
data := ef.Data()
|
||||
switch ef.Type() {
|
||||
case ExtServerName:
|
||||
return validateServerNames(data)
|
||||
case ExtALPN:
|
||||
return validateALPN(data)
|
||||
case ExtSupportedGroups, ExtSignatureAlgorithms, ExtSignatureAlgorithmsCert:
|
||||
body, err := vectorU16(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validateU16List(body)
|
||||
case ExtSupportedVersions:
|
||||
if ef.server {
|
||||
return validateU16List(data) // Bare uint16.
|
||||
}
|
||||
body, err := vectorU8(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validateU16List(body)
|
||||
case ExtKeyShare:
|
||||
return ef.validateKeyShare()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateU16List(b []byte) error {
|
||||
if len(b)%2 != 0 {
|
||||
return lneto.ErrInvalidLengthField
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateServerNames(data []byte) error {
|
||||
body, err := vectorU16(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for off := 0; off < len(body); {
|
||||
if len(body)-off < 3 {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(body[off+1 : off+3]))
|
||||
off += 3
|
||||
if n > len(body)-off {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
off += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateALPN(data []byte) error {
|
||||
body, err := vectorU16(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for off := 0; off < len(body); {
|
||||
n := int(body[off])
|
||||
off++
|
||||
if n == 0 {
|
||||
// A zero-length name would make a walk unable to advance.
|
||||
return lneto.ErrInvalidLengthField
|
||||
} else if n > len(body)-off {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
off += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ef ExtensionFrame) validateKeyShare() error {
|
||||
data := ef.Data()
|
||||
if ef.server {
|
||||
// A ServerHello names one group and its key; a HelloRetryRequest names
|
||||
// only the group.
|
||||
if len(data) == 2 {
|
||||
return nil
|
||||
}
|
||||
return validateKeyShareEntries(data)
|
||||
}
|
||||
body, err := vectorU16(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validateKeyShareEntries(body)
|
||||
}
|
||||
|
||||
func validateKeyShareEntries(b []byte) error {
|
||||
for off := 0; off < len(b); {
|
||||
if len(b)-off < 4 {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(b[off+2 : off+4]))
|
||||
off += 4
|
||||
if n > len(b)-off {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
off += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyShare is one KeyShareEntry of a key_share extension:
|
||||
//
|
||||
// struct {
|
||||
// NamedGroup group;
|
||||
// opaque key_exchange<1..2^16-1>;
|
||||
// } KeyShareEntry;
|
||||
//
|
||||
// A HelloRetryRequest names a group with no key, so Key may be empty. GREASE
|
||||
// key shares carry a deliberately absurd key, commonly one byte, so no length
|
||||
// constraint is placed on Key beyond fitting inside the extension.
|
||||
type KeyShare struct {
|
||||
Group NamedGroup
|
||||
Key []byte
|
||||
}
|
||||
|
||||
// ServerName is one entry of an SNI server_name_list. Type 0 is host_name, the
|
||||
// only type ever defined.
|
||||
type ServerName struct {
|
||||
Type uint8
|
||||
Name []byte
|
||||
}
|
||||
|
||||
// KeyShares iterates the key shares of a key_share extension, in the client or
|
||||
// server form as the enclosing hello requires, keyed by the offset of the
|
||||
// key_exchange bytes. Yields nothing for any other extension type.
|
||||
func (ef ExtensionFrame) KeyShares(yield func(off int, ks KeyShare) bool) {
|
||||
if ef.Type() != ExtKeyShare {
|
||||
return
|
||||
}
|
||||
body, base := ef.Data(), ef.DataOffset()
|
||||
if ef.server {
|
||||
if len(body) == 2 { // HelloRetryRequest: selected_group only.
|
||||
yield(base, KeyShare{Group: NamedGroup(binary.BigEndian.Uint16(body))})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
b, err := vectorU16(body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
body, base = b, base+2
|
||||
}
|
||||
for off := 0; off+4 <= len(body); {
|
||||
group := NamedGroup(binary.BigEndian.Uint16(body[off : off+2]))
|
||||
n := int(binary.BigEndian.Uint16(body[off+2 : off+4]))
|
||||
off += 4
|
||||
if n > len(body)-off {
|
||||
return
|
||||
}
|
||||
if !yield(base+off, KeyShare{Group: group, Key: body[off : off+n]}) {
|
||||
return
|
||||
}
|
||||
off += n
|
||||
}
|
||||
}
|
||||
|
||||
// ServerNames iterates the server_name_list of an SNI extension, keyed by the
|
||||
// offset of the name. Yields nothing for any other extension type.
|
||||
func (ef ExtensionFrame) ServerNames(yield func(off int, name ServerName) bool) {
|
||||
if ef.Type() != ExtServerName {
|
||||
return
|
||||
}
|
||||
body, err := vectorU16(ef.Data())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
base := ef.DataOffset() + 2
|
||||
for off := 0; off+3 <= len(body); {
|
||||
nameType := body[off]
|
||||
n := int(binary.BigEndian.Uint16(body[off+1 : off+3]))
|
||||
off += 3
|
||||
if n > len(body)-off {
|
||||
return
|
||||
}
|
||||
if !yield(base+off, ServerName{Type: nameType, Name: body[off : off+n]}) {
|
||||
return
|
||||
}
|
||||
off += n
|
||||
}
|
||||
}
|
||||
|
||||
// ALPNProtos iterates the protocol names of an ALPN extension, keyed by the
|
||||
// offset of the name. Chrome includes a GREASE entry here, so callers must match
|
||||
// against their own offer list rather than assuming the first name is
|
||||
// meaningful. Yields nothing for any other extension type.
|
||||
func (ef ExtensionFrame) ALPNProtos(yield func(off int, proto []byte) bool) {
|
||||
if ef.Type() != ExtALPN {
|
||||
return
|
||||
}
|
||||
body, err := vectorU16(ef.Data())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
base := ef.DataOffset() + 2
|
||||
for off := 0; off < len(body); {
|
||||
n := int(body[off])
|
||||
off++
|
||||
if n == 0 || n > len(body)-off {
|
||||
return
|
||||
}
|
||||
if !yield(base+off, body[off:off+n]) {
|
||||
return
|
||||
}
|
||||
off += n
|
||||
}
|
||||
}
|
||||
|
||||
// SupportedVersions iterates a supported_versions extension. The ClientHello
|
||||
// form is a list behind a one-byte prefix, unlike every other hello list; the
|
||||
// ServerHello form is a bare uint16, which yields a single value. Yields nothing
|
||||
// for any other extension type.
|
||||
func (ef ExtensionFrame) SupportedVersions(yield func(off int, version uint16) bool) {
|
||||
if ef.Type() != ExtSupportedVersions {
|
||||
return
|
||||
}
|
||||
body, base := ef.Data(), ef.DataOffset()
|
||||
if !ef.server {
|
||||
b, err := vectorU8(body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
body, base = b, base+1
|
||||
}
|
||||
for off := 0; off+2 <= len(body); off += 2 {
|
||||
if !yield(base+off, binary.BigEndian.Uint16(body[off:off+2])) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SupportedGroups iterates a supported_groups extension, keyed by the group's
|
||||
// offset. Yields nothing for any other extension type.
|
||||
func (ef ExtensionFrame) SupportedGroups(yield func(off int, group NamedGroup) bool) {
|
||||
body, base, ok := ef.u16Vector(ExtSupportedGroups)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for off := 0; off+2 <= len(body); off += 2 {
|
||||
if !yield(base+off, NamedGroup(binary.BigEndian.Uint16(body[off:off+2]))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SignatureSchemes iterates a signature_algorithms or signature_algorithms_cert
|
||||
// extension, keyed by the scheme's offset. Yields nothing for any other type.
|
||||
func (ef ExtensionFrame) SignatureSchemes(yield func(off int, scheme SignatureScheme) bool) {
|
||||
want := ef.Type()
|
||||
if want != ExtSignatureAlgorithms && want != ExtSignatureAlgorithmsCert {
|
||||
return
|
||||
}
|
||||
body, base, ok := ef.u16Vector(want)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for off := 0; off+2 <= len(body); off += 2 {
|
||||
if !yield(base+off, SignatureScheme(binary.BigEndian.Uint16(body[off:off+2]))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// u16Vector returns the contents of a two-byte-prefixed vector and where it
|
||||
// starts within the message body, or ok false for another extension type.
|
||||
func (ef ExtensionFrame) u16Vector(want ExtensionType) (body []byte, base int, ok bool) {
|
||||
if ef.Type() != want {
|
||||
return nil, 0, false
|
||||
}
|
||||
body, err := vectorU16(ef.Data())
|
||||
if err != nil {
|
||||
return nil, 0, false
|
||||
}
|
||||
return body, ef.DataOffset() + 2, true
|
||||
}
|
||||
|
||||
// vectorU16 strips a two-byte length prefix and returns the vector contents.
|
||||
// It requires the prefix to describe the buffer exactly: trailing bytes mean
|
||||
// the sender and this parser disagree on the structure, which is precisely the
|
||||
// ambiguity parser-differential attacks exploit.
|
||||
func vectorU16(b []byte) ([]byte, error) {
|
||||
if len(b) < 2 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(b[0:2]))
|
||||
if n != len(b)-2 {
|
||||
if n > len(b)-2 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return nil, errTrailingBytes
|
||||
}
|
||||
return b[2:], nil
|
||||
}
|
||||
|
||||
// vectorU8 strips a one-byte length prefix. See [vectorU16] for why trailing
|
||||
// bytes are rejected.
|
||||
func vectorU8(b []byte) ([]byte, error) {
|
||||
if len(b) < 1 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(b[0])
|
||||
if n != len(b)-1 {
|
||||
if n > len(b)-1 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return nil, errTrailingBytes
|
||||
}
|
||||
return b[1:], nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package tls_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/x/tls"
|
||||
)
|
||||
|
||||
func TestExtensionListWalksAndToleratesGREASE(t *testing.T) {
|
||||
// Two extensions: a GREASE type with empty data, then supported_versions.
|
||||
exts := []byte{
|
||||
0x0a, 0x0a, 0x00, 0x00, // GREASE, len 0
|
||||
0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04, // supported_versions
|
||||
}
|
||||
list, err := tls.ParseClientExtensions(exts, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var types []tls.ExtensionType
|
||||
for off, ext := range list.All {
|
||||
if want := 4*len(types) + 4; off != want {
|
||||
t.Errorf("%v at offset %d want %d", ext.Type(), off, want)
|
||||
}
|
||||
types = append(types, ext.Type())
|
||||
}
|
||||
if len(types) != 2 || types[1] != tls.ExtSupportedVersions {
|
||||
t.Fatalf("got %v", types)
|
||||
}
|
||||
if !tls.IsGREASE(uint16(types[0])) {
|
||||
t.Errorf("first extension %#x not recognized as GREASE", types[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtensionsTruncated(t *testing.T) {
|
||||
for _, tc := range [][]byte{
|
||||
{0x00}, // partial type
|
||||
{0x00, 0x2b, 0x00}, // partial length
|
||||
{0x00, 0x2b, 0x00, 0x05, 0x02, 0x03}, // length overruns
|
||||
} {
|
||||
_, err := tls.ParseClientExtensions(tc, 0)
|
||||
if !errors.Is(err, lneto.ErrTruncatedFrame) {
|
||||
t.Errorf("% x: got %v want ErrTruncatedFrame", tc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExtensionsRejectsMalformedInnerList(t *testing.T) {
|
||||
// Framing inside a recognized extension is checked at parse, which is what
|
||||
// lets the iterators be error-free.
|
||||
for _, tc := range [][]byte{
|
||||
{0x00, 0x0a, 0x00, 0x03, 0x00, 0x01, 0x1d}, // supported_groups, odd list
|
||||
{0x00, 0x2b, 0x00, 0x03, 0x04, 0x03, 0x04}, // supported_versions, prefix overruns
|
||||
{0x00, 0x10, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00}, // alpn, zero-length name
|
||||
{0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x00, 0x09}, // server_name, name overruns
|
||||
} {
|
||||
if _, err := tls.ParseClientExtensions(tc, 0); err == nil {
|
||||
t.Errorf("% x accepted", tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionIterationStopsOnBreak(t *testing.T) {
|
||||
// Two extensions with no inner structure of their own.
|
||||
exts := []byte{0x00, 0x15, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00}
|
||||
list, err := tls.ParseClientExtensions(exts, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := 0
|
||||
for range list.All {
|
||||
n++
|
||||
break
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("walk continued after break: %d iterations", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeySharesAcceptGREASEEntry(t *testing.T) {
|
||||
// Chrome sends a GREASE key share whose key_exchange is a single byte.
|
||||
// Rejecting it as malformed breaks Chrome outright.
|
||||
exts := []byte{
|
||||
0x00, 0x33, 0x00, 0x0d, // key_share, 13 bytes
|
||||
0x00, 0x0b, // client_shares length 11
|
||||
0x1a, 0x1a, 0x00, 0x01, 0x00, // GREASE group, 1 byte body
|
||||
0x00, 0x1d, 0x00, 0x02, 0xab, 0xcd, // x25519, 2 byte body
|
||||
}
|
||||
list, err := tls.ParseClientExtensions(exts, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type share struct {
|
||||
g tls.NamedGroup
|
||||
n int
|
||||
}
|
||||
var got []share
|
||||
for _, ext := range list.All {
|
||||
for _, ks := range ext.KeyShares {
|
||||
got = append(got, share{ks.Group, len(ks.Key)})
|
||||
}
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d shares want 2", len(got))
|
||||
}
|
||||
if !tls.IsGREASE(uint16(got[0].g)) || got[0].n != 1 {
|
||||
t.Errorf("GREASE share mishandled: %+v", got[0])
|
||||
}
|
||||
if got[1].g != tls.GroupX25519 || got[1].n != 2 {
|
||||
t.Errorf("x25519 share mishandled: %+v", got[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerKeyShareHelloRetryRequestForm(t *testing.T) {
|
||||
// A HelloRetryRequest names a group with no key.
|
||||
exts := []byte{0x00, 0x33, 0x00, 0x02, 0x00, 0x1d}
|
||||
list, err := tls.ParseServerExtensions(exts, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := 0
|
||||
for _, ext := range list.All {
|
||||
for _, ks := range ext.KeyShares {
|
||||
n++
|
||||
if ks.Group != tls.GroupX25519 || len(ks.Key) != 0 {
|
||||
t.Errorf("got %v with %d key bytes", ks.Group, len(ks.Key))
|
||||
}
|
||||
}
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("walked %d shares want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestALPNProtos(t *testing.T) {
|
||||
exts := []byte{
|
||||
0x00, 0x10, 0x00, 0x0e,
|
||||
0x00, 0x0c,
|
||||
0x02, 'h', '2',
|
||||
0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
|
||||
}
|
||||
list, err := tls.ParseClientExtensions(exts, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var names []string
|
||||
for _, ext := range list.All {
|
||||
for _, p := range ext.ALPNProtos {
|
||||
names = append(names, string(p))
|
||||
}
|
||||
}
|
||||
if len(names) != 2 || names[0] != "h2" || names[1] != "http/1.1" {
|
||||
t.Errorf("got %q", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportedVersionsClientAndServerForms(t *testing.T) {
|
||||
// The ClientHello form is a list behind a one-byte prefix; the ServerHello
|
||||
// form is a bare uint16.
|
||||
client := []byte{0x00, 0x2b, 0x00, 0x05, 0x04, 0x1a, 0x1a, 0x03, 0x04}
|
||||
list, err := tls.ParseClientExtensions(client, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var vers []uint16
|
||||
for _, ext := range list.All {
|
||||
for _, v := range ext.SupportedVersions {
|
||||
vers = append(vers, v)
|
||||
}
|
||||
}
|
||||
if len(vers) != 2 || vers[1] != tls.VersionTLS13 {
|
||||
t.Errorf("client form got %#x", vers)
|
||||
}
|
||||
|
||||
server := []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04}
|
||||
list, err = tls.ParseServerExtensions(server, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vers = vers[:0]
|
||||
for _, ext := range list.All {
|
||||
for _, v := range ext.SupportedVersions {
|
||||
vers = append(vers, v)
|
||||
}
|
||||
}
|
||||
if len(vers) != 1 || vers[0] != tls.VersionTLS13 {
|
||||
t.Errorf("server form got %#x", vers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVectorRejectsTrailingBytes(t *testing.T) {
|
||||
// A prefix that under-describes its buffer leaves bytes whose meaning this
|
||||
// parser and a middlebox could disagree about.
|
||||
exts := []byte{0x00, 0x0a, 0x00, 0x05, 0x00, 0x02, 0x00, 0x1d, 0xff}
|
||||
if _, err := tls.ParseClientExtensions(exts, 0); err == nil {
|
||||
t.Error("trailing bytes after vector accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubIteratorOffsetsAreBodyRelative(t *testing.T) {
|
||||
// base threads through every nesting level so a decoder adds no arithmetic.
|
||||
const base = 100
|
||||
exts := []byte{
|
||||
0x00, 0x00, 0x00, 0x0b, // server_name, 11 bytes
|
||||
0x00, 0x09, // list length
|
||||
0x00, 0x00, 0x06, // host_name, 6 bytes
|
||||
'a', '.', 'c', 'o', 'm', '!',
|
||||
}
|
||||
list, err := tls.ParseClientExtensions(exts, base)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for off, ext := range list.All {
|
||||
if off != base+4 {
|
||||
t.Errorf("extension data at %d want %d", off, base+4)
|
||||
}
|
||||
for noff, name := range ext.ServerNames {
|
||||
// 4 extension header + 2 list length + 3 entry header
|
||||
if want := base + 9; noff != want {
|
||||
t.Errorf("name at %d want %d", noff, want)
|
||||
}
|
||||
if string(name.Name) != "a.com!" {
|
||||
t.Errorf("name %q", name.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParseClientExtensions(f *testing.F) {
|
||||
f.Add([]byte{0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04})
|
||||
f.Add([]byte{0x0a, 0x0a, 0x00, 0x00})
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
list, err := tls.ParseClientExtensions(b, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
total := 0
|
||||
for off, ext := range list.All {
|
||||
total += 4 + len(ext.Data())
|
||||
if total > len(b) {
|
||||
t.Fatalf("walked %d bytes past input length %d", total, len(b))
|
||||
}
|
||||
if off+len(ext.Data()) > len(b) {
|
||||
t.Fatalf("extension data at %d overruns %d byte input", off, len(b))
|
||||
}
|
||||
// No sub-iterator may escape its slice, whatever the type says.
|
||||
for o, ks := range ext.KeyShares {
|
||||
if o+len(ks.Key) > len(b) {
|
||||
t.Fatalf("key share at %d overruns input", o)
|
||||
}
|
||||
}
|
||||
for o, name := range ext.ServerNames {
|
||||
if o+len(name.Name) > len(b) {
|
||||
t.Fatalf("server name at %d overruns input", o)
|
||||
}
|
||||
}
|
||||
for o, p := range ext.ALPNProtos {
|
||||
if o+len(p) > len(b) {
|
||||
t.Fatalf("alpn name at %d overruns input", o)
|
||||
}
|
||||
}
|
||||
for o := range ext.SupportedVersions {
|
||||
if o+2 > len(b) {
|
||||
t.Fatalf("version at %d overruns input", o)
|
||||
}
|
||||
}
|
||||
for o := range ext.SupportedGroups {
|
||||
if o+2 > len(b) {
|
||||
t.Fatalf("group at %d overruns input", o)
|
||||
}
|
||||
}
|
||||
for o := range ext.SignatureSchemes {
|
||||
if o+2 > len(b) {
|
||||
t.Fatalf("scheme at %d overruns input", o)
|
||||
}
|
||||
}
|
||||
}
|
||||
if total != len(b) {
|
||||
t.Fatalf("clean walk consumed %d of %d bytes", total, len(b))
|
||||
}
|
||||
})
|
||||
}
|
||||
-241
@@ -242,244 +242,3 @@ func (hf HandshakeFrame) ValidateSize(v *lneto.Validator) {
|
||||
v.AddError(lneto.ErrInvalidLengthField)
|
||||
}
|
||||
}
|
||||
|
||||
// ExtensionFrame provides access to a single hello extension (RFC 8446 4.2):
|
||||
//
|
||||
// struct {
|
||||
// ExtensionType extension_type; // 2 bytes
|
||||
// opaque extension_data<0..2^16-1>;
|
||||
// } Extension;
|
||||
type ExtensionFrame struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// NewExtensionFrame wraps buf as an [ExtensionFrame]. Unlike the record and
|
||||
// handshake frames, an extension is only ever parsed out of a fully buffered
|
||||
// hello, so the whole extension must be present.
|
||||
func NewExtensionFrame(buf []byte) (ExtensionFrame, error) {
|
||||
if len(buf) < 4 {
|
||||
return ExtensionFrame{}, lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(buf[2:4]))
|
||||
if 4+n > len(buf) {
|
||||
return ExtensionFrame{}, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return ExtensionFrame{buf: buf[:4+n]}, nil
|
||||
}
|
||||
|
||||
// Type returns the extension type.
|
||||
func (ef ExtensionFrame) Type() ExtensionType {
|
||||
return ExtensionType(binary.BigEndian.Uint16(ef.buf[0:2]))
|
||||
}
|
||||
|
||||
// Length returns the declared extension_data length.
|
||||
func (ef ExtensionFrame) Length() uint16 {
|
||||
return binary.BigEndian.Uint16(ef.buf[2:4])
|
||||
}
|
||||
|
||||
// Data returns the extension_data bytes.
|
||||
func (ef ExtensionFrame) Data() []byte { return ef.buf[4:] }
|
||||
|
||||
// RawData returns the extension bytes, type and length included.
|
||||
func (ef ExtensionFrame) RawData() []byte { return ef.buf }
|
||||
|
||||
// ForEachExtension walks an extension list, calling fn for each extension in
|
||||
// wire order. exts is the contents of the extensions block, with the outer
|
||||
// two-byte list length already stripped; [ClientHelloFrame.Extensions] returns
|
||||
// it in that form.
|
||||
//
|
||||
// The walker is deliberately permissive about extension types: unknown types,
|
||||
// including every GREASE value Chrome injects, are passed to fn like any other.
|
||||
// It is the caller's switch that skips them. The walker is strict about
|
||||
// framing: a length field that overruns the list aborts with
|
||||
// [lneto.ErrTruncatedFrame].
|
||||
//
|
||||
// Modelled on tcp.OptionCodec.ForEachOption.
|
||||
func ForEachExtension(exts []byte, fn func(ExtensionType, []byte) error) error {
|
||||
for off := 0; off < len(exts); {
|
||||
ef, err := NewExtensionFrame(exts[off:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = fn(ef.Type(), ef.Data())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
off += len(ef.RawData())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForEachU16 walks a bare list of big-endian uint16 values, such as the
|
||||
// cipher_suites vector of a ClientHello. b must have even length.
|
||||
func ForEachU16(b []byte, fn func(uint16) error) error {
|
||||
if len(b)%2 != 0 {
|
||||
return lneto.ErrInvalidLengthField
|
||||
}
|
||||
for off := 0; off < len(b); off += 2 {
|
||||
err := fn(binary.BigEndian.Uint16(b[off : off+2]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForEachSupportedGroup walks the contents of a supported_groups extension,
|
||||
// whose payload is a uint16-length-prefixed list of [NamedGroup] values.
|
||||
func ForEachSupportedGroup(extData []byte, fn func(NamedGroup) error) error {
|
||||
body, err := vectorU16(extData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ForEachU16(body, func(v uint16) error { return fn(NamedGroup(v)) })
|
||||
}
|
||||
|
||||
// ForEachSignatureScheme walks the contents of a signature_algorithms (or
|
||||
// signature_algorithms_cert) extension.
|
||||
func ForEachSignatureScheme(extData []byte, fn func(SignatureScheme) error) error {
|
||||
body, err := vectorU16(extData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ForEachU16(body, func(v uint16) error { return fn(SignatureScheme(v)) })
|
||||
}
|
||||
|
||||
// ForEachSupportedVersion walks the contents of a supported_versions extension
|
||||
// as it appears in a ClientHello. Note the prefix here is a single byte, unlike
|
||||
// every other list in the hello; the ServerHello form carries a bare uint16
|
||||
// instead and is not parsed by this function.
|
||||
func ForEachSupportedVersion(extData []byte, fn func(uint16) error) error {
|
||||
body, err := vectorU8(extData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ForEachU16(body, fn)
|
||||
}
|
||||
|
||||
// ForEachKeyShare walks the client_shares list of a key_share extension:
|
||||
//
|
||||
// struct {
|
||||
// NamedGroup group;
|
||||
// opaque key_exchange<1..2^16-1>;
|
||||
// } KeyShareEntry;
|
||||
//
|
||||
// GREASE key shares carry a deliberately absurd body, commonly a single byte,
|
||||
// and must not be treated as malformed. The walker therefore places no
|
||||
// constraint on key_exchange length beyond it fitting inside the list; group
|
||||
// selection is the caller's job.
|
||||
func ForEachKeyShare(extData []byte, fn func(NamedGroup, []byte) error) error {
|
||||
body, err := vectorU16(extData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for off := 0; off < len(body); {
|
||||
if len(body)-off < 4 {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
group := NamedGroup(binary.BigEndian.Uint16(body[off : off+2]))
|
||||
n := int(binary.BigEndian.Uint16(body[off+2 : off+4]))
|
||||
off += 4
|
||||
if n > len(body)-off {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
err = fn(group, body[off:off+n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
off += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForEachALPNProto walks the protocol name list of an ALPN extension. Each
|
||||
// name is a single-byte-length-prefixed string. Chrome includes a GREASE entry
|
||||
// here too, so callers must match against their own offer list rather than
|
||||
// assuming the first entry is meaningful.
|
||||
//
|
||||
// A zero-length protocol name is a protocol violation and aborts the walk.
|
||||
func ForEachALPNProto(extData []byte, fn func([]byte) error) error {
|
||||
body, err := vectorU16(extData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for off := 0; off < len(body); {
|
||||
n := int(body[off])
|
||||
off++
|
||||
if n == 0 {
|
||||
return lneto.ErrInvalidLengthField
|
||||
} else if n > len(body)-off {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
err = fn(body[off : off+n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
off += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForEachServerName walks the server_name_list of an SNI extension:
|
||||
//
|
||||
// struct {
|
||||
// NameType name_type; // 1 byte, 0 = host_name
|
||||
// opaque HostName<1..2^16-1>;
|
||||
// } ServerName;
|
||||
func ForEachServerName(extData []byte, fn func(nameType uint8, name []byte) error) error {
|
||||
body, err := vectorU16(extData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for off := 0; off < len(body); {
|
||||
if len(body)-off < 3 {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
nameType := body[off]
|
||||
n := int(binary.BigEndian.Uint16(body[off+1 : off+3]))
|
||||
off += 3
|
||||
if n > len(body)-off {
|
||||
return lneto.ErrTruncatedFrame
|
||||
}
|
||||
err = fn(nameType, body[off:off+n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
off += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// vectorU16 strips a two-byte length prefix and returns the vector contents.
|
||||
// It requires the prefix to describe the buffer exactly: trailing bytes mean
|
||||
// the sender and this parser disagree on the structure, which is precisely the
|
||||
// ambiguity parser-differential attacks exploit.
|
||||
func vectorU16(b []byte) ([]byte, error) {
|
||||
if len(b) < 2 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(binary.BigEndian.Uint16(b[0:2]))
|
||||
if n != len(b)-2 {
|
||||
if n > len(b)-2 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return nil, errTrailingBytes
|
||||
}
|
||||
return b[2:], nil
|
||||
}
|
||||
|
||||
// vectorU8 strips a one-byte length prefix. See [vectorU16] for why trailing
|
||||
// bytes are rejected.
|
||||
func vectorU8(b []byte) ([]byte, error) {
|
||||
if len(b) < 1 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
n := int(b[0])
|
||||
if n != len(b)-1 {
|
||||
if n > len(b)-1 {
|
||||
return nil, lneto.ErrTruncatedFrame
|
||||
}
|
||||
return nil, errTrailingBytes
|
||||
}
|
||||
return b[1:], nil
|
||||
}
|
||||
|
||||
@@ -171,148 +171,6 @@ func TestHandshakeFrameRawDataIncludesHeader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachExtensionWalksAndToleratesGREASE(t *testing.T) {
|
||||
// Two extensions: a GREASE type with empty data, then supported_versions.
|
||||
exts := []byte{
|
||||
0x0a, 0x0a, 0x00, 0x00, // GREASE, len 0
|
||||
0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04, // supported_versions
|
||||
}
|
||||
var types []tls.ExtensionType
|
||||
err := tls.ForEachExtension(exts, func(ext tls.ExtensionType, data []byte) error {
|
||||
types = append(types, ext)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(types) != 2 || types[1] != tls.ExtSupportedVersions {
|
||||
t.Fatalf("got %v", types)
|
||||
}
|
||||
if !tls.IsGREASE(uint16(types[0])) {
|
||||
t.Errorf("first extension %#x not recognized as GREASE", types[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachExtensionTruncated(t *testing.T) {
|
||||
for _, tc := range [][]byte{
|
||||
{0x00}, // partial type
|
||||
{0x00, 0x2b, 0x00}, // partial length
|
||||
{0x00, 0x2b, 0x00, 0x05, 0x02, 0x03}, // length overruns
|
||||
} {
|
||||
err := tls.ForEachExtension(tc, func(tls.ExtensionType, []byte) error { return nil })
|
||||
if !errors.Is(err, lneto.ErrTruncatedFrame) {
|
||||
t.Errorf("% x: got %v want ErrTruncatedFrame", tc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachExtensionPropagatesCallbackError(t *testing.T) {
|
||||
sentinel := errors.New("stop")
|
||||
exts := []byte{0x00, 0x2b, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00}
|
||||
n := 0
|
||||
err := tls.ForEachExtension(exts, func(tls.ExtensionType, []byte) error {
|
||||
n++
|
||||
return sentinel
|
||||
})
|
||||
if !errors.Is(err, sentinel) {
|
||||
t.Errorf("got %v want sentinel", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("walk continued after callback error: %d calls", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachKeyShareAcceptsGREASEEntry(t *testing.T) {
|
||||
// Chrome sends a GREASE key share whose key_exchange is a single byte.
|
||||
// Rejecting it as malformed breaks Chrome outright.
|
||||
extData := []byte{
|
||||
0x00, 0x0b, // client_shares length 11
|
||||
0x1a, 0x1a, 0x00, 0x01, 0x00, // GREASE group, 1 byte body
|
||||
0x00, 0x1d, 0x00, 0x02, 0xab, 0xcd, // x25519, 2 byte body
|
||||
}
|
||||
type share struct {
|
||||
g tls.NamedGroup
|
||||
n int
|
||||
}
|
||||
var got []share
|
||||
err := tls.ForEachKeyShare(extData, func(g tls.NamedGroup, key []byte) error {
|
||||
got = append(got, share{g, len(key)})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d shares want 2", len(got))
|
||||
}
|
||||
if !tls.IsGREASE(uint16(got[0].g)) || got[0].n != 1 {
|
||||
t.Errorf("GREASE share mishandled: %+v", got[0])
|
||||
}
|
||||
if got[1].g != tls.GroupX25519 || got[1].n != 2 {
|
||||
t.Errorf("x25519 share mishandled: %+v", got[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachALPNProtoRejectsEmptyName(t *testing.T) {
|
||||
// A zero-length protocol name would make the walk unable to advance.
|
||||
extData := []byte{0x00, 0x01, 0x00}
|
||||
err := tls.ForEachALPNProto(extData, func([]byte) error { return nil })
|
||||
if !errors.Is(err, lneto.ErrInvalidLengthField) {
|
||||
t.Errorf("got %v want ErrInvalidLengthField", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachALPNProto(t *testing.T) {
|
||||
extData := []byte{
|
||||
0x00, 0x0c,
|
||||
0x02, 'h', '2',
|
||||
0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
|
||||
}
|
||||
var names []string
|
||||
err := tls.ForEachALPNProto(extData, func(b []byte) error {
|
||||
names = append(names, string(b))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(names) != 2 || names[0] != "h2" || names[1] != "http/1.1" {
|
||||
t.Errorf("got %q", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachSupportedVersionUsesU8Prefix(t *testing.T) {
|
||||
// supported_versions is the one hello list with a single byte prefix.
|
||||
extData := []byte{0x04, 0x1a, 0x1a, 0x03, 0x04}
|
||||
var vers []uint16
|
||||
err := tls.ForEachSupportedVersion(extData, func(v uint16) error {
|
||||
vers = append(vers, v)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(vers) != 2 || vers[1] != tls.VersionTLS13 {
|
||||
t.Errorf("got %#x", vers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVectorRejectsTrailingBytes(t *testing.T) {
|
||||
// A prefix that under-describes its buffer leaves bytes whose meaning this
|
||||
// parser and a middlebox could disagree about.
|
||||
err := tls.ForEachSupportedGroup([]byte{0x00, 0x02, 0x00, 0x1d, 0xff}, func(tls.NamedGroup) error { return nil })
|
||||
if err == nil {
|
||||
t.Error("trailing bytes after vector accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForEachU16RejectsOddLength(t *testing.T) {
|
||||
err := tls.ForEachU16([]byte{0x00, 0x1d, 0x00}, func(uint16) error { return nil })
|
||||
if !errors.Is(err, lneto.ErrInvalidLengthField) {
|
||||
t.Errorf("got %v want ErrInvalidLengthField", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGREASE(t *testing.T) {
|
||||
// The 16 reserved values of RFC 8701.
|
||||
for i := range 16 {
|
||||
@@ -392,27 +250,3 @@ func FuzzNewInnerPlaintext(f *testing.F) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzForEachExtension(f *testing.F) {
|
||||
f.Add([]byte{0x00, 0x2b, 0x00, 0x03, 0x02, 0x03, 0x04})
|
||||
f.Add([]byte{0x0a, 0x0a, 0x00, 0x00})
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
total := 0
|
||||
err := tls.ForEachExtension(b, func(ext tls.ExtensionType, data []byte) error {
|
||||
total += 4 + len(data)
|
||||
if total > len(b) {
|
||||
t.Fatalf("walked %d bytes past input length %d", total, len(b))
|
||||
}
|
||||
// Sub-walkers must also never escape their slice.
|
||||
_ = tls.ForEachKeyShare(data, func(tls.NamedGroup, []byte) error { return nil })
|
||||
_ = tls.ForEachALPNProto(data, func([]byte) error { return nil })
|
||||
_ = tls.ForEachSupportedGroup(data, func(tls.NamedGroup) error { return nil })
|
||||
_ = tls.ForEachSupportedVersion(data, func(uint16) error { return nil })
|
||||
_ = tls.ForEachServerName(data, func(uint8, []byte) error { return nil })
|
||||
return nil
|
||||
})
|
||||
if err == nil && total != len(b) {
|
||||
t.Fatalf("clean walk consumed %d of %d bytes", total, len(b))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+218
-52
@@ -14,9 +14,26 @@ const SizeRandom = 32
|
||||
// compatibility mode and the server must echo it verbatim.
|
||||
const MaxSessionIDLen = 32
|
||||
|
||||
// ClientHelloFrame provides zero-copy access to a ClientHello body
|
||||
// (RFC 8446 4.1.2). The frame wraps the handshake message *body*, that is the
|
||||
// bytes returned by [HandshakeFrame.Body], not the handshake header.
|
||||
// Span is the position and length of a field within a message body.
|
||||
type Span struct {
|
||||
Off int
|
||||
Len int
|
||||
}
|
||||
|
||||
// HelloSpans locates the fields of a ClientHello or ServerHello body. Packet
|
||||
// decoders need a field's wire position, which the slice returning accessors
|
||||
// cannot give.
|
||||
type HelloSpans struct {
|
||||
Random Span
|
||||
SessionID Span
|
||||
CipherSuites Span // ServerHello: the single selected suite.
|
||||
Compression Span
|
||||
Extensions Span
|
||||
}
|
||||
|
||||
// ClientHelloMsg provides zero-copy access to a ClientHello body
|
||||
// (RFC 8446 4.1.2), wrapping the handshake message body returned by
|
||||
// [HandshakeFrame.Body], not the handshake header.
|
||||
//
|
||||
// struct {
|
||||
// ProtocolVersion legacy_version = 0x0303;
|
||||
@@ -27,28 +44,30 @@ const MaxSessionIDLen = 32
|
||||
// Extension extensions<8..2^16-1>;
|
||||
// } ClientHello;
|
||||
//
|
||||
// Every variable-length field is bounds-checked once by
|
||||
// [NewClientHelloFrame], so the accessors cannot slice out of range.
|
||||
type ClientHelloFrame struct {
|
||||
buf []byte
|
||||
// Offsets of each variable-length field's contents, resolved once at
|
||||
// construction so accessors stay branch-free.
|
||||
// Unlike a [RecordFrame] or [HandshakeFrame], whose fields sit at fixed offsets,
|
||||
// a hello's fields are length-prefixed and its extensions may arrive in any
|
||||
// order. [ParseClientHello] resolves every offset once so accessors and
|
||||
// iterators need no bounds checks and cannot fail.
|
||||
type ClientHelloMsg struct {
|
||||
buf []byte
|
||||
exts ExtensionList
|
||||
// Offsets of each variable-length field's contents, resolved at parse time.
|
||||
sessionIDOff, sessionIDLen int
|
||||
suitesOff, suitesLen int
|
||||
compOff, compLen int
|
||||
extsOff, extsLen int
|
||||
}
|
||||
|
||||
// NewClientHelloFrame parses the structure of a ClientHello body, validating
|
||||
// that every length prefix is consistent with the buffer. It performs no
|
||||
// policy checks: version negotiation, cipher suite selection and extension
|
||||
// validation are the handshake state machine's job.
|
||||
// ParseClientHello parses a ClientHello body, validating that every length
|
||||
// prefix is consistent with the buffer, down to the lists inside the extensions
|
||||
// this package recognizes. It performs no policy checks: version negotiation,
|
||||
// cipher suite selection and extension validation are the handshake state
|
||||
// machine's job.
|
||||
//
|
||||
// Trailing bytes after the extensions block are rejected. A sender and parser
|
||||
// that disagree about where a structure ends is the ambiguity that
|
||||
// parser-differential attacks are built on.
|
||||
func NewClientHelloFrame(body []byte) (ClientHelloFrame, error) {
|
||||
var ch ClientHelloFrame
|
||||
func ParseClientHello(body []byte) (ClientHelloMsg, error) {
|
||||
var ch ClientHelloMsg
|
||||
// legacy_version(2) + random(32) + session_id length(1)
|
||||
const fixed = 2 + SizeRandom + 1
|
||||
if len(body) < fixed {
|
||||
@@ -103,20 +122,23 @@ func NewClientHelloFrame(body []byte) (ClientHelloFrame, error) {
|
||||
} else if extsLen != len(body)-off {
|
||||
return ch, errTrailingBytes
|
||||
}
|
||||
ch.extsOff, ch.extsLen = off, extsLen
|
||||
|
||||
exts, err := ParseClientExtensions(body[off:off+extsLen], off)
|
||||
if err != nil {
|
||||
return ClientHelloMsg{}, err
|
||||
}
|
||||
ch.exts = exts
|
||||
ch.buf = body
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// LegacyVersion returns the legacy_version field, which TLS 1.3 pins to
|
||||
// 0x0303 regardless of the version actually negotiated.
|
||||
func (ch ClientHelloFrame) LegacyVersion() uint16 {
|
||||
func (ch ClientHelloMsg) LegacyVersion() uint16 {
|
||||
return binary.BigEndian.Uint16(ch.buf[0:2])
|
||||
}
|
||||
|
||||
// Random returns the 32-byte client_random.
|
||||
func (ch ClientHelloFrame) Random() *[SizeRandom]byte {
|
||||
func (ch ClientHelloMsg) Random() *[SizeRandom]byte {
|
||||
return (*[SizeRandom]byte)(ch.buf[2 : 2+SizeRandom])
|
||||
}
|
||||
|
||||
@@ -124,54 +146,67 @@ func (ch ClientHelloFrame) Random() *[SizeRandom]byte {
|
||||
// server must echo this verbatim in its ServerHello; a non-empty value means
|
||||
// the client is using middlebox compatibility mode and expects a dummy
|
||||
// ChangeCipherSpec record.
|
||||
func (ch ClientHelloFrame) LegacySessionID() []byte {
|
||||
func (ch ClientHelloMsg) LegacySessionID() []byte {
|
||||
return ch.buf[ch.sessionIDOff : ch.sessionIDOff+ch.sessionIDLen]
|
||||
}
|
||||
|
||||
// CipherSuites returns the cipher_suites vector with its length prefix
|
||||
// stripped, ready for [ForEachU16]. The list includes GREASE values.
|
||||
func (ch ClientHelloFrame) CipherSuites() []byte {
|
||||
// CipherSuites iterates the offered suites in wire order, keyed by each suite's
|
||||
// offset within the body. The list includes GREASE values.
|
||||
func (ch ClientHelloMsg) CipherSuites(yield func(off int, suite CipherSuite) bool) {
|
||||
suites := ch.CipherSuiteBytes()
|
||||
for off := 0; off+2 <= len(suites); off += 2 {
|
||||
if !yield(ch.suitesOff+off, CipherSuite(binary.BigEndian.Uint16(suites[off:off+2]))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CipherSuiteBytes returns the cipher_suites vector with its length prefix
|
||||
// stripped.
|
||||
func (ch ClientHelloMsg) CipherSuiteBytes() []byte {
|
||||
return ch.buf[ch.suitesOff : ch.suitesOff+ch.suitesLen]
|
||||
}
|
||||
|
||||
// LegacyCompressionMethods returns the legacy_compression_methods vector
|
||||
// contents. For a TLS 1.3 hello this must be exactly one zero byte; see
|
||||
// [ClientHelloFrame.ValidateCompression].
|
||||
func (ch ClientHelloFrame) LegacyCompressionMethods() []byte {
|
||||
// [ClientHelloMsg.ValidateCompression].
|
||||
func (ch ClientHelloMsg) LegacyCompressionMethods() []byte {
|
||||
return ch.buf[ch.compOff : ch.compOff+ch.compLen]
|
||||
}
|
||||
|
||||
// Extensions returns the extensions block contents with the outer length
|
||||
// prefix stripped, ready for [ForEachExtension].
|
||||
func (ch ClientHelloFrame) Extensions() []byte {
|
||||
return ch.buf[ch.extsOff : ch.extsOff+ch.extsLen]
|
||||
// Extensions iterates this hello's extensions in wire order, keyed by the
|
||||
// offset of each extension's data within the body. Duplicates were rejected at
|
||||
// parse time, so the walk needs no bookkeeping.
|
||||
func (ch ClientHelloMsg) Extensions(yield func(off int, ext ExtensionFrame) bool) {
|
||||
ch.exts.All(yield)
|
||||
}
|
||||
|
||||
// RawData returns the whole ClientHello body.
|
||||
func (ch ClientHelloFrame) RawData() []byte { return ch.buf }
|
||||
// ExtensionList returns the validated extensions block, for passing the block
|
||||
// itself around rather than ranging over it here.
|
||||
func (ch ClientHelloMsg) ExtensionList() ExtensionList { return ch.exts }
|
||||
|
||||
// ForEachExtension walks this hello's extensions, rejecting a repeated known
|
||||
// extension type with [lneto.ErrInvalidField]. RFC 8446 4.2 forbids duplicates,
|
||||
// and tolerating them invites parser-differential attacks in which this parser
|
||||
// and a middlebox act on different copies of the same extension.
|
||||
//
|
||||
// Unknown and GREASE extension types are passed through without duplicate
|
||||
// checking, since they are skipped rather than acted upon. Prefer this over
|
||||
// the bare [ForEachExtension] when parsing an untrusted hello.
|
||||
func (ch ClientHelloFrame) ForEachExtension(fn func(ExtensionType, []byte) error) error {
|
||||
var seen extSeen
|
||||
return ForEachExtension(ch.Extensions(), func(ext ExtensionType, data []byte) error {
|
||||
if seen.mark(ext) {
|
||||
return lneto.ErrInvalidField
|
||||
}
|
||||
return fn(ext, data)
|
||||
})
|
||||
// ExtensionBytes returns the extensions block contents with the outer length
|
||||
// prefix stripped.
|
||||
func (ch ClientHelloMsg) ExtensionBytes() []byte { return ch.exts.Bytes() }
|
||||
|
||||
// RawData returns the whole ClientHello body.
|
||||
func (ch ClientHelloMsg) RawData() []byte { return ch.buf }
|
||||
|
||||
// Spans locates this hello's fields inside [ClientHelloMsg.RawData].
|
||||
func (ch ClientHelloMsg) Spans() HelloSpans {
|
||||
return HelloSpans{
|
||||
Random: Span{Off: 2, Len: SizeRandom},
|
||||
SessionID: Span{Off: ch.sessionIDOff, Len: ch.sessionIDLen},
|
||||
CipherSuites: Span{Off: ch.suitesOff, Len: ch.suitesLen},
|
||||
Compression: Span{Off: ch.compOff, Len: ch.compLen},
|
||||
Extensions: Span{Off: ch.exts.base, Len: len(ch.exts.buf)},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateSize adds an error to v if the ClientHello is structurally invalid.
|
||||
// Since [NewClientHelloFrame] already rejects every inconsistent length, this
|
||||
// only re-checks that the frame was successfully constructed.
|
||||
func (ch ClientHelloFrame) ValidateSize(v *lneto.Validator) {
|
||||
// Since [ParseClientHello] already rejects every inconsistent length, this only
|
||||
// re-checks that the message was successfully parsed.
|
||||
func (ch ClientHelloMsg) ValidateSize(v *lneto.Validator) {
|
||||
if ch.buf == nil {
|
||||
v.AddError(lneto.ErrTruncatedFrame)
|
||||
}
|
||||
@@ -181,10 +216,139 @@ func (ch ClientHelloFrame) ValidateSize(v *lneto.Validator) {
|
||||
// the single null method TLS 1.3 mandates (RFC 8446 4.1.2). Anything else must
|
||||
// be rejected with an illegal_parameter alert: a client offering real
|
||||
// compression methods is either pre-1.3 or attempting a downgrade.
|
||||
func (ch ClientHelloFrame) ValidateCompression() bool {
|
||||
func (ch ClientHelloMsg) ValidateCompression() bool {
|
||||
return ch.compLen == 1 && ch.buf[ch.compOff] == 0
|
||||
}
|
||||
|
||||
// ServerHelloMsg provides zero-copy access to a ServerHello body
|
||||
// (RFC 8446 4.1.3). Like [ClientHelloMsg] it wraps the handshake message body.
|
||||
//
|
||||
// struct {
|
||||
// ProtocolVersion legacy_version = 0x0303;
|
||||
// Random random; // 32 bytes
|
||||
// opaque legacy_session_id_echo<0..32>;
|
||||
// CipherSuite cipher_suite; // 2 bytes
|
||||
// uint8 legacy_compression_method = 0;
|
||||
// Extension extensions<6..2^16-1>;
|
||||
// } ServerHello;
|
||||
//
|
||||
// A HelloRetryRequest has this same structure; it is told apart by its random
|
||||
// being the special value of RFC 8446 4.1.3, which is policy, not framing.
|
||||
type ServerHelloMsg struct {
|
||||
buf []byte
|
||||
exts ExtensionList
|
||||
sessionIDOff, sessionIDLen int
|
||||
suiteOff int
|
||||
}
|
||||
|
||||
// ParseServerHello parses a ServerHello body. Like [ParseClientHello] it
|
||||
// validates framing only, rejects trailing bytes, and validates the lists
|
||||
// inside recognized extensions, in their server forms.
|
||||
func ParseServerHello(body []byte) (ServerHelloMsg, error) {
|
||||
var sh ServerHelloMsg
|
||||
const fixed = 2 + SizeRandom + 1
|
||||
if len(body) < fixed {
|
||||
return sh, lneto.ErrTruncatedFrame
|
||||
}
|
||||
off := 2 + SizeRandom
|
||||
|
||||
sidLen := int(body[off])
|
||||
off++
|
||||
if sidLen > MaxSessionIDLen {
|
||||
return sh, lneto.ErrInvalidLengthField
|
||||
} else if sidLen > len(body)-off {
|
||||
return sh, lneto.ErrTruncatedFrame
|
||||
}
|
||||
sh.sessionIDOff, sh.sessionIDLen = off, sidLen
|
||||
off += sidLen
|
||||
|
||||
// cipher_suite(2) + legacy_compression_method(1)
|
||||
if len(body)-off < 3 {
|
||||
return sh, lneto.ErrTruncatedFrame
|
||||
}
|
||||
sh.suiteOff = off
|
||||
off += 3
|
||||
|
||||
if len(body)-off < 2 {
|
||||
return sh, lneto.ErrTruncatedFrame
|
||||
}
|
||||
extsLen := int(binary.BigEndian.Uint16(body[off : off+2]))
|
||||
off += 2
|
||||
if extsLen > len(body)-off {
|
||||
return sh, lneto.ErrTruncatedFrame
|
||||
} else if extsLen != len(body)-off {
|
||||
return sh, errTrailingBytes
|
||||
}
|
||||
exts, err := ParseServerExtensions(body[off:off+extsLen], off)
|
||||
if err != nil {
|
||||
return ServerHelloMsg{}, err
|
||||
}
|
||||
sh.exts = exts
|
||||
sh.buf = body
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
// LegacyVersion returns the legacy_version field, pinned to 0x0303 by TLS 1.3.
|
||||
func (sh ServerHelloMsg) LegacyVersion() uint16 {
|
||||
return binary.BigEndian.Uint16(sh.buf[0:2])
|
||||
}
|
||||
|
||||
// Random returns the 32-byte server_random.
|
||||
func (sh ServerHelloMsg) Random() *[SizeRandom]byte {
|
||||
return (*[SizeRandom]byte)(sh.buf[2 : 2+SizeRandom])
|
||||
}
|
||||
|
||||
// LegacySessionIDEcho returns the client's legacy_session_id as echoed back. A
|
||||
// client in middlebox compatibility mode requires it to match what it sent.
|
||||
func (sh ServerHelloMsg) LegacySessionIDEcho() []byte {
|
||||
return sh.buf[sh.sessionIDOff : sh.sessionIDOff+sh.sessionIDLen]
|
||||
}
|
||||
|
||||
// CipherSuite returns the selected cipher suite.
|
||||
func (sh ServerHelloMsg) CipherSuite() CipherSuite {
|
||||
return CipherSuite(binary.BigEndian.Uint16(sh.buf[sh.suiteOff : sh.suiteOff+2]))
|
||||
}
|
||||
|
||||
// LegacyCompressionMethod returns the legacy_compression_method, which TLS 1.3
|
||||
// requires to be zero.
|
||||
func (sh ServerHelloMsg) LegacyCompressionMethod() uint8 {
|
||||
return sh.buf[sh.suiteOff+2]
|
||||
}
|
||||
|
||||
// Extensions iterates this hello's extensions in wire order, keyed by the
|
||||
// offset of each extension's data within the body.
|
||||
func (sh ServerHelloMsg) Extensions(yield func(off int, ext ExtensionFrame) bool) {
|
||||
sh.exts.All(yield)
|
||||
}
|
||||
|
||||
// ExtensionList returns the validated extensions block.
|
||||
func (sh ServerHelloMsg) ExtensionList() ExtensionList { return sh.exts }
|
||||
|
||||
// ExtensionBytes returns the extensions block contents with the outer length
|
||||
// prefix stripped.
|
||||
func (sh ServerHelloMsg) ExtensionBytes() []byte { return sh.exts.Bytes() }
|
||||
|
||||
// RawData returns the whole ServerHello body.
|
||||
func (sh ServerHelloMsg) RawData() []byte { return sh.buf }
|
||||
|
||||
// Spans locates this hello's fields inside [ServerHelloMsg.RawData].
|
||||
func (sh ServerHelloMsg) Spans() HelloSpans {
|
||||
return HelloSpans{
|
||||
Random: Span{Off: 2, Len: SizeRandom},
|
||||
SessionID: Span{Off: sh.sessionIDOff, Len: sh.sessionIDLen},
|
||||
CipherSuites: Span{Off: sh.suiteOff, Len: 2},
|
||||
Compression: Span{Off: sh.suiteOff + 2, Len: 1},
|
||||
Extensions: Span{Off: sh.exts.base, Len: len(sh.exts.buf)},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateSize adds an error to v if the ServerHello was not parsed.
|
||||
func (sh ServerHelloMsg) ValidateSize(v *lneto.Validator) {
|
||||
if sh.buf == nil {
|
||||
v.AddError(lneto.ErrTruncatedFrame)
|
||||
}
|
||||
}
|
||||
|
||||
// extSeen tracks which known extension types have already been encountered in
|
||||
// a single hello. RFC 8446 4.2 forbids a duplicate extension type, and
|
||||
// tolerating duplicates invites parser-differential attacks where this parser
|
||||
@@ -259,6 +423,8 @@ func extBit(ext ExtensionType) (extSeen, bool) {
|
||||
i = 21
|
||||
case ExtRenegotiationInfo:
|
||||
i = 22
|
||||
case ExtECPointFormats:
|
||||
i = 23
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
|
||||
+50
-84
@@ -18,7 +18,7 @@ import (
|
||||
// exercised against genuine extension ordering, a 32-byte compatibility
|
||||
// session ID and GREASE-free but otherwise realistic content. Browser captures
|
||||
// arrive at Stage 6; this covers the structure in the meantime.
|
||||
func captureClientHello(t *testing.T) ltls.ClientHelloFrame {
|
||||
func captureClientHello(t *testing.T) ltls.ClientHelloMsg {
|
||||
t.Helper()
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
@@ -61,7 +61,7 @@ func captureClientHello(t *testing.T) ltls.ClientHelloFrame {
|
||||
if !hs.Complete() {
|
||||
t.Fatal("ClientHello spans multiple records")
|
||||
}
|
||||
ch, err := ltls.NewClientHelloFrame(hs.Body())
|
||||
ch, err := ltls.ParseClientHello(hs.Body())
|
||||
if err != nil {
|
||||
t.Fatalf("client hello: %v", err)
|
||||
}
|
||||
@@ -85,49 +85,29 @@ func TestClientHelloParseRealHello(t *testing.T) {
|
||||
}
|
||||
|
||||
var sawTLS13, sawX25519, sawSNI, sawALPN bool
|
||||
var suites []ltls.CipherSuite
|
||||
err := ch.ForEachExtension(func(ext ltls.ExtensionType, data []byte) error {
|
||||
switch ext {
|
||||
for _, ext := range ch.Extensions {
|
||||
switch ext.Type() {
|
||||
case ltls.ExtSupportedVersions:
|
||||
return ltls.ForEachSupportedVersion(data, func(v uint16) error {
|
||||
if v == ltls.VersionTLS13 {
|
||||
sawTLS13 = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, v := range ext.SupportedVersions {
|
||||
sawTLS13 = sawTLS13 || v == ltls.VersionTLS13
|
||||
}
|
||||
case ltls.ExtKeyShare:
|
||||
return ltls.ForEachKeyShare(data, func(g ltls.NamedGroup, key []byte) error {
|
||||
if g == ltls.GroupX25519 && len(key) == 32 {
|
||||
sawX25519 = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, ks := range ext.KeyShares {
|
||||
sawX25519 = sawX25519 || ks.Group == ltls.GroupX25519 && len(ks.Key) == 32
|
||||
}
|
||||
case ltls.ExtServerName:
|
||||
return ltls.ForEachServerName(data, func(nameType uint8, name []byte) error {
|
||||
if nameType == 0 && string(name) == "example.com" {
|
||||
sawSNI = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, name := range ext.ServerNames {
|
||||
sawSNI = sawSNI || name.Type == 0 && string(name.Name) == "example.com"
|
||||
}
|
||||
case ltls.ExtALPN:
|
||||
return ltls.ForEachALPNProto(data, func(p []byte) error {
|
||||
if string(p) == "http/1.1" {
|
||||
sawALPN = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, p := range ext.ALPNProtos {
|
||||
sawALPN = sawALPN || string(p) == "http/1.1"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walking extensions: %v", err)
|
||||
}
|
||||
|
||||
if err := ltls.ForEachU16(ch.CipherSuites(), func(v uint16) error {
|
||||
suites = append(suites, ltls.CipherSuite(v))
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("walking cipher suites: %v", err)
|
||||
var suites []ltls.CipherSuite
|
||||
for _, s := range ch.CipherSuites {
|
||||
suites = append(suites, s)
|
||||
}
|
||||
|
||||
if !sawTLS13 {
|
||||
@@ -153,43 +133,9 @@ func TestClientHelloParseRealHello(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientHelloRejectsDuplicateExtension(t *testing.T) {
|
||||
// Duplicating supported_versions must be caught. Tolerating it lets this
|
||||
// parser and a middlebox act on different copies.
|
||||
ch := captureClientHello(t)
|
||||
exts := ch.Extensions()
|
||||
|
||||
// Find the first extension and append a verbatim copy of it.
|
||||
var first []byte
|
||||
err := ltls.ForEachExtension(exts, func(ext ltls.ExtensionType, data []byte) error {
|
||||
if first == nil {
|
||||
first = make([]byte, 4+len(data))
|
||||
copy(first, exts)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil || first == nil {
|
||||
t.Fatalf("could not isolate first extension: %v", err)
|
||||
}
|
||||
|
||||
dup := make([]byte, 0, len(exts)+len(first))
|
||||
dup = append(dup, exts...)
|
||||
dup = append(dup, first...)
|
||||
|
||||
body := rebuildHelloWithExtensions(t, ch, dup)
|
||||
ch2, err := ltls.NewClientHelloFrame(body)
|
||||
if err != nil {
|
||||
t.Fatalf("rebuilt hello did not parse: %v", err)
|
||||
}
|
||||
err = ch2.ForEachExtension(func(ltls.ExtensionType, []byte) error { return nil })
|
||||
if !errors.Is(err, lneto.ErrInvalidField) {
|
||||
t.Errorf("duplicate extension got %v, want ErrInvalidField", err)
|
||||
}
|
||||
}
|
||||
|
||||
// rebuildHelloWithExtensions re-encodes ch with a replacement extensions block,
|
||||
// exercising Builder against a structure produced by a real client.
|
||||
func rebuildHelloWithExtensions(t *testing.T, ch ltls.ClientHelloFrame, exts []byte) []byte {
|
||||
func rebuildHelloWithExtensions(t *testing.T, ch ltls.ClientHelloMsg, exts []byte) []byte {
|
||||
t.Helper()
|
||||
var b ltls.Builder
|
||||
b.Reset(make([]byte, 0, len(ch.RawData())+len(exts)+64))
|
||||
@@ -199,7 +145,7 @@ func rebuildHelloWithExtensions(t *testing.T, ch ltls.ClientHelloFrame, exts []b
|
||||
b.AddBytes(ch.LegacySessionID())
|
||||
b.Close()
|
||||
b.OpenU16()
|
||||
b.AddBytes(ch.CipherSuites())
|
||||
b.AddBytes(ch.CipherSuiteBytes())
|
||||
b.Close()
|
||||
b.OpenU8()
|
||||
b.AddBytes(ch.LegacyCompressionMethods())
|
||||
@@ -225,7 +171,7 @@ func TestClientHelloRejectsOversizeSessionID(t *testing.T) {
|
||||
body = append(body, 0x00, 0x02, 0x13, 0x01) // cipher suites
|
||||
body = append(body, 0x01, 0x00) // compression
|
||||
body = append(body, 0x00, 0x00) // extensions, empty
|
||||
_, err := ltls.NewClientHelloFrame(body)
|
||||
_, err := ltls.ParseClientHello(body)
|
||||
if !errors.Is(err, lneto.ErrInvalidLengthField) {
|
||||
t.Errorf("got %v want ErrInvalidLengthField", err)
|
||||
}
|
||||
@@ -234,7 +180,7 @@ func TestClientHelloRejectsOversizeSessionID(t *testing.T) {
|
||||
func TestClientHelloRejectsTrailingBytes(t *testing.T) {
|
||||
ch := captureClientHello(t)
|
||||
body := append(append([]byte{}, ch.RawData()...), 0xff)
|
||||
if _, err := ltls.NewClientHelloFrame(body); err == nil {
|
||||
if _, err := ltls.ParseClientHello(body); err == nil {
|
||||
t.Error("trailing byte after extensions block accepted")
|
||||
}
|
||||
}
|
||||
@@ -245,11 +191,11 @@ func TestClientHelloTruncatedAtEveryOffset(t *testing.T) {
|
||||
ch := captureClientHello(t)
|
||||
full := ch.RawData()
|
||||
for n := range len(full) {
|
||||
frame, err := ltls.NewClientHelloFrame(full[:n])
|
||||
msg, err := ltls.ParseClientHello(full[:n])
|
||||
if err == nil {
|
||||
// A shorter prefix must never parse as a complete hello.
|
||||
t.Errorf("truncation to %d/%d bytes parsed clean", n, len(full))
|
||||
_ = frame.Extensions()
|
||||
_ = msg.ExtensionBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +211,7 @@ func FuzzNewClientHelloFrame(f *testing.F) {
|
||||
0x00, 0x00, // no extensions
|
||||
})
|
||||
f.Fuzz(func(t *testing.T, b []byte) {
|
||||
ch, err := ltls.NewClientHelloFrame(b)
|
||||
ch, err := ltls.ParseClientHello(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -273,17 +219,37 @@ func FuzzNewClientHelloFrame(f *testing.F) {
|
||||
if len(ch.LegacySessionID()) > ltls.MaxSessionIDLen {
|
||||
t.Fatalf("session id %d bytes exceeds max", len(ch.LegacySessionID()))
|
||||
}
|
||||
if len(ch.CipherSuites())%2 != 0 {
|
||||
if len(ch.CipherSuiteBytes())%2 != 0 {
|
||||
t.Fatal("cipher suites vector has odd length")
|
||||
}
|
||||
total := 2 + ltls.SizeRandom + 1 + len(ch.LegacySessionID()) +
|
||||
2 + len(ch.CipherSuites()) +
|
||||
2 + len(ch.CipherSuiteBytes()) +
|
||||
1 + len(ch.LegacyCompressionMethods()) +
|
||||
2 + len(ch.Extensions())
|
||||
2 + len(ch.ExtensionBytes())
|
||||
if total != len(b) {
|
||||
t.Fatalf("fields sum to %d but input is %d bytes", total, len(b))
|
||||
}
|
||||
_ = ch.ValidateCompression()
|
||||
_ = ch.ForEachExtension(func(ltls.ExtensionType, []byte) error { return nil })
|
||||
// Every nested iterator must stay inside the input too.
|
||||
for _, ext := range ch.Extensions {
|
||||
for _, ks := range ext.KeyShares {
|
||||
_ = ks
|
||||
}
|
||||
for _, name := range ext.ServerNames {
|
||||
_ = name
|
||||
}
|
||||
for _, p := range ext.ALPNProtos {
|
||||
_ = p
|
||||
}
|
||||
for _, v := range ext.SupportedVersions {
|
||||
_ = v
|
||||
}
|
||||
for _, g := range ext.SupportedGroups {
|
||||
_ = g
|
||||
}
|
||||
for _, s := range ext.SignatureSchemes {
|
||||
_ = s
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package tls_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
ltls "github.com/soypat/lneto/x/tls"
|
||||
)
|
||||
|
||||
// locates reports whether span points at want inside body.
|
||||
func locates(body []byte, off int, want []byte) bool {
|
||||
return off >= 0 && off+len(want) <= len(body) && bytes.Equal(body[off:off+len(want)], want)
|
||||
}
|
||||
|
||||
// Spans must locate every hello field, so decoders can map a field onto its wire
|
||||
// position without re-walking the structure.
|
||||
func TestClientHelloMsgSpans(t *testing.T) {
|
||||
msg := captureClientHello(t)
|
||||
body := msg.RawData()
|
||||
sp := msg.Spans()
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
span ltls.Span
|
||||
want []byte
|
||||
}{
|
||||
{"random", sp.Random, msg.Random()[:]},
|
||||
{"session id", sp.SessionID, msg.LegacySessionID()},
|
||||
{"compression", sp.Compression, msg.LegacyCompressionMethods()},
|
||||
{"extensions", sp.Extensions, msg.ExtensionBytes()},
|
||||
} {
|
||||
if tc.span.Len != len(tc.want) {
|
||||
t.Errorf("%s span len %d want %d", tc.name, tc.span.Len, len(tc.want))
|
||||
} else if !locates(body, tc.span.Off, tc.want) {
|
||||
t.Errorf("%s span %+v does not locate its field", tc.name, tc.span)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterator keys are offsets into the hello body, at every nesting level, so a
|
||||
// decoder can point at a cipher suite or an SNI hostname without arithmetic.
|
||||
func TestClientHelloMsgIterators(t *testing.T) {
|
||||
msg := captureClientHello(t)
|
||||
body := msg.RawData()
|
||||
|
||||
nsuites := 0
|
||||
for off, suite := range msg.CipherSuites {
|
||||
want := []byte{byte(suite >> 8), byte(suite)}
|
||||
if !locates(body, off, want) {
|
||||
t.Errorf("suite %v at offset %d does not match the wire", suite, off)
|
||||
}
|
||||
nsuites++
|
||||
}
|
||||
if nsuites == 0 {
|
||||
t.Fatal("no cipher suites walked")
|
||||
}
|
||||
|
||||
var sawSNI, sawALPN, sawTLS13, sawX25519 bool
|
||||
for off, ext := range msg.Extensions {
|
||||
if !locates(body, off, ext.Data()) {
|
||||
t.Errorf("%v data at offset %d does not match the wire", ext.Type(), off)
|
||||
}
|
||||
switch ext.Type() {
|
||||
case ltls.ExtServerName:
|
||||
for noff, name := range ext.ServerNames {
|
||||
sawSNI = true
|
||||
if name.Type != 0 || string(name.Name) != "example.com" {
|
||||
t.Errorf("server name %d %q", name.Type, name.Name)
|
||||
}
|
||||
if !locates(body, noff, name.Name) {
|
||||
t.Errorf("server name at offset %d does not match the wire", noff)
|
||||
}
|
||||
}
|
||||
case ltls.ExtALPN:
|
||||
for poff, proto := range ext.ALPNProtos {
|
||||
sawALPN = true
|
||||
if !locates(body, poff, proto) {
|
||||
t.Errorf("alpn %q at offset %d does not match the wire", proto, poff)
|
||||
}
|
||||
}
|
||||
case ltls.ExtSupportedVersions:
|
||||
for _, v := range ext.SupportedVersions {
|
||||
sawTLS13 = sawTLS13 || v == ltls.VersionTLS13
|
||||
}
|
||||
case ltls.ExtKeyShare:
|
||||
for koff, ks := range ext.KeyShares {
|
||||
if ks.Group == ltls.GroupX25519 {
|
||||
sawX25519 = true
|
||||
}
|
||||
if !locates(body, koff, ks.Key) {
|
||||
t.Errorf("key share %v at offset %d does not match the wire", ks.Group, koff)
|
||||
}
|
||||
}
|
||||
case ltls.ExtSupportedGroups:
|
||||
for goff, g := range ext.SupportedGroups {
|
||||
if !locates(body, goff, []byte{byte(g >> 8), byte(g)}) {
|
||||
t.Errorf("group %v at offset %d does not match the wire", g, goff)
|
||||
}
|
||||
}
|
||||
case ltls.ExtSignatureAlgorithms:
|
||||
for soff, s := range ext.SignatureSchemes {
|
||||
if !locates(body, soff, []byte{byte(s >> 8), byte(s)}) {
|
||||
t.Errorf("scheme %v at offset %d does not match the wire", s, soff)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sawSNI || !sawALPN || !sawTLS13 || !sawX25519 {
|
||||
t.Errorf("sni=%v alpn=%v tls13=%v x25519=%v", sawSNI, sawALPN, sawTLS13, sawX25519)
|
||||
}
|
||||
}
|
||||
|
||||
// A sub-iterator reached from the wrong extension yields nothing rather than
|
||||
// reinterpreting unrelated bytes.
|
||||
func TestExtensionSubIteratorTypeMismatch(t *testing.T) {
|
||||
msg := captureClientHello(t)
|
||||
for _, ext := range msg.Extensions {
|
||||
if ext.Type() != ltls.ExtServerName {
|
||||
continue
|
||||
}
|
||||
for range ext.KeyShares {
|
||||
t.Error("KeyShares walked a server_name extension")
|
||||
}
|
||||
for range ext.SupportedVersions {
|
||||
t.Error("SupportedVersions walked a server_name extension")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Walking a hello must not allocate: every iterator is a value type over the
|
||||
// caller's buffer.
|
||||
func TestClientHelloMsgZeroAlloc(t *testing.T) {
|
||||
msg := captureClientHello(t)
|
||||
body := msg.RawData()
|
||||
n := testing.AllocsPerRun(10, func() {
|
||||
m, err := ltls.ParseClientHello(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, suite := range m.CipherSuites {
|
||||
_ = suite
|
||||
}
|
||||
for _, ext := range m.Extensions {
|
||||
for _, name := range ext.ServerNames {
|
||||
_ = name
|
||||
}
|
||||
for _, ks := range ext.KeyShares {
|
||||
_ = ks
|
||||
}
|
||||
for _, p := range ext.ALPNProtos {
|
||||
_ = p
|
||||
}
|
||||
for _, v := range ext.SupportedVersions {
|
||||
_ = v
|
||||
}
|
||||
}
|
||||
})
|
||||
if n != 0 {
|
||||
t.Errorf("parse and walk allocated %v times, want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
// A known extension whose inner framing is broken must fail the parse, since
|
||||
// every iterator past construction is error-free.
|
||||
func TestParseClientHelloRejectsMalformedKnownExtension(t *testing.T) {
|
||||
msg := captureClientHello(t)
|
||||
// server_name with a host name length one past the extension data.
|
||||
bad := []byte{
|
||||
0x00, 0x00, 0x00, 0x0b, // server_name, 11 bytes
|
||||
0x00, 0x09, // server_name_list length
|
||||
0x00, // host_name
|
||||
0x00, 0x0a, // name length 10, but only 6 bytes follow
|
||||
'e', 'x', 'a', 'm', 'p', 'l',
|
||||
}
|
||||
body := rebuildHelloWithExtensions(t, msg, bad)
|
||||
if _, err := ltls.ParseClientHello(body); err == nil {
|
||||
t.Error("malformed server_name accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseClientHelloRejectsDuplicateExtension(t *testing.T) {
|
||||
// Duplicating an extension lets this parser and a middlebox act on
|
||||
// different copies, so it is rejected at parse time.
|
||||
msg := captureClientHello(t)
|
||||
exts := msg.ExtensionBytes()
|
||||
var first int
|
||||
for off, ext := range msg.Extensions {
|
||||
first = off + len(ext.Data())
|
||||
break
|
||||
}
|
||||
dup := make([]byte, 0, len(exts)+first)
|
||||
dup = append(dup, exts...)
|
||||
dup = append(dup, exts[:first-msg.Spans().Extensions.Off]...)
|
||||
|
||||
body := rebuildHelloWithExtensions(t, msg, dup)
|
||||
_, err := ltls.ParseClientHello(body)
|
||||
if !errors.Is(err, lneto.ErrInvalidField) {
|
||||
t.Errorf("duplicate extension got %v want ErrInvalidField", err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildServerHello encodes a ServerHello body with supported_versions and a
|
||||
// key_share, which in the server form is a single entry with no list prefix.
|
||||
func buildServerHello(t *testing.T, sid []byte) []byte {
|
||||
t.Helper()
|
||||
var b ltls.Builder
|
||||
b.Reset(make([]byte, 0, 256))
|
||||
b.AddU16(ltls.VersionTLS12)
|
||||
for range ltls.SizeRandom {
|
||||
b.AddU8(0xab)
|
||||
}
|
||||
b.OpenU8()
|
||||
b.AddBytes(sid)
|
||||
b.Close()
|
||||
b.AddU16(uint16(ltls.SuiteAES128GCMSHA256))
|
||||
b.AddU8(0) // legacy_compression_method
|
||||
b.OpenU16()
|
||||
b.AddU16(uint16(ltls.ExtSupportedVersions))
|
||||
b.OpenU16()
|
||||
b.AddU16(ltls.VersionTLS13)
|
||||
b.Close()
|
||||
b.AddU16(uint16(ltls.ExtKeyShare))
|
||||
b.OpenU16()
|
||||
b.AddU16(uint16(ltls.GroupX25519))
|
||||
b.OpenU16()
|
||||
for range 32 {
|
||||
b.AddU8(0xee)
|
||||
}
|
||||
b.Close()
|
||||
b.Close()
|
||||
b.Close()
|
||||
body, err := b.Bytes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func TestServerHelloMsg(t *testing.T) {
|
||||
sid := bytes.Repeat([]byte{0xcd}, 32)
|
||||
body := buildServerHello(t, sid)
|
||||
msg, err := ltls.ParseServerHello(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.LegacyVersion() != ltls.VersionTLS12 {
|
||||
t.Errorf("legacy_version %#04x want 0x0303", msg.LegacyVersion())
|
||||
}
|
||||
if !bytes.Equal(msg.LegacySessionIDEcho(), sid) {
|
||||
t.Errorf("session id echo % x want % x", msg.LegacySessionIDEcho(), sid)
|
||||
}
|
||||
if msg.CipherSuite() != ltls.SuiteAES128GCMSHA256 {
|
||||
t.Errorf("cipher suite %v want TLS_AES_128_GCM_SHA256", msg.CipherSuite())
|
||||
}
|
||||
if msg.LegacyCompressionMethod() != 0 {
|
||||
t.Errorf("compression method %d want 0", msg.LegacyCompressionMethod())
|
||||
}
|
||||
|
||||
var version uint16
|
||||
shares := 0
|
||||
for _, ext := range msg.Extensions {
|
||||
switch ext.Type() {
|
||||
case ltls.ExtSupportedVersions:
|
||||
// The ServerHello form is a bare uint16, not a list.
|
||||
if len(ext.Data()) != 2 {
|
||||
t.Fatalf("supported_versions %d bytes want 2", len(ext.Data()))
|
||||
}
|
||||
version = uint16(ext.Data()[0])<<8 | uint16(ext.Data()[1])
|
||||
case ltls.ExtKeyShare:
|
||||
for koff, ks := range ext.KeyShares {
|
||||
shares++
|
||||
if ks.Group != ltls.GroupX25519 || len(ks.Key) != 32 {
|
||||
t.Errorf("key share %v %d bytes", ks.Group, len(ks.Key))
|
||||
}
|
||||
if !locates(body, koff, ks.Key) {
|
||||
t.Errorf("key share at offset %d does not match the wire", koff)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if version != ltls.VersionTLS13 {
|
||||
t.Errorf("negotiated version %#04x want 0x0304", version)
|
||||
}
|
||||
if shares != 1 {
|
||||
t.Errorf("walked %d key shares want 1, the server sends a single entry", shares)
|
||||
}
|
||||
|
||||
sp := msg.Spans()
|
||||
if sp.CipherSuites.Len != 2 || !locates(body, sp.CipherSuites.Off, body[sp.CipherSuites.Off:sp.CipherSuites.Off+2]) {
|
||||
t.Errorf("cipher suite span %+v", sp.CipherSuites)
|
||||
}
|
||||
if sp.Compression.Len != 1 {
|
||||
t.Errorf("compression span len %d want 1", sp.Compression.Len)
|
||||
}
|
||||
if !locates(body, sp.Extensions.Off, msg.ExtensionBytes()) {
|
||||
t.Errorf("extensions span %+v does not locate the block", sp.Extensions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseServerHelloRejectsMalformed(t *testing.T) {
|
||||
body := buildServerHello(t, bytes.Repeat([]byte{0xcd}, 32))
|
||||
if _, err := ltls.ParseServerHello(append(append([]byte{}, body...), 0xff)); err == nil {
|
||||
t.Error("trailing byte after extensions block accepted")
|
||||
}
|
||||
// No truncation may parse clean, panic, or leave an accessor out of range.
|
||||
for n := range len(body) {
|
||||
msg, err := ltls.ParseServerHello(body[:n])
|
||||
if err == nil {
|
||||
t.Errorf("truncation to %d/%d bytes parsed clean", n, len(body))
|
||||
_ = msg.ExtensionBytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user