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_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 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_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_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"})