mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 16:49:37 +00:00
use top level method signature for iterators
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user