Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Assisted-by: GLM 5.3 Flash
426 lines
13 KiB
Go
426 lines
13 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
|
|
}
|