def devkey(conn: ServerConnection, devid: str, oldpair: EncryptionPair, newpair: EncryptionPair): '''Replaces the specified device's key stored on the server''' if not utils.validate_uuid(devid): return RetVal(AnsBadRequest, 'Invalid device ID').set_value('status', 400) conn.send_message({ 'Action' : "DEVKEY", 'Data' : { 'Device-ID': devid, 'Old-Key': oldpair.public.as_string(), 'New-Key': newpair.public.as_string() } }) # Receive, decrypt, and return the server challenge response = conn.read_response(server_response) if response.error(): return response if response['Code'] != 100: return wrap_server_error(response) if 'Challenge' not in response['Data'] or 'New-Challenge' not in response['Data']: return RetVal(ServerError, 'server did not return both device challenges') status = oldpair.decrypt(response['Data']['Challenge']) if status.error(): cancel(conn) return RetVal(DecryptionFailure, 'failed to decrypt device challenge for old key') request = { 'Action' : "DEVKEY", 'Data' : { 'Response' : status['data'] } } status = newpair.decrypt(response['Data']['New-Challenge']) if status.error(): cancel(conn) return RetVal(DecryptionFailure, 'failed to decrypt device challenge for new key') request['Data']['New-Response'] = status['data'] conn.send_message(request) response = conn.read_response(None) if response.error(): return response if response['Code'] == 200: return RetVal() return wrap_server_error(response)
def test_preregister_regcode(): '''Test the preregister and regcode commands''' dbconn = setup_test() dbdata = init_server(dbconn) conn = serverconn.ServerConnection() status = conn.connect('localhost', 2001) assert not status.error( ), f"test_login(): failed to connect to server: {status.info()}" password = Password('Linguini2Pegboard*Album') devid = '14142135-9c22-4d3e-84a3-2aa281f65714' keypair = EncryptionPair( CryptoString(r'CURVE25519:mO?WWA-k2B2O|Z%fA`~s3^$iiN{5R->#jxO@cy6{'), CryptoString(r'CURVE25519:2bLf2vMA?GA2?L~tv<PA9XOw6e}V~ObNi7C&qek>')) status = serverconn.regcode(conn, 'admin', dbdata['admin_regcode'], password.hashstring, devid, keypair, '') assert not status.error( ), f"test_preregister_regcode(): regcode failed: {status.info()}" status = serverconn.login(conn, dbdata['admin_wid'], CryptoString(dbdata['oekey'])) assert not status.error( ), f"test_preregister_regcode(): login phase failed: {status.info()}" status = serverconn.password(conn, dbdata['admin_wid'], password.hashstring) assert not status.error( ), f"test_preregister_regcode(): password phase failed: {status.info()}" status = serverconn.device(conn, devid, keypair) assert not status.error(), "test_preregister_regcode(): device phase failed: " \ f"{status.info()}" status = serverconn.preregister(conn, '', 'csimons', 'example.net') assert not status.error( ), "test_preregister_regcode(): uid preregistration failed" assert status['domain'] == 'example.net' and 'wid' in status and 'regcode' in status and \ status['uid'] == 'csimons', "test_preregister_regcode(): failed to return expected data" regdata = status password = Password('MyS3cretPassw*rd') devpair = EncryptionPair() status = serverconn.regcode(conn, 'csimons', regdata['regcode'], password.hashstring, '11111111-1111-1111-1111-111111111111', devpair, 'example.net') assert not status.error(), "test_preregister_regcode(): uid regcode failed" conn.disconnect()
def init_user(conn: serverconn.ServerConnection, config: dict) -> RetVal: '''Creates a test user for command testing''' userwid = '33333333-3333-3333-3333-333333333333' status = serverconn.preregister(conn, userwid, 'csimons', 'example.net') assert not status.error(), "init_user(): uid preregistration failed" assert status['domain'] == 'example.net' and 'wid' in status and 'regcode' in status and \ status['uid'] == 'csimons', "init_user(): failed to return expected data" regdata = status password = Password('MyS3cretPassw*rd') devpair = EncryptionPair() devid = '11111111-1111-1111-1111-111111111111' status = serverconn.regcode(conn, 'csimons', regdata['regcode'], password.hashstring, devid, devpair, 'example.net') assert not status.error(), "init_user(): uid regcode failed" config['user_wid'] = userwid config['user_uid'] = regdata['uid'] config['user_domain'] = regdata['domain'] config['user_devid'] = devid config['user_devpair'] = devpair config['user_password'] = password return RetVal()
def test_login(): '''Performs a basic login intended to be successful''' dbconn = setup_test() dbdata = init_server(dbconn) conn = ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair # Most of the code which was originally written for this test is needed for other tests # because commands like PREREG require being logged in as the administrator. Both of these # functions still perform all the necessary tests that were originally done here. regcode_admin(dbdata, conn) login_admin(dbdata, conn) conn.send_message({'Action': "QUIT"})
def test_setpassword(): '''Tests the SETPASSWORD command''' dbconn = setup_test() dbdata = init_server(dbconn) conn = ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) badpwhash = '$argon2id$v=19$m=65536,t=2,' \ 'p=1$l0nvPkQDxJDKIMZsA96x4A$c5+XHmZjzSJIg3/vXXr33YNB1Jl52mdXtbMASLP1abs' newpwhash = '$argon2id$v=19$m=65536,t=2,' \ 'p=1$4WOWeLn7oOmrkp41zMzMcQ$8G51UCiIC/eETPJf0RZ5cNkX7jr3NkT92etTwNEO+f0' # Subtest #1: Failure because of wrong password sent conn.send_message({ 'Action': "SETPASSWORD", 'Data': { 'Password-Hash': badpwhash, 'NewPassword-Hash': newpwhash } }) response = conn.read_response(None) assert response['Code'] == 402 and response['Status'] == 'AUTHENTICATION FAILURE', \ 'test_setpassword(): Failed to catch bad password' # Subtest #2: Successful password change conn.send_message({ 'Action': "SETPASSWORD", 'Data': { 'Password-Hash': pwhash, 'NewPassword-Hash': newpwhash } }) response = conn.read_response(None) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_setpassword(): Failed to update admin password' conn.send_message({'Action': "QUIT"})
def test_reset_password(): '''Tests password reset code''' dbconn = setup_test() dbdata = init_server(dbconn) conn = serverconn.ServerConnection() status = conn.connect('localhost', 2001) assert not status.error( ), f"test_login(): failed to connect to server: {status.info()}" password = Password('Linguini2Pegboard*Album') devid = '14142135-9c22-4d3e-84a3-2aa281f65714' keypair = EncryptionPair( CryptoString(r'CURVE25519:mO?WWA-k2B2O|Z%fA`~s3^$iiN{5R->#jxO@cy6{'), CryptoString(r'CURVE25519:2bLf2vMA?GA2?L~tv<PA9XOw6e}V~ObNi7C&qek>')) status = serverconn.regcode(conn, 'admin', dbdata['admin_regcode'], password.hashstring, devid, keypair, '') assert not status.error( ), f"test_reset_password(): regcode failed: {status.info()}" status = serverconn.login(conn, dbdata['admin_wid'], CryptoString(dbdata['oekey'])) assert not status.error( ), f"test_reset_password(): login phase failed: {status.info()}" status = serverconn.password(conn, dbdata['admin_wid'], password.hashstring) assert not status.error( ), f"test_reset_password(): password phase failed: {status.info()}" status = serverconn.device(conn, devid, keypair) assert not status.error(), "test_reset_password(): device phase failed: " \ f"{status.info()}" status = init_user(conn, dbdata) assert not status.error( ), f"test_reset_password(): user init failed: {status.info()}" status = serverconn.reset_password(conn, dbdata['user_wid']) assert not status.error( ), f"test_reset_password(): password reset failed: {status.info()}" resetdata = status status = serverconn.logout(conn) assert not status.error( ), f"test_reset_password(): admin logout failed: {status.info()}" newpassword = Password('SomeOth3rPassw*rd') status = serverconn.passcode(conn, dbdata['user_wid'], resetdata['resetcode'], newpassword.hashstring) assert not status.error( ), f"test_reset_password(): passcode failed: {status.info()}"
def device(conn: ServerConnection, devid: str, devpair: EncryptionPair) -> RetVal: '''Completes the login process by submitting device ID and its session string.''' if not utils.validate_uuid(devid): return RetVal(AnsBadRequest, 'Invalid device ID').set_value('status', 400) conn.send_message({ 'Action' : "DEVICE", 'Data' : { 'Device-ID' : devid, 'Device-Key' : devpair.public.as_string() } }) # Receive, decrypt, and return the server challenge response = conn.read_response(server_response) if response.error(): return response if response['Code'] != 100: return wrap_server_error(response) if 'Challenge' not in response['Data']: return RetVal(ServerError, 'server did not return a device challenge') status = devpair.decrypt(response['Data']['Challenge']) if status.error(): cancel(conn) return RetVal(DecryptionFailure, 'failed to decrypt device challenge') conn.send_message({ 'Action' : "DEVICE", 'Data' : { 'Device-ID' : devid, 'Device-Key' : devpair.public.as_string(), 'Response' : status['data'] } }) response = conn.read_response(None) if response.error(): return response if response['Code'] == 200: return RetVal() return wrap_server_error(response)
def keycard_admin(config, conn) -> dict: '''Uploads a keycard entry for the admin account''' conn = serverconn.ServerConnection() status = conn.connect('localhost', 2001) assert not status.error( ), f"test_addentry(): failed to connect to server: {status.info()}" crepair = EncryptionPair( CryptoString(r'CURVE25519:mO?WWA-k2B2O|Z%fA`~s3^$iiN{5R->#jxO@cy6{'), CryptoString(r'CURVE25519:2bLf2vMA?GA2?L~tv<PA9XOw6e}V~ObNi7C&qek>')) crspair = SigningPair( CryptoString(r'ED25519:E?_z~5@+tkQz!iXK?oV<Zx(ec;=27C8Pjm((kRc|'), CryptoString(r'ED25519:u4#h6LEwM6Aa+f<++?lma4Iy63^}V$JOP~ejYkB;')) epair = EncryptionPair( CryptoString(r'CURVE25519:Umbw0Y<^cf1DN|>X38HCZO@Je(zSe6crC6X_C_0F'), CryptoString(r'CURVE25519:Bw`F@ITv#sE)2NnngXWm7RQkxg{TYhZQbebcF5b$')) entry = keycard.UserEntry() entry.set_fields({ 'Name': 'Administrator', 'Workspace-ID': config['admin_wid'], 'User-ID': 'admin', 'Domain': 'example.com', 'Contact-Request-Verification-Key': crspair.get_public_key(), 'Contact-Request-Encryption-Key': crepair.get_public_key(), 'Public-Encryption-Key': epair.get_public_key() }) status = serverconn.addentry(conn, entry, CryptoString(config['ovkey']), crspair) assert not status.error( ), f"test_addentry: failed to add entry: {status.info()}" return { 'admin_crepair': crepair, 'admin_crspair': crspair, 'admin_epair': epair }
def test_overflow(): '''Tests the server's command handling for commands greater than 8K''' dbconn = setup_test() dbdata = init_server(dbconn) conn = serverconn.ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) conn.send_message({ 'Action': "REGISTER", 'Data': { 'Workspace-ID': 'A' * 10240, 'Password-Hash': pwhash, 'Device-ID': '11111111-1111-1111-1111-111111111111', 'Device-Key': 'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z' } }) response = conn.read_response(server_response) assert response['Code'] == 400 and response['Status'] == 'BAD REQUEST', \ 'test_overflow: failed to catch overflow' conn.send_message({'Action': "QUIT"})
def test_devkey(): '''Tests the devkey() command''' dbconn = setup_test() dbdata = init_server(dbconn) conn = serverconn.ServerConnection() status = conn.connect('localhost', 2001) assert not status.error( ), f"test_devkey(): failed to connect to server: {status.info()}" status = init_admin(conn, dbdata) assert not status.error( ), f"test_devkey(): init_admin failed: {status.info()}" newdevpair = EncryptionPair( CryptoString(r'CURVE25519:mO?WWA-k2B2O|Z%fA`~s3^$iiN{5R->#jxO@cy6{'), CryptoString(r'CURVE25519:2bLf2vMA?GA2?L~tv<PA9XOw6e}V~ObNi7C&qek>')) status = serverconn.devkey(conn, dbdata['admin_devid'], dbdata['admin_devpair'], newdevpair) assert not status.error( ), f"test_devkey(): error returned: {status.info()}" conn.disconnect()
def test_register(): '''Test worskpace registration''' # Registration testing only works when the server uses either network or public mode serverdbdata = load_server_config_file() if serverdbdata['global']['registration'] not in ['network', 'public']: return dbconn = setup_test() init_server(dbconn) conn = serverconn.ServerConnection() status = conn.connect('localhost', 2001) assert not status.error( ), f"test_login(): failed to connect to server: {status.info()}" password = Password('MyS3cretPassw*rd') devpair = EncryptionPair() status = serverconn.register(conn, 'csimons', password.hashstring, devpair.public) assert not status.error( ), f"test_register: failed to register test account: {status.info()}" conn.disconnect()
def test_unregister(): '''Test the UNREGISTER command''' # Testing the UNREGISTER command only works when the server uses either network or public mode serverconfig = load_server_config_file() if serverconfig['global']['registration'] not in ['network', 'public']: return dbconn = setup_test() dbdata = init_server(dbconn) conn = serverconn.ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) userwid = '11111111-1111-1111-1111-111111111111' # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devkey = 'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z' conn.send_message({ 'Action': "REGISTER", 'Data': { 'Workspace-ID': userwid, 'Password-Hash': pwhash, 'Device-ID': devid, 'Device-Key': devkey } }) response = conn.read_response(server_response) assert response['Code'] == 201 and response['Status'] == 'REGISTERED', \ f"test_unregister: test user registration failed: {response['Status']}" # Subtest #1: Try to unregister the admin account conn.send_message({ 'Action': "UNREGISTER", 'Data': { 'Password-Hash': pwhash } }) response = conn.read_response(server_response) assert response['Code'] == 403 and response['Status'] == 'FORBIDDEN', \ "test_unregister(): failed to properly handle trying to unregister admin account" conn.send_message({'Action': "LOGOUT", 'Data': {}}) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK' # Set up for subtest #2: log in as the user status = serverconn.login(conn, userwid, CryptoString(dbdata['oekey'])) assert not status.error( ), f"test_unregister(): user login phase failed: {status.info()}" status = serverconn.password(conn, userwid, pwhash) assert not status.error( ), f"test_unregister(): password phase failed: {status.info()}" status = serverconn.device(conn, devid, devpair) assert not status.error( ), f"test_unregister(): device phase failed: {status.info()}" # As a general rule, these anselusd integration tests don't call the regular pyanselus client # library calls because we do extra validation. However, we're going to make an exception in # this test because LOGIN and ADDENTRY are both really big. usercard = keycard.UserEntry() usercard.set_fields({ 'Name': 'Corbin Simons', 'Workspace-ID': userwid, 'User-ID': 'csimons', 'Domain': 'example.com', 'Contact-Request-Verification-Key': 'ED25519:E?_z~5@+tkQz!iXK?oV<Zx(ec;=27C8Pjm((kRc|', 'Contact-Request-Encryption-Key': 'CURVE25519:yBZ0{1fE9{2<b~#i^R+JT-yh-y5M(Wyw_)}_SZOn', 'Public-Encryption-Key': 'CURVE25519:_`UC|vltn_%P5}~vwV^)oY){#uvQSSy(dOD_l(yE' }) crspair = SigningPair( CryptoString(r'ED25519:E?_z~5@+tkQz!iXK?oV<Zx(ec;=27C8Pjm((kRc|'), CryptoString(r'ED25519:u4#h6LEwM6Aa+f<++?lma4Iy63^}V$JOP~ejYkB;')) status = usercard.is_data_compliant() assert not status.error( ), f"test_unregister: user card not compliant: {status.info()}" status = serverconn.addentry(conn, usercard, CryptoString(dbdata['ovkey']), crspair) assert not status.error(), f"test_unregister: addentry() failed: {status.info()}\n" \ f"Server Info: {status['Info']}" # Subtest #2: Unregister regular user from admin account conn.send_message({ 'Action': "UNREGISTER", 'Data': { 'Workspace-ID': userwid, 'Password-Hash': pwhash } }) response = conn.read_response(server_response) assert response['Code'] == 202 and response['Status'] == 'UNREGISTERED', \ f"test_unregister(): user unregistration failed: {response['Status']}" # Check to make sure the database end was handled correctly cur = dbconn.cursor() cur.execute('SELECT password,status FROM workspaces WHERE wid = %s ', (userwid, )) row = cur.fetchone() assert row, "test_unregister(): cleanup check query found no rows" assert row[0] == '-' and row[1] == 'deleted', \ "test_unregister(): server failed to clean up database properly" conn.send_message({'Action': "QUIT"})
def test_register(): '''Tests the server's REGISTER command - success and duplicate WID condition''' # Testing the REGISTER command only works when the server uses either network or public mode serverconfig = load_server_config_file() if serverconfig['global']['registration'] not in ['network', 'public']: return dbconn = setup_test() dbdata = init_server(dbconn) conn = serverconn.ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) wid = '11111111-1111-1111-1111-111111111111' # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devkey = 'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z' # Subtest #1: Regular registration that is supposed to succeed conn.send_message({ 'Action': "REGISTER", 'Data': { 'Workspace-ID': wid, 'Password-Hash': pwhash, 'Device-ID': '11111111-1111-1111-1111-111111111111', 'Device-Key': devkey } }) response = conn.read_response(server_response) assert response['Code'] == 201 and response['Status'] == 'REGISTERED', \ 'test_register: subtest #1 returned an error' # Subtest #2: Attempt registration of existing WID conn.send_message({ 'Action': "REGISTER", 'Data': { 'Workspace-ID': wid, 'Password-Hash': pwhash, 'Device-ID': '11111111-1111-1111-1111-111111111111', 'Device-Key': devkey } }) response = conn.read_response(server_response) assert response['Code'] == 408 and response['Status'] == 'RESOURCE EXISTS', \ 'test_register: subtest #2 failed to catch duplicate registration' conn.send_message({'Action': "QUIT"})
def test_addentry_usercard(): '''Tests the ADDENTRY and USERCARD command processes''' dbconn = setup_test() dbdata = init_server(dbconn) conn = ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) # Test setup is complete. Create a test keycard and do ADDENTRY # 1) Client sends the `ADDENTRY` command, attaching the entry data between the # `----- BEGIN USER KEYCARD -----` header and the `----- END USER KEYCARD -----` footer. # 2) The server then checks compliance of the entry data. Assuming that it complies, the server # generates a cryptographic signature and responds with `100 CONTINUE`, returning the # signature, the hash of the data, and the hash of the previous entry in the database. # 3) The client verifies the signature against the organization’s verification key. This has # the added benefit of ensuring that none of the fields were altered by the server and that # the signature is valid. # 4) The client appends the hash from the previous entry as the `Previous-Hash` field # 5) The client verifies the hash value for the entry from the server and sets the `Hash` field # 6) The client signs the entry as the `User-Signature` field and then uploads the result to # the server. # 7) Once uploaded, the server validates the `Hash` and `User-Signature` fields, and, # assuming that all is well, adds it to the keycard database and returns `200 OK`. # # Once that monster of a process completes, the card should be successfully added, but we're not # done yet. From there, create a new user entry, chain it to the first, and upload it. This will # test the chaining code in the AddEntry command processing. # # Finally, once that entire mess is complete, send a USERCARD command and verify what is # returned against the data set up so far. It doesn't seem like piggybacking a test for a # different command off another is the right thing to do, but in this case, it's the perfect # setup to confirm that a user's entire keycard is handled properly. usercard = keycard.UserEntry() usercard.set_fields({ 'Name': 'Test Administrator', 'Workspace-ID': dbdata['admin_wid'], 'User-ID': 'admin', 'Domain': 'example.com', 'Contact-Request-Verification-Key': 'ED25519:d0-oQb;{QxwnO{=!|^62+E=UYk2Y3mr2?XKScF4D', 'Contact-Request-Encryption-Key': 'CURVE25519:yBZ0{1fE9{2<b~#i^R+JT-yh-y5M(Wyw_)}_SZOn', 'Public-Encryption-Key': 'CURVE25519:_`UC|vltn_%P5}~vwV^)oY){#uvQSSy(dOD_l(yE' }) conn.send_message({ 'Action': "ADDENTRY", 'Data': { 'Base-Entry': usercard.make_bytestring(0).decode() } }) response = conn.read_response(server_response) assert response['Code'] == 100 and \ response['Status'] == 'CONTINUE' and \ 'Organization-Signature' in response['Data'] and \ 'Hash' in response['Data'] and \ 'Previous-Hash' in response['Data'], 'test_addentry(): server did return all needed fields' usercard.signatures['Organization'] = response['Data'][ 'Organization-Signature'] # A regular client will check the entry cache, pull updates to the org card, and get the # verification key. Because this is just an integration test, we skip all that and just use # the known verification key from earlier in the test. status = usercard.verify_signature(CryptoString(dbdata['ovkey']), 'Organization') assert not status.error( ), f"test_addentry(): org signature didn't verify: {status.info()}" assert response['Data']['Previous-Hash'] == dbdata['second_org_entry'].hash, \ "test_addentry(): server did not attach correct Previous Hash to root user entry" usercard.prev_hash = response['Data']['Previous-Hash'] usercard.hash = response['Data']['Hash'] status = usercard.verify_hash() assert not status.error( ), f"test_addentry(): hash didn't verify: {status.info()}" # User sign and verify skey = CryptoString('ED25519:ip52{ps^jH)t$k-9bc_RzkegpIW?}FFe~BX&<V}9') assert skey.is_valid( ), "test_addentry(): failed to set user's cr signing key" status = usercard.sign(skey, 'User') assert not status.error(), "test_addentry(): failed to user sign" vkey = CryptoString('ED25519:d0-oQb;{QxwnO{=!|^62+E=UYk2Y3mr2?XKScF4D') assert vkey.is_valid( ), "test_addentry(): failed to set user's cr verification key" status = usercard.verify_signature(vkey, 'User') status = usercard.is_compliant() assert not status.error( ), f"test_addentry(): compliance error: {str(status)}" conn.send_message({ 'Action': "ADDENTRY", 'Data': { 'User-Signature': usercard.signatures['User'] } }) response = conn.read_response(server_response) assert response['Code'] == 200 and \ response['Status'] == 'OK', f"test_addentry(): final upload server error {response}" # Now to test ADDENTRY with a chained entry. Hold on tight! # Submit the chained entry newkeys = usercard.chain(skey, True) assert not newkeys.error( ), f"test_addentry(): new entry failed to chain: {newkeys.info()}" second_user_entry = newkeys['entry'] conn.send_message({ 'Action': "ADDENTRY", 'Data': { 'Base-Entry': second_user_entry.make_bytestring(1).decode() } }) response = conn.read_response(server_response) assert response['Code'] == 100 and \ response['Status'] == 'CONTINUE' and \ 'Organization-Signature' in response['Data'] and \ 'Hash' in response['Data'] and \ 'Previous-Hash' in response['Data'], 'test_addentry(): server did return all needed fields' second_user_entry.signatures['Organization'] = response['Data'][ 'Organization-Signature'] # Verify the server's signature, add and verify the hashes, sign, and upload status = second_user_entry.verify_signature(CryptoString(dbdata['ovkey']), 'Organization') assert not status.error( ), f"test_addentry(): org signature didn't verify: {status.info()}" second_user_entry.prev_hash = response['Data']['Previous-Hash'] assert response['Data']['Previous-Hash'] == usercard.hash, \ "Server did not return correct chained user entry hash" second_user_entry.hash = response['Data']['Hash'] status = second_user_entry.verify_hash() assert not status.error( ), f"test_addentry(): hash didn't verify: {status.info()}" skey = CryptoString(newkeys['crsign.private']) assert skey.is_valid(), "test_addentry(): failed to set user signing key" status = second_user_entry.sign(skey, 'User') assert not status.error(), "test_addentry(): failed to user sign" vkey = CryptoString(newkeys['crsign.public']) assert vkey.is_valid( ), "test_addentry(): failed to set user verification key" status = second_user_entry.verify_signature(vkey, 'User') status = second_user_entry.is_compliant() assert not status.error( ), f"test_addentry(): compliance error: {str(status)}" conn.send_message({ 'Action': "ADDENTRY", 'Data': { 'User-Signature': second_user_entry.signatures['User'] } }) response = conn.read_response(server_response) assert response['Code'] == 200 and \ response['Status'] == 'OK', f"test_addentry(): final upload server error {response}" conn.send_message({ 'Action': "USERCARD", 'Data': { 'Owner': 'admin/example.com', 'Start-Index': '1' } }) response = conn.read_response(server_response) assert response['Code'] == 104 and response['Status'] == 'TRANSFER', \ 'test_addentry.usercard: server returned %s' % response['Status'] assert response['Data']['Item-Count'] == '2', \ 'test_addentry.usercard: server returned wrong number of items' data_size = int(response['Data']['Total-Size']) conn.send_message({'Action': 'TRANSFER'}) chunks = list() tempstr = conn.read() data_read = len(tempstr) chunks.append(tempstr) while data_read < data_size: tempstr = conn.read() data_read = data_read + len(tempstr) chunks.append(tempstr) assert data_read == data_size, 'test_orgcard.usercard: size mismatch' # Now that the data has been downloaded, we put it together and split it properly. We should # have two entries entries = ''.join(chunks).split('----- END USER ENTRY -----\r\n') if entries[-1] == '': entries.pop() # These entries are added to the database in init_server(). The insert operations are not # done here because the two org entries are needed for other tests, as well. assert len( entries ) == 2, "test_orgcard.usercard: server did not send the expected entry" assert entries[1] == '----- BEGIN USER ENTRY -----\r\n' + \ second_user_entry.make_bytestring(-1).decode(), \ "test_orgcard.usercard: entry didn't match" conn.send_message({'Action': "QUIT"})
def test_set_status(): '''Tests the SETSTATUS command''' dbconn = setup_test() dbdata = init_server(dbconn) uid = 'csimons' domain = 'example.net' conn = ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) # Prereg with a test user conn.send_message({ 'Action': "PREREG", 'Data': { 'User-ID': uid, 'Domain': domain } }) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_set_status: failed to prereg test user' # Call REGCODE to actually register the user regdata = response pwd = Password() status = pwd.Set('ShrivelCommuteGottenAgonizingElbowQuiver') assert not status.error(), 'test_set_status: Failed to set password' devid = '0e6406e3-1831-4352-9fbe-0de8faebf0f0' devkey = EncryptionPair() # Subtest #2: Regcode with User ID and domain conn.send_message({ 'Action': "REGCODE", 'Data': { 'User-ID': regdata['Data']['User-ID'], 'Domain': regdata['Data']['Domain'], 'Reg-Code': regdata['Data']['Reg-Code'], 'Password-Hash': pwd.hashstring, 'Device-ID': devid, 'Device-Key': devkey.get_public_key() } }) response = conn.read_response(server_response) assert response['Code'] == 201 and response['Status'] == 'REGISTERED', \ 'test_set_status: failed to register test user' conn.send_message({ 'Action': 'SETSTATUS', 'Data': { 'Workspace-ID': regdata['Data']['Workspace-ID'], 'Status': 'disabled' } }) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_set_status: failed to disable test user'
def chain(self, key: CryptoString, rotate_optional: bool) -> RetVal: '''Creates a new UserEntry object with new keys and a custody signature. It requires the previous contact request signing key passed as an CryptoString. The new keys are returned in CryptoString format using the following fields: entry sign.public / sign.private -- primary signing keypair crsign.public / crsign.private -- contact request signing keypair crencrypt.public / crencrypt.private -- contact request encryption keypair encrypt.public / encrypt.private -- general-purpose public encryption keypair altencrypt.public / altencrypt.private -- alternate public encryption keypair Note that the last two keys are not required to be updated during entry rotation so that they can be rotated on a different schedule from the other keys. These fields are only returned if there are no errors. ''' if key.prefix != 'ED25519': return RetVal(BadParameterValue, f'wrong key type {key.prefix}') status = self.is_compliant() if status.error(): return status new_entry = UserEntry() new_entry.fields = self.fields.copy() try: index = int(new_entry.fields['Index']) new_entry.fields['Index'] = str(index + 1) except Exception: return RetVal(BadData, 'invalid entry index') out = RetVal() skey = SigningPair() crskey = SigningPair() crekey = EncryptionPair() out['sign.public'] = skey.get_public_key() out['sign.private'] = skey.get_private_key() out['crsign.public'] = crskey.get_public_key() out['crsign.private'] = crskey.get_private_key() out['crencrypt.public'] = crekey.get_public_key() out['crencrypt.private'] = crekey.get_private_key() new_entry.fields['Contact-Request-Verification-Key'] = out[ 'crsign.public'] new_entry.fields['Contact-Request-Encryption-Key'] = out[ 'crencrypt.public'] if rotate_optional: ekey = EncryptionPair() out['encrypt.public'] = ekey.get_public_key() out['encrypt.private'] = ekey.get_private_key() aekey = EncryptionPair() out['altencrypt.public'] = aekey.get_public_key() out['altencrypt.private'] = aekey.get_private_key() new_entry.fields['Public-Encryption-Key'] = out['encrypt.public'] new_entry.fields['Alternate-Encryption-Key'] = out[ 'altencrypt.public'] else: out['encrypt.public'] = '' out['encrypt.private'] = '' out['altencrypt.public'] = '' out['altencrypt.private'] = '' status = new_entry.sign(key, 'Custody') if status.error(): return status out['entry'] = new_entry return out
def chain(self, key: CryptoString, rotate_optional: bool) -> RetVal: '''Creates a new OrgEntry object with new keys and a custody signature. The keys are returned in CryptoString format using the following fields: entry sign.public / sign.private -- primary signing keypair sign.pubhash / sign.privhash -- hashes of the corresponding keys altsign.public / altsign.private -- contact request signing keypair altsign.pubhash / altsign.privhash -- hashes of the corresponding keys encrypt.public / encrypt.private -- general-purpose public encryption keypair encrypt.pubhash / encrypt.privhash -- hashes of the corresponding keys For organization entries, rotating optional keys works a little differently: the primary signing key becomes the secondary signing key in the new entry. When rotation is False, which is recommended only in instances of revocation, the secondary key is removed. Only when rotate_optional is True is the field altsign.private returned. ''' if key.prefix != 'ED25519': return RetVal(BadParameterValue, f'wrong key type {key.prefix}') status = self.is_compliant() if status.error(): return status new_entry = OrgEntry() new_entry.fields = self.fields.copy() try: index = int(new_entry.fields['Index']) new_entry.fields['Index'] = str(index + 1) except Exception: return RetVal(BadData, 'invalid entry index') out = RetVal() skey = SigningPair() ekey = EncryptionPair() out['sign.public'] = skey.get_public_key() out['sign.pubhash'] = skey.get_public_hash() out['sign.private'] = skey.get_private_key() out['sign.privhash'] = skey.get_private_hash() out['encrypt.public'] = ekey.get_public_key() out['encrypt.pubhash'] = ekey.get_public_hash() out['encrypt.private'] = ekey.get_private_key() out['encrypt.privhash'] = ekey.get_private_hash() if rotate_optional: altskey = SigningPair() out['altsign.public'] = altskey.get_public_key() out['altsign.pubhash'] = altskey.get_public_hash() out['altsign.private'] = altskey.get_private_key() out['altsign.privhash'] = altskey.get_private_hash() else: out['altsign.public'] = self.fields['Primary-Verification-Key'] out['altsign.pubhash'] = blake2hash( self.fields['Primary-Verification-Key'].as_string().encode()) out['altsign.private'] = '' status = new_entry.sign(key, 'Custody') if status.error(): return status out['entry'] = new_entry return out
def register_account(self, server: str, userid: str, userpass: str) -> RetVal: '''Create a new account on the specified server.''' # Process for registration of a new account: # # Check to see if we already have a workspace allocated on this profile. Because we don't # yet support shared workspaces, it means that there are only individual ones. Each # profile can have only one individual workspace. # # Check active profile for an existing workspace entry # Get the password from the user # Check active workspace for device entries. Because we are registering, existing device # entries should be removed. # Add a device entry to the workspace. This includes both an encryption keypair and # a UUID for the device # Connect to requested server # Send registration request to server, which requires a hash of the user's supplied # password # Close the connection to the server # If the server returns an error, such as 304 REGISTRATION CLOSED, then return an error. # If the server has returned anything else, including a 101 PENDING, begin the # client-side workspace information to generate. # Call storage.generate_profile_data() # Add the device ID and session string to the profile # Create the necessary client-side folders # Generate the folder mappings # If the server returned 201 REGISTERED, we can proceed with the server-side setup # # Create the server-side folders based on the mappings on the client side # Save all encryption keys into an encrypted 7-zip archive which uses the hash of the # user's password has the archive encryption password and upload the archive to the server. if self.fs.pman.get_active_profile().domain: return RetVal(ResourceExists, 'a user workspace already exists') # Parse server string. Should be in the form of (ip/domain):portnum host = '' port = -1 if ':' in server: addressparts = server.split(':') host = addressparts[0] try: port = int(addressparts[1]) except ValueError: return RetVal(BadParameterValue, 'bad server string') serverstring = server else: host = server port = 2001 # Password requirements aren't really set here, but we do have to draw the # line *somewhere*. pw = Password() status = pw.Set(userpass) if status.error(): return status # Add the device to the workspace devpair = EncryptionPair() status = self.conn.connect(host, port) if status.error(): return status regdata = serverconn.register(self.conn, userid, pw.hashstring, devpair.public) if regdata.error(): return regdata self.conn.disconnect() # Possible status codes from register() # 304 - Registration closed # 406 - Payment required # 101 - Pending # 201 - Registered # 300 - Internal server error # 408 - Resource exists if regdata['status'] in [304, 406, 300, 408]: return regdata # Just a basic sanity check if 'wid' not in regdata: return RetVal(InternalError, 'BUG: bad data from serverconn.register()') \ .set_value('status', 300) w = Workspace(self.fs.pman.get_active_profile().db, self.fs.pman.get_active_profile().path) status = w.generate(self.fs.pman.get_active_profile(), server, regdata['wid'], pw) if status.error(): return status address = '/'.join([regdata['wid'], serverstring]) status = auth.add_device_session(self.fs.pman.get_active_profile().db, address, regdata['devid'], devpair.enctype, devpair.public, devpair.private, socket.gethostname()) if status.error(): return status return regdata
def init_admin(conn: serverconn.ServerConnection, config: dict) -> RetVal: '''Finishes setting up the admin account by registering it, logging in, and uploading a root keycard entry''' password = Password('Linguini2Pegboard*Album') config['admin_password'] = password devid = '14142135-9c22-4d3e-84a3-2aa281f65714' devpair = EncryptionPair( CryptoString(r'CURVE25519:mO?WWA-k2B2O|Z%fA`~s3^$iiN{5R->#jxO@cy6{'), CryptoString(r'CURVE25519:2bLf2vMA?GA2?L~tv<PA9XOw6e}V~ObNi7C&qek>')) config['admin_devid'] = devid config['admin_devpair'] = devpair crepair = EncryptionPair( CryptoString(r'CURVE25519:mO?WWA-k2B2O|Z%fA`~s3^$iiN{5R->#jxO@cy6{'), CryptoString(r'CURVE25519:2bLf2vMA?GA2?L~tv<PA9XOw6e}V~ObNi7C&qek>')) config['admin_crepair'] = devpair crspair = SigningPair( CryptoString(r'ED25519:E?_z~5@+tkQz!iXK?oV<Zx(ec;=27C8Pjm((kRc|'), CryptoString(r'ED25519:u4#h6LEwM6Aa+f<++?lma4Iy63^}V$JOP~ejYkB;')) config['admin_crspair'] = devpair epair = EncryptionPair( CryptoString(r'CURVE25519:Umbw0Y<^cf1DN|>X38HCZO@Je(zSe6crC6X_C_0F'), CryptoString(r'CURVE25519:Bw`F@ITv#sE)2NnngXWm7RQkxg{TYhZQbebcF5b$')) config['admin_epair'] = devpair status = serverconn.regcode(conn, 'admin', config['admin_regcode'], password.hashstring, devid, devpair, '') assert not status.error(), f"init_admin(): regcode failed: {status.info()}" status = serverconn.login(conn, config['admin_wid'], CryptoString(config['oekey'])) assert not status.error( ), f"init_admin(): login phase failed: {status.info()}" status = serverconn.password(conn, config['admin_wid'], password.hashstring) assert not status.error( ), f"init_admin(): password phase failed: {status.info()}" status = serverconn.device(conn, devid, devpair) assert not status.error(), "init_admin(): device phase failed: " \ f"{status.info()}" entry = keycard.UserEntry() entry.set_fields({ 'Name': 'Administrator', 'Workspace-ID': config['admin_wid'], 'User-ID': 'admin', 'Domain': 'example.com', 'Contact-Request-Verification-Key': crspair.get_public_key(), 'Contact-Request-Encryption-Key': crepair.get_public_key(), 'Public-Encryption-Key': epair.get_public_key() }) status = serverconn.addentry(conn, entry, CryptoString(config['ovkey']), crspair) assert not status.error( ), f"init_admin: failed to add entry: {status.info()}" status = serverconn.iscurrent(conn, 1, config['admin_wid']) assert not status.error(), "init_admin(): admin iscurrent() success check failed: " \ f"{status.info()}" status = serverconn.iscurrent(conn, 2, config['admin_wid']) assert not status.error(), "init_admin(): admin iscurrent() failure check failed: " \ f"{status.info()}" return RetVal()
def test_register_failures(): '''Tests the server's REGISTER command with failure conditions''' # Testing the REGISTER command only works when the server uses either network or public mode serverconfig = load_server_config_file() if serverconfig['global']['registration'] not in ['network', 'public']: return dbconn = setup_test() dbdata = init_server(dbconn) conn = serverconn.ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) # Test #1: Attempt registration with unsupported encryption type conn.send_message({ 'Action': "REGISTER", 'Data': { 'Workspace-ID': '11111111-1111-1111-1111-222222222222', 'Password-Hash': pwhash, 'Device-ID': '11111111-1111-1111-1111-111111111111', 'Device-Key': '3DES:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z' } }) response = conn.read_response(server_response) assert response['Code'] == 309 and response['Status'] == 'ENCRYPTION TYPE NOT SUPPORTED', \ 'test_register_failures: subtest #1 failed to catch unsupported encryption' # Test #2: Send bad WID conn.send_message({ 'Action': "REGISTER", 'Data': { 'Workspace-ID': 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 'Password-Hash': pwhash, 'Device-ID': '11111111-1111-1111-1111-111111111111', 'Device-Key': 'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z' } }) response = conn.read_response(server_response) assert response['Code'] == 400 and response['Status'] == 'BAD REQUEST', \ 'test_register_failures: subtest #2 failed to catch a bad WID' conn.send_message({'Action': "QUIT"})
def test_devkey(): '''Tests the DEVKEY command''' dbconn = setup_test() dbdata = init_server(dbconn) conn = ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair # Most of the code which was originally written for this test is needed for other tests # because commands like PREREG require being logged in as the administrator. Both of these # functions still perform all the necessary tests that were originally done here. regcode_admin(dbdata, conn) login_admin(dbdata, conn) newdevpair = EncryptionPair( CryptoString(r'CURVE25519:F0HjK;dB8tr<*2UkaHP?e1-tWAohWJ?IP+oP@o@C'), CryptoString(r'CURVE25519:|Cs(42=7FEwCoKG|5fVPC%MR6gD)_{h}}?ah%cIn')) conn.send_message({ 'Action': 'DEVKEY', 'Data': { 'Device-ID': devid, 'Old-Key': dbdata['devpair'].get_public_key(), 'New-Key': newdevpair.get_public_key() } }) response = conn.read_response(None) assert response != 100 and response['Status'] == 'CONTINUE' and \ 'Challenge' in response['Data'] and 'New-Challenge' in response['Data'], \ "test_devkey(): server failed to return new and old key challenge" msg = {'Action': "DEVKEY", 'Data': {}} status = devpair.decrypt(response['Data']['Challenge']) assert not status.error( ), 'login_devkey(): failed to decrypt device challenge for old key' msg['Data']['Response'] = status['data'] status = newdevpair.decrypt(response['Data']['New-Challenge']) assert not status.error( ), 'login_devkey(): failed to decrypt device challenge for new key' msg['Data']['New-Response'] = status['data'] conn.send_message(msg) response = conn.read_response(None) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'Server challenge-response phase failed' conn.send_message({'Action': "QUIT"})
def test_resetpassword(): '''Tests the RESETPASSWORD command''' dbconn = setup_test() dbdata = init_server(dbconn) conn = ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) init_user(dbdata, conn) # Subtest #1: Reset user password, server generates passcode and expiration conn.send_message({ 'Action': "RESETPASSWORD", 'Data': { 'Workspace-ID': dbdata['user_wid'] } }) response = conn.read_response(None) assert response['Code'] == 200 and response['Status'] == "OK", \ "test_resetpassword(): subtest #1: server returned an error" assert 'Expires' in response['Data'] and 'Reset-Code' in response['Data'], \ "test_resetpassword(): subtest #1: server didn't return all required fields" # Subtest #2: Reset user password, admin generates passcode and expiration expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=45) conn.send_message({ 'Action': "RESETPASSWORD", 'Data': { 'Workspace-ID': dbdata['user_wid'], 'Reset-Code': 'barbarian-chivalry-dungeon', 'Expires': expiration.strftime(r'%Y%m%dT%H%M%SZ') } }) response = conn.read_response(None) assert response['Code'] == 200 and response['Status'] == "OK", \ "test_resetpassword(): subtest #2: server returned an error" assert 'Expires' in response['Data'] and 'Reset-Code' in response['Data'], \ "test_resetpassword(): subtest #2: server didn't return all required fields" conn.send_message({'Action': 'LOGOUT', 'Data': {}}) response = conn.read_response(None) assert response[ 'Code'] == 200, 'test_resetpassword(): failed to log admin out' # Subtest #3: Failed attempt to complete the password reset newpassword = Password('SomeOth3rPassw*rd') conn.send_message({ 'Action': "PASSCODE", 'Data': { 'Workspace-ID': dbdata['user_wid'], 'Reset-Code': 'wrong-reset-code', 'Password-Hash': newpassword.hashstring } }) response = conn.read_response(None) assert response['Code'] == 402 and response['Status'] == "AUTHENTICATION FAILURE", \ "test_resetpassword(): subtest #3: server failed to catch a bad password" # Subtest #4: Successfully complete the password reset conn.send_message({ 'Action': "PASSCODE", 'Data': { 'Workspace-ID': dbdata['user_wid'], 'Reset-Code': 'barbarian-chivalry-dungeon', 'Password-Hash': newpassword.hashstring } }) response = conn.read_response(None) assert response['Code'] == 200 and response['Status'] == "OK", \ "test_resetpassword(): subtest #4: server returned an error"
def test_prereg(): '''Tests the server's PREREG command with failure conditions''' dbconn = setup_test() dbdata = init_server(dbconn) uid = 'TestUserID' wid = '11111111-1111-1111-1111-111111111111' domain = 'acme.com' conn = ServerConnection() assert conn.connect('localhost', 2001), "Connection to server at localhost:2001 failed" # password is 'SandstoneAgendaTricycle' pwhash = '$argon2id$v=19$m=65536,t=2,p=1$ew5lqHA5z38za+257DmnTA$0LWVrI2r7XCq' \ 'dcCYkJLok65qussSyhN5TTZP+OTgzEI' devid = '22222222-2222-2222-2222-222222222222' devpair = EncryptionPair( CryptoString(r'CURVE25519:@X~msiMmBq0nsNnn0%~x{M|NU_{?<Wj)cYybdh&Z'), CryptoString(r'CURVE25519:W30{oJ?w~NBbj{F8Ag4~<bcWy6_uQ{i{X?NDq4^l')) dbdata['pwhash'] = pwhash dbdata['devid'] = devid dbdata['devpair'] = devpair regcode_admin(dbdata, conn) login_admin(dbdata, conn) # Subtest #1: Prereg with user ID and domain conn.send_message({ 'Action': "PREREG", 'Data': { 'User-ID': uid, 'Domain': domain } }) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_prereg: subtest #1 returned an error' assert response['Data']['User-ID'] == uid, \ 'test_prereg: wrong user ID in subtest #1' assert response['Data']['Domain'] == domain, \ 'test_prereg: wrong domain in subtest #1' assert validate_uuid( response['Data']['Workspace-ID']), 'Server returned a bad WID' assert len(response['Data']['Workspace-ID']) <= 128, \ 'Server returned a regcode longer than allowed' # REGCODE subtest setup regdata = response pwd = Password() status = pwd.Set('ShrivelCommuteGottenAgonizingElbowQuiver') assert not status.error(), 'test_prereg: Failed to set password' devid = '0e6406e3-1831-4352-9fbe-0de8faebf0f0' devkey = EncryptionPair() # Subtest #2: Regcode with User ID and domain conn.send_message({ 'Action': "REGCODE", 'Data': { 'User-ID': regdata['Data']['User-ID'], 'Domain': regdata['Data']['Domain'], 'Reg-Code': regdata['Data']['Reg-Code'], 'Password-Hash': pwd.hashstring, 'Device-ID': devid, 'Device-Key': devkey.get_public_key() } }) response = conn.read_response(server_response) assert response['Code'] == 201 and response['Status'] == 'REGISTERED', \ 'test_prereg: subtest #2 returned an error' # Subtest #3: Plain prereg conn.send_message({'Action': "PREREG", 'Data': {}}) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_prereg: subtest #2 returned an error' assert validate_uuid( response['Data']['Workspace-ID']), 'Server returned a bad WID' assert len(response['Data']['Workspace-ID']) <= 128, \ 'Server returned a regcode longer than allowed' # Subtest #4: Plain regcode regdata = response conn.send_message({ 'Action': "REGCODE", 'Data': { 'Workspace-ID': regdata['Data']['Workspace-ID'], 'Reg-Code': regdata['Data']['Reg-Code'], 'Password-Hash': pwd.hashstring, 'Device-ID': devid, 'Device-Key': devkey.get_public_key() } }) response = conn.read_response(server_response) assert response['Code'] == 201 and response['Status'] == 'REGISTERED', \ 'test_prereg: subtest #4 returned an error' # Subtest #5: duplicate user ID conn.send_message({ 'Action': "PREREG", 'Data': { 'User-ID': uid, 'Domain': domain } }) response = conn.read_response(server_response) assert response['Code'] == 408 and response['Status'] == 'RESOURCE EXISTS', \ 'test_prereg: subtest #3 failed to catch duplicate user' # Subtest #6: WID as user ID conn.send_message({'Action': "PREREG", 'Data': {'User-ID': wid}}) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_prereg: subtest #4 returned an error' assert response['Data']['Workspace-ID'] == wid, 'Server returned a bad WID' assert len(response['Data']['Workspace-ID']) <= 128, \ 'Server returned a regcode longer than allowed' # Subtest #7: Specify WID wid = '22222222-2222-2222-2222-222222222222' conn.send_message({'Action': "PREREG", 'Data': {'Workspace-ID': wid}}) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_prereg: subtest #4 returned an error' assert response['Data']['Workspace-ID'] == wid, 'Server returned a bad WID' assert len(response['Data']['Workspace-ID']) <= 128, \ 'Server returned a regcode longer than allowed' # Subtest #8: Specify User ID only. uid = 'TestUserID2' conn.send_message({'Action': "PREREG", 'Data': {'User-ID': uid}}) response = conn.read_response(server_response) assert response['Code'] == 200 and response['Status'] == 'OK', \ 'test_prereg: subtest #6 returned an error' assert response['Data'][ 'User-ID'] == uid, 'Server returned the wrong user ID' assert validate_uuid( response['Data']['Workspace-ID']), 'Server returned a bad WID' assert len(response['Data']['Workspace-ID']) <= 128, \ 'Server returned a regcode longer than allowed' conn.send_message({'Action': "QUIT"})