# -*- coding: utf-8 -*- """ tests.test_authentication Unit tests for conda-content-trust/car/authentication.py as well as integration tests for the signing.py + authentication.py. Run the tests this way: pytest tests/test_authentication.py """ # Python2 Compatibility from __future__ import absolute_import, division, print_function, unicode_literals # std libs import copy import os # dependencies import pytest import cryptography.exceptions # this codebase from conda_content_trust.authentication import * from conda_content_trust.metadata_construction import ( gen_keys, gen_and_write_keys, # for new-key tests # build_repodata_verification_metadata ) from conda_content_trust.common import ( PrivateKey, PublicKey, keyfiles_to_bytes, keyfiles_to_keys, SignatureError, MetadataVerificationError) from conda_content_trust.signing import wrap_as_signable, sign_signable # Some REGRESSION test data. REG__KEYPAIR_NAME = 'keytest_old' REG__PRIVATE_BYTES = b'\xc9\xc2\x06\r~\r\x93al&T\x84\x0bI\x83\xd0\x02!\xd8\xb6\xb6\x9c\x85\x01\x07\xdat\xb4!h\xf97' REG__PUBLIC_BYTES = b"\x01=\xddqIb\x86m\x12\xba[\xae'?\x14\xd4\x8c\x89\xcf\x07s\xde\xe2\xdb\xf6\xd4V\x1eR\x1c\x83\xf7" REG__PUBLIC_HEX_ROOT = 'c8bd83b3bfc991face417d97b9c0db011b5d256476b602b92fec92849fc2b36c' REG__MESSAGE_THAT_WAS_SIGNED = b'123456\x067890' # Signature is over REG__MESSAGE_THAT_WAS_SIGNED using key REG__PRIVATE_BYTES. REG__SIGNATURE = b'\xb6\xda\x14\xa1\xedU\x9e\xbf\x01\xb3\xa9\x18\xc9\xb8\xbd\xccFM@\x87\x99\xe8\x98\x84C\xe4}9;\xa4\xe5\xfd\xcf\xdaau\x04\xf5\xcc\xc0\xe7O\x0f\xf0F\x91\xd3\xb8"\x7fD\x1dO)*\x1f?\xd7&\xd6\xd3\x1f\r\x0e' REG__SIGNATURE_HEX = 'b6da14a1ed559ebf01b3a918c9b8bdcc464d408799e8988443e47d393ba4e5fdcfda617504f5ccc0e74f0ff04691d3b8227f441d4f292a1f3fd726d6d31f0d0e' # REG__HASHED_VAL = b'string to hash\n' # REG__HASH_HEX = '73aec9a93f4beb41a9bad14b9d1398f60e78ccefd97e4eb7d3cf26ba71dbe0ce' # #REG__HASH_BYTES = b's\xae\xc9\xa9?K\xebA\xa9\xba\xd1K\x9d\x13\x98\xf6\x0ex\xcc\xef\xd9~N\xb7\xd3\xcf&\xbaq\xdb\xe0\xce' # REG__REPODATA_HASHMAP = { # "noarch/current_repodata.json": "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "noarch/repodata.json": "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "noarch/repodata_from_packages.json": "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "osx-64/current_repodata.json": "8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2", # "osx-64/repodata.json": "8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2", # "osx-64/repodata_from_packages.json": "8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2" # } REG__TEST_TIMESTAMP = '2019-10-01T00:00:00Z' REG__TEST_EXPIRY_DATE = '2025-01-01T10:30:00Z' # REG__EXPECTED_UNSIGNED_REPODATA_VERIFY = { # 'type': 'repodata_verify', 'timestamp': REG__TEST_TIMESTAMP, # 'metadata_spec_version': '0.0.5', 'expiration': REG__TEST_EXPIRY_DATE, # 'secured_files': { # 'noarch/current_repodata.json': '908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe', # 'noarch/repodata.json': '908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe', # 'noarch/repodata_from_packages.json': '908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe', # 'osx-64/current_repodata.json': '8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2', # 'osx-64/repodata.json': '8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2', # 'osx-64/repodata_from_packages.json': '8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2'} # } # REG__EXPECTED_REGSIGNED_REPODATA_VERIFY = { # # Re-sign this if its data changes: it's signed! # 'type': 'repodata_verify', 'timestamp': '2019-10-01T00:00:00Z', # 'metadata_spec_version': '0.0.5', 'expiration': '2025-01-01T10:30:00Z', # 'secured_files': { # 'noarch/current_repodata.json': '908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe', # 'noarch/repodata.json': '908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe', # 'noarch/repodata_from_packages.json': '908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe', # 'osx-64/current_repodata.json': '8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2', # 'osx-64/repodata.json': '8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2', # 'osx-64/repodata_from_packages.json': '8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2'} # } TEST_ROOT_MD_V1 = { "signatures": { "c8bd83b3bfc991face417d97b9c0db011b5d256476b602b92fec92849fc2b36c": { "other_headers": "04001608001d162104917adb684e2e9fb5ed4e59909ddd19a1268b62d005025f96ff1a", "signature": "9327c2c4907c964741924420c4c35eb01805c15ec2fe5cad17bc98c0c3daf03006fcafb332eaa543a1ed212fac05f227662d8617970afc6c919ee4b78bacb004" } }, "signed": { "delegations": { "key_mgr": { "pubkeys": [ "013ddd714962866d12ba5bae273f14d48c89cf0773dee2dbf6d4561e521c83f7" ], "threshold": 1 }, "root": { "pubkeys": [ "c8bd83b3bfc991face417d97b9c0db011b5d256476b602b92fec92849fc2b36c" ], "threshold": 1 } }, "expiration": "2021-10-26T16:53:46Z", "metadata_spec_version": "0.6.0", "timestamp": "2020-10-26T16:53:46Z", "type": "root", "version": 1 } } TEST_ROOT_MD_V2 = { "signatures": { "a59cea0987ee9046d68d2d011e919eb9278e3f478cca77f5204d65191ff8d7a5": { "other_headers": "04001608001d1621040a14b126c986f276831c7b04134f35b47db4364305025f96ff1b", "signature": "d406839499630a75350ba6f6c009aae173f15dd8c9be069c9b535ff77b6d624d6092487fe18e2c4f5c13b252a3ebe3f89ab15f4c52c66db752f8cbbfc6d96609" }, "c8bd83b3bfc991face417d97b9c0db011b5d256476b602b92fec92849fc2b36c": { "other_headers": "04001608001d162104917adb684e2e9fb5ed4e59909ddd19a1268b62d005025f96ff1b", "signature": "f4c13d4456028778026639fcdc63ec7d6005e1e88f2dcfaf87afa3b89ce6a1ec8938af83fdc9d3d7045d0ebd648654c6af027daaf7164e014a8a53f373e9b906" } }, "signed": { "delegations": { "key_mgr": { "pubkeys": [ "013ddd714962866d12ba5bae273f14d48c89cf0773dee2dbf6d4561e521c83f7" ], "threshold": 1 }, "root": { "pubkeys": [ "c8bd83b3bfc991face417d97b9c0db011b5d256476b602b92fec92849fc2b36c" ], "threshold": 1 } }, "expiration": "2021-10-26T16:53:46Z", "metadata_spec_version": "0.6.0", "timestamp": "2020-10-26T16:53:46Z", "type": "root", "version": 2 } } # NOTE to dev: # test_authenticate was originally a long sequence of tests in a single # function. I pulled out most of it, and what remains is has to be compared # to the new tests to see if it's still useful. def test_wrap_sign_verify_signable(): # Make a new keypair. Returns keys and writes keys to disk. # Then load it from disk and compare that to the return value. Exercise # some of the functions redundantly. generated_private, generated_public = gen_and_write_keys('keytest_new') loaded_new_private_bytes, loaded_new_public_bytes = keyfiles_to_bytes( 'keytest_new') loaded_new_private, loaded_new_public = keyfiles_to_keys('keytest_new') old_private = PrivateKey.from_bytes(REG__PRIVATE_BYTES) old_public = PublicKey.from_bytes(REG__PUBLIC_BYTES) assert generated_private.is_equivalent_to(loaded_new_private) assert generated_public.is_equivalent_to(loaded_new_public) assert loaded_new_private.is_equivalent_to( PrivateKey.from_bytes(loaded_new_private_bytes)) assert loaded_new_public.is_equivalent_to( PublicKey.from_bytes(loaded_new_public_bytes)) # Clean up a bit for the next tests. new_private = loaded_new_private new_public = loaded_new_public del ( loaded_new_public, loaded_new_private, generated_private, generated_public, loaded_new_private_bytes, loaded_new_public_bytes) # Test wrapping, signing signables, and verifying signables. d = {'foo': 'bar', '1': 2} d_modified = {'foo': 'DOOM', '1': 2} signable_d = wrap_as_signable(d) assert is_a_signable(signable_d) sign_signable(signable_d, old_private) assert is_a_signable(signable_d) verify_signable( signable=signable_d, authorized_pub_keys=[old_public.to_hex()], threshold=1) # Expect failure this time due to bad format. try: verify_signable( signable=signable_d['signed'], authorized_pub_keys=[old_public.to_hex()], threshold=1) except TypeError: pass else: assert False, 'Failed to raise expected exception.' # Expect failure this time due to non-matching signature. try: modified_signable_d = copy.deepcopy(signable_d) modified_signable_d['signed'] = d_modified verify_signable( signable=modified_signable_d, authorized_pub_keys=[old_public.to_hex()], threshold=1) except SignatureError: pass else: assert False, 'Failed to raise expected exception.' # Clean up a bit. for fname in [ 'keytest_new.pub', 'keytest_new.pri', 'keytest_old.pri', 'keytest_old.pub']: if os.path.exists(fname): os.remove(fname) # def test_repodata_verify_funcs(): # # Test construction and verification of signed repodata_verify, including # # wrapping, signing the signable, and verifying the signables with a real # # example. # repodata_hashmap = { # "noarch/current_repodata.json": # "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "noarch/repodata.json": # "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "noarch/repodata_from_packages.json": # "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "osx-64/current_repodata.json": # "8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2", # "osx-64/repodata.json": # "8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2", # "osx-64/repodata_from_packages.json": # "8120fb07a6a8a280ffa2b89fb2fbb89484823d0b0357ff0cfa7c333352b2faa2"} # rd_v_md = build_repodata_verification_metadata(repodata_hashmap) # signable_rd_v_md = wrap_as_signable(rd_v_md) # assert is_a_signable(signable_rd_v_md) # sign_signable(signable_rd_v_md, old_private) # assert is_a_signable(signable_rd_v_md) # verify_signable( # signable=signable_rd_v_md, # authorized_pub_keys=[old_public.to_hex()], # threshold=1) # # Expect failure this time due to non-matching signature. # try: # modified_signable_rd_v_md = copy.deepcopy(signable_rd_v_md) # modified_signable_rd_v_md[ # 'signed']['secured_files']['noarch/current_repodata.json' # ] = modified_signable_rd_v_md['signed']['secured_files'][ # 'noarch/current_repodata.json'][:-1] + 'f' # TODO: Generalize test condition. (Also, un-ugly.) # verify_signable( # signable=modified_signable_rd_v_md, # authorized_pub_keys=[old_public.to_hex()], # threshold=1) # except SignatureError: # pass # else: # assert False, 'Failed to raise expected exception.' # # DEBUG: 💥💥💥💥 Dump the various bits and pieces for debugging. # # Remove this. # with open('_test_output__repodata_hashmap.json', 'wb') as fobj: # fobj.write(canonserialize(repodata_hashmap)) # with open('_test_output__repodata_verify.json', 'wb') as fobj: # fobj.write(canonserialize(signable_rd_v_md)) # # Additional regression test for a file produced by the indexer. # # This should come up as good. # verify_signable( # signable={ # "signatures": { # "013ddd714962866d12ba5bae273f14d48c89cf0773dee2dbf6d4561e521c83f7": "740a426113cb83a62e58eb41fcd0b5f36691b0b18bffbe7eb3da30b5baf83f6c703a0fdb584599702470c74f55572a27cf9de250fc3afb723c43fef4dc778401" # }, # "signed": { # "expiration": "2019-10-28T15:36:32Z", # "metadata_spec_version": "0.0.4", # "secured_files": { # "noarch/current_repodata.json": "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "noarch/repodata.json": "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "noarch/repodata_from_packages.json": "908724926552827ab58dfc0bccba92426cec9f1f483883da3ff0d8664e18c0fe", # "osx-64/current_repodata.json": "fc9268ea2b4add37e090b7f2b2c88b95c513cab445fb099e8631d8815a384ae4", # "osx-64/repodata.json": "fc9268ea2b4add37e090b7f2b2c88b95c513cab445fb099e8631d8815a384ae4", # "osx-64/repodata_from_packages.json": "fc9268ea2b4add37e090b7f2b2c88b95c513cab445fb099e8631d8815a384ae4" # }, # "timestamp": "2019-09-27T15:36:32Z", # "type": "repodata_verify" # } # }, # authorized_pub_keys=[old_public.to_hex()], # threshold=1) def test_sign_and_verify(): """ Tests functions: - sign - verify """ # Generate new keys and construct key objects for old keys. new_private, new_public = gen_keys() old_private = PrivateKey.from_bytes(REG__PRIVATE_BYTES) old_public = PublicKey.from_bytes(REG__PUBLIC_BYTES) old_sig = old_private.sign(REG__MESSAGE_THAT_WAS_SIGNED) new_sig = new_private.sign(REG__MESSAGE_THAT_WAS_SIGNED) new_sig2 = new_private.sign(REG__MESSAGE_THAT_WAS_SIGNED) assert new_sig == new_sig2 # deterministic (obv not a thorough test) assert old_sig == REG__SIGNATURE # regression # Test verify() # Good signatures first. old_public.verify(REG__SIGNATURE, REG__MESSAGE_THAT_WAS_SIGNED) old_public.verify(old_sig, REG__MESSAGE_THAT_WAS_SIGNED) new_public.verify(new_sig, REG__MESSAGE_THAT_WAS_SIGNED) # Use wrong public key. wrong_pubkey_obj = PublicKey.from_hex( '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef') with pytest.raises(cryptography.exceptions.InvalidSignature): wrong_pubkey_obj.verify(REG__SIGNATURE, REG__MESSAGE_THAT_WAS_SIGNED) # Use bad data. with pytest.raises(cryptography.exceptions.InvalidSignature): old_public.verify(new_sig, REG__MESSAGE_THAT_WAS_SIGNED + b'a') with pytest.raises(cryptography.exceptions.InvalidSignature): new_public.verify(new_sig, REG__MESSAGE_THAT_WAS_SIGNED[0:-1]) def test_verify_signature(): verify_signature( REG__SIGNATURE_HEX, PublicKey.from_bytes(REG__PUBLIC_BYTES), REG__MESSAGE_THAT_WAS_SIGNED) # invalid signatures with pytest.raises(cryptography.exceptions.InvalidSignature): verify_signature( REG__SIGNATURE_HEX[:-6] + 'ffffff', # wrong value PublicKey.from_bytes(REG__PUBLIC_BYTES), REG__MESSAGE_THAT_WAS_SIGNED) with pytest.raises(TypeError): verify_signature( REG__SIGNATURE_HEX[:-1], # wrong length PublicKey.from_bytes(REG__PUBLIC_BYTES), REG__MESSAGE_THAT_WAS_SIGNED) with pytest.raises(TypeError): verify_signature( REG__SIGNATURE, # wrong type PublicKey.from_bytes(REG__PUBLIC_BYTES), REG__MESSAGE_THAT_WAS_SIGNED) # other bad args with pytest.raises(cryptography.exceptions.InvalidSignature): verify_signature( REG__SIGNATURE_HEX, # wrong type PublicKey.from_bytes(REG__PUBLIC_BYTES), REG__MESSAGE_THAT_WAS_SIGNED + b'\xc9') # altered message with pytest.raises(cryptography.exceptions.InvalidSignature): verify_signature( REG__SIGNATURE_HEX, PublicKey.from_bytes(REG__PUBLIC_BYTES[:-4] + b'0000'), # wrong key REG__MESSAGE_THAT_WAS_SIGNED) with pytest.raises(TypeError): verify_signature( REG__SIGNATURE_HEX, REG__PUBLIC_BYTES, # wrong type REG__MESSAGE_THAT_WAS_SIGNED) with pytest.raises(TypeError): verify_signature( REG__SIGNATURE_HEX, PublicKey.from_bytes(REG__PUBLIC_BYTES), {'this is not bytes': 1}) # wrong type # verify_root is also tested in test_root.py (but test_root.py expects GPG) def test_verify_root(): """ Tests conda_content_trust.authentication.verify_root """ # Root chaining: normal test verify_root(TEST_ROOT_MD_V1, TEST_ROOT_MD_V2) # Now we tinker a bit to break stuff. root_v2_edited = copy.deepcopy(TEST_ROOT_MD_V2) # Can't verify root v10 using root v1 (chaining) with pytest.raises(MetadataVerificationError): root_v2_edited['signed']['version'] = 10 verify_root(TEST_ROOT_MD_V1, root_v2_edited) # Reset. root_v2_edited['signed']['version'] = TEST_ROOT_MD_V2['signed']['version'] # Bad signature, same keys, same contents # with pytest.raises(cryptography.exceptions.InvalidSignature): with pytest.raises(SignatureError): sig = root_v2_edited['signatures'][REG__PUBLIC_HEX_ROOT]['signature'] sig = sig[:-6] + 'ffffff' root_v2_edited['signatures'][REG__PUBLIC_HEX_ROOT]['signature'] = sig verify_root(TEST_ROOT_MD_V1, root_v2_edited) # Reset. root_v2_edited['signatures'] = copy.deepcopy(TEST_ROOT_MD_V2['signatures']) # Not enough signatures from authorized keys: # Have one of the signatures claim to be from the wrong key. with pytest.raises(SignatureError): root_v2_edited['signatures'][REG__PUBLIC_HEX_ROOT[:-6] + 'ffffff'] \ = root_v2_edited['signatures'][REG__PUBLIC_HEX_ROOT] del root_v2_edited['signatures'][REG__PUBLIC_HEX_ROOT] verify_root(TEST_ROOT_MD_V1, root_v2_edited) # Reset. root_v2_edited['signatures'] = copy.deepcopy(TEST_ROOT_MD_V2['signatures']) # Not enough signatures from authorized keys: # Change the trusted metadata such that we expect sigs from 3 distinct # authorized keys (and still provide only 2). with pytest.raises(SignatureError): root_v1_edited = copy.deepcopy(TEST_ROOT_MD_V1) root_v1_edited['signed']['delegations']['root']['threshold'] += 1 verify_root(root_v1_edited, TEST_ROOT_MD_V2) # Reset. root_v1_edited['signed']['delegations']['root']['threshold'] -= 1 # def test_verify_delegation(): # """ # Tests conda_content_trust.authentication.verify_delegation # """ # raise NotImplementedError('verify_delegation requires unit tests.')