Skip to content

Commit

Permalink
#1789: initial U2F support
Browse files Browse the repository at this point in the history
git-svn-id: https://xpra.org/svn/Xpra/trunk@18801 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Mar 23, 2018
1 parent 80ec5b5 commit 03a788b
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 12 deletions.
7 changes: 7 additions & 0 deletions src/etc/xpra/conf.d/40_client.conf.in
Original file line number Diff line number Diff line change
Expand Up @@ -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@

Expand Down
6 changes: 5 additions & 1 deletion src/man/xpra.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand All @@ -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
Expand Down
35 changes: 32 additions & 3 deletions src/xpra/client/client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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"):
Expand Down Expand Up @@ -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:")
Expand Down Expand Up @@ -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)

Expand Down
110 changes: 110 additions & 0 deletions src/xpra/server/auth/u2f_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python
# This file is part of Xpra.
# Copyright (C) 2018 Antoine Martin <[email protected]>
# 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))
22 changes: 14 additions & 8 deletions src/xpra/server/server_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,43 +498,49 @@ 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
AUTH_MODULES["hosts"] = hosts_auth
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:
Expand Down

0 comments on commit 03a788b

Please sign in to comment.