From 03a788ba9d6bf83ff070ca6584d2d06e01a09b17 Mon Sep 17 00:00:00 2001 From: Antoine Martin Date: Fri, 23 Mar 2018 12:19:00 +0000 Subject: [PATCH] #1789: initial U2F support git-svn-id: https://xpra.org/svn/Xpra/trunk@18801 3bb7dfac-3a0b-4e04-842a-767bc560f471 --- src/etc/xpra/conf.d/40_client.conf.in | 7 ++ src/man/xpra.1 | 6 +- src/xpra/client/client_base.py | 35 +++++++- src/xpra/server/auth/u2f_auth.py | 110 ++++++++++++++++++++++++++ src/xpra/server/server_core.py | 22 ++++-- 5 files changed, 168 insertions(+), 12 deletions(-) create mode 100755 src/xpra/server/auth/u2f_auth.py diff --git a/src/etc/xpra/conf.d/40_client.conf.in b/src/etc/xpra/conf.d/40_client.conf.in index 564d89755a..f7294f4957 100644 --- a/src/etc/xpra/conf.d/40_client.conf.in +++ b/src/etc/xpra/conf.d/40_client.conf.in @@ -7,6 +7,13 @@ #opengl = gtk, native opengl = auto +# How we handle server authentication requests: +#authentication-handlers=all +#authentication-handlers=none +#authentication-handlers=env,file +#authentication-handlers=env,file,kerberos,gss,u2f,prompt +authentication-handlers=auto + # Client window title: title = @title@ on @client-machine@ diff --git a/src/man/xpra.1 b/src/man/xpra.1 index 2d940880b2..9ea740fe81 100644 --- a/src/man/xpra.1 +++ b/src/man/xpra.1 @@ -676,6 +676,8 @@ for testing. validates a kerberos ticket obtained by the client. .IP \fBgss\fP validates a GSS ticket obtained by the client. +.IP \fBu2f\fP +requests a U2F token from the client. .RE .PP .TP @@ -715,7 +717,7 @@ to VSOCK sockets (sockets defined using the \fBbind\-vsock\fP switch). Configures which challenge handlers are used by the client and in which order. The default value is: \fIall\fP which corresponds to: -\fIuri,file,env,kerberos,gss,prompt\fP. +\fIuri,file,env,kerberos,gss,u2f,prompt\fP. Note: some of these modules will fall through to others if they are enable to supply the challenge requested (\fIuri\fP, \fIfile\fP, \fIkerberos\fP and \fIgss\fP), others do not (\fIenv\fP and \fIprompt\fP). @@ -732,6 +734,8 @@ environment variable. Request a kerberos token for the service specified. .IP \fBgss\fP Request a gss token for the service specified. +.IP \fBu2f\fP +Requests a token from a U2F device. .IP \fBprompt\fP Prompt the user for the value. Terminal clients prompt using text input, GUI clients diff --git a/src/xpra/client/client_base.py b/src/xpra/client/client_base.py index f76f4fe0e6..bea2e73594 100644 --- a/src/xpra/client/client_base.py +++ b/src/xpra/client/client_base.py @@ -11,7 +11,7 @@ import string from collections import OrderedDict -from xpra.log import Logger +from xpra.log import Logger, is_debug_enabled log = Logger("client") netlog = Logger("network") authlog = Logger("auth") @@ -76,6 +76,7 @@ def __init__(self): dcm["env"] = self.process_challenge_env dcm["kerberos"] = self.process_challenge_kerberos dcm["gss"] = self.process_challenge_gss + dcm["u2f"] = self.process_challenge_u2f dcm["prompt"] = self.process_challenge_prompt self.default_challenge_methods = dcm @@ -367,7 +368,7 @@ def make_hello_base(self): capabilities = flatten_dict(get_network_caps()) #add "kerberos" and "gss" if enabled: default_on = "all" in self.challenge_handlers or "auto" in self.challenge_handlers - for auth in ("kerberos", "gss"): + for auth in ("kerberos", "gss", "u2f"): if default_on or auth in self.challenge_handlers: capabilities["digest"].append(auth) capabilities.update(FilePrintMixin.get_caps(self)) @@ -734,7 +735,7 @@ def process_challenge_gss(self, packet): import gssapi if OSX and False: from gssapi.raw import (cython_converters, cython_types, oids) - assert (cython_converters, cython_types, oids) + assert cython_converters and cython_types and oids except ImportError as e: authlog("import gssapi", exc_info=True) if first_time("no-kerberos"): @@ -765,6 +766,31 @@ def process_challenge_gss(self, packet): self.send_challenge_reply(packet, token) return True + def process_challenge_u2f(self, packet): + import binascii + import logging + if not is_debug_enabled("auth"): + logging.getLogger("pyu2f.hardware").setLevel(logging.INFO) + logging.getLogger("pyu2f.hidtransport").setLevel(logging.INFO) + from pyu2f import model + from pyu2f.u2f import GetLocalU2FInterface + dev = GetLocalU2FInterface() + APP_ID = os.environ.get("XPRA_U2F_APP_ID", "Xpra") + key_handle_str = os.environ.get("XPRA_U2F_KEY_HANDLE", + "584f1a158d83d965713467a37f3b33b6aca6f4feb9e18899432d4be68fcd46773d2a6fba6d3b8cfe49838abcf1b7ae28adad6fbead538f018519bee023faa2d3") + authlog("process_challenge_u2f key_handle=%s", key_handle_str) + key_handle = binascii.unhexlify(key_handle_str) + key = model.RegisteredKey(key_handle) + #use server salt as challenge directly + challenge = packet[1] + authlog.info("activate your U2F device for authentication") + response = dev.Authenticate(APP_ID, challenge, [key]) + sig = response.signature_data + client_data = response.client_data + authlog("process_challenge_u2f client data=%s, signature=%s", client_data, binascii.hexlify(sig)) + self.do_send_challenge_reply(bytes(sig), client_data.origin) + return True + def auth_error(self, code, message, server_message="authentication failed"): authlog.error("Error: authentication failed:") @@ -845,6 +871,9 @@ def send_challenge_reply(self, packet, password): self.auth_error(EXIT_UNSUPPORTED, "server requested '%s' digest but it is not supported" % actual_digest, "invalid digest") return authlog("%s(%s, %s)=%s", actual_digest, repr(password), repr(salt), repr(challenge_response)) + self.do_send_challenge_reply(challenge_response, client_salt) + + def do_send_challenge_reply(self, challenge_response, client_salt): self.password_sent = True self.send_hello(challenge_response, client_salt) diff --git a/src/xpra/server/auth/u2f_auth.py b/src/xpra/server/auth/u2f_auth.py new file mode 100755 index 0000000000..f7f3b24461 --- /dev/null +++ b/src/xpra/server/auth/u2f_auth.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# This file is part of Xpra. +# Copyright (C) 2018 Antoine Martin +# Xpra is released under the terms of the GNU GPL v2, or, at your option, any +# later version. See the file COPYING for details. + +import sys +import json +import struct +import binascii +import base64 +from hashlib import sha256 + +#python-cryptography to verify signatures: +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import load_der_public_key +from cryptography.hazmat.backends import default_backend + +from xpra.os_util import hexstr +from xpra.net.crypto import get_salt +from xpra.server.auth.sys_auth_base import SysAuthenticator, init, log +assert init and log #tests will disable logging from here + + +PUB_KEY_DER_PREFIX = binascii.a2b_hex("3059301306072a8648ce3d020106082a8648ce3d030107034200") +#this won't work for anyone but me: +TEST_PUBLIC_KEY = "04df0a3c35cf3f4c68120e3d90a1106330b89ea80c8cb37cce25ea563692db0eec9b95792966efa699c40d9cab6017197a59288440a0ab80d818c0db2b110a29c7" + + +class Authenticator(SysAuthenticator): + + def __init__(self, username, **kwargs): + self.app_id = kwargs.pop("app_id", "Xpra") + #TODO: load public key per user, from file? + key_hexstring = kwargs.pop("public_key", TEST_PUBLIC_KEY) + key = binascii.unhexlify(key_hexstring) + log("u2f: trying to load DER public key %s", repr(key)) + if not key.startswith(PUB_KEY_DER_PREFIX): + key = PUB_KEY_DER_PREFIX+key + self.public_key = load_der_public_key(key, default_backend()) + SysAuthenticator.__init__(self, username, **kwargs) + + def get_challenge(self, digests): + if "u2f" not in digests: + log.error("Error: client does not support u2f authentication") + return None + self.salt = get_salt() + self.digest = "u2f:xor" + self.challenge_sent = True + return self.salt, self.digest + + def __repr__(self): + return "u2f" + + def authenticate(self, challenge_response=None, client_salt=None): + log("authenticate(%s, %s)", repr(challenge_response), repr(client_salt)) + user_presence, counter = struct.unpack(">BI", challenge_response[:5]) + sig = challenge_response[5:] + log("u2f user_presence=%s, counter=%s, signature=%s", user_presence, counter, hexstr(sig)) + verifier = self.public_key.verifier(sig, ec.ECDSA(hashes.SHA256())) + app_param = sha256(self.app_id.encode('utf8')).digest() + server_challenge_b64 = base64.urlsafe_b64encode(self.salt).decode() + server_challenge_b64 = server_challenge_b64.rstrip('=') + log("challenge_b64(%s)=%s", repr(self.salt), server_challenge_b64) + client_data = { + "challenge" : server_challenge_b64, + "origin" : client_salt, + "typ" : "navigator.id.getAssertion", + } + client_param = sha256(json.dumps(client_data, sort_keys=True).encode('utf8')).digest() + verifier.update(app_param+ + struct.pack('>B', user_presence) + + struct.pack('>I', counter) + + client_param, + ) + try: + verifier.verify() + log("ECDSA SHA256 verification passed") + return True + except Exception as e: + log("authenticate failed", exc_info=True) + log.error("Error: authentication failed:") + log.error(" %s", e) + return False + + +def main(argv): + from xpra.platform import program_context + with program_context("U2F-Register", "U2F Registration Tool"): + from pyu2f.u2f import GetLocalU2FInterface + dev = GetLocalU2FInterface() + + print("activate your U2F device to generate a new key") + APP_ID = u"Xpra" + registered_keys = [] + challenge= b'01234567890123456789012345678901' #unused + rr = dev.Register(APP_ID, challenge, registered_keys) + b = rr.registration_data + assert b[0]==5 + pubkey = bytes(b[1:66]) + khl = b[66] + key_handle = bytes(b[67:67 + khl]) + print("XPRA_U2F_KEY_HANDLE=%s" % hexstr(key_handle)) + print("auth=u2f,public key=%s" % hexstr(pubkey)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/src/xpra/server/server_core.py b/src/xpra/server/server_core.py index 03db2e7305..672b38afdf 100644 --- a/src/xpra/server/server_core.py +++ b/src/xpra/server/server_core.py @@ -498,6 +498,11 @@ def get_auth_module(self, socket_type, auth_str, opts): "file" : file_auth, "exec" : exec_auth, } + try: + from xpra.server.auth import u2f_auth + AUTH_MODULES["u2f"] = u2f_auth + except ImportError: + authlog("cannot load u2f auth: %s", exc_info=True) if POSIX and not OSX: from xpra.server.auth import peercred_auth, hosts_auth AUTH_MODULES["peercred"] = peercred_auth @@ -505,36 +510,37 @@ def get_auth_module(self, socket_type, auth_str, opts): try: from xpra.server.auth import sqlite_auth AUTH_MODULES["sqlite"] = sqlite_auth - except Exception as e: - authlog("cannot load sql auth: %s", e) + except Exception: + authlog("cannot load sql auth: %s", exc_info=True) try: from xpra.server.auth import kerberos_password_auth AUTH_MODULES["kerberos-password"] = kerberos_password_auth except Exception as e: - authlog("cannot load kerberos-password auth: %s", e) + authlog("cannot load kerberos-password auth", exc_info=True) try: from xpra.server.auth import kerberos_token_auth AUTH_MODULES["kerberos-token"] = kerberos_token_auth - except Exception as e: - authlog("cannot load kerberos-token auth: %s", e) + except Exception: + authlog("cannot load kerberos-token auth", exc_info=True) try: from xpra.server.auth import gss_auth AUTH_MODULES["gss"] = gss_auth except Exception as e: - authlog("cannot load kerberos-token auth: %s", e) + authlog("cannot load kerberos-token auth", exc_info=True) if WIN32: try: from xpra.server.auth import win32_auth AUTH_MODULES["win32"] = win32_auth except Exception as e: + authlog("cannot load win32 auth", exc_info=True) authlog.error("Error: cannot load the MS Windows authentication module:") authlog.error(" %s", e) else: try: from xpra.server.auth import pam_auth AUTH_MODULES["pam"] = pam_auth - except Exception as e: - authlog("cannot load pam auth: %s", e) + except Exception: + authlog("cannot load pam auth", exc_info=True) if auth=="sys": #resolve virtual "sys" auth: if WIN32: