// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4server_test import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "math/big" "net" "testing" "time" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/nfs4server" "sourcedock.dev/petrbalvin/nfs/internal/nfsclient" "sourcedock.dev/petrbalvin/nfs/internal/nfsfs" "sourcedock.dev/petrbalvin/nfs/internal/server" ) // benchCert mints one self signed certificate for the TLS tests. func benchCert(t *testing.T) tls.Certificate { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("key: %v", err) } tmpl := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "nfs-test"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, DNSNames: []string{"localhost"}, } der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key) if err != nil { t.Fatalf("certificate: %v", err) } return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key} } // serveTLS starts the handler behind a loopback listener with the given // certificate and returns the address. func serveTLS(t *testing.T, cert tls.Certificate) string { t.Helper() root := t.TempDir() backend, err := nfsfs.NewLocal(root) if err != nil { t.Fatalf("NewLocal: %v", err) } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Listen: %v", err) } t.Cleanup(func() { _ = ln.Close() }) handler := &nfs4server.Handler{FS: backend, TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}}} go func() { _ = (&server.Server{Handle: handler.HandleConn}).Serve(t.Context(), ln) }() return ln.Addr().String() } // TestWireTLSSession drives a full session over an in place TLS upgrade: // the probe answers the STARTTLS token, the handshake runs, and a // compound rides the encrypted connection. func TestWireTLSSession(t *testing.T) { addr := serveTLS(t, benchCert(t)) cl, err := nfsclient.Dial(addr) if err != nil { t.Fatalf("Dial: %v", err) } defer cl.Close() if err := cl.EnableTLS(&tls.Config{InsecureSkipVerify: true, ServerName: "localhost"}); err != nil { t.Fatalf("EnableTLS: %v", err) } if err := cl.Establish("tls-test"); err != nil { t.Fatalf("Establish: %v", err) } res, _, err := cl.Compound("tls-getattr", [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)), }) if err != nil { t.Fatalf("Compound: %v", err) } if res.Status != nfs4.ErrOK { t.Fatalf("getattr status %d over TLS", res.Status) } } // TestWirePlaintextRefusedUnderTLS covers the RFC 9289 policy: a client // that skips STARTTLS and runs procedures in plaintext is refused with // auth too weak, while the NULL procedure still answers. func TestWirePlaintextRefusedUnderTLS(t *testing.T) { addr := serveTLS(t, benchCert(t)) cl, err := nfsclient.Dial(addr) if err != nil { t.Fatalf("Dial: %v", err) } defer cl.Close() if err := cl.Null(); err != nil { t.Fatalf("the plaintext NULL no longer answers: %v", err) } if err := cl.Establish("plaintext-test"); err == nil { t.Fatal("a plaintext session was accepted under a TLS certificate") } }