// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfsclient import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "math/big" "net" "os" "path/filepath" "testing" "time" "sourcedock.dev/petrbalvin/nfs/internal/krb5" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/nfs4server" "sourcedock.dev/petrbalvin/nfs/internal/nfsfs" "sourcedock.dev/petrbalvin/nfs/internal/rpc" "sourcedock.dev/petrbalvin/nfs/internal/server" ) func testCert(t *testing.T) *tls.Certificate { t.Helper() priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatal(err) } tmpl := x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "nfs.test"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, } der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) if err != nil { t.Fatal(err) } return &tls.Certificate{Certificate: [][]byte{der}, PrivateKey: priv} } // The whole modern security stack of one client session: the AUTH_TLS // upgrade of RFC 9289, the RPCSEC_GSS context of RFC 2203 and the // version three child context with assertions of RFC 7861, all against // the real server over TCP at the privacy level. func TestTLSAndGSSv3Stack(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "s.txt"), []byte("stack"), 0o644); err != nil { t.Fatal(err) } backend, err := nfsfs.NewLocal(root) if err != nil { t.Fatal(err) } cert := testCert(t) key := make([]byte, 32) for i := range key { key[i] = byte(i + 5) } h := &nfs4server.Handler{FS: backend, ServerKey: key, ServiceName: "nfs", TLSConfig: &tls.Config{Certificates: []tls.Certificate{*cert}}} ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } srv := &server.Server{Handle: h.HandleConn} go srv.Serve(t.Context(), ln) defer ln.Close() cl, err := Dial(ln.Addr().String()) if err != nil { t.Fatal(err) } defer cl.Close() // The TLS upgrade rides the AUTH_TLS probe. cfg := &tls.Config{ServerName: "localhost", RootCAs: func() *x509.CertPool { pool := x509.NewCertPool() leaf, lerr := x509.ParseCertificate(cert.Certificate[0]) if lerr != nil { t.Fatal(lerr) } pool.AddCert(leaf) return pool }()} if err := cl.EnableTLS(cfg); err != nil { t.Fatalf("enable tls: %v", err) } // The session and the parent GSS context establish on the encrypted // channel. if err := cl.Establish("stack"); err != nil { t.Fatalf("establish: %v", err) } if err := cl.EnableGSS(krb5.EtypeAES256, key, "EXAMPLE.ORG", "nfs", "petr@EXAMPLE.ORG", rpc.SvcPrivacy); err != nil { t.Fatalf("enable gss: %v", err) } // CREATE binds a label assertion to a version three child handle. child, err := cl.CreateGSSChild([]rpc.Assertion{{ Type: rpc.AssertionLabel, Label: rpc.Label{LfsId: 4, Bytes: []byte("top")}, }}) if err != nil { t.Fatalf("create: %v", err) } // A compound under the child handle rides TLS plus the privacy // protected version three credential. res, _, err := cl.Compound("stack", [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "s.txt"), }) if err != nil || res.Status != nfs4.ErrOK { t.Fatalf("compound: status %d %v", res.Status, err) } // The server bound the label to the child context. if lbl := h.LabelOf(child); lbl == nil || string(lbl.Bytes) != "top" { t.Fatalf("label not bound") } // LIST answers the supported assertion types. types, err := cl.ListGSSAssertions() if err != nil { t.Fatalf("list: %v", err) } if len(types) != 2 { t.Fatalf("types %v", types) } // The destroy of the parent retires the child with it. if err := cl.DisableGSS(); err != nil { t.Fatalf("disable: %v", err) } }