mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 16:49:37 +00:00
clanker tls
This commit is contained in:
+266
@@ -0,0 +1,266 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
)
|
||||
|
||||
// SizeRandom is the length of the client_random and server_random fields.
|
||||
const SizeRandom = 32
|
||||
|
||||
// MaxSessionIDLen is the largest legal legacy_session_id. TLS 1.3 does not use
|
||||
// session IDs, but a client sends a 32-byte one to trigger middlebox
|
||||
// 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.
|
||||
//
|
||||
// struct {
|
||||
// ProtocolVersion legacy_version = 0x0303;
|
||||
// Random random; // 32 bytes
|
||||
// opaque legacy_session_id<0..32>;
|
||||
// CipherSuite cipher_suites<2..2^16-2>;
|
||||
// opaque legacy_compression_methods<1..2^8-1>;
|
||||
// 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.
|
||||
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.
|
||||
//
|
||||
// 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
|
||||
// legacy_version(2) + random(32) + session_id length(1)
|
||||
const fixed = 2 + SizeRandom + 1
|
||||
if len(body) < fixed {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
}
|
||||
off := 2 + SizeRandom
|
||||
|
||||
sidLen := int(body[off])
|
||||
off++
|
||||
if sidLen > MaxSessionIDLen {
|
||||
// Bounds-checked before it can reach a fixed [32]byte echo buffer.
|
||||
return ch, lneto.ErrInvalidLengthField
|
||||
} else if sidLen > len(body)-off {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
}
|
||||
ch.sessionIDOff, ch.sessionIDLen = off, sidLen
|
||||
off += sidLen
|
||||
|
||||
if len(body)-off < 2 {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
}
|
||||
suitesLen := int(binary.BigEndian.Uint16(body[off : off+2]))
|
||||
off += 2
|
||||
if suitesLen > len(body)-off {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
} else if suitesLen%2 != 0 || suitesLen == 0 {
|
||||
return ch, lneto.ErrInvalidLengthField
|
||||
}
|
||||
ch.suitesOff, ch.suitesLen = off, suitesLen
|
||||
off += suitesLen
|
||||
|
||||
if len(body)-off < 1 {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
}
|
||||
compLen := int(body[off])
|
||||
off++
|
||||
if compLen > len(body)-off {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
}
|
||||
ch.compOff, ch.compLen = off, compLen
|
||||
off += compLen
|
||||
|
||||
// TLS 1.3 requires extensions; a ClientHello without them cannot possibly
|
||||
// carry supported_versions and so cannot be a 1.3 hello.
|
||||
if len(body)-off < 2 {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
}
|
||||
extsLen := int(binary.BigEndian.Uint16(body[off : off+2]))
|
||||
off += 2
|
||||
if extsLen > len(body)-off {
|
||||
return ch, lneto.ErrTruncatedFrame
|
||||
} else if extsLen != len(body)-off {
|
||||
return ch, errTrailingBytes
|
||||
}
|
||||
ch.extsOff, ch.extsLen = off, extsLen
|
||||
|
||||
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 {
|
||||
return binary.BigEndian.Uint16(ch.buf[0:2])
|
||||
}
|
||||
|
||||
// Random returns the 32-byte client_random.
|
||||
func (ch ClientHelloFrame) Random() *[SizeRandom]byte {
|
||||
return (*[SizeRandom]byte)(ch.buf[2 : 2+SizeRandom])
|
||||
}
|
||||
|
||||
// LegacySessionID returns the legacy_session_id, at most 32 bytes. A TLS 1.3
|
||||
// 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 {
|
||||
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 {
|
||||
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 {
|
||||
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]
|
||||
}
|
||||
|
||||
// RawData returns the whole ClientHello body.
|
||||
func (ch ClientHelloFrame) RawData() []byte { return ch.buf }
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if ch.buf == nil {
|
||||
v.AddError(lneto.ErrTruncatedFrame)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateCompression reports whether legacy_compression_methods is exactly
|
||||
// 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 {
|
||||
return ch.compLen == 1 && ch.buf[ch.compOff] == 0
|
||||
}
|
||||
|
||||
// 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
|
||||
// and a middlebox act on different copies.
|
||||
//
|
||||
// Only known types are tracked; unknown and GREASE types are exempt because
|
||||
// they are skipped without being acted upon.
|
||||
type extSeen uint64
|
||||
|
||||
// mark records ext and reports whether it had already been seen. Extension
|
||||
// types with no assigned bit are never reported as duplicates.
|
||||
func (s *extSeen) mark(ext ExtensionType) (duplicate bool) {
|
||||
bit, ok := extBit(ext)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if *s&bit != 0 {
|
||||
return true
|
||||
}
|
||||
*s |= bit
|
||||
return false
|
||||
}
|
||||
|
||||
// extBit maps an extension type to its bit in [extSeen]. The mapping covers
|
||||
// every extension this server reads or must reject a duplicate of.
|
||||
func extBit(ext ExtensionType) (extSeen, bool) {
|
||||
var i uint
|
||||
switch ext {
|
||||
case ExtServerName:
|
||||
i = 0
|
||||
case ExtMaxFragmentLength:
|
||||
i = 1
|
||||
case ExtStatusRequest:
|
||||
i = 2
|
||||
case ExtSupportedGroups:
|
||||
i = 3
|
||||
case ExtSignatureAlgorithms:
|
||||
i = 4
|
||||
case ExtALPN:
|
||||
i = 5
|
||||
case ExtSignedCertificateTimestamp:
|
||||
i = 6
|
||||
case ExtPadding:
|
||||
i = 7
|
||||
case ExtExtendedMasterSecret:
|
||||
i = 8
|
||||
case ExtCompressCertificate:
|
||||
i = 9
|
||||
case ExtRecordSizeLimit:
|
||||
i = 10
|
||||
case ExtSessionTicket:
|
||||
i = 11
|
||||
case ExtPreSharedKey:
|
||||
i = 12
|
||||
case ExtEarlyData:
|
||||
i = 13
|
||||
case ExtSupportedVersions:
|
||||
i = 14
|
||||
case ExtCookie:
|
||||
i = 15
|
||||
case ExtPSKKeyExchangeModes:
|
||||
i = 16
|
||||
case ExtCertificateAuthorities:
|
||||
i = 17
|
||||
case ExtSignatureAlgorithmsCert:
|
||||
i = 18
|
||||
case ExtKeyShare:
|
||||
i = 19
|
||||
case ExtApplicationSettings:
|
||||
i = 20
|
||||
case ExtEncryptedClientHello:
|
||||
i = 21
|
||||
case ExtRenegotiationInfo:
|
||||
i = 22
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
return 1 << i, true
|
||||
}
|
||||
Reference in New Issue
Block a user