def test_key_storage_basic_get_set_api(tmp_path): with pytest.raises(TypeError, match="Can't instantiate abstract class"): KeyStorageBase() dummy_key_storage = DummyKeyStorage() filesystem_key_storage = FilesystemKeyStorage(keys_dir=tmp_path) filesystem_key_storage_test_locals = None for key_storage in (dummy_key_storage, filesystem_key_storage): res = check_key_storage_basic_get_set_api(key_storage=key_storage) if isinstance(key_storage, FilesystemKeyStorage): filesystem_key_storage_test_locals = res # Specific tests for filesystem storage keychain_uid = filesystem_key_storage_test_locals["keychain_uid"] is_public = random.choice([True, False]) basename = filesystem_key_storage._get_filename(keychain_uid, key_type="abxz", is_public=is_public) with open(os.path.join(str(tmp_path), basename), "rb") as f: key_data = f.read() assert key_data == (b"public_data" if is_public else b"private_data" ) # IMPORTANT no exchange of keys in files!
def test_get_free_keys_generator_worker(): generate_keys_count = 0 key_storage = DummyKeyStorage() def key_generation_func(key_type, serialize): nonlocal generate_keys_count generate_keys_count += 1 time.sleep(0.01) return dict(private_key="someprivatekey", public_key="somepublickey") worker = get_free_keys_generator_worker( key_storage=key_storage, max_free_keys_per_type=30, sleep_on_overflow_s=0.5, key_generation_func=key_generation_func, ) try: worker.start() time.sleep(0.5) worker.stop() worker.join() assert ( 10 < generate_keys_count < 50 ), generate_keys_count # Not enough time to generate all worker.start() time.sleep(3) worker.stop() worker.join() assert ( generate_keys_count == 120 # 4 key types for now ), generate_keys_count # All keys had the time to be generated start = time.time() worker.start() worker.stop() worker.join() end = time.time() assert (end - start) > 0.4 # sleep-on-overflow occurred finally: if worker.is_running: worker.stop()
def test_generate_free_keypair_for_least_provisioned_key_type(): generated_keys_count = 0 def key_generation_func(key_type, serialize): nonlocal generated_keys_count generated_keys_count += 1 return dict(private_key=b"someprivatekey", public_key=b"somepublickey") # Check the fallback on "all types of keys" for key_types parameter key_storage = DummyKeyStorage() for _ in range(4): res = generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=10, key_generation_func=key_generation_func, # no key_types parameter provided ) assert res assert key_storage.get_free_keypairs_count("DSA_DSS") == 1 assert key_storage.get_free_keypairs_count("ECC_DSS") == 1 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 1 assert key_storage.get_free_keypairs_count("RSA_PSS") == 1 assert generated_keys_count == 4 # Now test with a restricted set of key types key_storage = DummyKeyStorage() restricted_key_types = ["DSA_DSS", "ECC_DSS", "RSA_OAEP"] generated_keys_count = 0 for _ in range(7): res = generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=10, key_generation_func=key_generation_func, key_types=restricted_key_types, ) assert res assert key_storage.get_free_keypairs_count("DSA_DSS") == 3 assert key_storage.get_free_keypairs_count("ECC_DSS") == 2 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 2 assert generated_keys_count == 7 for _ in range(23): res = generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=10, key_generation_func=key_generation_func, key_types=restricted_key_types, ) assert res assert key_storage.get_free_keypairs_count("DSA_DSS") == 10 assert key_storage.get_free_keypairs_count("ECC_DSS") == 10 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 10 assert generated_keys_count == 30 res = generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=10, key_generation_func=key_generation_func, key_types=restricted_key_types, ) assert not res assert generated_keys_count == 30 # Unchanged for _ in range(7): generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=15, key_generation_func=key_generation_func, key_types=["RSA_OAEP", "DSA_DSS"], ) assert key_storage.get_free_keypairs_count( "DSA_DSS") == 14 # First in sorting order assert key_storage.get_free_keypairs_count("ECC_DSS") == 10 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 13 assert generated_keys_count == 37 res = generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=20, key_generation_func=key_generation_func, key_types=restricted_key_types, ) assert res assert key_storage.get_free_keypairs_count("DSA_DSS") == 14 assert key_storage.get_free_keypairs_count("ECC_DSS") == 11 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 13 assert generated_keys_count == 38 res = generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=5, key_generation_func=key_generation_func, key_types=restricted_key_types, ) assert not res assert generated_keys_count == 38
def test_escrow_api_workflow(): key_storage = DummyKeyStorage() escrow_api = EscrowApi(key_storage=key_storage) keychain_uid = generate_uuid0() keychain_uid_other = generate_uuid0() keychain_uid_unexisting = generate_uuid0() secret = get_random_bytes(127) secret_too_big = get_random_bytes(140) for _ in range(2): generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=10, key_types=["RSA_OAEP", "DSA_DSS"]) assert key_storage.get_free_keypairs_count("DSA_DSS") == 1 assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 1 assert key_storage.get_free_keypairs_count( "RSA_PSS") == 0 # Different from other RSA keys # Keypair is well auto-created by get_public_key(), by default public_key_rsa_oaep_pem = escrow_api.fetch_public_key( keychain_uid=keychain_uid, key_type="RSA_OAEP") with pytest.raises(KeyDoesNotExist, match="not found"): # Key NOT autogenerated escrow_api.fetch_public_key(keychain_uid=generate_uuid0(), key_type="RSA_OAEP", must_exist=True) _public_key_rsa_oaep_pem2 = escrow_api.fetch_public_key( keychain_uid=keychain_uid, key_type="RSA_OAEP") assert _public_key_rsa_oaep_pem2 == public_key_rsa_oaep_pem # Same KEYS! _public_key_rsa_pss_pem = escrow_api.fetch_public_key( keychain_uid=keychain_uid, key_type="RSA_PSS") assert _public_key_rsa_pss_pem != public_key_rsa_oaep_pem # Different KEYS! public_key_rsa_oaep = load_asymmetric_key_from_pem_bytestring( key_pem=public_key_rsa_oaep_pem, key_type="RSA_OAEP") assert key_storage.get_free_keypairs_count("DSA_DSS") == 1 assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 0 # Taken assert key_storage.get_free_keypairs_count("RSA_PSS") == 0 signature = escrow_api.get_message_signature(keychain_uid=keychain_uid, message=secret, signature_algo="DSA_DSS") with pytest.raises(ValueError, match="too big"): escrow_api.get_message_signature(keychain_uid=keychain_uid, message=secret_too_big, signature_algo="DSA_DSS") assert key_storage.get_free_keypairs_count("DSA_DSS") == 0 # Taken assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 0 assert key_storage.get_free_keypairs_count("RSA_PSS") == 0 public_key_dsa_pem = escrow_api.fetch_public_key(keychain_uid=keychain_uid, key_type="DSA_DSS") public_key_dsa = load_asymmetric_key_from_pem_bytestring( key_pem=public_key_dsa_pem, key_type="DSA_DSS") verify_message_signature(message=secret, signature=signature, key=public_key_dsa, signature_algo="DSA_DSS") signature["digest"] += b"xyz" with pytest.raises(SignatureVerificationError, match="Failed.*verification"): verify_message_signature(message=secret, signature=signature, key=public_key_dsa, signature_algo="DSA_DSS") # Keypair is well auto-created by get_message_signature(), even when no more free keys signature = escrow_api.get_message_signature( keychain_uid=keychain_uid_other, message=secret, signature_algo="RSA_PSS") assert signature # Keypair well autocreated by get_public_key(), even when no more free keys public_key_pem = escrow_api.fetch_public_key( keychain_uid=keychain_uid_other, key_type="DSA_DSS") assert public_key_pem cipherdict = _encrypt_via_rsa_oaep(plaintext=secret, key=public_key_rsa_oaep) # Works even without decryption authorization request, by default: decrypted = escrow_api.decrypt_with_private_key(keychain_uid=keychain_uid, encryption_algo="RSA_OAEP", cipherdict=cipherdict) assert decrypted == secret # NO auto-creation of keypair in decrypt_with_private_key() with pytest.raises(KeyDoesNotExist, match="not found"): escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid_unexisting, encryption_algo="RSA_OAEP", cipherdict=cipherdict) wrong_cipherdict = copy.deepcopy(cipherdict) wrong_cipherdict["digest_list"].append(b"aaabbbccc") with pytest.raises(ValueError, match="Ciphertext with incorrect length"): escrow_api.decrypt_with_private_key(keychain_uid=keychain_uid, encryption_algo="RSA_OAEP", cipherdict=wrong_cipherdict) with pytest.raises(ValueError, match="empty"): escrow_api.request_decryption_authorization( keypair_identifiers=[], request_message="I need this decryption!") # Authorization always granted for now, in dummy implementation result = escrow_api.request_decryption_authorization( keypair_identifiers=[ dict(keychain_uid=keychain_uid, key_type="RSA_OAEP") ], request_message="I need this decryption!", ) assert "accepted" in result["response_message"] assert not result["has_errors"] assert result["keypair_statuses"]["accepted"] # TEST PASSPHRASE PROTECTIONS keychain_uid_passphrased = generate_uuid0() good_passphrase = "good_passphrase" keypair_cipher_passphrased = generate_asymmetric_keypair_for_storage( key_type="RSA_OAEP", key_storage=key_storage, keychain_uid=keychain_uid_passphrased, passphrase=good_passphrase) result = escrow_api.request_decryption_authorization( keypair_identifiers=[ dict(keychain_uid=keychain_uid_passphrased, key_type="RSA_OAEP") ], request_message="I need this decryption too!", ) assert "denied" in result["response_message"] assert result["has_errors"] assert result["keypair_statuses"]["missing_passphrase"] result = escrow_api.request_decryption_authorization( keypair_identifiers=[ dict(keychain_uid=keychain_uid_passphrased, key_type="RSA_OAEP") ], request_message="I need this decryption too!", passphrases=["aaa"], ) assert "denied" in result["response_message"] assert result["has_errors"] assert result["keypair_statuses"]["missing_passphrase"] result = escrow_api.request_decryption_authorization( keypair_identifiers=[ dict(keychain_uid=keychain_uid_passphrased, key_type="RSA_OAEP") ], request_message="I need this decryption too!", passphrases=["dsd", good_passphrase], ) assert "accepted" in result["response_message"] assert not result["has_errors"] assert result["keypair_statuses"]["accepted"] public_key_rsa_oaep2 = load_asymmetric_key_from_pem_bytestring( key_pem=keypair_cipher_passphrased["public_key"], key_type="RSA_OAEP") cipherdict = _encrypt_via_rsa_oaep(plaintext=secret, key=public_key_rsa_oaep2) with pytest.raises(DecryptionError, match="not decrypt"): escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid_passphrased, encryption_algo="RSA_OAEP", cipherdict=cipherdict) with pytest.raises(DecryptionError, match="not decrypt"): escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid_passphrased, encryption_algo="RSA_OAEP", cipherdict=cipherdict, passphrases=["something"], ) decrypted = escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid_passphrased, encryption_algo="RSA_OAEP", cipherdict=cipherdict, passphrases=[good_passphrase], ) assert decrypted == secret assert key_storage.get_free_keypairs_count("DSA_DSS") == 0 assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 0 assert key_storage.get_free_keypairs_count("RSA_PSS") == 0
def test_readonly_escrow_api_behaviour(): key_storage = DummyKeyStorage() escrow_api = ReadonlyEscrowApi(key_storage=key_storage) keychain_uid = generate_uuid0() key_type_cipher = "RSA_OAEP" key_type_signature = "RSA_PSS" secret = get_random_bytes(127) for must_exist in (True, False): with pytest.raises(KeyDoesNotExist, match="not found"): escrow_api.fetch_public_key(keychain_uid=keychain_uid, key_type=key_type_signature, must_exist=must_exist) with pytest.raises(KeyDoesNotExist, match="not found"): escrow_api.get_message_signature(keychain_uid=keychain_uid, message=secret, signature_algo="RSA_PSS") # Always accepted for now, dummy implementation result = escrow_api.request_decryption_authorization( keypair_identifiers=[ dict(keychain_uid=keychain_uid, key_type=key_type_cipher) ], request_message="I need this decryption!", ) assert "denied" in result["response_message"] assert result["has_errors"] assert result["keypair_statuses"]["missing_private_key"] # Still no auto-creation of keypair in decrypt_with_private_key() with pytest.raises(KeyDoesNotExist, match="not found"): escrow_api.decrypt_with_private_key(keychain_uid=keychain_uid, encryption_algo=key_type_cipher, cipherdict={}) # Now we generate wanted keys # keypair_cipher = generate_asymmetric_keypair_for_storage( key_type=key_type_cipher, key_storage=key_storage, keychain_uid=keychain_uid) keypair_signature = generate_asymmetric_keypair_for_storage( key_type=key_type_signature, key_storage=key_storage, keychain_uid=keychain_uid) public_key2 = escrow_api.fetch_public_key(keychain_uid=keychain_uid, key_type=key_type_signature, must_exist=must_exist) assert public_key2 == keypair_signature["public_key"] signature = escrow_api.get_message_signature(keychain_uid=keychain_uid, message=secret, signature_algo="RSA_PSS") assert signature and isinstance(signature, dict) private_key_cipher = load_asymmetric_key_from_pem_bytestring( key_pem=keypair_cipher["private_key"], key_type=key_type_cipher) cipherdict = _encrypt_via_rsa_oaep(plaintext=secret, key=private_key_cipher) decrypted = escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid, encryption_algo=key_type_cipher, cipherdict=cipherdict) assert decrypted == secret
load_from_json_file, generate_uuid0, hash_message, synchronized, catch_and_log_exception, ) logger = logging.getLogger(__name__) CONTAINER_FORMAT = "WA_0.1a" CONTAINER_SUFFIX = ".crypt" MEDIUM_SUFFIX = ( ".medium" ) # To construct decrypted filename when no previous extensions are found in container filename DUMMY_KEY_STORAGE = DummyKeyStorage() # Fallback storage with in-memory keys class ContainerBase: """ BEWARE - this class-based design is provisional and might change a lot. """ def __init__(self, local_key_storage=None): if not local_key_storage: logger.warning( "No local key storage provided for %s instance, falling back to DummyKeyStorage()", self.__class__.__name__, ) local_key_storage = DUMMY_KEY_STORAGE assert isinstance(local_key_storage, KeyStorageBase), local_key_storage self._local_escrow_api = LocalEscrowApi(key_storage=local_key_storage)
def test_escrow_api_workflow(): key_storage = DummyKeyStorage() escrow_api = EscrowApi(key_storage=key_storage) keychain_uid = generate_uuid0() keychain_uid_other = generate_uuid0() keychain_uid_unexisting = generate_uuid0() secret = get_random_bytes(127) secret_too_big = get_random_bytes(140) for _ in range(2): generate_free_keypair_for_least_provisioned_key_type( key_storage=key_storage, max_free_keys_per_type=10, key_types=["RSA_OAEP", "DSA_DSS"], ) assert key_storage.get_free_keypairs_count("DSA_DSS") == 1 assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 1 assert ( key_storage.get_free_keypairs_count("RSA_PSS") == 0 ) # Different from other RSA keys # Keypair is well auto-created by get_public_key() public_key_rsa_oaep_pem = escrow_api.get_public_key( keychain_uid=keychain_uid, key_type="RSA_OAEP" ) _public_key_rsa_oaep_pem2 = escrow_api.get_public_key( keychain_uid=keychain_uid, key_type="RSA_OAEP" ) assert _public_key_rsa_oaep_pem2 == public_key_rsa_oaep_pem # Same KEYS! _public_key_rsa_pss_pem = escrow_api.get_public_key( keychain_uid=keychain_uid, key_type="RSA_PSS" ) assert _public_key_rsa_pss_pem != public_key_rsa_oaep_pem # Different KEYS! public_key_rsa_oaep = load_asymmetric_key_from_pem_bytestring( key_pem=public_key_rsa_oaep_pem, key_type="RSA_OAEP" ) assert key_storage.get_free_keypairs_count("DSA_DSS") == 1 assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 0 # Taken assert key_storage.get_free_keypairs_count("RSA_PSS") == 0 signature = escrow_api.get_message_signature( keychain_uid=keychain_uid, message=secret, signature_algo="DSA_DSS" ) with pytest.raises(ValueError, match="too big"): escrow_api.get_message_signature( keychain_uid=keychain_uid, message=secret_too_big, signature_algo="DSA_DSS" ) assert key_storage.get_free_keypairs_count("DSA_DSS") == 0 # Taken assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 0 assert key_storage.get_free_keypairs_count("RSA_PSS") == 0 public_key_dsa_pem = escrow_api.get_public_key( keychain_uid=keychain_uid, key_type="DSA_DSS" ) public_key_dsa = load_asymmetric_key_from_pem_bytestring( key_pem=public_key_dsa_pem, key_type="DSA_DSS" ) verify_message_signature( message=secret, signature=signature, key=public_key_dsa, signature_algo="DSA_DSS", ) signature["digest"] += b"xyz" with pytest.raises(ValueError, match="not authentic"): verify_message_signature( message=secret, signature=signature, key=public_key_dsa, signature_algo="DSA_DSS", ) # Keypair is well auto-created by get_message_signature(), even when no more free keys signature = escrow_api.get_message_signature( keychain_uid=keychain_uid_other, message=secret, signature_algo="RSA_PSS" ) assert signature # Keypair well autocreated by get_public_key(), even when no more free keys public_key_pem = escrow_api.get_public_key( keychain_uid=keychain_uid_other, key_type="DSA_DSS" ) assert public_key_pem cipherdict = _encrypt_via_rsa_oaep(plaintext=secret, key=public_key_rsa_oaep) # Works even without decryption authorization request, by default: decrypted = escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid, encryption_algo="RSA_OAEP", cipherdict=cipherdict ) # NO auto-creation of keypair in decrypt_with_private_key() with pytest.raises(ValueError, match="Unexisting"): escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid_unexisting, encryption_algo="RSA_OAEP", cipherdict=cipherdict, ) cipherdict["digest_list"].append(b"aaabbbccc") with pytest.raises(ValueError, match="Ciphertext with incorrect length"): escrow_api.decrypt_with_private_key( keychain_uid=keychain_uid, encryption_algo="RSA_OAEP", cipherdict=cipherdict ) assert decrypted == secret result = escrow_api.request_decryption_authorization( keypair_identifiers=[(keychain_uid, "RSA_OAEP")], request_message="I need this decryption!", ) assert result["response_message"] with pytest.raises(ValueError, match="empty"): escrow_api.request_decryption_authorization( keypair_identifiers=[], request_message="I need this decryption!" ) assert key_storage.get_free_keypairs_count("DSA_DSS") == 0 assert key_storage.get_free_keypairs_count("ECC_DSS") == 0 assert key_storage.get_free_keypairs_count("RSA_OAEP") == 0 assert key_storage.get_free_keypairs_count("RSA_PSS") == 0