mirror of
https://github.com/soypat/lneto.git
synced 2026-07-29 20:18:46 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9758d48696 |
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user