mirror of
https://github.com/hanzoai/pubsub.git
synced 2026-10-30 15:06:26 +00:00
411 lines
9.4 KiB
Go
411 lines
9.4 KiB
Go
// Copyright 2025 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
tlsHandshake byte = 22
|
|
tlsClientHello byte = 1
|
|
)
|
|
|
|
type ClientHelloInjector struct {
|
|
sock io.ReadWriteCloser
|
|
tlsVersion uint16
|
|
buf []byte
|
|
}
|
|
|
|
func NewClientHelloInjector(s io.ReadWriteCloser, tlsVer uint16, b []byte) *ClientHelloInjector {
|
|
return &ClientHelloInjector{
|
|
sock: s,
|
|
tlsVersion: tlsVer,
|
|
buf: b,
|
|
}
|
|
}
|
|
|
|
func (i *ClientHelloInjector) inject(b []byte) []byte {
|
|
if !(b != nil && b[0] == tlsHandshake) {
|
|
return b
|
|
}
|
|
|
|
hsLen := (uint16(b[3]) << 8) + uint16(b[4])
|
|
|
|
if !(hsLen > 0 && b[5] == tlsClientHello) {
|
|
return b
|
|
}
|
|
|
|
// fuzz tls version in client hello
|
|
b[9] = uint8(i.tlsVersion >> 8)
|
|
b[10] = uint8(i.tlsVersion & 0xFF)
|
|
|
|
// Go to begin of random opaque
|
|
offset := 11
|
|
|
|
randomOpaque := b[offset : offset+32]
|
|
|
|
copy(randomOpaque, i.buf)
|
|
|
|
offset += 32
|
|
|
|
sessionIDLen := b[offset]
|
|
|
|
// Skip session id len + opaque
|
|
offset += 1 + int(sessionIDLen)
|
|
|
|
cypherSuiteLen := (uint16(b[offset]) << 8) + uint16(b[offset+1])
|
|
|
|
// Skip cypherSuiteLen
|
|
offset += 2
|
|
|
|
cupherSuites := b[offset : offset+int(cypherSuiteLen)]
|
|
|
|
// Leave unchanged if i.cypherSuites empty
|
|
copy(cupherSuites, i.buf)
|
|
|
|
// Skip cypherSuites
|
|
offset += int(cypherSuiteLen)
|
|
|
|
// Skip CompressionMethod
|
|
offset += 2
|
|
|
|
extensionsLen := (uint16(b[offset]) << 8) + uint16(b[offset+1])
|
|
|
|
// Skip extensions length
|
|
offset += 2
|
|
|
|
// Extensions slice. Stub for future use
|
|
_ = b[offset : offset+int(extensionsLen)]
|
|
|
|
return b
|
|
}
|
|
|
|
func (i *ClientHelloInjector) Write(b []byte) (int, error) {
|
|
return i.sock.Write(i.inject(b))
|
|
}
|
|
|
|
func (i *ClientHelloInjector) Read(b []byte) (int, error) {
|
|
return i.sock.Read(b)
|
|
}
|
|
|
|
func (i *ClientHelloInjector) Close() error {
|
|
return i.sock.Close()
|
|
}
|
|
|
|
type FakeSocket struct {
|
|
sockName string
|
|
buf []byte
|
|
data chan []byte
|
|
done chan struct{}
|
|
}
|
|
|
|
func NewFakeSocket(name string, capacity int) *FakeSocket {
|
|
return &FakeSocket{
|
|
sockName: name,
|
|
data: make(chan []byte, capacity),
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (s *FakeSocket) Write(b []byte) (int, error) {
|
|
select {
|
|
case s.data <- b:
|
|
return len(b), nil
|
|
case <-s.done:
|
|
return 0, net.ErrClosed
|
|
}
|
|
}
|
|
|
|
func (s *FakeSocket) readChunk(b []byte) (int, error) {
|
|
n := copy(b, s.buf)
|
|
s.buf = s.buf[n:]
|
|
return n, nil
|
|
}
|
|
|
|
func (s *FakeSocket) Read(b []byte) (int, error) {
|
|
if len(s.buf) > 0 {
|
|
return s.readChunk(b)
|
|
}
|
|
|
|
select {
|
|
case buf, ok := <-s.data:
|
|
if !ok {
|
|
return 0, nil
|
|
}
|
|
s.buf = buf
|
|
return s.readChunk(b)
|
|
case <-s.done:
|
|
return 0, nil
|
|
}
|
|
}
|
|
|
|
func (s *FakeSocket) Close() error {
|
|
close(s.done)
|
|
return nil
|
|
}
|
|
|
|
type FakeConn struct {
|
|
local io.ReadWriteCloser
|
|
remote io.ReadWriteCloser
|
|
}
|
|
|
|
func NewFakeConn(loc io.ReadWriteCloser, rem io.ReadWriteCloser) *FakeConn {
|
|
return &FakeConn{
|
|
local: loc,
|
|
remote: rem,
|
|
}
|
|
}
|
|
|
|
func (c *FakeConn) Read(b []byte) (int, error) {
|
|
return c.local.Read(b)
|
|
}
|
|
|
|
func (c *FakeConn) Write(b []byte) (int, error) {
|
|
return c.remote.Write(b)
|
|
}
|
|
|
|
func (c *FakeConn) Close() error {
|
|
return c.local.Close()
|
|
}
|
|
|
|
func (c *FakeConn) LocalAddr() net.Addr {
|
|
return &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 4222, Zone: ""}
|
|
}
|
|
|
|
func (c *FakeConn) RemoteAddr() net.Addr {
|
|
return &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 4222, Zone: ""}
|
|
}
|
|
|
|
func (c *FakeConn) SetDeadline(t time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
func (c *FakeConn) SetReadDeadline(t time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
func (c *FakeConn) SetWriteDeadline(t time.Time) error {
|
|
return nil
|
|
}
|
|
|
|
type FakeListener struct {
|
|
ch chan *FakeConn
|
|
acceptErr error
|
|
}
|
|
|
|
func NewFakeListener() *FakeListener {
|
|
return &FakeListener{
|
|
ch: make(chan *FakeConn),
|
|
acceptErr: nil,
|
|
}
|
|
}
|
|
|
|
func (ln *FakeListener) Accept() (c net.Conn, err error) {
|
|
return <-ln.ch, ln.acceptErr
|
|
}
|
|
|
|
func (ln *FakeListener) Close() error {
|
|
ln.acceptErr = io.EOF
|
|
close(ln.ch)
|
|
return nil
|
|
}
|
|
|
|
func (ln *FakeListener) Addr() net.Addr {
|
|
return &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 4222, Zone: ""}
|
|
}
|
|
|
|
func getTlsVersion(useTls13 bool) uint16 {
|
|
if useTls13 {
|
|
return tls.VersionTLS13
|
|
}
|
|
|
|
return tls.VersionTLS12
|
|
}
|
|
|
|
func corruptCert(crt []byte, i uint16) []byte {
|
|
crt[int(i)%len(crt)] ^= 0xFF
|
|
return crt
|
|
}
|
|
|
|
func runServerWithListener(ln net.Listener, opts *Options) *Server {
|
|
if opts == nil {
|
|
opts = DefaultOptions()
|
|
}
|
|
s, err := NewServer(opts)
|
|
if err != nil || s == nil {
|
|
panic(fmt.Sprintf("No NATS Server object returned: %v", err))
|
|
}
|
|
|
|
if !opts.NoLog {
|
|
s.ConfigureLogger()
|
|
}
|
|
|
|
s.listener = ln
|
|
s.listenerErr = nil
|
|
|
|
// Run server in Go routine.
|
|
s.Start()
|
|
|
|
// Wait for accept loop(s) to be started
|
|
if err := s.readyForConnections(10 * time.Second); err != nil {
|
|
panic(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
type MathRandReader byte
|
|
|
|
func (m MathRandReader) Read(buf []byte) (int, error) {
|
|
for i := range buf {
|
|
buf[i] = byte(m)
|
|
}
|
|
return len(buf), nil
|
|
}
|
|
|
|
// FuzzServerTLS performs fuzz testing of the NATS server's TLS handshake implementation.
|
|
// It verifies the server's ability to handle various TLS connection scenarios, including:
|
|
// - Different TLS versions (1.2 and 1.3)
|
|
// - Malformed/mutated client certificates
|
|
// - Corrupted TLS handshake data
|
|
// - Edge cases in the TLS negotiation process
|
|
//
|
|
// Test Setup:
|
|
// - Configures a server with mutual TLS authentication using test certificates
|
|
// - Creates a client with configurable TLS parameters
|
|
// - Uses fake network connections to inject test cases
|
|
//
|
|
// Fuzzing Parameters:
|
|
// - useTls13: Boolean flag to test TLS 1.3 (true) or TLS 1.2 (false)
|
|
// - tlsVer: TLS version number to use in ClientHello
|
|
// - buf: Additional bytes to inject into ClientHello message
|
|
// - corruptCertOffset: Position to corrupt in client certificate (MaxUint16 = no corruption)
|
|
//
|
|
// Expectations
|
|
// The server should either:
|
|
// - Successfully complete the TLS handshake and protocol exchange, or
|
|
// - Cleanly reject invalid connections without crashing
|
|
func FuzzServerTLS(f *testing.F) {
|
|
srvTc := &TLSConfigOpts{
|
|
CertFile: "../test/configs/certs/tlsauth/server.pem",
|
|
KeyFile: "../test/configs/certs/tlsauth/server-key.pem",
|
|
CaFile: "../test/configs/certs/tlsauth/ca.pem",
|
|
Insecure: false,
|
|
Verify: true,
|
|
}
|
|
|
|
srvTlsCfg, err := GenTLSConfig(srvTc)
|
|
if err != nil {
|
|
f.Fatalf("Error generating server tls config: %v", err)
|
|
}
|
|
|
|
opts := &Options{
|
|
Host: "127.0.0.1",
|
|
Port: 4222,
|
|
NoLog: true,
|
|
NoSigs: true,
|
|
Debug: true,
|
|
Trace: true,
|
|
TLSHandshakeFirst: true,
|
|
AllowNonTLS: false,
|
|
JetStream: false,
|
|
TLSConfig: srvTlsCfg,
|
|
CheckConfig: false,
|
|
}
|
|
|
|
clientCerts, err := tls.LoadX509KeyPair("../test/configs/certs/tlsauth/client.pem", "../test/configs/certs/tlsauth/client-key.pem")
|
|
if err != nil {
|
|
f.Fatalf("client1 certificate load error: %s", err)
|
|
}
|
|
|
|
clientTlsCfg := &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
Rand: MathRandReader(0),
|
|
Time: func() time.Time { return time.Date(2025, 1, 1, 1, 1, 1, 1, nil) },
|
|
}
|
|
|
|
tlsVer := uint16(0x0303)
|
|
|
|
corpuses := []struct {
|
|
useTls13 bool
|
|
clientHelloTlsVer uint16
|
|
buf []byte
|
|
corruptCertOffset uint16
|
|
}{
|
|
{useTls13: false, clientHelloTlsVer: tlsVer, buf: []byte{}, corruptCertOffset: math.MaxUint16},
|
|
{useTls13: true, clientHelloTlsVer: tlsVer, buf: []byte{}, corruptCertOffset: math.MaxUint16},
|
|
}
|
|
|
|
for _, crp := range corpuses {
|
|
f.Add(crp.useTls13, crp.clientHelloTlsVer, crp.buf, crp.corruptCertOffset)
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, useTls13 bool, tlsVer uint16, buf []byte, corruptCertOffset uint16) {
|
|
ln := NewFakeListener()
|
|
s := runServerWithListener(ln, opts.Clone())
|
|
defer s.Shutdown()
|
|
|
|
clientSocket := NewFakeSocket("CLIENT", 8)
|
|
serverSocket := NewClientHelloInjector(NewFakeSocket("SERVER", 8), tlsVer, buf)
|
|
|
|
clientConn := NewFakeConn(clientSocket, serverSocket)
|
|
serverConn := NewFakeConn(serverSocket, clientSocket)
|
|
|
|
// Connect to server
|
|
ln.ch <- serverConn
|
|
|
|
tlsVersion := getTlsVersion(useTls13)
|
|
|
|
tlsCfg := clientTlsCfg.Clone()
|
|
tlsCfg.GetClientCertificate = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
|
if corruptCertOffset == math.MaxUint16 {
|
|
t.Log("Leave certificate unchanged")
|
|
return &clientCerts, nil
|
|
}
|
|
|
|
origCert := clientCerts.Certificate[0]
|
|
newCert := make([]byte, len(origCert))
|
|
copy(newCert, origCert)
|
|
|
|
newTlsCerts := clientCerts
|
|
newTlsCerts.Certificate[0] = corruptCert(newCert, corruptCertOffset)
|
|
|
|
return &newTlsCerts, nil
|
|
}
|
|
tlsCfg.MaxVersion = tlsVersion
|
|
tlsCfg.MinVersion = tlsVersion
|
|
|
|
tlsClientConn := tls.Client(clientConn, tlsCfg)
|
|
defer tlsClientConn.Close()
|
|
|
|
if err := tlsClientConn.Handshake(); err != nil {
|
|
t.Logf("Handshake error: %v", err)
|
|
return
|
|
}
|
|
|
|
br := bufio.NewReaderSize(tlsClientConn, 128)
|
|
if _, err := br.ReadString('\n'); err != nil {
|
|
t.Logf("Unexpected error reading INFO message: %v", err)
|
|
return
|
|
}
|
|
})
|
|
}
|