mirror of
https://github.com/soypat/lneto.git
synced 2026-09-01 04:19:05 +00:00
use top level method signature for iterators
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user