// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4server import ( "net" "testing" "sourcedock.dev/petrbalvin/nfs/internal/rpc" "sourcedock.dev/petrbalvin/nfs/internal/server" "sourcedock.dev/petrbalvin/nfs/internal/krb5" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/nfsclient" ) // The three RPCSEC_GSS service levels round trip against the real TCP // client: the credential sequence window, the verifier MIC over the // call header, the checksummed arguments and results at integrity and // the sealed ones at privacy. func TestKerberosServiceLevels(t *testing.T) { h := testTree(t) key := make([]byte, 32) for i := range key { key[i] = byte(i + 1) } h.ServerKey = key h.ServiceName = "nfs" 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() cases := []struct { name string svc uint32 }{ {"krb5", rpc.SvcNone}, {"krb5i", rpc.SvcIntegrity}, {"krb5p", rpc.SvcPrivacy}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cl, err := nfsclient.Dial(ln.Addr().String()) if err != nil { t.Fatal(err) } defer cl.Close() // The session exists before the GSS switch: the COMPOUND // then carries SEQUENCE under the GSS credential. if err := cl.Establish("gss-" + tc.name); err != nil { t.Fatalf("establish: %v", err) } if err := cl.EnableGSS(krb5.EtypeAES256, key, "EXAMPLE.ORG", "nfs", "petr@EXAMPLE.ORG", tc.svc); err != nil { t.Fatalf("enable gss: %v", err) } res, bodies, err := cl.Compound("gss", [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType, nfs4.AttrSize)), }) if err != nil { t.Fatalf("compound: %v", err) } if res.Status != nfs4.ErrOK || len(bodies) != 2 { t.Fatalf("compound: status %d bodies %d", res.Status, len(bodies)) } // A second call walks the sequence window one further. res, _, err = cl.Compound("gss2", [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), }) if err != nil || res.Status != nfs4.ErrOK { t.Fatalf("second compound: status %d %v", res.Status, err) } if err := cl.DisableGSS(); err != nil { t.Fatalf("disable: %v", err) } // After the destroy the client falls back to AUTH_SYS and the // compound succeeds anonymously again. res2, _, err2 := cl.Compound("after", [][]byte{nfs4.AppendPutRootfh(nil)}) if err2 != nil || res2.Status != nfs4.ErrOK { t.Fatalf("compound after disable: %d %v", res2.Status, err2) } }) } } // RPCSEC_GSSv3: the CREATE control procedure binds assertions to a // child handle and the compounds under the child carry the version // three credential, RFC 7861. func TestGSSv3CreateAndUse(t *testing.T) { h := testTree(t) key := make([]byte, 32) for i := range key { key[i] = byte(i + 9) } h.ServerKey = key h.ServiceName = "nfs" addr := startCBServer(t, h) cl, err := nfsclient.Dial(addr) if err != nil { t.Fatal(err) } defer cl.Close() if err := cl.Establish("v3"); err != nil { t.Fatalf("establish: %v", err) } if err := cl.EnableGSS(krb5.EtypeAES256, key, "EXAMPLE.ORG", "nfs", "petr@EXAMPLE.ORG", rpc.SvcIntegrity); err != nil { t.Fatalf("enable gss: %v", err) } // CREATE with a label assertion over the parent context. child, err := cl.CreateGSSChild([]rpc.Assertion{{ Type: rpc.AssertionLabel, Label: rpc.Label{ LfsId: 1, PiId: 0, Bytes: []byte("secret"), }, }}) if err != nil { t.Fatalf("create: %v", err) } if len(child) == 0 { t.Fatal("no child handle") } // A compound under the child handle rides the version three // credential at the integrity level. res, _, err := cl.Compound("v3", [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), }) if err != nil || res.Status != nfs4.ErrOK { t.Fatalf("compound under child: 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) != "secret" { t.Fatalf("label not bound: %+v", lbl) } // LIST answers the supported assertion types. types, err := cl.ListGSSAssertions() if err != nil { t.Fatalf("list: %v", err) } if len(types) != 2 { t.Fatalf("list types %v", types) } }