Files
lneto/x/tls/builder.go
T
Patricio Whittingslow 9758d48696 clanker tls
2026-07-24 20:56:52 -03:00

215 lines
5.7 KiB
Go

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)
}
}