Files

107 lines
3.6 KiB
Go
Raw Permalink Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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")
}
}