I've applied the "Fixed gssapi usage" patch, by the way, thanks for doing that! --b. On Thu, Sep 30, 2021 at 06:22:09PM +0300, Volodymyr Khomenko wrote: > Hello pynfs devs, > > It turned out that the pynfs library doesn't work well with the > current version of the 'gssapi' package (API was changed some time > ago) so tests with security=krb5* didn't work. > > The latest version of gssapi (aka python-gssapi) and documentation for > it can be found here: > https://github.com/pythongssapi/python-gssapi > https://pythongssapi.github.io/python-gssapi/latest/ > > Attached patches fixed the problem (tested with CentOS 7.7 NFS4 server > with pynfs + gssapi 1.6.2). > > Note - changes are not a complete solution, only nfs4.1 client code is fixed. > nfs4.1 server code and all the code for nfs4.0 should be fixed separately. > > Thanks, > Volodymyr Khomenko. > commit b77dc49c775756f08bdd0c6ebbe67a96f0ffe41f > Author: Volodymyr Khomenko <volodymyr@xxxxxxxxxxxx> > Date: Thu Sep 30 17:53:04 2021 +0300 > > Fixed GSSContext to start sequence numbering from 1 > > GSS sequence number 0 is usually used by NFS4 NULL request > during GSS context establishment (but ignored by server). > Client should never reuse GSS sequence number, so using > 0 for the next real operation (EXCHANGE_ID) is possible but > looks suspicious. Fixed the code so numbering for operations > is done from 1 to avoid confusion. > > Signed-off-by: Volodymyr Khomenko <volodymyr@xxxxxxxxxxxx> > > diff --git a/rpc/security.py b/rpc/security.py > index 0682f43..86f6592 100644 > --- a/rpc/security.py > +++ b/rpc/security.py > @@ -174,7 +174,9 @@ class GSSContext(object): > def __init__(self, context_ptr): > self.lock = threading.Lock() > self.ptr = context_ptr > - self.seqid = 0 # client - next seqid to use > + # Note - seqid=0 is usually used during GSS context establishment, > + # to have the unique number we need to use the next value now. > + self.seqid = 1 # client - next seqid to use > self.highest = 0 # server - highest seqid seen > self.seen = 0 # server - bitmask of seen requests > > commit a612cf9897f0fa5b5de94885e00ef9293e93ffa3 > Author: Volodymyr Khomenko <volodymyr@xxxxxxxxxxxx> > Date: Thu Sep 30 16:29:07 2021 +0300 > > Fixed gssapi usage (RPCGSS) for nfs4.1 client > > gssapi library used in the code has been changed and > current code is not compatible with API of new library version. > Fixed the code to work with recent gssapi (tested with 1.6.2). > Tested with krb5, krb5i and krb5p security: > ./nfs4.1/testserver.py server.fqdn:/export --maketree --security=krb5 all > > Signed-off-by: Volodymyr Khomenko <volodymyr@xxxxxxxxxxxx> > > diff --git a/rpc/security.py b/rpc/security.py > index fe4390c..0682f43 100644 > --- a/rpc/security.py > +++ b/rpc/security.py > @@ -10,6 +10,7 @@ from . import gss_type > from .gss_type import rpc_gss_init_res > try: > import gssapi > + from gssapi.raw.misc import GSSError > except ImportError: > print("Could not find gssapi module, proceeding without") > gssapi = None > @@ -242,11 +243,11 @@ class AuthGss(AuthNone): > > def init_cred(self, call, target="nfs@jupiter", source=None, oid=None): > # STUB - need intelligent way to set defaults > - good_major = [gssapi.GSS_S_COMPLETE, gssapi.GSS_S_CONTINUE_NEEDED] > + good_major = [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED] > p = Packer() > up = GSSUnpacker('') > # Set target (of form nfs@SERVER) > - target = gssapi.Name(target, gssapi.NT_HOSTBASED_SERVICE) > + target = gssapi.Name(target, gssapi.NameType.hostbased_service) > # Set source (of form USERNAME) > if source is not None: > source = gssapi.Name(source, gssapi.NT_USER_NAME) > @@ -254,18 +255,26 @@ class AuthGss(AuthNone): > else: > # Just use default cred > gss_cred = None > - context = gssapi.Context() > - token = None > - handle = '' > + # RFC2203 5.2.2. Context Creation Requests > + # When GSS_Init_sec_context() is called, the parameters > + # replay_det_req_flag and sequence_req_flag must be turned off. > + > + # Note - by default, out_of_sequence_detection flag (sequence_req_flag) is used by gssapi.init_sec_context() > + # and we have 'An expected per-message token was not received' error (GSS_S_GAP_TOKEN). > + # To prevent this, we need to use default flags without out_of_sequence_detection bit. > + flags = gssapi.IntEnumFlagSet(gssapi.RequirementFlag, [gssapi.RequirementFlag.mutual_authentication]) > + context = gssapi.SecurityContext(name=target, creds=gss_cred, flags=flags) > + input_token = None > + handle = b'' > proc = RPCSEC_GSS_INIT > - while True: > + while not context.complete: > # Call initSecContext. If it returns COMPLETE, we are done. > # If it returns CONTINUE_NEEDED, we must send d['token'] > # to the target, which will run it through acceptSecContext, > # and give us back a token we need to send through initSecContext. > # Repeat as necessary. > - token = context.init(target, token, gss_cred) > - if context.open: > + output_token = context.step(input_token) > + if context.complete: > # XXX if res.major == CONTINUE there is a bug in library code > # STUB - now what? Just use context? > # XXX need to use res.seq_window > @@ -277,16 +286,16 @@ class AuthGss(AuthNone): > gss_proc=proc) > proc = RPCSEC_GSS_CONTINUE_INIT > p.reset() > - p.pack_opaque(token) > + p.pack_opaque(output_token) > header, reply = call(p.get_buffer(), credinfo) > up.reset(reply) > res = up.unpack_rpc_gss_init_res() > up.done() > # res now holds relevent output from target's acceptSecContext call > if res.gss_major not in good_major: > - raise gssapi.Error(res.gss_major, res.gss_minor) > + raise GSSError(res.gss_major, res.gss_minor) > handle = res.handle # Should not change between calls > - token = res.gss_token # This needs to be sent to initSecContext > + input_token = res.gss_token # This needs to be sent to SecurityContext.step() > return CredInfo(self, context=handle) > > @staticmethod > @@ -361,7 +370,7 @@ class AuthGss(AuthNone): > except: > log_gss.exception("unsecure_data - initial unpacking") > raise rpclib.RPCUnsuccessfulReply(GARBAGE_ARGS) > - qop = context.verifyMIC(data, checksum) > + qop = context.verify_signature(data, checksum) > check_gssapi(qop) > data = pull_seqnum(data) > elif cred.service == rpc_gss_svc_privacy: > @@ -373,14 +382,14 @@ class AuthGss(AuthNone): > log_gss.exception("unsecure_data - initial unpacking") > raise rpclib.RPCUnsuccessfulReply(GARBAGE_ARGS) > # data, qop, conf = context.unwrap(data) > - data, qop = context.unwrap(data) > + data, encrypted, qop = context.unwrap(data) > check_gssapi(qop) > data = pull_seqnum(data) > else: > # Can't get here, but doesn't hurt > log_gss.error("Unknown service %i for RPCSEC_GSS" % cred.service) > - except gssapi.Error as e: > - log_gss.warn("unsecure_data: gssapi call returned %s" % e.name) > + except GSSError as e: > + log_gss.warn("unsecure_data: gssapi call returned %s" % str(e)) > raise rpclib.RPCUnsuccessfulReply(GARBAGE_ARGS) > return data > > @@ -397,7 +406,7 @@ class AuthGss(AuthNone): > # data = opaque[gss_seq_num+data] + opaque[checksum] > p.pack_uint(cred.seq_num) > data = p.get_buffer() + data > - token = context.getMIC(data) # XXX BUG set qop > + token = context.get_signature(data) # XXX BUG set qop > p.reset() > p.pack_opaque(data) > p.pack_opaque(token) > @@ -406,16 +415,16 @@ class AuthGss(AuthNone): > # data = opaque[wrap([gss_seq_num+data])] > p.pack_uint(cred.seq_num) > data = p.get_buffer() + data > - token = context.wrap(data) # XXX BUG set qop > + wrap_res = context.wrap(data, encrypt=True) # XXX BUG set qop > p.reset() > - p.pack_opaque(token) > + p.pack_opaque(wrap_res.message) > data = p.get_buffer() > else: > # Can't get here, but doesn't hurt > log_gss.error("Unknown service %i for RPCSEC_GSS" % cred.service) > - except gssapi.Error as e: > + except GSSError as e: > # XXX What now? > - log_gss.warn("secure_data: gssapi call returned %s" % e.name) > + log_gss.warn("secure_data: gssapi call returned %s" % str(e)) > raise > return data > > @@ -436,8 +445,8 @@ class AuthGss(AuthNone): > return rpclib.NULL_CRED > else: > data = self.partially_packed_header(xid, body) > - # XXX how handle gssapi.Error? > - token = self._get_context(body.cred.body.handle).getMIC(data) > + # XXX how handle GSSError? > + token = self._get_context(body.cred.body.handle).get_signature(data) > return opaque_auth(RPCSEC_GSS, token) > > def check_call_verf(self, xid, body): > @@ -448,10 +457,10 @@ class AuthGss(AuthNone): > return False > data = self.partially_packed_header(xid, body) > try: > - qop = self._get_context(body.cred.body.handle).verifyMIC(data, body.verf.body) > - except gssapi.Error as e: > + qop = self._get_context(body.cred.body.handle).verify_signature(data, body.verf.body) > + except GSSError as e: > log_gss.warn("Verifier checksum failed verification with %s" % > - e.name) > + str(e)) > return False > body.cred.body.qop = qop # XXX Where store this? > log_gss.debug("verifier checks out (qop=%i)" % qop) > @@ -522,10 +531,10 @@ class AuthGss(AuthNone): > context = self._get_context(cred.body.handle) > try: > token = context.accept(token) > - except gssapi.Error as e: > + except GSSError as e: > log_gss.debug("RPCSEC_GSS_INIT failed (%s, %i)!" % > - (e.name, e.minor)) > - res = rpc_gss_init_res('', e.major, e.minor, 0, '') > + (str(e), e.min_code)) > + res = rpc_gss_init_res('', e.maj_code, e.min_code, 0, '') > else: > log_gss.debug("RPCSEC_GSS_*INIT succeeded!") > if first: > @@ -538,9 +547,9 @@ class AuthGss(AuthNone): > else: > handle = cred.body.handle > if context.open: > - major = gssapi.GSS_S_COMPLETE > + major = GSS_S_COMPLETE > else: > - major = gssapi.GSS_S_CONTINUE_NEEDED > + major = GSS_S_CONTINUE_NEEDED > res = rpc_gss_init_res(handle, major, 0, # XXX can't see minor > WINDOWSIZE, token) > # Prepare response > @@ -559,15 +568,15 @@ class AuthGss(AuthNone): > # NOTE this relies on GSS_S_COMPLETE == rpc.SUCCESS == 0 > return rpclib.NULL_CRED > elif cred.gss_proc in (RPCSEC_GSS_INIT, RPCSEC_GSS_CONTINUE_INIT): > - # init requires getMIC(seq_window) > + # init requires get_signature(seq_window) > i = WINDOWSIZE > else: > - # Else return getMIC(cred.seq_num) > + # Else return get_signature(cred.seq_num) > i = cred.seq_num > p = Packer() > p.pack_uint(i) > # XXX BUG - need to set qop > - token = self._get_context(cred.handle).getMIC(p.get_buffer()) > + token = self._get_context(cred.handle).get_signature(p.get_buffer()) > return opaque_auth(RPCSEC_GSS, token) > > def check_reply_verf(self, msg, call_cred, data): > @@ -593,12 +602,12 @@ class AuthGss(AuthNone): > if res.gss_major != GSS_S_COMPLETE: > raise SecError("Expected NULL") > # BUG - context establishment is not finished on client > - # - so how get context? How run verifyMIC? > + # - so how get context? How run verify_signature? > # - This seems to be a protocol problem. Just ignore for now > else: > p = Packer() > p.pack_uint(call_cred.body.seq_num) > - qop = call_cred.context.verifyMIC(p.get_buffer(), verf.body) > + qop = call_cred.context.verify_signature(p.get_buffer(), verf.body) > if qop != call_cred.body.qop: > raise SecError("Mismatched qop") >