Files
lneto/x/tls/extension.go
T
2026-08-08 23:17:27 -07:00

459 lines
13 KiB
Go

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
}