// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package rpc import ( "bytes" "encoding/hex" "testing" "sourcedock.dev/petrbalvin/nfs/internal/krb5" ) // The version one credential pins the exact wire order RFC 2203 // section 5.2.1 fixes: version, procedure, sequence, service, handle. func TestGSSCredWireOrder(t *testing.T) { data := AppendGSSCred(nil, GSSProcData, 7, SvcIntegrity, []byte("handle-1")) want, err := hex.DecodeString("00000001" + "00000000" + "00000007" + "00000002" + "00000008" + "68616e646c652d31") if err != nil { t.Fatal(err) } if !bytes.Equal(data, want) { t.Fatalf("cred bytes %x, want %x", data, want) } cred, err := DecodeGSSCred(data) if err != nil { t.Fatal(err) } if cred.Proc != GSSProcData || cred.Version != GSSVersion1 || cred.Service != SvcIntegrity || string(cred.Handle) != "handle-1" || cred.Seq != 7 { t.Fatalf("cred %+v", cred) } // The control credentials a conformant peer sends: the same order, // an empty handle and the token in the procedure arguments. init := AppendGSSCred(nil, GSSProcInit, 0, 0, nil) if cred, err = DecodeGSSCred(init); err != nil || cred.Proc != GSSProcInit || len(cred.Handle) != 0 { t.Fatalf("init cred %+v %v", cred, err) } cont := AppendGSSCred(nil, GSSProcContinue, 0, 0, nil) if cred, err = DecodeGSSCred(cont); err != nil || cred.Proc != GSSProcContinue { t.Fatalf("continue cred %+v %v", cred, err) } dest := AppendGSSCred(nil, GSSProcDestroy, 8, SvcIntegrity, []byte("handle-1")) if cred, err = DecodeGSSCred(dest); err != nil || cred.Proc != GSSProcDestroy || string(cred.Handle) != "handle-1" { t.Fatalf("destroy cred %+v %v", cred, err) } // A credential that opens with another version or names an unknown // procedure is refused. bad := append([]byte{}, dest...) bad[0] = 3 // a version three body belongs to the GSSv3 decoder if _, err = DecodeGSSCred(bad); err == nil { t.Fatal("version three accepted by the version one decoder") } bad[0] = GSSVersion1 bad[4] = 9 if _, err = DecodeGSSCred(bad); err == nil { t.Fatal("unknown procedure accepted") } } // A full DATA call: the header with an empty verifier is checksummed, // then the call is re-encoded with the MIC as the verifier. func TestGSSCredAndVerf(t *testing.T) { data := AppendGSSCred(nil, GSSProcData, 7, SvcIntegrity, []byte("handle-1")) call := Call{XID: 99, Program: 100003, Version: 4, Procedure: 1, Cred: Auth{Flavor: FlavorGSS, Body: data}} prefix, err := AppendCall(nil, call) if err != nil { t.Fatal(err) } // Two halves of one established context share the session key. clientCtx := &krb5.Context{Etype: krb5.EtypeAES256, Key: make([]byte, 32)} serverCtx := &krb5.Context{Etype: krb5.EtypeAES256, Key: clientCtx.Key, Accepting: true} mic, err := clientCtx.GetMIC(prefix) if err != nil { t.Fatal(err) } call.Verifier = Auth{Flavor: FlavorGSS, Body: mic} full, err := AppendCall(nil, call) if err != nil { t.Fatal(err) } decoded, args, err := DecodeCall(full) if err != nil { t.Fatal(err) } if decoded.Verifier.Flavor != FlavorGSS { t.Fatalf("verifier flavor %d", decoded.Verifier.Flavor) } // The receiver re-derives the prefix by re-encoding with an empty // verifier and verifies the MIC over it. again, err := AppendCall(nil, Call{XID: decoded.XID, Program: decoded.Program, Version: decoded.Version, Procedure: decoded.Procedure, Cred: decoded.Cred}) if err != nil { t.Fatal(err) } if err := serverCtx.VerifyMIC(again, decoded.Verifier.Body); err != nil { t.Fatalf("verifier MIC: %v", err) } if len(args) != 0 { t.Fatal("stray arguments after the header") } }