From 95866a205ce1362ad4b6e41b6dcb8a7aac668c55 Mon Sep 17 00:00:00 2001 From: Matthew Neeley Date: Thu, 27 Aug 2015 13:18:48 -0700 Subject: [PATCH] Add support for SRP authentication to the manager --- labrad/crypto.py | 235 +++++++++++++++++++++++++++++++++++++++++++++ labrad/protocol.py | 58 ++++++----- 2 files changed, 271 insertions(+), 22 deletions(-) diff --git a/labrad/crypto.py b/labrad/crypto.py index 820c9fb0..acb1f21f 100644 --- a/labrad/crypto.py +++ b/labrad/crypto.py @@ -3,6 +3,7 @@ import logging import os import re +from collections import namedtuple try: @@ -143,3 +144,237 @@ def grouped(s, n=2): s = s[n:] return ':'.join(g.upper() for g in grouped(digest, 2)) + + +def hexstr_to_int(s): + """Convert a string of hex digits to an int after stripping whitespace.""" + return int(re.sub(r'\s', '', s), 16) + + +def int_to_bytes(n): + """Convert the given int to a byte string in big-endian byte order.""" + bs = [] + while n > 0: + b, n = n & 0xFF, n >> 8 + bs.insert(0, chr(b)) + return b''.join(bs) + + +def bytes_to_int(bs): + """Convert a big-endian byte string to an int.""" + n = 0 + for b in bs: + n = (n << 8) + ord(b) + return n + + +def hash_all(hash_class, *args): + """Concatenate and hash the given args using the given hash function. + + hash_class: a callable that returns a hash algorithm with the same + interface as the hash algorithms defined in hashlib. + + args: Each arg can be given as a byte string or an int. Integer args are + converted to bytes using int_to_bytes, while byte strings are left + untouched. + """ + m = hash_class() + for a in args: + if isinstance(a, (int, long)): + a = int_to_bytes(a) + m.update(a) + return m.digest() + + +# SRP (Secure Remote Password) protocol support. +# For details see RFC 5054 (https://tools.ietf.org/html/rfc5054) + +SrpGroup = namedtuple('SrpGroup', ['N', 'g']) + +SrpGroup1024 = SrpGroup( + N = hexstr_to_int(""" + EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C + 9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4 + 8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29 + 7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A + FD5138FE 8376435B 9FC61D2F C0EB06E3 + """), + g = 2 +) + +SrpGroup1536 = SrpGroup( + N = hexstr_to_int(""" + 9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961 + 4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843 + 80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B + E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5 + 6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A + F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E + 8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB + """), + g = 2 +) + +SrpGroup2048 = SrpGroup( + N = hexstr_to_int(""" + AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294 + 3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D + CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB + D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74 + 7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A + 436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D + 5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73 + 03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6 + 94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F + 9E4AFF73 + """), + g = 2 +) + +SrpGroup4096 = SrpGroup( + N = hexstr_to_int(""" + FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 + 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B + 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 + A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 + 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 + FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D + 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C + 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 + 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D + 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D + B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 + 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C + BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC + E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 + 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB + 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 + 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 + D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199 + FFFFFFFF FFFFFFFF + """), + g = 5 +) + +SrpGroup8192 = SrpGroup( + N = hexstr_to_int(""" + FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08 + 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B + 302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9 + A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 + 49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 + FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D + 670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C + 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718 + 3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D + 04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D + B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 + 1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C + BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC + E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26 + 99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB + 04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2 + 233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127 + D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492 + 36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406 + AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918 + DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151 + 2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03 + F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F + BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA + CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B + B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632 + 387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E + 6DBE1159 74A3926F 12FEE5E4 38777CB6 A932DF8C D8BEC4D0 73B931BA + 3BC832B6 8D9DD300 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C + 5AE4F568 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9 + 22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B 4BCBC886 + 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A 062B3CF5 B3A278A6 + 6D2A13F8 3F44F82D DF310EE0 74AB6A36 4597E899 A0255DC1 64F31CC5 + 0846851D F9AB4819 5DED7EA1 B1D510BD 7EE74D73 FAF36BC3 1ECFA268 + 359046F4 EB879F92 4009438B 481C6CD7 889A002E D5EE382B C9190DA6 + FC026E47 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71 + 60C980DD 98EDD3DF FFFFFFFF FFFFFFFF + """), + g = 19 +) + +srp_groups = { + "1024": SrpGroup1024, + "1536": SrpGroup1536, + "2048": SrpGroup2048, + "4096": SrpGroup4096, + "8192": SrpGroup8192 +} + + +class SRP(object): + """Secure Remote Password (SRP) protocol implementation. + + For details of the algorithm, see https://tools.ietf.org/html/rfc5054 + """ + def __init__(self, identity, password, group=SrpGroup1024, + hash_class=hashlib.sha1): + """Initialize an SRP instance. + + Args: + identity (str): username + password (str): password + group (SrpGroup | str): Group parameters to use in the SRP protocol + or name of a group to use. + hash_class (callable): Called with no args and should return a hash + algorithm with the same interface as those defined in hashlib. + """ + if isinstance(group, str): + group = srp_groups[group] + + def generator(): + N, g = group.N, group.g + N_bytes = int_to_bytes(N) + + def H(*args): + return hash_all(hash_class, *args) + + def PAD(x): + return int_to_bytes(x).rjust(len(N_bytes), chr(0)) + + # send identity. get back salt and server B + salt, B_bytes = yield + + a = bytes_to_int(os.urandom(32)) + A = pow(g, a, N) + + B = bytes_to_int(B_bytes) + if B % N == 0: + raise Exception("auth failed: invalid challenge from server") + + x = bytes_to_int(H(salt, H(identity, ':', password))) + k = bytes_to_int(H(N, PAD(g))) + u = bytes_to_int(H(PAD(A), PAD(B))) + if u == 0: + raise Exception("auth failed: incompatible server and client " + "challenges") + + S = pow(B - k * pow(g, x, N), a + u * x, N) + + M1 = bytes_to_int(H(PAD(A), PAD(B), PAD(S))) + M2 = bytes_to_int(H(PAD(A), PAD(M1), PAD(S))) + + # send client confirmation M1, get back server confirmation M2 + M2_server_bytes = yield int_to_bytes(A), int_to_bytes(M1) + M2_server = bytes_to_int(M2_server_bytes) + + # authentication succeeded if the server M2 matched ours + yield M2_server == M2 + + self._generator = generator() + self._generator.next() + + def process_server_challenge(self, salt, B_bytes): + A_bytes, M1_bytes = self._generator.send((salt, B_bytes)) + return A_bytes, M1_bytes + + def process_server_confirmation(self, M2_server_bytes): + success = self._generator.send(M2_server_bytes) + if not success: + raise Exception("auth failed: unable to confirm server message") diff --git a/labrad/protocol.py b/labrad/protocol.py index d8c195b6..c90d72dc 100644 --- a/labrad/protocol.py +++ b/labrad/protocol.py @@ -387,7 +387,6 @@ def require_secure_connection(auth_type): 'username+password auth. Cannot log in ' 'as user {}.'.format(username)) credential = auth.Password(username, password) - else: # Have neither username nor password. # Check manager-supported auth methods; use OAuth if available. @@ -416,20 +415,34 @@ def require_secure_connection(auth_type): if isinstance(credential, auth.Password): if not credential.username: - # send login packet to get password challenge - challenge = yield self._sendManagerRequest() - - # send password response - m = hashlib.md5() - m.update(challenge) - if isinstance(credential.password, bytes): - m.update(credential.password) + if 'srp' in self.manager_features: + # send identity + group, salt, B_bytes = yield self._sendManagerRequest(10, username) + + # send password response + srp = crypto.SRP(identity=username, group=group, + password=credential.password) + A_bytes, M1 = srp.process_server_challenge(salt, B_bytes) + try: + resp, M2 = yield self._sendManagerRequest(11, (A_bytes, M1)) + except Exception: + raise errors.LoginFailedError('Incorrect password.') + srp.process_server_confirmation(M2) else: - m.update(credential.password.encode('UTF-8')) - try: - resp = yield self._sendManagerRequest(0, m.digest()) - except Exception: - raise errors.LoginFailedError('Incorrect password.') + # send login packet to get password challenge + challenge = yield self._sendManagerRequest() + + # send password response + m = hashlib.md5() + m.update(challenge) + if isinstance(credential.password, bytes): + m.update(credential.password) + else: + m.update(credential.password.encode('UTF-8')) + try: + resp = yield self._sendManagerRequest(0, m.digest()) + except Exception: + raise errors.LoginFailedError('Incorrect password.') else: method = 'username+password' require_secure_connection(method) @@ -543,14 +556,6 @@ def connect(host=C.MANAGER_HOST, port=None, tls_mode=C.MANAGER_TLS, def authenticate(p): yield p.authenticate(username, password, headless) - if tls_mode == 'on': - tls_options = crypto.tls_options(host) - p = yield _factory.connectSSL(host, port, tls_options, timeout=C.TIMEOUT) - p.set_address(host, port) - p.spawn_kw = spawn_kw - yield authenticate(p) - returnValue(p) - @inlineCallbacks def do_connect(): p = yield _factory.connectTCP(host, port, timeout=C.TIMEOUT) @@ -580,6 +585,15 @@ def ping(p): manager_features = set() returnValue(manager_features) + if tls_mode == 'on': + tls_options = crypto.tls_options(host) + p = yield _factory.connectSSL(host, port, tls_options, timeout=C.TIMEOUT) + p.set_address(host, port) + p.spawn_kw = spawn_kw + p.manager_features = yield ping(p) + yield authenticate(p) + returnValue(p) + p = yield do_connect() is_local_connection = util.is_local_connection(p.transport) if ((tls_mode == 'starttls-force') or