1 Commits

Author SHA1 Message Date
Patricio Whittingslow 9758d48696 clanker tls 2026-07-24 20:56:52 -03:00
9 changed files with 2192 additions and 31 deletions
-3
View File
@@ -44,9 +44,6 @@ type Frame struct {
buf []byte
}
// RawData returns the underlying frame buffer.
func (frm Frame) RawData() []byte { return frm.buf }
// OptionsPayload returns the options portion of the DHCP frame. May be zero lengthed.
func (frm Frame) OptionsPayload() []byte {
return frm.buf[OptionsOffset:]
+214
View File
@@ -0,0 +1,214 @@
package tls
import (
"encoding/binary"
"unsafe"
)
// maxBuilderNest is the deepest length-prefix nesting the [Builder] supports.
// The deepest real TLS 1.3 structure is the ClientHello key_share extension:
// handshake body, extensions block, extension, client_shares list,
// key_exchange. Eight leaves generous headroom.
const maxBuilderNest = 8
// Builder writes TLS wire structures into a caller-supplied buffer that never
// grows. It exists because TLS nests length-prefixed vectors several levels
// deep and the length of each is only known after its contents are written;
// the standard answer, golang.org/x/crypto/cryptobyte, allocates.
//
// Open a length prefix, write the contents, then Close to backpatch the real
// length:
//
// var b tls.Builder
// b.Reset(buf)
// b.AddU16(uint16(tls.ExtSupportedVersions))
// b.OpenU16() // extension_data length
// b.AddU16(tls.VersionTLS13)
// b.Close()
// out, err := b.Bytes()
//
// Errors are sticky: once the destination buffer overflows or the nesting is
// misused, every later call is a no-op and [Builder.Err] reports the first
// failure. Callers therefore need to check only once, at the end.
//
// The zero Builder is not usable; call [Builder.Reset] first.
type Builder struct {
buf []byte
stack [maxBuilderNest]int32 // offset of each open length field
width [maxBuilderNest]int8 // 1, 2 or 3 byte length prefix
n int8 // open prefix count
err error
}
// Reset prepares the Builder to write into dst, discarding any previous state
// and error. The Builder writes into dst[:0] and never reallocates, so dst's
// capacity is a hard ceiling on the structure being built.
func (b *Builder) Reset(dst []byte) {
b.buf = dst[:0]
b.n = 0
b.err = nil
}
// Err returns the first error encountered since [Builder.Reset], if any.
func (b *Builder) Err() error { return b.err }
// Len returns the number of bytes written so far, including the placeholder
// bytes of any currently open length prefix.
func (b *Builder) Len() int { return len(b.buf) }
// Bytes returns the built structure. It reports an error if the Builder failed
// at any point, or if a length prefix was opened and never closed, since the
// resulting bytes would contain an unpatched placeholder.
func (b *Builder) Bytes() ([]byte, error) {
if b.err != nil {
return nil, b.err
}
if b.n != 0 {
return nil, errBuilderUnbal
}
return b.buf, nil
}
// fail records err if no error has been recorded yet.
func (b *Builder) fail(err error) {
if b.err == nil {
b.err = err
}
}
// grow extends the buffer by n bytes and returns the new region, or nil if the
// buffer is full or the Builder has already failed.
func (b *Builder) grow(n int) []byte {
if b.err != nil {
return nil
}
if n > cap(b.buf)-len(b.buf) {
b.fail(errShortBuffer)
return nil
}
start := len(b.buf)
b.buf = b.buf[:start+n]
return b.buf[start:]
}
// AddU8 appends a single byte.
func (b *Builder) AddU8(v uint8) {
if p := b.grow(1); p != nil {
p[0] = v
}
}
// AddU16 appends a big-endian uint16.
func (b *Builder) AddU16(v uint16) {
if p := b.grow(2); p != nil {
binary.BigEndian.PutUint16(p, v)
}
}
// AddU24 appends a big-endian 24-bit value, the length encoding used by
// handshake message headers and certificate entries.
func (b *Builder) AddU24(v uint32) {
if p := b.grow(3); p != nil {
p[0] = byte(v >> 16)
p[1] = byte(v >> 8)
p[2] = byte(v)
}
}
// AddU32 appends a big-endian uint32.
func (b *Builder) AddU32(v uint32) {
if p := b.grow(4); p != nil {
binary.BigEndian.PutUint32(p, v)
}
}
// AddBytes appends raw bytes.
func (b *Builder) AddBytes(v []byte) {
if p := b.grow(len(v)); p != nil {
copy(p, v)
}
}
// AddString appends the bytes of s without allocating. TLS labels and ALPN
// protocol names are naturally string constants, and converting them with
// []byte(s) would copy to the heap on every call.
func (b *Builder) AddString(s string) {
if len(s) == 0 {
return
}
// unsafe.Slice over the string's backing array; the bytes are only read,
// and only for the duration of the copy inside AddBytes.
b.AddBytes(unsafe.Slice(unsafe.StringData(s), len(s)))
}
// openN reserves width bytes for a length prefix whose value is filled in by
// the matching [Builder.Close].
func (b *Builder) openN(width int8) {
if b.err != nil {
return
}
if int(b.n) >= maxBuilderNest {
b.fail(errBuilderNest)
return
}
start := len(b.buf)
if b.grow(int(width)) == nil {
return
}
b.stack[b.n] = int32(start)
b.width[b.n] = width
b.n++
}
// OpenU8 begins a vector with a one-byte length prefix.
func (b *Builder) OpenU8() { b.openN(1) }
// OpenU16 begins a vector with a two-byte length prefix.
func (b *Builder) OpenU16() { b.openN(2) }
// OpenU24 begins a vector with a three-byte length prefix, as used by the
// handshake message header.
func (b *Builder) OpenU24() { b.openN(3) }
// Close ends the innermost open vector and backpatches its length prefix with
// the number of bytes written since the matching Open.
//
// A length that does not fit the reserved prefix width is a failure rather
// than a silent truncation: writing a value modulo 2^16 into a two-byte prefix
// would produce a structurally valid but semantically wrong record.
func (b *Builder) Close() {
if b.err != nil {
return
}
if b.n == 0 {
b.fail(errBuilderUnbal)
return
}
b.n--
start := int(b.stack[b.n])
width := int(b.width[b.n])
n := len(b.buf) - start - width
p := b.buf[start:]
switch width {
case 1:
if n > 0xff {
b.fail(errBadLength)
return
}
p[0] = byte(n)
case 2:
if n > 0xffff {
b.fail(errBadLength)
return
}
binary.BigEndian.PutUint16(p, uint16(n))
case 3:
if n > 0xffffff {
b.fail(errBadLength)
return
}
p[0] = byte(n >> 16)
p[1] = byte(n >> 8)
p[2] = byte(n)
}
}
+242
View File
@@ -0,0 +1,242 @@
package tls_test
import (
"errors"
"testing"
"github.com/soypat/lneto"
"github.com/soypat/lneto/x/tls"
)
// buildSupportedVersions writes the extension a TLS 1.3 ServerHello carries,
// exercising one level of nesting.
func buildSupportedVersions(b *tls.Builder) {
b.AddU16(uint16(tls.ExtSupportedVersions))
b.OpenU16()
b.AddU16(tls.VersionTLS13)
b.Close()
}
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.
var b tls.Builder
buf := make([]byte, 64)
b.Reset(buf)
b.OpenU16() // extensions block length
buildSupportedVersions(&b)
b.AddU16(uint16(tls.ExtKeyShare))
b.OpenU16()
b.OpenU16() // client_shares
b.AddU16(uint16(tls.GroupX25519))
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.
if len(out) < 2 {
t.Fatal("output too short")
}
exts := out[2:]
if int(out[0])<<8|int(out[1]) != len(exts) {
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
})
if err != nil {
t.Fatal(err)
}
if len(seen) != 2 || seen[0] != tls.ExtSupportedVersions || seen[1] != tls.ExtKeyShare {
t.Errorf("walked %v", seen)
}
}
func TestBuilderStickyShortBuffer(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 4))
b.AddU32(0x01020304) // exactly fills
if b.Err() != nil {
t.Fatalf("unexpected error filling buffer: %v", b.Err())
}
b.AddU8(0xff) // overflows
if !errors.Is(b.Err(), lneto.ErrShortBuffer) {
t.Fatalf("got %v want ErrShortBuffer", b.Err())
}
// Once failed, later writes must not panic, must not write, and must not
// replace the original error.
b.AddBytes(make([]byte, 100))
b.OpenU16()
b.Close()
if !errors.Is(b.Err(), lneto.ErrShortBuffer) {
t.Fatalf("error changed to %v", b.Err())
}
if _, err := b.Bytes(); !errors.Is(err, lneto.ErrShortBuffer) {
t.Fatalf("Bytes returned %v", err)
}
}
func TestBuilderUnclosedPrefixIsAnError(t *testing.T) {
// Returning bytes with an unpatched placeholder would emit a structurally
// wrong record that looks valid.
var b tls.Builder
b.Reset(make([]byte, 16))
b.OpenU16()
b.AddU8(1)
if _, err := b.Bytes(); err == nil {
t.Error("Bytes accepted an unclosed length prefix")
}
}
func TestBuilderUnbalancedClose(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 16))
b.Close()
if b.Err() == nil {
t.Error("Close without Open accepted")
}
}
func TestBuilderNestingLimit(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 64))
for range 8 {
b.OpenU8()
}
if b.Err() != nil {
t.Fatalf("8 levels rejected: %v", b.Err())
}
b.OpenU8()
if b.Err() == nil {
t.Error("9th nesting level accepted")
}
}
func TestBuilderLengthOverflowRejected(t *testing.T) {
// 300 bytes cannot be described by a one-byte prefix. Truncating modulo 256
// would produce a valid-looking but wrong structure, so this must fail.
var b tls.Builder
b.Reset(make([]byte, 512))
b.OpenU8()
b.AddBytes(make([]byte, 300))
b.Close()
if !errors.Is(b.Err(), lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", b.Err())
}
}
func TestBuilderU24RoundTrip(t *testing.T) {
var b tls.Builder
b.Reset(make([]byte, 8))
b.AddU8(byte(tls.HandshakeTypeServerHello))
b.OpenU24()
b.AddBytes([]byte{1, 2, 3})
b.Close()
out, err := b.Bytes()
if err != nil {
t.Fatal(err)
}
hf, err := tls.NewHandshakeFrame(out)
if err != nil {
t.Fatal(err)
}
if hf.MsgType() != tls.HandshakeTypeServerHello {
t.Errorf("type got %v", hf.MsgType())
}
if hf.Length() != 3 {
t.Errorf("length got %d want 3", hf.Length())
}
if !hf.Complete() || len(hf.Body()) != 3 {
t.Errorf("body got % x", hf.Body())
}
}
func TestBuilderAddStringMatchesAddBytes(t *testing.T) {
const label = "tls13 derived"
var b1, b2 tls.Builder
buf1, buf2 := make([]byte, 32), make([]byte, 32)
b1.Reset(buf1)
b1.AddString(label)
b2.Reset(buf2)
b2.AddBytes([]byte(label))
out1, err1 := b1.Bytes()
out2, err2 := b2.Bytes()
if err1 != nil || err2 != nil {
t.Fatalf("%v %v", err1, err2)
}
if string(out1) != string(out2) {
t.Errorf("AddString %q != AddBytes %q", out1, out2)
}
}
func TestBuilderZeroAlloc(t *testing.T) {
// The whole point of not using cryptobyte. A regression here means the
// outbound handshake path started allocating per connection.
buf := make([]byte, 256)
var b tls.Builder
n := testing.AllocsPerRun(100, func() {
b.Reset(buf)
b.AddU8(byte(tls.HandshakeTypeServerHello))
b.OpenU24()
b.AddU16(tls.VersionTLS12)
b.AddBytes(make([]byte, 0, 0)) // no-op, must not allocate
b.OpenU16()
buildSupportedVersions(&b)
b.Close()
b.AddString("x")
b.Close()
_, _ = b.Bytes()
})
if n != 0 {
t.Errorf("Builder allocated %v times per run, want 0", n)
}
}
func FuzzBuilderNeverEscapesBuffer(f *testing.F) {
f.Add([]byte{1, 2, 3}, uint8(16))
f.Fuzz(func(t *testing.T, payload []byte, size uint8) {
buf := make([]byte, size)
canary := make([]byte, len(buf))
var b tls.Builder
b.Reset(buf)
b.OpenU16()
b.AddBytes(payload)
b.OpenU8()
b.AddBytes(payload)
b.Close()
b.Close()
out, err := b.Bytes()
if err != nil {
return
}
if len(out) > len(buf) {
t.Fatalf("wrote %d bytes into a %d byte buffer", len(out), len(buf))
}
// Whatever was produced must parse back as a well-formed vector.
if len(out) < 2 {
t.Fatalf("output %d bytes is too short to hold its own prefix", len(out))
}
if declared := int(out[0])<<8 | int(out[1]); declared != len(out)-2 {
t.Fatalf("declared %d != actual %d", declared, len(out)-2)
}
_ = canary
})
}
+275
View File
@@ -0,0 +1,275 @@
// Package tls implements a heapless, preallocated TLS 1.3 server (RFC 8446).
//
// All buffers are supplied by the caller and never grow. There is no
// allocation in the steady-state record datapath, no goroutines, and no
// dependency on an operating system. A [Conn] wraps any [io.ReadWriter], so it
// works over a lneto tcp.Conn, over net.Pipe, or over a bytes.Buffer in tests.
//
// # Scope
//
// TLS 1.3 only. There is deliberately no TLS 1.2 fallback, no renegotiation,
// no 0-RTT/early data, and no client certificate support. Each omission
// removes a whole class of attack surface.
//
// # Cryptographic primitives
//
// This package implements the record layer, the key schedule and the handshake
// state machine, but ships no cryptographic primitives of its own. Concrete
// hashes, AEADs, key agreements and signers cross the [Hasher], [AEADSuite],
// [KeyAgreement] and [Signer] interfaces. Standard library implementations
// live in the tlsstd subpackage; a hardware AEAD or a secure-element signer
// slots in at the same seam without protocol code changing.
//
// Note that on embedded targets the Signer is expected to be supplied by the
// integrator: Go's crypto/ecdsa carries an 88kB precomputed P-256 basepoint
// table that lands in RAM, which is a third of an RP2040's SRAM.
//
// # Randomness
//
// Config.Rand is required and has no default. Embedded targets frequently have
// a weak or stubbed entropy source, and a biased ECDSA nonce recovers the
// signing key from two signatures, so the choice is made explicit rather than
// silently defaulted. Never seed anything here from internal.Prand32/64; those
// are for TCP initial sequence numbers and DNS IDs, not for key material.
package tls
import (
"errors"
"github.com/soypat/lneto"
)
// Sizes of fixed length headers and the record size limits of RFC 8446 5.2.
const (
// SizeHeaderRecord is the size of a TLSPlaintext/TLSCiphertext header:
// content type(1) + legacy record version(2) + length(2).
SizeHeaderRecord = 5
// SizeHeaderHandshake is the size of a Handshake header:
// message type(1) + 24-bit length(3).
SizeHeaderHandshake = 4
// MaxPlaintext is the largest legal TLSPlaintext.fragment.
MaxPlaintext = 1 << 14 // 16384
// MaxCiphertext is the largest legal TLSCiphertext.encrypted_record.
// The extra 256 bytes cover the content type byte, padding and the AEAD tag.
MaxCiphertext = 1<<14 + 256 // 16640
// MaxRecord is the largest legal record as it appears on the wire.
MaxRecord = SizeHeaderRecord + MaxCiphertext // 16645
// MinRecordSizeLimit is the smallest value a peer may advertise in the
// record_size_limit extension of RFC 8449 4.
MinRecordSizeLimit = 64
)
//go:generate stringer -type=ContentType,HandshakeType,ExtensionType,AlertDescription,AlertLevel,NamedGroup,SignatureScheme,CipherSuite -linecomment -output stringers.go .
// ContentType is the outermost record demultiplexing tag of RFC 8446 5.1.
type ContentType uint8
// Record content types. Values below are the only ones TLS 1.3 defines;
// everything else must be rejected with an unexpected_message alert.
const (
ContentTypeInvalid ContentType = 0 // invalid
ContentTypeChangeCipherSpec ContentType = 20 // change_cipher_spec
ContentTypeAlert ContentType = 21 // alert
ContentTypeHandshake ContentType = 22 // handshake
ContentTypeApplicationData ContentType = 23 // application_data
)
// HandshakeType identifies a handshake message, RFC 8446 4.
type HandshakeType uint8
// Handshake message types. Types this server never sends or accepts are still
// listed so that a peer sending one can be logged and rejected precisely.
const (
HandshakeTypeClientHello HandshakeType = 1 // client_hello
HandshakeTypeServerHello HandshakeType = 2 // server_hello
HandshakeTypeNewSessionTicket HandshakeType = 4 // new_session_ticket
HandshakeTypeEndOfEarlyData HandshakeType = 5 // end_of_early_data
HandshakeTypeEncryptedExtensions HandshakeType = 8 // encrypted_extensions
HandshakeTypeCertificate HandshakeType = 11 // certificate
HandshakeTypeCertificateRequest HandshakeType = 13 // certificate_request
HandshakeTypeCertificateVerify HandshakeType = 15 // certificate_verify
HandshakeTypeFinished HandshakeType = 20 // finished
HandshakeTypeKeyUpdate HandshakeType = 24 // key_update
HandshakeTypeMessageHash HandshakeType = 254 // message_hash
)
// ExtensionType identifies a hello extension, RFC 8446 4.2.
type ExtensionType uint16
// Extension types. Those marked "ignored" are ones a browser sends and this
// server parses past without erroring; being strict about unknown extensions
// breaks real clients.
const (
ExtServerName ExtensionType = 0 // server_name
ExtMaxFragmentLength ExtensionType = 1 // max_fragment_length
ExtStatusRequest ExtensionType = 5 // status_request
ExtSupportedGroups ExtensionType = 10 // supported_groups
ExtSignatureAlgorithms ExtensionType = 13 // signature_algorithms
ExtALPN ExtensionType = 16 // application_layer_protocol_negotiation
ExtSignedCertificateTimestamp ExtensionType = 18 // signed_certificate_timestamp
ExtPadding ExtensionType = 21 // padding
ExtExtendedMasterSecret ExtensionType = 23 // extended_master_secret
ExtCompressCertificate ExtensionType = 27 // compress_certificate
ExtRecordSizeLimit ExtensionType = 28 // record_size_limit
ExtSessionTicket ExtensionType = 35 // session_ticket
ExtPreSharedKey ExtensionType = 41 // pre_shared_key
ExtEarlyData ExtensionType = 42 // early_data
ExtSupportedVersions ExtensionType = 43 // supported_versions
ExtCookie ExtensionType = 44 // cookie
ExtPSKKeyExchangeModes ExtensionType = 45 // psk_key_exchange_modes
ExtCertificateAuthorities ExtensionType = 47 // certificate_authorities
ExtSignatureAlgorithmsCert ExtensionType = 50 // signature_algorithms_cert
ExtKeyShare ExtensionType = 51 // key_share
ExtApplicationSettings ExtensionType = 17513 // application_settings
ExtEncryptedClientHello ExtensionType = 65037 // encrypted_client_hello
ExtRenegotiationInfo ExtensionType = 65281 // renegotiation_info
)
// AlertLevel is the legacy severity byte of an alert. In TLS 1.3 every alert
// except close_notify and user_canceled is fatal regardless of this field
// (RFC 8446 6.1), so it is carried for wire compatibility only and must never
// be used to decide whether to continue.
type AlertLevel uint8
// Alert levels.
const (
AlertLevelWarning AlertLevel = 1 // warning
AlertLevelFatal AlertLevel = 2 // fatal
)
// AlertDescription is the alert code of RFC 8446 6.
type AlertDescription uint8
// Alert descriptions. Only the subset a TLS 1.3 server can legitimately send
// or receive is listed.
const (
AlertCloseNotify AlertDescription = 0 // close_notify
AlertUnexpectedMessage AlertDescription = 10 // unexpected_message
AlertBadRecordMAC AlertDescription = 20 // bad_record_mac
AlertRecordOverflow AlertDescription = 22 // record_overflow
AlertHandshakeFailure AlertDescription = 40 // handshake_failure
AlertBadCertificate AlertDescription = 42 // bad_certificate
AlertUnsupportedCertificate AlertDescription = 43 // unsupported_certificate
AlertCertificateRevoked AlertDescription = 44 // certificate_revoked
AlertCertificateExpired AlertDescription = 45 // certificate_expired
AlertCertificateUnknown AlertDescription = 46 // certificate_unknown
AlertIllegalParameter AlertDescription = 47 // illegal_parameter
AlertUnknownCA AlertDescription = 48 // unknown_ca
AlertAccessDenied AlertDescription = 49 // access_denied
AlertDecodeError AlertDescription = 50 // decode_error
AlertDecryptError AlertDescription = 51 // decrypt_error
AlertProtocolVersion AlertDescription = 70 // protocol_version
AlertInsufficientSecurity AlertDescription = 71 // insufficient_security
AlertInternalError AlertDescription = 80 // internal_error
AlertInappropriateFallback AlertDescription = 86 // inappropriate_fallback
AlertUserCanceled AlertDescription = 90 // user_canceled
AlertMissingExtension AlertDescription = 109 // missing_extension
AlertUnsupportedExtension AlertDescription = 110 // unsupported_extension
AlertUnrecognizedName AlertDescription = 112 // unrecognized_name
AlertBadCertificateStatusResponse AlertDescription = 113 // bad_certificate_status_response
AlertUnknownPSKIdentity AlertDescription = 115 // unknown_psk_identity
AlertCertificateRequired AlertDescription = 116 // certificate_required
AlertNoApplicationProtocol AlertDescription = 120 // no_application_protocol
)
// Protocol versions as they appear on the wire.
const (
// VersionTLS12 is the value TLS 1.3 requires in ClientHello.legacy_version
// and in ServerHello.legacy_version for middlebox compatibility.
VersionTLS12 uint16 = 0x0303
// VersionTLS13 is the real negotiated version, carried only in the
// supported_versions extension.
VersionTLS13 uint16 = 0x0304
// VersionTLS10 appears in the legacy_record_version of an initial
// ClientHello record. The field is ignored entirely on receipt.
VersionTLS10 uint16 = 0x0301
)
// NamedGroup identifies a key exchange group, RFC 8446 4.2.7.
type NamedGroup uint16
// Named groups. Only X25519 is implemented; the rest are recognized so that
// group selection and HelloRetryRequest can report precisely what was offered.
const (
GroupSECP256R1 NamedGroup = 0x0017 // secp256r1
GroupSECP384R1 NamedGroup = 0x0018 // secp384r1
GroupSECP521R1 NamedGroup = 0x0019 // secp521r1
GroupX25519 NamedGroup = 0x001d // x25519
GroupX448 NamedGroup = 0x001e // x448
GroupX25519MLKEM768 NamedGroup = 0x11ec // x25519mlkem768
)
// SignatureScheme identifies a signature algorithm, RFC 8446 4.2.3.
type SignatureScheme uint16
// Signature schemes. Browsers do not accept Ed25519 certificates, so
// [SigECDSAP256SHA256] is the practical minimum for a public-facing server.
const (
SigRSAPKCS1SHA256 SignatureScheme = 0x0401 // rsa_pkcs1_sha256
SigRSAPKCS1SHA384 SignatureScheme = 0x0501 // rsa_pkcs1_sha384
SigRSAPKCS1SHA512 SignatureScheme = 0x0601 // rsa_pkcs1_sha512
SigECDSAP256SHA256 SignatureScheme = 0x0403 // ecdsa_secp256r1_sha256
SigECDSAP384SHA384 SignatureScheme = 0x0503 // ecdsa_secp384r1_sha384
SigECDSAP521SHA512 SignatureScheme = 0x0603 // ecdsa_secp521r1_sha512
SigRSAPSSRSAESHA256 SignatureScheme = 0x0804 // rsa_pss_rsae_sha256
SigRSAPSSRSAESHA384 SignatureScheme = 0x0805 // rsa_pss_rsae_sha384
SigRSAPSSRSAESHA512 SignatureScheme = 0x0806 // rsa_pss_rsae_sha512
SigEd25519 SignatureScheme = 0x0807 // ed25519
SigRSAPSSPSSSHA256 SignatureScheme = 0x0809 // rsa_pss_pss_sha256
)
// CipherSuite identifies an AEAD plus hash pair, RFC 8446 B.4.
type CipherSuite uint16
// TLS 1.3 cipher suites. Only [SuiteAES128GCMSHA256] is implemented; it is the
// one suite RFC 8446 9.1 makes mandatory to implement.
const (
SuiteAES128GCMSHA256 CipherSuite = 0x1301 // TLS_AES_128_GCM_SHA256
SuiteAES256GCMSHA384 CipherSuite = 0x1302 // TLS_AES_256_GCM_SHA384
SuiteChaCha20Poly1305SHA256 CipherSuite = 0x1303 // TLS_CHACHA20_POLY1305_SHA256
SuiteAES128CCMSHA256 CipherSuite = 0x1304 // TLS_AES_128_CCM_SHA256
SuiteAES128CCM8SHA256 CipherSuite = 0x1305 // TLS_AES_128_CCM_8_SHA256
)
// IsGREASE reports whether v is one of the 16 reserved GREASE values of
// RFC 8701. Chrome injects GREASE values into its offered cipher suites,
// supported groups, extensions, ALPN protocol list and key shares. They carry
// no meaning and must be skipped wherever they appear; rejecting them breaks
// Chrome outright.
//
// GREASE values have both bytes equal and of the form 0x?a.
func IsGREASE(v uint16) bool {
return v&0x0f0f == 0x0a0a && v>>8 == v&0xff
}
// ErrNeedMore is returned by incremental parsers and by the record layer when
// the input available so far is a valid prefix but not yet a complete unit. It
// is not a failure: the caller should retry once more data has arrived.
//
// It is deliberately distinct from [lneto.ErrTruncatedFrame], which means the
// data is complete but malformed.
var ErrNeedMore = errors.New("tls: need more data")
// ErrUnexpectedClose is returned by Conn.Read when the underlying transport
// reached EOF before a close_notify alert was received. Returning io.EOF here
// instead would let an attacker who can inject a TCP FIN or RST silently
// truncate a response or a request body. Callers must treat this as a failure
// unless the application layer has independently confirmed the message was
// complete, for example by satisfying a Content-Length.
var ErrUnexpectedClose = errors.New("tls: connection closed without close_notify")
// Errors reported by frame constructors and walkers. These reuse the generic
// lneto error set so that callers can compare against a single vocabulary.
var (
errTruncated error = lneto.ErrTruncatedFrame
errShortBuffer error = lneto.ErrShortBuffer
errBadLength error = lneto.ErrInvalidLengthField
errBadField error = lneto.ErrInvalidField
errUnsupported error = lneto.ErrUnsupported
errTrailingBytes = errors.New("tls: trailing bytes after structure")
errBuilderNest = errors.New("tls: builder nesting depth exceeded")
errBuilderUnbal = errors.New("tls: builder length prefix unbalanced")
errAllZeroPlaintext = errors.New("tls: inner plaintext is all padding")
)
+485
View File
@@ -0,0 +1,485 @@
package tls
import (
"encoding/binary"
"github.com/soypat/lneto"
)
// RecordFrame provides zero-copy access to a TLS record (RFC 8446 5.1).
//
// struct {
// ContentType type; // 1 byte
// ProtocolVersion legacy_record_version; // 2 bytes
// uint16 length; // 2 bytes
// opaque fragment[length];
// } TLSPlaintext;
//
// A RecordFrame may be constructed over a buffer holding only the header, so
// that [RecordFrame.Length] can be consulted to decide how many more bytes to
// read. Accessors that reach into the fragment return nil until the whole
// record is present; call [RecordFrame.Complete] to test for that explicitly.
type RecordFrame struct {
buf []byte
}
// NewRecordFrame wraps buf as a [RecordFrame]. It validates only that the
// 5-byte header is present, since the fragment commonly arrives later.
func NewRecordFrame(buf []byte) (RecordFrame, error) {
if len(buf) < SizeHeaderRecord {
return RecordFrame{}, lneto.ErrTruncatedFrame
}
return RecordFrame{buf: buf}, nil
}
// ContentType returns the record's outer content type. For a protected record
// this is always [ContentTypeApplicationData]; the real type lives in the
// encrypted [InnerPlaintext].
func (rf RecordFrame) ContentType() ContentType { return ContentType(rf.buf[0]) }
// SetContentType sets the outer content type.
func (rf RecordFrame) SetContentType(ct ContentType) { rf.buf[0] = byte(ct) }
// LegacyVersion returns the legacy_record_version field. TLS 1.3 requires
// receivers to ignore this field entirely; it is exposed for logging only.
func (rf RecordFrame) LegacyVersion() uint16 {
return binary.BigEndian.Uint16(rf.buf[1:3])
}
// SetLegacyVersion sets the legacy_record_version field.
func (rf RecordFrame) SetLegacyVersion(v uint16) {
binary.BigEndian.PutUint16(rf.buf[1:3], v)
}
// Length returns the declared fragment length. It is attacker controlled and
// must be checked against [MaxCiphertext] before being used to size a read;
// [RecordFrame.ValidateSize] does this.
func (rf RecordFrame) Length() uint16 {
return binary.BigEndian.Uint16(rf.buf[3:5])
}
// SetLength sets the declared fragment length.
func (rf RecordFrame) SetLength(n uint16) {
binary.BigEndian.PutUint16(rf.buf[3:5], n)
}
// RecordLength returns the total wire size of this record, header included.
func (rf RecordFrame) RecordLength() int {
return SizeHeaderRecord + int(rf.Length())
}
// Complete reports whether the whole record, header and fragment, is present
// in the underlying buffer.
func (rf RecordFrame) Complete() bool {
return len(rf.buf) >= rf.RecordLength()
}
// Payload returns the record fragment, or nil if the whole record has not
// arrived yet. The result aliases the underlying buffer.
func (rf RecordFrame) Payload() []byte {
if !rf.Complete() {
return nil
}
return rf.buf[SizeHeaderRecord:rf.RecordLength()]
}
// RawData returns the record bytes, header included, truncated to the declared
// length when the full record is present.
func (rf RecordFrame) RawData() []byte {
if !rf.Complete() {
return rf.buf
}
return rf.buf[:rf.RecordLength()]
}
// ValidateSize adds an error to v if the record is structurally invalid.
// It does not require the fragment to have arrived; it only rejects a declared
// length that could never be legal.
func (rf RecordFrame) ValidateSize(v *lneto.Validator) {
if len(rf.buf) < SizeHeaderRecord {
v.AddError(lneto.ErrTruncatedFrame)
return
}
if rf.Length() > MaxCiphertext {
// Checked before the length is ever used to size a read.
v.AddError(lneto.ErrInvalidLengthField)
}
}
// InnerPlaintext provides access to a decrypted TLSInnerPlaintext
// (RFC 8446 5.2):
//
// struct {
// opaque content[length];
// ContentType type;
// uint8 zeros[length_of_padding];
// } TLSInnerPlaintext;
//
// The real content type is the last non-zero byte, and everything after the
// content is padding that must be stripped before use.
type InnerPlaintext struct {
buf []byte // content only, padding and type byte already stripped
ctype ContentType
padding int
}
// NewInnerPlaintext scans decrypted for its trailing content type byte,
// stripping any zero padding that follows it.
//
// A record whose plaintext is entirely zeros carries no content type and is a
// protocol violation; it is reported so the caller can send an
// unexpected_message alert rather than silently treating it as empty.
func NewInnerPlaintext(decrypted []byte) (InnerPlaintext, error) {
i := len(decrypted) - 1
for i >= 0 && decrypted[i] == 0 {
i--
}
if i < 0 {
return InnerPlaintext{}, errAllZeroPlaintext
}
return InnerPlaintext{
buf: decrypted[:i],
ctype: ContentType(decrypted[i]),
padding: len(decrypted) - i - 1,
}, nil
}
// ContentType returns the true content type recovered from the inner plaintext.
func (ip InnerPlaintext) ContentType() ContentType { return ip.ctype }
// Content returns the plaintext with the content type byte and padding removed.
// The result aliases the buffer passed to [NewInnerPlaintext].
func (ip InnerPlaintext) Content() []byte { return ip.buf }
// PaddingLen returns how many padding bytes followed the content type byte.
func (ip InnerPlaintext) PaddingLen() int { return ip.padding }
// HandshakeFrame provides zero-copy access to a handshake message
// (RFC 8446 4):
//
// struct {
// HandshakeType msg_type; // 1 byte
// uint24 length; // 3 bytes
// opaque body[length];
// } Handshake;
//
// As with [RecordFrame], a HandshakeFrame may be constructed over a buffer
// holding only the 4-byte header so that the body length can be consulted
// before the rest has arrived.
type HandshakeFrame struct {
buf []byte
}
// NewHandshakeFrame wraps buf as a [HandshakeFrame], validating that the
// 4-byte header is present.
func NewHandshakeFrame(buf []byte) (HandshakeFrame, error) {
if len(buf) < SizeHeaderHandshake {
return HandshakeFrame{}, lneto.ErrTruncatedFrame
}
return HandshakeFrame{buf: buf}, nil
}
// MsgType returns the handshake message type.
func (hf HandshakeFrame) MsgType() HandshakeType { return HandshakeType(hf.buf[0]) }
// SetMsgType sets the handshake message type.
func (hf HandshakeFrame) SetMsgType(t HandshakeType) { hf.buf[0] = byte(t) }
// Length returns the declared 24-bit body length. It is attacker controlled;
// the value is returned as an int32 rather than an int so that behaviour is
// identical on 32- and 64-bit targets.
func (hf HandshakeFrame) Length() int32 {
return int32(hf.buf[1])<<16 | int32(hf.buf[2])<<8 | int32(hf.buf[3])
}
// SetLength sets the declared 24-bit body length. Values outside the 24-bit
// range are silently masked; callers building messages should ensure the body
// fits first.
func (hf HandshakeFrame) SetLength(n int32) {
hf.buf[1] = byte(n >> 16)
hf.buf[2] = byte(n >> 8)
hf.buf[3] = byte(n)
}
// MessageLength returns the total size of this handshake message, header
// included.
func (hf HandshakeFrame) MessageLength() int {
return SizeHeaderHandshake + int(hf.Length())
}
// Complete reports whether the entire handshake message is present.
func (hf HandshakeFrame) Complete() bool {
return len(hf.buf) >= hf.MessageLength()
}
// Body returns the handshake message body, or nil if the whole message has not
// arrived. The result aliases the underlying buffer.
func (hf HandshakeFrame) Body() []byte {
if !hf.Complete() {
return nil
}
return hf.buf[SizeHeaderHandshake:hf.MessageLength()]
}
// RawData returns the handshake message bytes, header included. This is what
// must be fed to the transcript hash: the header is hashed along with the body,
// exactly once per message, even when the message spanned several records.
func (hf HandshakeFrame) RawData() []byte {
if !hf.Complete() {
return hf.buf
}
return hf.buf[:hf.MessageLength()]
}
// ValidateSize adds an error to v if the handshake header is malformed.
func (hf HandshakeFrame) ValidateSize(v *lneto.Validator) {
if len(hf.buf) < SizeHeaderHandshake {
v.AddError(lneto.ErrTruncatedFrame)
return
}
if hf.Length() > MaxPlaintext {
// No handshake message this server accepts approaches 2^14 bytes.
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
}
+418
View File
@@ -0,0 +1,418 @@
package tls_test
import (
"errors"
"testing"
"github.com/soypat/lneto"
"github.com/soypat/lneto/x/tls"
)
func TestRecordFrameHeaderBeforeBody(t *testing.T) {
// A record layer sees the 5 byte header first and must be able to consult
// the length to know how much more to read.
hdr := []byte{22, 0x03, 0x01, 0x00, 0x10}
rf, err := tls.NewRecordFrame(hdr)
if err != nil {
t.Fatal(err)
}
if rf.ContentType() != tls.ContentTypeHandshake {
t.Errorf("content type got %v want handshake", rf.ContentType())
}
if rf.Length() != 16 {
t.Errorf("length got %d want 16", rf.Length())
}
if rf.RecordLength() != 21 {
t.Errorf("record length got %d want 21", rf.RecordLength())
}
if rf.Complete() {
t.Error("record with header only reported complete")
}
if rf.Payload() != nil {
t.Error("payload of incomplete record must be nil, not a short slice")
}
full := append(hdr, make([]byte, 16)...)
rf, err = tls.NewRecordFrame(full)
if err != nil {
t.Fatal(err)
}
if !rf.Complete() {
t.Fatal("full record reported incomplete")
}
if len(rf.Payload()) != 16 {
t.Errorf("payload len got %d want 16", len(rf.Payload()))
}
}
func TestRecordFrameShortHeader(t *testing.T) {
for n := range tls.SizeHeaderRecord {
_, err := tls.NewRecordFrame(make([]byte, n))
if !errors.Is(err, lneto.ErrTruncatedFrame) {
t.Errorf("len=%d got %v want ErrTruncatedFrame", n, err)
}
}
}
func TestRecordFrameRejectsOversizeLength(t *testing.T) {
// The length field is attacker controlled and must be refused before it is
// ever used to size a read.
hdr := []byte{23, 0x03, 0x03, 0xff, 0xff} // 65535 > MaxCiphertext
rf, err := tls.NewRecordFrame(hdr)
if err != nil {
t.Fatal(err)
}
var v lneto.Validator
rf.ValidateSize(&v)
if err := v.ErrPop(); !errors.Is(err, lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", err)
}
// Exactly at the limit is legal.
maxCT := uint16(tls.MaxCiphertext)
hdr[3], hdr[4] = byte(maxCT>>8), byte(maxCT)
rf, _ = tls.NewRecordFrame(hdr)
rf.ValidateSize(&v)
if err := v.ErrPop(); err != nil {
t.Errorf("MaxCiphertext rejected: %v", err)
}
}
func TestInnerPlaintextStripsPadding(t *testing.T) {
for _, tc := range []struct {
name string
in []byte
content string
ctype tls.ContentType
padding int
}{
{"no padding", []byte{'h', 'i', 23}, "hi", tls.ContentTypeApplicationData, 0},
{"padded", []byte{'h', 'i', 23, 0, 0, 0}, "hi", tls.ContentTypeApplicationData, 3},
{"empty content", []byte{22}, "", tls.ContentTypeHandshake, 0},
{"content ends in zero", []byte{'a', 0, 'b', 21, 0}, "a\x00b", tls.ContentTypeAlert, 1},
} {
t.Run(tc.name, func(t *testing.T) {
ip, err := tls.NewInnerPlaintext(tc.in)
if err != nil {
t.Fatal(err)
}
if string(ip.Content()) != tc.content {
t.Errorf("content got %q want %q", ip.Content(), tc.content)
}
if ip.ContentType() != tc.ctype {
t.Errorf("type got %v want %v", ip.ContentType(), tc.ctype)
}
if ip.PaddingLen() != tc.padding {
t.Errorf("padding got %d want %d", ip.PaddingLen(), tc.padding)
}
})
}
}
func TestInnerPlaintextAllZeroRejected(t *testing.T) {
// An all-zero inner plaintext carries no content type. Silently treating it
// as empty would let a peer inject records the state machine cannot
// classify; RFC 8446 5.4 requires unexpected_message.
for _, n := range []int{0, 1, 8} {
_, err := tls.NewInnerPlaintext(make([]byte, n))
if err == nil {
t.Errorf("len=%d: all-zero plaintext accepted", n)
}
}
}
func TestHandshakeFrame24BitLength(t *testing.T) {
hdr := []byte{1, 0x01, 0x02, 0x03} // ClientHello, length 0x010203
hf, err := tls.NewHandshakeFrame(hdr)
if err != nil {
t.Fatal(err)
}
if hf.MsgType() != tls.HandshakeTypeClientHello {
t.Errorf("type got %v", hf.MsgType())
}
if hf.Length() != 0x010203 {
t.Errorf("length got %#x want 0x010203", hf.Length())
}
if hf.Complete() {
t.Error("header-only message reported complete")
}
if hf.Body() != nil {
t.Error("body of incomplete message must be nil")
}
}
func TestHandshakeFrameRoundTripLength(t *testing.T) {
buf := make([]byte, tls.SizeHeaderHandshake)
hf, err := tls.NewHandshakeFrame(buf)
if err != nil {
t.Fatal(err)
}
for _, n := range []int32{0, 1, 255, 256, 65535, 65536, 0xffffff} {
hf.SetLength(n)
if got := hf.Length(); got != n {
t.Errorf("SetLength(%d) round-tripped to %d", n, got)
}
}
}
func TestHandshakeFrameRawDataIncludesHeader(t *testing.T) {
// The transcript hash covers the header plus body, exactly once per
// message. Getting this wrong breaks Finished verification.
msg := []byte{20, 0, 0, 2, 0xaa, 0xbb}
hf, err := tls.NewHandshakeFrame(msg)
if err != nil {
t.Fatal(err)
}
if got := hf.RawData(); len(got) != 6 {
t.Errorf("RawData len got %d want 6", len(got))
}
if got := hf.Body(); len(got) != 2 || got[0] != 0xaa {
t.Errorf("Body got % x", got)
}
}
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 {
v := uint16(i)<<12 | 0x0a00 | uint16(i)<<4 | 0x0a
if !tls.IsGREASE(v) {
t.Errorf("%#04x not detected as GREASE", v)
}
}
for _, v := range []uint16{
0x0000, 0x1301, 0x001d, 0x0403, 0x0a0b, 0x0b0a, 0x1a2a, 0xffff,
} {
if tls.IsGREASE(v) {
t.Errorf("%#04x falsely detected as GREASE", v)
}
}
}
func FuzzNewRecordFrame(f *testing.F) {
f.Add([]byte{22, 3, 1, 0, 5, 1, 2, 3, 4, 5})
f.Add([]byte{23, 3, 3, 0xff, 0xff})
f.Fuzz(func(t *testing.T, b []byte) {
rf, err := tls.NewRecordFrame(b)
if err != nil {
return
}
var v lneto.Validator
rf.ValidateSize(&v)
// Accessors must stay in bounds regardless of validation outcome.
_ = rf.ContentType()
_ = rf.LegacyVersion()
_ = rf.Length()
if p := rf.Payload(); p != nil && len(p) != int(rf.Length()) {
t.Fatalf("payload len %d != declared %d", len(p), rf.Length())
}
if raw := rf.RawData(); len(raw) > len(b) {
t.Fatalf("RawData %d longer than input %d", len(raw), len(b))
}
})
}
func FuzzNewHandshakeFrame(f *testing.F) {
f.Add([]byte{1, 0, 0, 2, 3, 4})
f.Add([]byte{20, 0xff, 0xff, 0xff})
f.Fuzz(func(t *testing.T, b []byte) {
hf, err := tls.NewHandshakeFrame(b)
if err != nil {
return
}
var v lneto.Validator
hf.ValidateSize(&v)
if hf.Length() < 0 {
t.Fatalf("negative 24-bit length %d", hf.Length())
}
if body := hf.Body(); body != nil && len(body) != int(hf.Length()) {
t.Fatalf("body len %d != declared %d", len(body), hf.Length())
}
if raw := hf.RawData(); len(raw) > len(b) {
t.Fatalf("RawData %d longer than input %d", len(raw), len(b))
}
})
}
func FuzzNewInnerPlaintext(f *testing.F) {
f.Add([]byte{1, 2, 23, 0, 0})
f.Add([]byte{0, 0, 0})
f.Fuzz(func(t *testing.T, b []byte) {
ip, err := tls.NewInnerPlaintext(b)
if err != nil {
return
}
if ip.ContentType() == 0 {
t.Fatal("accepted a zero content type")
}
if len(ip.Content())+ip.PaddingLen()+1 != len(b) {
t.Fatalf("content %d + padding %d + 1 != input %d",
len(ip.Content()), ip.PaddingLen(), len(b))
}
})
}
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))
}
})
}
+266
View File
@@ -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
}
+289
View File
@@ -0,0 +1,289 @@
package tls_test
import (
"crypto/tls"
"errors"
"net"
"testing"
"time"
"github.com/soypat/lneto"
ltls "github.com/soypat/lneto/x/tls"
)
// captureClientHello drives a standard library TLS client far enough to emit
// its first flight and returns the ClientHello handshake message body.
//
// Using a real client rather than a hand-written fixture means the parser is
// 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 {
t.Helper()
client, server := net.Pipe()
defer client.Close()
defer server.Close()
go func() {
c := tls.Client(client, &tls.Config{
ServerName: "example.com",
MinVersion: tls.VersionTLS13,
MaxVersion: tls.VersionTLS13,
NextProtos: []string{"h2", "http/1.1"},
})
_ = c.Handshake() // will fail; we only need the first flight
}()
server.SetReadDeadline(time.Now().Add(10 * time.Second))
var buf [4096]byte
n, err := server.Read(buf[:])
if err != nil {
t.Fatalf("reading ClientHello: %v", err)
}
rec, err := ltls.NewRecordFrame(buf[:n])
if err != nil {
t.Fatalf("record: %v", err)
}
if rec.ContentType() != ltls.ContentTypeHandshake {
t.Fatalf("first record is %v, want handshake", rec.ContentType())
}
if !rec.Complete() {
t.Fatalf("ClientHello record split across reads: have %d want %d",
n, rec.RecordLength())
}
hs, err := ltls.NewHandshakeFrame(rec.Payload())
if err != nil {
t.Fatalf("handshake: %v", err)
}
if hs.MsgType() != ltls.HandshakeTypeClientHello {
t.Fatalf("first message is %v, want client_hello", hs.MsgType())
}
if !hs.Complete() {
t.Fatal("ClientHello spans multiple records")
}
ch, err := ltls.NewClientHelloFrame(hs.Body())
if err != nil {
t.Fatalf("client hello: %v", err)
}
return ch
}
func TestClientHelloParseRealHello(t *testing.T) {
ch := captureClientHello(t)
if ch.LegacyVersion() != ltls.VersionTLS12 {
t.Errorf("legacy_version %#04x want 0x0303", ch.LegacyVersion())
}
if !ch.ValidateCompression() {
t.Errorf("legacy_compression_methods % x, want exactly {0}",
ch.LegacyCompressionMethods())
}
if n := len(ch.LegacySessionID()); n != 32 {
// A TLS 1.3 client sends a fake 32-byte session ID to trigger
// middlebox compatibility mode. The server must echo it.
t.Errorf("session id len %d, want 32 for middlebox compat", n)
}
var sawTLS13, sawX25519, sawSNI, sawALPN bool
var suites []ltls.CipherSuite
err := ch.ForEachExtension(func(ext ltls.ExtensionType, data []byte) error {
switch ext {
case ltls.ExtSupportedVersions:
return ltls.ForEachSupportedVersion(data, func(v uint16) error {
if v == ltls.VersionTLS13 {
sawTLS13 = true
}
return nil
})
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
})
case ltls.ExtServerName:
return ltls.ForEachServerName(data, func(nameType uint8, name []byte) error {
if nameType == 0 && string(name) == "example.com" {
sawSNI = true
}
return nil
})
case ltls.ExtALPN:
return ltls.ForEachALPNProto(data, func(p []byte) error {
if string(p) == "http/1.1" {
sawALPN = true
}
return nil
})
}
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)
}
if !sawTLS13 {
t.Error("supported_versions did not offer TLS 1.3")
}
if !sawX25519 {
t.Error("no 32-byte x25519 key share found")
}
if !sawSNI {
t.Error("SNI host not recovered")
}
if !sawALPN {
t.Error("ALPN http/1.1 not recovered")
}
var mandatory bool
for _, s := range suites {
if s == ltls.SuiteAES128GCMSHA256 {
mandatory = true
}
}
if !mandatory {
t.Errorf("TLS_AES_128_GCM_SHA256 not offered; got %v", suites)
}
}
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 {
t.Helper()
var b ltls.Builder
b.Reset(make([]byte, 0, len(ch.RawData())+len(exts)+64))
b.AddU16(ch.LegacyVersion())
b.AddBytes(ch.Random()[:])
b.OpenU8()
b.AddBytes(ch.LegacySessionID())
b.Close()
b.OpenU16()
b.AddBytes(ch.CipherSuites())
b.Close()
b.OpenU8()
b.AddBytes(ch.LegacyCompressionMethods())
b.Close()
b.OpenU16()
b.AddBytes(exts)
b.Close()
out, err := b.Bytes()
if err != nil {
t.Fatalf("rebuilding hello: %v", err)
}
return out
}
func TestClientHelloRejectsOversizeSessionID(t *testing.T) {
// legacy_session_id feeds a fixed [32]byte echo buffer in the server, so
// the bound must be enforced at parse time.
body := make([]byte, 0, 128)
body = append(body, 0x03, 0x03)
body = append(body, make([]byte, ltls.SizeRandom)...)
body = append(body, 33) // session id length, one over
body = append(body, make([]byte, 33)...) //
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)
if !errors.Is(err, lneto.ErrInvalidLengthField) {
t.Errorf("got %v want ErrInvalidLengthField", err)
}
}
func TestClientHelloRejectsTrailingBytes(t *testing.T) {
ch := captureClientHello(t)
body := append(append([]byte{}, ch.RawData()...), 0xff)
if _, err := ltls.NewClientHelloFrame(body); err == nil {
t.Error("trailing byte after extensions block accepted")
}
}
func TestClientHelloTruncatedAtEveryOffset(t *testing.T) {
// Truncating a valid hello anywhere must produce an error, never a panic
// and never a frame whose accessors read out of bounds.
ch := captureClientHello(t)
full := ch.RawData()
for n := range len(full) {
frame, err := ltls.NewClientHelloFrame(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()
}
}
}
func FuzzNewClientHelloFrame(f *testing.F) {
f.Add([]byte{
0x03, 0x03,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0x00, // no session id
0x00, 0x02, 0x13, 0x01, // one cipher suite
0x01, 0x00, // null compression
0x00, 0x00, // no extensions
})
f.Fuzz(func(t *testing.T, b []byte) {
ch, err := ltls.NewClientHelloFrame(b)
if err != nil {
return
}
// Every accessor must stay inside the input.
if len(ch.LegacySessionID()) > ltls.MaxSessionIDLen {
t.Fatalf("session id %d bytes exceeds max", len(ch.LegacySessionID()))
}
if len(ch.CipherSuites())%2 != 0 {
t.Fatal("cipher suites vector has odd length")
}
total := 2 + ltls.SizeRandom + 1 + len(ch.LegacySessionID()) +
2 + len(ch.CipherSuites()) +
1 + len(ch.LegacyCompressionMethods()) +
2 + len(ch.Extensions())
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 })
})
}
+3 -28
View File
@@ -69,8 +69,6 @@ type StackAsync struct {
ipv6enabled bool
stack6 Stack6
log *slog.Logger
}
type StackConfig struct {
@@ -107,9 +105,6 @@ type StackConfig struct {
AcceptMulticast bool
// Accept broadcast IPv4 packets. Needed for managing access points and DHCPv4 servers.
AcceptIPv4Broadcast bool
// Logger receives the stack's Debug and DebugErr output. A nil Logger silences
// them; the heap allocation probe still runs so allocation bisection keeps working.
Logger *slog.Logger
}
func (cfg *StackConfig) id() uint16 {
@@ -207,7 +202,6 @@ func (s *StackAsync) Reset(cfg StackConfig) (err error) {
defer s.mu.Unlock()
s.prng = uint32(cfg.RandSeed)
s.hostname = cfg.Hostname
s.log = cfg.Logger
// Treat last character of hostname as number.
id := cfg.id()
linkNodes := 2 // ARP and IPv4 nodes
@@ -849,39 +843,20 @@ func addr4(addr [4]byte, ok bool) netip.Addr {
}
// Debug prints debugging and heap information.
//
// The heap allocation probe runs unconditionally; only the log line is gated on
// the configured Logger's level. Building the slog.Attr list allocates, so the
// gate must come first or the allocation happens even when nothing is logged.
func (s *StackAsync) Debug(msg string) {
internal.LogAllocs(msg)
if !internal.LogEnabled(s.log, slog.LevelDebug) {
return
}
internal.LogAttrsAndAllocs(msg, s.log, slog.LevelDebug, "stackasync",
internal.LogAttrsAndAllocs(msg, slog.Default(), slog.LevelDebug, "stackasync",
slog.String("umsg", msg),
slog.Uint64("sent", s.stats.TotalSent),
slog.Uint64("recv", s.stats.TotalReceived),
)
}
// DebugErr prints debugging and heap information with [slog.LevelError] level. See [StackAsync.Debug] on gating.
// DebugErr prints debugging and heap information.
func (s *StackAsync) DebugErr(msg, err string) {
internal.LogAllocs(msg)
if !internal.LogEnabled(s.log, slog.LevelError) {
return
}
internal.LogAttrsAndAllocs(msg, s.log, slog.LevelError, "stackasync",
internal.LogAttrsAndAllocs(msg, slog.Default(), slog.LevelError, "stackasync",
slog.String("umsg", msg),
slog.String("err", err),
slog.Uint64("sent", s.stats.TotalSent),
slog.Uint64("recv", s.stats.TotalReceived),
)
}
// LogAllocs is an lneto-tracked allocation logger. If there was an allocation between this call and a previous call
// to LogAllocs it will be printed. This is called globally by StackAsync.Debug methods and by all logging calls in lneto
// when build tag debugheaplog is set.
func LogAllocs(msg string) {
internal.LogAllocs(msg)
}