Files
nats-server/server/server_fuzz_test.go
2025-08-04 11:34:10 +03:00

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