xpra icon
Bug tracker and wiki

Ticket #1660: server-challenge.patch

File server-challenge.patch, 11.2 KB (added by Antoine Martin, 3 years ago)

implement server challenge support

  • xpra/client/client_base.py

     
    2424from xpra.net import compression
    2525from xpra.net.protocol import Protocol, sanity_checks
    2626from xpra.net.net_util import get_network_caps
    27 from xpra.net.crypto import crypto_backend_init, get_iterations, get_iv, get_salt, choose_padding, gendigest, \
     27from xpra.net.crypto import crypto_backend_init, get_iterations, get_iv, get_salt, choose_padding, gendigest, get_digests, verify_digest, \
    2828    ENCRYPTION_CIPHERS, ENCRYPT_FIRST_PACKET, DEFAULT_IV, DEFAULT_SALT, DEFAULT_ITERATIONS, INITIAL_PADDING, DEFAULT_PADDING, ALL_PADDING_OPTIONS, PADDING_OPTIONS
    2929from xpra.version_util import version_compat_check, get_version_info, XPRA_VERSION
    3030from xpra.platform.info import get_name
     
    5252SKIP_STOPPED_PRINTERS = envbool("XPRA_SKIP_STOPPED_PRINTERS", True)
    5353PASSWORD_PROMPT = envbool("XPRA_PASSWORD_PROMPT", True)
    5454LEGACY_SALT_DIGEST = envbool("XPRA_LEGACY_SALT_DIGEST", True)
     55SERVER_CHALLENGE = envbool("XPRA_SERVER_CHALLENGE", False)
    5556
    5657
    5758class XpraClientBase(FileTransferHandler):
     
    9293        self.password = None
    9394        self.password_file = None
    9495        self.password_sent = False
     96        self.challenge_salt = None
    9597        self.encryption = None
    9698        self.encryption_keyfile = None
    9799        self.server_padding_options = [DEFAULT_PADDING]
     
    329331                hello["challenge"] = True
    330332            else:
    331333                hello.update(self.make_hello())
     334            #ask the server to prove it knows the password too:
     335            if self.has_password() or challenge_response:
     336                self.challenge_salt = get_salt()
     337                digests = salt_digests = get_digests()
     338                hello["server-challenge"] = self.challenge_salt, digests, salt_digests
    332339        except InitExit as e:
    333340            log.error("error preparing connection:")
    334341            log.error(" %s", e)
     
    679686    def load_password(self):
    680687        if self.password:
    681688            return self.password
    682         password = os.environ.get('XPRA_PASSWORD')
     689        self.password = os.environ.get('XPRA_PASSWORD')
    683690        if self.password_file:
    684691            filename = os.path.expanduser(self.password_file)
    685             password = load_binary_file(filename)
    686             authlog("password read from file %s is %s", self.password_file, "".join(["*" for _ in (password or "")]))
    687         if not password and PASSWORD_PROMPT:
     692            self.password = load_binary_file(filename)
     693            authlog("password read from file %s is %s", self.password_file, "".join(["*" for _ in (self.password or "")]))
     694        if not self.password and PASSWORD_PROMPT:
    688695            try:
    689696                if sys.stdin.isatty():
    690697                    import getpass
    691698                    authlog("stdin isatty, using password prompt")
    692                     password = getpass.getpass("%s :" % self.get_challenge_prompt())
     699                    self.password = getpass.getpass("%s :" % self.get_challenge_prompt())
    693700            except Exception:
    694701                authlog("password request failure", exc_info=True)
    695         return password
     702        return self.password
    696703
    697704    def _process_hello(self, packet):
    698705        if not self.password_sent and self.has_password():
     
    719726        if not self.parse_version_capabilities():
    720727            netlog("server_connection_established() failed version capabilities")
    721728            return False
     729        if not self.parse_challenge_capabilities():
     730            netlog("server_connection_established() failed challenge capabilities")
     731            return False
    722732        if not self.parse_server_capabilities():
    723733            netlog("server_connection_established() failed server capabilities")
    724734            return False
     
    880890            return False
    881891        return True
    882892
     893    def parse_challenge_capabilities(self):
     894        c = self.server_capabilities
     895        cr = c.listget("challenge-response")
     896        authlog("parse_challenge_capabilities() got challenge-response=%s, challenge salt=%s, SERVER_CHALLENGE=%s", bool(cr), hexstr(self.challenge_salt), SERVER_CHALLENGE)
     897        if self.challenge_salt and SERVER_CHALLENGE and not cr:
     898            log.error("Error: server did not respond to the challenge request")
     899            return False
     900        if cr:
     901            password = self.load_password()
     902            challenge_response, server_salt, digest, salt_digest = cr[:4]
     903            salt = gendigest(salt_digest, server_salt, self.challenge_salt)
     904            authlog("server challenge: combined %s salt(%s, %s)=%s", salt_digest, hexstr(server_salt), hexstr(self.challenge_salt), hexstr(salt))
     905            if not verify_digest(digest, password, salt, challenge_response):
     906                log.error("Error: server challenge response is incorrect!")
     907                return False
     908        return True
     909
    883910    def parse_server_capabilities(self):
    884911        c = self.server_capabilities
    885912        self.can_shutdown_server = c.boolget("client-shutdown", True)
  • xpra/net/crypto.py

     
    115115    except AttributeError:
    116116        return None
    117117
     118def choose_safe_digest(options):
     119    assert len(options)>0, "no digest options"
     120    log("choose_safe_digest(%s)", options)
     121    #prefer stronger hashes:
     122    for h in ("sha512", "sha384", "sha256", "sha224", "sha1", "md5"):
     123        hname = "hmac+%s" % h
     124        if hname in options:
     125            return hname
     126    raise Exception("no known digest options found in '%s'" % csv(options))
     127
    118128def choose_digest(options):
    119129    assert len(options)>0, "no digest options"
    120130    log("choose_digest(%s)", options)
  • xpra/server/server_base.py

     
    3232webcamlog = Logger("webcam")
    3333notifylog = Logger("notify")
    3434httplog = Logger("http")
     35authlog = Logger("auth")
    3536
    3637from xpra.keyboard.mask import DEFAULT_MODIFIER_MEANINGS
    3738from xpra.server.server_core import ServerCore, get_thread_info
     
    14941495            updict(capabilities, "encoding", codec_versions, "version")
    14951496        return capabilities
    14961497
    1497     def send_hello(self, server_source, root_w, root_h, key_repeat, server_cipher):
     1498    def send_hello(self, server_source, root_w, root_h, key_repeat, extra_caps):
    14981499        capabilities = self.make_hello(server_source)
    14991500        if server_source.wants_encodings:
    15001501            for k,v in self.get_encoding_info().items():
     
    15211522            capabilities["remote-logging.multi-line"] = True
    15221523        if self._reverse_aliases and server_source.wants_aliases:
    15231524            capabilities["aliases"] = self._reverse_aliases
    1524         if server_cipher:
    1525             capabilities.update(server_cipher)
     1525        if extra_caps:
     1526            capabilities.update(extra_caps)
    15261527        server_source.send_hello(capabilities)
    15271528
    15281529
  • xpra/server/server_core.py

     
    3535from xpra.os_util import load_binary_file, get_machine_id, get_user_uuid, platform_name, bytestostr, get_hex_uuid, monotonic_time, get_peercred, hexstr, SIGNAMES, WIN32, OSX, POSIX, PYTHON3
    3636from xpra.version_util import version_compat_check, get_version_info_full, get_platform_info, get_host_info
    3737from xpra.net.protocol import Protocol, sanity_checks
    38 from xpra.net.crypto import crypto_backend_init, new_cipher_caps, get_salt, choose_digest, \
     38from xpra.net.crypto import crypto_backend_init, new_cipher_caps, get_salt, choose_digest, choose_safe_digest, gendigest, \
    3939        ENCRYPTION_CIPHERS, ENCRYPT_FIRST_PACKET, DEFAULT_IV, DEFAULT_SALT, DEFAULT_ITERATIONS, INITIAL_PADDING, DEFAULT_PADDING, ALL_PADDING_OPTIONS
    4040from xpra.server.background_worker import stop_worker, get_worker
    4141from xpra.make_thread import start_thread
     
    5050SIMULATE_SERVER_HELLO_ERROR = envbool("XPRA_SIMULATE_SERVER_HELLO_ERROR", False)
    5151SERVER_SOCKET_TIMEOUT = float(os.environ.get("XPRA_SERVER_SOCKET_TIMEOUT", "0.1"))
    5252LEGACY_SALT_DIGEST = envbool("XPRA_LEGACY_SALT_DIGEST", True)
     53SERVER_CHALLENGE = envbool("XPRA_SERVER_CHALLENGE", True)
    5354
    5455
    5556#class used to distinguish internal errors
     
    12771278                authlog("client does not provide encryption tokens")
    12781279                auth_failed("missing encryption tokens")
    12791280                return False
    1280             auth_caps = None
     1281            auth_caps = {}
    12811282
    12821283        #verify authentication if required:
    12831284        if (proto.authenticator and proto.authenticator.requires_challenge()) or c.get("challenge") is not None:
     
    13311332            #did the client expect a challenge?
    13321333            if c.boolget("challenge"):
    13331334                authlog.warn("this server does not require authentication (client supplied a challenge)")
     1335
     1336        #client can also challenge the server:
     1337        server_challenge = c.listget("server-challenge")
     1338        auth = proto.authenticator
     1339        #clients can request the server to prove that it also knows the password:
     1340        #(but not all authentication modules have access to the password)
     1341        if server_challenge:
     1342            authlog("got a server challenge: %s", server_challenge)
     1343            if not SERVER_CHALLENGE:
     1344                authlog.warn("Warning: not responding to client challenge request")
     1345                authlog.warn(" the feature is disabled: XPRA_SERVER_CHALLENGE=%s", SERVER_CHALLENGE)
     1346            elif not auth:
     1347                authlog.warn("Warning: cannot respond to client challenge request")
     1348                authlog.warn(" this connection is unauthenticated")
     1349            else:
     1350                authlog("client requesting a server challenge, auth=%s", auth)
     1351                password = auth.get_password()
     1352                if not password:
     1353                    authlog.warn("Warning: cannot respond to client challenge request")
     1354                    authlog.warn(" no password data from %s", auth)
     1355                else:
     1356                    client_salt, digests, salt_digests = server_challenge[:3]
     1357                    digest = choose_safe_digest(digests)
     1358                    salt_digest = choose_safe_digest(salt_digests)
     1359                    l = len(client_salt)
     1360                    assert l>=16, "client challenge salt is too short"
     1361                    server_salt = get_salt(l)
     1362                    salt = gendigest(salt_digest, server_salt, client_salt)
     1363                    authlog("combined %s salt(%s, %s)=%s", salt_digest, hexstr(server_salt), hexstr(client_salt), hexstr(salt))
     1364                    challenge_response = gendigest(digest, password, salt)
     1365                    auth_caps = auth_caps or {}
     1366                    authlog("challenge-response=%s", (challenge_response, server_salt, digest, salt_digest))
     1367                    auth_caps["challenge-response"] = (challenge_response, server_salt, digest, salt_digest)
     1368
    13341369        return auth_caps
    13351370
    13361371    def filedata_nocrlf(self, filename):