mirror of
https://github.com/soypat/lneto.git
synced 2026-08-18 13:44:02 +00:00
486 lines
15 KiB
Go
486 lines
15 KiB
Go
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
|
|
}
|