mirror of
https://github.com/soypat/lneto.git
synced 2026-08-11 18:33:46 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed86ad0d0e | |||
| 91c646c636 | |||
| 9758d48696 |
@@ -52,11 +52,12 @@ type PacketBreakdown struct {
|
|||||||
// 0: Ethernet (3 base + VLAN tag = 4)
|
// 0: Ethernet (3 base + VLAN tag = 4)
|
||||||
// 1: L3 max(IPv4=12, ARP=9, IPv6=8) = 12
|
// 1: L3 max(IPv4=12, ARP=9, IPv6=8) = 12
|
||||||
// 2: L4 max(TCP=10, ICMP=8, UDP=4) = 10
|
// 2: L4 max(TCP=10, ICMP=8, UDP=4) = 10
|
||||||
// 3: App max(DHCP=15, NTP=13, HTTP=2, DNS=1) = 16
|
// 3: App max(DHCP=15, NTP=13, HTTP=2, DNS=1, TLS record=4) = 16
|
||||||
// 4-5: overflow/remaining = 2
|
// 4: TLS handshake message (ClientHello=8) = 8
|
||||||
|
// 5-7: extra TLS records/overflow/remaining = 4,2,2
|
||||||
func (pc *PacketBreakdown) initFrames() []Frame {
|
func (pc *PacketBreakdown) initFrames() []Frame {
|
||||||
const nframes = 6
|
const nframes = 8
|
||||||
var fieldCaps = [nframes]int{4, 12, 10, 16, 2, 2}
|
var fieldCaps = [nframes]int{4, 12, 10, 16, 8, 4, 2, 2}
|
||||||
frames := make([]Frame, nframes)
|
frames := make([]Frame, nframes)
|
||||||
for i := range frames {
|
for i := range frames {
|
||||||
frames[i].Fields = make([]FrameField, 0, fieldCaps[i])
|
frames[i].Fields = make([]FrameField, 0, fieldCaps[i])
|
||||||
@@ -328,9 +329,18 @@ func (pc *PacketBreakdown) CaptureTCP(dst []Frame, pkt []byte, bitOffset int) ([
|
|||||||
}
|
}
|
||||||
payload := tfrm.Payload()
|
payload := tfrm.Payload()
|
||||||
if len(payload) > 0 {
|
if len(payload) > 0 {
|
||||||
debuglog("pcap:tcp:http-start")
|
// Application protocol is picked by inspecting the payload rather than
|
||||||
dst, err = pc.CaptureHTTP(dst, pkt, end)
|
// by port, so that TLS on a port other than 443 and HTTP on a port
|
||||||
debuglog("pcap:tcp:http-done")
|
// other than 80 are both broken down correctly.
|
||||||
|
if payloadIsTLS(payload) {
|
||||||
|
debuglog("pcap:tcp:tls-start")
|
||||||
|
dst, err = pc.CaptureTLS(dst, pkt, end)
|
||||||
|
debuglog("pcap:tcp:tls-done")
|
||||||
|
} else {
|
||||||
|
debuglog("pcap:tcp:http-start")
|
||||||
|
dst, err = pc.CaptureHTTP(dst, pkt, end)
|
||||||
|
debuglog("pcap:tcp:http-done")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
|
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, end, octet*len(pkt))
|
||||||
}
|
}
|
||||||
@@ -835,10 +845,13 @@ const (
|
|||||||
// FlagContainer is used for [FrameField]s whose SubFields represent
|
// FlagContainer is used for [FrameField]s whose SubFields represent
|
||||||
// the entirety of the FrameField's data. i.e: DNS Questions/Answers.
|
// the entirety of the FrameField's data. i.e: DNS Questions/Answers.
|
||||||
FlagContainer
|
FlagContainer
|
||||||
|
// FlagEncrypted marks fields whose bytes are ciphertext. i.e: TLS application_data fragment.
|
||||||
|
FlagEncrypted
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
|
func (ff Flags) IsLegacy() bool { return ff&FlagLegacy != 0 }
|
||||||
func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
|
func (ff Flags) IsRightAligned() bool { return ff&FlagRightAligned != 0 }
|
||||||
|
func (ff Flags) IsEncrypted() bool { return ff&FlagEncrypted != 0 }
|
||||||
|
|
||||||
type Frame struct {
|
type Frame struct {
|
||||||
PacketBitOffset int
|
PacketBitOffset int
|
||||||
@@ -1009,6 +1022,9 @@ func (frm Frame) LenBits() (totalBitlen int) {
|
|||||||
|
|
||||||
func (ff FrameField) String() string {
|
func (ff FrameField) String() string {
|
||||||
if ff.Class == FieldClassPayload {
|
if ff.Class == FieldClassPayload {
|
||||||
|
if ff.Flags.IsEncrypted() {
|
||||||
|
return "Encrypted len=" + strconv.Itoa(ff.BitLength/8)
|
||||||
|
}
|
||||||
return "Payload len=" + strconv.Itoa(ff.BitLength/8)
|
return "Payload len=" + strconv.Itoa(ff.BitLength/8)
|
||||||
}
|
}
|
||||||
if ff.Name != "" {
|
if ff.Name != "" {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/soypat/lneto/dns"
|
"github.com/soypat/lneto/dns"
|
||||||
"github.com/soypat/lneto/ethernet"
|
"github.com/soypat/lneto/ethernet"
|
||||||
"github.com/soypat/lneto/ipv4"
|
"github.com/soypat/lneto/ipv4"
|
||||||
|
"github.com/soypat/lneto/tcp"
|
||||||
"github.com/soypat/lneto/udp"
|
"github.com/soypat/lneto/udp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,6 +107,39 @@ func buildDNSPacket(b testing.TB) []byte {
|
|||||||
return pkt
|
return pkt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildTLSPacket builds an Ethernet+IPv4+TCP packet carrying a real
|
||||||
|
// ClientHello record, exercising the string-heavy TLS path: cipher suite and
|
||||||
|
// extension subfields, SNI and ALPN text.
|
||||||
|
func buildTLSPacket(b testing.TB) []byte {
|
||||||
|
const (
|
||||||
|
ethSize = 14
|
||||||
|
ipv4Size = 20
|
||||||
|
tcpSize = 20
|
||||||
|
)
|
||||||
|
hello := captureClientHelloRecord(b, "example.com", []string{"h2", "http/1.1"})
|
||||||
|
pkt := make([]byte, ethSize+ipv4Size+tcpSize+len(hello))
|
||||||
|
|
||||||
|
efrm, _ := ethernet.NewFrame(pkt)
|
||||||
|
*efrm.DestinationHardwareAddr() = [6]byte{0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe}
|
||||||
|
*efrm.SourceHardwareAddr() = [6]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
efrm.SetEtherType(ethernet.TypeIPv4)
|
||||||
|
|
||||||
|
ifrm, _ := ipv4.NewFrame(pkt[ethSize:])
|
||||||
|
ifrm.SetVersionAndIHL(4, 5)
|
||||||
|
ifrm.SetID(0x1234)
|
||||||
|
ifrm.SetTTL(64)
|
||||||
|
ifrm.SetProtocol(lneto.IPProtoTCP)
|
||||||
|
ifrm.SetTotalLength(uint16(ipv4Size + tcpSize + len(hello)))
|
||||||
|
|
||||||
|
tfrm, _ := tcp.NewFrame(pkt[ethSize+ipv4Size:])
|
||||||
|
tfrm.SetSourcePort(51000)
|
||||||
|
tfrm.SetDestinationPort(443)
|
||||||
|
tfrm.SetOffsetAndFlags(5, tcp.FlagPSH|tcp.FlagACK)
|
||||||
|
|
||||||
|
copy(pkt[ethSize+ipv4Size+tcpSize:], hello)
|
||||||
|
return pkt
|
||||||
|
}
|
||||||
|
|
||||||
func configureBenchFormatter(f *Formatter) {
|
func configureBenchFormatter(f *Formatter) {
|
||||||
f.SubfieldLimit = benchSubfieldLimit
|
f.SubfieldLimit = benchSubfieldLimit
|
||||||
f.FrameSep = "\n"
|
f.FrameSep = "\n"
|
||||||
@@ -123,6 +157,7 @@ func BenchmarkPcap(b *testing.B) {
|
|||||||
}{
|
}{
|
||||||
{"DHCP", buildDHCPPacket(b)},
|
{"DHCP", buildDHCPPacket(b)},
|
||||||
{"DNS", buildDNSPacket(b)},
|
{"DNS", buildDNSPacket(b)},
|
||||||
|
{"TLS", buildTLSPacket(b)},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
b.Run(tc.name, func(b *testing.B) {
|
b.Run(tc.name, func(b *testing.B) {
|
||||||
@@ -182,6 +217,7 @@ func BenchmarkPcapPhases(b *testing.B) {
|
|||||||
}{
|
}{
|
||||||
{"DHCP", buildDHCPPacket(b)},
|
{"DHCP", buildDHCPPacket(b)},
|
||||||
{"DNS", buildDNSPacket(b)},
|
{"DNS", buildDNSPacket(b)},
|
||||||
|
{"TLS", buildTLSPacket(b)},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
b.Run(tc.name, func(b *testing.B) {
|
b.Run(tc.name, func(b *testing.B) {
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
package pcap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
|
"github.com/soypat/lneto/x/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxTLSRecordsPerPacket bounds how many record frames a single packet may
|
||||||
|
// produce. A TCP segment commonly coalesces several small records, but without
|
||||||
|
// a bound a segment full of 5-byte empty records would produce thousands of
|
||||||
|
// frames.
|
||||||
|
const maxTLSRecordsPerPacket = 8
|
||||||
|
|
||||||
|
// payloadIsTLS reports whether payload begins with what could be a TLS record
|
||||||
|
// header. TLS has no magic number, so this is a heuristic: the content type
|
||||||
|
// must be one of the four TLS 1.3 defines, the legacy record version must be
|
||||||
|
// 0x03xx and the declared fragment length must be legal.
|
||||||
|
//
|
||||||
|
// It is what lets TLS be captured on any port instead of only on 443, and it
|
||||||
|
// does not collide with HTTP, whose first byte is a method or version letter
|
||||||
|
// and never a valid content type.
|
||||||
|
func payloadIsTLS(payload []byte) bool {
|
||||||
|
if len(payload) < tls.SizeHeaderRecord {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch tls.ContentType(payload[0]) {
|
||||||
|
case tls.ContentTypeChangeCipherSpec, tls.ContentTypeAlert,
|
||||||
|
tls.ContentTypeHandshake, tls.ContentTypeApplicationData:
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if payload[1] != 0x03 || payload[2] > 0x04 {
|
||||||
|
// Every record version in use is 3.x: TLS 1.0 through 1.3 inclusive.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := int(binary.BigEndian.Uint16(payload[3:5]))
|
||||||
|
return n > 0 && n <= tls.MaxCiphertext
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureTLS breaks down the TLS records starting at bitOffset. A single entry
|
||||||
|
// point handles every kind of TLS traffic: a record names its own content type,
|
||||||
|
// so the caller does not need to know whether it is looking at a handshake, an
|
||||||
|
// alert or application data, which is what makes capturing a whole port 443
|
||||||
|
// conversation possible without tracking connection state.
|
||||||
|
//
|
||||||
|
// One [Frame] is produced per record, plus one more per cleartext handshake
|
||||||
|
// message carried inside a handshake record. Everything after the ServerHello
|
||||||
|
// is encrypted and appears on the wire as application_data; its fragment is
|
||||||
|
// reported as an opaque payload, since decrypting it needs keys a capture does
|
||||||
|
// not have.
|
||||||
|
//
|
||||||
|
// TLS is a byte stream: a record may span TCP segments and a handshake message
|
||||||
|
// may span records. Whatever arrived is reported and the affected frame carries
|
||||||
|
// [tls.ErrNeedMore]; reassembly is out of scope for a stateless breakdown.
|
||||||
|
func (pc *PacketBreakdown) CaptureTLS(dst []Frame, pkt []byte, bitOffset int) ([]Frame, error) {
|
||||||
|
debuglog("pcap:tls:start")
|
||||||
|
if bitOffset%8 != 0 {
|
||||||
|
return dst, errNotByteAligned
|
||||||
|
}
|
||||||
|
off := bitOffset / 8
|
||||||
|
for nrec := 0; off < len(pkt); nrec++ {
|
||||||
|
if nrec == maxTLSRecordsPerPacket {
|
||||||
|
reclaimRemainingFrame(&dst, "TLS records?", FieldClassPayload, off*octet, octet*len(pkt))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
newdst, consumed, err := pc.captureTLSRecord(dst, pkt, off*octet)
|
||||||
|
dst = newdst
|
||||||
|
if err != nil {
|
||||||
|
if nrec == 0 {
|
||||||
|
// Not TLS after all; let the caller frame the payload.
|
||||||
|
return dst, err
|
||||||
|
}
|
||||||
|
// Bytes trailing the last complete record that are too few or too
|
||||||
|
// malformed to be a record header of their own.
|
||||||
|
reclaimRemainingFrame(&dst, unknownPayloadProto, FieldClassPayload, off*octet, octet*len(pkt))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
off += consumed
|
||||||
|
}
|
||||||
|
debuglog("pcap:tls:done")
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureTLSRecord appends the frames of the single record at bitOffset and
|
||||||
|
// returns how many bytes of pkt the record occupied.
|
||||||
|
func (pc *PacketBreakdown) captureTLSRecord(dst []Frame, pkt []byte, bitOffset int) ([]Frame, int, error) {
|
||||||
|
rec := pkt[bitOffset/8:]
|
||||||
|
rfrm, err := tls.NewRecordFrame(rec)
|
||||||
|
if err != nil {
|
||||||
|
return dst, 0, err
|
||||||
|
}
|
||||||
|
rfrm.ValidateSize(pc.validator())
|
||||||
|
if pc.validator().HasError() {
|
||||||
|
return dst, 0, pc.validator().ErrPop()
|
||||||
|
}
|
||||||
|
debuglog("pcap:tls:validated")
|
||||||
|
const fragOff = tls.SizeHeaderRecord * octet
|
||||||
|
ctype := rfrm.ContentType()
|
||||||
|
finfo := reclaimFrame(&dst, "TLS", bitOffset, baseTLSRecordFields[:])
|
||||||
|
finfo.Fields[0].Name = ctype.StringConst()
|
||||||
|
frag := rfrm.Payload()
|
||||||
|
if frag == nil {
|
||||||
|
// Fragment continues in a later segment. Report what arrived.
|
||||||
|
avail := len(rec) - tls.SizeHeaderRecord
|
||||||
|
finfo.Errors = append(finfo.Errors, tls.ErrNeedMore)
|
||||||
|
if avail > 0 {
|
||||||
|
var flags Flags
|
||||||
|
if ctype == tls.ContentTypeApplicationData {
|
||||||
|
flags = FlagEncrypted
|
||||||
|
}
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Class: FieldClassPayload,
|
||||||
|
FrameBitOffset: fragOff,
|
||||||
|
BitLength: avail * octet,
|
||||||
|
Flags: flags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dst, len(rec), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ctype {
|
||||||
|
case tls.ContentTypeHandshake:
|
||||||
|
// Handshake messages get frames of their own. finfo must not be touched
|
||||||
|
// past this point: appending to dst may move the Frame it points at.
|
||||||
|
dst = pc.captureTLSHandshake(dst, pkt, bitOffset+fragOff, len(frag))
|
||||||
|
|
||||||
|
case tls.ContentTypeAlert:
|
||||||
|
if len(frag) < 2 {
|
||||||
|
finfo.Errors = append(finfo.Errors, lneto.ErrTruncatedFrame)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// The level byte is advisory only: in TLS 1.3 every alert except
|
||||||
|
// close_notify and user_canceled is fatal whatever it says.
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Name: tls.AlertLevel(frag[0]).StringConst(),
|
||||||
|
Class: FieldClassType,
|
||||||
|
FrameBitOffset: fragOff,
|
||||||
|
BitLength: octet,
|
||||||
|
Flags: FlagLegacy,
|
||||||
|
}, FrameField{
|
||||||
|
Name: tls.AlertDescription(frag[1]).StringConst(),
|
||||||
|
Class: FieldClassType,
|
||||||
|
FrameBitOffset: fragOff + octet,
|
||||||
|
BitLength: octet,
|
||||||
|
})
|
||||||
|
|
||||||
|
case tls.ContentTypeApplicationData:
|
||||||
|
// Either genuine application data or a protected handshake or alert
|
||||||
|
// record; which of the three is only knowable after decryption.
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Class: FieldClassPayload,
|
||||||
|
FrameBitOffset: fragOff,
|
||||||
|
BitLength: len(frag) * octet,
|
||||||
|
Flags: FlagEncrypted,
|
||||||
|
})
|
||||||
|
|
||||||
|
default: // change_cipher_spec and unrecognized content types.
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Class: FieldClassPayload,
|
||||||
|
FrameBitOffset: fragOff,
|
||||||
|
BitLength: len(frag) * octet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dst, rfrm.RecordLength(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureTLSHandshake appends one frame per handshake message found in the
|
||||||
|
// fragLen bytes of handshake record fragment starting at bitOffset.
|
||||||
|
func (pc *PacketBreakdown) captureTLSHandshake(dst []Frame, pkt []byte, bitOffset, fragLen int) []Frame {
|
||||||
|
debuglog("pcap:tls:hs-start")
|
||||||
|
const hdr = tls.SizeHeaderHandshake
|
||||||
|
frag := pkt[bitOffset/8:][:fragLen]
|
||||||
|
fragEnd := bitOffset + fragLen*octet
|
||||||
|
for off := 0; off < fragLen; {
|
||||||
|
msgBitOff := bitOffset + off*octet
|
||||||
|
hfrm, err := tls.NewHandshakeFrame(frag[off:])
|
||||||
|
if err != nil {
|
||||||
|
// Fewer than 4 bytes left: a message header split across records.
|
||||||
|
reclaimRemainingFrame(&dst, "TLS Handshake?", FieldClassPayload, msgBitOff, fragEnd)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
mtype := hfrm.MsgType()
|
||||||
|
finfo := reclaimFrame(&dst, tlsHandshakeProto(mtype), msgBitOff, baseTLSHandshakeFields[:])
|
||||||
|
finfo.Fields[0].Name = mtype.StringConst()
|
||||||
|
body := hfrm.Body()
|
||||||
|
if body == nil {
|
||||||
|
// Message body continues in the next record.
|
||||||
|
finfo.Errors = append(finfo.Errors, tls.ErrNeedMore)
|
||||||
|
if avail := fragLen - off - hdr; avail > 0 {
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Class: FieldClassPayload,
|
||||||
|
FrameBitOffset: hdr * octet,
|
||||||
|
BitLength: avail * octet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
switch mtype {
|
||||||
|
case tls.HandshakeTypeClientHello:
|
||||||
|
pc.captureTLSClientHello(finfo, body)
|
||||||
|
case tls.HandshakeTypeServerHello:
|
||||||
|
pc.captureTLSServerHello(finfo, body)
|
||||||
|
default:
|
||||||
|
if len(body) > 0 {
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Class: FieldClassPayload,
|
||||||
|
FrameBitOffset: hdr * octet,
|
||||||
|
BitLength: len(body) * octet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
off += hfrm.MessageLength()
|
||||||
|
}
|
||||||
|
debuglog("pcap:tls:hs-done")
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureTLSClientHello appends the fields of a ClientHello body to finfo.
|
||||||
|
// Field positions come from the parsed message, so the only arithmetic here is
|
||||||
|
// shifting past the handshake header.
|
||||||
|
func (pc *PacketBreakdown) captureTLSClientHello(finfo *Frame, body []byte) {
|
||||||
|
msg, err := tls.ParseClientHello(body)
|
||||||
|
if err != nil {
|
||||||
|
appendTLSHelloError(finfo, body, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sp := msg.Spans()
|
||||||
|
appendTLSHelloHead(finfo, sp)
|
||||||
|
pc.appendTLSCipherSuites(finfo, msg, sp.CipherSuites)
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Name: "compression methods",
|
||||||
|
Class: FieldClassOptions,
|
||||||
|
FrameBitOffset: helloBitOffset(sp.Compression.Off),
|
||||||
|
BitLength: sp.Compression.Len * octet,
|
||||||
|
Flags: FlagLegacy,
|
||||||
|
})
|
||||||
|
pc.appendTLSExtensions(finfo, msg.ExtensionList(), sp.Extensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// captureTLSServerHello appends the fields of a ServerHello body to finfo. It
|
||||||
|
// differs from the client's in naming one suite and one compression method
|
||||||
|
// where the client offers a list of each.
|
||||||
|
func (pc *PacketBreakdown) captureTLSServerHello(finfo *Frame, body []byte) {
|
||||||
|
msg, err := tls.ParseServerHello(body)
|
||||||
|
if err != nil {
|
||||||
|
appendTLSHelloError(finfo, body, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sp := msg.Spans()
|
||||||
|
appendTLSHelloHead(finfo, sp)
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Name: msg.CipherSuite().StringConst(),
|
||||||
|
Class: FieldClassType,
|
||||||
|
FrameBitOffset: helloBitOffset(sp.CipherSuites.Off),
|
||||||
|
BitLength: sp.CipherSuites.Len * octet,
|
||||||
|
}, FrameField{
|
||||||
|
Name: "compression method",
|
||||||
|
Class: FieldClassOptions,
|
||||||
|
FrameBitOffset: helloBitOffset(sp.Compression.Off),
|
||||||
|
BitLength: sp.Compression.Len * octet,
|
||||||
|
Flags: FlagLegacy,
|
||||||
|
})
|
||||||
|
pc.appendTLSExtensions(finfo, msg.ExtensionList(), sp.Extensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helloBitOffset converts an offset within a hello body to one within the
|
||||||
|
// handshake message frame.
|
||||||
|
func helloBitOffset(bodyOff int) int {
|
||||||
|
return (tls.SizeHeaderHandshake + bodyOff) * octet
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendTLSHelloHead appends the fields both hellos begin with.
|
||||||
|
func appendTLSHelloHead(finfo *Frame, sp tls.HelloSpans) {
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
// legacy_version. TLS 1.3 pins it to 0x0303 and carries the real version
|
||||||
|
// in supported_versions.
|
||||||
|
Class: FieldClassVersion,
|
||||||
|
FrameBitOffset: helloBitOffset(0),
|
||||||
|
BitLength: 2 * octet,
|
||||||
|
Flags: FlagLegacy,
|
||||||
|
}, FrameField{
|
||||||
|
Name: "Random",
|
||||||
|
Class: FieldClassID,
|
||||||
|
FrameBitOffset: helloBitOffset(sp.Random.Off),
|
||||||
|
BitLength: sp.Random.Len * octet,
|
||||||
|
})
|
||||||
|
if sp.SessionID.Len > 0 {
|
||||||
|
// TLS 1.3 has no resumption by session ID; a non-empty value means
|
||||||
|
// middlebox compatibility mode, echoed verbatim by the server.
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Name: "Session ID",
|
||||||
|
Class: FieldClassID,
|
||||||
|
FrameBitOffset: helloBitOffset(sp.SessionID.Off),
|
||||||
|
BitLength: sp.SessionID.Len * octet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendTLSHelloError reports a hello that did not parse. The parsers reject a
|
||||||
|
// whole message on any inconsistent length, which is what makes their iterators
|
||||||
|
// error-free, so there are no field offsets to show: the error and the raw bytes
|
||||||
|
// are all a capture can say.
|
||||||
|
func appendTLSHelloError(finfo *Frame, body []byte, err error) {
|
||||||
|
finfo.Errors = append(finfo.Errors, err)
|
||||||
|
finfo.Fields = append(finfo.Fields, FrameField{
|
||||||
|
Class: FieldClassPayload,
|
||||||
|
FrameBitOffset: helloBitOffset(0),
|
||||||
|
BitLength: len(body) * octet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendTLSCipherSuites appends a cipher_suites container field whose subfields
|
||||||
|
// name each offered suite. GREASE values show up as such, which is what a
|
||||||
|
// capture should display: they carry no meaning and are not an error.
|
||||||
|
func (pc *PacketBreakdown) appendTLSCipherSuites(finfo *Frame, msg tls.ClientHelloMsg, sp tls.Span) {
|
||||||
|
// Reclaim from the Fields backing array to reuse its SubFields backing array.
|
||||||
|
sfield := internal.SliceReclaim(&finfo.Fields)
|
||||||
|
*sfield = FrameField{
|
||||||
|
Name: "cipher suites",
|
||||||
|
Class: FieldClassOptions,
|
||||||
|
SubFields: sfield.SubFields[:0],
|
||||||
|
FrameBitOffset: helloBitOffset(sp.Off),
|
||||||
|
BitLength: sp.Len * octet,
|
||||||
|
}
|
||||||
|
if pc.SubfieldLimit <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for off, suite := range msg.CipherSuites {
|
||||||
|
if len(sfield.SubFields) >= pc.SubfieldLimit {
|
||||||
|
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sfield.SubFields = append(sfield.SubFields, FrameField{
|
||||||
|
Name: suite.StringConst(),
|
||||||
|
Class: FieldClassType,
|
||||||
|
FrameBitOffset: helloBitOffset(off),
|
||||||
|
BitLength: 2 * octet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendTLSExtensions appends an extensions container field whose subfields are
|
||||||
|
// the individual extensions.
|
||||||
|
func (pc *PacketBreakdown) appendTLSExtensions(finfo *Frame, exts tls.ExtensionList, sp tls.Span) {
|
||||||
|
extfield := internal.SliceReclaim(&finfo.Fields)
|
||||||
|
*extfield = FrameField{
|
||||||
|
Name: "extensions",
|
||||||
|
Class: FieldClassOptions,
|
||||||
|
SubFields: extfield.SubFields[:0],
|
||||||
|
FrameBitOffset: helloBitOffset(sp.Off),
|
||||||
|
BitLength: sp.Len * octet,
|
||||||
|
}
|
||||||
|
if pc.SubfieldLimit <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for off, ext := range exts.All {
|
||||||
|
if len(extfield.SubFields) >= pc.SubfieldLimit {
|
||||||
|
finfo.Errors = append(finfo.Errors, ErrLimitExceeded)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
extfield.SubFields = append(extfield.SubFields, tlsExtensionField(ext, off))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsExtensionField describes a single hello extension. Extensions carrying a
|
||||||
|
// human readable value point at that value, located by the extension's own
|
||||||
|
// iterators, and the bulky opaque ones are classed as payload so that a
|
||||||
|
// [Formatter.FilterClasses] can drop them without losing the rest of the hello.
|
||||||
|
func tlsExtensionField(ext tls.ExtensionFrame, dataOff int) FrameField {
|
||||||
|
field := FrameField{
|
||||||
|
Name: ext.Type().StringConst(),
|
||||||
|
Class: FieldClassOptions,
|
||||||
|
FrameBitOffset: helloBitOffset(dataOff),
|
||||||
|
BitLength: len(ext.Data()) * octet,
|
||||||
|
}
|
||||||
|
switch ext.Type() {
|
||||||
|
case tls.ExtServerName:
|
||||||
|
for off, name := range ext.ServerNames {
|
||||||
|
if name.Type != 0 {
|
||||||
|
continue // Only host_name is defined.
|
||||||
|
}
|
||||||
|
field.Class = FieldClassText
|
||||||
|
field.FrameBitOffset = helloBitOffset(off)
|
||||||
|
field.BitLength = len(name.Name) * octet
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case tls.ExtALPN:
|
||||||
|
// Span the first name through the last so every offered protocol stays
|
||||||
|
// visible; the length bytes between them show up as escapes.
|
||||||
|
first, end := -1, 0
|
||||||
|
for off, proto := range ext.ALPNProtos {
|
||||||
|
if first < 0 {
|
||||||
|
first = off
|
||||||
|
}
|
||||||
|
end = off + len(proto)
|
||||||
|
}
|
||||||
|
if first >= 0 {
|
||||||
|
field.Class = FieldClassText
|
||||||
|
field.FrameBitOffset = helloBitOffset(first)
|
||||||
|
field.BitLength = (end - first) * octet
|
||||||
|
}
|
||||||
|
|
||||||
|
case tls.ExtKeyShare, tls.ExtPreSharedKey, tls.ExtPadding, tls.ExtSessionTicket,
|
||||||
|
tls.ExtCookie, tls.ExtEncryptedClientHello, tls.ExtSignedCertificateTimestamp:
|
||||||
|
// Opaque and large: a post-quantum key share alone runs past 1kB.
|
||||||
|
field.Class = FieldClassPayload
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsHandshakeProto names the frame of a handshake message. Only the messages a
|
||||||
|
// capture can see in cleartext get a name of their own; the rest travel inside
|
||||||
|
// a protected record and never reach here undecrypted.
|
||||||
|
func tlsHandshakeProto(t tls.HandshakeType) string {
|
||||||
|
switch t {
|
||||||
|
case tls.HandshakeTypeClientHello:
|
||||||
|
return "TLS ClientHello"
|
||||||
|
case tls.HandshakeTypeServerHello:
|
||||||
|
return "TLS ServerHello"
|
||||||
|
}
|
||||||
|
return "TLS Handshake"
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseTLSRecordFields = [...]FrameField{
|
||||||
|
{
|
||||||
|
// Name is filled in with the content type's name by captureTLSRecord.
|
||||||
|
Class: FieldClassType,
|
||||||
|
FrameBitOffset: 0,
|
||||||
|
BitLength: 1 * octet,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// legacy_record_version, which TLS 1.3 receivers ignore entirely.
|
||||||
|
Class: FieldClassVersion,
|
||||||
|
FrameBitOffset: 1 * octet,
|
||||||
|
BitLength: 2 * octet,
|
||||||
|
Flags: FlagLegacy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Class: FieldClassSize,
|
||||||
|
FrameBitOffset: 3 * octet,
|
||||||
|
BitLength: 2 * octet,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseTLSHandshakeFields = [...]FrameField{
|
||||||
|
{
|
||||||
|
// Name is filled in with the message type's name by captureTLSHandshake.
|
||||||
|
Class: FieldClassType,
|
||||||
|
FrameBitOffset: 0,
|
||||||
|
BitLength: 1 * octet,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Class: FieldClassSize,
|
||||||
|
FrameBitOffset: 1 * octet,
|
||||||
|
BitLength: 3 * octet,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
package pcap
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdtls "crypto/tls"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto/ethernet"
|
||||||
|
"github.com/soypat/lneto/internal/ltesto"
|
||||||
|
"github.com/soypat/lneto/tcp"
|
||||||
|
"github.com/soypat/lneto/x/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// captureClientHelloRecord drives a standard library TLS client far enough to
|
||||||
|
// emit its first flight and returns the complete handshake record, header
|
||||||
|
// included. A real client gives realistic extension ordering, a 32-byte
|
||||||
|
// middlebox compatibility session ID and a post-quantum key share.
|
||||||
|
func captureClientHelloRecord(t testing.TB, serverName string, protos []string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
client, server := net.Pipe()
|
||||||
|
defer client.Close()
|
||||||
|
defer server.Close()
|
||||||
|
go func() {
|
||||||
|
c := stdtls.Client(client, &stdtls.Config{
|
||||||
|
ServerName: serverName,
|
||||||
|
MinVersion: stdtls.VersionTLS13,
|
||||||
|
MaxVersion: stdtls.VersionTLS13,
|
||||||
|
NextProtos: protos,
|
||||||
|
})
|
||||||
|
_ = c.Handshake() // Will fail; only the first flight is needed.
|
||||||
|
}()
|
||||||
|
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 := tls.NewRecordFrame(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !rec.Complete() {
|
||||||
|
t.Fatalf("ClientHello record split across reads: have %d want %d", n, rec.RecordLength())
|
||||||
|
}
|
||||||
|
return append([]byte{}, rec.RawData()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCaptureTLSClientHello runs a real ClientHello through the full
|
||||||
|
// Ethernet/IPv4/TCP path to check that TLS is detected by payload content
|
||||||
|
// rather than by port, and that the SNI hostname is recovered.
|
||||||
|
func TestCaptureTLSClientHello(t *testing.T) {
|
||||||
|
const mtu = ethernet.MaxMTU
|
||||||
|
const serverName = "example.com"
|
||||||
|
payload := captureClientHelloRecord(t, serverName, []string{"h2", "http/1.1"})
|
||||||
|
|
||||||
|
var buf [mtu]byte
|
||||||
|
var gen ltesto.PacketGen
|
||||||
|
rng := rand.New(rand.NewSource(1))
|
||||||
|
gen.RandomizeAddrs(rng)
|
||||||
|
pkt := gen.AppendRandomIPv4TCPPacket(buf[:0], rng, tcp.Segment{
|
||||||
|
SEQ: 100,
|
||||||
|
ACK: 200,
|
||||||
|
DATALEN: tcp.Size(len(payload)),
|
||||||
|
WND: 1024,
|
||||||
|
Flags: tcp.FlagPSH | tcp.FlagACK,
|
||||||
|
})
|
||||||
|
copy(pkt[len(pkt)-len(payload):], payload)
|
||||||
|
|
||||||
|
var pbreak PacketBreakdown
|
||||||
|
pbreak.SubfieldLimit = 32
|
||||||
|
frames, err := pbreak.CaptureEthernet(nil, pkt, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Ethernet+IPv4+TCP+TLS record+TLS ClientHello = 5 frames.
|
||||||
|
if len(frames) != 5 {
|
||||||
|
for i := range frames {
|
||||||
|
t.Logf("frame[%d]=%s", i, frames[i].String())
|
||||||
|
}
|
||||||
|
t.Fatalf("want 5 frames, got %d", len(frames))
|
||||||
|
}
|
||||||
|
if frames[3].Protocol != "TLS" {
|
||||||
|
t.Errorf("frame 3 protocol=%q want TLS", frames[3].Protocol)
|
||||||
|
}
|
||||||
|
if frames[4].Protocol != "TLS ClientHello" {
|
||||||
|
t.Errorf("frame 4 protocol=%q want TLS ClientHello", frames[4].Protocol)
|
||||||
|
}
|
||||||
|
if len(frames[3].Errors) > 0 || len(frames[4].Errors) > 0 {
|
||||||
|
t.Errorf("unexpected errors: %v %v", frames[3].Errors, frames[4].Errors)
|
||||||
|
}
|
||||||
|
// A handshake record frame describes the record header only; its fragment is
|
||||||
|
// broken down by the handshake frame that follows, so that no bytes are
|
||||||
|
// claimed by two frames at once.
|
||||||
|
if got := frames[3].LenBits() / 8; got != tls.SizeHeaderRecord {
|
||||||
|
t.Errorf("TLS record frame len=%d want %d", got, tls.SizeHeaderRecord)
|
||||||
|
}
|
||||||
|
if got := frames[4].LenBits() / 8; got != len(payload)-tls.SizeHeaderRecord {
|
||||||
|
t.Errorf("handshake frame len=%d want %d", got, len(payload)-tls.SizeHeaderRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctype := fieldByName(frames[3], "handshake")
|
||||||
|
if ctype == nil {
|
||||||
|
t.Fatal("no handshake content type field")
|
||||||
|
}
|
||||||
|
if v, _ := frames[3].FieldAsUint(indexOfField(frames[3], "handshake"), pkt); v != uint64(tls.ContentTypeHandshake) {
|
||||||
|
t.Errorf("content type=%d want %d", v, tls.ContentTypeHandshake)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SNI and ALPN live as subfields of the extensions container.
|
||||||
|
exts := fieldByName(frames[4], "extensions")
|
||||||
|
if exts == nil {
|
||||||
|
t.Fatal("no extensions field in ClientHello")
|
||||||
|
}
|
||||||
|
var sni, alpn *FrameField
|
||||||
|
for i := range exts.SubFields {
|
||||||
|
switch exts.SubFields[i].Name {
|
||||||
|
case tls.ExtServerName.String():
|
||||||
|
sni = &exts.SubFields[i]
|
||||||
|
case tls.ExtALPN.String():
|
||||||
|
alpn = &exts.SubFields[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sni == nil {
|
||||||
|
t.Fatal("no server_name extension field")
|
||||||
|
}
|
||||||
|
got := string(pkt[(frames[4].PacketBitOffset+sni.FrameBitOffset)/8:][:sni.BitLength/8])
|
||||||
|
if got != serverName {
|
||||||
|
t.Errorf("server_name=%q want %q", got, serverName)
|
||||||
|
}
|
||||||
|
if alpn == nil {
|
||||||
|
t.Error("no application_layer_protocol_negotiation extension field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatted output is the point of the exercise: the hostname and the
|
||||||
|
// negotiated suites must be readable in one line.
|
||||||
|
var f Formatter
|
||||||
|
f.SubfieldLimit = 32
|
||||||
|
out, err := f.FormatFrames(nil, frames, pkt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
str := string(out)
|
||||||
|
for _, want := range []string{"TLS", "handshake=", "client_hello=", serverName,
|
||||||
|
"TLS_AES_128_GCM_SHA256", "http/1.1"} {
|
||||||
|
if !strings.Contains(str, want) {
|
||||||
|
t.Errorf("formatted output missing %q:\n%s", want, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCaptureTLSRecordSequence checks the multi-record path: a server flight
|
||||||
|
// coalesces ServerHello, the compatibility ChangeCipherSpec and the first
|
||||||
|
// protected record into a single segment.
|
||||||
|
func TestCaptureTLSRecordSequence(t *testing.T) {
|
||||||
|
var b tls.Builder
|
||||||
|
var buf [512]byte
|
||||||
|
b.Reset(buf[:])
|
||||||
|
|
||||||
|
// ServerHello record.
|
||||||
|
b.AddU8(uint8(tls.ContentTypeHandshake))
|
||||||
|
b.AddU16(tls.VersionTLS12)
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddU8(uint8(tls.HandshakeTypeServerHello))
|
||||||
|
b.OpenU24()
|
||||||
|
b.AddU16(tls.VersionTLS12) // legacy_version
|
||||||
|
for range tls.SizeRandom {
|
||||||
|
b.AddU8(0xab) // server_random
|
||||||
|
}
|
||||||
|
b.OpenU8() // legacy_session_id echo
|
||||||
|
for range 32 {
|
||||||
|
b.AddU8(0xcd)
|
||||||
|
}
|
||||||
|
b.Close()
|
||||||
|
b.AddU16(uint16(tls.SuiteAES128GCMSHA256))
|
||||||
|
b.AddU8(0) // legacy_compression_method
|
||||||
|
b.OpenU16() // extensions
|
||||||
|
b.AddU16(uint16(tls.ExtSupportedVersions))
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddU16(tls.VersionTLS13)
|
||||||
|
b.Close()
|
||||||
|
b.AddU16(uint16(tls.ExtKeyShare))
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddU16(uint16(tls.GroupX25519))
|
||||||
|
b.OpenU16()
|
||||||
|
for range 32 {
|
||||||
|
b.AddU8(0xee)
|
||||||
|
}
|
||||||
|
b.Close()
|
||||||
|
b.Close()
|
||||||
|
b.Close() // extensions
|
||||||
|
b.Close() // handshake body
|
||||||
|
b.Close() // record fragment
|
||||||
|
|
||||||
|
// change_cipher_spec record.
|
||||||
|
b.AddU8(uint8(tls.ContentTypeChangeCipherSpec))
|
||||||
|
b.AddU16(tls.VersionTLS12)
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddU8(1)
|
||||||
|
b.Close()
|
||||||
|
|
||||||
|
// First protected record: outwardly application_data.
|
||||||
|
b.AddU8(uint8(tls.ContentTypeApplicationData))
|
||||||
|
b.AddU16(tls.VersionTLS12)
|
||||||
|
b.OpenU16()
|
||||||
|
for range 24 {
|
||||||
|
b.AddU8(0x5a)
|
||||||
|
}
|
||||||
|
b.Close()
|
||||||
|
|
||||||
|
pkt, err := b.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !payloadIsTLS(pkt) {
|
||||||
|
t.Fatal("payloadIsTLS did not recognize a ServerHello record")
|
||||||
|
}
|
||||||
|
|
||||||
|
var pbreak PacketBreakdown
|
||||||
|
pbreak.SubfieldLimit = 8
|
||||||
|
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []string{"TLS", "TLS ServerHello", "TLS", "TLS"}
|
||||||
|
if len(frames) != len(want) {
|
||||||
|
for i := range frames {
|
||||||
|
t.Logf("frame[%d]=%s", i, frames[i].String())
|
||||||
|
}
|
||||||
|
t.Fatalf("got %d frames want %d", len(frames), len(want))
|
||||||
|
}
|
||||||
|
for i, wantProto := range want {
|
||||||
|
if frames[i].Protocol != wantProto {
|
||||||
|
t.Errorf("frame[%d] protocol=%q want %q", i, frames[i].Protocol, wantProto)
|
||||||
|
}
|
||||||
|
if len(frames[i].Errors) > 0 {
|
||||||
|
t.Errorf("frame[%d] errors=%v", i, frames[i].Errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fieldByName(frames[1], tls.SuiteAES128GCMSHA256.String()) == nil {
|
||||||
|
t.Error("ServerHello cipher suite field not named after the selected suite")
|
||||||
|
}
|
||||||
|
i, err := frames[3].FieldByClass(FieldClassPayload)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("no payload field in application_data record:", err)
|
||||||
|
} else if !frames[3].Fields[i].Flags.IsEncrypted() {
|
||||||
|
t.Error("application_data fragment not flagged as encrypted")
|
||||||
|
}
|
||||||
|
// Record frames must start exactly where the previous record ended, and the
|
||||||
|
// last one must end at the packet end: no record skipped, none double read.
|
||||||
|
off := 0
|
||||||
|
for i := range frames {
|
||||||
|
if frames[i].Protocol != "TLS" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if frames[i].PacketBitOffset != off*octet {
|
||||||
|
t.Errorf("frame[%d] starts at bit %d want %d", i, frames[i].PacketBitOffset, off*octet)
|
||||||
|
}
|
||||||
|
rec, err := tls.NewRecordFrame(pkt[off:])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
off += rec.RecordLength()
|
||||||
|
}
|
||||||
|
if off != len(pkt) {
|
||||||
|
t.Errorf("records cover %d bytes of %d", off, len(pkt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCaptureTLSIncomplete checks the stream cases a stateless breakdown cannot
|
||||||
|
// reassemble: a record whose fragment continues in the next TCP segment, and a
|
||||||
|
// handshake message whose body continues in the next record.
|
||||||
|
func TestCaptureTLSIncomplete(t *testing.T) {
|
||||||
|
t.Run("record", func(t *testing.T) {
|
||||||
|
pkt := []byte{byte(tls.ContentTypeApplicationData), 0x03, 0x03, 0x04, 0x00, 1, 2, 3}
|
||||||
|
var pbreak PacketBreakdown
|
||||||
|
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(frames) != 1 {
|
||||||
|
t.Fatalf("got %d frames want 1", len(frames))
|
||||||
|
}
|
||||||
|
if len(frames[0].Errors) != 1 || frames[0].Errors[0] != tls.ErrNeedMore {
|
||||||
|
t.Errorf("errors=%v want %v", frames[0].Errors, tls.ErrNeedMore)
|
||||||
|
}
|
||||||
|
if got := frames[0].LenBits() / 8; got != len(pkt) {
|
||||||
|
t.Errorf("frame covers %d bytes want %d", got, len(pkt))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("handshake", func(t *testing.T) {
|
||||||
|
// A 5-byte record carrying a Certificate message header that declares a
|
||||||
|
// 1000-byte body: the rest arrives in later records.
|
||||||
|
pkt := []byte{
|
||||||
|
byte(tls.ContentTypeHandshake), 0x03, 0x03, 0x00, 0x06,
|
||||||
|
byte(tls.HandshakeTypeCertificate), 0x00, 0x03, 0xe8, 0xaa, 0xbb,
|
||||||
|
}
|
||||||
|
var pbreak PacketBreakdown
|
||||||
|
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(frames) != 2 {
|
||||||
|
t.Fatalf("got %d frames want 2", len(frames))
|
||||||
|
}
|
||||||
|
if frames[1].Protocol != "TLS Handshake" {
|
||||||
|
t.Errorf("protocol=%q want TLS Handshake", frames[1].Protocol)
|
||||||
|
}
|
||||||
|
if len(frames[1].Errors) != 1 || frames[1].Errors[0] != tls.ErrNeedMore {
|
||||||
|
t.Errorf("errors=%v want %v", frames[1].Errors, tls.ErrNeedMore)
|
||||||
|
}
|
||||||
|
if got := frames[1].LenBits() / 8; got != 6 {
|
||||||
|
t.Errorf("handshake frame covers %d bytes want 6", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCaptureTLSAlert checks the cleartext alert path, which is the only way a
|
||||||
|
// TLS 1.3 handshake failure is visible to a capture.
|
||||||
|
func TestCaptureTLSAlert(t *testing.T) {
|
||||||
|
pkt := []byte{
|
||||||
|
byte(tls.ContentTypeAlert), 0x03, 0x03, 0x00, 0x02,
|
||||||
|
byte(tls.AlertLevelFatal), byte(tls.AlertHandshakeFailure),
|
||||||
|
}
|
||||||
|
var pbreak PacketBreakdown
|
||||||
|
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(frames) != 1 {
|
||||||
|
t.Fatalf("got %d frames want 1", len(frames))
|
||||||
|
}
|
||||||
|
if fieldByName(frames[0], tls.AlertHandshakeFailure.String()) == nil {
|
||||||
|
t.Errorf("no field named %q in %s", tls.AlertHandshakeFailure.String(), frames[0].String())
|
||||||
|
}
|
||||||
|
var f Formatter
|
||||||
|
f.DisableLegacyFilter = true
|
||||||
|
out, err := f.FormatFrame(nil, frames[0], pkt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(out), "handshake_failure") {
|
||||||
|
t.Errorf("formatted alert missing description: %s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPayloadIsTLS guards the heuristic that routes a TCP payload to CaptureTLS
|
||||||
|
// against the HTTP traffic it shares the datapath with.
|
||||||
|
func TestPayloadIsTLS(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
payload []byte
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"client hello", []byte{22, 3, 1, 0, 10}, true},
|
||||||
|
{"app data", []byte{23, 3, 3, 0x40, 0x00}, true},
|
||||||
|
{"alert", []byte{21, 3, 3, 0, 2}, true},
|
||||||
|
{"http request", []byte("GET / HTTP/1.1\r\n"), false},
|
||||||
|
{"http response", []byte("HTTP/1.1 200 OK\r\n"), false},
|
||||||
|
{"bad content type", []byte{25, 3, 3, 0, 10}, false},
|
||||||
|
{"bad version", []byte{22, 2, 1, 0, 10}, false},
|
||||||
|
{"future version", []byte{22, 3, 5, 0, 10}, false},
|
||||||
|
{"zero length", []byte{22, 3, 3, 0, 0}, false},
|
||||||
|
{"oversize length", []byte{23, 3, 3, 0xff, 0xff}, false},
|
||||||
|
{"short", []byte{22, 3, 3}, false},
|
||||||
|
} {
|
||||||
|
if got := payloadIsTLS(tc.payload); got != tc.want {
|
||||||
|
t.Errorf("%s: payloadIsTLS=%v want %v", tc.name, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCaptureTLSRecordLimit checks that a segment packed with tiny records
|
||||||
|
// cannot make a single packet produce unbounded frames.
|
||||||
|
func TestCaptureTLSRecordLimit(t *testing.T) {
|
||||||
|
var pkt []byte
|
||||||
|
for range maxTLSRecordsPerPacket + 3 {
|
||||||
|
pkt = append(pkt, byte(tls.ContentTypeApplicationData), 3, 3, 0, 1, 0x99)
|
||||||
|
}
|
||||||
|
var pbreak PacketBreakdown
|
||||||
|
frames, err := pbreak.CaptureTLS(nil, pkt, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(frames) != maxTLSRecordsPerPacket+1 {
|
||||||
|
t.Fatalf("got %d frames want %d", len(frames), maxTLSRecordsPerPacket+1)
|
||||||
|
}
|
||||||
|
last := frames[len(frames)-1]
|
||||||
|
if last.Protocol != "TLS records?" {
|
||||||
|
t.Errorf("last frame protocol=%q want TLS records?", last.Protocol)
|
||||||
|
}
|
||||||
|
if end := (last.PacketBitOffset + last.LenBits()) / 8; end != len(pkt) {
|
||||||
|
t.Errorf("remaining frame ends at %d want %d", end, len(pkt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldByName(frm Frame, name string) *FrameField {
|
||||||
|
i := indexOfField(frm, name)
|
||||||
|
if i < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &frm.Fields[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOfField(frm Frame, name string) int {
|
||||||
|
for i := range frm.Fields {
|
||||||
|
if frm.Fields[i].Name == name {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
@@ -136,6 +136,9 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
|
|||||||
name := field.Name
|
name := field.Name
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = field.Class.String()
|
name = field.Class.String()
|
||||||
|
if field.Flags.IsEncrypted() {
|
||||||
|
name = "encrypted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
hasSpaces := strings.IndexByte(name, ' ') >= 0
|
hasSpaces := strings.IndexByte(name, ' ') >= 0
|
||||||
if hasSpaces {
|
if hasSpaces {
|
||||||
@@ -191,8 +194,10 @@ func (f *Formatter) formatField(dst []byte, pktStartOff int, field FrameField, p
|
|||||||
nameOff := field.FrameBitOffset / 8
|
nameOff := field.FrameBitOffset / 8
|
||||||
dst = dnsAppendDottedName(dst, dnsMsg, nameOff)
|
dst = dnsAppendDottedName(dst, dnsMsg, nameOff)
|
||||||
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
|
case FieldClassDst, FieldClassSrc, FieldClassSize, FieldClassAddress, FieldClassOperation:
|
||||||
// IP, MAC addresses and ports.
|
// IP, MAC addresses and ports. Sizes are always numbers, never
|
||||||
if field.BitLength <= 16 {
|
// addresses, so a wide one such as the TLS 24-bit handshake length is
|
||||||
|
// still printed in decimal.
|
||||||
|
if field.BitLength <= 16 || field.Class == FieldClassSize && field.BitLength <= 64 {
|
||||||
v, err := f.fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
|
v, err := f.fieldAsUint(pkt, fieldBitStart, field.BitLength, field.Flags.IsRightAligned())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dst, err
|
return dst, err
|
||||||
|
|||||||
@@ -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,240 @@
|
|||||||
|
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 a ServerHello 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.AddU16(uint16(tls.GroupX25519)) // server form: one entry, no list prefix
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddBytes(make([]byte, 8))
|
||||||
|
b.Close()
|
||||||
|
b.Close()
|
||||||
|
b.Close()
|
||||||
|
|
||||||
|
out, err := b.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Strip the outer block length the way ServerHelloMsg.ExtensionBytes 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := tls.ParseServerExtensions(exts, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var seen []tls.ExtensionType
|
||||||
|
for _, ext := range list.All {
|
||||||
|
seen = append(seen, ext.Type())
|
||||||
|
if ext.Type() != tls.ExtKeyShare {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, ks := range ext.KeyShares {
|
||||||
|
if ks.Group != tls.GroupX25519 || len(ks.Key) != 8 {
|
||||||
|
t.Errorf("key share got %v len %d", ks.Group, len(ks.Key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,276 @@
|
|||||||
|
// 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
|
||||||
|
ExtECPointFormats ExtensionType = 11 // ec_point_formats
|
||||||
|
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")
|
||||||
|
)
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Iterators in this package have the signature of an [iter.Seq2] instead of
|
||||||
|
// returning one, so they are ranged over as a method value without a call:
|
||||||
|
//
|
||||||
|
// for off, ext := range list.All {
|
||||||
|
// for off, ks := range ext.KeyShares {
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// A method value bound to a receiver stays on the caller's stack, while a
|
||||||
|
// closure returned from a method escapes to the heap. The int key is always the
|
||||||
|
// item's offset within the enclosing message body, and nested iterators inherit
|
||||||
|
// that base, so a decoder never has to add up prefix widths.
|
||||||
|
//
|
||||||
|
// Every length prefix was checked when the message was parsed, so no iterator
|
||||||
|
// can fail and none returns an error.
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
//
|
||||||
|
// The lists nested inside an extension are reached with the iterators below,
|
||||||
|
// each of which walks only its own extension type.
|
||||||
|
type ExtensionFrame struct {
|
||||||
|
buf []byte
|
||||||
|
// base is where buf starts within the enclosing message body.
|
||||||
|
base int
|
||||||
|
// server records that this came from a server hello. The server forms of
|
||||||
|
// key_share and supported_versions carry a single value, not a list.
|
||||||
|
server bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExtensionFrame wraps buf as an [ExtensionFrame]. It checks the extension's
|
||||||
|
// own framing but not the structure nested inside extension_data; prefer
|
||||||
|
// [ParseClientExtensions] or [ParseServerExtensions], which check both.
|
||||||
|
func NewExtensionFrame(buf []byte) (ExtensionFrame, error) {
|
||||||
|
return newExtensionFrame(buf, 0, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExtensionFrame(buf []byte, base int, server bool) (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], base: base, server: server}, 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 }
|
||||||
|
|
||||||
|
// DataOffset is where Data starts within the enclosing message body.
|
||||||
|
func (ef ExtensionFrame) DataOffset() int { return ef.base + 4 }
|
||||||
|
|
||||||
|
// ExtensionList is an extensions block whose framing has been checked, down to
|
||||||
|
// the lists inside the extensions this package recognizes.
|
||||||
|
type ExtensionList struct {
|
||||||
|
buf []byte
|
||||||
|
base int
|
||||||
|
server bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseClientExtensions validates a ClientHello extensions block. exts is the
|
||||||
|
// block contents with the outer two-byte length already stripped, and base is
|
||||||
|
// where exts starts within the enclosing message body.
|
||||||
|
//
|
||||||
|
// Unknown extension types, GREASE among them, are accepted and left unchecked.
|
||||||
|
// A repeated known type is rejected with [lneto.ErrInvalidField]: RFC 8446 4.2
|
||||||
|
// forbids duplicates, and tolerating them lets this parser and a middlebox act
|
||||||
|
// on different copies.
|
||||||
|
func ParseClientExtensions(exts []byte, base int) (ExtensionList, error) {
|
||||||
|
return parseExtensions(exts, base, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseServerExtensions validates a ServerHello extensions block. It differs
|
||||||
|
// from [ParseClientExtensions] in the key_share and supported_versions forms.
|
||||||
|
func ParseServerExtensions(exts []byte, base int) (ExtensionList, error) {
|
||||||
|
return parseExtensions(exts, base, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExtensions(exts []byte, base int, server bool) (ExtensionList, error) {
|
||||||
|
var seen extSeen
|
||||||
|
for off := 0; off < len(exts); {
|
||||||
|
ef, err := newExtensionFrame(exts[off:], base+off, server)
|
||||||
|
if err != nil {
|
||||||
|
return ExtensionList{}, err
|
||||||
|
}
|
||||||
|
if seen.mark(ef.Type()) {
|
||||||
|
return ExtensionList{}, lneto.ErrInvalidField
|
||||||
|
}
|
||||||
|
err = ef.validate()
|
||||||
|
if err != nil {
|
||||||
|
return ExtensionList{}, err
|
||||||
|
}
|
||||||
|
off += len(ef.RawData())
|
||||||
|
}
|
||||||
|
return ExtensionList{buf: exts, base: base, server: server}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All iterates the extensions in wire order, keyed by the offset of each
|
||||||
|
// extension's data within the enclosing message body.
|
||||||
|
func (l ExtensionList) All(yield func(off int, ext ExtensionFrame) bool) {
|
||||||
|
for off := 0; off < len(l.buf); {
|
||||||
|
ef, err := newExtensionFrame(l.buf[off:], l.base+off, l.server)
|
||||||
|
if err != nil {
|
||||||
|
return // Framing was checked at parse.
|
||||||
|
}
|
||||||
|
if !yield(ef.DataOffset(), ef) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off += len(ef.RawData())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the block contents.
|
||||||
|
func (l ExtensionList) Bytes() []byte { return l.buf }
|
||||||
|
|
||||||
|
// validate checks the structure nested inside extension_data for the types this
|
||||||
|
// package walks. The switch mirrors the iterators below: an extension with no
|
||||||
|
// iterator has no shape to check here.
|
||||||
|
func (ef ExtensionFrame) validate() error {
|
||||||
|
data := ef.Data()
|
||||||
|
switch ef.Type() {
|
||||||
|
case ExtServerName:
|
||||||
|
return validateServerNames(data)
|
||||||
|
case ExtALPN:
|
||||||
|
return validateALPN(data)
|
||||||
|
case ExtSupportedGroups, ExtSignatureAlgorithms, ExtSignatureAlgorithmsCert:
|
||||||
|
body, err := vectorU16(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validateU16List(body)
|
||||||
|
case ExtSupportedVersions:
|
||||||
|
if ef.server {
|
||||||
|
return validateU16List(data) // Bare uint16.
|
||||||
|
}
|
||||||
|
body, err := vectorU8(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validateU16List(body)
|
||||||
|
case ExtKeyShare:
|
||||||
|
return ef.validateKeyShare()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateU16List(b []byte) error {
|
||||||
|
if len(b)%2 != 0 {
|
||||||
|
return lneto.ErrInvalidLengthField
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateServerNames(data []byte) error {
|
||||||
|
body, err := vectorU16(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for off := 0; off < len(body); {
|
||||||
|
if len(body)-off < 3 {
|
||||||
|
return lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
n := int(binary.BigEndian.Uint16(body[off+1 : off+3]))
|
||||||
|
off += 3
|
||||||
|
if n > len(body)-off {
|
||||||
|
return lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateALPN(data []byte) error {
|
||||||
|
body, err := vectorU16(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for off := 0; off < len(body); {
|
||||||
|
n := int(body[off])
|
||||||
|
off++
|
||||||
|
if n == 0 {
|
||||||
|
// A zero-length name would make a walk unable to advance.
|
||||||
|
return lneto.ErrInvalidLengthField
|
||||||
|
} else if n > len(body)-off {
|
||||||
|
return lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ef ExtensionFrame) validateKeyShare() error {
|
||||||
|
data := ef.Data()
|
||||||
|
if ef.server {
|
||||||
|
// A ServerHello names one group and its key; a HelloRetryRequest names
|
||||||
|
// only the group.
|
||||||
|
if len(data) == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return validateKeyShareEntries(data)
|
||||||
|
}
|
||||||
|
body, err := vectorU16(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validateKeyShareEntries(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateKeyShareEntries(b []byte) error {
|
||||||
|
for off := 0; off < len(b); {
|
||||||
|
if len(b)-off < 4 {
|
||||||
|
return lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
n := int(binary.BigEndian.Uint16(b[off+2 : off+4]))
|
||||||
|
off += 4
|
||||||
|
if n > len(b)-off {
|
||||||
|
return lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyShare is one KeyShareEntry of a key_share extension:
|
||||||
|
//
|
||||||
|
// struct {
|
||||||
|
// NamedGroup group;
|
||||||
|
// opaque key_exchange<1..2^16-1>;
|
||||||
|
// } KeyShareEntry;
|
||||||
|
//
|
||||||
|
// A HelloRetryRequest names a group with no key, so Key may be empty. GREASE
|
||||||
|
// key shares carry a deliberately absurd key, commonly one byte, so no length
|
||||||
|
// constraint is placed on Key beyond fitting inside the extension.
|
||||||
|
type KeyShare struct {
|
||||||
|
Group NamedGroup
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerName is one entry of an SNI server_name_list. Type 0 is host_name, the
|
||||||
|
// only type ever defined.
|
||||||
|
type ServerName struct {
|
||||||
|
Type uint8
|
||||||
|
Name []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyShares iterates the key shares of a key_share extension, in the client or
|
||||||
|
// server form as the enclosing hello requires, keyed by the offset of the
|
||||||
|
// key_exchange bytes. Yields nothing for any other extension type.
|
||||||
|
func (ef ExtensionFrame) KeyShares(yield func(off int, ks KeyShare) bool) {
|
||||||
|
if ef.Type() != ExtKeyShare {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, base := ef.Data(), ef.DataOffset()
|
||||||
|
if ef.server {
|
||||||
|
if len(body) == 2 { // HelloRetryRequest: selected_group only.
|
||||||
|
yield(base, KeyShare{Group: NamedGroup(binary.BigEndian.Uint16(body))})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b, err := vectorU16(body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, base = b, base+2
|
||||||
|
}
|
||||||
|
for off := 0; off+4 <= len(body); {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if !yield(base+off, KeyShare{Group: group, Key: body[off : off+n]}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerNames iterates the server_name_list of an SNI extension, keyed by the
|
||||||
|
// offset of the name. Yields nothing for any other extension type.
|
||||||
|
func (ef ExtensionFrame) ServerNames(yield func(off int, name ServerName) bool) {
|
||||||
|
if ef.Type() != ExtServerName {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := vectorU16(ef.Data())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
base := ef.DataOffset() + 2
|
||||||
|
for off := 0; off+3 <= len(body); {
|
||||||
|
nameType := body[off]
|
||||||
|
n := int(binary.BigEndian.Uint16(body[off+1 : off+3]))
|
||||||
|
off += 3
|
||||||
|
if n > len(body)-off {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !yield(base+off, ServerName{Type: nameType, Name: body[off : off+n]}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ALPNProtos iterates the protocol names of an ALPN extension, keyed by the
|
||||||
|
// offset of the name. Chrome includes a GREASE entry here, so callers must match
|
||||||
|
// against their own offer list rather than assuming the first name is
|
||||||
|
// meaningful. Yields nothing for any other extension type.
|
||||||
|
func (ef ExtensionFrame) ALPNProtos(yield func(off int, proto []byte) bool) {
|
||||||
|
if ef.Type() != ExtALPN {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := vectorU16(ef.Data())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
base := ef.DataOffset() + 2
|
||||||
|
for off := 0; off < len(body); {
|
||||||
|
n := int(body[off])
|
||||||
|
off++
|
||||||
|
if n == 0 || n > len(body)-off {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !yield(base+off, body[off:off+n]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedVersions iterates a supported_versions extension. The ClientHello
|
||||||
|
// form is a list behind a one-byte prefix, unlike every other hello list; the
|
||||||
|
// ServerHello form is a bare uint16, which yields a single value. Yields nothing
|
||||||
|
// for any other extension type.
|
||||||
|
func (ef ExtensionFrame) SupportedVersions(yield func(off int, version uint16) bool) {
|
||||||
|
if ef.Type() != ExtSupportedVersions {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, base := ef.Data(), ef.DataOffset()
|
||||||
|
if !ef.server {
|
||||||
|
b, err := vectorU8(body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, base = b, base+1
|
||||||
|
}
|
||||||
|
for off := 0; off+2 <= len(body); off += 2 {
|
||||||
|
if !yield(base+off, binary.BigEndian.Uint16(body[off:off+2])) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedGroups iterates a supported_groups extension, keyed by the group's
|
||||||
|
// offset. Yields nothing for any other extension type.
|
||||||
|
func (ef ExtensionFrame) SupportedGroups(yield func(off int, group NamedGroup) bool) {
|
||||||
|
body, base, ok := ef.u16Vector(ExtSupportedGroups)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for off := 0; off+2 <= len(body); off += 2 {
|
||||||
|
if !yield(base+off, NamedGroup(binary.BigEndian.Uint16(body[off:off+2]))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureSchemes iterates a signature_algorithms or signature_algorithms_cert
|
||||||
|
// extension, keyed by the scheme's offset. Yields nothing for any other type.
|
||||||
|
func (ef ExtensionFrame) SignatureSchemes(yield func(off int, scheme SignatureScheme) bool) {
|
||||||
|
want := ef.Type()
|
||||||
|
if want != ExtSignatureAlgorithms && want != ExtSignatureAlgorithmsCert {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, base, ok := ef.u16Vector(want)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for off := 0; off+2 <= len(body); off += 2 {
|
||||||
|
if !yield(base+off, SignatureScheme(binary.BigEndian.Uint16(body[off:off+2]))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// u16Vector returns the contents of a two-byte-prefixed vector and where it
|
||||||
|
// starts within the message body, or ok false for another extension type.
|
||||||
|
func (ef ExtensionFrame) u16Vector(want ExtensionType) (body []byte, base int, ok bool) {
|
||||||
|
if ef.Type() != want {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
body, err := vectorU16(ef.Data())
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, false
|
||||||
|
}
|
||||||
|
return body, ef.DataOffset() + 2, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,283 @@
|
|||||||
|
package tls_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/x/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtensionListWalksAndToleratesGREASE(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
|
||||||
|
}
|
||||||
|
list, err := tls.ParseClientExtensions(exts, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var types []tls.ExtensionType
|
||||||
|
for off, ext := range list.All {
|
||||||
|
if want := 4*len(types) + 4; off != want {
|
||||||
|
t.Errorf("%v at offset %d want %d", ext.Type(), off, want)
|
||||||
|
}
|
||||||
|
types = append(types, ext.Type())
|
||||||
|
}
|
||||||
|
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 TestParseExtensionsTruncated(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.ParseClientExtensions(tc, 0)
|
||||||
|
if !errors.Is(err, lneto.ErrTruncatedFrame) {
|
||||||
|
t.Errorf("% x: got %v want ErrTruncatedFrame", tc, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseExtensionsRejectsMalformedInnerList(t *testing.T) {
|
||||||
|
// Framing inside a recognized extension is checked at parse, which is what
|
||||||
|
// lets the iterators be error-free.
|
||||||
|
for _, tc := range [][]byte{
|
||||||
|
{0x00, 0x0a, 0x00, 0x03, 0x00, 0x01, 0x1d}, // supported_groups, odd list
|
||||||
|
{0x00, 0x2b, 0x00, 0x03, 0x04, 0x03, 0x04}, // supported_versions, prefix overruns
|
||||||
|
{0x00, 0x10, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00}, // alpn, zero-length name
|
||||||
|
{0x00, 0x00, 0x00, 0x05, 0x00, 0x03, 0x00, 0x00, 0x09}, // server_name, name overruns
|
||||||
|
} {
|
||||||
|
if _, err := tls.ParseClientExtensions(tc, 0); err == nil {
|
||||||
|
t.Errorf("% x accepted", tc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtensionIterationStopsOnBreak(t *testing.T) {
|
||||||
|
// Two extensions with no inner structure of their own.
|
||||||
|
exts := []byte{0x00, 0x15, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00}
|
||||||
|
list, err := tls.ParseClientExtensions(exts, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for range list.All {
|
||||||
|
n++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
t.Errorf("walk continued after break: %d iterations", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeySharesAcceptGREASEEntry(t *testing.T) {
|
||||||
|
// Chrome sends a GREASE key share whose key_exchange is a single byte.
|
||||||
|
// Rejecting it as malformed breaks Chrome outright.
|
||||||
|
exts := []byte{
|
||||||
|
0x00, 0x33, 0x00, 0x0d, // key_share, 13 bytes
|
||||||
|
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
|
||||||
|
}
|
||||||
|
list, err := tls.ParseClientExtensions(exts, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
type share struct {
|
||||||
|
g tls.NamedGroup
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
var got []share
|
||||||
|
for _, ext := range list.All {
|
||||||
|
for _, ks := range ext.KeyShares {
|
||||||
|
got = append(got, share{ks.Group, len(ks.Key)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 TestServerKeyShareHelloRetryRequestForm(t *testing.T) {
|
||||||
|
// A HelloRetryRequest names a group with no key.
|
||||||
|
exts := []byte{0x00, 0x33, 0x00, 0x02, 0x00, 0x1d}
|
||||||
|
list, err := tls.ParseServerExtensions(exts, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n := 0
|
||||||
|
for _, ext := range list.All {
|
||||||
|
for _, ks := range ext.KeyShares {
|
||||||
|
n++
|
||||||
|
if ks.Group != tls.GroupX25519 || len(ks.Key) != 0 {
|
||||||
|
t.Errorf("got %v with %d key bytes", ks.Group, len(ks.Key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
t.Errorf("walked %d shares want 1", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestALPNProtos(t *testing.T) {
|
||||||
|
exts := []byte{
|
||||||
|
0x00, 0x10, 0x00, 0x0e,
|
||||||
|
0x00, 0x0c,
|
||||||
|
0x02, 'h', '2',
|
||||||
|
0x08, 'h', 't', 't', 'p', '/', '1', '.', '1',
|
||||||
|
}
|
||||||
|
list, err := tls.ParseClientExtensions(exts, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for _, ext := range list.All {
|
||||||
|
for _, p := range ext.ALPNProtos {
|
||||||
|
names = append(names, string(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(names) != 2 || names[0] != "h2" || names[1] != "http/1.1" {
|
||||||
|
t.Errorf("got %q", names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSupportedVersionsClientAndServerForms(t *testing.T) {
|
||||||
|
// The ClientHello form is a list behind a one-byte prefix; the ServerHello
|
||||||
|
// form is a bare uint16.
|
||||||
|
client := []byte{0x00, 0x2b, 0x00, 0x05, 0x04, 0x1a, 0x1a, 0x03, 0x04}
|
||||||
|
list, err := tls.ParseClientExtensions(client, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var vers []uint16
|
||||||
|
for _, ext := range list.All {
|
||||||
|
for _, v := range ext.SupportedVersions {
|
||||||
|
vers = append(vers, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(vers) != 2 || vers[1] != tls.VersionTLS13 {
|
||||||
|
t.Errorf("client form got %#x", vers)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04}
|
||||||
|
list, err = tls.ParseServerExtensions(server, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
vers = vers[:0]
|
||||||
|
for _, ext := range list.All {
|
||||||
|
for _, v := range ext.SupportedVersions {
|
||||||
|
vers = append(vers, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(vers) != 1 || vers[0] != tls.VersionTLS13 {
|
||||||
|
t.Errorf("server form 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.
|
||||||
|
exts := []byte{0x00, 0x0a, 0x00, 0x05, 0x00, 0x02, 0x00, 0x1d, 0xff}
|
||||||
|
if _, err := tls.ParseClientExtensions(exts, 0); err == nil {
|
||||||
|
t.Error("trailing bytes after vector accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubIteratorOffsetsAreBodyRelative(t *testing.T) {
|
||||||
|
// base threads through every nesting level so a decoder adds no arithmetic.
|
||||||
|
const base = 100
|
||||||
|
exts := []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x0b, // server_name, 11 bytes
|
||||||
|
0x00, 0x09, // list length
|
||||||
|
0x00, 0x00, 0x06, // host_name, 6 bytes
|
||||||
|
'a', '.', 'c', 'o', 'm', '!',
|
||||||
|
}
|
||||||
|
list, err := tls.ParseClientExtensions(exts, base)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for off, ext := range list.All {
|
||||||
|
if off != base+4 {
|
||||||
|
t.Errorf("extension data at %d want %d", off, base+4)
|
||||||
|
}
|
||||||
|
for noff, name := range ext.ServerNames {
|
||||||
|
// 4 extension header + 2 list length + 3 entry header
|
||||||
|
if want := base + 9; noff != want {
|
||||||
|
t.Errorf("name at %d want %d", noff, want)
|
||||||
|
}
|
||||||
|
if string(name.Name) != "a.com!" {
|
||||||
|
t.Errorf("name %q", name.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzParseClientExtensions(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) {
|
||||||
|
list, err := tls.ParseClientExtensions(b, 0)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total := 0
|
||||||
|
for off, ext := range list.All {
|
||||||
|
total += 4 + len(ext.Data())
|
||||||
|
if total > len(b) {
|
||||||
|
t.Fatalf("walked %d bytes past input length %d", total, len(b))
|
||||||
|
}
|
||||||
|
if off+len(ext.Data()) > len(b) {
|
||||||
|
t.Fatalf("extension data at %d overruns %d byte input", off, len(b))
|
||||||
|
}
|
||||||
|
// No sub-iterator may escape its slice, whatever the type says.
|
||||||
|
for o, ks := range ext.KeyShares {
|
||||||
|
if o+len(ks.Key) > len(b) {
|
||||||
|
t.Fatalf("key share at %d overruns input", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for o, name := range ext.ServerNames {
|
||||||
|
if o+len(name.Name) > len(b) {
|
||||||
|
t.Fatalf("server name at %d overruns input", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for o, p := range ext.ALPNProtos {
|
||||||
|
if o+len(p) > len(b) {
|
||||||
|
t.Fatalf("alpn name at %d overruns input", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for o := range ext.SupportedVersions {
|
||||||
|
if o+2 > len(b) {
|
||||||
|
t.Fatalf("version at %d overruns input", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for o := range ext.SupportedGroups {
|
||||||
|
if o+2 > len(b) {
|
||||||
|
t.Fatalf("group at %d overruns input", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for o := range ext.SignatureSchemes {
|
||||||
|
if o+2 > len(b) {
|
||||||
|
t.Fatalf("scheme at %d overruns input", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total != len(b) {
|
||||||
|
t.Fatalf("clean walk consumed %d of %d bytes", total, len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+244
@@ -0,0 +1,244 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
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 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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+432
@@ -0,0 +1,432 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
// Span is the position and length of a field within a message body.
|
||||||
|
type Span struct {
|
||||||
|
Off int
|
||||||
|
Len int
|
||||||
|
}
|
||||||
|
|
||||||
|
// HelloSpans locates the fields of a ClientHello or ServerHello body. Packet
|
||||||
|
// decoders need a field's wire position, which the slice returning accessors
|
||||||
|
// cannot give.
|
||||||
|
type HelloSpans struct {
|
||||||
|
Random Span
|
||||||
|
SessionID Span
|
||||||
|
CipherSuites Span // ServerHello: the single selected suite.
|
||||||
|
Compression Span
|
||||||
|
Extensions Span
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientHelloMsg provides zero-copy access to a ClientHello body
|
||||||
|
// (RFC 8446 4.1.2), wrapping the handshake message body 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;
|
||||||
|
//
|
||||||
|
// Unlike a [RecordFrame] or [HandshakeFrame], whose fields sit at fixed offsets,
|
||||||
|
// a hello's fields are length-prefixed and its extensions may arrive in any
|
||||||
|
// order. [ParseClientHello] resolves every offset once so accessors and
|
||||||
|
// iterators need no bounds checks and cannot fail.
|
||||||
|
type ClientHelloMsg struct {
|
||||||
|
buf []byte
|
||||||
|
exts ExtensionList
|
||||||
|
// Offsets of each variable-length field's contents, resolved at parse time.
|
||||||
|
sessionIDOff, sessionIDLen int
|
||||||
|
suitesOff, suitesLen int
|
||||||
|
compOff, compLen int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseClientHello parses a ClientHello body, validating that every length
|
||||||
|
// prefix is consistent with the buffer, down to the lists inside the extensions
|
||||||
|
// this package recognizes. 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 ParseClientHello(body []byte) (ClientHelloMsg, error) {
|
||||||
|
var ch ClientHelloMsg
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
exts, err := ParseClientExtensions(body[off:off+extsLen], off)
|
||||||
|
if err != nil {
|
||||||
|
return ClientHelloMsg{}, err
|
||||||
|
}
|
||||||
|
ch.exts = exts
|
||||||
|
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 ClientHelloMsg) LegacyVersion() uint16 {
|
||||||
|
return binary.BigEndian.Uint16(ch.buf[0:2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random returns the 32-byte client_random.
|
||||||
|
func (ch ClientHelloMsg) 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 ClientHelloMsg) LegacySessionID() []byte {
|
||||||
|
return ch.buf[ch.sessionIDOff : ch.sessionIDOff+ch.sessionIDLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CipherSuites iterates the offered suites in wire order, keyed by each suite's
|
||||||
|
// offset within the body. The list includes GREASE values.
|
||||||
|
func (ch ClientHelloMsg) CipherSuites(yield func(off int, suite CipherSuite) bool) {
|
||||||
|
suites := ch.CipherSuiteBytes()
|
||||||
|
for off := 0; off+2 <= len(suites); off += 2 {
|
||||||
|
if !yield(ch.suitesOff+off, CipherSuite(binary.BigEndian.Uint16(suites[off:off+2]))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CipherSuiteBytes returns the cipher_suites vector with its length prefix
|
||||||
|
// stripped.
|
||||||
|
func (ch ClientHelloMsg) CipherSuiteBytes() []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
|
||||||
|
// [ClientHelloMsg.ValidateCompression].
|
||||||
|
func (ch ClientHelloMsg) LegacyCompressionMethods() []byte {
|
||||||
|
return ch.buf[ch.compOff : ch.compOff+ch.compLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions iterates this hello's extensions in wire order, keyed by the
|
||||||
|
// offset of each extension's data within the body. Duplicates were rejected at
|
||||||
|
// parse time, so the walk needs no bookkeeping.
|
||||||
|
func (ch ClientHelloMsg) Extensions(yield func(off int, ext ExtensionFrame) bool) {
|
||||||
|
ch.exts.All(yield)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionList returns the validated extensions block, for passing the block
|
||||||
|
// itself around rather than ranging over it here.
|
||||||
|
func (ch ClientHelloMsg) ExtensionList() ExtensionList { return ch.exts }
|
||||||
|
|
||||||
|
// ExtensionBytes returns the extensions block contents with the outer length
|
||||||
|
// prefix stripped.
|
||||||
|
func (ch ClientHelloMsg) ExtensionBytes() []byte { return ch.exts.Bytes() }
|
||||||
|
|
||||||
|
// RawData returns the whole ClientHello body.
|
||||||
|
func (ch ClientHelloMsg) RawData() []byte { return ch.buf }
|
||||||
|
|
||||||
|
// Spans locates this hello's fields inside [ClientHelloMsg.RawData].
|
||||||
|
func (ch ClientHelloMsg) Spans() HelloSpans {
|
||||||
|
return HelloSpans{
|
||||||
|
Random: Span{Off: 2, Len: SizeRandom},
|
||||||
|
SessionID: Span{Off: ch.sessionIDOff, Len: ch.sessionIDLen},
|
||||||
|
CipherSuites: Span{Off: ch.suitesOff, Len: ch.suitesLen},
|
||||||
|
Compression: Span{Off: ch.compOff, Len: ch.compLen},
|
||||||
|
Extensions: Span{Off: ch.exts.base, Len: len(ch.exts.buf)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSize adds an error to v if the ClientHello is structurally invalid.
|
||||||
|
// Since [ParseClientHello] already rejects every inconsistent length, this only
|
||||||
|
// re-checks that the message was successfully parsed.
|
||||||
|
func (ch ClientHelloMsg) 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 ClientHelloMsg) ValidateCompression() bool {
|
||||||
|
return ch.compLen == 1 && ch.buf[ch.compOff] == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerHelloMsg provides zero-copy access to a ServerHello body
|
||||||
|
// (RFC 8446 4.1.3). Like [ClientHelloMsg] it wraps the handshake message body.
|
||||||
|
//
|
||||||
|
// struct {
|
||||||
|
// ProtocolVersion legacy_version = 0x0303;
|
||||||
|
// Random random; // 32 bytes
|
||||||
|
// opaque legacy_session_id_echo<0..32>;
|
||||||
|
// CipherSuite cipher_suite; // 2 bytes
|
||||||
|
// uint8 legacy_compression_method = 0;
|
||||||
|
// Extension extensions<6..2^16-1>;
|
||||||
|
// } ServerHello;
|
||||||
|
//
|
||||||
|
// A HelloRetryRequest has this same structure; it is told apart by its random
|
||||||
|
// being the special value of RFC 8446 4.1.3, which is policy, not framing.
|
||||||
|
type ServerHelloMsg struct {
|
||||||
|
buf []byte
|
||||||
|
exts ExtensionList
|
||||||
|
sessionIDOff, sessionIDLen int
|
||||||
|
suiteOff int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseServerHello parses a ServerHello body. Like [ParseClientHello] it
|
||||||
|
// validates framing only, rejects trailing bytes, and validates the lists
|
||||||
|
// inside recognized extensions, in their server forms.
|
||||||
|
func ParseServerHello(body []byte) (ServerHelloMsg, error) {
|
||||||
|
var sh ServerHelloMsg
|
||||||
|
const fixed = 2 + SizeRandom + 1
|
||||||
|
if len(body) < fixed {
|
||||||
|
return sh, lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
off := 2 + SizeRandom
|
||||||
|
|
||||||
|
sidLen := int(body[off])
|
||||||
|
off++
|
||||||
|
if sidLen > MaxSessionIDLen {
|
||||||
|
return sh, lneto.ErrInvalidLengthField
|
||||||
|
} else if sidLen > len(body)-off {
|
||||||
|
return sh, lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
sh.sessionIDOff, sh.sessionIDLen = off, sidLen
|
||||||
|
off += sidLen
|
||||||
|
|
||||||
|
// cipher_suite(2) + legacy_compression_method(1)
|
||||||
|
if len(body)-off < 3 {
|
||||||
|
return sh, lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
sh.suiteOff = off
|
||||||
|
off += 3
|
||||||
|
|
||||||
|
if len(body)-off < 2 {
|
||||||
|
return sh, lneto.ErrTruncatedFrame
|
||||||
|
}
|
||||||
|
extsLen := int(binary.BigEndian.Uint16(body[off : off+2]))
|
||||||
|
off += 2
|
||||||
|
if extsLen > len(body)-off {
|
||||||
|
return sh, lneto.ErrTruncatedFrame
|
||||||
|
} else if extsLen != len(body)-off {
|
||||||
|
return sh, errTrailingBytes
|
||||||
|
}
|
||||||
|
exts, err := ParseServerExtensions(body[off:off+extsLen], off)
|
||||||
|
if err != nil {
|
||||||
|
return ServerHelloMsg{}, err
|
||||||
|
}
|
||||||
|
sh.exts = exts
|
||||||
|
sh.buf = body
|
||||||
|
return sh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegacyVersion returns the legacy_version field, pinned to 0x0303 by TLS 1.3.
|
||||||
|
func (sh ServerHelloMsg) LegacyVersion() uint16 {
|
||||||
|
return binary.BigEndian.Uint16(sh.buf[0:2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random returns the 32-byte server_random.
|
||||||
|
func (sh ServerHelloMsg) Random() *[SizeRandom]byte {
|
||||||
|
return (*[SizeRandom]byte)(sh.buf[2 : 2+SizeRandom])
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegacySessionIDEcho returns the client's legacy_session_id as echoed back. A
|
||||||
|
// client in middlebox compatibility mode requires it to match what it sent.
|
||||||
|
func (sh ServerHelloMsg) LegacySessionIDEcho() []byte {
|
||||||
|
return sh.buf[sh.sessionIDOff : sh.sessionIDOff+sh.sessionIDLen]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CipherSuite returns the selected cipher suite.
|
||||||
|
func (sh ServerHelloMsg) CipherSuite() CipherSuite {
|
||||||
|
return CipherSuite(binary.BigEndian.Uint16(sh.buf[sh.suiteOff : sh.suiteOff+2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegacyCompressionMethod returns the legacy_compression_method, which TLS 1.3
|
||||||
|
// requires to be zero.
|
||||||
|
func (sh ServerHelloMsg) LegacyCompressionMethod() uint8 {
|
||||||
|
return sh.buf[sh.suiteOff+2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extensions iterates this hello's extensions in wire order, keyed by the
|
||||||
|
// offset of each extension's data within the body.
|
||||||
|
func (sh ServerHelloMsg) Extensions(yield func(off int, ext ExtensionFrame) bool) {
|
||||||
|
sh.exts.All(yield)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtensionList returns the validated extensions block.
|
||||||
|
func (sh ServerHelloMsg) ExtensionList() ExtensionList { return sh.exts }
|
||||||
|
|
||||||
|
// ExtensionBytes returns the extensions block contents with the outer length
|
||||||
|
// prefix stripped.
|
||||||
|
func (sh ServerHelloMsg) ExtensionBytes() []byte { return sh.exts.Bytes() }
|
||||||
|
|
||||||
|
// RawData returns the whole ServerHello body.
|
||||||
|
func (sh ServerHelloMsg) RawData() []byte { return sh.buf }
|
||||||
|
|
||||||
|
// Spans locates this hello's fields inside [ServerHelloMsg.RawData].
|
||||||
|
func (sh ServerHelloMsg) Spans() HelloSpans {
|
||||||
|
return HelloSpans{
|
||||||
|
Random: Span{Off: 2, Len: SizeRandom},
|
||||||
|
SessionID: Span{Off: sh.sessionIDOff, Len: sh.sessionIDLen},
|
||||||
|
CipherSuites: Span{Off: sh.suiteOff, Len: 2},
|
||||||
|
Compression: Span{Off: sh.suiteOff + 2, Len: 1},
|
||||||
|
Extensions: Span{Off: sh.exts.base, Len: len(sh.exts.buf)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSize adds an error to v if the ServerHello was not parsed.
|
||||||
|
func (sh ServerHelloMsg) ValidateSize(v *lneto.Validator) {
|
||||||
|
if sh.buf == nil {
|
||||||
|
v.AddError(lneto.ErrTruncatedFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
case ExtECPointFormats:
|
||||||
|
i = 23
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return 1 << i, true
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
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.ClientHelloMsg {
|
||||||
|
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.ParseClientHello(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
|
||||||
|
for _, ext := range ch.Extensions {
|
||||||
|
switch ext.Type() {
|
||||||
|
case ltls.ExtSupportedVersions:
|
||||||
|
for _, v := range ext.SupportedVersions {
|
||||||
|
sawTLS13 = sawTLS13 || v == ltls.VersionTLS13
|
||||||
|
}
|
||||||
|
case ltls.ExtKeyShare:
|
||||||
|
for _, ks := range ext.KeyShares {
|
||||||
|
sawX25519 = sawX25519 || ks.Group == ltls.GroupX25519 && len(ks.Key) == 32
|
||||||
|
}
|
||||||
|
case ltls.ExtServerName:
|
||||||
|
for _, name := range ext.ServerNames {
|
||||||
|
sawSNI = sawSNI || name.Type == 0 && string(name.Name) == "example.com"
|
||||||
|
}
|
||||||
|
case ltls.ExtALPN:
|
||||||
|
for _, p := range ext.ALPNProtos {
|
||||||
|
sawALPN = sawALPN || string(p) == "http/1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var suites []ltls.CipherSuite
|
||||||
|
for _, s := range ch.CipherSuites {
|
||||||
|
suites = append(suites, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.ClientHelloMsg, 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.CipherSuiteBytes())
|
||||||
|
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.ParseClientHello(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.ParseClientHello(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) {
|
||||||
|
msg, err := ltls.ParseClientHello(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))
|
||||||
|
_ = msg.ExtensionBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.ParseClientHello(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.CipherSuiteBytes())%2 != 0 {
|
||||||
|
t.Fatal("cipher suites vector has odd length")
|
||||||
|
}
|
||||||
|
total := 2 + ltls.SizeRandom + 1 + len(ch.LegacySessionID()) +
|
||||||
|
2 + len(ch.CipherSuiteBytes()) +
|
||||||
|
1 + len(ch.LegacyCompressionMethods()) +
|
||||||
|
2 + len(ch.ExtensionBytes())
|
||||||
|
if total != len(b) {
|
||||||
|
t.Fatalf("fields sum to %d but input is %d bytes", total, len(b))
|
||||||
|
}
|
||||||
|
_ = ch.ValidateCompression()
|
||||||
|
// Every nested iterator must stay inside the input too.
|
||||||
|
for _, ext := range ch.Extensions {
|
||||||
|
for _, ks := range ext.KeyShares {
|
||||||
|
_ = ks
|
||||||
|
}
|
||||||
|
for _, name := range ext.ServerNames {
|
||||||
|
_ = name
|
||||||
|
}
|
||||||
|
for _, p := range ext.ALPNProtos {
|
||||||
|
_ = p
|
||||||
|
}
|
||||||
|
for _, v := range ext.SupportedVersions {
|
||||||
|
_ = v
|
||||||
|
}
|
||||||
|
for _, g := range ext.SupportedGroups {
|
||||||
|
_ = g
|
||||||
|
}
|
||||||
|
for _, s := range ext.SignatureSchemes {
|
||||||
|
_ = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
package tls_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
ltls "github.com/soypat/lneto/x/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// locates reports whether span points at want inside body.
|
||||||
|
func locates(body []byte, off int, want []byte) bool {
|
||||||
|
return off >= 0 && off+len(want) <= len(body) && bytes.Equal(body[off:off+len(want)], want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spans must locate every hello field, so decoders can map a field onto its wire
|
||||||
|
// position without re-walking the structure.
|
||||||
|
func TestClientHelloMsgSpans(t *testing.T) {
|
||||||
|
msg := captureClientHello(t)
|
||||||
|
body := msg.RawData()
|
||||||
|
sp := msg.Spans()
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
span ltls.Span
|
||||||
|
want []byte
|
||||||
|
}{
|
||||||
|
{"random", sp.Random, msg.Random()[:]},
|
||||||
|
{"session id", sp.SessionID, msg.LegacySessionID()},
|
||||||
|
{"compression", sp.Compression, msg.LegacyCompressionMethods()},
|
||||||
|
{"extensions", sp.Extensions, msg.ExtensionBytes()},
|
||||||
|
} {
|
||||||
|
if tc.span.Len != len(tc.want) {
|
||||||
|
t.Errorf("%s span len %d want %d", tc.name, tc.span.Len, len(tc.want))
|
||||||
|
} else if !locates(body, tc.span.Off, tc.want) {
|
||||||
|
t.Errorf("%s span %+v does not locate its field", tc.name, tc.span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterator keys are offsets into the hello body, at every nesting level, so a
|
||||||
|
// decoder can point at a cipher suite or an SNI hostname without arithmetic.
|
||||||
|
func TestClientHelloMsgIterators(t *testing.T) {
|
||||||
|
msg := captureClientHello(t)
|
||||||
|
body := msg.RawData()
|
||||||
|
|
||||||
|
nsuites := 0
|
||||||
|
for off, suite := range msg.CipherSuites {
|
||||||
|
want := []byte{byte(suite >> 8), byte(suite)}
|
||||||
|
if !locates(body, off, want) {
|
||||||
|
t.Errorf("suite %v at offset %d does not match the wire", suite, off)
|
||||||
|
}
|
||||||
|
nsuites++
|
||||||
|
}
|
||||||
|
if nsuites == 0 {
|
||||||
|
t.Fatal("no cipher suites walked")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sawSNI, sawALPN, sawTLS13, sawX25519 bool
|
||||||
|
for off, ext := range msg.Extensions {
|
||||||
|
if !locates(body, off, ext.Data()) {
|
||||||
|
t.Errorf("%v data at offset %d does not match the wire", ext.Type(), off)
|
||||||
|
}
|
||||||
|
switch ext.Type() {
|
||||||
|
case ltls.ExtServerName:
|
||||||
|
for noff, name := range ext.ServerNames {
|
||||||
|
sawSNI = true
|
||||||
|
if name.Type != 0 || string(name.Name) != "example.com" {
|
||||||
|
t.Errorf("server name %d %q", name.Type, name.Name)
|
||||||
|
}
|
||||||
|
if !locates(body, noff, name.Name) {
|
||||||
|
t.Errorf("server name at offset %d does not match the wire", noff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ltls.ExtALPN:
|
||||||
|
for poff, proto := range ext.ALPNProtos {
|
||||||
|
sawALPN = true
|
||||||
|
if !locates(body, poff, proto) {
|
||||||
|
t.Errorf("alpn %q at offset %d does not match the wire", proto, poff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ltls.ExtSupportedVersions:
|
||||||
|
for _, v := range ext.SupportedVersions {
|
||||||
|
sawTLS13 = sawTLS13 || v == ltls.VersionTLS13
|
||||||
|
}
|
||||||
|
case ltls.ExtKeyShare:
|
||||||
|
for koff, ks := range ext.KeyShares {
|
||||||
|
if ks.Group == ltls.GroupX25519 {
|
||||||
|
sawX25519 = true
|
||||||
|
}
|
||||||
|
if !locates(body, koff, ks.Key) {
|
||||||
|
t.Errorf("key share %v at offset %d does not match the wire", ks.Group, koff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ltls.ExtSupportedGroups:
|
||||||
|
for goff, g := range ext.SupportedGroups {
|
||||||
|
if !locates(body, goff, []byte{byte(g >> 8), byte(g)}) {
|
||||||
|
t.Errorf("group %v at offset %d does not match the wire", g, goff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ltls.ExtSignatureAlgorithms:
|
||||||
|
for soff, s := range ext.SignatureSchemes {
|
||||||
|
if !locates(body, soff, []byte{byte(s >> 8), byte(s)}) {
|
||||||
|
t.Errorf("scheme %v at offset %d does not match the wire", s, soff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sawSNI || !sawALPN || !sawTLS13 || !sawX25519 {
|
||||||
|
t.Errorf("sni=%v alpn=%v tls13=%v x25519=%v", sawSNI, sawALPN, sawTLS13, sawX25519)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sub-iterator reached from the wrong extension yields nothing rather than
|
||||||
|
// reinterpreting unrelated bytes.
|
||||||
|
func TestExtensionSubIteratorTypeMismatch(t *testing.T) {
|
||||||
|
msg := captureClientHello(t)
|
||||||
|
for _, ext := range msg.Extensions {
|
||||||
|
if ext.Type() != ltls.ExtServerName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for range ext.KeyShares {
|
||||||
|
t.Error("KeyShares walked a server_name extension")
|
||||||
|
}
|
||||||
|
for range ext.SupportedVersions {
|
||||||
|
t.Error("SupportedVersions walked a server_name extension")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walking a hello must not allocate: every iterator is a value type over the
|
||||||
|
// caller's buffer.
|
||||||
|
func TestClientHelloMsgZeroAlloc(t *testing.T) {
|
||||||
|
msg := captureClientHello(t)
|
||||||
|
body := msg.RawData()
|
||||||
|
n := testing.AllocsPerRun(10, func() {
|
||||||
|
m, err := ltls.ParseClientHello(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, suite := range m.CipherSuites {
|
||||||
|
_ = suite
|
||||||
|
}
|
||||||
|
for _, ext := range m.Extensions {
|
||||||
|
for _, name := range ext.ServerNames {
|
||||||
|
_ = name
|
||||||
|
}
|
||||||
|
for _, ks := range ext.KeyShares {
|
||||||
|
_ = ks
|
||||||
|
}
|
||||||
|
for _, p := range ext.ALPNProtos {
|
||||||
|
_ = p
|
||||||
|
}
|
||||||
|
for _, v := range ext.SupportedVersions {
|
||||||
|
_ = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if n != 0 {
|
||||||
|
t.Errorf("parse and walk allocated %v times, want 0", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A known extension whose inner framing is broken must fail the parse, since
|
||||||
|
// every iterator past construction is error-free.
|
||||||
|
func TestParseClientHelloRejectsMalformedKnownExtension(t *testing.T) {
|
||||||
|
msg := captureClientHello(t)
|
||||||
|
// server_name with a host name length one past the extension data.
|
||||||
|
bad := []byte{
|
||||||
|
0x00, 0x00, 0x00, 0x0b, // server_name, 11 bytes
|
||||||
|
0x00, 0x09, // server_name_list length
|
||||||
|
0x00, // host_name
|
||||||
|
0x00, 0x0a, // name length 10, but only 6 bytes follow
|
||||||
|
'e', 'x', 'a', 'm', 'p', 'l',
|
||||||
|
}
|
||||||
|
body := rebuildHelloWithExtensions(t, msg, bad)
|
||||||
|
if _, err := ltls.ParseClientHello(body); err == nil {
|
||||||
|
t.Error("malformed server_name accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseClientHelloRejectsDuplicateExtension(t *testing.T) {
|
||||||
|
// Duplicating an extension lets this parser and a middlebox act on
|
||||||
|
// different copies, so it is rejected at parse time.
|
||||||
|
msg := captureClientHello(t)
|
||||||
|
exts := msg.ExtensionBytes()
|
||||||
|
var first int
|
||||||
|
for off, ext := range msg.Extensions {
|
||||||
|
first = off + len(ext.Data())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dup := make([]byte, 0, len(exts)+first)
|
||||||
|
dup = append(dup, exts...)
|
||||||
|
dup = append(dup, exts[:first-msg.Spans().Extensions.Off]...)
|
||||||
|
|
||||||
|
body := rebuildHelloWithExtensions(t, msg, dup)
|
||||||
|
_, err := ltls.ParseClientHello(body)
|
||||||
|
if !errors.Is(err, lneto.ErrInvalidField) {
|
||||||
|
t.Errorf("duplicate extension got %v want ErrInvalidField", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildServerHello encodes a ServerHello body with supported_versions and a
|
||||||
|
// key_share, which in the server form is a single entry with no list prefix.
|
||||||
|
func buildServerHello(t *testing.T, sid []byte) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var b ltls.Builder
|
||||||
|
b.Reset(make([]byte, 0, 256))
|
||||||
|
b.AddU16(ltls.VersionTLS12)
|
||||||
|
for range ltls.SizeRandom {
|
||||||
|
b.AddU8(0xab)
|
||||||
|
}
|
||||||
|
b.OpenU8()
|
||||||
|
b.AddBytes(sid)
|
||||||
|
b.Close()
|
||||||
|
b.AddU16(uint16(ltls.SuiteAES128GCMSHA256))
|
||||||
|
b.AddU8(0) // legacy_compression_method
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddU16(uint16(ltls.ExtSupportedVersions))
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddU16(ltls.VersionTLS13)
|
||||||
|
b.Close()
|
||||||
|
b.AddU16(uint16(ltls.ExtKeyShare))
|
||||||
|
b.OpenU16()
|
||||||
|
b.AddU16(uint16(ltls.GroupX25519))
|
||||||
|
b.OpenU16()
|
||||||
|
for range 32 {
|
||||||
|
b.AddU8(0xee)
|
||||||
|
}
|
||||||
|
b.Close()
|
||||||
|
b.Close()
|
||||||
|
b.Close()
|
||||||
|
body, err := b.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerHelloMsg(t *testing.T) {
|
||||||
|
sid := bytes.Repeat([]byte{0xcd}, 32)
|
||||||
|
body := buildServerHello(t, sid)
|
||||||
|
msg, err := ltls.ParseServerHello(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if msg.LegacyVersion() != ltls.VersionTLS12 {
|
||||||
|
t.Errorf("legacy_version %#04x want 0x0303", msg.LegacyVersion())
|
||||||
|
}
|
||||||
|
if !bytes.Equal(msg.LegacySessionIDEcho(), sid) {
|
||||||
|
t.Errorf("session id echo % x want % x", msg.LegacySessionIDEcho(), sid)
|
||||||
|
}
|
||||||
|
if msg.CipherSuite() != ltls.SuiteAES128GCMSHA256 {
|
||||||
|
t.Errorf("cipher suite %v want TLS_AES_128_GCM_SHA256", msg.CipherSuite())
|
||||||
|
}
|
||||||
|
if msg.LegacyCompressionMethod() != 0 {
|
||||||
|
t.Errorf("compression method %d want 0", msg.LegacyCompressionMethod())
|
||||||
|
}
|
||||||
|
|
||||||
|
var version uint16
|
||||||
|
shares := 0
|
||||||
|
for _, ext := range msg.Extensions {
|
||||||
|
switch ext.Type() {
|
||||||
|
case ltls.ExtSupportedVersions:
|
||||||
|
// The ServerHello form is a bare uint16, not a list.
|
||||||
|
if len(ext.Data()) != 2 {
|
||||||
|
t.Fatalf("supported_versions %d bytes want 2", len(ext.Data()))
|
||||||
|
}
|
||||||
|
version = uint16(ext.Data()[0])<<8 | uint16(ext.Data()[1])
|
||||||
|
case ltls.ExtKeyShare:
|
||||||
|
for koff, ks := range ext.KeyShares {
|
||||||
|
shares++
|
||||||
|
if ks.Group != ltls.GroupX25519 || len(ks.Key) != 32 {
|
||||||
|
t.Errorf("key share %v %d bytes", ks.Group, len(ks.Key))
|
||||||
|
}
|
||||||
|
if !locates(body, koff, ks.Key) {
|
||||||
|
t.Errorf("key share at offset %d does not match the wire", koff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if version != ltls.VersionTLS13 {
|
||||||
|
t.Errorf("negotiated version %#04x want 0x0304", version)
|
||||||
|
}
|
||||||
|
if shares != 1 {
|
||||||
|
t.Errorf("walked %d key shares want 1, the server sends a single entry", shares)
|
||||||
|
}
|
||||||
|
|
||||||
|
sp := msg.Spans()
|
||||||
|
if sp.CipherSuites.Len != 2 || !locates(body, sp.CipherSuites.Off, body[sp.CipherSuites.Off:sp.CipherSuites.Off+2]) {
|
||||||
|
t.Errorf("cipher suite span %+v", sp.CipherSuites)
|
||||||
|
}
|
||||||
|
if sp.Compression.Len != 1 {
|
||||||
|
t.Errorf("compression span len %d want 1", sp.Compression.Len)
|
||||||
|
}
|
||||||
|
if !locates(body, sp.Extensions.Off, msg.ExtensionBytes()) {
|
||||||
|
t.Errorf("extensions span %+v does not locate the block", sp.Extensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseServerHelloRejectsMalformed(t *testing.T) {
|
||||||
|
body := buildServerHello(t, bytes.Repeat([]byte{0xcd}, 32))
|
||||||
|
if _, err := ltls.ParseServerHello(append(append([]byte{}, body...), 0xff)); err == nil {
|
||||||
|
t.Error("trailing byte after extensions block accepted")
|
||||||
|
}
|
||||||
|
// No truncation may parse clean, panic, or leave an accessor out of range.
|
||||||
|
for n := range len(body) {
|
||||||
|
msg, err := ltls.ParseServerHello(body[:n])
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("truncation to %d/%d bytes parsed clean", n, len(body))
|
||||||
|
_ = msg.ExtensionBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
// Code generated by "stringer -type=ContentType,HandshakeType,ExtensionType,AlertDescription,AlertLevel,NamedGroup,SignatureScheme,CipherSuite -linecomment -output stringers.go ."; DO NOT EDIT.
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[ContentTypeInvalid-0]
|
||||||
|
_ = x[ContentTypeChangeCipherSpec-20]
|
||||||
|
_ = x[ContentTypeAlert-21]
|
||||||
|
_ = x[ContentTypeHandshake-22]
|
||||||
|
_ = x[ContentTypeApplicationData-23]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ContentType_name_0 = "invalid"
|
||||||
|
_ContentType_name_1 = "change_cipher_specalerthandshakeapplication_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ContentType_index_1 = [...]uint8{0, 18, 23, 32, 48}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i ContentType) String() string {
|
||||||
|
switch {
|
||||||
|
case i == 0:
|
||||||
|
return _ContentType_name_0
|
||||||
|
case 20 <= i && i <= 23:
|
||||||
|
i -= 20
|
||||||
|
return _ContentType_name_1[_ContentType_index_1[i]:_ContentType_index_1[i+1]]
|
||||||
|
default:
|
||||||
|
return "ContentType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[HandshakeTypeClientHello-1]
|
||||||
|
_ = x[HandshakeTypeServerHello-2]
|
||||||
|
_ = x[HandshakeTypeNewSessionTicket-4]
|
||||||
|
_ = x[HandshakeTypeEndOfEarlyData-5]
|
||||||
|
_ = x[HandshakeTypeEncryptedExtensions-8]
|
||||||
|
_ = x[HandshakeTypeCertificate-11]
|
||||||
|
_ = x[HandshakeTypeCertificateRequest-13]
|
||||||
|
_ = x[HandshakeTypeCertificateVerify-15]
|
||||||
|
_ = x[HandshakeTypeFinished-20]
|
||||||
|
_ = x[HandshakeTypeKeyUpdate-24]
|
||||||
|
_ = x[HandshakeTypeMessageHash-254]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_HandshakeType_name_0 = "client_helloserver_hello"
|
||||||
|
_HandshakeType_name_1 = "new_session_ticketend_of_early_data"
|
||||||
|
_HandshakeType_name_2 = "encrypted_extensions"
|
||||||
|
_HandshakeType_name_3 = "certificate"
|
||||||
|
_HandshakeType_name_4 = "certificate_request"
|
||||||
|
_HandshakeType_name_5 = "certificate_verify"
|
||||||
|
_HandshakeType_name_6 = "finished"
|
||||||
|
_HandshakeType_name_7 = "key_update"
|
||||||
|
_HandshakeType_name_8 = "message_hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_HandshakeType_index_0 = [...]uint8{0, 12, 24}
|
||||||
|
_HandshakeType_index_1 = [...]uint8{0, 18, 35}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i HandshakeType) String() string {
|
||||||
|
switch {
|
||||||
|
case 1 <= i && i <= 2:
|
||||||
|
i -= 1
|
||||||
|
return _HandshakeType_name_0[_HandshakeType_index_0[i]:_HandshakeType_index_0[i+1]]
|
||||||
|
case 4 <= i && i <= 5:
|
||||||
|
i -= 4
|
||||||
|
return _HandshakeType_name_1[_HandshakeType_index_1[i]:_HandshakeType_index_1[i+1]]
|
||||||
|
case i == 8:
|
||||||
|
return _HandshakeType_name_2
|
||||||
|
case i == 11:
|
||||||
|
return _HandshakeType_name_3
|
||||||
|
case i == 13:
|
||||||
|
return _HandshakeType_name_4
|
||||||
|
case i == 15:
|
||||||
|
return _HandshakeType_name_5
|
||||||
|
case i == 20:
|
||||||
|
return _HandshakeType_name_6
|
||||||
|
case i == 24:
|
||||||
|
return _HandshakeType_name_7
|
||||||
|
case i == 254:
|
||||||
|
return _HandshakeType_name_8
|
||||||
|
default:
|
||||||
|
return "HandshakeType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[ExtServerName-0]
|
||||||
|
_ = x[ExtMaxFragmentLength-1]
|
||||||
|
_ = x[ExtStatusRequest-5]
|
||||||
|
_ = x[ExtSupportedGroups-10]
|
||||||
|
_ = x[ExtECPointFormats-11]
|
||||||
|
_ = x[ExtSignatureAlgorithms-13]
|
||||||
|
_ = x[ExtALPN-16]
|
||||||
|
_ = x[ExtSignedCertificateTimestamp-18]
|
||||||
|
_ = x[ExtPadding-21]
|
||||||
|
_ = x[ExtExtendedMasterSecret-23]
|
||||||
|
_ = x[ExtCompressCertificate-27]
|
||||||
|
_ = x[ExtRecordSizeLimit-28]
|
||||||
|
_ = x[ExtSessionTicket-35]
|
||||||
|
_ = x[ExtPreSharedKey-41]
|
||||||
|
_ = x[ExtEarlyData-42]
|
||||||
|
_ = x[ExtSupportedVersions-43]
|
||||||
|
_ = x[ExtCookie-44]
|
||||||
|
_ = x[ExtPSKKeyExchangeModes-45]
|
||||||
|
_ = x[ExtCertificateAuthorities-47]
|
||||||
|
_ = x[ExtSignatureAlgorithmsCert-50]
|
||||||
|
_ = x[ExtKeyShare-51]
|
||||||
|
_ = x[ExtApplicationSettings-17513]
|
||||||
|
_ = x[ExtEncryptedClientHello-65037]
|
||||||
|
_ = x[ExtRenegotiationInfo-65281]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _ExtensionType_name = "server_namemax_fragment_lengthstatus_requestsupported_groupsec_point_formatssignature_algorithmsapplication_layer_protocol_negotiationsigned_certificate_timestamppaddingextended_master_secretcompress_certificaterecord_size_limitsession_ticketpre_shared_keyearly_datasupported_versionscookiepsk_key_exchange_modescertificate_authoritiessignature_algorithms_certkey_shareapplication_settingsencrypted_client_hellorenegotiation_info"
|
||||||
|
|
||||||
|
var _ExtensionType_map = map[ExtensionType]string{
|
||||||
|
0: _ExtensionType_name[0:11],
|
||||||
|
1: _ExtensionType_name[11:30],
|
||||||
|
5: _ExtensionType_name[30:44],
|
||||||
|
10: _ExtensionType_name[44:60],
|
||||||
|
11: _ExtensionType_name[60:76],
|
||||||
|
13: _ExtensionType_name[76:96],
|
||||||
|
16: _ExtensionType_name[96:134],
|
||||||
|
18: _ExtensionType_name[134:162],
|
||||||
|
21: _ExtensionType_name[162:169],
|
||||||
|
23: _ExtensionType_name[169:191],
|
||||||
|
27: _ExtensionType_name[191:211],
|
||||||
|
28: _ExtensionType_name[211:228],
|
||||||
|
35: _ExtensionType_name[228:242],
|
||||||
|
41: _ExtensionType_name[242:256],
|
||||||
|
42: _ExtensionType_name[256:266],
|
||||||
|
43: _ExtensionType_name[266:284],
|
||||||
|
44: _ExtensionType_name[284:290],
|
||||||
|
45: _ExtensionType_name[290:312],
|
||||||
|
47: _ExtensionType_name[312:335],
|
||||||
|
50: _ExtensionType_name[335:360],
|
||||||
|
51: _ExtensionType_name[360:369],
|
||||||
|
17513: _ExtensionType_name[369:389],
|
||||||
|
65037: _ExtensionType_name[389:411],
|
||||||
|
65281: _ExtensionType_name[411:429],
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i ExtensionType) String() string {
|
||||||
|
if str, ok := _ExtensionType_map[i]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return "ExtensionType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[AlertCloseNotify-0]
|
||||||
|
_ = x[AlertUnexpectedMessage-10]
|
||||||
|
_ = x[AlertBadRecordMAC-20]
|
||||||
|
_ = x[AlertRecordOverflow-22]
|
||||||
|
_ = x[AlertHandshakeFailure-40]
|
||||||
|
_ = x[AlertBadCertificate-42]
|
||||||
|
_ = x[AlertUnsupportedCertificate-43]
|
||||||
|
_ = x[AlertCertificateRevoked-44]
|
||||||
|
_ = x[AlertCertificateExpired-45]
|
||||||
|
_ = x[AlertCertificateUnknown-46]
|
||||||
|
_ = x[AlertIllegalParameter-47]
|
||||||
|
_ = x[AlertUnknownCA-48]
|
||||||
|
_ = x[AlertAccessDenied-49]
|
||||||
|
_ = x[AlertDecodeError-50]
|
||||||
|
_ = x[AlertDecryptError-51]
|
||||||
|
_ = x[AlertProtocolVersion-70]
|
||||||
|
_ = x[AlertInsufficientSecurity-71]
|
||||||
|
_ = x[AlertInternalError-80]
|
||||||
|
_ = x[AlertInappropriateFallback-86]
|
||||||
|
_ = x[AlertUserCanceled-90]
|
||||||
|
_ = x[AlertMissingExtension-109]
|
||||||
|
_ = x[AlertUnsupportedExtension-110]
|
||||||
|
_ = x[AlertUnrecognizedName-112]
|
||||||
|
_ = x[AlertBadCertificateStatusResponse-113]
|
||||||
|
_ = x[AlertUnknownPSKIdentity-115]
|
||||||
|
_ = x[AlertCertificateRequired-116]
|
||||||
|
_ = x[AlertNoApplicationProtocol-120]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _AlertDescription_name = "close_notifyunexpected_messagebad_record_macrecord_overflowhandshake_failurebad_certificateunsupported_certificatecertificate_revokedcertificate_expiredcertificate_unknownillegal_parameterunknown_caaccess_denieddecode_errordecrypt_errorprotocol_versioninsufficient_securityinternal_errorinappropriate_fallbackuser_canceledmissing_extensionunsupported_extensionunrecognized_namebad_certificate_status_responseunknown_psk_identitycertificate_requiredno_application_protocol"
|
||||||
|
|
||||||
|
var _AlertDescription_map = map[AlertDescription]string{
|
||||||
|
0: _AlertDescription_name[0:12],
|
||||||
|
10: _AlertDescription_name[12:30],
|
||||||
|
20: _AlertDescription_name[30:44],
|
||||||
|
22: _AlertDescription_name[44:59],
|
||||||
|
40: _AlertDescription_name[59:76],
|
||||||
|
42: _AlertDescription_name[76:91],
|
||||||
|
43: _AlertDescription_name[91:114],
|
||||||
|
44: _AlertDescription_name[114:133],
|
||||||
|
45: _AlertDescription_name[133:152],
|
||||||
|
46: _AlertDescription_name[152:171],
|
||||||
|
47: _AlertDescription_name[171:188],
|
||||||
|
48: _AlertDescription_name[188:198],
|
||||||
|
49: _AlertDescription_name[198:211],
|
||||||
|
50: _AlertDescription_name[211:223],
|
||||||
|
51: _AlertDescription_name[223:236],
|
||||||
|
70: _AlertDescription_name[236:252],
|
||||||
|
71: _AlertDescription_name[252:273],
|
||||||
|
80: _AlertDescription_name[273:287],
|
||||||
|
86: _AlertDescription_name[287:309],
|
||||||
|
90: _AlertDescription_name[309:322],
|
||||||
|
109: _AlertDescription_name[322:339],
|
||||||
|
110: _AlertDescription_name[339:360],
|
||||||
|
112: _AlertDescription_name[360:377],
|
||||||
|
113: _AlertDescription_name[377:408],
|
||||||
|
115: _AlertDescription_name[408:428],
|
||||||
|
116: _AlertDescription_name[428:448],
|
||||||
|
120: _AlertDescription_name[448:471],
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i AlertDescription) String() string {
|
||||||
|
if str, ok := _AlertDescription_map[i]; ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return "AlertDescription(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[AlertLevelWarning-1]
|
||||||
|
_ = x[AlertLevelFatal-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _AlertLevel_name = "warningfatal"
|
||||||
|
|
||||||
|
var _AlertLevel_index = [...]uint8{0, 7, 12}
|
||||||
|
|
||||||
|
func (i AlertLevel) String() string {
|
||||||
|
i -= 1
|
||||||
|
if i >= AlertLevel(len(_AlertLevel_index)-1) {
|
||||||
|
return "AlertLevel(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||||
|
}
|
||||||
|
return _AlertLevel_name[_AlertLevel_index[i]:_AlertLevel_index[i+1]]
|
||||||
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[GroupSECP256R1-23]
|
||||||
|
_ = x[GroupSECP384R1-24]
|
||||||
|
_ = x[GroupSECP521R1-25]
|
||||||
|
_ = x[GroupX25519-29]
|
||||||
|
_ = x[GroupX448-30]
|
||||||
|
_ = x[GroupX25519MLKEM768-4588]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_NamedGroup_name_0 = "secp256r1secp384r1secp521r1"
|
||||||
|
_NamedGroup_name_1 = "x25519x448"
|
||||||
|
_NamedGroup_name_2 = "x25519mlkem768"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_NamedGroup_index_0 = [...]uint8{0, 9, 18, 27}
|
||||||
|
_NamedGroup_index_1 = [...]uint8{0, 6, 10}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i NamedGroup) String() string {
|
||||||
|
switch {
|
||||||
|
case 23 <= i && i <= 25:
|
||||||
|
i -= 23
|
||||||
|
return _NamedGroup_name_0[_NamedGroup_index_0[i]:_NamedGroup_index_0[i+1]]
|
||||||
|
case 29 <= i && i <= 30:
|
||||||
|
i -= 29
|
||||||
|
return _NamedGroup_name_1[_NamedGroup_index_1[i]:_NamedGroup_index_1[i+1]]
|
||||||
|
case i == 4588:
|
||||||
|
return _NamedGroup_name_2
|
||||||
|
default:
|
||||||
|
return "NamedGroup(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[SigRSAPKCS1SHA256-1025]
|
||||||
|
_ = x[SigRSAPKCS1SHA384-1281]
|
||||||
|
_ = x[SigRSAPKCS1SHA512-1537]
|
||||||
|
_ = x[SigECDSAP256SHA256-1027]
|
||||||
|
_ = x[SigECDSAP384SHA384-1283]
|
||||||
|
_ = x[SigECDSAP521SHA512-1539]
|
||||||
|
_ = x[SigRSAPSSRSAESHA256-2052]
|
||||||
|
_ = x[SigRSAPSSRSAESHA384-2053]
|
||||||
|
_ = x[SigRSAPSSRSAESHA512-2054]
|
||||||
|
_ = x[SigEd25519-2055]
|
||||||
|
_ = x[SigRSAPSSPSSSHA256-2057]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_SignatureScheme_name_0 = "rsa_pkcs1_sha256"
|
||||||
|
_SignatureScheme_name_1 = "ecdsa_secp256r1_sha256"
|
||||||
|
_SignatureScheme_name_2 = "rsa_pkcs1_sha384"
|
||||||
|
_SignatureScheme_name_3 = "ecdsa_secp384r1_sha384"
|
||||||
|
_SignatureScheme_name_4 = "rsa_pkcs1_sha512"
|
||||||
|
_SignatureScheme_name_5 = "ecdsa_secp521r1_sha512"
|
||||||
|
_SignatureScheme_name_6 = "rsa_pss_rsae_sha256rsa_pss_rsae_sha384rsa_pss_rsae_sha512ed25519"
|
||||||
|
_SignatureScheme_name_7 = "rsa_pss_pss_sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_SignatureScheme_index_6 = [...]uint8{0, 19, 38, 57, 64}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (i SignatureScheme) String() string {
|
||||||
|
switch {
|
||||||
|
case i == 1025:
|
||||||
|
return _SignatureScheme_name_0
|
||||||
|
case i == 1027:
|
||||||
|
return _SignatureScheme_name_1
|
||||||
|
case i == 1281:
|
||||||
|
return _SignatureScheme_name_2
|
||||||
|
case i == 1283:
|
||||||
|
return _SignatureScheme_name_3
|
||||||
|
case i == 1537:
|
||||||
|
return _SignatureScheme_name_4
|
||||||
|
case i == 1539:
|
||||||
|
return _SignatureScheme_name_5
|
||||||
|
case 2052 <= i && i <= 2055:
|
||||||
|
i -= 2052
|
||||||
|
return _SignatureScheme_name_6[_SignatureScheme_index_6[i]:_SignatureScheme_index_6[i+1]]
|
||||||
|
case i == 2057:
|
||||||
|
return _SignatureScheme_name_7
|
||||||
|
default:
|
||||||
|
return "SignatureScheme(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[SuiteAES128GCMSHA256-4865]
|
||||||
|
_ = x[SuiteAES256GCMSHA384-4866]
|
||||||
|
_ = x[SuiteChaCha20Poly1305SHA256-4867]
|
||||||
|
_ = x[SuiteAES128CCMSHA256-4868]
|
||||||
|
_ = x[SuiteAES128CCM8SHA256-4869]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _CipherSuite_name = "TLS_AES_128_GCM_SHA256TLS_AES_256_GCM_SHA384TLS_CHACHA20_POLY1305_SHA256TLS_AES_128_CCM_SHA256TLS_AES_128_CCM_8_SHA256"
|
||||||
|
|
||||||
|
var _CipherSuite_index = [...]uint8{0, 22, 44, 72, 94, 118}
|
||||||
|
|
||||||
|
func (i CipherSuite) String() string {
|
||||||
|
i -= 4865
|
||||||
|
if i >= CipherSuite(len(_CipherSuite_index)-1) {
|
||||||
|
return "CipherSuite(" + strconv.FormatInt(int64(i+4865), 10) + ")"
|
||||||
|
}
|
||||||
|
return _CipherSuite_name[_CipherSuite_index[i]:_CipherSuite_index[i+1]]
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
// StringConst methods name values as String does but always return a
|
||||||
|
// compile-time constant, unlike String which formats an unrecognized value with
|
||||||
|
// strconv. Wire values are attacker controlled, so decoders and logs on
|
||||||
|
// embedded targets use StringConst and print the number alongside it.
|
||||||
|
|
||||||
|
const (
|
||||||
|
// nameGREASE names the reserved values of RFC 8701, which carry no meaning.
|
||||||
|
nameGREASE = "GREASE"
|
||||||
|
// nameUnknown names a value this package does not recognize. Other RFCs may
|
||||||
|
// well define it: this is TLS 1.3 only.
|
||||||
|
nameUnknown = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringConst returns the content type's name, or "unknown".
|
||||||
|
func (v ContentType) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case ContentTypeInvalid, ContentTypeChangeCipherSpec, ContentTypeAlert,
|
||||||
|
ContentTypeHandshake, ContentTypeApplicationData:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConst returns the handshake message type's name, or "unknown".
|
||||||
|
func (v HandshakeType) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case HandshakeTypeClientHello, HandshakeTypeServerHello, HandshakeTypeNewSessionTicket,
|
||||||
|
HandshakeTypeEndOfEarlyData, HandshakeTypeEncryptedExtensions, HandshakeTypeCertificate,
|
||||||
|
HandshakeTypeCertificateRequest, HandshakeTypeCertificateVerify, HandshakeTypeFinished,
|
||||||
|
HandshakeTypeKeyUpdate, HandshakeTypeMessageHash:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConst returns the extension type's name, "GREASE" or "unknown".
|
||||||
|
func (v ExtensionType) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case ExtServerName, ExtMaxFragmentLength, ExtStatusRequest, ExtSupportedGroups,
|
||||||
|
ExtECPointFormats, ExtSignatureAlgorithms, ExtALPN, ExtSignedCertificateTimestamp,
|
||||||
|
ExtPadding, ExtExtendedMasterSecret, ExtCompressCertificate, ExtRecordSizeLimit,
|
||||||
|
ExtSessionTicket, ExtPreSharedKey, ExtEarlyData, ExtSupportedVersions, ExtCookie,
|
||||||
|
ExtPSKKeyExchangeModes, ExtCertificateAuthorities, ExtSignatureAlgorithmsCert,
|
||||||
|
ExtKeyShare, ExtApplicationSettings, ExtEncryptedClientHello, ExtRenegotiationInfo:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
if IsGREASE(uint16(v)) {
|
||||||
|
return nameGREASE
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConst returns the alert level's name, or "unknown".
|
||||||
|
func (v AlertLevel) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case AlertLevelWarning, AlertLevelFatal:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConst returns the alert description's name, or "unknown".
|
||||||
|
func (v AlertDescription) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case AlertCloseNotify, AlertUnexpectedMessage, AlertBadRecordMAC, AlertRecordOverflow,
|
||||||
|
AlertHandshakeFailure, AlertBadCertificate, AlertUnsupportedCertificate,
|
||||||
|
AlertCertificateRevoked, AlertCertificateExpired, AlertCertificateUnknown,
|
||||||
|
AlertIllegalParameter, AlertUnknownCA, AlertAccessDenied, AlertDecodeError,
|
||||||
|
AlertDecryptError, AlertProtocolVersion, AlertInsufficientSecurity,
|
||||||
|
AlertInternalError, AlertInappropriateFallback, AlertUserCanceled,
|
||||||
|
AlertMissingExtension, AlertUnsupportedExtension, AlertUnrecognizedName,
|
||||||
|
AlertBadCertificateStatusResponse, AlertUnknownPSKIdentity,
|
||||||
|
AlertCertificateRequired, AlertNoApplicationProtocol:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConst returns the named group's name, "GREASE" or "unknown".
|
||||||
|
func (v NamedGroup) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case GroupSECP256R1, GroupSECP384R1, GroupSECP521R1,
|
||||||
|
GroupX25519, GroupX448, GroupX25519MLKEM768:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
if IsGREASE(uint16(v)) {
|
||||||
|
return nameGREASE
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConst returns the signature scheme's name, "GREASE" or "unknown".
|
||||||
|
func (v SignatureScheme) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case SigRSAPKCS1SHA256, SigRSAPKCS1SHA384, SigRSAPKCS1SHA512,
|
||||||
|
SigECDSAP256SHA256, SigECDSAP384SHA384, SigECDSAP521SHA512,
|
||||||
|
SigRSAPSSRSAESHA256, SigRSAPSSRSAESHA384, SigRSAPSSRSAESHA512,
|
||||||
|
SigEd25519, SigRSAPSSPSSSHA256:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
if IsGREASE(uint16(v)) {
|
||||||
|
return nameGREASE
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringConst returns the cipher suite's name, "GREASE" or "unknown".
|
||||||
|
// Every TLS 1.2 suite a browser still offers is undefined here: this package
|
||||||
|
// implements TLS 1.3 only.
|
||||||
|
func (v CipherSuite) StringConst() string {
|
||||||
|
switch v {
|
||||||
|
case SuiteAES128GCMSHA256, SuiteAES256GCMSHA384, SuiteChaCha20Poly1305SHA256,
|
||||||
|
SuiteAES128CCMSHA256, SuiteAES128CCM8SHA256:
|
||||||
|
return v.String()
|
||||||
|
}
|
||||||
|
if IsGREASE(uint16(v)) {
|
||||||
|
return nameGREASE
|
||||||
|
}
|
||||||
|
return nameUnknown
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package tls_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto/x/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringConst must name defined values as String does, GREASE values "GREASE"
|
||||||
|
// and everything else "unknown", over the whole range and without allocating.
|
||||||
|
// A constant added without extending a StringConst switch fails here.
|
||||||
|
func TestStringConst(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
n int
|
||||||
|
grease bool // type carries GREASE values
|
||||||
|
str func(int) string
|
||||||
|
cst func(int) string
|
||||||
|
}{
|
||||||
|
{"ContentType", 1 << 8, false,
|
||||||
|
func(i int) string { return tls.ContentType(i).String() },
|
||||||
|
func(i int) string { return tls.ContentType(i).StringConst() }},
|
||||||
|
{"HandshakeType", 1 << 8, false,
|
||||||
|
func(i int) string { return tls.HandshakeType(i).String() },
|
||||||
|
func(i int) string { return tls.HandshakeType(i).StringConst() }},
|
||||||
|
{"AlertLevel", 1 << 8, false,
|
||||||
|
func(i int) string { return tls.AlertLevel(i).String() },
|
||||||
|
func(i int) string { return tls.AlertLevel(i).StringConst() }},
|
||||||
|
{"AlertDescription", 1 << 8, false,
|
||||||
|
func(i int) string { return tls.AlertDescription(i).String() },
|
||||||
|
func(i int) string { return tls.AlertDescription(i).StringConst() }},
|
||||||
|
{"ExtensionType", 1 << 16, true,
|
||||||
|
func(i int) string { return tls.ExtensionType(i).String() },
|
||||||
|
func(i int) string { return tls.ExtensionType(i).StringConst() }},
|
||||||
|
{"NamedGroup", 1 << 16, true,
|
||||||
|
func(i int) string { return tls.NamedGroup(i).String() },
|
||||||
|
func(i int) string { return tls.NamedGroup(i).StringConst() }},
|
||||||
|
{"SignatureScheme", 1 << 16, true,
|
||||||
|
func(i int) string { return tls.SignatureScheme(i).String() },
|
||||||
|
func(i int) string { return tls.SignatureScheme(i).StringConst() }},
|
||||||
|
{"CipherSuite", 1 << 16, true,
|
||||||
|
func(i int) string { return tls.CipherSuite(i).String() },
|
||||||
|
func(i int) string { return tls.CipherSuite(i).StringConst() }},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
for i := range tc.n {
|
||||||
|
str, cst := tc.str(i), tc.cst(i)
|
||||||
|
if !strings.ContainsRune(str, '(') { // stringer names undefined values "Type(9)".
|
||||||
|
if cst != str {
|
||||||
|
t.Fatalf("%s(%d)=%q want %q", tc.name, i, cst, str)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
want := "unknown"
|
||||||
|
if tc.grease && tls.IsGREASE(uint16(i)) {
|
||||||
|
want = "GREASE"
|
||||||
|
}
|
||||||
|
if cst != want {
|
||||||
|
t.Fatalf("%s(%d)=%q want %q", tc.name, i, cst, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allocs := testing.AllocsPerRun(1, func() {
|
||||||
|
for i := range tc.n {
|
||||||
|
_ = tc.cst(i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if allocs != 0 {
|
||||||
|
t.Errorf("%s allocated %v times", tc.name, allocs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user