// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // The RPCSEC_GSS server side: the context store keyed by handle, the // RPCSEC_GSS_INIT and DESTROY procedures, and the credential evaluation // of COMPOUND calls at the service levels none, integrity and privacy. package nfs4server import ( crand "crypto/rand" "sync" "sourcedock.dev/petrbalvin/nfs/internal/krb5" "sourcedock.dev/petrbalvin/nfs/internal/rpc" ) // gssMajorStatus values as RFC 2743 section 1.2.2 encodes them: the // continue needed supplementary bit, and the routine errors shifted // into bits sixteen and up. const ( gssMajorOK = 0 gssMajorContinueNeeded = 1 gssMajorDefectiveToken = 9 << 16 gssMajorFailure = 16 << 16 ) // The GSS sequence window the server accepts, RFC 2203 section 5.2.2. const gssWindow = 32 // A gssSession is one established security context and the sequence // bookkeeping of its credential. The mutex guards the bookkeeping and // the token operations together: one context handle presented on two // connections must not interleave its anti-replay window or its // sequence counters, RFC 2203 section 5.2.2. type gssSession struct { mu sync.Mutex ctx *krb5.Context service uint32 lastSeq uint32 seqSet bool label *rpc.Label privs []rpc.Privs } // gssStore keeps the established contexts by their handle. The handles // are random, not a counter, so one cannot be guessed and destroyed by // enumeration. type gssStore struct { mu sync.Mutex byKey map[string]*gssSession } func newGSSStore() *gssStore { return &gssStore{byKey: make(map[string]*gssSession)} } func (s *gssStore) put(sess *gssSession) []byte { handle := make([]byte, 8) if _, err := crand.Read(handle); err != nil { panic("nfs4server: the random source failed: " + err.Error()) } handle[0] = 'G' handle[1] = 'S' s.mu.Lock() defer s.mu.Unlock() s.byKey[string(handle)] = sess return handle } func (s *gssStore) get(handle []byte) (*gssSession, bool) { s.mu.Lock() defer s.mu.Unlock() sess, ok := s.byKey[string(handle)] return sess, ok } func (s *gssStore) drop(handle []byte) { s.mu.Lock() delete(s.byKey, string(handle)) s.mu.Unlock() } // gssStore returns the context store, made once per handler. A handler // without a ServerKey never establishes contexts. func (h *Handler) gssSessions() *gssStore { h.mu.Lock() defer h.mu.Unlock() if h.gssSt == nil { h.gssSt = newGSSStore() } return h.gssSt } // gssInit serves RPCSEC_GSS_INIT: the call data carries the initiator // context token, which is verified against the service key, stored // under a fresh handle and answered with the RPCSEC_GSS_INIT result // holding the handle and the AP-REP. func (h *Handler) gssInit(token []byte) []byte { var major, minor uint32 var handle, reply []byte if h.ServerKey == nil { major = gssMajorFailure // no key configured: refuse } else { ctx, rep, aerr := krb5.AcceptInit(token, h.ServerKey) if aerr != nil { major = gssMajorDefectiveToken minor = 1 } else { handle = h.gssSessions().put(&gssSession{ctx: ctx}) reply = rep } } return rpc.AppendGSSInitRes(nil, handle, major, minor, gssWindow, reply) } // gssDestroy retires the context the credential names. RFC 2203 // section 5.2.3 requires the request to carry a valid verifier under // the very context it destroys; a request without one, or for a context // this server never established, is refused. func (h *Handler) gssDestroy(call rpc.Call) bool { cred, err := rpc.DecodeGSSCred(call.Cred.Body) if err != nil || cred.Proc != rpc.GSSProcDestroy { return false } sess, ok := h.gssSessions().get(cred.Handle) if !ok { return false } prefix, err := rpc.AppendCall(nil, rpc.Call{ XID: call.XID, Program: call.Program, Version: call.Version, Procedure: call.Procedure, Cred: call.Cred, }) if err != nil { return false } if sess.ctx.VerifyMIC(prefix, call.Verifier.Body) != nil { return false } h.gssSessions().drop(cred.Handle) return true } // gssCompound evaluates a COMPOUND call under RPCSEC_GSS: it verifies // the verifier MIC over the call header, unwraps or checksum-verifies // the arguments per the service level, runs the compound and protects // the results the same way. The reply verifier is generated under the // same session lock as the protected body, so one session's tokens // leave the server in the order a client verifies them. The last answer // is false when the RPC layer must answer GARBAGE_ARGS. func (h *Handler) gssCompound(call rpc.Call, args []byte) ([]byte, rpc.Auth, bool) { var gcred rpc.GSSCred if peekU32(call.Cred.Body) == rpc.GSSVersion3 { // A version three credential carries the version in front. v3, verr := rpc.DecodeGSSv3Cred(call.Cred.Body) if verr != nil || v3.Proc != rpc.GSSProcData { return nil, rpc.Auth{}, false } gcred = rpc.GSSCred{Proc: rpc.GSSProcData, Version: rpc.GSSVersion3, Service: v3.Service, Handle: v3.Handle, Seq: v3.Seq} } else { var derr error gcred, derr = rpc.DecodeGSSCred(call.Cred.Body) if derr != nil || gcred.Proc != rpc.GSSProcData || gcred.Version != rpc.GSSVersion1 { return nil, rpc.Auth{}, false } } sess, ok := h.gssSessions().get(gcred.Handle) if !ok { return nil, rpc.Auth{}, false } sess.mu.Lock() defer sess.mu.Unlock() if sess.seqSet && gcred.Seq != sess.lastSeq+1 && !(gcred.Seq > sess.lastSeq) { return nil, rpc.Auth{}, false } // The verifier is a MIC over the call header with an empty verifier // field: re-encode that prefix and check the token against it. prefix, err := rpc.AppendCall(nil, rpc.Call{ XID: call.XID, Program: call.Program, Version: call.Version, Procedure: call.Procedure, Cred: call.Cred, }) if err != nil { return nil, rpc.Auth{}, false } if err := sess.ctx.VerifyMIC(prefix, call.Verifier.Body); err != nil { return nil, rpc.Auth{}, false } sess.lastSeq = gcred.Seq sess.service = gcred.Service sess.seqSet = true var compoundArgs []byte switch sess.service { case rpc.SvcPrivacy: if compoundArgs, err = sess.ctx.Unwrap(args); err != nil { return nil, rpc.Auth{}, false } case rpc.SvcIntegrity: if len(args) < 28 { return nil, rpc.Auth{}, false } compoundArgs = args[:len(args)-28] if err := sess.ctx.VerifyMIC(compoundArgs, args[len(args)-28:]); err != nil { return nil, rpc.Auth{}, false } default: compoundArgs = args } body, ok := h.compoundCtx(compoundArgs, h.gssCred(sess), nil) if !ok { return nil, rpc.Auth{}, false } verf := rpc.Auth{} if sess.service != rpc.SvcNone { replyPrefix, perr := rpc.AppendAcceptedReply(nil, call.XID, rpc.Auth{}, rpc.AcceptSuccess, rpc.Mismatch{}) if perr != nil { return nil, rpc.Auth{}, false } mic, merr := sess.ctx.GetMIC(replyPrefix) if merr != nil { return nil, rpc.Auth{}, false } verf = rpc.Auth{Flavor: rpc.FlavorGSS, Body: mic} } switch sess.service { case rpc.SvcPrivacy: if body, err = sess.ctx.Wrap(body); err != nil { return nil, rpc.Auth{}, false } case rpc.SvcIntegrity: mic, merr := sess.ctx.GetMIC(body) if merr != nil { return nil, rpc.Auth{}, false } body = append(append([]byte{}, body...), mic...) } return body, verf, true } // gssCred answers the identity the operations of one context run as. // Every principal this server authenticates maps to the anonymous // identity until a mapping table exists, so no principal silently // becomes root over the export. func (h *Handler) gssCred(sess *gssSession) cred { return cred{uid: 65534, gid: 65534} } // gssCreate serves RPCSEC_GSS_CREATE over a version three credential: // the parent context is looked up, the multi-principal assertion is // verified against the inner handle, labels and privileges are accepted // into the new child session and the child handle answers the request, // RFC 7861 section 2.7.1. func (h *Handler) gssCreate(v3 rpc.GSSv3Cred, headerPrefix []byte, callData []byte) ([]byte, bool) { parent, ok := h.gssSessions().get(v3.Handle) if !ok { return nil, false } mpAuth, chanBind, assertions, err := rpc.DecodeCreateArgs(callData) if err != nil { return nil, false } // Multi-principal authentication rides only over privacy and binds // the inner handle by its MIC over this call header. var resMp *rpc.MpAuth if mpAuth != nil { if v3.Service != rpc.SvcPrivacy { return nil, false } inner, ok := h.gssSessions().get(mpAuth.InnerHandle) if !ok { return nil, false } if err := inner.ctx.VerifyMIC(headerPrefix, mpAuth.HeaderMic); err != nil { return nil, false } resMic, merr := inner.ctx.GetMIC(headerPrefix) if merr != nil { return nil, false } resMp = &rpc.MpAuth{InnerHandle: mpAuth.InnerHandle, HeaderMic: resMic} } _ = chanBind // channel binding: asserted, unverified in this build var granted []rpc.Assertion child := &gssSession{ctx: parent.ctx, service: v3.Service} for _, a := range assertions { switch a.Type { case rpc.AssertionLabel: child.label = &a.Label granted = append(granted, a) case rpc.AssertionPrivs: child.privs = append(child.privs, a.Privs) granted = append(granted, a) default: // Unsupported assertions are dropped, not granted. } } childHandle := h.gssSessions().put(child) return rpc.AppendCreateRes(nil, childHandle, resMp, nil, granted), true } // gssList serves RPCSEC_GSS_LIST: the assertion types this server // grants. func (h *Handler) gssList() []byte { return rpc.AppendListRes(nil, []uint32{rpc.AssertionLabel, rpc.AssertionPrivs}) } // labelOf answers the label assertion of a context, if any. func (h *Handler) labelOf(handle []byte) *rpc.Label { sess, ok := h.gssSessions().get(handle) if !ok { return nil } return sess.label } // peekU32 reads the first word of a credential body without consuming // it: the version three credential starts with the version field while // the version one form starts with the control procedure. func peekU32(body []byte) uint32 { if len(body) < 4 { return 0 } return uint32(body[0])<<24 | uint32(body[1])<<16 | uint32(body[2])<<8 | uint32(body[3]) } // gssv3Control serves the RPCSEC_GSS_CREATE and LIST control messages, // which ride on NULLPROC under a version three credential protected at // the integrity or privacy level, RFC 7861 section 5.2. The reply // verifier is generated under the same session lock as the protected // result, so the session's tokens leave the server in the order a // client verifies them. func (h *Handler) gssv3Control(call rpc.Call, args []byte) ([]byte, rpc.Auth, bool) { v3, err := rpc.DecodeGSSv3Cred(call.Cred.Body) if err != nil { return nil, rpc.Auth{}, false } sess, ok := h.gssSessions().get(v3.Handle) if !ok { return nil, rpc.Auth{}, false } sess.mu.Lock() defer sess.mu.Unlock() if sess.seqSet && v3.Seq != sess.lastSeq+1 && !(v3.Seq > sess.lastSeq) { return nil, rpc.Auth{}, false } prefix, err := rpc.AppendCall(nil, rpc.Call{ XID: call.XID, Program: call.Program, Version: call.Version, Procedure: call.Procedure, Cred: call.Cred, }) if err != nil { return nil, rpc.Auth{}, false } if err := sess.ctx.VerifyMIC(prefix, call.Verifier.Body); err != nil { return nil, rpc.Auth{}, false } sess.lastSeq = v3.Seq sess.seqSet = true // The call data carries the control payload protected at the // session's service level, for every control procedure: a client // that checksummed or sealed its arguments must see them verified, // or the shared sequence counters of the context drift apart. var callData []byte switch v3.Service { case rpc.SvcPrivacy: if callData, err = sess.ctx.Unwrap(args); err != nil { return nil, rpc.Auth{}, false } case rpc.SvcIntegrity: if len(args) < 28 { return nil, rpc.Auth{}, false } callData = args[:len(args)-28] if err := sess.ctx.VerifyMIC(callData, args[len(args)-28:]); err != nil { return nil, rpc.Auth{}, false } default: return nil, rpc.Auth{}, false } var res []byte switch v3.Proc { case rpc.GSSProcCreate: if ok, cok := h.gssCreate(v3, prefix, callData); !cok { return nil, rpc.Auth{}, false } else { res = ok } case rpc.GSSProcList: res = h.gssList() default: return nil, rpc.Auth{}, false } replyPrefix, perr := rpc.AppendAcceptedReply(nil, call.XID, rpc.Auth{}, rpc.AcceptSuccess, rpc.Mismatch{}) if perr != nil { return nil, rpc.Auth{}, false } mic, merr := sess.ctx.GetMIC(replyPrefix) if merr != nil { return nil, rpc.Auth{}, false } verf := rpc.Auth{Flavor: rpc.FlavorGSS, Body: mic} switch v3.Service { case rpc.SvcPrivacy: sealed, serr := sess.ctx.Wrap(res) if serr != nil { return nil, rpc.Auth{}, false } return sealed, verf, true case rpc.SvcIntegrity: mic, merr := sess.ctx.GetMIC(res) if merr != nil { return nil, rpc.Auth{}, false } return append(append([]byte{}, res...), mic...), verf, true default: return nil, rpc.Auth{}, false } } // LabelOf answers the label assertion bound to a context handle, or nil // when the context carries none. func (h *Handler) LabelOf(handle []byte) *rpc.Label { return h.labelOf(handle) }